83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
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
|
|
}
|