firecloud-crypto 0.1.0

Encryption and key management for FireCloud distributed storage
Documentation
//! XChaCha20-Poly1305 encryption/decryption

use crate::{CryptoError, CryptoResult, KEY_SIZE, NONCE_SIZE};
use chacha20poly1305::{
    aead::{Aead, KeyInit},
    XChaCha20Poly1305, XNonce,
};
use rand::rngs::OsRng;

/// Encrypt data using XChaCha20-Poly1305
///
/// Returns: nonce (24 bytes) || ciphertext || tag (16 bytes)
pub fn encrypt(key: &[u8; KEY_SIZE], plaintext: &[u8]) -> CryptoResult<Vec<u8>> {
    let cipher = XChaCha20Poly1305::new_from_slice(key)
        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;

    // Generate random nonce
    let mut nonce_bytes = [0u8; NONCE_SIZE];
    rand::RngCore::fill_bytes(&mut OsRng, &mut nonce_bytes);
    let nonce = XNonce::from_slice(&nonce_bytes);

    // Encrypt
    let ciphertext = cipher
        .encrypt(nonce, plaintext)
        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;

    // Prepend nonce to ciphertext
    let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
    result.extend_from_slice(&nonce_bytes);
    result.extend_from_slice(&ciphertext);

    Ok(result)
}

/// Decrypt data encrypted with XChaCha20-Poly1305
///
/// Input: nonce (24 bytes) || ciphertext || tag (16 bytes)
pub fn decrypt(key: &[u8; KEY_SIZE], ciphertext: &[u8]) -> CryptoResult<Vec<u8>> {
    if ciphertext.len() < NONCE_SIZE {
        return Err(CryptoError::DecryptionFailed(
            "Ciphertext too short".to_string(),
        ));
    }

    let cipher = XChaCha20Poly1305::new_from_slice(key)
        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;

    // Extract nonce
    let nonce = XNonce::from_slice(&ciphertext[..NONCE_SIZE]);
    let encrypted_data = &ciphertext[NONCE_SIZE..];

    // Decrypt
    cipher
        .decrypt(nonce, encrypted_data)
        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
}

/// Encrypt a DEK with the KEK
/// Note: Currently used via Kek struct methods in keys.rs
#[allow(dead_code)]
pub fn encrypt_dek(kek: &[u8; KEY_SIZE], dek: &[u8; KEY_SIZE]) -> CryptoResult<Vec<u8>> {
    encrypt(kek, dek)
}

/// Decrypt a DEK with the KEK
/// Note: Currently used via Kek struct methods in keys.rs
#[allow(dead_code)]
pub fn decrypt_dek(kek: &[u8; KEY_SIZE], encrypted_dek: &[u8]) -> CryptoResult<[u8; KEY_SIZE]> {
    let decrypted = decrypt(kek, encrypted_dek)?;
    if decrypted.len() != KEY_SIZE {
        return Err(CryptoError::DecryptionFailed(
            "Invalid DEK length".to_string(),
        ));
    }
    let mut dek = [0u8; KEY_SIZE];
    dek.copy_from_slice(&decrypted);
    Ok(dek)
}

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

    #[test]
    fn test_encrypt_decrypt_roundtrip() {
        let key = [0x42u8; KEY_SIZE];
        let plaintext = b"Hello, FireCloud!";

        let ciphertext = encrypt(&key, plaintext).unwrap();
        let decrypted = decrypt(&key, &ciphertext).unwrap();

        assert_eq!(decrypted, plaintext);
    }

    #[test]
    fn test_dek_encryption() {
        let kek = [0x42u8; KEY_SIZE];
        let dek = [0x55u8; KEY_SIZE];

        let encrypted = encrypt_dek(&kek, &dek).unwrap();
        let decrypted = decrypt_dek(&kek, &encrypted).unwrap();

        assert_eq!(decrypted, dek);
    }

    #[test]
    fn test_wrong_key_fails() {
        let key1 = [0x42u8; KEY_SIZE];
        let key2 = [0x43u8; KEY_SIZE];
        let plaintext = b"Secret data";

        let ciphertext = encrypt(&key1, plaintext).unwrap();
        let result = decrypt(&key2, &ciphertext);

        assert!(result.is_err());
    }
}