corteq 0.1.0

Enterprise-grade multi-tenant SaaS framework for Rust with security-first design
Documentation
//! Encryption module for data at rest using AES-256-GCM
//!
//! This module provides encryption services for sensitive data storage.
//! Each tenant uses their own encryption key derived from their tenant context.
//!
//! # Security Features
//! - AES-256-GCM authenticated encryption
//! - Tenant-specific key derivation
//! - Unique nonces for each encryption operation
//! - Tampering detection via authentication tags
//! - Key isolation between tenants

use crate::{CorteqError, Result, TenantContext};
use aes_gcm::{
    aead::{Aead, KeyInit, OsRng},
    Aes256Gcm, Nonce,
};
use rand::RngCore;
use serde::{Deserialize, Serialize};

/// AES-GCM nonce size in bytes (96 bits / 12 bytes is standard)
const NONCE_SIZE: usize = 12;

/// Encryption service for tenant data at rest
///
/// Provides AES-256-GCM encryption with tenant-specific keys.
/// Each encryption operation uses a unique randomly-generated nonce.
#[derive(Debug, Clone)]
pub struct EncryptionService;

impl EncryptionService {
    /// Create a new encryption service
    pub fn new() -> Self {
        Self
    }

    /// Encrypt data using the tenant's encryption key
    ///
    /// # Example
    ///
    /// ```rust
    /// use corteq::encryption::EncryptionService;
    /// use corteq::TenantContext;
    /// use uuid::Uuid;
    ///
    /// let tenant_ctx = TenantContext::new(
    ///     Uuid::new_v4(),
    ///     "acme-corp".to_string(),
    ///     "encryption-key-32-bytes-long!".to_string(),
    /// );
    ///
    /// let service = EncryptionService::new();
    /// let plaintext = b"Sensitive customer data";
    ///
    /// let encrypted = service.encrypt(plaintext, &tenant_ctx)
    ///     .expect("Encryption failed");
    /// ```
    pub fn encrypt(&self, plaintext: &[u8], tenant_ctx: &TenantContext) -> Result<EncryptedData> {
        // Derive encryption key from tenant context
        let key = derive_key(&tenant_ctx.encryption_key_id)?;

        // Create cipher
        let cipher = Aes256Gcm::new(&key);

        // Generate random nonce
        let mut nonce_bytes = [0u8; NONCE_SIZE];
        OsRng.fill_bytes(&mut nonce_bytes);
        let nonce = Nonce::from_slice(&nonce_bytes);

        // Encrypt
        let ciphertext = cipher
            .encrypt(nonce, plaintext)
            .map_err(|e| CorteqError::EncryptionError(format!("Encryption failed: {e}")))?;

        Ok(EncryptedData {
            nonce: nonce_bytes.to_vec(),
            ciphertext,
        })
    }

    /// Decrypt data using the tenant's encryption key
    ///
    /// Returns an error if:
    /// - The data has been tampered with (authentication tag mismatch)
    /// - The wrong encryption key is used
    /// - The nonce is invalid
    pub fn decrypt(
        &self,
        encrypted: &EncryptedData,
        tenant_ctx: &TenantContext,
    ) -> Result<Vec<u8>> {
        // Derive encryption key from tenant context
        let key = derive_key(&tenant_ctx.encryption_key_id)?;

        // Create cipher
        let cipher = Aes256Gcm::new(&key);

        // Validate nonce size
        if encrypted.nonce.len() != NONCE_SIZE {
            return Err(CorteqError::EncryptionError(format!(
                "Invalid nonce size: expected {}, got {}",
                NONCE_SIZE,
                encrypted.nonce.len()
            )));
        }

        let nonce = Nonce::from_slice(&encrypted.nonce);

        // Decrypt
        let plaintext = cipher
            .decrypt(nonce, encrypted.ciphertext.as_ref())
            .map_err(|e| {
                CorteqError::EncryptionError(format!(
                    "Decryption failed (wrong key or tampered data): {e}"
                ))
            })?;

        Ok(plaintext)
    }
}

impl Default for EncryptionService {
    fn default() -> Self {
        Self::new()
    }
}

/// Encrypted data with nonce
///
/// This structure contains the ciphertext and nonce needed for decryption.
/// It can be serialized for database storage.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EncryptedData {
    /// Nonce (number used once) - must be unique for each encryption
    nonce: Vec<u8>,

    /// Encrypted data with authentication tag
    ciphertext: Vec<u8>,
}

impl EncryptedData {
    /// Get the nonce
    pub fn nonce(&self) -> &[u8] {
        &self.nonce
    }

    /// Get the ciphertext
    pub fn ciphertext(&self) -> &[u8] {
        &self.ciphertext
    }

    /// Set the ciphertext (for testing tampering)
    ///
    /// # Warning
    /// This method is intended for testing only. Do not use in production code.
    #[doc(hidden)]
    pub fn set_ciphertext(&mut self, ciphertext: Vec<u8>) {
        self.ciphertext = ciphertext;
    }

    /// Set the nonce (for testing invalid nonces)
    ///
    /// # Warning
    /// This method is intended for testing only. Do not use in production code.
    #[doc(hidden)]
    pub fn set_nonce(&mut self, nonce: Vec<u8>) {
        self.nonce = nonce;
    }

    /// Serialize to bytes for database storage
    ///
    /// Format: [nonce_len: u32][nonce][ciphertext]
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::new();

        // Write nonce length (4 bytes)
        let nonce_len = self.nonce.len() as u32;
        bytes.extend_from_slice(&nonce_len.to_le_bytes());

        // Write nonce
        bytes.extend_from_slice(&self.nonce);

        // Write ciphertext
        bytes.extend_from_slice(&self.ciphertext);

        bytes
    }

    /// Deserialize from bytes
    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
        if bytes.len() < 4 {
            return Err(CorteqError::EncryptionError(
                "Invalid encrypted data: too short".to_string(),
            ));
        }

        // Read nonce length
        let nonce_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;

        if bytes.len() < 4 + nonce_len {
            return Err(CorteqError::EncryptionError(format!(
                "Invalid encrypted data: expected {} bytes, got {}",
                4 + nonce_len,
                bytes.len()
            )));
        }

        // Read nonce
        let nonce = bytes[4..4 + nonce_len].to_vec();

        // Read ciphertext
        let ciphertext = bytes[4 + nonce_len..].to_vec();

        Ok(EncryptedData { nonce, ciphertext })
    }
}

/// Derive a 32-byte encryption key from the tenant's encryption key ID
///
/// This uses SHA-256 hashing for key derivation to ensure:
/// - Fixed-length output (32 bytes for AES-256)
/// - Consistent key for same input (deterministic)
/// - One-way function
///
/// Note: In production, tenant encryption keys should be:
/// - Generated using cryptographically secure random number generators
/// - Already 32 bytes (256 bits) of high entropy
/// - Stored securely (e.g., in a key management service)
///
/// This function allows using arbitrary strings as keys for development/testing.
fn derive_key(encryption_key_id: &str) -> Result<aes_gcm::Key<Aes256Gcm>> {
    use ring::digest;

    // Use SHA-256 to derive a 32-byte key from the encryption key ID
    // This ensures:
    // 1. Same input always produces same output (deterministic)
    // 2. Output is exactly 32 bytes (required for AES-256)
    // 3. One-way function (can't reverse to get original key)
    let hash = digest::digest(&digest::SHA256, encryption_key_id.as_bytes());
    let hash_bytes = hash.as_ref();

    // Verify we have 32 bytes (SHA-256 always produces 32 bytes)
    assert_eq!(hash_bytes.len(), 32, "SHA-256 should produce 32 bytes");

    let mut key_bytes = [0u8; 32];
    key_bytes.copy_from_slice(hash_bytes);

    Ok(aes_gcm::Key::<Aes256Gcm>::from(key_bytes))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_key_derivation_consistent() {
        let key_id = "test-key-123";
        let key1 = derive_key(key_id).expect("Key derivation 1 failed");
        let key2 = derive_key(key_id).expect("Key derivation 2 failed");

        assert_eq!(
            key1.as_slice(),
            key2.as_slice(),
            "Same key ID should produce same derived key"
        );
    }

    #[test]
    fn test_key_derivation_different_inputs() {
        let key1 = derive_key("key-1").expect("Key derivation 1 failed");
        let key2 = derive_key("key-2").expect("Key derivation 2 failed");

        assert_ne!(
            key1.as_slice(),
            key2.as_slice(),
            "Different key IDs should produce different derived keys"
        );
    }

    #[test]
    fn test_encrypted_data_serialization() {
        let data = EncryptedData {
            nonce: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
            ciphertext: vec![100, 101, 102, 103, 104],
        };

        let bytes = data.to_bytes();
        let restored = EncryptedData::from_bytes(&bytes).expect("Deserialization failed");

        assert_eq!(restored.nonce, data.nonce);
        assert_eq!(restored.ciphertext, data.ciphertext);
    }
}