2023-04-27 07:09:10 +02:00
|
|
|
package hash
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/base64"
|
2025-11-20 22:11:17 +01:00
|
|
|
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
2023-04-27 07:09:10 +02:00
|
|
|
)
|
|
|
|
|
|
2025-11-20 22:11:17 +01:00
|
|
|
// String creates a SHA256 hash of a string (legacy, for non-sensitive data)
|
2023-04-27 07:09:10 +02:00
|
|
|
func String(s string) string {
|
|
|
|
|
encoded := sha256.New().Sum([]byte(s))
|
|
|
|
|
return base64.StdEncoding.EncodeToString(encoded)
|
|
|
|
|
}
|
2025-11-20 22:11:17 +01:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|