package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "fmt" "strings" ) // TokenPrefixLen is how many characters of a token are stored in the clear to // narrow the database lookup. It is not a secret: it only identifies which row // to compare against. const TokenPrefixLen = 8 // tokenLabel prefixes every issued token so a leaked string is recognisable in // logs and secret scanners. const tokenLabel = "vibedns_" // Token is a freshly minted API credential. type Token struct { Secret string // shown to the operator exactly once Prefix string // stored in the clear, used to find the row Hash string // stored, never reversible } // GenerateToken creates a 256-bit API token. func GenerateToken() (Token, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return Token{}, fmt.Errorf("generate API token: %w", err) } body := base64.RawURLEncoding.EncodeToString(buf) secret := tokenLabel + body return Token{ Secret: secret, Prefix: body[:TokenPrefixLen], Hash: HashToken(secret), }, nil } // HashToken hashes an API token with SHA-256. // // Unlike a human-chosen password, an API token is 256 bits of output from a // CSPRNG, so there is no low-entropy guess space for an attacker to search: a // fast hash is sufficient and, unlike Argon2id, can be computed on every API // request without adding tens of milliseconds and 64 MiB of allocation to each // one. func HashToken(secret string) string { sum := sha256.Sum256([]byte(secret)) return hex.EncodeToString(sum[:]) } // TokenPrefix extracts the lookup prefix from a presented token. func TokenPrefix(secret string) (string, error) { body := strings.TrimPrefix(secret, tokenLabel) if len(body) < TokenPrefixLen { return "", errors.New("API token is malformed") } return body[:TokenPrefixLen], nil } // VerifyToken compares a presented token against a stored hash in constant // time. func VerifyToken(storedHash, secret string) bool { got := HashToken(secret) return subtle.ConstantTimeCompare([]byte(got), []byte(storedHash)) == 1 } // RandomKey returns n cryptographically random bytes, base64 encoded. It backs // the CSRF signing key. func RandomKey(n int) (string, error) { buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", fmt.Errorf("generate random key: %w", err) } return base64.RawStdEncoding.EncodeToString(buf), nil }