Cooking with credentials – pepper

No meal is complete without salt AND pepper. No password hashing scheme is truly secure without both, either!

No dinner is complete without the proper application spices. At a minimum, this includes both salt and pepper. We’ve already talked about using salts in a cryptographic context to protect passwords. Now we introduce the concept of peppers.

Even with proper salting, you still have to send the user’s password from the browser to the server for both registration and authentication. For this reason, modern browsers will warn users that their login is insecure if the site is not delivered over HTTP. This extra warning helps motivate application developers to properly configure a TLS certificate, but exposes a second potential issue with authentication.

As your users must send their password in plaintext to the server, there is the possibility that some other process on the server might inadvertantly record this data. If your application is aggressively logging requests, passwords might end up in system logs. If the application errs for any reason, the raw password might be emitted as context along with a PHP stack trace for debugging. In either of these situations, though you’re properly salting passwords for storage you have still accidentally stored a password in plaintext on the server.

A straight-forward way to avoid this is with a second salt used to create a hash on the client side. This kind of salt is actually called a “pepper.”

Peppers are like salts because they’re used along with a hashing algorithm to further separate a resultant hash from the algorithm’s plaintext input. Peppers differ from salts in that there is one pepper used for every password in your application rather than a unique salt for every person.

With a peppered system, your application presents a pepper to the client (in most cases the web browser) when authenticating or logging in. The client uses that pepper along with a hashing algorithm to hash the user’s plaintext password. Then the client sends the user’s identification details (username or password) and this hashed value to the server.

The server still salts this hash to create a second hash and only stores that second hash in the database. The server-side salting continues to protect users’ privacy, but the client-side peppering prevents plaintext passwords from ever being exchanged in the first place!

In browser-land, this flow can be implmented entirely in JavaScript using the SubtleCrypto interface of the Web Cryptography API. First, you leverage a TextEncoder to convert the user’s plaintext password to a “raw key” we can leverage with the API.

const pepper = new TextEncoder().encode('...');

async function getKeyMaterial(password) {
  let encoder = new TextEncoder();
  return window.crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    'PBKDF2',
    false,
    ['deriveBits']
  );
}

Then, we use this raw key to seed the PBKDF2 (password-based key derivation function) algorithm so we can generate a discrete hash from our password. Note in this example we’re using the SHA256 hash at the core of our derivation, with 100,000 iterations of the hash to slow things down. This helps protect our users from attack should someone ever capture any of the resulting hashes. We also pass our fixed server “pepper” into the algorithm as its salt.

async function pepperPassword(password) {
  let keyMaterial = await getKeyMaterial(password);
  let keyBuffer = await window.crypto.subtle.deriveBits(
    {
      'name': 'PBKDF2',
      'salt': pepper,
      'iterations': 100000,
      'hash': 'SHA-256'
    },
    keyMaterial,
    256
  );
  let keyArray = Array.from(new Uint8Array(keyBuffer));

  return keyArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

The PBKDF2 algorithm has broad browser support across Firefox, Chrome, and Safari. However, deriving a key is entirely unsupported on Internet Explorer and the PBKDF2 algorithm is unsupported on Edge.

The final step in the code block above is to hex-encode our resultant hash so we’re sending valid bytes across to the server during registration and authentication. Tying everything together allows us to reliably convert a user’s known password to a fixed, pseudo-random string of bytes we can use for authentication:

peppered = await pepperPassword('...');

Now, instead of sending a plaintext password to the server for salting and hashing, you send your peppered password. Even if this version of the password is somehow intercepted prior to the server hashing it, the party doing the capturing can’t reverse it to discover the original plaintext.