question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Prehashing causes interoperability issues

See original GitHub issue

This 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:

  1. 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.
  2. 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:closed
  • Created 3 years ago
  • Comments:6

github_iconTop GitHub Comments

1reaction
andreimiltocommented, Apr 14, 2020

@MiltoA thanks for the heads-up.

@ChrisMcKee, you’re welcome)

1reaction
ChrisMcKeecommented, Apr 14, 2020

@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).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Data Deduplication interoperability
This is similar to preindexing or prehashing a BranchCache-enabled server. DFS Replication. Data Deduplication works with Distributed File ...
Read more >
Top 5 Challenges with Interoperability in Healthcare
Challenges of Healthcare Interoperability · 1. Managing inconsistent information across multiple sources · 2. Validating electronic requests for ...
Read more >
SHA vs MD5 vs SHA256: Encryption Compatibility Guide
Learn how to handle compatibility and interoperability issues with SHA, MD5, and SHA256 encryption. Compare their differences, uses, and challenges.
Read more >
Whether to hash-then-sign with Dilithium and Falcon?
This makes the performance of hash-then-sign schemes more consistent, ... implementations can make use of pre-hashing the message to prevent rehashing with ...
Read more >
More efficient and just as secure to sign message hash ...
The section "Security notes on prehashing", page 5, says that the Ed25519 ... One motivation is that there have been to many issues...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found