Files
schemas/hash/hash.go
T
argoyle 4468903535 feat(cache): implement hashed API key storage and retrieval
Adds a new hashed key storage mechanism for API keys in the cache. 
Replaces direct mapping to API keys with composite keys based on 
organizationId and name. Implements searching of API keys using 
hash comparisons for improved security. Updates related tests to 
ensure correct functionality and validate the hashing. Also, 
adds support for a new dependency `golang.org/x/crypto`.
2025-11-20 22:11:24 +01:00

75 lines
2.4 KiB
Go

package hash
import (
"crypto/sha256"
"encoding/base64"
"golang.org/x/crypto/bcrypt"
)
// String creates a SHA256 hash of a string (legacy, for non-sensitive data)
func String(s string) string {
encoded := sha256.New().Sum([]byte(s))
return base64.StdEncoding.EncodeToString(encoded)
}
// APIKey hashes an API key using bcrypt for secure storage
// Cost of 12 provides a good balance between security and performance
func APIKey(key string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(key), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
// CompareAPIKey compares a plaintext API key with a hash
// Supports both bcrypt (new) and SHA256 (legacy) hashes for backwards compatibility
// Returns true if they match, false otherwise
//
// Migration Strategy:
// Old API keys stored with SHA256 will continue to work. To upgrade them to bcrypt:
// 1. Keys are automatically upgraded when users re-authenticate (if implemented)
// 2. Or, run a one-time migration using MigrateAPIKeyHash when convenient
func CompareAPIKey(hashedKey, plainKey string) bool {
// Bcrypt hashes start with $2a$, $2b$, or $2y$
// If the hash starts with $2, it's a bcrypt hash
if len(hashedKey) > 2 && hashedKey[0] == '$' && hashedKey[1] == '2' {
// New bcrypt hash
err := bcrypt.CompareHashAndPassword([]byte(hashedKey), []byte(plainKey))
return err == nil
}
// Legacy SHA256 hash - compare using the old method
legacyHash := String(plainKey)
return hashedKey == legacyHash
}
// IsLegacyHash returns true if the hash is a legacy SHA256 hash (not bcrypt)
func IsLegacyHash(hashedKey string) bool {
return len(hashedKey) <= 2 || hashedKey[0] != '$' || hashedKey[1] != '2'
}
// MigrateAPIKeyHash can be used to upgrade a legacy SHA256 hash to bcrypt
// This is useful for one-time migrations of existing keys
// Returns the new bcrypt hash if the key is legacy, otherwise returns the original
func MigrateAPIKeyHash(currentHash, plainKey string) (string, bool, error) {
// If already bcrypt, no migration needed
if !IsLegacyHash(currentHash) {
return currentHash, false, nil
}
// Verify the legacy hash is correct before migrating
if !CompareAPIKey(currentHash, plainKey) {
return "", false, nil // Invalid key, don't migrate
}
// Generate new bcrypt hash
newHash, err := APIKey(plainKey)
if err != nil {
return "", false, err
}
return newHash, true, nil
}