package utils import ( "crypto/rand" "crypto/subtle" "encoding/hex" "errors" "os" "strings" "sync" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // ── UID ─────────────────────────────────────────────────────────────────────── func NewUID() string { raw := strings.ReplaceAll(uuid.New().String(), "-", "") return "cos_" + raw[:16] } // ── Password ────────────────────────────────────────────────────────────────── func HashPassword(pw string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(pw), 12) return string(b), err } func CheckPassword(hash, pw string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil } // ── JWT ─────────────────────────────────────────────────────────────────────── const jwtTTL = 7 * 24 * time.Hour type Claims struct { UID string `json:"uid"` Email string `json:"email"` Provider string `json:"provider"` jwt.RegisteredClaims } func SignJWT(uid, email, provider string) (string, error) { secret := os.Getenv("JWT_SECRET") if secret == "" { return "", errors.New("JWT_SECRET not set") } c := Claims{ UID: uid, Email: email, Provider: provider, RegisteredClaims: jwt.RegisteredClaims{ Subject: uid, IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtTTL)), }, } return jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString([]byte(secret)) } func VerifyJWT(tokenStr string) (*Claims, error) { secret := os.Getenv("JWT_SECRET") if secret == "" { return nil, errors.New("JWT_SECRET not set") } t, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("unexpected signing method") } return []byte(secret), nil }) if err != nil { return nil, err } c, ok := t.Claims.(*Claims) if !ok || !t.Valid { return nil, errors.New("invalid token") } return c, nil } // ── Constant-time key comparison ────────────────────────────────────────────── // KeyMatch compares two API keys in constant time to prevent timing attacks. func KeyMatch(a, b string) bool { if len(a) == 0 || len(b) == 0 { return false } return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } // ── Nonce store (auth pipe challenge) ───────────────────────────────────────── const nonceTTL = 15 * time.Minute // full pipeline window type nonceEntry struct{ exp time.Time } var ( nonceMu sync.Mutex nonceStore = map[string]nonceEntry{} ) func NewNonce() string { b := make([]byte, 24) _, _ = rand.Read(b) n := hex.EncodeToString(b) nonceMu.Lock() nonceStore[n] = nonceEntry{exp: time.Now().Add(nonceTTL)} nonceMu.Unlock() go sweepNonces() return n } func ClaimNonce(n string) bool { nonceMu.Lock() defer nonceMu.Unlock() e, ok := nonceStore[n] if !ok { return false } delete(nonceStore, n) return time.Now().Before(e.exp) } func sweepNonces() { nonceMu.Lock() defer nonceMu.Unlock() now := time.Now() for k, v := range nonceStore { if now.After(v.exp) { delete(nonceStore, k) } } }