package crypto import ( "bytes" "os" "path/filepath" "testing" ) func TestEncryptDecrypt(t *testing.T) { // Create a temp directory for the test tmpDir := t.TempDir() // Create the crypto manager cm, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } testCases := []struct { name string plaintext []byte }{ {"empty", []byte{}}, {"short", []byte("hello")}, {"medium", []byte("this is a medium length test string for encryption")}, {"with nulls", []byte("test\x00with\x00null\x00bytes")}, {"binary", []byte{0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd}}, {"unicode", []byte("こんにちは世界")}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { encrypted, err := cm.Encrypt(tc.plaintext) if err != nil { t.Fatalf("Encrypt() error: %v", err) } // Encrypted data should be different from plaintext (unless empty) if len(tc.plaintext) > 0 && bytes.Equal(encrypted, tc.plaintext) { t.Error("Encrypt() returned plaintext unchanged") } // Decrypt should return original decrypted, err := cm.Decrypt(encrypted) if err != nil { t.Fatalf("Decrypt() error: %v", err) } if !bytes.Equal(decrypted, tc.plaintext) { t.Errorf("Decrypt() = %v, want %v", decrypted, tc.plaintext) } }) } } func TestEncryptDecryptString(t *testing.T) { tmpDir := t.TempDir() cm, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } testStrings := []string{ "", "hello world", "password123!@#", "unicode: 日本語 中文 한국어", "special chars: \n\t\r\\\"'", } for _, s := range testStrings { t.Run(s, func(t *testing.T) { encrypted, err := cm.EncryptString(s) if err != nil { t.Fatalf("EncryptString() error: %v", err) } // Should be base64 encoded (printable) for _, c := range encrypted { if c > 127 { t.Errorf("EncryptString() contains non-ASCII: %c", c) } } decrypted, err := cm.DecryptString(encrypted) if err != nil { t.Fatalf("DecryptString() error: %v", err) } if decrypted != s { t.Errorf("DecryptString() = %q, want %q", decrypted, s) } }) } } func TestEncryptionKeyPersistence(t *testing.T) { tmpDir := t.TempDir() // Create first crypto manager - should generate key cm1, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() first call error: %v", err) } // Encrypt something plaintext := []byte("test data for key persistence") encrypted, err := cm1.Encrypt(plaintext) if err != nil { t.Fatalf("Encrypt() error: %v", err) } // Create second crypto manager - should load same key cm2, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() second call error: %v", err) } // Should be able to decrypt with second manager decrypted, err := cm2.Decrypt(encrypted) if err != nil { t.Fatalf("Decrypt() with second manager error: %v", err) } if !bytes.Equal(decrypted, plaintext) { t.Error("Second crypto manager couldn't decrypt data from first") } } func TestEncryptionKeyFilePermissions(t *testing.T) { tmpDir := t.TempDir() _, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } keyPath := filepath.Join(tmpDir, ".encryption.key") info, err := os.Stat(keyPath) if err != nil { t.Fatalf("Failed to stat key file: %v", err) } // Key file should have restricted permissions (0600) mode := info.Mode().Perm() if mode != 0600 { t.Errorf("Key file permissions = %o, want 0600", mode) } } func TestDecryptInvalidData(t *testing.T) { tmpDir := t.TempDir() cm, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } // Try to decrypt garbage _, err = cm.Decrypt([]byte("not encrypted data")) if err == nil { t.Error("Decrypt() should fail on invalid data") } // Try to decrypt empty _, err = cm.Decrypt([]byte{}) if err == nil { t.Error("Decrypt() should fail on empty data") } // Try to decrypt data that's too short for nonce _, err = cm.Decrypt([]byte{0x01, 0x02, 0x03}) if err == nil { t.Error("Decrypt() should fail on data too short for nonce") } } func TestDecryptStringInvalidBase64(t *testing.T) { tmpDir := t.TempDir() cm, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } // Invalid base64 _, err = cm.DecryptString("not!valid@base64#string") if err == nil { t.Error("DecryptString() should fail on invalid base64") } } func TestEncryptionUniqueness(t *testing.T) { tmpDir := t.TempDir() cm, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } plaintext := []byte("same plaintext") // Encrypt the same data twice encrypted1, _ := cm.Encrypt(plaintext) encrypted2, _ := cm.Encrypt(plaintext) // Should produce different ciphertext (due to random nonce) if bytes.Equal(encrypted1, encrypted2) { t.Error("Encrypting same plaintext produced identical ciphertext (nonce reuse?)") } // But both should decrypt to same plaintext decrypted1, _ := cm.Decrypt(encrypted1) decrypted2, _ := cm.Decrypt(encrypted2) if !bytes.Equal(decrypted1, plaintext) || !bytes.Equal(decrypted2, plaintext) { t.Error("Different ciphertexts didn't decrypt to same plaintext") } } func TestNewCryptoManagerRefusesOrphanedData(t *testing.T) { tmpDir := t.TempDir() // Create an encrypted data file without a key encFile := filepath.Join(tmpDir, "nodes.enc") err := os.WriteFile(encFile, []byte("fake encrypted data"), 0644) if err != nil { t.Fatalf("Failed to create test file: %v", err) } // Should fail because encrypted data exists but no key _, err = NewCryptoManagerAt(tmpDir) if err == nil { t.Error("NewCryptoManagerAt() should fail when encrypted data exists without key") } } func TestLargeDataEncryption(t *testing.T) { tmpDir := t.TempDir() cm, err := NewCryptoManagerAt(tmpDir) if err != nil { t.Fatalf("NewCryptoManagerAt() error: %v", err) } // Create 1MB of random-ish data largeData := make([]byte, 1024*1024) for i := range largeData { largeData[i] = byte(i % 256) } encrypted, err := cm.Encrypt(largeData) if err != nil { t.Fatalf("Encrypt() large data error: %v", err) } decrypted, err := cm.Decrypt(encrypted) if err != nil { t.Fatalf("Decrypt() large data error: %v", err) } if !bytes.Equal(decrypted, largeData) { t.Error("Large data round-trip failed") } }