Salts

A salt is extra data added to a string before password hashing. It is named "salt" because it is similar to adding table salt to food—it modifies the food slightly and improves it. A string being encrypted is improved by adding a salt value because the algorithm outputs a different hash than what it would without the salt. This prevents the hash from matching any pre-computed hash stored in rainbow tables. Using salt with passwords is the best defense against rainbow tables.

A salt adds some piece of custom data which is unique to this hashing process so that it will not be like anyone else's hashing process. With a salt in place, an attacker would need to know the salt value and then also re-compute an entire set of rainbow tables to crack the password.

Salt with md5() (using a non-random salt)

<?php

  $password = 'password1234';

  echo md5($password);
  // bdc87b9c894da5168059e00ebffb9077

  echo md5($password . 'any string');
  // 17b5c2d5e5bc4418a65c19b2af58ce2d
  
?>

Rainbow tables could have any entry for the first hash&mdsah;sadly 'password1234' is a common password. Rainbow tables could have an entry for the second hash too, but if they do, the string that is associated with it would not be the correct password.


Fresh, Random Salt

While adding any salt at all will prevent rainbow tables from finding the password, unfortunately, the password will still not be very secure. If an attacker is able to decrypt a few of the passwords, the salt would become obvious.

Instead of using a fixed string, a salt should always be a random string, generated fresh for each hash. If every password gets its own random salt, then discovering one will not be helpful for any other.

As funny as it sounds, random functions do not return equally random values. Some are "more random" than others. For cryptography it is important to pick values from the "most random" end of the scale. random_bytes() is well-designed and is the easiest way to get values random enough for use in encryption. The return value needs to be converted from binary to a string using base64_encode().

<?php

  function random_string($length=22) {
    // random_bytes requires an integer larger than 1
    $length = max(1, (int) $length);
    // generates a longer string than needed
    $rand_str = base64_encode(random_bytes($length));
    // substr cuts it to the correct size
    return substr($rand_str, 0, $length);
  }
  
?>

For hashing algorithms, this means keeping track of the salt used so that it can be used again to hash a candidate password to see if it matches the stored password. The salt could be stored in a database (as a new column next to the encrypted password column). Alternatively, the salt can be joined with the encrypted password with a distinct separating character between them (to split them up again later). This is the approach which bcrypt uses.


Salts for bcrypt

Salt values used for bcrypt should be 22 characters long and should only contain letters, numbers, . and /. The random_string() code above is a good way to generate this string, with only one catch. base64_encode() can return + which is not one of the allowed characters for a bcrypt salt. strtr() is an easy way to translate the + into a different character.

<?php

  function make_salt() {
    $rand_str = random_string(22);
    // bcrypt doesn't like '+'
    $salt = strtr($rand_str, '+', '.');
    return $salt;
  }

?>

In PHP, the salt value must be appended to the end of the hash format string which is sent to the crypt() function.

<?php

  $password = 'password1234';
  $hash_format = "$2y$10$";
  $salt = make_salt();

  echo $salt;
  // 2701e447941be9fd4652d8

  $hash = crypt($password, $hash_format.$salt);
  echo $hash;
  // $2y$10$2701e447941be9fd4652duwJbh/eGrIGzuSpJVL3t2GEwYXD2Gx1q
  
?>

Notice in the above example that the value of $salt is included in the value of $hash. This is a technique that bcrypt uses to keep track of the salt it used for encryption, it prepends the format and salt value to the hash.

Keeping the salt with the result enables a neat trick when it is time to compare a new string with an existing hash. Instead of providing the $hash_format.$salt as the second argument to crypt() (as above), the full result of the previous hash can be used as the second argument, which includes the format string and salt at the start. bcrypt clips the salt value off the front of the string and uses it for hashing the new plain text string.

<?php

  $new_hash = crypt($new_password, $hashed_password);
  $is_match = ($new_hash === $hashed_password);

?>

If the password matches then the result should be $2y$10$2701e447941be9fd4652duwJbh/eGrIGzuSpJVL3t2GEwYXD2Gx1q.

If it is not a match, then it will have the same format string and salt value at the start, but the hash result at the end will be different.

Fork me on GitHub