188 lines
5.2 KiB
Go
188 lines
5.2 KiB
Go
package database
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
// Argon2 parameters - these are up-to-date secure defaults
|
|
const (
|
|
argon2Time = 1
|
|
argon2Memory = 64 * 1024 // 64 MB
|
|
argon2Threads = 4
|
|
argon2KeyLen = 32
|
|
argon2SaltLen = 16
|
|
)
|
|
|
|
// PasswordConfig holds Argon2 configuration
|
|
type PasswordConfig struct {
|
|
Time uint32
|
|
Memory uint32
|
|
Threads uint8
|
|
KeyLen uint32
|
|
SaltLen uint32
|
|
}
|
|
|
|
// DefaultPasswordConfig returns secure default Argon2 parameters
|
|
func DefaultPasswordConfig() *PasswordConfig {
|
|
return &PasswordConfig{
|
|
Time: argon2Time,
|
|
Memory: argon2Memory,
|
|
Threads: argon2Threads,
|
|
KeyLen: argon2KeyLen,
|
|
SaltLen: argon2SaltLen,
|
|
}
|
|
}
|
|
|
|
// HashPassword generates an Argon2id hash of the password
|
|
func HashPassword(password string) (string, error) {
|
|
config := DefaultPasswordConfig()
|
|
|
|
// Generate a salt
|
|
salt := make([]byte, config.SaltLen)
|
|
if _, err := rand.Read(salt); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Generate the hash
|
|
hash := argon2.IDKey([]byte(password), salt, config.Time, config.Memory, config.Threads, config.KeyLen)
|
|
|
|
// Encode the hash and parameters
|
|
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
|
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
|
|
|
// Format: $argon2id$v=19$m=65536,t=1,p=4$salt$hash
|
|
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
|
argon2.Version, config.Memory, config.Time, config.Threads, b64Salt, b64Hash)
|
|
|
|
return encoded, nil
|
|
}
|
|
|
|
// VerifyPassword checks if the provided password matches the hash
|
|
func VerifyPassword(password, encodedHash string) (bool, error) {
|
|
// Extract parameters and hash from encoded string
|
|
config, salt, hash, err := decodeHash(encodedHash)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Generate hash of the provided password
|
|
otherHash := argon2.IDKey([]byte(password), salt, config.Time, config.Memory, config.Threads, config.KeyLen)
|
|
|
|
// Compare hashes using constant time comparison
|
|
return subtle.ConstantTimeCompare(hash, otherHash) == 1, nil
|
|
}
|
|
|
|
// decodeHash extracts parameters, salt, and hash from an encoded hash string
|
|
func decodeHash(encodedHash string) (*PasswordConfig, []byte, []byte, error) {
|
|
parts := strings.Split(encodedHash, "$")
|
|
if len(parts) != 6 {
|
|
return nil, nil, nil, fmt.Errorf("invalid hash format")
|
|
}
|
|
|
|
if parts[1] != "argon2id" {
|
|
return nil, nil, nil, fmt.Errorf("invalid algorithm")
|
|
}
|
|
|
|
var version int
|
|
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if version != argon2.Version {
|
|
return nil, nil, nil, fmt.Errorf("incompatible argon2 version")
|
|
}
|
|
|
|
config := &PasswordConfig{}
|
|
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &config.Memory, &config.Time, &config.Threads); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
saltLen := len(salt)
|
|
if saltLen < 0 || saltLen > int(^uint32(0)) {
|
|
return nil, nil, nil, fmt.Errorf("salt length out of range")
|
|
}
|
|
config.SaltLen = uint32(saltLen) // nolint:gosec // checked above
|
|
|
|
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
hashLen := len(hash)
|
|
if hashLen < 0 || hashLen > int(^uint32(0)) {
|
|
return nil, nil, nil, fmt.Errorf("hash length out of range")
|
|
}
|
|
config.KeyLen = uint32(hashLen) // nolint:gosec // checked above
|
|
|
|
return config, salt, hash, nil
|
|
}
|
|
|
|
// GenerateRandomPassword generates a cryptographically secure random password
|
|
func GenerateRandomPassword(length int) (string, error) {
|
|
const (
|
|
uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
lowercase = "abcdefghijklmnopqrstuvwxyz"
|
|
digits = "0123456789"
|
|
special = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
|
)
|
|
|
|
// Combine all character sets
|
|
allChars := uppercase + lowercase + digits + special
|
|
|
|
// Create password slice
|
|
password := make([]byte, length)
|
|
|
|
// Ensure at least one character from each set for password complexity
|
|
if length >= 4 {
|
|
// Get one character from each set
|
|
password[0] = uppercase[cryptoRandInt(len(uppercase))]
|
|
password[1] = lowercase[cryptoRandInt(len(lowercase))]
|
|
password[2] = digits[cryptoRandInt(len(digits))]
|
|
password[3] = special[cryptoRandInt(len(special))]
|
|
|
|
// Fill the rest randomly from all characters
|
|
for i := 4; i < length; i++ {
|
|
password[i] = allChars[cryptoRandInt(len(allChars))]
|
|
}
|
|
|
|
// Shuffle the password to avoid predictable pattern
|
|
for i := len(password) - 1; i > 0; i-- {
|
|
j := cryptoRandInt(i + 1)
|
|
password[i], password[j] = password[j], password[i]
|
|
}
|
|
} else {
|
|
// For very short passwords, just use all characters
|
|
for i := 0; i < length; i++ {
|
|
password[i] = allChars[cryptoRandInt(len(allChars))]
|
|
}
|
|
}
|
|
|
|
return string(password), nil
|
|
}
|
|
|
|
// cryptoRandInt generates a cryptographically secure random integer in [0, max)
|
|
func cryptoRandInt(max int) int {
|
|
if max <= 0 {
|
|
panic("max must be positive")
|
|
}
|
|
|
|
// Calculate the maximum valid value to avoid modulo bias
|
|
// For example, if max=200 and we have 256 possible values,
|
|
// we only accept values 0-199 (reject 200-255)
|
|
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
|
if err != nil {
|
|
panic(fmt.Sprintf("crypto/rand error: %v", err))
|
|
}
|
|
|
|
return int(nBig.Int64())
|
|
}
|