package kate import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // Argon2Config configures Argon2id password hashing parameters. // // Leave any fields at their zero value to use the default. type Argon2Config struct { // Time is the number of iterations (default: 1) Time uint32 // Memory is the memory usage in KiB (default: 64*1024 = 64MB) Memory uint32 // Threads is the number of parallel threads (default: 4) Threads uint8 // KeyLen is the length of the derived key in bytes (default: 32) KeyLen uint32 } var defaultArgon2Config = Argon2Config{ Time: 1, Memory: 64 * 1024, Threads: 4, KeyLen: 32, } // HashPassword hashes a password using Argon2id. // // Returns a PHC-formatted string containing all parameters needed for verification. // If config is nil, secure defaults are used. func HashPassword(password string, config *Argon2Config) (string, error) { // Use defaults if no config provided if config == nil { config = &defaultArgon2Config } salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("HashPassword: %w", err) } // Use defaults for zero values in config time := config.Time if time == 0 { time = defaultArgon2Config.Time } memory := config.Memory if memory == 0 { memory = defaultArgon2Config.Memory } threads := config.Threads if threads == 0 { threads = defaultArgon2Config.Threads } keyLen := config.KeyLen if keyLen == 0 { keyLen = defaultArgon2Config.KeyLen } hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen) return buildPHC(salt, hash, memory, time, threads), nil } // ComparePassword verifies a password against a stored hash in PHC format. // // Returns (false, nil) for incorrect passwords and (false, error) for malformed hashes. // Uses constant-time comparison to prevent timing attacks. func ComparePassword(loginpass, storedhash string) (bool, error) { salt, hash, memory, time, threads, err := parsePHC(storedhash) if err != nil { return false, fmt.Errorf("ComparePassword: %w", err) } loginHash := argon2.IDKey([]byte(loginpass), salt, time, memory, threads, uint32(len(hash))) return subtle.ConstantTimeCompare(loginHash, hash) == 1, nil } func buildPHC(salt, hash []byte, memory, time uint32, threads uint8) string { b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, memory, time, threads, b64Salt, b64Hash) } func parsePHC(phc string) (salt, hash []byte, memory, time uint32, threads uint8, err error) { parts := strings.Split(phc, "$") if len(parts) != 6 { err = errors.New("invalid PHC format") return } if parts[1] != "argon2id" { err = errors.New("unsupported algorithm") return } var version int if _, err = fmt.Sscanf(parts[2], "v=%d", &version); err != nil { return } if version > argon2.Version { err = errors.New("unsupported argon2 version") return } var p uint32 if _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &p); err != nil { return } threads = uint8(p) if salt, err = base64.RawStdEncoding.DecodeString(parts[4]); err != nil { return } if hash, err = base64.RawStdEncoding.DecodeString(parts[5]); err != nil { return } return }