apikey

package
v0.0.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 14, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

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

View Source
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

View Source
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

func LooksLikeKey(s string) bool

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

func Mint() (Minted, error)

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

func Parse(s string) (Parsed, error)

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

func (p Parsed) VerifySecret(expectedHash string) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL