Skip to main content

Module api_key

Module api_key 

Source
Expand description

Scoped API keys (spec §v2.4 Task 2): mint a high-entropy key, store only its SHA-256 hash, authenticate requests, and scope-check them.

§Why SHA-256, not argon2

API keys are machine-generated with 256 bits of entropy (mint draws 32 bytes from the OS CSPRNG), so brute-forcing the preimage is infeasible regardless of hash speed. argon2 buys nothing here and would make the lookup column slow and variable-length. A plain hex SHA-256 gives a fast, fixed-width (64-char) lookup key — exactly what a DB index wants.

§Why the verify is constant-time

verify hashes the presented plaintext and compares the two 32-byte digests in constant time via hmac’s verify_slice (the same primitive the webhook code uses). It deliberately does NOT compare the hex Strings with ==: String/&str equality short-circuits on the first differing byte, which leaks, through timing, how many leading hex chars of the stored hash a guess matched. Comparing the raw digests in fixed time closes that channel. (The stored hash is not itself a secret the way an HMAC key is, but verifying in constant time is cheap and removes the channel by construction.)

§DI: how the store reaches the extractor

ApiKey resolves the store as the concrete newtype ApiKeys (which wraps Arc<dyn ApiKeyStore>), NOT a bare Arc<dyn ApiKeyStore>. The DI layer keys providers on TypeId and round-trips a bare trait-object Arc just fine, but the newtype is the documented, unambiguous contract: the app calls app.provide(ApiKeys::new(store)) and the extractor resolves ApiKeys. See the bare_arc_dyn_also_round_trips_through_di test for evidence the bare form works too; we still standardize on the newtype.

Structs§

ApiKey
API-key extractor. Reads the key from Authorization: Bearer <key> (checked first) or X-API-Key: <key>, hashes it, looks it up in the provided ApiKeys store, and yields the matching ApiKeyRecord. A missing, malformed, or unknown key is a 401. Scope checks are a separate, explicit step (ApiKeyRecord::require_scope) so a handler decides what it needs.
ApiKeyRecord
A stored API-key record: its DB id, display prefix, hex hash, and scopes.
ApiKeys
The DI handle for an ApiKeyStore. Apps register the store with app.provide(ApiKeys::new(store)); the ApiKey extractor resolves this concrete type. A newtype (rather than a bare Arc<dyn ApiKeyStore>) is the documented contract — see the module docs.
InMemoryApiKeyStore
In-memory store keyed by hex hash — for tests, the mock, and small deploys. Production apps implement ApiKeyStore over their database instead.
MintedApiKey
A freshly minted key. The plaintext is shown to the operator exactly once and is NEVER stored — only its hash (and prefix) are persisted.

Traits§

ApiKeyStore
Looks up a stored key by its hex hash. Object-safe so apps can back it with a DB table (returning None for an unknown hash, NOT an error).

Functions§

hash_key
Hex SHA-256 of the full key plaintext — the value stored in the lookup column.
mint
Mint a new API key: 32 bytes from the OS CSPRNG, formatted as {prefix}_{base64url_nopad(random)}. The returned MintedApiKey::hash is what you store; MintedApiKey::plaintext is shown once and discarded.
require_scope
Scope check (mirrors crate::require_role): Ok(()) when scopes contains needed or the wildcard "*", otherwise Error::forbidden() (403). The wildcard is an admin/root grant — a "*" key passes every check.
verify
Constant-time check that plaintext hashes to stored_hash.

Type Aliases§

ApiKeyFuture
The boxed Send future every ApiKeyStore method returns. Hand-boxed (not async-trait) so the trait stays object-safe behind dyn — the same idiom as jerrycan_jobs’s JobFuture and jerrycan_ratelimit’s HitFuture.