Documentation
¶
Overview ¶
Package apikey implements the wire format and crypto for HTTP API authentication tokens this server issues for non-interactive callers (Prometheus, scripts, CI). It's deliberately small and DB- free so it can be unit-tested without spinning up SQLite — the storage layer lives in the httpserver package.
The wire format is:
htca-v1-<key_id>-<secret>
where key_id is 12 hex chars (6 random bytes; this is what we use as the indexable lookup column) and secret is 32 hex chars (16 random bytes; only its SHA-256 hash is persisted). The "htca-v1-" prefix is a leak-scan signature so a key accidentally pasted to a public log can be detected by tools that match literal substrings.
Why SHA-256, not bcrypt: the secret already carries 128 bits of uniform entropy from crypto/rand. bcrypt's slow KDF is for low- entropy passwords; using it here would only add latency to every authenticated request. We do constant-time compare on the hash to avoid timing-leak shenanigans even though the security model doesn't strictly require it for cryptographically random secrets.
Index ¶
Constants ¶
const Prefix = "htca-v1-"
Prefix is the leak-scan signature that every key carries. Operators can grep logs / public sources for this literal to find leaks.
Variables ¶
var ( ErrMalformed = errors.New("apikey: malformed key (expected htca-v1-<key_id>-<secret>)") ErrBadPrefix = errors.New("apikey: missing htca-v1- prefix") ErrBadComponent = errors.New("apikey: key_id or secret is not hex / wrong length") ErrSecretInvalid = errors.New("apikey: secret does not match stored hash") )
Errors callers should test for. Don't cmp the error string; errors.Is on these sentinels is the contract.
Functions ¶
func LooksLikeKey ¶
LooksLikeKey is a pre-flight check the auth handler can use to decide "is this Bearer token an API key, or do I try other methods?". Cheaper than Parse and only inspects the prefix.
Types ¶
type Minted ¶
type Minted struct {
// Full is the wire-format string (htca-v1-<key_id>-<secret>).
// Show this to the user exactly once; we never recover it.
Full string
// KeyID is the indexable lookup column. Safe to log / display.
KeyID string
// SecretHash is the hex SHA-256 of the secret half. Safe to
// store at rest.
SecretHash string
}
Minted is the result of Mint: the full wire-format string the caller MUST hand to the user once (and only once), plus the derived key_id and secret_hash that go into the DB.
func Mint ¶
Mint generates a fresh API key. Returns the wire-format string AND the (key_id, secret_hash) pair to persist. The wire-format string is NOT recoverable from anything we keep — show it once.
Mint reads from crypto/rand; failures bubble up with errors that embed the operation. Real-world failure of crypto/rand is catastrophic and rare enough that we don't try to retry.
type Parsed ¶
type Parsed struct {
// KeyID is the lookup column. Use it to fetch the row.
KeyID string
// contains filtered or unexported fields
}
Parsed is the result of Parse: the components of a presented key. We intentionally don't expose the secret bytes — callers verify against a stored hash via VerifySecret, which means the secret only lives on the request stack, never in any longer-term structure.
func Parse ¶
Parse extracts the key_id and the secret from a presented wire- format string. Returns ErrMalformed / ErrBadPrefix / ErrBadComponent for invalid inputs; never panics. Callers should treat ErrBadPrefix as "this isn't an API key, try other auth methods" — only ErrMalformed / ErrBadComponent are "this LOOKED like a key but was invalid".
func (Parsed) VerifySecret ¶
VerifySecret returns nil iff p.secret hashes to expectedHash. Uses constant-time compare on the hash byte sequence so a (theoretical) timing oracle can't reveal which bytes matched.
expectedHash should be the hex-encoded SHA-256 stored in the DB. We re-hash p.secret here rather than letting callers do it so the "compare hashes" path is always constant-time and there's no way to accidentally `==` two strings.