package security import ( "crypto/rand" "crypto/subtle" "encoding/hex" "errors" "fmt" "strconv" "strings" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" ) // PasswordHasher provides password hashing and verification type PasswordHasher struct { time uint32 memory uint32 threads uint8 keyLen uint32 saltLen int } // NewPasswordHasher creates a new password hasher with sensible defaults func NewPasswordHasher() *PasswordHasher { return &PasswordHasher{ time: 1, memory: 64 * 1024, // 64 MB threads: 4, keyLen: 32, saltLen: 16, } } // HashPassword hashes a password using Argon2id // Returns hash in format "$argon2id$v=19$m=65536,t=1,p=4$salt$hash" func (ph *PasswordHasher) HashPassword(password string) (string, error) { salt := make([]byte, ph.saltLen) if _, err := rand.Read(salt); err != nil { return "", err } hash := argon2.IDKey( []byte(password), salt, ph.time, ph.memory, ph.threads, ph.keyLen, ) hashStr := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", 19, ph.memory, ph.time, ph.threads, hex.EncodeToString(salt), hex.EncodeToString(hash), ) return hashStr, nil } // VerifyPassword verifies a password against a hash func (ph *PasswordHasher) VerifyPassword(password, hash string) (bool, error) { // Backward compatibility: accept legacy bcrypt hashes. if strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$") { if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { return false, nil } return true, nil } parts := strings.Split(hash, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, errors.New("invalid password hash format") } versionPart := strings.TrimPrefix(parts[2], "v=") version, err := strconv.Atoi(versionPart) if err != nil || version != 19 { return false, errors.New("invalid password hash version") } var memory, timeCost uint32 var threads uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &timeCost, &threads); err != nil { return false, errors.New("invalid password hash parameters") } saltStr := parts[4] hashStr := parts[5] salt, err := hex.DecodeString(saltStr) if err != nil { return false, errors.New("invalid salt in password hash") } expectedHashBytes, err := hex.DecodeString(hashStr) if err != nil { return false, errors.New("invalid hash in password hash") } // Hash the input password with the extracted parameters computedHash := argon2.IDKey( []byte(password), salt, timeCost, memory, threads, uint32(len(expectedHashBytes)), ) if subtle.ConstantTimeCompare(computedHash, expectedHashBytes) == 1 { return true, nil } return false, nil }