// Package security provides password hashing and JWT issuing/verification // shared across auth modules. package security import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" ) const ( argon2Time = 3 argon2Memory = 64 * 1024 // 64 MB argon2Threads = 2 argon2SaltLen = 16 argon2KeyLen = 32 ) // HashPassword hashes a plaintext password using argon2id and returns the // PHC-formatted string ($argon2id$v=19$m=...,t=...,p=...$salt$hash). func HashPassword(password string) (string, error) { salt := make([]byte, argon2SaltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("generate salt: %w", err) } hash := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen) encoded := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argon2Memory, argon2Time, argon2Threads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash), ) return encoded, nil } // VerifyPassword checks a plaintext password against a PHC-formatted argon2id hash. func VerifyPassword(encodedHash, password string) (bool, error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, fmt.Errorf("invalid hash format") } var version int if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { return false, fmt.Errorf("invalid hash version segment: %w", err) } if version != argon2.Version { return false, fmt.Errorf("unsupported argon2 version: %d", version) } var memory uint32 var time uint32 var threads uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil { return false, fmt.Errorf("invalid hash params segment: %w", err) } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, fmt.Errorf("decode salt: %w", err) } wantHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, fmt.Errorf("decode hash: %w", err) } gotHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(wantHash))) if subtle.ConstantTimeCompare(gotHash, wantHash) == 1 { return true, nil } return false, nil }