monorepo/cloud/maplepress-backend/pkg/security/apikey/generator.go

96 lines
2.4 KiB
Go

package apikey
import (
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
)
const (
// PrefixLive is the prefix for production API keys
PrefixLive = "live_sk_"
// PrefixTest is the prefix for test/sandbox API keys
PrefixTest = "test_sk_"
// KeyLength is the length of the random part (40 chars in base64url)
KeyLength = 30 // 30 bytes = 40 base64url chars
)
// Generator generates API keys
type Generator interface {
// Generate creates a new live API key
Generate() (string, error)
// GenerateTest creates a new test API key
GenerateTest() (string, error)
}
type generator struct{}
// NewGenerator creates a new API key generator
func NewGenerator() Generator {
return &generator{}
}
// Generate creates a new live API key
func (g *generator) Generate() (string, error) {
return g.generateWithPrefix(PrefixLive)
}
// GenerateTest creates a new test API key
func (g *generator) GenerateTest() (string, error) {
return g.generateWithPrefix(PrefixTest)
}
func (g *generator) generateWithPrefix(prefix string) (string, error) {
// Generate cryptographically secure random bytes
b := make([]byte, KeyLength)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Encode to base64url (URL-safe, no padding)
key := base64.RawURLEncoding.EncodeToString(b)
// Remove any special chars and make lowercase for consistency
key = strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return r
}
return -1 // Remove character
}, key)
// Ensure we have at least 40 characters
if len(key) < 40 {
// Pad with additional random bytes if needed
additional := make([]byte, 10)
rand.Read(additional)
extraKey := base64.RawURLEncoding.EncodeToString(additional)
key += extraKey
}
// Trim to exactly 40 characters
key = key[:40]
return prefix + key, nil
}
// ExtractPrefix extracts the prefix from an API key
func ExtractPrefix(apiKey string) string {
if len(apiKey) < 13 {
return ""
}
return apiKey[:13] // "live_sk_a1b2" or "test_sk_a1b2"
}
// ExtractLastFour extracts the last 4 characters from an API key
func ExtractLastFour(apiKey string) string {
if len(apiKey) < 4 {
return ""
}
return apiKey[len(apiKey)-4:]
}
// IsValid checks if an API key has a valid format
func IsValid(apiKey string) bool {
return strings.HasPrefix(apiKey, PrefixLive) || strings.HasPrefix(apiKey, PrefixTest)
}