authkit initial

This commit is contained in:
juancwu 2026-04-26 01:36:53 +00:00
commit 134393fbca
43 changed files with 5188 additions and 1 deletions

67
tokens.go Normal file
View file

@ -0,0 +1,67 @@
package authkit
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"io"
"strings"
"git.juancwu.dev/juancwu/errx"
)
const secretRandomBytes = 32
// Secret prefixes. The leading token namespaces the secret so callers can
// route them to the right verifier without trial-and-error.
const (
prefixSession = "sess"
prefixRefresh = "rfr"
prefixAPIKey = "ak"
prefixEmailVerify = "evr"
prefixPasswordRset = "pwr"
prefixMagicLink = "mlnk"
)
// mintSecret generates a new opaque secret of the given prefix kind and
// returns the plaintext (to be returned to the user, never stored) and the
// SHA-256 lookup hash (to be stored).
func mintSecret(prefix string, rng io.Reader) (plaintext string, hash []byte, err error) {
const op = "authkit.mintSecret"
if rng == nil {
rng = rand.Reader
}
buf := make([]byte, secretRandomBytes)
if _, err := io.ReadFull(rng, buf); err != nil {
return "", nil, errx.Wrap(op, err)
}
body := base64.RawURLEncoding.EncodeToString(buf)
plaintext = prefix + "_" + body
hash = hashSecret(plaintext)
return plaintext, hash, nil
}
// hashSecret returns sha256(plaintext) — the lookup key for opaque secrets.
// Plaintexts have full entropy from a CSPRNG so a plain hash is sufficient
// (no per-record salt needed; the random body is the salt).
func hashSecret(plaintext string) []byte {
sum := sha256.Sum256([]byte(plaintext))
return sum[:]
}
// parseSecret validates that a plaintext starts with the expected prefix
// and returns the lookup hash. The prefix check is constant-time relative
// to a fixed-length comparison.
func parseSecret(prefix, plaintext string) (hash []byte, ok bool) {
want := prefix + "_"
if !strings.HasPrefix(plaintext, want) {
return nil, false
}
return hashSecret(plaintext), true
}
// constantTimeEqual is a thin wrapper for readability at call sites.
func constantTimeEqual(a, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) == 1
}