// Package password provides password hashing and verification using argon2id. package password import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "math" "strings" "golang.org/x/crypto/argon2" ) const ( // Default parameters for argon2id (OWASP recommended) memory = 64 * 1024 // 64 MB iterations = 3 parallelism = 4 saltLength = 16 keyLength = 32 ) // Hash hashes a password using argon2id. func Hash(password string) (string, error) { // Generate random salt salt := make([]byte, saltLength) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("failed to generate salt: %w", err) } // Hash password hash := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keyLength) // Encode salt and hash b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) // Return formatted hash: $argon2id$v=19$m=65536,t=3,p=4$salt$hash return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, memory, iterations, parallelism, b64Salt, b64Hash), nil } // Verify verifies a password against a hash. func Verify(password, hash string) (bool, error) { // Parse hash format: $argon2id$v=19$m=65536,t=3,p=4$salt$hash parts := strings.Split(hash, "$") if len(parts) != 6 { return false, errors.New("invalid hash format") } if parts[1] != "argon2id" { return false, fmt.Errorf("unsupported algorithm: %s", parts[1]) } // Parse version var version int if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { return false, fmt.Errorf("failed to parse version: %w", err) } // Parse parameters var m, t, p int if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p); err != nil { return false, fmt.Errorf("failed to parse parameters: %w", err) } // Decode salt and hash salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, fmt.Errorf("failed to decode salt: %w", err) } expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, fmt.Errorf("failed to decode hash: %w", err) } // Compute hash with same parameters hashLen := len(expectedHash) if hashLen < 0 || hashLen > math.MaxUint32 { return false, fmt.Errorf("invalid hash length: %d", hashLen) } var hashLenUint32 uint32 if hashLen > math.MaxUint32 { hashLenUint32 = math.MaxUint32 } else { hashLenUint32 = uint32(hashLen) } actualHash := argon2.IDKey([]byte(password), salt, uint32(t), uint32(m), uint8(p), hashLenUint32) // Constant-time comparison if subtle.ConstantTimeCompare(expectedHash, actualHash) == 1 { return true, nil } return false, nil }