From 828c5818995de3b86a57330bd662ea7cfd14fd9e Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Wed, 17 Sep 2025 22:22:40 -0500 Subject: [PATCH] add bcrypt and configs to password --- README.md | 39 ++++++++- password/password.go | 188 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 218 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index feaee5a..643b70b 100644 --- a/README.md +++ b/README.md @@ -169,14 +169,49 @@ admin.Use(auth.RequireAuth("/login")) ### 1. Password Hashing +The password package supports both Argon2 (default) and Bcrypt algorithms with configurable settings: + ```go import "git.sharkk.net/Sharkk/Sushi/password" -// Hash password for storage +// Using default Argon2 configuration hashedPassword := password.HashPassword("userpassword123") -// Verify password during login +// Verify password (auto-detects algorithm) isValid, err := password.VerifyPassword("userpassword123", hashedPassword) + +// Configure for Bcrypt +password.SetConfig(password.Config{ + Algorithm: password.Bcrypt, + Bcrypt: password.BcryptConfig{ + Cost: 12, // Higher = more secure but slower + }, +}) + +// Configure for Argon2 with custom settings +password.SetConfig(password.Config{ + Algorithm: password.Argon2, + Argon2: password.Argon2Config{ + Time: 3, // Iterations + Memory: 128 * 1024, // 128 MB + Threads: 4, + KeyLen: 32, + }, +}) + +// Use preset configurations +password.SetConfig(password.Config{ + Algorithm: password.Argon2, + Argon2: password.SecureArgon2Config(), // More secure, slower + // Or: password.FastArgon2Config() for development +}) + +// Check if password needs rehashing (algorithm or params changed) +if password.NeedsRehash(userHashedPassword) { + // Rehash password after successful verification + newHash := password.HashPassword(plainPassword) + // Update stored hash in database +} ``` ### 2. User Structure diff --git a/password/password.go b/password/password.go index 2c09565..606b72c 100644 --- a/password/password.go +++ b/password/password.go @@ -8,33 +8,158 @@ import ( "strings" "golang.org/x/crypto/argon2" + "golang.org/x/crypto/bcrypt" ) +// Algorithm represents the hashing algorithm to use +type Algorithm int + const ( - argonTime = 1 - argonMemory = 64 * 1024 - argonThreads = 4 - argonKeyLen = 32 + Argon2 Algorithm = iota + Bcrypt ) -// HashPassword creates an argon2id hash of the password +// 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, argonTime, argonMemory, argonThreads, argonKeyLen) + 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, argonMemory, argonTime, argonThreads, b64Salt, b64Hash) + 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") @@ -77,3 +202,52 @@ func VerifyPassword(password, encodedHash string) (bool, error) { 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 +} \ No newline at end of file