Files
Gists/internal/auth/password/argon2id_test.go
2025-10-31 15:37:45 +07:00

428 lines
10 KiB
Go

package password
import (
"encoding/base64"
"strings"
"testing"
)
func TestArgon2ID_Hash(t *testing.T) {
tests := []struct {
name string
plain string
wantErr bool
}{
{
name: "basic password",
plain: "password123",
wantErr: false,
},
{
name: "empty string",
plain: "",
wantErr: false,
},
{
name: "long password",
plain: strings.Repeat("a", 10000),
wantErr: false,
},
{
name: "unicode password",
plain: "パスワード🔒",
wantErr: false,
},
{
name: "special characters",
plain: "!@#$%^&*()_+-=[]{}|;:',.<>?/`~",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash, err := Argon2id.Hash(tt.plain)
if (err != nil) != tt.wantErr {
t.Errorf("Argon2id.Hash() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
// Verify the hash format
if !strings.HasPrefix(hash, "$argon2id$") {
t.Errorf("Hash does not start with $argon2id$: %v", hash)
}
// Verify all parts are present
parts := strings.Split(hash, "$")
if len(parts) != 6 {
t.Errorf("Hash has %d parts, expected 6: %v", len(parts), hash)
}
// Verify salt is properly encoded
if len(parts) >= 5 {
_, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
t.Errorf("Salt is not properly base64 encoded: %v", err)
}
}
// Verify hash is properly encoded
if len(parts) >= 6 {
_, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
t.Errorf("Hash is not properly base64 encoded: %v", err)
}
}
}
})
}
}
func TestArgon2ID_Verify(t *testing.T) {
// Generate a valid hash for testing
testPassword := "correctpassword"
validHash, err := Argon2id.Hash(testPassword)
if err != nil {
t.Fatalf("Failed to generate test hash: %v", err)
}
tests := []struct {
name string
plain string
hash string
wantMatch bool
wantErr bool
}{
{
name: "correct password",
plain: testPassword,
hash: validHash,
wantMatch: true,
wantErr: false,
},
{
name: "incorrect password",
plain: "wrongpassword",
hash: validHash,
wantMatch: false,
wantErr: false,
},
{
name: "empty password",
plain: "",
hash: validHash,
wantMatch: false,
wantErr: false,
},
{
name: "empty hash",
plain: testPassword,
hash: "",
wantMatch: false,
wantErr: false,
},
{
name: "invalid hash - too few parts",
plain: testPassword,
hash: "$argon2id$v=19$m=65536",
wantMatch: false,
wantErr: true,
},
{
name: "invalid hash - too many parts",
plain: testPassword,
hash: "$argon2id$v=19$m=65536,t=1,p=4$salt$hash$extra",
wantMatch: false,
wantErr: true,
},
{
name: "invalid hash - malformed parameters",
plain: testPassword,
hash: "$argon2id$v=19$invalid$salt$hash",
wantMatch: false,
wantErr: true,
},
{
name: "invalid hash - bad base64 salt",
plain: testPassword,
hash: "$argon2id$v=19$m=65536,t=1,p=4$not-valid-base64!@#$hash",
wantMatch: false,
wantErr: true,
},
{
name: "invalid hash - bad base64 hash",
plain: testPassword,
hash: "$argon2id$v=19$m=65536,t=1,p=4$dGVzdA$not-valid-base64!@#",
wantMatch: false,
wantErr: true,
},
{
name: "wrong algorithm prefix",
plain: testPassword,
hash: "$bcrypt$rounds=10$saltsaltsaltsaltsalt",
wantMatch: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
match, err := Argon2id.Verify(tt.plain, tt.hash)
if (err != nil) != tt.wantErr {
t.Errorf("Argon2id.Verify() error = %v, wantErr %v", err, tt.wantErr)
return
}
if match != tt.wantMatch {
t.Errorf("Argon2id.Verify() match = %v, wantMatch %v", match, tt.wantMatch)
}
})
}
}
func TestArgon2ID_SaltUniqueness(t *testing.T) {
password := "testpassword"
iterations := 10
hashes := make(map[string]bool)
salts := make(map[string]bool)
for i := 0; i < iterations; i++ {
hash, err := Argon2id.Hash(password)
if err != nil {
t.Fatalf("Hash iteration %d failed: %v", i, err)
}
// Check hash uniqueness
if hashes[hash] {
t.Errorf("Duplicate hash generated at iteration %d", i)
}
hashes[hash] = true
// Extract and check salt uniqueness
parts := strings.Split(hash, "$")
if len(parts) >= 5 {
salt := parts[4]
if salts[salt] {
t.Errorf("Duplicate salt generated at iteration %d", i)
}
salts[salt] = true
}
// Verify each hash works
match, err := Argon2id.Verify(password, hash)
if err != nil || !match {
t.Errorf("Hash %d failed verification: err=%v, match=%v", i, err, match)
}
}
}
func TestArgon2ID_HashFormat(t *testing.T) {
password := "testformat"
hash, err := Argon2id.Hash(password)
if err != nil {
t.Fatalf("Hash failed: %v", err)
}
parts := strings.Split(hash, "$")
if len(parts) != 6 {
t.Fatalf("Expected 6 parts, got %d: %v", len(parts), hash)
}
// Part 0 should be empty (before first $)
if parts[0] != "" {
t.Errorf("Part 0 should be empty, got: %v", parts[0])
}
// Part 1 should be "argon2id"
if parts[1] != "argon2id" {
t.Errorf("Part 1 should be 'argon2id', got: %v", parts[1])
}
// Part 2 should be version
if !strings.HasPrefix(parts[2], "v=") {
t.Errorf("Part 2 should start with 'v=', got: %v", parts[2])
}
// Part 3 should be parameters
if !strings.Contains(parts[3], "m=") || !strings.Contains(parts[3], "t=") || !strings.Contains(parts[3], "p=") {
t.Errorf("Part 3 should contain m=, t=, and p=, got: %v", parts[3])
}
// Part 4 should be base64 encoded salt
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
t.Errorf("Salt (part 4) is not valid base64: %v", err)
}
if len(salt) != int(Argon2id.saltLen) {
t.Errorf("Salt length is %d, expected %d", len(salt), Argon2id.saltLen)
}
// Part 5 should be base64 encoded hash
decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
t.Errorf("Hash (part 5) is not valid base64: %v", err)
}
if len(decodedHash) != int(Argon2id.keyLen) {
t.Errorf("Hash length is %d, expected %d", len(decodedHash), Argon2id.keyLen)
}
}
func TestArgon2ID_CaseModification(t *testing.T) {
// Passwords should be case-sensitive
password := "TestPassword"
hash, err := Argon2id.Hash(password)
if err != nil {
t.Fatalf("Hash failed: %v", err)
}
// Correct case should match
match, err := Argon2id.Verify(password, hash)
if err != nil || !match {
t.Errorf("Correct password failed: err=%v, match=%v", err, match)
}
// Wrong case should not match
match, err = Argon2id.Verify("testpassword", hash)
if err != nil {
t.Errorf("Verify returned error: %v", err)
}
if match {
t.Error("Password verification should be case-sensitive")
}
match, err = Argon2id.Verify("TESTPASSWORD", hash)
if err != nil {
t.Errorf("Verify returned error: %v", err)
}
if match {
t.Error("Password verification should be case-sensitive")
}
}
func TestArgon2ID_InvalidParameters(t *testing.T) {
password := "testpassword"
tests := []struct {
name string
hash string
wantErr bool
}{
{
name: "negative memory parameter",
hash: "$argon2id$v=19$m=-1,t=1,p=4$dGVzdHNhbHQ$testhash",
wantErr: true,
},
{
name: "negative time parameter",
hash: "$argon2id$v=19$m=65536,t=-1,p=4$dGVzdHNhbHQ$testhash",
wantErr: true,
},
{
name: "negative parallelism parameter",
hash: "$argon2id$v=19$m=65536,t=1,p=-4$dGVzdHNhbHQ$testhash",
wantErr: true,
},
{
name: "zero memory parameter",
hash: "$argon2id$v=19$m=0,t=1,p=4$dGVzdHNhbHQ$testhash",
wantErr: false, // argon2 may handle this, we just test parsing
},
{
name: "missing parameter value",
hash: "$argon2id$v=19$m=,t=1,p=4$dGVzdHNhbHQ$testhash",
wantErr: true,
},
{
name: "non-numeric parameter",
hash: "$argon2id$v=19$m=abc,t=1,p=4$dGVzdHNhbHQ$testhash",
wantErr: true,
},
{
name: "missing parameters separator",
hash: "$argon2id$v=19$m=65536 t=1 p=4$dGVzdHNhbHQ$testhash",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Argon2id.Verify(password, tt.hash)
if (err != nil) != tt.wantErr {
t.Errorf("Argon2id.Verify() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestArgon2ID_ConcurrentHashing(t *testing.T) {
password := "testpassword"
concurrency := 10
type result struct {
hash string
err error
}
results := make(chan result, concurrency)
// Generate hashes concurrently
for i := 0; i < concurrency; i++ {
go func() {
hash, err := Argon2id.Hash(password)
results <- result{hash: hash, err: err}
}()
}
// Collect results
hashes := make(map[string]bool)
for i := 0; i < concurrency; i++ {
res := <-results
if res.err != nil {
t.Errorf("Concurrent hash %d failed: %v", i, res.err)
continue
}
// Check for duplicates
if hashes[res.hash] {
t.Errorf("Duplicate hash generated in concurrent test")
}
hashes[res.hash] = true
// Verify each hash works
match, err := Argon2id.Verify(password, res.hash)
if err != nil || !match {
t.Errorf("Hash %d failed verification: err=%v, match=%v", i, err, match)
}
}
}
func TestArgon2ID_VeryLongPassword(t *testing.T) {
// Test with extremely long password (100KB)
password := strings.Repeat("a", 100*1024)
hash, err := Argon2id.Hash(password)
if err != nil {
t.Fatalf("Failed to hash very long password: %v", err)
}
match, err := Argon2id.Verify(password, hash)
if err != nil {
t.Fatalf("Failed to verify very long password: %v", err)
}
if !match {
t.Error("Very long password failed verification")
}
// Verify wrong password still fails
wrongPassword := strings.Repeat("b", 100*1024)
match, err = Argon2id.Verify(wrongPassword, hash)
if err != nil {
t.Errorf("Verify returned error: %v", err)
}
if match {
t.Error("Wrong very long password should not match")
}
}