This commit is contained in:
Leon Bösche
2026-01-09 18:58:09 +01:00
parent 2a62e13fc7
commit f18e779375
8 changed files with 759 additions and 23 deletions

View File

@@ -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