corteq 0.1.0

Enterprise-grade multi-tenant SaaS framework for Rust with security-first design
Documentation
//! Encryption tests for AES-256-GCM data at rest
//!
//! These tests verify that the framework properly encrypts sensitive data
//! using AES-256-GCM with tenant-specific keys:
//! - Encryption/decryption round-trip
//! - Tenant key isolation
//! - Tampering detection
//! - Nonce uniqueness
//! - Key derivation

use corteq::encryption::{EncryptedData, EncryptionService};
use corteq::TenantContext;
use uuid::Uuid;

/// Create a tenant context for testing
fn create_tenant_context(tenant_id: Uuid, encryption_key: &str) -> TenantContext {
    TenantContext::new(
        tenant_id,
        "test-tenant".to_string(),
        encryption_key.to_string(),
    )
}

#[test]
fn test_encryption_basic_round_trip() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "test-encryption-key-32-bytes!!");

    let service = EncryptionService::new();
    let plaintext = b"Hello, World! This is sensitive data.";

    // Encrypt
    let encrypted = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Encryption should succeed");

    // Verify encrypted data is different from plaintext
    assert_ne!(encrypted.ciphertext(), plaintext);
    assert!(!encrypted.ciphertext().is_empty());

    // Decrypt
    let decrypted = service
        .decrypt(&encrypted, &tenant_ctx)
        .expect("Decryption should succeed");

    assert_eq!(decrypted, plaintext);
}

#[test]
fn test_encryption_different_tenants_different_keys() {
    let tenant1_id = Uuid::new_v4();
    let tenant2_id = Uuid::new_v4();

    let tenant1_ctx = create_tenant_context(tenant1_id, "tenant1-key-32-bytes-long!!!!");
    let tenant2_ctx = create_tenant_context(tenant2_id, "tenant2-key-32-bytes-long!!!!");

    let service = EncryptionService::new();
    let plaintext = b"Shared secret message";

    // Encrypt with tenant 1's key
    let encrypted1 = service
        .encrypt(plaintext, &tenant1_ctx)
        .expect("Encryption with tenant 1 key should succeed");

    // Encrypt with tenant 2's key
    let encrypted2 = service
        .encrypt(plaintext, &tenant2_ctx)
        .expect("Encryption with tenant 2 key should succeed");

    // Ciphertexts should be different (different keys + nonces)
    assert_ne!(encrypted1.ciphertext(), encrypted2.ciphertext());

    // Tenant 1 can decrypt their own data
    let decrypted1 = service
        .decrypt(&encrypted1, &tenant1_ctx)
        .expect("Tenant 1 should decrypt their own data");
    assert_eq!(decrypted1, plaintext);

    // Tenant 2 can decrypt their own data
    let decrypted2 = service
        .decrypt(&encrypted2, &tenant2_ctx)
        .expect("Tenant 2 should decrypt their own data");
    assert_eq!(decrypted2, plaintext);

    // Tenant 1 CANNOT decrypt tenant 2's data
    let cross_decrypt_result = service.decrypt(&encrypted2, &tenant1_ctx);
    assert!(
        cross_decrypt_result.is_err(),
        "Tenant 1 should NOT be able to decrypt tenant 2's data"
    );

    // Tenant 2 CANNOT decrypt tenant 1's data
    let cross_decrypt_result = service.decrypt(&encrypted1, &tenant2_ctx);
    assert!(
        cross_decrypt_result.is_err(),
        "Tenant 2 should NOT be able to decrypt tenant 1's data"
    );
}

#[test]
fn test_encryption_detects_tampering() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "test-tamper-key-32-bytes-long!");

    let service = EncryptionService::new();
    let plaintext = b"Original message";

    let mut encrypted = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Encryption should succeed");

    // Tamper with the ciphertext
    let mut tampered_ciphertext = encrypted.ciphertext().to_vec();
    if !tampered_ciphertext.is_empty() {
        tampered_ciphertext[0] ^= 0xFF; // Flip bits in first byte
    }

    encrypted.set_ciphertext(tampered_ciphertext);

    // Decryption should fail due to authentication tag mismatch
    let result = service.decrypt(&encrypted, &tenant_ctx);
    assert!(result.is_err(), "Decryption of tampered data should fail");
}

#[test]
fn test_encryption_nonce_uniqueness() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "nonce-test-key-32-bytes-long!");

    let service = EncryptionService::new();
    let plaintext = b"Same plaintext";

    // Encrypt the same plaintext multiple times
    let encrypted1 = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("First encryption should succeed");

    let encrypted2 = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Second encryption should succeed");

    let encrypted3 = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Third encryption should succeed");

    // Nonces should be different
    assert_ne!(encrypted1.nonce(), encrypted2.nonce());
    assert_ne!(encrypted2.nonce(), encrypted3.nonce());
    assert_ne!(encrypted1.nonce(), encrypted3.nonce());

    // Ciphertexts should be different (due to different nonces)
    assert_ne!(encrypted1.ciphertext(), encrypted2.ciphertext());
    assert_ne!(encrypted2.ciphertext(), encrypted3.ciphertext());

    // But all should decrypt to the same plaintext
    let decrypted1 = service
        .decrypt(&encrypted1, &tenant_ctx)
        .expect("Decrypt 1");
    let decrypted2 = service
        .decrypt(&encrypted2, &tenant_ctx)
        .expect("Decrypt 2");
    let decrypted3 = service
        .decrypt(&encrypted3, &tenant_ctx)
        .expect("Decrypt 3");

    assert_eq!(decrypted1, plaintext);
    assert_eq!(decrypted2, plaintext);
    assert_eq!(decrypted3, plaintext);
}

#[test]
fn test_encryption_empty_data() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "empty-test-key-32-bytes-long!");

    let service = EncryptionService::new();
    let plaintext = b"";

    let encrypted = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Encrypting empty data should succeed");

    let decrypted = service
        .decrypt(&encrypted, &tenant_ctx)
        .expect("Decrypting empty data should succeed");

    assert_eq!(decrypted, plaintext);
    assert!(decrypted.is_empty());
}

#[test]
fn test_encryption_large_data() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "large-data-key-32-bytes-long!");

    let service = EncryptionService::new();

    // Create 1MB of data
    let plaintext = vec![0xAB; 1024 * 1024];

    let encrypted = service
        .encrypt(&plaintext, &tenant_ctx)
        .expect("Encrypting large data should succeed");

    let decrypted = service
        .decrypt(&encrypted, &tenant_ctx)
        .expect("Decrypting large data should succeed");

    assert_eq!(decrypted, plaintext);
}

#[test]
fn test_encryption_key_derivation_consistent() {
    let tenant_id = Uuid::new_v4();
    let encryption_key = "consistent-key-32-bytes-long!";
    let tenant_ctx = create_tenant_context(tenant_id, encryption_key);

    let service = EncryptionService::new();
    let plaintext = b"Test message";

    // Encrypt
    let encrypted = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Encryption should succeed");

    // Create a new service instance (simulating app restart)
    let service2 = EncryptionService::new();

    // Create same tenant context again
    let tenant_ctx2 = create_tenant_context(tenant_id, encryption_key);

    // Should be able to decrypt with new service instance
    let decrypted = service2
        .decrypt(&encrypted, &tenant_ctx2)
        .expect("Decryption with new service should succeed");

    assert_eq!(decrypted, plaintext);
}

#[test]
fn test_encryption_invalid_nonce_length() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "invalid-nonce-key-32-bytes-lo!");

    let service = EncryptionService::new();
    let plaintext = b"Test";

    let mut encrypted = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Encryption should succeed");

    // Create invalid nonce (wrong length)
    let invalid_nonce = vec![0u8; 8]; // AES-GCM typically uses 12 bytes
    encrypted.set_nonce(invalid_nonce);

    let result = service.decrypt(&encrypted, &tenant_ctx);
    assert!(
        result.is_err(),
        "Decryption with invalid nonce length should fail"
    );
}

#[test]
fn test_encryption_serialization() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "serialization-key-32-bytes-lo!");

    let service = EncryptionService::new();
    let plaintext = b"Data to serialize";

    let encrypted = service
        .encrypt(plaintext, &tenant_ctx)
        .expect("Encryption should succeed");

    // Serialize to bytes (for database storage)
    let serialized = encrypted.to_bytes();
    assert!(!serialized.is_empty());

    // Deserialize back
    let deserialized =
        EncryptedData::from_bytes(&serialized).expect("Deserialization should succeed");

    // Verify nonce and ciphertext are preserved
    assert_eq!(deserialized.nonce(), encrypted.nonce());
    assert_eq!(deserialized.ciphertext(), encrypted.ciphertext());

    // Should still decrypt correctly
    let decrypted = service
        .decrypt(&deserialized, &tenant_ctx)
        .expect("Decryption after serialization should succeed");

    assert_eq!(decrypted, plaintext);
}

#[test]
fn test_encryption_utf8_text() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx = create_tenant_context(tenant_id, "utf8-test-key-32-bytes-long!!");

    let service = EncryptionService::new();
    let plaintext = "Hello δΈ–η•Œ! πŸ¦€ Emoji and unicode ζ—₯本θͺž";

    let encrypted = service
        .encrypt(plaintext.as_bytes(), &tenant_ctx)
        .expect("Encryption should succeed");

    let decrypted = service
        .decrypt(&encrypted, &tenant_ctx)
        .expect("Decryption should succeed");

    let decrypted_text = String::from_utf8(decrypted).expect("Should be valid UTF-8");

    assert_eq!(decrypted_text, plaintext);
}

#[test]
fn test_encryption_wrong_key_fails() {
    let tenant_id = Uuid::new_v4();
    let tenant_ctx1 = create_tenant_context(tenant_id, "correct-key-32-bytes-long!!!");
    let tenant_ctx2 = create_tenant_context(tenant_id, "wrong-key-32-bytes-long!!!!!");

    let service = EncryptionService::new();
    let plaintext = b"Secret data";

    // Encrypt with first key
    let encrypted = service
        .encrypt(plaintext, &tenant_ctx1)
        .expect("Encryption should succeed");

    // Try to decrypt with different key (same tenant ID but different key)
    let result = service.decrypt(&encrypted, &tenant_ctx2);
    assert!(
        result.is_err(),
        "Decryption with wrong key should fail even for same tenant"
    );
}