mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:24:18 -04:00
* implement reverse proxy --------- Co-authored-by: Alisdair MacLeod <git@alisdairmacleod.co.uk> Co-authored-by: mlsmaycon <mlsmaycon@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: Viktor Liu <viktor@netbird.io> Co-authored-by: Diego Noguês <diego.sure@gmail.com> Co-authored-by: Diego Noguês <49420+diegocn@users.noreply.github.com> Co-authored-by: Bethuel Mmbaga <bethuelmbaga12@gmail.com> Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Ashley Mensah <ashleyamo982@gmail.com>
328 lines
7.8 KiB
Go
328 lines
7.8 KiB
Go
package argon2id
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
func TestHash(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
secret string
|
|
}{
|
|
{
|
|
name: "simple password",
|
|
secret: "password123",
|
|
},
|
|
{
|
|
name: "complex password with special chars",
|
|
secret: "P@ssw0rd!#$%^&*()",
|
|
},
|
|
{
|
|
name: "long password",
|
|
secret: strings.Repeat("a", 100),
|
|
},
|
|
{
|
|
name: "empty password",
|
|
secret: "",
|
|
},
|
|
{
|
|
name: "unicode password",
|
|
secret: "пароль密码🔐",
|
|
},
|
|
{
|
|
name: "numeric PIN",
|
|
secret: "123456",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
hash, err := Hash(tt.secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
|
|
// Verify hash format
|
|
if !strings.HasPrefix(hash, "$argon2id$") {
|
|
t.Errorf("Hash() = %v, want hash starting with $argon2id$", hash)
|
|
}
|
|
|
|
// Verify hash has correct number of components
|
|
parts := strings.Split(hash, "$")
|
|
if len(parts) != 6 {
|
|
t.Errorf("Hash() has %d parts, want 6", len(parts))
|
|
}
|
|
|
|
// Verify version is present
|
|
if !strings.HasPrefix(hash, "$argon2id$v=") {
|
|
t.Errorf("Hash() missing version, got %v", hash)
|
|
}
|
|
|
|
// Verify each hash is unique (different salt)
|
|
hash2, err := Hash(tt.secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() second call error = %v", err)
|
|
}
|
|
if hash == hash2 {
|
|
t.Error("Hash() produces identical hashes for same input (salt not random)")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerify(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
secret string
|
|
wantError error
|
|
}{
|
|
{
|
|
name: "valid password",
|
|
secret: "correctPassword",
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "valid PIN",
|
|
secret: "1234",
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "empty secret",
|
|
secret: "",
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "unicode secret",
|
|
secret: "密码🔐",
|
|
wantError: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Generate hash
|
|
hash, err := Hash(tt.secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
|
|
// Verify correct secret
|
|
err = Verify(tt.secret, hash)
|
|
if !errors.Is(err, tt.wantError) {
|
|
t.Errorf("Verify() error = %v, wantError %v", err, tt.wantError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifyIncorrectPassword(t *testing.T) {
|
|
secret := "correctPassword"
|
|
wrongSecret := "wrongPassword"
|
|
|
|
hash, err := Hash(secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
|
|
err = Verify(wrongSecret, hash)
|
|
if !errors.Is(err, ErrMismatchedHashAndPassword) {
|
|
t.Errorf("Verify() error = %v, want %v", err, ErrMismatchedHashAndPassword)
|
|
}
|
|
}
|
|
|
|
func TestVerifyInvalidHashFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
invalidHash string
|
|
expectedError error
|
|
}{
|
|
{
|
|
name: "empty hash",
|
|
invalidHash: "",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "wrong algorithm",
|
|
invalidHash: "$bcrypt$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "missing parts",
|
|
invalidHash: "$argon2id$v=19$m=19456",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "too many parts",
|
|
invalidHash: "$argon2id$v=19$m=19456,t=2,p=1$salt$hash$extra",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "invalid version format",
|
|
invalidHash: "$argon2id$vXX$m=19456,t=2,p=1$c2FsdA$aGFzaA",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "invalid parameters format",
|
|
invalidHash: "$argon2id$v=19$mXX,tYY,pZZ$c2FsdA$aGFzaA",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "invalid salt base64",
|
|
invalidHash: "$argon2id$v=19$m=19456,t=2,p=1$not-valid-base64!@#$aGFzaA",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
{
|
|
name: "invalid hash base64",
|
|
invalidHash: "$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$not-valid-base64!@#",
|
|
expectedError: ErrInvalidHash,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := Verify("password", tt.invalidHash)
|
|
if err == nil {
|
|
t.Errorf("Verify() expected error, got nil")
|
|
return
|
|
}
|
|
|
|
if !errors.Is(err, tt.expectedError) && !strings.Contains(err.Error(), tt.expectedError.Error()) {
|
|
t.Errorf("Verify() error = %v, want error containing %v", err, tt.expectedError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifyIncompatibleVersion(t *testing.T) {
|
|
// Manually craft a hash with wrong version
|
|
invalidVersionHash := "$argon2id$v=18$m=19456,t=2,p=1$c2FsdDEyMzQ1Njc4OTA$aGFzaDEyMzQ1Njc4OTBhYmNkZWZnaGlqa2xtbm9w"
|
|
|
|
err := Verify("password", invalidVersionHash)
|
|
if !errors.Is(err, ErrIncompatibleVersion) {
|
|
t.Errorf("Verify() error = %v, want %v", err, ErrIncompatibleVersion)
|
|
}
|
|
}
|
|
|
|
func TestHashDeterminism(t *testing.T) {
|
|
// Ensure different hashes for same password (random salt)
|
|
password := "testPassword"
|
|
hashes := make(map[string]bool)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
hash, err := Hash(password)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
if hashes[hash] {
|
|
t.Error("Hash() produced duplicate hash (salt generation may be broken)")
|
|
}
|
|
hashes[hash] = true
|
|
}
|
|
|
|
if len(hashes) != 10 {
|
|
t.Errorf("Expected 10 unique hashes, got %d", len(hashes))
|
|
}
|
|
}
|
|
|
|
func TestOWASPCompliance(t *testing.T) {
|
|
// Test that generated hashes use OWASP-recommended parameters
|
|
secret := "testPassword"
|
|
hash, err := Hash(secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
|
|
params, _, _, err := decodeHash(hash)
|
|
if err != nil {
|
|
t.Fatalf("decodeHash() error = %v", err)
|
|
}
|
|
|
|
// Verify OWASP minimum baseline parameters
|
|
if params.memory != 19456 {
|
|
t.Errorf("memory = %d, want 19456 (OWASP baseline)", params.memory)
|
|
}
|
|
if params.iterations != 2 {
|
|
t.Errorf("iterations = %d, want 2 (OWASP baseline)", params.iterations)
|
|
}
|
|
if params.parallelism != 1 {
|
|
t.Errorf("parallelism = %d, want 1 (OWASP baseline)", params.parallelism)
|
|
}
|
|
if params.keyLength != 32 {
|
|
t.Errorf("keyLength = %d, want 32", params.keyLength)
|
|
}
|
|
if params.version != argon2.Version {
|
|
t.Errorf("version = %d, want %d", params.version, argon2.Version)
|
|
}
|
|
}
|
|
|
|
func TestConstantTimeComparison(t *testing.T) {
|
|
// This test verifies that Verify() is using constant-time comparison
|
|
// by ensuring it doesn't fail differently for similar vs different hashes
|
|
secret := "password123"
|
|
wrongSecret := "password124" // One character different
|
|
|
|
hash, err := Hash(secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
|
|
// Both wrong passwords should return the same error
|
|
err1 := Verify(wrongSecret, hash)
|
|
err2 := Verify("completelydifferent", hash)
|
|
|
|
if !errors.Is(err1, ErrMismatchedHashAndPassword) {
|
|
t.Errorf("Verify() error = %v, want %v", err1, ErrMismatchedHashAndPassword)
|
|
}
|
|
if !errors.Is(err2, ErrMismatchedHashAndPassword) {
|
|
t.Errorf("Verify() error = %v, want %v", err2, ErrMismatchedHashAndPassword)
|
|
}
|
|
|
|
// Errors should be identical (same error type and message)
|
|
if err1.Error() != err2.Error() {
|
|
t.Error("Verify() returns different errors for different wrong passwords (potential timing attack)")
|
|
}
|
|
}
|
|
|
|
func TestCaseSensitivity(t *testing.T) {
|
|
// Passwords should be case-sensitive
|
|
secret := "Password123"
|
|
wrongSecret := "password123"
|
|
|
|
hash, err := Hash(secret)
|
|
if err != nil {
|
|
t.Fatalf("Hash() error = %v", err)
|
|
}
|
|
|
|
// Correct password should verify
|
|
if err := Verify(secret, hash); err != nil {
|
|
t.Errorf("Verify() with correct password error = %v, want nil", err)
|
|
}
|
|
|
|
// Wrong case should not verify
|
|
if err := Verify(wrongSecret, hash); !errors.Is(err, ErrMismatchedHashAndPassword) {
|
|
t.Errorf("Verify() with wrong case error = %v, want %v", err, ErrMismatchedHashAndPassword)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
func BenchmarkHash(b *testing.B) {
|
|
secret := "benchmarkPassword123"
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = Hash(secret)
|
|
}
|
|
}
|
|
|
|
func BenchmarkVerify(b *testing.B) {
|
|
secret := "benchmarkPassword123"
|
|
hash, _ := Hash(secret)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = Verify(secret, hash)
|
|
}
|
|
}
|