Prehashing causes interoperability issues
See original GitHub issueThis is the fragment of the source code responsible for prehashing:
private const string Nul = "\0";
...
public static string HashPassword(string inputKey, string salt, bool enhancedEntropy, HashType hashType = DefaultEnhancedHashType)
{
...
byte[] inputBytes = SafeUTF8.GetBytes(inputKey + (bcryptMinorRevision >= 'a' ? Nul : EmptyString));
if (enhancedEntropy)
{
inputBytes = EnhancedHash(inputBytes, hashType);
}
...
byte[] hashed = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor);
// Generate result string
var result = new StringBuilder(60);
result.Append("$2").Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$');
result.Append(EncodeBase64(saltBytes, saltBytes.Length));
result.Append(EncodeBase64(hashed, (BfCryptCiphertext.Length * 4) - 1));
return result.ToString();
}
...
private static byte[] EnhancedHash(byte[] inputBytes, HashType hashType)
{
switch (hashType)
{
case HashType.SHA256:
inputBytes = SafeUTF8.GetBytes(Convert.ToBase64String(SHA256.Create().ComputeHash(inputBytes)));
break;
case HashType.SHA384:
inputBytes = SafeUTF8.GetBytes(Convert.ToBase64String(SHA384.Create().ComputeHash(inputBytes)));
break;
case HashType.SHA512:
inputBytes = SafeUTF8.GetBytes(Convert.ToBase64String(SHA512.Create().ComputeHash(inputBytes)));
break;
case HashType.Legacy384:
inputBytes = SHA384.Create().ComputeHash(inputBytes);
break;
default:
throw new ArgumentOutOfRangeException(nameof(hashType), hashType, null);
}
return inputBytes;
}
Judging from it and simply put, the raw bytes that are passed into bcrypt hash-function are derived as:
- if prehashing isn’t used
byte[] bcryptInput = Encoding.UTF8.GetBytes(password + "\0");
- with prehashing
byte[] passwordBytes = Encoding.UTF8.GetBytes(password + "\0");
byte[] bcryptInput = Encoding.UTF8.GetBytes(Convert.ToBase64String(SHA256.Create().ComputeHash(passwordBytes)));
This raises questions:
- When prehashing is used, why is null-terminating char added to the password? It looks like there’s no reason to do that, because the password is then passed through SHA-2, which will process it anyway (with or without null-terminator), so adding it seems like unnecessary work. Maybe you could clarify.
- When prehashing is used, why isn’t null-terminator added to the end of base64-string? Basically base64-string acts here like a password, which normally gets appended a null-terminator before being converted from string representation into bytes for bcrypt (as shown in the pseudo-code fragment without prehashing). Hope you can clarify this.
The second issue causes serious interoperability problem. When coding for different platform where there’s no BCrypt.Net-Next library, the prehashing code can’t be easily reproduced: one can generate base64-string the same exact way, but when it gets passed through bcrypt, it will automatically be appended with null-character (which doesn’t happen in BCrypt.Net-Next when prehashing is used)
string password = "MyPassword";
string prehashedPassword;
using (var sha256 = SHA256.Create())
prehashedPassword = Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(password + "\0")));
// Null-terminator gets appended inside this method, before converting argument into bytes:
string hash = SomeOtherBcryptLibrary.HashPassword(prehashedPassword);
The same is true for verification:
string hash = "$2b$12$...";
string password = "MyPassword";
string prehashedPassword;
using (var sha256 = SHA256.Create())
prehashedPassword = Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(password + "\0")));
// The method computes bcrypt-hash for the supplied password and compares it to the supplied hash;
// it adds null-terminating character to the password before running it through bcrypt - something BCrypt.Net-Next doesn't do when prehashing is used:
bool isPasswordValid = SomeOtherBcryptLibrary.Verify(hash, prehashedPassword);
As a result, if prehashing is used in BCrypt.Net-Next
- hashes generated in BCrypt.Net-Next can’t be verified in a 3rd-party bcrypt library;
- hashes generated in a 3rd-party bcrypt library can’t be verified in BCrypt.Net-Next (using built-in prehashing mechanism).
The workaround is to use 3rd-bcrypt library that accepts password as bytes, not string - this will allow to reproduce the hashing steps of BCrypt.Net-Next exactly. But most libraries don’t support hashing raw bytes (including BCrypt.Net-Next itself).
And back to the first issue, not adding null-terminator to the password while prehashing simplifies the code. With both issues fixed the code would look like
string password = "MyPassword";
string prehashedPassword;
using (var sha256 = SHA256.Create())
prehashedPassword = Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(password)));
string hash = AnyBcryptLibrary.HashPassword(prehashedPassword)
This code still solves the problem of long passwords for bcrypt and uses SHA-2 prehashing. But it requires an implementation of bcrypt that accepts password as a string - a functionality commonly supported in other bcrypt libraries, which makes this code highly interoperable. So, I suggest using this approach for prehashing in BCrypt.Net-Next.
Issue Analytics
- State:
- Created 3 years ago
- Comments:6
Top GitHub Comments
@ChrisMcKee, you’re welcome)
@snekbaev yeah it only effects the SHA hashed route. The standard routes are unaffected (we already had good pre-existing vectors to test the classic route as well so it should stay safe anyway).