# auth.password-hash Store a password as a string that says how it was hashed, check a login against it, and notice when it was hashed more weakly than today's setting: ``` stored = hashPassword("correct horse battery staple", salt, 600000) # pbkdf2_sha256$600000$AAECAwQFBgcICQoLDA0ODw==$<44 base64 characters> verifyPassword(attempt, stored) # true / false passwordNeedsRehash(stored, 600000) # after a successful login: re-hash and save if true ``` **The salt is an argument.** A capability cannot read randomness and stay testable, so the application generates it: 16 or more bytes from a secure source, fresh for every hash (`list(secrets.token_bytes(16))` in Python, `Array.from(crypto.getRandomValues(new Uint8Array(16)))` in TypeScript), never reused, never derived from the user. Fewer than 16 bytes is refused (NIST SP 800-132 section 5.1 asks for at least 128 bits). **The algorithm** is PBKDF2-HMAC-SHA256 (`crypto.pbkdf2-sha256`) over the password's UTF-8 bytes, deriving 32 bytes. The iteration count is the work factor: OWASP's Password Storage Cheat Sheet recommends 600,000 for this algorithm, which takes about 50 ms in Python and about 200 ms in pure TypeScript or Rust (timings in `crypto.pbkdf2-sha256`). Fewer than 1,000 is refused (NIST SP 800-132's minimum). The count, salt and hash are all in the stored string, so raising the count later does not break existing hashes: `verifyPassword` uses the stored count, and `passwordNeedsRehash` says which hashes to upgrade at their owner's next successful login. The stored form is `pbkdf2_sha256$$$`, with the salt and hash in standard, padded base64. It looks like Django's, but Django keeps its salt as text, so the two are not interchangeable. **verifyPassword** re-derives the key with the stored salt and count and compares it with `crypto.constant-time-equal`, so the time taken does not reveal how close a guess was. A wrong password is `false`. A stored string that is not this format (another algorithm, a missing field, bad base64, a count of 0 or with a leading zero) throws, because it means the database holds something this code did not write, and quietly answering `false` would lock the user out with no trace of why. The password is used exactly as given: not trimmed (a trailing space is part of it) and not Unicode-normalised, so `"é"` typed as one code point and as `e` plus a combining accent are different passwords. NIST SP 800-63B suggests normalising; the standard libraries of Rust and browser-free TypeScript have no normaliser, so it is left to the caller to apply NFC consistently if it wants one. Sources: RFC 8018 section 5.2 (PBKDF2); NIST SP 800-132, Recommendation for Password-Based Key Derivation, sections 5.1 and 5.2 (https://csrc.nist.gov/pubs/sp/800/132/final); NIST SP 800-63B section 5.1.1.2 (https://pages.nist.gov/800-63-3/sp800-63b.html); OWASP Password Storage Cheat Sheet (https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).