- Refactor internal/worker and internal/queue packages - Update cmd/tui for monitoring interface - Update test configurations
146 lines
4.2 KiB
Go
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
|
|
}
|