diff options
author | T <t@tjp.lol> | 2025-06-26 11:42:17 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-07-01 17:50:49 -0600 |
commit | 639ad6a02cbb4b713434671ec09f309aa5410921 (patch) | |
tree | 7dde9cce8136636d11f2f7c961072984cfc705e7 /password.go |
Create authentic_kate: user authentication for go HTTP applications
Diffstat (limited to 'password.go')
-rw-r--r-- | password.go | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/password.go b/password.go new file mode 100644 index 0000000..d1a5c54 --- /dev/null +++ b/password.go @@ -0,0 +1,131 @@ +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 +} |