authkit initial
This commit is contained in:
parent
5173b0a43d
commit
134393fbca
43 changed files with 5188 additions and 1 deletions
67
tokens.go
Normal file
67
tokens.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue