package authconfig import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "io" "strconv" "strings" "unicode/utf8" "golang.org/x/crypto/argon2" ) const ( argon2MemoryKiB uint32 = 65536 argon2Passes uint32 = 3 argon2Parallel uint8 = 1 argon2SaltBytes = 16 argon2KeyBytes uint32 = 32 maximumPHCBytes = 256 ) type argon2Parameters struct { memory uint32 passes uint32 parallelism uint8 salt []byte digest []byte } // HashPassword derives a fixed-parameter Argon2id v19 PHC string. random must be a // cryptographically secure source in production; it is injected only to permit the shared vector. func HashPassword(password []byte, random io.Reader) (string, error) { if !validPassword(password) { return "", errInvalidAuthenticationConfig } if random == nil { random = rand.Reader } salt := make([]byte, argon2SaltBytes) if _, err := io.ReadFull(random, salt); err != nil { return "", errInvalidAuthenticationConfig } digest := argon2.IDKey(password, salt, argon2Passes, argon2MemoryKiB, argon2Parallel, argon2KeyBytes) return "$argon2id$v=19$m=65536,t=3,p=1$" + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(digest), nil } // VerifyPassword accepts only the exact canonical Argon2id v19 policy. func VerifyPassword(password []byte, encoded string) bool { if !validPassword(password) { return false } parameters, ok := parsePHC(encoded) if !ok { return false } derived := argon2.IDKey(password, parameters.salt, parameters.passes, parameters.memory, parameters.parallelism, uint32(len(parameters.digest))) return subtle.ConstantTimeCompare(derived, parameters.digest) == 1 } func validPassword(password []byte) bool { return len(password) >= passwordMinBytes && len(password) <= passwordMaxBytes && utf8.Valid(password) } func parsePHC(encoded string) (argon2Parameters, bool) { if len(encoded) == 0 || len(encoded) > maximumPHCBytes { return argon2Parameters{}, false } parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" || parts[2] != "v=19" { return argon2Parameters{}, false } parameters, ok := parsePHCParameters(parts[3]) if !ok { return argon2Parameters{}, false } salt, ok := decodePHCBase64(parts[4], argon2SaltBytes, argon2SaltBytes) if !ok { return argon2Parameters{}, false } digest, ok := decodePHCBase64(parts[5], int(argon2KeyBytes), int(argon2KeyBytes)) if !ok { return argon2Parameters{}, false } parameters.salt = salt parameters.digest = digest return parameters, true } func parsePHCParameters(value string) (argon2Parameters, bool) { parts := strings.Split(value, ",") if len(parts) != 3 || !strings.HasPrefix(parts[0], "m=") || !strings.HasPrefix(parts[1], "t=") || !strings.HasPrefix(parts[2], "p=") { return argon2Parameters{}, false } memory, ok := parseDecimal(parts[0][2:], uint64(argon2MemoryKiB)) if !ok || memory != uint64(argon2MemoryKiB) { return argon2Parameters{}, false } passes, ok := parseDecimal(parts[1][2:], uint64(argon2Passes)) if !ok || passes != uint64(argon2Passes) { return argon2Parameters{}, false } parallelism, ok := parseDecimal(parts[2][2:], uint64(argon2Parallel)) if !ok || parallelism != uint64(argon2Parallel) { return argon2Parameters{}, false } return argon2Parameters{memory: uint32(memory), passes: uint32(passes), parallelism: uint8(parallelism)}, true } func parseDecimal(value string, maximum uint64) (uint64, bool) { if value == "" || (len(value) > 1 && value[0] == '0') || len(value) > 10 { return 0, false } for _, character := range value { if character < '0' || character > '9' { return 0, false } } parsed, err := strconv.ParseUint(value, 10, 64) if err != nil || parsed > maximum { return 0, false } return parsed, true } func decodePHCBase64(value string, minimum, maximum int) ([]byte, bool) { if value == "" || strings.ContainsRune(value, '=') || len(value) > base64.RawStdEncoding.EncodedLen(maximum) || len(value) < base64.RawStdEncoding.EncodedLen(minimum)-1 { return nil, false } encoding := base64.RawStdEncoding.Strict() decoded, err := encoding.DecodeString(value) if err != nil || len(decoded) < minimum || len(decoded) > maximum || encoding.EncodeToString(decoded) != value { return nil, false } return decoded, true } func validatePasswordHash(encoded string) error { if _, ok := parsePHC(encoded); !ok { return errors.New("invalid password hash") } return nil }