fetch_ml/internal/auth/crypto.go
Jeremie Fraeys 23e5f3d1dc
refactor(api): internal refactoring for TUI and worker modules
- Refactor internal/worker and internal/queue packages
- Update cmd/tui for monitoring interface
- Update test configurations
2026-02-20 15:51:23 -05:00

146 lines
4.2 KiB
Go

// 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
}