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()) }