Skip to main content

firecloud_crypto/
cipher.rs

1//! XChaCha20-Poly1305 encryption/decryption
2
3use crate::{CryptoError, CryptoResult, KEY_SIZE, NONCE_SIZE};
4use chacha20poly1305::{
5    aead::{Aead, KeyInit},
6    XChaCha20Poly1305, XNonce,
7};
8use rand::rngs::OsRng;
9
10/// Encrypt data using XChaCha20-Poly1305
11///
12/// Returns: nonce (24 bytes) || ciphertext || tag (16 bytes)
13pub fn encrypt(key: &[u8; KEY_SIZE], plaintext: &[u8]) -> CryptoResult<Vec<u8>> {
14    let cipher = XChaCha20Poly1305::new_from_slice(key)
15        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
16
17    // Generate random nonce
18    let mut nonce_bytes = [0u8; NONCE_SIZE];
19    rand::RngCore::fill_bytes(&mut OsRng, &mut nonce_bytes);
20    let nonce = XNonce::from_slice(&nonce_bytes);
21
22    // Encrypt
23    let ciphertext = cipher
24        .encrypt(nonce, plaintext)
25        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
26
27    // Prepend nonce to ciphertext
28    let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
29    result.extend_from_slice(&nonce_bytes);
30    result.extend_from_slice(&ciphertext);
31
32    Ok(result)
33}
34
35/// Decrypt data encrypted with XChaCha20-Poly1305
36///
37/// Input: nonce (24 bytes) || ciphertext || tag (16 bytes)
38pub fn decrypt(key: &[u8; KEY_SIZE], ciphertext: &[u8]) -> CryptoResult<Vec<u8>> {
39    if ciphertext.len() < NONCE_SIZE {
40        return Err(CryptoError::DecryptionFailed(
41            "Ciphertext too short".to_string(),
42        ));
43    }
44
45    let cipher = XChaCha20Poly1305::new_from_slice(key)
46        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
47
48    // Extract nonce
49    let nonce = XNonce::from_slice(&ciphertext[..NONCE_SIZE]);
50    let encrypted_data = &ciphertext[NONCE_SIZE..];
51
52    // Decrypt
53    cipher
54        .decrypt(nonce, encrypted_data)
55        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
56}
57
58/// Encrypt a DEK with the KEK
59/// Note: Currently used via Kek struct methods in keys.rs
60#[allow(dead_code)]
61pub fn encrypt_dek(kek: &[u8; KEY_SIZE], dek: &[u8; KEY_SIZE]) -> CryptoResult<Vec<u8>> {
62    encrypt(kek, dek)
63}
64
65/// Decrypt a DEK with the KEK
66/// Note: Currently used via Kek struct methods in keys.rs
67#[allow(dead_code)]
68pub fn decrypt_dek(kek: &[u8; KEY_SIZE], encrypted_dek: &[u8]) -> CryptoResult<[u8; KEY_SIZE]> {
69    let decrypted = decrypt(kek, encrypted_dek)?;
70    if decrypted.len() != KEY_SIZE {
71        return Err(CryptoError::DecryptionFailed(
72            "Invalid DEK length".to_string(),
73        ));
74    }
75    let mut dek = [0u8; KEY_SIZE];
76    dek.copy_from_slice(&decrypted);
77    Ok(dek)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_encrypt_decrypt_roundtrip() {
86        let key = [0x42u8; KEY_SIZE];
87        let plaintext = b"Hello, FireCloud!";
88
89        let ciphertext = encrypt(&key, plaintext).unwrap();
90        let decrypted = decrypt(&key, &ciphertext).unwrap();
91
92        assert_eq!(decrypted, plaintext);
93    }
94
95    #[test]
96    fn test_dek_encryption() {
97        let kek = [0x42u8; KEY_SIZE];
98        let dek = [0x55u8; KEY_SIZE];
99
100        let encrypted = encrypt_dek(&kek, &dek).unwrap();
101        let decrypted = decrypt_dek(&kek, &encrypted).unwrap();
102
103        assert_eq!(decrypted, dek);
104    }
105
106    #[test]
107    fn test_wrong_key_fails() {
108        let key1 = [0x42u8; KEY_SIZE];
109        let key2 = [0x43u8; KEY_SIZE];
110        let plaintext = b"Secret data";
111
112        let ciphertext = encrypt(&key1, plaintext).unwrap();
113        let result = decrypt(&key2, &ciphertext);
114
115        assert!(result.is_err());
116    }
117}