64 lines
3.4 KiB
Markdown
64 lines
3.4 KiB
Markdown
# 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.
|