// Package authn implements password hashing and high-entropy credential generation. package authn import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strconv" "strings" "golang.org/x/crypto/argon2" ) var ErrInvalidPasswordHash = errors.New("invalid password hash") type Argon2Parameters struct { MemoryKiB uint32 Time uint32 Threads uint8 SaltBytes uint32 KeyBytes uint32 } type PasswordHasher struct { parameters Argon2Parameters } func NewPasswordHasher() PasswordHasher { return PasswordHasher{parameters: Argon2Parameters{ MemoryKiB: 64 * 1024, Time: 3, Threads: 4, SaltBytes: 16, KeyBytes: 32, }} } func newPasswordHasher(parameters Argon2Parameters) PasswordHasher { return PasswordHasher{parameters: parameters} } func (h PasswordHasher) Hash(password string) (string, error) { salt := make([]byte, h.parameters.SaltBytes) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("generate password salt: %w", err) } derived := argon2.IDKey([]byte(password), salt, h.parameters.Time, h.parameters.MemoryKiB, h.parameters.Threads, h.parameters.KeyBytes) return fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, h.parameters.MemoryKiB, h.parameters.Time, h.parameters.Threads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(derived), ), nil } func (h PasswordHasher) Verify(password, encoded string) (bool, error) { parameters, salt, expected, err := parsePasswordHash(encoded) if err != nil { return false, err } actual := argon2.IDKey([]byte(password), salt, parameters.Time, parameters.MemoryKiB, parameters.Threads, uint32(len(expected))) return subtle.ConstantTimeCompare(actual, expected) == 1, nil } func parsePasswordHash(encoded string) (Argon2Parameters, []byte, []byte, error) { parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" { return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash } version, err := parseNamedUint(parts[2], "v", 8) if err != nil || version != argon2.Version { return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash } parameters, err := parseArgon2Parameters(parts[3]) if err != nil { return Argon2Parameters{}, nil, nil, err } salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4]) if err != nil || len(salt) < 16 || len(salt) > 64 { return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash } derived, err := base64.RawStdEncoding.Strict().DecodeString(parts[5]) if err != nil || len(derived) < 16 || len(derived) > 64 { return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash } parameters.SaltBytes = uint32(len(salt)) parameters.KeyBytes = uint32(len(derived)) return parameters, salt, derived, nil } func parseArgon2Parameters(value string) (Argon2Parameters, error) { parts := strings.Split(value, ",") if len(parts) != 3 { return Argon2Parameters{}, ErrInvalidPasswordHash } memory, err := parseNamedUint(parts[0], "m", 32) if err != nil || memory < 8*1024 || memory > 1024*1024 { return Argon2Parameters{}, ErrInvalidPasswordHash } timeCost, err := parseNamedUint(parts[1], "t", 32) if err != nil || timeCost == 0 || timeCost > 10 { return Argon2Parameters{}, ErrInvalidPasswordHash } threads, err := parseNamedUint(parts[2], "p", 8) if err != nil || threads == 0 || threads > 16 { return Argon2Parameters{}, ErrInvalidPasswordHash } return Argon2Parameters{MemoryKiB: uint32(memory), Time: uint32(timeCost), Threads: uint8(threads)}, nil } func parseNamedUint(value, name string, bitSize int) (uint64, error) { prefix := name + "=" if !strings.HasPrefix(value, prefix) { return 0, ErrInvalidPasswordHash } parsed, err := strconv.ParseUint(strings.TrimPrefix(value, prefix), 10, bitSize) if err != nil { return 0, ErrInvalidPasswordHash } return parsed, nil }