BcryptNet/bcrypt.net

HashPassword changes existing salt

YOUR1 opened this issue · 1 comments

YOUR1 commented

Hi,

I've encountered a bug where I'm using a predefined salt. See attached code:

public const string INPUT_PASSWORD = "abcdefg";
public const string INPUT_SALT = "1sHdZFnB7gGdMG0gTRO/kg==";

public static void Main() {

   // Set salt with options
   string saltWithOptions = String.Format("$2a$08${0}", INPUT_SALT);
   // Generate hash
   string BCryptHash = BCrypt.Net.BCrypt.HashPassword(INPUT_PASSWORD, saltWithOptions, false, HashType.None);

   // Run regex 
   Regex re = new Regex("[$](?<version>2[abxy]?)[$](?<strength>(?<cost>(0[4-9]|[12][0-9]|3[01])))[$](?<password>((?<salt>[./0-9a-zA-Z]{22})(?<hash>[./0-9a-zA-Z]{31})))");
   GroupCollection matches = re.Match(BCryptHash).Groups;

   // Validate hash
   if (matches.Count != 9)
      throw new ApplicationException("Invalid BCrypt hash generated");

   string hashVersion = matches["version"].Value;
   string hashCost = matches["cost"].Value;
   string hashSalt = matches["salt"].Value;
   string hashHash = matches["hash"].Value;
   byte[] hashBytes = encoding.GetBytes(hashHash);

   Console.WriteLine("Got hash: " + BCryptHash);
   Console.WriteLine(String.Format(" - Version: {0}", hashVersion));
   Console.WriteLine(String.Format(" - Strength: {0}", hashCost));
   Console.WriteLine(String.Format(" - Salt: {0}", hashSalt));
   Console.WriteLine(String.Format(" - Hash: {0} ({1} bytes)", Base64UrlEncode(hashBytes), hashBytes.Count()));
   Console.WriteLine("Salt matches original salt: " + (INPUT_SALT == hashSalt).ToString());
}

Console output:

Got hash: $2a$08$1sHdZFnB7gGdMG0gTRO/ke3gj9C9vhaGDE7bysgSivGBzWe5t3pPi
 - Version: 2a
 - Strength: 08
 - Salt: 1sHdZFnB7gGdMG0gTRO/ke
 - Hash: M2dqOUM5dmhhR0RFN2J5c2dTaXZHQnpXZTV0M3BQaQ (31 bytes)
Salt matches original salt: False

I expect to be the salt exactly the same as the input. But the last character of the input salt has changed from g to e.

Can somebody explain me why this is happening, and what's the best way to deal with this?

The salt1sHdZFnB7gGdMG0gTRO/kg== is 24 characters; salts are only 22, so its truncated.
So the salt used becomes 1sHdZFnB7gGdMG0gTRO/kg
BCrypt the decodes the 22 char salt into a 16-byte value salt using BCrypt's base64 scheme decoder.

Byte16 - 222,226,95,108,122,67,246,34,31,56,141,162,85,52,1,154

The decoders equal, the encoder converts this back as 1sHdZFnB7gGdMG0gTRO/ke

This is the same true in all implementations.
If you have python; pip install bcrypt --user

import bcrypt
bcrypt.hashpw("abcdefg","$2a$08$1sHdZFnB7gGdMG0gTRO/kg==")
#OUTPUT: '$2a$08$1sHdZFnB7gGdMG0gTRO/ke3gj9C9vhaGDE7bysgSivGBzWe5t3pPi'

We could throw if the salt is too large to point out that what you're passing is being truncated but its not standard behaviour across other libraries; generating salts yourself is generally discouraged though across all major implementations.

| Name | Value in .net b64 representation | Type
-- | -- | -- | --
| Full "1sHdZFnB7gGdMG0gTRO/kg==" | "3uJfbHpD9iIfOI2iVTQBmg==" | string
| Truncated"1sHdZFnB7gGdMG0gTRO/kg" | "3uJfbHpD9iIfOI2iVTQBmg==" | string