Merge "Null-terminate passwords by default before hashing them"
diff --git a/java/com/google/gerrit/server/account/HashedPassword.java b/java/com/google/gerrit/server/account/HashedPassword.java
index 64a4495..09115503 100644
--- a/java/com/google/gerrit/server/account/HashedPassword.java
+++ b/java/com/google/gerrit/server/account/HashedPassword.java
@@ -31,6 +31,7 @@
*/
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 @@
* @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 @@
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 @@
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 @@
* @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);
}
}
diff --git a/javatests/com/google/gerrit/server/account/HashedPasswordTest.java b/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
index 3443720..066502d 100644
--- a/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
+++ b/javatests/com/google/gerrit/server/account/HashedPasswordTest.java
@@ -53,4 +53,22 @@
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();
+ }
}