package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" ) type passwordConfig struct { time uint32 memory uint32 threads uint8 keyLen uint32 saltLen uint32 } var defaultParams = passwordConfig{ time: 3, memory: 64 * 1024, threads: 4, keyLen: 32, saltLen: 16, } // HashPassword returns an Argon2id encoded string. func HashPassword(password string) (string, error) { salt := make([]byte, defaultParams.saltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("generate salt: %w", err) } hash := argon2.IDKey([]byte(password), salt, defaultParams.time, defaultParams.memory, defaultParams.threads, defaultParams.keyLen) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, defaultParams.memory, defaultParams.time, defaultParams.threads, b64Salt, b64Hash) return encodedHash, nil } // VerifyPassword compares a password against an encoded Argon2id hash. func VerifyPassword(password, encodedHash string) (bool, error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 { return false, fmt.Errorf("invalid hash format") } var version int _, err := fmt.Sscanf(parts[2], "v=%d", &version) if err != nil { return false, fmt.Errorf("parse version: %w", err) } var memory, time uint32 var threads uint8 _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads) if err != nil { return false, fmt.Errorf("parse params: %w", err) } decodedSalt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, fmt.Errorf("decode salt: %w", err) } decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, fmt.Errorf("decode hash: %w", err) } computed := argon2.IDKey([]byte(password), decodedSalt, time, memory, threads, uint32(len(decodedHash))) if subtle.ConstantTimeCompare(computed, decodedHash) == 1 { return true, nil } return false, nil }