package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "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) { var version int var memory, time uint32 var threads uint8 var salt, hash []byte _, err := fmt.Sscanf(encodedHash, "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", &version, &memory, &time, &threads, &salt, &hash) if err != nil { return false, fmt.Errorf("parse hash: %w", err) } decodedSalt, err := base64.RawStdEncoding.DecodeString(string(salt)) if err != nil { return false, fmt.Errorf("decode salt: %w", err) } decodedHash, err := base64.RawStdEncoding.DecodeString(string(hash)) 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 }