org
This commit is contained in:
261
kms/internal/crypto/token.go
Normal file
261
kms/internal/crypto/token.go
Normal file
@ -0,0 +1,261 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// TokenLength defines the length of generated tokens in bytes
|
||||
TokenLength = 32
|
||||
// TokenPrefix is prepended to all tokens for identification
|
||||
TokenPrefix = "kms_"
|
||||
// BcryptCost defines the bcrypt cost for 2025 security standards (minimum 14)
|
||||
BcryptCost = 14
|
||||
)
|
||||
|
||||
// TokenGenerator provides secure token generation and validation
|
||||
type TokenGenerator struct {
|
||||
hmacKey []byte
|
||||
bcryptCost int
|
||||
}
|
||||
|
||||
// NewTokenGenerator creates a new token generator with the provided HMAC key
|
||||
func NewTokenGenerator(hmacKey string) *TokenGenerator {
|
||||
return &TokenGenerator{
|
||||
hmacKey: []byte(hmacKey),
|
||||
bcryptCost: BcryptCost,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTokenGeneratorWithCost creates a new token generator with custom bcrypt cost
|
||||
func NewTokenGeneratorWithCost(hmacKey string, bcryptCost int) *TokenGenerator {
|
||||
// Validate bcrypt cost (must be between 4 and 31)
|
||||
if bcryptCost < 4 {
|
||||
bcryptCost = 4
|
||||
} else if bcryptCost > 31 {
|
||||
bcryptCost = 31
|
||||
}
|
||||
|
||||
// Warn if cost is too low for production
|
||||
if bcryptCost < 12 {
|
||||
// This should log a warning, but we don't have logger here
|
||||
// In a real implementation, you'd pass a logger or use a global one
|
||||
}
|
||||
|
||||
return &TokenGenerator{
|
||||
hmacKey: []byte(hmacKey),
|
||||
bcryptCost: bcryptCost,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateSecureToken generates a cryptographically secure random token
|
||||
func (tg *TokenGenerator) GenerateSecureToken() (string, error) {
|
||||
return tg.GenerateSecureTokenWithPrefix("", "")
|
||||
}
|
||||
|
||||
// GenerateSecureTokenWithPrefix generates a cryptographically secure random token with custom prefix
|
||||
func (tg *TokenGenerator) GenerateSecureTokenWithPrefix(appPrefix string, tokenType string) (string, error) {
|
||||
// Generate random bytes
|
||||
tokenBytes := make([]byte, TokenLength)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random token: %w", err)
|
||||
}
|
||||
|
||||
// Encode to base64 for safe transmission
|
||||
tokenData := base64.URLEncoding.EncodeToString(tokenBytes)
|
||||
|
||||
// Build prefix based on application and token type
|
||||
var prefix string
|
||||
if appPrefix != "" {
|
||||
// Use custom application prefix
|
||||
if tokenType == "user" {
|
||||
prefix = appPrefix + "UT-" // User Token
|
||||
} else {
|
||||
prefix = appPrefix + "T-" // Static Token
|
||||
}
|
||||
} else {
|
||||
// Use default prefix
|
||||
prefix = TokenPrefix
|
||||
}
|
||||
|
||||
token := prefix + tokenData
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// HashToken creates a secure hash of the token for storage
|
||||
func (tg *TokenGenerator) HashToken(token string) (string, error) {
|
||||
// Use bcrypt with configured cost
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(token), tg.bcryptCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash token with bcrypt cost %d: %w", tg.bcryptCost, err)
|
||||
}
|
||||
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// VerifyToken verifies a token against its stored hash
|
||||
func (tg *TokenGenerator) VerifyToken(token, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(token))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GenerateHMACKey generates a new HMAC key for token signing
|
||||
func GenerateHMACKey() (string, error) {
|
||||
key := make([]byte, 32) // 256-bit key
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", fmt.Errorf("failed to generate HMAC key: %w", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// SignToken creates an HMAC signature for a token
|
||||
func (tg *TokenGenerator) SignToken(token string, timestamp time.Time) string {
|
||||
h := hmac.New(sha256.New, tg.hmacKey)
|
||||
h.Write([]byte(token))
|
||||
h.Write([]byte(timestamp.Format(time.RFC3339)))
|
||||
|
||||
signature := h.Sum(nil)
|
||||
return hex.EncodeToString(signature)
|
||||
}
|
||||
|
||||
// VerifyTokenSignature verifies an HMAC signature for a token
|
||||
func (tg *TokenGenerator) VerifyTokenSignature(token, signature string, timestamp time.Time) bool {
|
||||
expectedSignature := tg.SignToken(token, timestamp)
|
||||
return hmac.Equal([]byte(signature), []byte(expectedSignature))
|
||||
}
|
||||
|
||||
// ExtractTokenFromHeader extracts a token from an Authorization header
|
||||
func ExtractTokenFromHeader(authHeader string) string {
|
||||
// Support both "Bearer token" and "token" formats
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return strings.TrimPrefix(authHeader, "Bearer ")
|
||||
}
|
||||
return authHeader
|
||||
}
|
||||
|
||||
// IsValidTokenFormat checks if a token has the expected format
|
||||
func IsValidTokenFormat(token string) bool {
|
||||
return IsValidTokenFormatWithPrefix(token, "")
|
||||
}
|
||||
|
||||
// IsValidTokenFormatWithPrefix checks if a token has the expected format with custom prefix
|
||||
func IsValidTokenFormatWithPrefix(token string, expectedPrefix string) bool {
|
||||
var prefix string
|
||||
if expectedPrefix != "" {
|
||||
prefix = expectedPrefix
|
||||
} else {
|
||||
prefix = TokenPrefix
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(token, prefix) {
|
||||
// If expected prefix doesn't match, check if it's a valid token with any custom prefix
|
||||
if expectedPrefix == "" {
|
||||
// Check for custom prefix pattern: 2-4 uppercase letters + "T-" or "UT-"
|
||||
if len(token) < 6 { // minimum: "ABT-" + some data
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for T- or UT- suffix in the first part
|
||||
dashIndex := strings.Index(token, "-")
|
||||
if dashIndex < 2 || dashIndex > 6 { // 2-4 chars + "T" or "UT"
|
||||
// Not a custom prefix, check default
|
||||
if !strings.HasPrefix(token, TokenPrefix) {
|
||||
return false
|
||||
}
|
||||
prefix = TokenPrefix
|
||||
} else {
|
||||
prefixPart := token[:dashIndex+1]
|
||||
if !strings.HasSuffix(prefixPart, "T-") && !strings.HasSuffix(prefixPart, "UT-") {
|
||||
if !strings.HasPrefix(token, TokenPrefix) {
|
||||
return false
|
||||
}
|
||||
prefix = TokenPrefix
|
||||
} else {
|
||||
prefix = prefixPart
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Remove prefix and check if remaining part is valid base64
|
||||
tokenData := strings.TrimPrefix(token, prefix)
|
||||
if len(tokenData) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to decode base64
|
||||
_, err := base64.URLEncoding.DecodeString(tokenData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// TokenInfo holds information about a token
|
||||
type TokenInfo struct {
|
||||
Token string
|
||||
Hash string
|
||||
Signature string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// GenerateTokenWithInfo generates a complete token with hash and signature
|
||||
func (tg *TokenGenerator) GenerateTokenWithInfo() (*TokenInfo, error) {
|
||||
return tg.GenerateTokenWithInfoAndPrefix("", "")
|
||||
}
|
||||
|
||||
// GenerateTokenWithInfoAndPrefix generates a complete token with hash, signature, and custom prefix
|
||||
func (tg *TokenGenerator) GenerateTokenWithInfoAndPrefix(appPrefix string, tokenType string) (*TokenInfo, error) {
|
||||
// Generate the token
|
||||
token, err := tg.GenerateSecureTokenWithPrefix(appPrefix, tokenType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
// Hash the token for storage
|
||||
hash, err := tg.HashToken(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash token: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamp and signature
|
||||
now := time.Now()
|
||||
signature := tg.SignToken(token, now)
|
||||
|
||||
return &TokenInfo{
|
||||
Token: token,
|
||||
Hash: hash,
|
||||
Signature: signature,
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateTokenInfo validates a complete token with all its components
|
||||
func (tg *TokenGenerator) ValidateTokenInfo(token, hash, signature string, createdAt time.Time) error {
|
||||
// Check token format
|
||||
if !IsValidTokenFormat(token) {
|
||||
return fmt.Errorf("invalid token format")
|
||||
}
|
||||
|
||||
// Verify token against hash
|
||||
if !tg.VerifyToken(token, hash) {
|
||||
return fmt.Errorf("token verification failed")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if !tg.VerifyTokenSignature(token, signature, createdAt) {
|
||||
return fmt.Errorf("token signature verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user