Spec covers data model, API, auth, frontend, error handling, and testing. ADR-0001 records PBKDF2 + pepper (FIPS) over argon2; ADR-0002 records revocable server-side sessions over Flask's signed cookie.
47 lines
2.3 KiB
Markdown
47 lines
2.3 KiB
Markdown
# 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_b64>$<derived_b64>"
|
||
```
|
||
|
||
- **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).
|