Skip to main content

reddb_server/storage/encryption/
page_encryptor.rs

1//! Per-page encryption adapter binding a [`SecureKey`] to the
2//! canonical `reddb-io-crypto` envelope (#1053, ADR 0054).
3//!
4//! This type used to carry its own AES-256-GCM framing (a magic-less
5//! `nonce ‖ ct ‖ tag` layout with a UUIDv4-truncated nonce). That
6//! framing is **retired**: the byte-format and parameters now live in
7//! `reddb-io-crypto`, and the rival self-describing `RDEP` envelope is
8//! retired too. `PageEncryptor` survives only as a server-local
9//! convenience wrapper that holds a key (with secure zeroing on drop)
10//! and delegates to the canonical free functions. The on-disk frame is
11//! byte-identical to the previous `PageEncryptor` output, so the
12//! dormant pager wiring and the page-0 `key_check` blob are unchanged.
13
14use super::key::SecureKey;
15
16/// Size of the nonce (IV) in bytes.
17pub const NONCE_SIZE: usize = reddb_crypto::NONCE_SIZE;
18/// Size of the authentication tag in bytes.
19pub const TAG_SIZE: usize = reddb_crypto::TAG_SIZE;
20/// Total encryption overhead per page (nonce + tag = 28).
21pub const OVERHEAD: usize = reddb_crypto::PAGE_ENVELOPE_OVERHEAD;
22
23/// Binds a [`SecureKey`] to the canonical per-page envelope.
24pub struct PageEncryptor {
25    key: SecureKey,
26}
27
28impl PageEncryptor {
29    /// Create a new page encryptor.
30    pub fn new(key: SecureKey) -> Self {
31        Self { key }
32    }
33
34    /// Encrypt a page through the canonical envelope.
35    ///
36    /// Layout: `[nonce (12)] [ciphertext (N)] [tag (16)]`; the
37    /// `plaintext` of size N yields N + [`OVERHEAD`] bytes. The caller
38    /// ensures the plaintext fits the target page size (e.g. 4068
39    /// bytes → a 4096-byte page). `page_id` is bound as AAD.
40    pub fn encrypt(&self, page_id: u32, plaintext: &[u8]) -> Vec<u8> {
41        reddb_crypto::encrypt_page(self.key_bytes(), page_id, plaintext)
42            .expect("page envelope encryption failed (CSPRNG)")
43    }
44
45    /// Decrypt a page produced by [`Self::encrypt`].
46    pub fn decrypt(&self, page_id: u32, encrypted_data: &[u8]) -> Result<Vec<u8>, String> {
47        reddb_crypto::decrypt_page(self.key_bytes(), page_id, encrypted_data)
48            .map_err(|e| e.to_string())
49    }
50
51    fn key_bytes(&self) -> &[u8; 32] {
52        self.key
53            .as_bytes()
54            .try_into()
55            .expect("Key must be 32 bytes")
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_page_encryption_roundtrip() {
65        let key = SecureKey::new(&[0x42u8; 32]);
66        let encryptor = PageEncryptor::new(key);
67
68        let page_id = 123;
69        let plaintext = b"This is a secret page content.";
70
71        let encrypted = encryptor.encrypt(page_id, plaintext);
72
73        // Size check
74        assert_eq!(encrypted.len(), plaintext.len() + OVERHEAD);
75
76        // Decrypt
77        let decrypted = encryptor.decrypt(page_id, &encrypted).unwrap();
78        assert_eq!(decrypted, plaintext);
79    }
80
81    #[test]
82    fn test_page_encryption_bad_page_id() {
83        let key = SecureKey::new(&[0x42u8; 32]);
84        let encryptor = PageEncryptor::new(key);
85
86        let plaintext = b"content";
87        let encrypted = encryptor.encrypt(100, plaintext);
88
89        // Try decrypting with wrong page ID
90        let result = encryptor.decrypt(101, &encrypted);
91        assert!(result.is_err());
92    }
93
94    #[test]
95    fn test_page_encryption_tampering() {
96        let key = SecureKey::new(&[0x42u8; 32]);
97        let encryptor = PageEncryptor::new(key);
98
99        let plaintext = b"content";
100        let mut encrypted = encryptor.encrypt(100, plaintext);
101
102        // Tamper with the last byte (tag)
103        let last = encrypted.len() - 1;
104        encrypted[last] ^= 1;
105
106        let result = encryptor.decrypt(100, &encrypted);
107        assert!(result.is_err());
108    }
109}