package password import ( "context" "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "runtime" "strconv" "strings" "sync" "golang.org/x/crypto/argon2" ) var ( ErrMalformed = errors.New("password: malformed stored hash") ErrBusy = errors.New("password: verifier busy") ) var errEmptyPassword = errors.New("password: empty password refused") const ( defaultMemory = 19456 defaultTime = 2 defaultThreads = 1 keyLength = 32 saltLength = 16 minMemory = 8192 maxMemory = 65536 minTime = 1 maxTime = 16 minThreads = 1 maxThreads = 16 minBytes = 16 maxBytes = 64 maxDigits = 7 ) const variantField = "argon2id" var versionField = "v=" + strconv.Itoa(argon2.Version) var b64 = base64.RawStdEncoding.Strict() var kdf = argon2.IDKey var sem = make(chan struct{}, semaphoreSize(runtime.GOMAXPROCS(0))) var dummy = sync.OnceValue(newDummy) type phc struct { memory uint32 time uint32 threads uint8 salt []byte key []byte } func semaphoreSize(gomaxprocs int) int { return min(gomaxprocs, 3) } func Hash(password string) ([]byte, error) { if password == "" { return nil, errEmptyPassword } salt := make([]byte, saltLength) rand.Read(salt) key := kdf([]byte(password), salt, defaultTime, defaultMemory, defaultThreads, keyLength) return encode(phc{memory: defaultMemory, time: defaultTime, threads: defaultThreads, salt: salt, key: key}), nil } func HashContext(ctx context.Context, password string) ([]byte, error) { if password == "" { return nil, errEmptyPassword } if err := acquire(ctx); err != nil { return nil, err } defer release() return Hash(password) } func Verify(ctx context.Context, stored []byte, password string) (bool, error) { h, ok := decode(stored) if !ok { return false, ErrMalformed } if err := acquire(ctx); err != nil { return false, err } defer release() got := kdf([]byte(password), h.salt, h.time, h.memory, h.threads, uint32(len(h.key))) return subtle.ConstantTimeCompare(got, h.key) == 1, nil } func VerifyAbsent(ctx context.Context, password string) error { _, err := Verify(ctx, dummy(), password) return err } func Unusable() []byte { return newDummy() } func newDummy() []byte { salt := make([]byte, saltLength) key := make([]byte, keyLength) rand.Read(salt) rand.Read(key) return encode(phc{memory: defaultMemory, time: defaultTime, threads: defaultThreads, salt: salt, key: key}) } func acquire(ctx context.Context) error { if ctx.Err() != nil { busy.Add(1) return ErrBusy } select { case sem <- struct{}{}: return nil default: } waits.Add(1) select { case sem <- struct{}{}: return nil case <-ctx.Done(): busy.Add(1) return ErrBusy } } func release() { <-sem } func encode(h phc) []byte { return fmt.Appendf(nil, "$%s$%s$m=%d,t=%d,p=%d$%s$%s", variantField, versionField, h.memory, h.time, h.threads, b64.EncodeToString(h.salt), b64.EncodeToString(h.key)) } func decode(stored []byte) (phc, bool) { fields := strings.Split(string(stored), "$") if len(fields) != 6 || fields[0] != "" || fields[1] != variantField || fields[2] != versionField { return phc{}, false } params := strings.Split(fields[3], ",") if len(params) != 3 { return phc{}, false } m, okM := parseParam(params[0], "m=") t, okT := parseParam(params[1], "t=") p, okP := parseParam(params[2], "p=") if !okM || !okT || !okP { return phc{}, false } if m < minMemory || m > maxMemory || t < minTime || t > maxTime || p < minThreads || p > maxThreads { return phc{}, false } salt, okS := decodeSegment(fields[4]) key, okK := decodeSegment(fields[5]) if !okS || !okK { return phc{}, false } return phc{memory: uint32(m), time: uint32(t), threads: uint8(p), salt: salt, key: key}, true } func parseParam(field, prefix string) (uint64, bool) { digits, ok := strings.CutPrefix(field, prefix) if !ok || digits == "" || len(digits) > maxDigits || (len(digits) > 1 && digits[0] == '0') { return 0, false } for i := 0; i < len(digits); i++ { if digits[i] < '0' || digits[i] > '9' { return 0, false } } v, err := strconv.ParseUint(digits, 10, 32) return v, err == nil } func decodeSegment(segment string) ([]byte, bool) { raw, err := b64.DecodeString(segment) if err != nil || len(raw) < minBytes || len(raw) > maxBytes || b64.EncodeToString(raw) != segment { return nil, false } return raw, true }