start working on the frontend side
This commit is contained in:
106
backend/internal/common/password/password.go
Normal file
106
backend/internal/common/password/password.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/atlasos/calypso/internal/common/config"
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// HashPassword hashes a password using Argon2id
|
||||
func HashPassword(password string, params config.Argon2Params) (string, error) {
|
||||
// Generate a random salt
|
||||
salt := make([]byte, params.SaltLength)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
hash := argon2.IDKey(
|
||||
[]byte(password),
|
||||
salt,
|
||||
params.Iterations,
|
||||
params.Memory,
|
||||
params.Parallelism,
|
||||
params.KeyLength,
|
||||
)
|
||||
|
||||
// Encode the hash and salt in the standard format
|
||||
// Format: $argon2id$v=<version>$m=<memory>,t=<iterations>,p=<parallelism>$<salt>$<hash>
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
|
||||
encodedHash := fmt.Sprintf(
|
||||
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
params.Memory,
|
||||
params.Iterations,
|
||||
params.Parallelism,
|
||||
b64Salt,
|
||||
b64Hash,
|
||||
)
|
||||
|
||||
return encodedHash, nil
|
||||
}
|
||||
|
||||
// VerifyPassword verifies a password against an Argon2id hash
|
||||
func VerifyPassword(password, encodedHash string) (bool, error) {
|
||||
// Parse the encoded hash
|
||||
parts := strings.Split(encodedHash, "$")
|
||||
if len(parts) != 6 {
|
||||
return false, errors.New("invalid hash format")
|
||||
}
|
||||
|
||||
if parts[1] != "argon2id" {
|
||||
return false, errors.New("unsupported hash algorithm")
|
||||
}
|
||||
|
||||
// Parse version
|
||||
var version int
|
||||
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
|
||||
return false, fmt.Errorf("failed to parse version: %w", err)
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return false, errors.New("incompatible version")
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
var memory, iterations uint32
|
||||
var parallelism uint8
|
||||
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, ¶llelism); err != nil {
|
||||
return false, fmt.Errorf("failed to parse parameters: %w", err)
|
||||
}
|
||||
|
||||
// Decode salt and hash
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to decode salt: %w", err)
|
||||
}
|
||||
|
||||
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to decode hash: %w", err)
|
||||
}
|
||||
|
||||
// Compute the hash of the provided password
|
||||
otherHash := argon2.IDKey(
|
||||
[]byte(password),
|
||||
salt,
|
||||
iterations,
|
||||
memory,
|
||||
parallelism,
|
||||
uint32(len(hash)),
|
||||
)
|
||||
|
||||
// Compare hashes using constant-time comparison
|
||||
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
182
backend/internal/common/password/password_test.go
Normal file
182
backend/internal/common/password/password_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/atlasos/calypso/internal/common/config"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
params := config.Argon2Params{
|
||||
Memory: 64 * 1024,
|
||||
Iterations: 3,
|
||||
Parallelism: 4,
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
|
||||
password := "test-password-123"
|
||||
hash, err := HashPassword(password, params)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify hash format
|
||||
if hash == "" {
|
||||
t.Error("HashPassword returned empty string")
|
||||
}
|
||||
|
||||
// Verify hash starts with Argon2id prefix
|
||||
if len(hash) < 12 || hash[:12] != "$argon2id$v=" {
|
||||
t.Errorf("Hash does not start with expected prefix, got: %s", hash[:min(30, len(hash))])
|
||||
}
|
||||
|
||||
// Verify hash contains required components
|
||||
if !contains(hash, "$m=") || !contains(hash, ",t=") || !contains(hash, ",p=") {
|
||||
t.Errorf("Hash missing required components, got: %s", hash[:min(50, len(hash))])
|
||||
}
|
||||
|
||||
// Verify hash is different each time (due to random salt)
|
||||
hash2, err := HashPassword(password, params)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword failed on second call: %v", err)
|
||||
}
|
||||
|
||||
if hash == hash2 {
|
||||
t.Error("HashPassword returned same hash for same password (salt should be random)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassword(t *testing.T) {
|
||||
params := config.Argon2Params{
|
||||
Memory: 64 * 1024,
|
||||
Iterations: 3,
|
||||
Parallelism: 4,
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
|
||||
password := "test-password-123"
|
||||
hash, err := HashPassword(password, params)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword failed: %v", err)
|
||||
}
|
||||
|
||||
// Test correct password
|
||||
valid, err := VerifyPassword(password, hash)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword failed: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("VerifyPassword returned false for correct password")
|
||||
}
|
||||
|
||||
// Test wrong password
|
||||
valid, err = VerifyPassword("wrong-password", hash)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword failed: %v", err)
|
||||
}
|
||||
if valid {
|
||||
t.Error("VerifyPassword returned true for wrong password")
|
||||
}
|
||||
|
||||
// Test empty password
|
||||
valid, err = VerifyPassword("", hash)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword failed: %v", err)
|
||||
}
|
||||
if valid {
|
||||
t.Error("VerifyPassword returned true for empty password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassword_InvalidHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
}{
|
||||
{"empty hash", ""},
|
||||
{"invalid format", "not-a-hash"},
|
||||
{"wrong algorithm", "$argon2$v=19$m=65536,t=3,p=4$salt$hash"},
|
||||
{"incomplete hash", "$argon2id$v=19"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
valid, err := VerifyPassword("test-password", tt.hash)
|
||||
if err == nil {
|
||||
t.Error("VerifyPassword should return error for invalid hash")
|
||||
}
|
||||
if valid {
|
||||
t.Error("VerifyPassword should return false for invalid hash")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPassword_DifferentPasswords(t *testing.T) {
|
||||
params := config.Argon2Params{
|
||||
Memory: 64 * 1024,
|
||||
Iterations: 3,
|
||||
Parallelism: 4,
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
|
||||
password1 := "password1"
|
||||
password2 := "password2"
|
||||
|
||||
hash1, err := HashPassword(password1, params)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword failed: %v", err)
|
||||
}
|
||||
|
||||
hash2, err := HashPassword(password2, params)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword failed: %v", err)
|
||||
}
|
||||
|
||||
// Hashes should be different
|
||||
if hash1 == hash2 {
|
||||
t.Error("Different passwords produced same hash")
|
||||
}
|
||||
|
||||
// Each password should verify against its own hash
|
||||
valid, err := VerifyPassword(password1, hash1)
|
||||
if err != nil || !valid {
|
||||
t.Error("Password1 should verify against its own hash")
|
||||
}
|
||||
|
||||
valid, err = VerifyPassword(password2, hash2)
|
||||
if err != nil || !valid {
|
||||
t.Error("Password2 should verify against its own hash")
|
||||
}
|
||||
|
||||
// Passwords should not verify against each other's hash
|
||||
valid, err = VerifyPassword(password1, hash2)
|
||||
if err != nil || valid {
|
||||
t.Error("Password1 should not verify against password2's hash")
|
||||
}
|
||||
|
||||
valid, err = VerifyPassword(password2, hash1)
|
||||
if err != nil || valid {
|
||||
t.Error("Password2 should not verify against password1's hash")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user