Sikau

Security · Day 6 · about 40 min

Storing passwords

Salted, slow PBKDF2 hashes and checking them in constant time.

Assume the database leaks

Databases get stolen. If you stored passwords — or fast hashes of them — attackers recover most of them in hours. Store a salted, deliberately slow hash (PBKDF2, bcrypt, scrypt or Argon2): a unique random salt per user defeats precomputed tables, and 100,000 iterations makes every guess expensive.

PBKDF2 with WebCrypto

const toHex = (bytes) => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
const fromHex = (hex) => Uint8Array.from(hex.match(/../g) ?? [], (h) => parseInt(h, 16));

async function pbkdf2(password, salt, iterations) {
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
  const bits = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations }, key, 256);
  return toHex(bits);
}

async function hashPassword(password, { salt = crypto.getRandomValues(new Uint8Array(16)), iterations = 100000 } = {}) {
  const saltHex = typeof salt === "string" ? salt : toHex(salt);
  const hash = await pbkdf2(password, fromHex(saltHex), iterations);
  return `pbkdf2$${iterations}$${saltHex}$${hash}`; // everything needed to check it later
}

async function verifyPassword(password, stored) {
  const [scheme, iterations, saltHex, hash] = stored.split("$");
  if (scheme !== "pbkdf2") return false;
  return safeEqual(await pbkdf2(password, fromHex(saltHex), Number(iterations)), hash);
}

The rest of this lesson

  • Why slow is goodexplain
  • The stored formatexplain
  • Signing up, then logging inorder
  • Threat and defencematch
  • Hash and verifywrite code
  • Badge quizquiz
  • What you learnedrecap

Continue this lesson free — run real code in your browser

Continue with Google

See all 8 lessons of Security