135 lines
3.7 KiB
Go
135 lines
3.7 KiB
Go
// Keywarden - Centralized SSH Key Management and Deployment
|
|
// Copyright (C) 2026 Patrick Asmus (scriptos)
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package auth
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"unicode"
|
|
)
|
|
|
|
// ---------- generateSecurePassword ----------
|
|
|
|
func TestGenerateSecurePassword_Length(t *testing.T) {
|
|
for _, length := range []int{8, 16, 20, 32, 64} {
|
|
pw, err := generateSecurePassword(length)
|
|
if err != nil {
|
|
t.Fatalf("generateSecurePassword(%d) error: %v", length, err)
|
|
}
|
|
if len(pw) != length {
|
|
t.Fatalf("generateSecurePassword(%d) returned length %d", length, len(pw))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateSecurePassword_CharacterSet(t *testing.T) {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
pw, err := generateSecurePassword(1000)
|
|
if err != nil {
|
|
t.Fatalf("generateSecurePassword error: %v", err)
|
|
}
|
|
for i, c := range pw {
|
|
if !strings.ContainsRune(charset, c) {
|
|
t.Fatalf("character at position %d (%c) not in allowed charset", i, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateSecurePassword_Uniqueness(t *testing.T) {
|
|
passwords := make(map[string]bool)
|
|
for i := 0; i < 100; i++ {
|
|
pw, err := generateSecurePassword(20)
|
|
if err != nil {
|
|
t.Fatalf("generateSecurePassword error: %v", err)
|
|
}
|
|
if passwords[pw] {
|
|
t.Fatal("generated duplicate password — insufficient randomness")
|
|
}
|
|
passwords[pw] = true
|
|
}
|
|
}
|
|
|
|
func TestGenerateSecurePassword_NoBias(t *testing.T) {
|
|
// Generate many characters and check that the distribution is roughly
|
|
// uniform. With rejection sampling and charset length 62, each character
|
|
// should appear about 1/62 ≈ 1.6% of the time. We use a generous margin.
|
|
const total = 62000
|
|
pw, err := generateSecurePassword(total)
|
|
if err != nil {
|
|
t.Fatalf("generateSecurePassword error: %v", err)
|
|
}
|
|
|
|
freq := make(map[rune]int)
|
|
for _, c := range pw {
|
|
freq[c]++
|
|
}
|
|
|
|
expected := float64(total) / 62.0 // ~1000
|
|
for c, count := range freq {
|
|
ratio := float64(count) / expected
|
|
// Allow 20% deviation (generous for 62k samples)
|
|
if ratio < 0.8 || ratio > 1.2 {
|
|
t.Errorf("character %c appeared %d times (expected ~%.0f, ratio %.2f) — possible bias", c, count, expected, ratio)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- dummyHash (timing attack prevention) ----------
|
|
|
|
func TestDummyHash_IsValid(t *testing.T) {
|
|
if dummyHash == nil {
|
|
t.Fatal("dummyHash should not be nil")
|
|
}
|
|
if len(dummyHash) == 0 {
|
|
t.Fatal("dummyHash should not be empty")
|
|
}
|
|
// It should be a valid bcrypt hash (starts with $2a$ or $2b$)
|
|
s := string(dummyHash)
|
|
if !strings.HasPrefix(s, "$2a$") && !strings.HasPrefix(s, "$2b$") {
|
|
t.Fatalf("dummyHash does not look like a bcrypt hash: %s", s)
|
|
}
|
|
}
|
|
|
|
// ---------- Password character class helpers ----------
|
|
|
|
func TestPasswordCharacterClasses(t *testing.T) {
|
|
tests := []struct {
|
|
password string
|
|
hasUpper bool
|
|
hasLower bool
|
|
hasDigit bool
|
|
hasSpecial bool
|
|
}{
|
|
{"abc", false, true, false, false},
|
|
{"ABC", true, false, false, false},
|
|
{"123", false, false, true, false},
|
|
{"!@#", false, false, false, true},
|
|
{"aB1!", true, true, true, true},
|
|
{"", false, false, false, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
var upper, lower, digit, special bool
|
|
for _, r := range tt.password {
|
|
if unicode.IsUpper(r) {
|
|
upper = true
|
|
}
|
|
if unicode.IsLower(r) {
|
|
lower = true
|
|
}
|
|
if unicode.IsDigit(r) {
|
|
digit = true
|
|
}
|
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
|
special = true
|
|
}
|
|
}
|
|
if upper != tt.hasUpper || lower != tt.hasLower || digit != tt.hasDigit || special != tt.hasSpecial {
|
|
t.Errorf("password %q: got upper=%v lower=%v digit=%v special=%v, want upper=%v lower=%v digit=%v special=%v",
|
|
tt.password, upper, lower, digit, special, tt.hasUpper, tt.hasLower, tt.hasDigit, tt.hasSpecial)
|
|
}
|
|
}
|
|
}
|