0

Salt — random data added to a password before hashing to defeat rainbow tables

Beginner5 min read·eng-19-006
interviewsecurity

Concept

Salt — a random value added to a password BEFORE hashing. Each user gets a unique salt, stored alongside their hash. This defeats precomputed rainbow table attacks.

The problem salts solve — rainbow tables:

  1. Attackers precompute a table of hashes for millions of common passwords: MD5("password") = 5f4dcc3b5aa765d61d8327deb882cf99.
  2. If you store MD5("password") in your database and it leaks, attackers look up the hash in their table → instant match.
  3. With a salt: MD5("password" + "x7Kq9mZ2") → completely different hash. Attacker's rainbow table is useless — they'd need a separate table for every possible salt.

How salting works:

  1. Generate a random salt when creating a password: $salt = bin2hex(random_bytes(16)).
  2. Hash: hash('sha256', $password . $salt).
  3. Store BOTH the hash and the salt in the database.
  4. On login: retrieve the salt, re-hash the attempt, compare.

Modern approach: You don't manage salts manually. password_hash() (PHP) and Hash::make() (Laravel) handle salt generation and storage internally. The output (e.g., $2y$12$...) contains the algorithm, cost factor, AND salt encoded together.

Salt vs pepper:

  • Salt: Unique per user, stored with the hash. Public (stored in DB).
  • Pepper: One secret value added to ALL passwords, stored in application config/environment. Not stored in DB — if DB leaks without the app's pepper, hashes are harder to crack. But if config leaks, ALL passwords are compromised at once.

Never reuse salts: If two users have the same password AND the same salt, their hashes are identical — attackers can deduce duplicates. Unique salts per user prevent this.

Code Example

php
<?php
// HOW SALTS WORK (manual illustration — don't actually do this)
function manualSaltHash(string $password): string
{
    $salt = bin2hex(random_bytes(16)); // 32 char random hex
    $hash = hash('sha256', $password . $salt);
    return $salt . ':' . $hash; // store both
}

function manualSaltVerify(string $password, string $stored): bool
{
    [$salt, $hash] = explode(':', $stored);
    return hash_equals($hash, hash('sha256', $password . $salt));
}

// Same password, different salts → DIFFERENT stored values (defeats rainbow tables)
$a = manualSaltHash('secret'); // "a3f9b2:8c91d4..."
$b = manualSaltHash('secret'); // "7e2k1m:4a82c1..."  completely different!

// PHP's password_hash() does this automatically (and better)
$hash1 = password_hash('secret', PASSWORD_BCRYPT);  // includes salt
$hash2 = password_hash('secret', PASSWORD_BCRYPT);  // different salt, different output
// $hash1 !== $hash2  — even for the same password!

// The hash format includes everything (algo + cost + salt + hash):
// $2y$12$   {22 chars of salt}  {31 chars of hash}
// bcrypt  cost=12

password_verify('secret', $hash1); // true  — extracts salt from $hash1, re-hashes
password_verify('wrong',  $hash1); // false

// LARAVEL — uses password_hash() under the hood
$hash = Hash::make('mypassword');
// $hash = "$2y$12$IKqjH4ek..." — bcrypt format with embedded salt

Hash::check('mypassword', $hash); // true

// PEPPER (application-level secret — optional)
class PepperedHasher implements Hasher
{
    private string $pepper;

    public function __construct()
    {
        $this->pepper = config('hashing.pepper'); // secret in .env
    }

    public function make(string $value, array $options = []): string
    {
        return Hash::make($value . $this->pepper, $options);
    }

    public function check(string $value, string $hashedValue, array $options = []): bool
    {
        return Hash::check($value . $this->pepper, $hashedValue, $options);
    }
}