2820823b36
Add an optional API-key layer to the public read-only API. Keys grant higher per-minute rate limits and attribute usage; anonymous callers are still allowed at a lower IP-based budget. - migration 0008_api_key: api_key table (sha256 hash only, plaintext shown once) - apikey pkg: key generation + hashing - ratelimit pkg: Redis fixed-window limiter + per-key usage counters; fails open - public API middleware: X-API-Key / Bearer auth, X-RateLimit-* headers, 429+Retry-After - admin: issue/list/revoke keys + usage view (API + UI tab) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
50 lines
1.6 KiB
Go
50 lines
1.6 KiB
Go
// Package apikey handles generation and hashing of public-API keys.
|
|
//
|
|
// A key looks like "og_live_<random>". Only the SHA-256 hash is ever persisted;
|
|
// the plaintext is returned once at creation time and cannot be recovered.
|
|
package apikey
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"strings"
|
|
)
|
|
|
|
// Prefix is the human-readable scheme prefix every key carries.
|
|
const Prefix = "og_live_"
|
|
|
|
// prefixLen is how many leading characters (including Prefix) are stored in
|
|
// api_key.key_prefix for identifying a key without revealing its secret.
|
|
const prefixLen = 12
|
|
|
|
// Generate returns a new random key (plaintext), its SHA-256 hash, and a short
|
|
// display prefix. The plaintext must be shown to the caller exactly once.
|
|
func Generate() (key, hash, displayPrefix string, err error) {
|
|
buf := make([]byte, 24)
|
|
if _, err = rand.Read(buf); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
// URL-safe, no padding => stable, copy-pasteable token body.
|
|
body := base64.RawURLEncoding.EncodeToString(buf)
|
|
key = Prefix + body
|
|
hash = Hash(key)
|
|
displayPrefix = key
|
|
if len(displayPrefix) > prefixLen {
|
|
displayPrefix = displayPrefix[:prefixLen]
|
|
}
|
|
return key, hash, displayPrefix, nil
|
|
}
|
|
|
|
// Hash returns the hex-encoded SHA-256 of a key, used for storage and lookup.
|
|
func Hash(key string) string {
|
|
sum := sha256.Sum256([]byte(strings.TrimSpace(key)))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// Looks like a key issued by this service (cheap pre-check before hashing).
|
|
func IsWellFormed(key string) bool {
|
|
return strings.HasPrefix(key, Prefix) && len(key) > len(Prefix)+8
|
|
}
|