summaryrefslogtreecommitdiff
path: root/password.go
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-06-26 11:42:17 -0600
committerT <t@tjp.lol>2025-07-01 17:50:49 -0600
commit639ad6a02cbb4b713434671ec09f309aa5410921 (patch)
tree7dde9cce8136636d11f2f7c961072984cfc705e7 /password.go
Create authentic_kate: user authentication for go HTTP applications
Diffstat (limited to 'password.go')
-rw-r--r--password.go131
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
+}