Merge "Null-terminate passwords by default before hashing them"
This commit is contained in:
@@ -31,6 +31,7 @@ import org.bouncycastle.util.Arrays;
|
|||||||
*/
|
*/
|
||||||
public class HashedPassword {
|
public class HashedPassword {
|
||||||
private static final String ALGORITHM_PREFIX = "bcrypt:";
|
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 SecureRandom secureRandom = new SecureRandom();
|
||||||
private static final BaseEncoding codec = BaseEncoding.base64();
|
private static final BaseEncoding codec = BaseEncoding.base64();
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ public class HashedPassword {
|
|||||||
* @throws DecoderException if input is malformed.
|
* @throws DecoderException if input is malformed.
|
||||||
*/
|
*/
|
||||||
public static HashedPassword decode(String encoded) throws DecoderException {
|
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");
|
throw new DecoderException("unrecognized algorithm");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,19 +75,24 @@ public class HashedPassword {
|
|||||||
if (salt.length != 16) {
|
if (salt.length != 16) {
|
||||||
throw new DecoderException("salt should be 16 bytes, got " + salt.length);
|
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);
|
byte[] pwBytes = password.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (nullTerminate && !password.endsWith("\0")) {
|
||||||
|
pwBytes = Arrays.append(pwBytes, (byte) 0);
|
||||||
|
}
|
||||||
return BCrypt.generate(pwBytes, salt, cost);
|
return BCrypt.generate(pwBytes, salt, cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashedPassword fromPassword(String password) {
|
public static HashedPassword fromPassword(String password) {
|
||||||
byte[] salt = newSalt();
|
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() {
|
private static byte[] newSalt() {
|
||||||
@@ -98,11 +104,15 @@ public class HashedPassword {
|
|||||||
private byte[] salt;
|
private byte[] salt;
|
||||||
private byte[] hashed;
|
private byte[] hashed;
|
||||||
private int cost;
|
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.salt = salt;
|
||||||
this.hashed = hashed;
|
this.hashed = hashed;
|
||||||
this.cost = cost;
|
this.cost = cost;
|
||||||
|
this.nullTerminate = nullTerminate;
|
||||||
|
|
||||||
checkState(cost >= 4 && cost < 32);
|
checkState(cost >= 4 && cost < 32);
|
||||||
|
|
||||||
@@ -116,11 +126,16 @@ public class HashedPassword {
|
|||||||
* @return one-line string encoding the hash and salt.
|
* @return one-line string encoding the hash and salt.
|
||||||
*/
|
*/
|
||||||
public String encode() {
|
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) {
|
public boolean checkPassword(String password) {
|
||||||
// Constant-time comparison, because we're paranoid.
|
// Constant-time comparison, because we're paranoid.
|
||||||
return Arrays.areEqual(hashPassword(password, salt, cost), hashed);
|
return Arrays.areEqual(hashPassword(password, salt, cost, nullTerminate), hashed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,4 +53,22 @@ public class HashedPasswordTest {
|
|||||||
assertThat(hashed.checkPassword("false")).isFalse();
|
assertThat(hashed.checkPassword("false")).isFalse();
|
||||||
assertThat(hashed.checkPassword(password)).isTrue();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user