// Package auth provides API key authentication for rdev. package auth import ( "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "strings" "time" ) const ( // KeyPrefix is the prefix for all rdev API keys. KeyPrefix = "rdev_sk_" // KeyRandomLength is the length of the random portion of the key. KeyRandomLength = 32 // KeyIdentifierLength is the length of the identifier portion. KeyIdentifierLength = 8 ) // Expiration durations for API keys. var ( Expiration30Days = 30 * 24 * time.Hour Expiration60Days = 60 * 24 * time.Hour Expiration90Days = 90 * 24 * time.Hour Expiration1Year = 365 * 24 * time.Hour ExpirationNoLimit time.Duration = 0 ) // ParseExpiration converts an expiration string to duration. // Valid values: "30d", "60d", "90d", "1y", "never" func ParseExpiration(s string) (time.Duration, error) { switch strings.ToLower(s) { case "30d", "30": return Expiration30Days, nil case "60d", "60": return Expiration60Days, nil case "90d", "90": return Expiration90Days, nil case "1y", "1year", "365d": return Expiration1Year, nil case "never", "none", "": return ExpirationNoLimit, nil default: return 0, fmt.Errorf("invalid expiration: %s (use 30d, 60d, 90d, 1y, or never)", s) } } // ExpiresAt calculates the expiration timestamp from a duration. // Returns nil if duration is 0 (no expiration). func ExpiresAt(d time.Duration) *time.Time { if d == 0 { return nil } t := time.Now().Add(d) return &t } // GenerateKey creates a new API key with format: rdev_sk__ // Returns the full key (to show once) and the identifier (for storage). func GenerateKey() (fullKey, identifier string, err error) { // Generate identifier (8 chars) idBytes := make([]byte, KeyIdentifierLength/2) if _, err := rand.Read(idBytes); err != nil { return "", "", fmt.Errorf("generate identifier: %w", err) } identifier = hex.EncodeToString(idBytes) // Generate random portion (32 chars) randomBytes := make([]byte, KeyRandomLength/2) if _, err := rand.Read(randomBytes); err != nil { return "", "", fmt.Errorf("generate random: %w", err) } random := hex.EncodeToString(randomBytes) fullKey = fmt.Sprintf("%s%s_%s", KeyPrefix, identifier, random) return fullKey, identifier, nil } // HashKey computes the SHA-256 hash of an API key. func HashKey(key string) string { h := sha256.Sum256([]byte(key)) return hex.EncodeToString(h[:]) } // ValidateKeyFormat checks if a key has the correct format. func ValidateKeyFormat(key string) bool { if !strings.HasPrefix(key, KeyPrefix) { return false } // Format: rdev_sk_<8-char-id>_<32-char-random> parts := strings.Split(strings.TrimPrefix(key, KeyPrefix), "_") if len(parts) != 2 { return false } if len(parts[0]) != KeyIdentifierLength { return false } if len(parts[1]) != KeyRandomLength { return false } return true } // ExtractPrefix extracts the identifier prefix from a key. func ExtractPrefix(key string) string { if !strings.HasPrefix(key, KeyPrefix) { return "" } trimmed := strings.TrimPrefix(key, KeyPrefix) parts := strings.Split(trimmed, "_") if len(parts) < 1 { return "" } return parts[0] }