package crypto import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strconv" "strings" "golang.org/x/crypto/argon2" ) type Argon2Params struct { Memory uint32 // KiB Time uint32 Threads uint8 SaltLen uint32 KeyLen uint32 } var DefaultArgon2 = Argon2Params{ Memory: 7168, // 7 MiB Time: 5, Threads: 1, SaltLen: 16, KeyLen: 32, } // Hash returns PHC string: $argon2id$v=19$m=...,t=...,p=...$$ func Hash(password string, p Argon2Params) (string, error) { salt := make([]byte, p.SaltLen) if _, err := rand.Read(salt); err != nil { return "", err } sum := argon2.IDKey([]byte(password), salt, p.Time, p.Memory, p.Threads, p.KeyLen) return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", p.Memory, p.Time, p.Threads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(sum), ), nil } func Verify(password, phc string) (bool, error) { parts := strings.Split(phc, "$") // Expect: ["", "argon2id", "v=19", "m=...,t=...,p=...", "", ""] if len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" { return false, errors.New("invalid PHC header") } // parse params var mem, iters uint64 var threads uint64 for _, kv := range strings.Split(parts[3], ",") { if strings.HasPrefix(kv, "m=") { v := strings.TrimPrefix(kv, "m=") x, err := strconv.ParseUint(v, 10, 32) if err != nil { return false, fmt.Errorf("mem: %w", err) } mem = x } else if strings.HasPrefix(kv, "t=") { v := strings.TrimPrefix(kv, "t=") x, err := strconv.ParseUint(v, 10, 32) if err != nil { return false, fmt.Errorf("time: %w", err) } iters = x } else if strings.HasPrefix(kv, "p=") { v := strings.TrimPrefix(kv, "p=") x, err := strconv.ParseUint(v, 10, 8) if err != nil { return false, fmt.Errorf("threads: %w", err) } threads = x } } if mem == 0 || iters == 0 || threads == 0 { return false, errors.New("invalid PHC params") } salt, err := b64DecodeFlex(parts[4]) if err != nil { return false, fmt.Errorf("salt decode: %w", err) } want, err := b64DecodeFlex(parts[5]) if err != nil { return false, fmt.Errorf("hash decode: %w", err) } got := argon2.IDKey([]byte(password), salt, uint32(iters), uint32(mem), uint8(threads), uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1, nil } func b64DecodeFlex(s string) ([]byte, error) { if b, err := base64.RawStdEncoding.DecodeString(s); err == nil { return b, nil } return base64.StdEncoding.DecodeString(s) // padded fallback }