Skip to main content

reddb_server/storage/encryption/
page_encryptor.rs

1//! Per-page Encryption using AES-256-GCM
2//!
3//! Handles encryption and decryption of individual pages.
4//! Uses a unique nonce per page (stored with the page) and authenticates
5//! the Page ID to prevent page swapping attacks.
6
7use super::key::SecureKey;
8use crate::crypto::aes_gcm::{aes256_gcm_decrypt, aes256_gcm_encrypt};
9use crate::crypto::uuid::Uuid;
10
11/// Size of the nonce (IV) in bytes
12pub const NONCE_SIZE: usize = 12;
13/// Size of the authentication tag in bytes
14pub const TAG_SIZE: usize = 16;
15/// Total encryption overhead per page (Nonce + Tag)
16pub const OVERHEAD: usize = NONCE_SIZE + TAG_SIZE;
17
18/// Handles page encryption/decryption
19pub struct PageEncryptor {
20    key: SecureKey,
21}
22
23impl PageEncryptor {
24    /// Create a new page encryptor
25    pub fn new(key: SecureKey) -> Self {
26        Self { key }
27    }
28
29    /// Encrypt a page
30    ///
31    /// Layout:
32    /// `[Nonce (12 bytes)] [Ciphertext (N bytes)] [Tag (16 bytes)]`
33    ///
34    /// The `plaintext` size N will result in an output of size N + 28 bytes.
35    /// The caller is responsible for ensuring the plaintext fits within the
36    /// target page size (e.g., passing 4068 bytes to get a 4096 byte page).
37    pub fn encrypt(&self, page_id: u32, plaintext: &[u8]) -> Vec<u8> {
38        // Generate random nonce (12 bytes)
39        // We use uuid v4 (16 random bytes) and truncate to 12
40        let uuid = Uuid::new_v4();
41        let mut nonce = [0u8; NONCE_SIZE];
42        nonce.copy_from_slice(&uuid.as_bytes()[0..NONCE_SIZE]);
43
44        // AAD is the Page ID (prevents moving pages to different IDs)
45        let aad = page_id.to_le_bytes();
46
47        let key: &[u8; 32] = self
48            .key
49            .as_bytes()
50            .try_into()
51            .expect("Key must be 32 bytes");
52
53        // Encrypt: returns Ciphertext || Tag
54        let ciphertext_with_tag = aes256_gcm_encrypt(key, &nonce, &aad, plaintext);
55
56        // Result: Nonce || Ciphertext || Tag
57        let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext_with_tag.len());
58        result.extend_from_slice(&nonce);
59        result.extend_from_slice(&ciphertext_with_tag);
60
61        result
62    }
63
64    /// Decrypt a page
65    pub fn decrypt(&self, page_id: u32, encrypted_data: &[u8]) -> Result<Vec<u8>, String> {
66        if encrypted_data.len() < OVERHEAD {
67            return Err("Encrypted data too short".to_string());
68        }
69
70        // Extract parts
71        let nonce = &encrypted_data[..NONCE_SIZE];
72        let ciphertext_with_tag = &encrypted_data[NONCE_SIZE..];
73
74        let mut nonce_arr = [0u8; NONCE_SIZE];
75        nonce_arr.copy_from_slice(nonce);
76
77        // AAD must match
78        let aad = page_id.to_le_bytes();
79
80        let key: &[u8; 32] = self
81            .key
82            .as_bytes()
83            .try_into()
84            .expect("Key must be 32 bytes");
85
86        aes256_gcm_decrypt(key, &nonce_arr, &aad, ciphertext_with_tag)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_page_encryption_roundtrip() {
96        let key = SecureKey::new(&[0x42u8; 32]);
97        let encryptor = PageEncryptor::new(key);
98
99        let page_id = 123;
100        let plaintext = b"This is a secret page content.";
101
102        let encrypted = encryptor.encrypt(page_id, plaintext);
103
104        // Size check
105        assert_eq!(encrypted.len(), plaintext.len() + OVERHEAD);
106
107        // Decrypt
108        let decrypted = encryptor.decrypt(page_id, &encrypted).unwrap();
109        assert_eq!(decrypted, plaintext);
110    }
111
112    #[test]
113    fn test_page_encryption_bad_page_id() {
114        let key = SecureKey::new(&[0x42u8; 32]);
115        let encryptor = PageEncryptor::new(key);
116
117        let plaintext = b"content";
118        let encrypted = encryptor.encrypt(100, plaintext);
119
120        // Try decrypting with wrong page ID
121        let result = encryptor.decrypt(101, &encrypted);
122        assert!(result.is_err());
123    }
124
125    #[test]
126    fn test_page_encryption_tampering() {
127        let key = SecureKey::new(&[0x42u8; 32]);
128        let encryptor = PageEncryptor::new(key);
129
130        let plaintext = b"content";
131        let mut encrypted = encryptor.encrypt(100, plaintext);
132
133        // Tamper with the last byte (tag)
134        let last = encrypted.len() - 1;
135        encrypted[last] ^= 1;
136
137        let result = encryptor.decrypt(100, &encrypted);
138        assert!(result.is_err());
139    }
140}