- Replace nil context with context.TODO() in logger calls - Fix import ordering and struct field alignment - Restore .env.example required by open-source audit tests
268 lines
7.5 KiB
Go
268 lines
7.5 KiB
Go
package security
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ─── crypto.go ─────────────────────────────────────────────────────────
|
|
|
|
func TestEncryptDecrypt_RoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
plain := []byte("sensitive data here")
|
|
password := []byte("test-password-123")
|
|
enc, err := Encrypt(plain, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt error: %v", err)
|
|
}
|
|
dec, err := Decrypt(enc, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt error: %v", err)
|
|
}
|
|
if string(dec) != string(plain) {
|
|
t.Fatalf("round-trip mismatch: %q vs %q", dec, plain)
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecrypt_EmptyPlaintext(t *testing.T) {
|
|
t.Parallel()
|
|
enc, err := Encrypt([]byte{}, []byte("pass"))
|
|
if err != nil {
|
|
t.Fatalf("Encrypt empty error: %v", err)
|
|
}
|
|
dec, err := Decrypt(enc, []byte("pass"))
|
|
if err != nil {
|
|
t.Fatalf("Decrypt empty error: %v", err)
|
|
}
|
|
if len(dec) != 0 {
|
|
t.Fatalf("expected empty, got %d bytes", len(dec))
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_TooShort(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := Decrypt([]byte("short"), []byte("pass"))
|
|
if err == nil {
|
|
t.Fatal("expected error for short ciphertext")
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_Corrupted(t *testing.T) {
|
|
t.Parallel()
|
|
enc, _ := Encrypt([]byte("data"), []byte("pass"))
|
|
// Corrupt the last byte (GCM tag)
|
|
enc[len(enc)-1] ^= 0xff
|
|
_, err := Decrypt(enc, []byte("pass"))
|
|
if err == nil {
|
|
t.Fatal("expected error for corrupted ciphertext")
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_WrongPassword(t *testing.T) {
|
|
t.Parallel()
|
|
enc, _ := Encrypt([]byte("secret"), []byte("correct"))
|
|
_, err := Decrypt(enc, []byte("wrong"))
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong password")
|
|
}
|
|
}
|
|
|
|
func TestDeriveKey_Deterministic(t *testing.T) {
|
|
t.Parallel()
|
|
salt := []byte("fixed-salt-value-32-bytes-long!!")
|
|
k1 := DeriveKey([]byte("pass"), salt)
|
|
k2 := DeriveKey([]byte("pass"), salt)
|
|
if len(k1) != KeySize {
|
|
t.Fatalf("expected %d bytes, got %d", KeySize, len(k1))
|
|
}
|
|
for i := range k1 {
|
|
if k1[i] != k2[i] {
|
|
t.Fatal("same inputs should produce same key")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── fingerprint.go ────────────────────────────────────────────────────
|
|
|
|
func TestIsVirtualMAC(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
mac string
|
|
want bool
|
|
}{
|
|
{"02:42:ac:11:00:02", true}, // Docker
|
|
{"00:50:56:c0:00:01", true}, // VMware
|
|
{"00:0c:29:ab:cd:ef", true}, // VMware
|
|
{"08:00:27:12:34:56", true}, // VirtualBox
|
|
{"52:54:00:ab:cd:ef", true}, // KVM/QEMU
|
|
{"aa:bb:cc:dd:ee:ff", false}, // physical
|
|
{"14:98:77:ab:cd:ef", false}, // physical
|
|
}
|
|
for _, tt := range tests {
|
|
if got := isVirtualMAC(tt.mac); got != tt.want {
|
|
t.Errorf("isVirtualMAC(%s) = %v, want %v", tt.mac, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetMACAddress_ReturnsNonEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
mac, err := GetMACAddress()
|
|
if err != nil {
|
|
t.Skipf("no NIC available: %v", err)
|
|
}
|
|
if mac == "" {
|
|
t.Fatal("expected non-empty MAC address")
|
|
}
|
|
// Should look like a MAC
|
|
if len(mac) < 11 {
|
|
t.Fatalf("MAC too short: %s", mac)
|
|
}
|
|
}
|
|
|
|
func TestSelectMAC_PhysicalPreferred(t *testing.T) {
|
|
t.Parallel()
|
|
ifaces := []net.Interface{
|
|
{Index: 1, Name: "lo", Flags: net.FlagLoopback, HardwareAddr: nil},
|
|
{Index: 2, Name: "eth0", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xac, 0x11, 0x00, 0x02}}, // Docker virtual
|
|
{Index: 3, Name: "enp0s3", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x14, 0x98, 0x77, 0xab, 0xcd, 0xef}}, // physical
|
|
}
|
|
mac, err := selectMAC(ifaces)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if mac != "14:98:77:ab:cd:ef" {
|
|
t.Fatalf("expected physical MAC, got %s", mac)
|
|
}
|
|
}
|
|
|
|
func TestSelectMAC_VirtualFallback(t *testing.T) {
|
|
t.Parallel()
|
|
// Docker container scenario: only loopback + Docker veth
|
|
ifaces := []net.Interface{
|
|
{Index: 1, Name: "lo", Flags: net.FlagLoopback, HardwareAddr: nil},
|
|
{Index: 2, Name: "eth0", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xac, 0x11, 0x00, 0x02}},
|
|
}
|
|
mac, err := selectMAC(ifaces)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if mac != "02:42:ac:11:00:02" {
|
|
t.Fatalf("expected Docker virtual MAC, got %s", mac)
|
|
}
|
|
}
|
|
|
|
func TestSelectMAC_MultipleVirtualPicksFirst(t *testing.T) {
|
|
t.Parallel()
|
|
ifaces := []net.Interface{
|
|
{Index: 1, Name: "lo", Flags: net.FlagLoopback},
|
|
{Index: 2, Name: "eth0", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xbb, 0x00, 0x00, 0x01}},
|
|
{Index: 3, Name: "eth1", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xaa, 0x00, 0x00, 0x01}},
|
|
}
|
|
mac, err := selectMAC(ifaces)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// Lexicographically: 02:42:aa:... < 02:42:bb:...
|
|
if mac != "02:42:aa:00:00:01" {
|
|
t.Fatalf("expected lexicographically first virtual MAC, got %s", mac)
|
|
}
|
|
}
|
|
|
|
func TestSelectMAC_NoInterfaces(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := selectMAC(nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty interface list")
|
|
}
|
|
}
|
|
|
|
func TestSelectMAC_OnlyLoopback(t *testing.T) {
|
|
t.Parallel()
|
|
ifaces := []net.Interface{
|
|
{Index: 1, Name: "lo", Flags: net.FlagLoopback, HardwareAddr: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
|
|
}
|
|
_, err := selectMAC(ifaces)
|
|
if err == nil {
|
|
t.Fatal("expected error when only loopback exists")
|
|
}
|
|
}
|
|
|
|
func TestSelectMAC_NoHardwareAddr(t *testing.T) {
|
|
t.Parallel()
|
|
ifaces := []net.Interface{
|
|
{Index: 1, Name: "tun0", Flags: net.FlagUp, HardwareAddr: nil},
|
|
}
|
|
_, err := selectMAC(ifaces)
|
|
if err == nil {
|
|
t.Fatal("expected error for interfaces without hardware address")
|
|
}
|
|
}
|
|
|
|
// ─── storage.go ────────────────────────────────────────────────────────
|
|
|
|
func TestDeleteEncryptedData(t *testing.T) {
|
|
t.Parallel()
|
|
dir1 := t.TempDir()
|
|
dir2 := t.TempDir()
|
|
// Create .data files
|
|
os.WriteFile(filepath.Join(dir1, DataFileName), []byte("enc1"), 0o600)
|
|
os.WriteFile(filepath.Join(dir2, DataFileName), []byte("enc2"), 0o600)
|
|
|
|
err := DeleteEncryptedData(dir1, dir2)
|
|
if err != nil {
|
|
t.Fatalf("DeleteEncryptedData error: %v", err)
|
|
}
|
|
if DataFileExistsInAny(dir1, dir2) {
|
|
t.Fatal("files should be deleted")
|
|
}
|
|
}
|
|
|
|
func TestDeleteEncryptedData_EmptyDirs(t *testing.T) {
|
|
t.Parallel()
|
|
err := DeleteEncryptedData("", "")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteEncryptedData_NonExistent(t *testing.T) {
|
|
t.Parallel()
|
|
err := DeleteEncryptedData("/nonexistent/dir")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for nonexistent: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSecureTokenStorage_ExistsInFallback(t *testing.T) {
|
|
t.Parallel()
|
|
primary := t.TempDir()
|
|
fallback := t.TempDir()
|
|
os.WriteFile(filepath.Join(fallback, DataFileName), []byte("data"), 0o600)
|
|
|
|
s := NewSecureTokenStorage(primary, fallback, "")
|
|
if !s.Exists() {
|
|
t.Fatal("should find .data in fallback dir")
|
|
}
|
|
}
|
|
|
|
func TestSecureTokenStorage_DeleteTokenBothDirs(t *testing.T) {
|
|
t.Parallel()
|
|
dir1 := t.TempDir()
|
|
dir2 := t.TempDir()
|
|
s1 := NewSecureTokenStorage(dir1, "", "aa:bb:cc:dd:ee:ff")
|
|
s1.SaveToken(&TokenData{AccessToken: "t", ExpiresAt: time.Now().Add(time.Hour)})
|
|
os.WriteFile(filepath.Join(dir2, DataFileName), []byte("enc"), 0o600)
|
|
|
|
s := NewSecureTokenStorage(dir1, dir2, "aa:bb:cc:dd:ee:ff")
|
|
if err := s.DeleteToken(); err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
if DataFileExistsInAny(dir1, dir2) {
|
|
t.Fatal("both should be deleted")
|
|
}
|
|
}
|