rchouinard/phpass

Last character in BCrypt Salt can only consist of one of four characters.

Closed this issue · 1 comments

The BCrypt adapter takes 16 bytes of random data and tries to create a salt using 64 possible characters (a period, a slash, 26 uppercase letters, 26 lowercase letters, and 10 numerics).

It does this by taking six bits of data from the 16 bytes provided to create a number between 0-63 to index into an array of salt characters. When there is no more data in a given byte, it takes the next 2 or 4 bits from the next byte, or begins a new byte entirely.

The problem is that only 16 bytes of data are being used to create a 22 character salt. 16 bytes only provides 128 bits of data. 22 six-bit indexes would take 132 bits to create.

The way PHPass creates the final salt character is to take the remaining two bits from the final byte, prep them to receive the next four bits from the next byte, which doesn't exist, and writes this as the final character.

This means that the rightmost four bits in the final byte will always be zeroes, creating only four possible character indexes, 0, 16, 32, and 48 (i.e. 000000, 100000, 010000, and 110000). These indexes correspond to the period character, uppercase O, lowercase E, and lowercase U.

One of these four characters will always be the last character in the BCrypt salt.

You can fix this by providing 17 bytes of data to the _encode64 method, and then taking trimming the output from that method to only use the first 22 characters provided. If you have a better way, then by all means please use that.

This is actually by design in accordance with the reference OpenBSD implementation of bcrypt, and in order to be compatible with other implementations such as py-bcrypt.

While technically the salt value can be any 22-character string in the range [./0-9A-Za-z], the reality is that the salt value is expected to be an encoded 16-byte (128-bit) value. Each character in the salt string represents 6 bits of salt value. As it's been pointed out, that means that the last character of the salt will only hold 2 bits of salt value and 4 bits of padding.

Various bcrypt implementations handle the salt padding in different ways. Some will ignore extra bits and will only compare the checksum values when verifying a password. These implementations will work with any valid character in the last position of the salt. Others, such as pybcrypt, will actually clear the extra bits and compare the entire hash string. These implementations will fail to validate any hash string which does not contain a properly zero-padded salt string.

Since the salt is a 128-bit value, adjusting an implementation to generate a non-padded salt string adds nothing as the last 4 bits are discarded anyway. In fact, doing so will simply break compatibility with other implementations.

A similar issue in a bcrypt implementation was found in the PassLib Python library, except in that case the library was generating non-padded salts and had to be patched to add padding: Issue 25 - passlib.

I'm afraid this has to be closed as "won't fix" as the implementation matches the spec, and implementing the suggested change would break compatibility with other bcrypt implementations.