package service import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // argon2id parameters. ~64 MiB memory / 1 pass / 4 lanes is the interactive // profile recommended by the argon2 authors and is comfortable on a // self-hosted box. They are encoded into every stored hash, so raising them // later leaves old hashes verifiable. const ( argonMemKiB = 64 * 1024 // 64 MiB argonTime = 1 argonThreads = 4 argonKeyLen = 32 argonSaltLen = 16 ) // errBadHash marks a stored hash string that could not be parsed — a data or // programming error, not a wrong password. Callers treat it as an auth failure // but should log it. var errBadHash = errors.New("service: malformed password hash") // b64 is the padding-free base64 used inside the PHC-style hash string. var b64 = base64.RawStdEncoding // hashPassword returns a self-describing argon2id hash in the PHC string format // "$argon2id$v=19$m=...,t=...,p=...$salt$hash" (all base64, no padding). func hashPassword(password string) (string, error) { salt := make([]byte, argonSaltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("service: generate salt: %w", err) } key := argon2.IDKey([]byte(password), salt, argonTime, argonMemKiB, argonThreads, argonKeyLen) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argonMemKiB, argonTime, argonThreads, b64.EncodeToString(salt), b64.EncodeToString(key), ), nil } // verifyPassword reports whether password matches the encoded argon2id hash. The // comparison is constant-time. A malformed encoded value returns errBadHash. func verifyPassword(encoded, password string) (bool, error) { mem, t, threads, salt, want, err := decodeHash(encoded) if err != nil { return false, err } got := argon2.IDKey([]byte(password), salt, t, mem, threads, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1, nil } // decodeHash parses a PHC-format argon2id string into its parameters, salt, and // derived key. func decodeHash(encoded string) (mem, t uint32, threads uint8, salt, key []byte, err error) { parts := strings.Split(encoded, "$") // ["", "argon2id", "v=19", "m=..,t=..,p=..", "", ""] if len(parts) != 6 || parts[1] != "argon2id" { return 0, 0, 0, nil, nil, errBadHash } var version int if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil || version != argon2.Version { return 0, 0, 0, nil, nil, errBadHash } if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &mem, &t, &threads); err != nil { return 0, 0, 0, nil, nil, errBadHash } if salt, err = b64.DecodeString(parts[4]); err != nil { return 0, 0, 0, nil, nil, errBadHash } if key, err = b64.DecodeString(parts[5]); err != nil || len(key) == 0 { return 0, 0, 0, nil, nil, errBadHash } return mem, t, threads, salt, key, nil }