# Use HMAC-SHA256 for API key hashing * Status: accepted * Date: 2026-04-26 ## Context and Problem Statement API keys must be stored hashed in the database (never in plaintext). Authentication requires looking up the corresponding `Application` record by the hash of the incoming key. This makes the hashing algorithm subject to an unusual constraint: **the hash must be deterministic** so that the same key always produces the same hash and the database lookup works. ## Decision Drivers * Deterministic hashing is required to enable `findOne({ apikey: hash })` lookups. * The previous implementation used bcrypt with the JWT secret as a fixed salt — this made hashes deterministic but defeated bcrypt's core security property (random salting). * API keys are UUID v4 values (128 bits of entropy) — they are not low-entropy secrets like passwords. * A slow hashing function creates a DoS vector: an attacker flooding the API key auth endpoint forces expensive bcrypt operations on every request. ## Considered Options * bcrypt with random salt (standard password hashing) * bcrypt with fixed salt (previous implementation) * HMAC-SHA256 with the application secret ## Decision Outcome Chosen option: "HMAC-SHA256", because it is deterministic (lookup by hash works), fast (no DoS surface), and cryptographically appropriate for high-entropy secrets. The security guarantee shifts from computational hardness (bcrypt) to secret confidentiality (HMAC key), which is the correct model for API keys. Implementation: `crypto.createHmac('sha256', secret).update(key).digest('hex')` in `src/middlewares/auth.js` (key generation) and `src/config/passport-headerapikey.js` (verification). The HMAC key is the application `SECRET` environment variable, shared with JWT signing. ### Positive Consequences * Database lookup by hash remains possible — no change to the query pattern. * No DoS vector: HMAC-SHA256 is fast (~microseconds vs bcrypt's ~100ms). * bcrypt's fixed-salt weakness eliminated. ### Negative Consequences * Security depends on the `SECRET` environment variable remaining confidential. If it leaks, an attacker can pre-compute hashes for any key. `SECRET` must be rotated and all API keys regenerated if a leak is suspected. * The same `SECRET` is used for JWT signing and HMAC — a future improvement would be to use a dedicated `APIKEY_SECRET` env var. ## Pros and Cons of the Options ### HMAC-SHA256 * Good, because deterministic — lookup by hash works without additional query logic. * Good, because fast — no DoS risk on the auth endpoint. * Good, because appropriate for high-entropy inputs (UUID keys have 128 bits of entropy; bcrypt's brute-force resistance is unnecessary). * Bad, because security depends on secret confidentiality rather than computational cost. ### bcrypt with random salt * Good, because industry standard for secrets that require brute-force resistance. * Bad, because non-deterministic — lookup by hash is impossible without storing additional plaintext identifiers. ### bcrypt with fixed salt (previous implementation) * Good, because deterministic. * Bad, because a fixed salt makes all hashes pre-computable for a given salt — defeating bcrypt's main purpose. * Bad, because bcrypt's slowness creates a DoS surface on the auth endpoint. ## Links * Replaces the fixed-salt bcrypt approach introduced with `passport-headerapikey` strategy. * Related to [ADR 0006](0006-jwt-authentication.md) — both auth mechanisms share the `SECRET` env var.