initial commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user