package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // Argon2Params — параметры argon2id. Хранятся в самом хэше (формат PHC), поэтому их можно менять: // старые хэши проверяются со своими параметрами и пересчитываются при следующем входе. type Argon2Params struct { MemoryKiB uint32 Time uint32 Threads uint8 SaltLen int KeyLen uint32 } // DefaultArgon2Params — m=64 MiB, t=3, p=2 (docs/auth-and-roles.md). var DefaultArgon2Params = Argon2Params{MemoryKiB: 64 * 1024, Time: 3, Threads: 2, SaltLen: 16, KeyLen: 32} // Hasher хэширует и проверяет пароли. type Hasher struct{ p Argon2Params } // NewHasher создаёт Hasher с заданными параметрами. func NewHasher(p Argon2Params) *Hasher { return &Hasher{p: p} } // Hash возвращает PHC-строку `$argon2id$v=19$m=…,t=…,p=…$salt$key`. func (h *Hasher) Hash(password string) (string, error) { salt := make([]byte, h.p.SaltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("auth: salt: %w", err) } key := argon2.IDKey([]byte(password), salt, h.p.Time, h.p.MemoryKiB, h.p.Threads, h.p.KeyLen) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, h.p.MemoryKiB, h.p.Time, h.p.Threads, b64.EncodeToString(salt), b64.EncodeToString(key)), nil } // Verify проверяет пароль. needsRehash = true, если хэш посчитан с устаревшими параметрами. func (h *Hasher) Verify(password, encoded string) (ok, needsRehash bool, err error) { p, salt, key, err := decodePHC(encoded) if err != nil { return false, false, err } got := argon2.IDKey([]byte(password), salt, p.Time, p.MemoryKiB, p.Threads, uint32(len(key))) if subtle.ConstantTimeCompare(got, key) != 1 { return false, false, nil } needsRehash = p.MemoryKiB != h.p.MemoryKiB || p.Time != h.p.Time || p.Threads != h.p.Threads || len(salt) != h.p.SaltLen || uint32(len(key)) != h.p.KeyLen return true, needsRehash, nil } var b64 = base64.RawStdEncoding var errBadHash = errors.New("auth: malformed password hash") func decodePHC(s string) (Argon2Params, []byte, []byte, error) { var p Argon2Params parts := strings.Split(s, "$") if len(parts) != 6 || parts[1] != "argon2id" { return p, nil, nil, errBadHash } var v int if _, err := fmt.Sscanf(parts[2], "v=%d", &v); err != nil || v != argon2.Version { return p, nil, nil, errBadHash } if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.MemoryKiB, &p.Time, &p.Threads); err != nil { return p, nil, nil, errBadHash } salt, err := b64.DecodeString(parts[4]) if err != nil { return p, nil, nil, errBadHash } key, err := b64.DecodeString(parts[5]) if err != nil || len(key) == 0 { return p, nil, nil, errBadHash } p.SaltLen, p.KeyLen = len(salt), uint32(len(key)) return p, salt, key, nil } // Алфавит временных паролей: без похожих символов (0/O, 1/l/I), чтобы пароль легко продиктовать. const tempPasswordAlphabet = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" // GeneratePassword возвращает случайный пароль длины n из tempPasswordAlphabet. func GeneratePassword(n int) (string, error) { buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", fmt.Errorf("auth: random password: %w", err) } // 256 % 56 != 0: отбрасываем байты из «хвоста», чтобы распределение было равномерным. const limit = 256 - 256%len(tempPasswordAlphabet) out := make([]byte, 0, n) for len(out) < n { for _, b := range buf { if int(b) < limit && len(out) < n { out = append(out, tempPasswordAlphabet[int(b)%len(tempPasswordAlphabet)]) } } if _, err := rand.Read(buf); err != nil { return "", fmt.Errorf("auth: random password: %w", err) } } return string(out), nil }