# ADR-0001: Password hashing with peppered PBKDF2-HMAC-SHA256 Date: 2026-10-02 Status: Accepted ## Context Users authenticate with a username and password. The application targets a healthcare firm, so cryptographic choices should be FIPS 140-approved where practical. The assignment restricts the backend to standard Python libraries (Flask and psycopg2 are provided by the environment). The runtime container has 0.5 GB of RAM. Memory-hard algorithms (Argon2id, scrypt) resist GPU cracking better, but neither is FIPS-approved and Argon2 is not in the Python standard library. ## Decision Hash passwords with PBKDF2-HMAC-SHA256 from `hashlib`, keyed by a secret pepper: ``` peppered = HMAC-SHA256(key=PEPPER, msg=password_utf8) derived = hashlib.pbkdf2_hmac("sha256", peppered, salt, 600_000) stored = "pbkdf2_sha256$600000$$" ``` - **Salt:** `secrets.token_bytes(16)` per user (128 bits; NIST SP 800-132 requires ≥ 128). - **Iterations:** 600,000 (OWASP Password Storage Cheat Sheet figure for PBKDF2-HMAC-SHA256). Stored with the hash; hashes below the current count are re-derived on the next successful login. - **Pepper:** a secret held outside the database, applied as an HMAC key (NIST SP 800-63B's "secret salt" recommendation). A database-only leak does not allow offline guessing. Source: `PASSWORD_PEPPER` (≥ 32 chars) when set; otherwise a 32-byte random value generated once into `backend/.pepper` (mode 0600, gitignored) with a startup warning that this fallback is for development only. - **Comparison:** `hmac.compare_digest` (constant time). - **Enumeration resistance:** a login for an unknown username verifies against a fixed dummy hash so response time matches a real account; both failures return the same message. - **Policy:** 12–1024 characters, no composition rules (NIST SP 800-63B). The upper bound caps CPU cost per request. ## Consequences - FIPS-approved primitives only (HMAC-SHA256, PBKDF2); no third-party dependency. - Each login costs ~0.3–0.5 s of CPU and negligible memory, which fits the container. - Weaker than Argon2id against GPU attacks on a leaked database; the pepper offsets this, because the attacker also needs the application secret. - Losing the pepper invalidates every password. Production must keep it in a secrets manager and add pepper versioning for rotation (out of scope here).