work
This commit is contained in:
@@ -6,9 +6,11 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -17,6 +19,12 @@ const (
|
||||
RPID = "b0esche.cloud"
|
||||
RPName = "b0esche Cloud"
|
||||
Origin = "https://b0esche.cloud"
|
||||
|
||||
// Argon2id parameters (OWASP recommendations)
|
||||
Argon2Time = 2 // iterations
|
||||
Argon2Memory = 19 * 1024 // 19 MB
|
||||
Argon2Threads = 1
|
||||
Argon2KeyLen = 32
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -284,19 +292,76 @@ func byteArraysEqual(a, b []byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// HashPassword hashes a password using bcrypt
|
||||
// HashPassword hashes a password using Argon2id (quantum-resistant)
|
||||
// Format: $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
|
||||
func (s *Service) HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash password: %w", err)
|
||||
// Generate 16-byte random salt
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
return string(hash), nil
|
||||
|
||||
// Hash with Argon2id
|
||||
hash := argon2.IDKey([]byte(password), salt, Argon2Time, Argon2Memory, Argon2Threads, Argon2KeyLen)
|
||||
|
||||
// Encode in PHC string format
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
|
||||
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
|
||||
Argon2Memory, Argon2Time, Argon2Threads, b64Salt, b64Hash), nil
|
||||
}
|
||||
|
||||
// VerifyPassword checks if a password matches its hash
|
||||
// Supports both Argon2id (new) and bcrypt (legacy) for backward compatibility
|
||||
func (s *Service) VerifyPassword(passwordHash string, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
|
||||
return err == nil
|
||||
// Detect hash format
|
||||
if strings.HasPrefix(passwordHash, "$argon2id$") {
|
||||
return s.verifyArgon2(passwordHash, password)
|
||||
} else if strings.HasPrefix(passwordHash, "$2") {
|
||||
// Legacy bcrypt hash
|
||||
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) verifyArgon2(encodedHash string, password string) bool {
|
||||
// Parse PHC format: $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
|
||||
parts := strings.Split(encodedHash, "$")
|
||||
if len(parts) != 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
var memory, time uint32
|
||||
var threads uint8
|
||||
_, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compute hash with same parameters
|
||||
computedHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(hash)))
|
||||
|
||||
// Constant-time comparison
|
||||
if len(hash) != len(computedHash) {
|
||||
return false
|
||||
}
|
||||
var diff byte
|
||||
for i := 0; i < len(hash); i++ {
|
||||
diff |= hash[i] ^ computedHash[i]
|
||||
}
|
||||
return diff == 0
|
||||
}
|
||||
|
||||
// VerifyPasswordLogin verifies username and password credentials
|
||||
|
||||
Reference in New Issue
Block a user