117 lines
2.7 KiB
Go
117 lines
2.7 KiB
Go
package lib
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
tokenPrefix = "cvt_"
|
|
tokenBytes = 32 // 8 bytes L1 + 24 bytes random
|
|
)
|
|
|
|
// base62 alphabet (digits + lowercase + uppercase)
|
|
const base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
// MintToken creates a new agent bearer token embedding the L1 key.
|
|
// Returns the raw token (shown once) and its sha256 hex hash (stored in DB).
|
|
// Token format: cvt_ + base62(l1Raw[8] + random[24])
|
|
func MintToken(l1Raw []byte) (raw string, hash string) {
|
|
buf := make([]byte, tokenBytes)
|
|
copy(buf[:8], l1Raw)
|
|
rand.Read(buf[8:])
|
|
|
|
raw = tokenPrefix + base62Encode(buf)
|
|
hash = HashToken(raw)
|
|
return
|
|
}
|
|
|
|
// ParseToken extracts the L1 key (8 bytes, raw) from a cvt_ bearer token.
|
|
// Returns l1Raw and the token hash for agent lookup.
|
|
func ParseToken(raw string) (l1Raw []byte, hash string, err error) {
|
|
if !strings.HasPrefix(raw, tokenPrefix) {
|
|
return nil, "", fmt.Errorf("missing cvt_ prefix")
|
|
}
|
|
decoded, err := base62Decode(strings.TrimPrefix(raw, tokenPrefix))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("invalid token encoding: %w", err)
|
|
}
|
|
if len(decoded) != tokenBytes {
|
|
return nil, "", fmt.Errorf("invalid token length: got %d, want %d", len(decoded), tokenBytes)
|
|
}
|
|
l1Raw = decoded[:8]
|
|
hash = HashToken(raw)
|
|
return
|
|
}
|
|
|
|
// HashToken returns the sha256 hex digest of a raw token string.
|
|
func HashToken(raw string) string {
|
|
h := sha256.Sum256([]byte(raw))
|
|
return fmt.Sprintf("%x", h)
|
|
}
|
|
|
|
// base62Encode encodes bytes as a base62 string.
|
|
func base62Encode(data []byte) string {
|
|
n := new(big.Int).SetBytes(data)
|
|
base := big.NewInt(62)
|
|
zero := big.NewInt(0)
|
|
mod := new(big.Int)
|
|
|
|
var chars []byte
|
|
for n.Cmp(zero) > 0 {
|
|
n.DivMod(n, base, mod)
|
|
chars = append(chars, base62Chars[mod.Int64()])
|
|
}
|
|
|
|
// Preserve leading zeros
|
|
for _, b := range data {
|
|
if b != 0 {
|
|
break
|
|
}
|
|
chars = append(chars, base62Chars[0])
|
|
}
|
|
|
|
// Reverse
|
|
for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
|
|
chars[i], chars[j] = chars[j], chars[i]
|
|
}
|
|
|
|
return string(chars)
|
|
}
|
|
|
|
// base62Decode decodes a base62 string back to bytes.
|
|
func base62Decode(s string) ([]byte, error) {
|
|
n := new(big.Int)
|
|
base := big.NewInt(62)
|
|
|
|
for _, c := range s {
|
|
idx := strings.IndexRune(base62Chars, c)
|
|
if idx < 0 {
|
|
return nil, fmt.Errorf("invalid base62 character: %c", c)
|
|
}
|
|
n.Mul(n, base)
|
|
n.Add(n, big.NewInt(int64(idx)))
|
|
}
|
|
|
|
// Convert to fixed-size byte slice
|
|
b := n.Bytes()
|
|
|
|
// Count leading zeros in the encoded string
|
|
leadingZeros := 0
|
|
for _, c := range s {
|
|
if c == rune(base62Chars[0]) {
|
|
leadingZeros++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Prepend zero bytes
|
|
result := make([]byte, leadingZeros+len(b))
|
|
copy(result[leadingZeros:], b)
|
|
return result, nil
|
|
}
|