Functional Weave
Code in Rust

crypto.pbkdf2-sha256@1.0.0

README.md

2,634 bytes · view raw

# crypto.pbkdf2-sha256

PBKDF2 (RFC 8018 section 5.2) with HMAC-SHA256 as its pseudorandom function:
it stretches a password and a salt into a key by running HMAC `iterations`
times, so that every guess an attacker makes costs the same. It is the
algorithm under `auth.password-hash`, which is what an application should
normally call; use this directly only to derive keys.

Bytes in and out are lists of integers 0 to 255, as everywhere in the registry
(see `encoding.hex`). Encode a text password with `encoding.utf8` first.

**How many iterations.** OWASP's Password Storage Cheat Sheet recommends
600,000 for PBKDF2-HMAC-SHA256 (2023). Measured on an Apple Silicon laptop,
one 32-byte key at a time:

| iterations | Python (hashlib) | Rust (release) | TypeScript (Node 24) |
|-----------:|-----------------:|---------------:|---------------------:|
| 100,000    | 8 ms             | 48 ms          | 39 ms                |
| 310,000    | 24 ms            | 95 ms          | 113 ms               |
| 600,000    | 47 ms            | 178 ms         | 222 ms               |

so 600,000 is affordable in a login request in every language. Record the
count with the hash (`auth.password-hash` does) so it can be raised later.

**Implementation.** Python uses the standard library's
`hashlib.pbkdf2_hmac`, which runs the loop in C. TypeScript and Rust are
written out with no `node:crypto` or crates; they precompute the HMAC key's
inner and outer pad states once, and hash each 32-byte intermediate as a
single pre-padded block, which halves the work of calling HMAC naively and
allocates nothing inside the loop. That needs SHA-256's compression function
itself, which `crypto.sha256` does not expose, so it is repeated here rather
than required.

A password longer than 64 bytes is hashed first, as HMAC requires (RFC 2104),
and the key may be longer than 32 bytes: each further 32-byte block runs the
full iteration count again (RFC 8018's `T_i`), so ask for only what you use.

Empty passwords and empty salts are computed, as the RFC allows; refusing a
weak salt is `auth.password-hash`'s job (it wants 16 bytes or more).

Sources: RFC 8018, PKCS #5: Password-Based Cryptography Specification
Version 2.1, section 5.2 (https://www.rfc-editor.org/rfc/rfc8018); the
PBKDF2-HMAC-SHA256 test vectors of RFC 7914 section 11
(https://www.rfc-editor.org/rfc/rfc7914#section-11); RFC 6070's inputs
(https://www.rfc-editor.org/rfc/rfc6070) with their published SHA-256
results, each confirmed against Python's hashlib; OWASP Password Storage
Cheat Sheet (https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).