428 lines
10 KiB
Go
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")
|
|
}
|
|
}
|