-
Notifications
You must be signed in to change notification settings - Fork 283
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve security of BCrypt-based password encoding
- Loading branch information
1 parent
f6136a7
commit 8a13cfc
Showing
5 changed files
with
134 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,7 +31,12 @@ | |
public class BCryptPasswordEncoder implements PasswordEncoder { | ||
|
||
public static final int SALT_SIZE_BYTES = 16; | ||
/** | ||
* This isn't the standard implementation of BCrypt, and is used for Turms only. | ||
*/ | ||
public static final byte VERSION_BYTES = 1; | ||
private static final int COST = 10; | ||
private static final byte NULL_TERMINATOR = (byte) 0; | ||
|
||
private static final FastThreadLocal<BCrypt> BCRYPT = new FastThreadLocal<>() { | ||
@Override | ||
|
@@ -40,26 +45,74 @@ protected BCrypt initialValue() { | |
} | ||
}; | ||
|
||
/** | ||
* @implNote The implementation doesn't validate the null terminator in the raw password to | ||
* avoid double checks, and it is the responsibility of the caller to ensure that no | ||
* null terminator is in the raw password. | ||
*/ | ||
@Override | ||
public byte[] encode(byte[] rawPassword) { | ||
int length = rawPassword.length; | ||
if (length > BCrypt.MAX_PASSWORD_BYTES) { | ||
throw new IllegalArgumentException( | ||
"The password length must be less than " | ||
+ BCrypt.MAX_PASSWORD_BYTES); | ||
} | ||
if (length != BCrypt.MAX_PASSWORD_BYTES) { | ||
rawPassword = ArrayUtil.concat(rawPassword, NULL_TERMINATOR); | ||
} | ||
byte[] salt = new byte[SALT_SIZE_BYTES]; | ||
ThreadLocalRandom.current() | ||
.nextBytes(salt); | ||
byte[] password = BCRYPT.get() | ||
.generate(rawPassword, salt, COST); | ||
return ArrayUtil.concat(salt, password); | ||
byte[] encodedPassword = BCRYPT.get() | ||
.deriveRawKey(COST, salt, rawPassword); | ||
|
||
int encodedPasswordLength = encodedPassword.length; | ||
int resultLength = VERSION_BYTES + SALT_SIZE_BYTES + encodedPasswordLength; | ||
byte[] result = new byte[resultLength]; | ||
System.arraycopy(salt, 0, result, 0, SALT_SIZE_BYTES); | ||
System.arraycopy(encodedPassword, 0, result, SALT_SIZE_BYTES, encodedPasswordLength); | ||
// The last byte indicates the version and cost. | ||
result[resultLength - 1] = (byte) COST; | ||
return result; | ||
} | ||
|
||
@Override | ||
public boolean matches(byte[] rawPassword, byte[] saltedPasswordWithSalt) { | ||
int length = rawPassword.length; | ||
if (length > BCrypt.MAX_PASSWORD_BYTES) { | ||
return false; | ||
} | ||
for (byte b : rawPassword) { | ||
if (b == NULL_TERMINATOR) { | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
JamesChenX
Author
Member
|
||
return false; | ||
} | ||
} | ||
// If the length is 40, it means the password of the first version that has no null | ||
// terminator. | ||
int saltedPasswordWithSaltLength = saltedPasswordWithSalt.length; | ||
if (saltedPasswordWithSaltLength == 40) { | ||
byte[] saltedPassword = BCRYPT.get() | ||
.deriveRawKey(10, saltedPasswordWithSalt, rawPassword); | ||
return Arrays.equals(saltedPassword, | ||
0, | ||
saltedPassword.length, | ||
saltedPasswordWithSalt, | ||
SALT_SIZE_BYTES, | ||
saltedPasswordWithSaltLength); | ||
} | ||
int cost = saltedPasswordWithSalt[saltedPasswordWithSaltLength - 1] & 0xFF; | ||
if (length != BCrypt.MAX_PASSWORD_BYTES) { | ||
rawPassword = ArrayUtil.concat(rawPassword, NULL_TERMINATOR); | ||
} | ||
byte[] saltedPassword = BCRYPT.get() | ||
.generate(rawPassword, saltedPasswordWithSalt, COST); | ||
.deriveRawKey(cost, saltedPasswordWithSalt, rawPassword); | ||
return Arrays.equals(saltedPassword, | ||
0, | ||
saltedPassword.length, | ||
saltedPasswordWithSalt, | ||
SALT_SIZE_BYTES, | ||
saltedPasswordWithSalt.length); | ||
saltedPasswordWithSaltLength - 1); | ||
} | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Should this check for the
NULL_TERMINATOR
also exist for theencode
method (unless I overlooked it)?Otherwise it seems one can create a password containing
(byte) 0
but can then never log in with that password.