// Package auth provides authentication and authorization functionality package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "golang.org/x/crypto/argon2" ) // HashAlgorithm identifies the hashing algorithm used for API keys type HashAlgorithm string const ( // HashAlgorithmSHA256 is the legacy SHA256 hashing (deprecated, for backward compatibility) HashAlgorithmSHA256 HashAlgorithm = "sha256" // HashAlgorithmArgon2id is the modern Argon2id hashing (recommended) HashAlgorithmArgon2id HashAlgorithm = "argon2id" ) // Argon2id parameters (OWASP recommended minimum) const ( argon2idTime = 1 // Iterations argon2idMemory = 64 * 1024 // 64 MB argon2idThreads = 4 // Parallelism argon2idKeyLen = 32 // 256-bit output argon2idSaltLen = 16 // 128-bit salt ) // HashedKey represents a hashed API key with its algorithm and salt type HashedKey struct { Hash string `json:"hash"` Salt string `json:"salt,omitempty"` Algorithm HashAlgorithm `json:"algorithm"` } // HashAPIKeyArgon2id creates an Argon2id hash of an API key // This is the recommended hashing method for new keys func HashAPIKeyArgon2id(key string) (*HashedKey, error) { // Generate random salt salt := make([]byte, argon2idSaltLen) if _, err := rand.Read(salt); err != nil { return nil, fmt.Errorf("failed to generate salt: %w", err) } // Hash with Argon2id hash := argon2.IDKey( []byte(key), salt, argon2idTime, argon2idMemory, argon2idThreads, argon2idKeyLen, ) return &HashedKey{ Hash: hex.EncodeToString(hash), Salt: hex.EncodeToString(salt), Algorithm: HashAlgorithmArgon2id, }, nil } // HashAPIKey creates a legacy SHA256 hash of an API key // Deprecated: Use HashAPIKeyArgon2id for new keys func HashAPIKey(key string) string { hash := sha256.Sum256([]byte(key)) return hex.EncodeToString(hash[:]) } // VerifyAPIKey verifies an API key against a stored hash // Supports both Argon2id (preferred) and SHA256 (legacy) for backward compatibility func VerifyAPIKey(key string, stored *HashedKey) (bool, error) { switch stored.Algorithm { case HashAlgorithmArgon2id: return verifyArgon2id(key, stored) case HashAlgorithmSHA256, "": // Empty string treated as legacy SHA256 return verifySHA256(key, stored.Hash), nil default: return false, fmt.Errorf("unsupported hash algorithm: %s", stored.Algorithm) } } // verifyArgon2id verifies a key against an Argon2id hash func verifyArgon2id(key string, stored *HashedKey) (bool, error) { // Decode salt salt, err := hex.DecodeString(stored.Salt) if err != nil { return false, fmt.Errorf("invalid salt encoding: %w", err) } // Compute hash with same parameters computedHash := argon2.IDKey( []byte(key), salt, argon2idTime, argon2idMemory, argon2idThreads, argon2idKeyLen, ) // Decode stored hash storedHash, err := hex.DecodeString(stored.Hash) if err != nil { return false, fmt.Errorf("invalid hash encoding: %w", err) } // Constant-time comparison to prevent timing attacks return subtle.ConstantTimeCompare(computedHash, storedHash) == 1, nil } // verifySHA256 verifies a key against a legacy SHA256 hash func verifySHA256(key, storedHash string) bool { computedHash := HashAPIKey(key) return subtle.ConstantTimeCompare([]byte(computedHash), []byte(storedHash)) == 1 } // GenerateAPIKey generates a new random API key // Returns the plaintext key (to be shown once) and its Argon2id hash func GenerateAPIKey() (plaintext string, hashed *HashedKey, err error) { // Generate 32 bytes of randomness (256 bits) buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", nil, fmt.Errorf("failed to generate API key: %w", err) } plaintext = hex.EncodeToString(buf) hashed, err = HashAPIKeyArgon2id(plaintext) if err != nil { return "", nil, err } return plaintext, hashed, nil } // GenerateAPISalt creates a random salt for API key hashing // Primarily used for testing and migration scenarios func GenerateAPISalt() ([]byte, error) { salt := make([]byte, argon2idSaltLen) if _, err := rand.Read(salt); err != nil { return nil, fmt.Errorf("failed to generate salt: %w", err) } return salt, nil }