package password import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" ) // Algorithm represents the hashing algorithm to use type Algorithm int const ( Argon2 Algorithm = iota Bcrypt ) // Argon2Config holds configuration for Argon2 hashing type Argon2Config struct { Time uint32 // Number of iterations Memory uint32 // Memory usage in KB Threads uint8 // Number of threads KeyLen uint32 // Length of generated hash } // BcryptConfig holds configuration for Bcrypt hashing type BcryptConfig struct { Cost int // Bcrypt cost factor (4-31) } // Config holds the password hashing configuration type Config struct { Algorithm Algorithm Argon2 Argon2Config Bcrypt BcryptConfig } // DefaultArgon2Config returns recommended Argon2 settings func DefaultArgon2Config() Argon2Config { return Argon2Config{ Time: 1, Memory: 64 * 1024, // 64 MB Threads: 4, KeyLen: 32, } } // SecureArgon2Config returns more secure Argon2 settings (slower but more resistant) func SecureArgon2Config() Argon2Config { return Argon2Config{ Time: 3, Memory: 128 * 1024, // 128 MB Threads: 4, KeyLen: 32, } } // FastArgon2Config returns faster Argon2 settings (for testing/development) func FastArgon2Config() Argon2Config { return Argon2Config{ Time: 1, Memory: 32 * 1024, // 32 MB Threads: 2, KeyLen: 32, } } // DefaultBcryptConfig returns recommended Bcrypt settings func DefaultBcryptConfig() BcryptConfig { return BcryptConfig{ Cost: bcrypt.DefaultCost, // 10 } } // SecureBcryptConfig returns more secure Bcrypt settings (slower but more resistant) func SecureBcryptConfig() BcryptConfig { return BcryptConfig{ Cost: 12, } } // DefaultConfig returns the default configuration (Argon2 with default settings) func DefaultConfig() Config { return Config{ Algorithm: Argon2, Argon2: DefaultArgon2Config(), Bcrypt: DefaultBcryptConfig(), } } var globalConfig = DefaultConfig() // SetConfig sets the global password configuration func SetConfig(config Config) { globalConfig = config } // GetConfig returns the current global password configuration func GetConfig() Config { return globalConfig } // HashPassword creates a hash of the password using the global configuration func HashPassword(password string) string { return HashPasswordWithConfig(password, globalConfig) } // HashPasswordWithConfig creates a hash of the password using the specified configuration func HashPasswordWithConfig(password string, config Config) string { switch config.Algorithm { case Bcrypt: return hashBcrypt(password, config.Bcrypt) case Argon2: fallthrough default: return hashArgon2(password, config.Argon2) } } // hashArgon2 creates an argon2id hash of the password func hashArgon2(password string, config Argon2Config) string { salt := make([]byte, 16) rand.Read(salt) hash := argon2.IDKey([]byte(password), salt, config.Time, config.Memory, config.Threads, config.KeyLen) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, config.Memory, config.Time, config.Threads, b64Salt, b64Hash) return encoded } // hashBcrypt creates a bcrypt hash of the password func hashBcrypt(password string, config BcryptConfig) string { hash, err := bcrypt.GenerateFromPassword([]byte(password), config.Cost) if err != nil { // Fallback to default cost if provided cost is invalid hash, _ = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) } return string(hash) } // VerifyPassword checks if a password matches the hash func VerifyPassword(password, encodedHash string) (bool, error) { // Detect hash type by prefix if strings.HasPrefix(encodedHash, "$2a$") || strings.HasPrefix(encodedHash, "$2b$") || strings.HasPrefix(encodedHash, "$2y$") { return verifyBcrypt(password, encodedHash) } else if strings.HasPrefix(encodedHash, "$argon2id$") { return verifyArgon2(password, encodedHash) } return false, fmt.Errorf("unsupported hash format") } // verifyArgon2 checks if a password matches an argon2 hash func verifyArgon2(password, encodedHash string) (bool, error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 { return false, fmt.Errorf("invalid hash format") } if parts[1] != "argon2id" { return false, fmt.Errorf("invalid hash variant") } var version int _, err := fmt.Sscanf(parts[2], "v=%d", &version) if err != nil { return false, err } if version != argon2.Version { return false, fmt.Errorf("incompatible argon2 version") } var m, t, p uint32 _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p) if err != nil { return false, err } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, err } expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, err } hash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(expectedHash))) if subtle.ConstantTimeCompare(hash, expectedHash) == 1 { return true, nil } return false, nil } // verifyBcrypt checks if a password matches a bcrypt hash func verifyBcrypt(password, encodedHash string) (bool, error) { err := bcrypt.CompareHashAndPassword([]byte(encodedHash), []byte(password)) if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil } if err != nil { return false, err } return true, nil } // NeedsRehash checks if a hash needs to be updated to current configuration func NeedsRehash(encodedHash string) bool { // Check if using bcrypt when we want argon2 if globalConfig.Algorithm == Argon2 && (strings.HasPrefix(encodedHash, "$2a$") || strings.HasPrefix(encodedHash, "$2b$") || strings.HasPrefix(encodedHash, "$2y$")) { return true } // Check if using argon2 when we want bcrypt if globalConfig.Algorithm == Bcrypt && strings.HasPrefix(encodedHash, "$argon2id$") { return true } // For bcrypt, check if cost has changed if strings.HasPrefix(encodedHash, "$2") { cost, err := bcrypt.Cost([]byte(encodedHash)) if err == nil && cost != globalConfig.Bcrypt.Cost { return true } } // For argon2, check if parameters have changed if strings.HasPrefix(encodedHash, "$argon2id$") { parts := strings.Split(encodedHash, "$") if len(parts) == 6 { var m, t, p uint32 _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p) if err == nil { if m != globalConfig.Argon2.Memory || t != globalConfig.Argon2.Time || p != uint32(globalConfig.Argon2.Threads) { return true } } } } return false }