Merge "Null-terminate passwords by default before hashing them"

This commit is contained in:
David Pursehouse
2020-03-11 23:48:14 +00:00
committed by Gerrit Code Review
2 changed files with 41 additions and 8 deletions

View File

@@ -31,6 +31,7 @@ import org.bouncycastle.util.Arrays;
*/
public class HashedPassword {
private static final String ALGORITHM_PREFIX = "bcrypt:";
private static final String ALGORITHM_PREFIX_0 = "bcrypt0:";
private static final SecureRandom secureRandom = new SecureRandom();
private static final BaseEncoding codec = BaseEncoding.base64();
@@ -52,7 +53,7 @@ public class HashedPassword {
* @throws DecoderException if input is malformed.
*/
public static HashedPassword decode(String encoded) throws DecoderException {
if (!encoded.startsWith(ALGORITHM_PREFIX)) {
if (!encoded.startsWith(ALGORITHM_PREFIX) && !encoded.startsWith(ALGORITHM_PREFIX_0)) {
throw new DecoderException("unrecognized algorithm");
}
@@ -74,19 +75,24 @@ public class HashedPassword {
if (salt.length != 16) {
throw new DecoderException("salt should be 16 bytes, got " + salt.length);
}
return new HashedPassword(codec.decode(fields.get(3)), salt, cost);
return new HashedPassword(
codec.decode(fields.get(3)), salt, cost, encoded.startsWith(ALGORITHM_PREFIX_0));
}
private static byte[] hashPassword(String password, byte[] salt, int cost) {
private static byte[] hashPassword(
String password, byte[] salt, int cost, boolean nullTerminate) {
byte[] pwBytes = password.getBytes(StandardCharsets.UTF_8);
if (nullTerminate && !password.endsWith("\0")) {
pwBytes = Arrays.append(pwBytes, (byte) 0);
}
return BCrypt.generate(pwBytes, salt, cost);
}
public static HashedPassword fromPassword(String password) {
byte[] salt = newSalt();
return new HashedPassword(hashPassword(password, salt, DEFAULT_COST), salt, DEFAULT_COST);
return new HashedPassword(
hashPassword(password, salt, DEFAULT_COST, true), salt, DEFAULT_COST, true);
}
private static byte[] newSalt() {
@@ -98,11 +104,15 @@ public class HashedPassword {
private byte[] salt;
private byte[] hashed;
private int cost;
// Raw bcrypt repeats the password, so "ABC" works for "ABCABC" too. To prevent this, add
// the terminating null char to the password.
boolean nullTerminate;
private HashedPassword(byte[] hashed, byte[] salt, int cost) {
private HashedPassword(byte[] hashed, byte[] salt, int cost, boolean nullTerminate) {
this.salt = salt;
this.hashed = hashed;
this.cost = cost;
this.nullTerminate = nullTerminate;
checkState(cost >= 4 && cost < 32);
@@ -116,11 +126,16 @@ public class HashedPassword {
* @return one-line string encoding the hash and salt.
*/
public String encode() {
return ALGORITHM_PREFIX + cost + ":" + codec.encode(salt) + ":" + codec.encode(hashed);
return (nullTerminate ? ALGORITHM_PREFIX_0 : ALGORITHM_PREFIX)
+ cost
+ ":"
+ codec.encode(salt)
+ ":"
+ codec.encode(hashed);
}
public boolean checkPassword(String password) {
// Constant-time comparison, because we're paranoid.
return Arrays.areEqual(hashPassword(password, salt, cost), hashed);
return Arrays.areEqual(hashPassword(password, salt, cost, nullTerminate), hashed);
}
}

View File

@@ -53,4 +53,22 @@ public class HashedPasswordTest {
assertThat(hashed.checkPassword("false")).isFalse();
assertThat(hashed.checkPassword(password)).isTrue();
}
@Test
public void repeatedPasswordFail() throws Exception {
String password = "secret";
HashedPassword hashed = HashedPassword.fromPassword(password);
assertThat(hashed.checkPassword(password + password)).isFalse();
assertThat(hashed.checkPassword(password)).isTrue();
}
@Test
public void cyclicPasswordTest() throws Exception {
String encoded = "bcrypt:4:/KgSxlmbopLXb1eDm35DBA==:98n3gu2pKW9D5mCoZ5kNn9v4HcVFPPJy";
HashedPassword hashedPassword = HashedPassword.decode(encoded);
String password = "abcdef";
assertThat(hashedPassword.checkPassword(password)).isTrue();
assertThat(hashedPassword.checkPassword(password + password)).isTrue();
}
}