Skip to main content

clt_database/storage/
encryption.rs

1#![allow(unused_variables, dead_code)]
2use crate::turso_assert;
3use crate::{LimboError, Result};
4use aegis::aegis128l::Aegis128L;
5use aegis::aegis128x2::Aegis128X2;
6use aegis::aegis128x4::Aegis128X4;
7use aegis::aegis256::Aegis256;
8use aegis::aegis256x2::Aegis256X2;
9use aegis::aegis256x4::Aegis256X4;
10use aes_gcm::{
11    aead::{Aead, AeadCore, AeadInPlace, KeyInit, OsRng},
12    Aes128Gcm, Aes256Gcm, Key, Nonce,
13};
14use turso_macros::{match_ignore_ascii_case, AtomicEnum};
15
16/// Encryption Scheme
17/// We support two major algorithms: AEGIS, AES GCM. These algorithms picked so that they also do
18/// verification of the ciphertext, so we don't need to implement. That is if the page is corrupted
19/// (or tampered), then we will know if we got garbage bytes post decryption.
20///
21/// We perform encryption at the page level, i.e., each page is encrypted and decrypted individually.
22/// We store the nonce and tag (or the verification bits) in the page itself.  We also generate a
23/// random nonce every time we encrypt a page.
24///
25/// Example: Assume the page size is 4096 bytes and we use AEGIS 256. So we reserve the last 48 bytes
26/// for the nonce (32 bytes) and tag (16 bytes).
27///
28/// ```ignore
29///             Unencrypted Page              Encrypted Page
30///             ┌───────────────┐            ┌───────────────┐
31///             │               │            │               │
32///             │ Page Content  │            │   Encrypted   │
33///             │ (4048 bytes)  │  ────────► │    Content    │
34///             │               │            │ (4048 bytes)  │
35///             ├───────────────┤            ├───────────────┤
36///             │   Reserved    │            │    Tag (16)   │
37///             │  (48 bytes)   │            ├───────────────┤
38///             │   [empty]     │            │   Nonce (32)  │
39///             └───────────────┘            └───────────────┘
40///                4096 bytes                   4096 bytes
41/// ```
42///
43/// The above applies to all the pages except Page 1. The page 1 contains the SQLite header (the
44/// first 100 bytes). Specifically, the bytes 16 to 24 contain metadata which is required to
45/// initialise the connection, which happens before we can setup the encryption context. So, we
46/// don't encrypt the header but instead use the header data as additional data (AD) for the
47/// encryption of the rest of the page. This provides us protection against tampering and
48/// corruption for the unencrypted portion.
49///
50/// On disk, the encrypted page 1 contains special bytes replacing the SQLite's magic bytes (the
51/// first 16 bytes):
52///
53/// ```ignore
54///                    Turso Header (16 bytes)
55///        ┌─────────┬───────┬────────┬──────────────────┐
56///        │         │       │        │                  │
57///        │  Turso  │Version│ Cipher │     Unused       │
58///        │  (5)    │ (1)   │  (1)   │    (9 bytes)     │
59///        │         │       │        │                  │
60///        └─────────┴───────┴────────┴──────────────────┘
61///         0-4      5       6        7-15
62///
63///        Standard SQLite Header: "SQLite format 3\0" (16 bytes)
64///                            ↓
65///        Turso Encrypted Header: "Turso" + Version + Cipher ID + Unused
66/// ```
67///
68/// constants used for the Turso page header in the encrypted dbs.
69pub const TURSO_HEADER_PREFIX: &[u8] = b"Turso";
70pub const SQLITE_HEADER: &[u8] = b"SQLite format 3\0";
71const TURSO_VERSION: u8 = 0x00;
72const VERSION_OFFSET: usize = 5;
73const CIPHER_OFFSET: usize = 6;
74const TURSO_HEADER_SIZE: usize = 16;
75
76#[derive(Clone)]
77pub enum EncryptionKey {
78    Key128([u8; 16]),
79    Key256([u8; 32]),
80}
81
82impl EncryptionKey {
83    pub fn new_256(key: [u8; 32]) -> Self {
84        Self::Key256(key)
85    }
86
87    pub fn new_128(key: [u8; 16]) -> Self {
88        Self::Key128(key)
89    }
90
91    pub fn from_hex_string(s: &str) -> Result<Self> {
92        let hex_str = s.trim();
93        let bytes = hex::decode(hex_str)
94            .map_err(|e| LimboError::InvalidArgument(format!("Invalid hex string: {e}")))?;
95
96        match bytes.len() {
97            16 => {
98                let key: [u8; 16] = bytes.try_into().unwrap();
99                Ok(Self::Key128(key))
100            }
101            32 => {
102                let key: [u8; 32] = bytes.try_into().unwrap();
103                Ok(Self::Key256(key))
104            }
105            _ => Err(LimboError::InvalidArgument(format!(
106                "Hex string must decode to exactly 16 or 32 bytes, got {}",
107                bytes.len()
108            ))),
109        }
110    }
111
112    pub fn as_slice(&self) -> &[u8] {
113        match self {
114            Self::Key128(key) => key,
115            Self::Key256(key) => key,
116        }
117    }
118
119    #[allow(clippy::len_without_is_empty)]
120    pub fn len(&self) -> usize {
121        match self {
122            Self::Key128(_) => 16,
123            Self::Key256(_) => 32,
124        }
125    }
126
127    pub fn as_128(&self) -> Option<&[u8; 16]> {
128        match self {
129            Self::Key128(key) => Some(key),
130            _ => None,
131        }
132    }
133
134    pub fn as_256(&self) -> Option<&[u8; 32]> {
135        match self {
136            Self::Key256(key) => Some(key),
137            _ => None,
138        }
139    }
140}
141
142impl std::fmt::Debug for EncryptionKey {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("EncryptionKey")
145            .field("key", &"<encryption key redacted>")
146            .finish()
147    }
148}
149
150impl Drop for EncryptionKey {
151    fn drop(&mut self) {
152        // securely zero out the key bytes before dropping
153        match self {
154            Self::Key128(key) => {
155                for byte in key.iter_mut() {
156                    unsafe {
157                        std::ptr::write_volatile(byte, 0);
158                    }
159                }
160            }
161            Self::Key256(key) => {
162                for byte in key.iter_mut() {
163                    unsafe {
164                        std::ptr::write_volatile(byte, 0);
165                    }
166                }
167            }
168        }
169    }
170}
171
172macro_rules! define_aegis_cipher {
173    ($struct_name:ident, $cipher_type:ty, key128, $nonce_size:literal, $name:literal) => {
174        define_aegis_cipher!(@impl $struct_name, $cipher_type, $nonce_size, $name, 16, as_128);
175    };
176    ($struct_name:ident, $cipher_type:ty, key256, $nonce_size:literal, $name:literal) => {
177        define_aegis_cipher!(@impl $struct_name, $cipher_type, $nonce_size, $name, 32, as_256);
178    };
179    (@impl $struct_name:ident, $cipher_type:ty, $nonce_size:literal, $name:literal, $key_size:literal, $key_method:ident) => {
180        #[derive(Clone)]
181        pub struct $struct_name {
182            key: EncryptionKey,
183        }
184
185        impl $struct_name {
186            const TAG_SIZE: usize = 16;
187
188            fn new(key: &EncryptionKey) -> Self {
189                Self { key: key.clone() }
190            }
191
192            fn encrypt(&self, plaintext: &[u8], ad: &[u8]) -> Result<(Vec<u8>, [u8; $nonce_size])> {
193                let nonce = generate_secure_nonce::<$nonce_size>();
194                let key_bytes = self.key.$key_method()
195                    .ok_or_else(|| -> LimboError { CipherError::InvalidKeySize { cipher: $name, expected: $key_size }.into() })?;
196                let (ciphertext, tag) = <$cipher_type>::new(key_bytes, &nonce).encrypt(plaintext, ad);
197                let mut result = ciphertext;
198                result.extend_from_slice(&tag);
199                Ok((result, nonce))
200            }
201
202            fn decrypt(&self, ciphertext: &[u8], nonce: &[u8; $nonce_size], ad: &[u8]) -> Result<Vec<u8>> {
203                let mut out = Vec::with_capacity(ciphertext.len().saturating_sub(Self::TAG_SIZE));
204                self.decrypt_into(ciphertext, nonce, ad, &mut out)?;
205                Ok(out)
206            }
207
208            fn decrypt_into(
209                &self,
210                ciphertext: &[u8],
211                nonce: &[u8; $nonce_size],
212                ad: &[u8],
213                out: &mut Vec<u8>,
214            ) -> Result<()> {
215                if ciphertext.len() < Self::TAG_SIZE {
216                    return Err(LimboError::from(CipherError::CiphertextTooShort { cipher: $name }));
217                }
218                let (ct, tag) = ciphertext.split_at(ciphertext.len() - Self::TAG_SIZE);
219                let tag_array: [u8; 16] = tag.try_into().map_err(|_| -> LimboError { CipherError::InvalidTagSize { cipher: $name }.into() })?;
220
221                let key_bytes = self.key.$key_method()
222                    .ok_or_else(|| -> LimboError { CipherError::InvalidKeySize { cipher: $name, expected: $key_size }.into() })?;
223                out.clear();
224                out.extend_from_slice(ct);
225                <$cipher_type>::new(key_bytes, nonce)
226                    .decrypt_in_place(out.as_mut_slice(), &tag_array, ad)
227                    .map_err(|_| -> LimboError { CipherError::DecryptionFailed { cipher: $name }.into() })
228            }
229        }
230
231        impl std::fmt::Debug for $struct_name {
232            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233                f.debug_struct(stringify!($struct_name))
234                    .field("key", &"<redacted>")
235                    .finish()
236            }
237        }
238    };
239}
240
241macro_rules! define_aes_gcm_cipher {
242    ($struct_name:ident, $cipher_type:ty, key128, $name:literal) => {
243        define_aes_gcm_cipher!(@impl $struct_name, $cipher_type, $name, 16, as_128);
244    };
245    ($struct_name:ident, $cipher_type:ty, key256, $name:literal) => {
246        define_aes_gcm_cipher!(@impl $struct_name, $cipher_type, $name, 32, as_256);
247    };
248    (@impl $struct_name:ident, $cipher_type:ty, $name:literal, $key_size:literal, $key_method:ident) => {
249        #[derive(Clone)]
250        pub struct $struct_name {
251            cipher: $cipher_type,
252        }
253
254        impl $struct_name {
255            const TAG_SIZE: usize = 16;
256            const NONCE_SIZE: usize = 12;
257
258            fn new(key: &EncryptionKey) -> Result<Self> {
259                let key_bytes = key.$key_method()
260                    .ok_or_else(|| -> LimboError { CipherError::InvalidKeySize { cipher: $name, expected: $key_size }.into() })?;
261                let cipher_key: &Key<$cipher_type> = key_bytes.into();
262                Ok(Self {
263                    cipher: <$cipher_type>::new(cipher_key),
264                })
265            }
266
267            fn encrypt(&self, plaintext: &[u8], ad: &[u8]) -> Result<(Vec<u8>, [u8; 12])> {
268                let nonce = <$cipher_type>::generate_nonce(&mut OsRng);
269                let ciphertext = self.cipher.encrypt(&nonce, aes_gcm::aead::Payload {
270                    msg: plaintext,
271                    aad: ad,
272                }).map_err(|e| {
273                    LimboError::InternalError(format!("{} encryption failed: {e:?}", $name))
274                })?;
275                let mut nonce_array = [0u8; 12];
276                nonce_array.copy_from_slice(&nonce);
277                Ok((ciphertext, nonce_array))
278            }
279
280            fn decrypt(&self, ciphertext: &[u8], nonce: &[u8; 12], ad: &[u8]) -> Result<Vec<u8>> {
281                let mut out = Vec::with_capacity(ciphertext.len().saturating_sub(Self::TAG_SIZE));
282                self.decrypt_into(ciphertext, nonce, ad, &mut out)?;
283                Ok(out)
284            }
285
286            fn decrypt_into(
287                &self,
288                ciphertext: &[u8],
289                nonce: &[u8; 12],
290                ad: &[u8],
291                out: &mut Vec<u8>,
292            ) -> Result<()> {
293                let nonce = Nonce::from_slice(nonce);
294                out.clear();
295                out.extend_from_slice(ciphertext);
296                self.cipher
297                    .decrypt_in_place(nonce, ad, out)
298                    .map_err(|_| -> LimboError { CipherError::DecryptionFailed { cipher: $name }.into() })
299            }
300        }
301
302        impl std::fmt::Debug for $struct_name {
303            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304                f.debug_struct(stringify!($struct_name))
305                    .field("key", &"<redacted>")
306                    .finish()
307            }
308        }
309    };
310}
311
312// AES-GCM ciphers
313define_aes_gcm_cipher!(Aes128GcmCipher, Aes128Gcm, key128, "AES-128-GCM");
314define_aes_gcm_cipher!(Aes256GcmCipher, Aes256Gcm, key256, "AES-256-GCM");
315
316// AEGIS ciphers
317define_aegis_cipher!(Aegis256Cipher, Aegis256::<16>, key256, 32, "AEGIS-256");
318define_aegis_cipher!(
319    Aegis256X2Cipher,
320    Aegis256X2::<16>,
321    key256,
322    32,
323    "AEGIS-256X2"
324);
325define_aegis_cipher!(
326    Aegis256X4Cipher,
327    Aegis256X4::<16>,
328    key256,
329    32,
330    "AEGIS-256X4"
331);
332define_aegis_cipher!(
333    Aegis128X2Cipher,
334    Aegis128X2::<16>,
335    key128,
336    16,
337    "AEGIS-128X2"
338);
339define_aegis_cipher!(Aegis128LCipher, Aegis128L::<16>, key128, 16, "AEGIS-128L");
340define_aegis_cipher!(
341    Aegis128X4Cipher,
342    Aegis128X4::<16>,
343    key128,
344    16,
345    "AEGIS-128X4"
346);
347
348#[derive(Debug, AtomicEnum, Clone, Copy, PartialEq, Eq)]
349pub enum CipherMode {
350    None,
351    Aes128Gcm,
352    Aes256Gcm,
353    Aegis256,
354    Aegis128L,
355    Aegis128X2,
356    Aegis128X4,
357    Aegis256X2,
358    Aegis256X4,
359}
360
361impl TryFrom<&str> for CipherMode {
362    type Error = LimboError;
363
364    fn try_from(s: &str) -> Result<Self, Self::Error> {
365        let s_bytes = s.as_bytes();
366        match_ignore_ascii_case!(match s_bytes {
367            b"aes128gcm" | b"aes-128-gcm" | b"aes_128_gcm" => Ok(CipherMode::Aes128Gcm),
368            b"aes256gcm" | b"aes-256-gcm" | b"aes_256_gcm" => Ok(CipherMode::Aes256Gcm),
369            b"aegis256" | b"aegis-256" | b"aegis_256" => Ok(CipherMode::Aegis256),
370            b"aegis128l" | b"aegis-128l" | b"aegis_128l" => Ok(CipherMode::Aegis128L),
371            b"aegis128x2" | b"aegis-128x2" | b"aegis_128x2" => Ok(CipherMode::Aegis128X2),
372            b"aegis128x4" | b"aegis-128x4" | b"aegis_128x4" => Ok(CipherMode::Aegis128X4),
373            b"aegis256x2" | b"aegis-256x2" | b"aegis_256x2" => Ok(CipherMode::Aegis256X2),
374            b"aegis256x4" | b"aegis-256x4" | b"aegis_256x4" => Ok(CipherMode::Aegis256X4),
375            _ => Err(LimboError::InvalidArgument(format!(
376                "Unknown cipher name: {s}"
377            ))),
378        })
379    }
380}
381
382impl std::fmt::Display for CipherMode {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        match self {
385            CipherMode::Aes128Gcm => write!(f, "aes128gcm"),
386            CipherMode::Aes256Gcm => write!(f, "aes256gcm"),
387            CipherMode::Aegis256 => write!(f, "aegis256"),
388            CipherMode::Aegis128L => write!(f, "aegis128l"),
389            CipherMode::Aegis128X2 => write!(f, "aegis128x2"),
390            CipherMode::Aegis128X4 => write!(f, "aegis128x4"),
391            CipherMode::Aegis256X2 => write!(f, "aegis256x2"),
392            CipherMode::Aegis256X4 => write!(f, "aegis256x4"),
393            CipherMode::None => write!(f, "None"),
394        }
395    }
396}
397
398impl CipherMode {
399    /// Every cipher requires a specific key size. For 256-bit algorithms, this is 32 bytes.
400    /// For 128-bit algorithms, it would be 16 bytes, etc.
401    pub fn required_key_size(&self) -> usize {
402        match self {
403            CipherMode::Aes128Gcm => 16,
404            CipherMode::Aes256Gcm => 32,
405            CipherMode::Aegis256 => 32,
406            CipherMode::Aegis256X2 => 32,
407            CipherMode::Aegis256X4 => 32,
408            CipherMode::Aegis128L => 16,
409            CipherMode::Aegis128X2 => 16,
410            CipherMode::Aegis128X4 => 16,
411            CipherMode::None => 0,
412        }
413    }
414
415    /// Returns the nonce size for this cipher mode.
416    pub fn nonce_size(&self) -> usize {
417        match self {
418            CipherMode::Aes128Gcm => 12,
419            CipherMode::Aes256Gcm => 12,
420            CipherMode::Aegis256 => 32,
421            CipherMode::Aegis256X2 => 32,
422            CipherMode::Aegis256X4 => 32,
423            CipherMode::Aegis128L => 16,
424            CipherMode::Aegis128X2 => 16,
425            CipherMode::Aegis128X4 => 16,
426            CipherMode::None => 0,
427        }
428    }
429
430    /// Returns the authentication tag size for this cipher mode.
431    pub fn tag_size(&self) -> usize {
432        match self {
433            CipherMode::Aes128Gcm => 16,
434            CipherMode::Aes256Gcm => 16,
435            CipherMode::Aegis256 => 16,
436            CipherMode::Aegis256X2 => 16,
437            CipherMode::Aegis256X4 => 16,
438            CipherMode::Aegis128L => 16,
439            CipherMode::Aegis128X2 => 16,
440            CipherMode::Aegis128X4 => 16,
441            CipherMode::None => 0,
442        }
443    }
444
445    /// Returns the total metadata size (nonce + tag) for this cipher mode.
446    pub fn metadata_size(&self) -> usize {
447        self.nonce_size() + self.tag_size()
448    }
449
450    /// Returns the cipher identifier byte for Turso header
451    pub fn cipher_id(&self) -> u8 {
452        match self {
453            CipherMode::Aes128Gcm => 1,
454            CipherMode::Aes256Gcm => 2,
455            CipherMode::Aegis256 => 3,
456            CipherMode::Aegis256X2 => 4,
457            CipherMode::Aegis256X4 => 5,
458            CipherMode::Aegis128L => 6,
459            CipherMode::Aegis128X2 => 7,
460            CipherMode::Aegis128X4 => 8,
461            CipherMode::None => 0,
462        }
463    }
464
465    /// Creates a CipherMode from cipher identifier byte. This is used when read from Turso header.
466    pub fn from_cipher_id(id: u8) -> Result<Self> {
467        match id {
468            1 => Ok(CipherMode::Aes128Gcm),
469            2 => Ok(CipherMode::Aes256Gcm),
470            3 => Ok(CipherMode::Aegis256),
471            4 => Ok(CipherMode::Aegis256X2),
472            5 => Ok(CipherMode::Aegis256X4),
473            6 => Ok(CipherMode::Aegis128L),
474            7 => Ok(CipherMode::Aegis128X2),
475            8 => Ok(CipherMode::Aegis128X4),
476            _ => Err(LimboError::InvalidArgument(format!(
477                "Unknown cipher ID: {id}"
478            ))),
479        }
480    }
481}
482
483#[derive(Clone)]
484pub enum Cipher {
485    Aes128Gcm(Box<Aes128GcmCipher>),
486    Aes256Gcm(Box<Aes256GcmCipher>),
487    Aegis256(Box<Aegis256Cipher>),
488    Aegis256X2(Box<Aegis256X2Cipher>),
489    Aegis256X4(Box<Aegis256X4Cipher>),
490    Aegis128L(Box<Aegis128LCipher>),
491    Aegis128X2(Box<Aegis128X2Cipher>),
492    Aegis128X4(Box<Aegis128X4Cipher>),
493}
494
495impl std::fmt::Debug for Cipher {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        match self {
498            Cipher::Aes128Gcm(_) => write!(f, "Cipher::Aes128Gcm"),
499            Cipher::Aes256Gcm(_) => write!(f, "Cipher::Aes256Gcm"),
500            Cipher::Aegis256(_) => write!(f, "Cipher::Aegis256"),
501            Cipher::Aegis256X2(_) => write!(f, "Cipher::Aegis256X2"),
502            Cipher::Aegis256X4(_) => write!(f, "Cipher::Aegis256X4"),
503            Cipher::Aegis128L(_) => write!(f, "Cipher::Aegis128L"),
504            Cipher::Aegis128X2(_) => write!(f, "Cipher::Aegis128X2"),
505            Cipher::Aegis128X4(_) => write!(f, "Cipher::Aegis128X4"),
506        }
507    }
508}
509
510#[derive(Debug, Clone)]
511pub struct EncryptionContext {
512    cipher_mode: CipherMode,
513    cipher: Cipher,
514    page_size: usize,
515}
516
517impl EncryptionContext {
518    pub fn new(cipher_mode: CipherMode, key: &EncryptionKey, page_size: usize) -> Result<Self> {
519        let required_size = cipher_mode.required_key_size();
520        if key.len() != required_size {
521            return Err(crate::LimboError::InvalidArgument(format!(
522                "Invalid key size for {:?}: expected {} bytes, got {}",
523                cipher_mode,
524                required_size,
525                key.len()
526            )));
527        }
528
529        let cipher = match cipher_mode {
530            CipherMode::Aes128Gcm => Cipher::Aes128Gcm(Box::new(Aes128GcmCipher::new(key)?)),
531            CipherMode::Aes256Gcm => Cipher::Aes256Gcm(Box::new(Aes256GcmCipher::new(key)?)),
532            CipherMode::Aegis256 => Cipher::Aegis256(Box::new(Aegis256Cipher::new(key))),
533            CipherMode::Aegis256X2 => Cipher::Aegis256X2(Box::new(Aegis256X2Cipher::new(key))),
534            CipherMode::Aegis256X4 => Cipher::Aegis256X4(Box::new(Aegis256X4Cipher::new(key))),
535            CipherMode::Aegis128L => Cipher::Aegis128L(Box::new(Aegis128LCipher::new(key))),
536            CipherMode::Aegis128X2 => Cipher::Aegis128X2(Box::new(Aegis128X2Cipher::new(key))),
537            CipherMode::Aegis128X4 => Cipher::Aegis128X4(Box::new(Aegis128X4Cipher::new(key))),
538            CipherMode::None => {
539                return Err(LimboError::InvalidArgument(
540                    "must select valid CipherMode".into(),
541                ))
542            }
543        };
544        Ok(Self {
545            cipher_mode,
546            cipher,
547            page_size,
548        })
549    }
550
551    pub fn cipher_mode(&self) -> CipherMode {
552        self.cipher_mode
553    }
554
555    /// Returns the number of reserved bytes required at the end of each page for encryption metadata.
556    pub fn required_reserved_bytes(&self) -> u8 {
557        self.cipher_mode.metadata_size() as u8
558    }
559
560    pub fn encrypt_chunk(&self, plaintext: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
561        self.encrypt_raw_with_ad(plaintext, aad)
562    }
563
564    pub fn decrypt_chunk(&self, ciphertext: &[u8], nonce: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
565        self.decrypt_raw_with_ad(ciphertext, nonce, aad)
566    }
567
568    pub fn decrypt_chunk_into(
569        &self,
570        ciphertext: &[u8],
571        nonce: &[u8],
572        aad: &[u8],
573        out: &mut Vec<u8>,
574    ) -> Result<()> {
575        self.decrypt_raw_with_ad_into(ciphertext, nonce, aad, out)
576    }
577
578    pub fn nonce_size(&self) -> usize {
579        self.cipher_mode.nonce_size()
580    }
581
582    pub fn tag_size(&self) -> usize {
583        self.cipher_mode.tag_size()
584    }
585
586    /// Creates Turso header for encrypted page 1
587    fn create_turso_header(&self) -> [u8; TURSO_HEADER_SIZE] {
588        let mut header = [0u8; TURSO_HEADER_SIZE];
589
590        // "Turso" prefix (5 bytes)
591        header[..TURSO_HEADER_PREFIX.len()].copy_from_slice(TURSO_HEADER_PREFIX);
592
593        // version byte (1 byte)
594        header[VERSION_OFFSET] = TURSO_VERSION;
595
596        // cipher identifier (1 byte)
597        header[CIPHER_OFFSET] = self.cipher_mode.cipher_id();
598
599        // remaining unused 9 bytes
600        header
601    }
602
603    /// Validates and extracts cipher mode from Turso header
604    fn validate_turso_header(&self, header: &[u8]) -> Result<()> {
605        if header.len() < TURSO_HEADER_SIZE {
606            return Err(LimboError::InternalError(
607                "Header too short for encrypted Turso db".into(),
608            ));
609        }
610
611        if &header[..TURSO_HEADER_PREFIX.len()] != TURSO_HEADER_PREFIX {
612            return Err(LimboError::InternalError(
613                "Invalid Turso header: prefix mismatch".into(),
614            ));
615        }
616
617        let version = header[VERSION_OFFSET];
618        if version != TURSO_VERSION {
619            return Err(LimboError::InternalError(format!(
620                "Unsupported Turso header version: expected {TURSO_VERSION}, got {version}"
621            )));
622        }
623
624        let cipher_id = header[CIPHER_OFFSET];
625        let header_cipher = CipherMode::from_cipher_id(cipher_id)?;
626        if header_cipher != self.cipher_mode {
627            return Err(LimboError::InternalError(format!(
628                "Cipher mode mismatch: expected {:?} (ID {}), got {:?} (ID {})",
629                self.cipher_mode,
630                self.cipher_mode.cipher_id(),
631                header_cipher,
632                cipher_id
633            )));
634        }
635
636        if header[CIPHER_OFFSET + 1..TURSO_HEADER_SIZE]
637            .iter()
638            .any(|&b| b != 0)
639        {
640            return Err(LimboError::InternalError(
641                "Invalid Turso header: unused bytes must be zero".into(),
642            ));
643        }
644        Ok(())
645    }
646
647    #[cfg(clt_turso_feature = "encryption")]
648    pub fn encrypt_page(&self, page: &[u8], page_id: usize) -> Result<Vec<u8>> {
649        use crate::storage::sqlite3_ondisk::DatabaseHeader;
650        if page_id == DatabaseHeader::PAGE_ID {
651            return self.encrypt_page_1(page);
652        }
653        tracing::debug!("encrypting page {}", page_id);
654        assert_eq!(
655            page.len(),
656            self.page_size,
657            "Page data must be exactly {} bytes",
658            self.page_size
659        );
660
661        let metadata_size = self.cipher_mode.metadata_size();
662        let reserved_bytes = &page[self.page_size - metadata_size..];
663
664        #[cfg(debug_assertions)]
665        {
666            let reserved_bytes_zeroed = reserved_bytes.iter().all(|&b| b == 0);
667            turso_assert!(
668                reserved_bytes_zeroed,
669                "last reserved bytes must be empty/zero, but found non-zero bytes"
670            );
671        }
672
673        let payload = &page[..self.page_size - metadata_size];
674        let (encrypted, nonce) = self.encrypt_raw(payload)?;
675
676        let nonce_size = self.cipher_mode.nonce_size();
677        assert_eq!(
678            encrypted.len(),
679            self.page_size - nonce_size,
680            "Encrypted page must be exactly {} bytes",
681            self.page_size - nonce_size
682        );
683
684        let mut result = Vec::with_capacity(self.page_size);
685        result.extend_from_slice(&encrypted);
686        result.extend_from_slice(&nonce);
687        assert_eq!(
688            result.len(),
689            self.page_size,
690            "Encrypted page must be exactly {} bytes",
691            self.page_size
692        );
693        Ok(result)
694    }
695
696    #[cfg(clt_turso_feature = "encryption")]
697    pub fn decrypt_page(&self, encrypted_page: &[u8], page_id: usize) -> Result<Vec<u8>> {
698        use crate::storage::sqlite3_ondisk::DatabaseHeader;
699        if page_id == DatabaseHeader::PAGE_ID {
700            return self.decrypt_page_1(encrypted_page);
701        }
702        tracing::debug!("decrypting page {}", page_id);
703        assert_eq!(
704            encrypted_page.len(),
705            self.page_size,
706            "Encrypted page data must be exactly {} bytes",
707            self.page_size
708        );
709
710        let nonce_size = self.cipher_mode.nonce_size();
711        let nonce_offset = encrypted_page.len() - nonce_size;
712        let payload = &encrypted_page[..nonce_offset];
713        let nonce = &encrypted_page[nonce_offset..];
714
715        let decrypted_data = self.decrypt_raw(payload, nonce)?;
716        let metadata_size = self.cipher_mode.metadata_size();
717        assert_eq!(
718            decrypted_data.len(),
719            self.page_size - metadata_size,
720            "Decrypted page data must be exactly {} bytes",
721            self.page_size - metadata_size
722        );
723
724        let mut result = Vec::with_capacity(self.page_size);
725        result.extend_from_slice(&decrypted_data);
726        result.resize(self.page_size, 0);
727
728        assert_eq!(
729            result.len(),
730            self.page_size,
731            "Decrypted page data must be exactly {} bytes",
732            self.page_size
733        );
734        Ok(result)
735    }
736
737    #[cfg(clt_turso_feature = "encryption")]
738    fn encrypt_page_1(&self, page: &[u8]) -> Result<Vec<u8>> {
739        use crate::storage::sqlite3_ondisk::DatabaseHeader;
740
741        tracing::debug!("encrypting page 1");
742        assert_eq!(
743            page.len(),
744            self.page_size,
745            "Page data must be exactly {} bytes",
746            self.page_size
747        );
748
749        // since this is page 1, this must have header
750        turso_assert!(
751            page.starts_with(SQLITE_HEADER),
752            "Page 1 must start with SQLite header"
753        );
754
755        let metadata_size = self.cipher_mode.metadata_size();
756        let reserved_bytes = &page[self.page_size - metadata_size..];
757
758        #[cfg(debug_assertions)]
759        {
760            // In debug builds, ensure that the reserved bytes are zeroed out. So even when we are
761            // reusing a page from buffer pool, we zero out in debug build so that we can be
762            // sure that b tree layer is not writing any data into the reserved space.
763            // We avoid calling `memset` in release builds for performance reasons.
764            let reserved_bytes_zeroed = reserved_bytes.iter().all(|&b| b == 0);
765            turso_assert!(
766                reserved_bytes_zeroed,
767                "last reserved bytes must be empty/zero, but found non-zero bytes"
768            );
769        }
770
771        // page 1 encryption:
772        // 1. First 16 bytes are replaced with Turso magic bytes
773        // 2. Next 84 bytes (16-100) are kept as-is (not encrypted)
774        // 3. Remaining bytes (100-end) are encrypted
775        // 4. The header (the first 100 bytes) as associated data
776        let turso_header = self.create_turso_header();
777        let mut new_header = Vec::with_capacity(DatabaseHeader::SIZE);
778        new_header.extend_from_slice(&turso_header);
779        new_header.extend_from_slice(&page[TURSO_HEADER_SIZE..DatabaseHeader::SIZE]);
780
781        let payload = &page[DatabaseHeader::SIZE..self.page_size - metadata_size];
782        let (encrypted, nonce) = self.encrypt_raw_with_ad(payload, &new_header)?;
783
784        let nonce_size = self.cipher_mode.nonce_size();
785        assert_eq!(
786            encrypted.len(),
787            self.page_size - nonce_size - DatabaseHeader::SIZE,
788            "Encrypted page must be exactly {} bytes",
789            self.page_size - nonce_size - DatabaseHeader::SIZE
790        );
791
792        let mut result = Vec::with_capacity(self.page_size);
793
794        // 1. copy the header
795        result.append(&mut new_header);
796        // 2. copy the encrypted payload
797        result.extend_from_slice(&encrypted);
798        // 3. now add the nonce
799        result.extend_from_slice(&nonce);
800
801        assert_eq!(
802            result.len(),
803            self.page_size,
804            "Encrypted page must be exactly {} bytes",
805            self.page_size
806        );
807        Ok(result)
808    }
809
810    #[cfg(clt_turso_feature = "encryption")]
811    fn decrypt_page_1(&self, encrypted_page: &[u8]) -> Result<Vec<u8>> {
812        use crate::storage::sqlite3_ondisk::DatabaseHeader;
813
814        tracing::debug!("decrypting page 1");
815        assert_eq!(
816            encrypted_page.len(),
817            self.page_size,
818            "Encrypted page data must be exactly {} bytes",
819            self.page_size
820        );
821
822        self.validate_turso_header(&encrypted_page[..TURSO_HEADER_SIZE])?;
823
824        let nonce_size = self.cipher_mode.nonce_size();
825        let nonce_offset = encrypted_page.len() - nonce_size;
826        let payload = &encrypted_page[DatabaseHeader::SIZE..nonce_offset];
827        let nonce = &encrypted_page[nonce_offset..];
828
829        // it's important to use the header on disk (with Turso magic bytes) as associated data
830        // for protection against tampering the header
831        let header = &encrypted_page[..DatabaseHeader::SIZE];
832        let decrypted_data = self.decrypt_raw_with_ad(payload, nonce, header)?;
833
834        let metadata_size = self.cipher_mode.metadata_size();
835        assert_eq!(
836            decrypted_data.len(),
837            self.page_size - metadata_size - DatabaseHeader::SIZE,
838            "Decrypted page data must be exactly {} bytes",
839            self.page_size - metadata_size - DatabaseHeader::SIZE
840        );
841
842        // reconstruct the page with the appropriate SQLite header
843        let mut result = Vec::with_capacity(self.page_size);
844        result.extend_from_slice(SQLITE_HEADER);
845        result.extend_from_slice(&encrypted_page[TURSO_HEADER_SIZE..DatabaseHeader::SIZE]);
846        result.extend_from_slice(&decrypted_data);
847        result.resize(self.page_size, 0);
848
849        assert_eq!(
850            result.len(),
851            self.page_size,
852            "Decrypted page data must be exactly {} bytes",
853            self.page_size
854        );
855        Ok(result)
856    }
857
858    /// encrypts raw data using the configured cipher, returns ciphertext and nonce
859    fn encrypt_raw(&self, plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
860        const AD: &[u8] = b"";
861        self.encrypt_raw_with_ad(plaintext, AD)
862    }
863
864    /// encrypts raw data with associated data using the configured cipher
865    fn encrypt_raw_with_ad(&self, plaintext: &[u8], ad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
866        macro_rules! encrypt_cipher {
867            ($cipher:expr) => {{
868                let (ciphertext, nonce) = $cipher.encrypt(plaintext, ad)?;
869                Ok((ciphertext, nonce.to_vec()))
870            }};
871        }
872
873        match &self.cipher {
874            Cipher::Aes128Gcm(cipher) => encrypt_cipher!(cipher),
875            Cipher::Aes256Gcm(cipher) => encrypt_cipher!(cipher),
876            Cipher::Aegis256(cipher) => encrypt_cipher!(cipher),
877            Cipher::Aegis256X2(cipher) => encrypt_cipher!(cipher),
878            Cipher::Aegis256X4(cipher) => encrypt_cipher!(cipher),
879            Cipher::Aegis128L(cipher) => encrypt_cipher!(cipher),
880            Cipher::Aegis128X2(cipher) => encrypt_cipher!(cipher),
881            Cipher::Aegis128X4(cipher) => encrypt_cipher!(cipher),
882        }
883    }
884
885    fn decrypt_raw(&self, ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
886        const AD: &[u8] = b"";
887        self.decrypt_raw_with_ad(ciphertext, nonce, AD)
888    }
889
890    fn decrypt_raw_with_ad(&self, ciphertext: &[u8], nonce: &[u8], ad: &[u8]) -> Result<Vec<u8>> {
891        let mut out = Vec::with_capacity(ciphertext.len().saturating_sub(self.tag_size()));
892        self.decrypt_raw_with_ad_into(ciphertext, nonce, ad, &mut out)?;
893        Ok(out)
894    }
895
896    fn decrypt_raw_with_ad_into(
897        &self,
898        ciphertext: &[u8],
899        nonce: &[u8],
900        ad: &[u8],
901        out: &mut Vec<u8>,
902    ) -> Result<()> {
903        macro_rules! decrypt_with_nonce {
904            ($cipher:expr, $nonce_size:literal, $name:literal) => {{
905                let nonce_array: [u8; $nonce_size] = nonce.try_into().map_err(|_| {
906                    LimboError::InternalError(format!(
907                        "Invalid nonce size for {}: expected {}, got {}",
908                        $name,
909                        $nonce_size,
910                        nonce.len()
911                    ))
912                })?;
913                $cipher.decrypt_into(ciphertext, &nonce_array, ad, out)
914            }};
915        }
916
917        match &self.cipher {
918            Cipher::Aes128Gcm(cipher) => decrypt_with_nonce!(cipher, 12, "AES-128-GCM"),
919            Cipher::Aes256Gcm(cipher) => decrypt_with_nonce!(cipher, 12, "AES-256-GCM"),
920            Cipher::Aegis256(cipher) => decrypt_with_nonce!(cipher, 32, "AEGIS-256"),
921            Cipher::Aegis256X2(cipher) => decrypt_with_nonce!(cipher, 32, "AEGIS-256X2"),
922            Cipher::Aegis256X4(cipher) => decrypt_with_nonce!(cipher, 32, "AEGIS-256X4"),
923            Cipher::Aegis128L(cipher) => decrypt_with_nonce!(cipher, 16, "AEGIS-128L"),
924            Cipher::Aegis128X2(cipher) => decrypt_with_nonce!(cipher, 16, "AEGIS-128X2"),
925            Cipher::Aegis128X4(cipher) => decrypt_with_nonce!(cipher, 16, "AEGIS-128X4"),
926        }
927    }
928
929    #[cfg(not(clt_turso_feature = "encryption"))]
930    pub fn encrypt_page(&self, _page: &[u8], _page_id: usize) -> Result<Vec<u8>> {
931        Err(LimboError::InvalidArgument(
932            "encryption is not enabled, cannot encrypt page. enable via passing `--features encryption`".into(),
933        ))
934    }
935
936    #[cfg(not(clt_turso_feature = "encryption"))]
937    pub fn decrypt_page(&self, _encrypted_page: &[u8], _page_id: usize) -> Result<Vec<u8>> {
938        Err(LimboError::InvalidArgument(
939            "encryption is not enabled, cannot decrypt page. enable via passing `--features encryption`".into(),
940        ))
941    }
942}
943
944fn generate_secure_nonce<const N: usize>() -> [u8; N] {
945    // use OsRng directly to fill bytes, generic over nonce size
946    use aes_gcm::aead::rand_core::RngCore;
947    let mut nonce = [0u8; N];
948    OsRng.fill_bytes(&mut nonce);
949    nonce
950}
951
952// Helper functions for consistent error messages
953enum CipherError {
954    InvalidKeySize {
955        cipher: &'static str,
956        expected: usize,
957    },
958    InvalidTagSize {
959        cipher: &'static str,
960    },
961    DecryptionFailed {
962        cipher: &'static str,
963    },
964    CiphertextTooShort {
965        cipher: &'static str,
966    },
967}
968
969impl From<CipherError> for LimboError {
970    fn from(err: CipherError) -> Self {
971        let msg = match err {
972            CipherError::InvalidKeySize { cipher, expected } => {
973                format!("{cipher} requires {expected}-byte key")
974            }
975            CipherError::InvalidTagSize { cipher } => format!("Invalid tag size for {cipher}"),
976            CipherError::DecryptionFailed { cipher } => {
977                format!("{cipher} decryption failed: invalid tag")
978            }
979            CipherError::CiphertextTooShort { cipher } => {
980                format!("Ciphertext too short for {cipher}")
981            }
982        };
983        LimboError::InternalError(msg)
984    }
985}
986
987#[cfg(clt_turso_tests)]
988#[cfg(clt_turso_feature = "encryption")]
989mod tests {
990    use crate::storage::sqlite3_ondisk::DatabaseHeader;
991
992    use super::*;
993    use rand::Rng;
994    const DEFAULT_ENCRYPTED_PAGE_SIZE: usize = 4096;
995
996    macro_rules! test_cipher_wrapper {
997        ($test_name:ident, $cipher_type:ty, $key_gen:expr, $nonce_size:literal, $message:literal) => {
998            #[test]
999            fn $test_name() {
1000                let key = EncryptionKey::from_hex_string(&$key_gen()).unwrap();
1001                let cipher = <$cipher_type>::new(&key);
1002
1003                let plaintext = $message.as_bytes();
1004                let ad = b"additional data";
1005
1006                let (ciphertext, nonce) = cipher.encrypt(plaintext, ad).unwrap();
1007                assert_eq!(nonce.len(), $nonce_size);
1008                assert_ne!(ciphertext[..plaintext.len()], plaintext[..]);
1009
1010                let decrypted = cipher.decrypt(&ciphertext, &nonce, ad).unwrap();
1011                assert_eq!(decrypted, plaintext);
1012            }
1013        };
1014    }
1015
1016    macro_rules! test_aes_cipher_wrapper {
1017        ($test_name:ident, $cipher_type:ty, $key_gen:expr, $nonce_size:literal, $message:literal) => {
1018            #[test]
1019            fn $test_name() {
1020                let key = EncryptionKey::from_hex_string(&$key_gen()).unwrap();
1021                let cipher = <$cipher_type>::new(&key).unwrap();
1022
1023                let plaintext = $message.as_bytes();
1024                let ad = b"additional data";
1025
1026                let (ciphertext, nonce) = cipher.encrypt(plaintext, ad).unwrap();
1027                assert_eq!(nonce.len(), $nonce_size);
1028                assert_ne!(ciphertext[..plaintext.len()], plaintext[..]);
1029
1030                let decrypted = cipher.decrypt(&ciphertext, &nonce, ad).unwrap();
1031                assert_eq!(decrypted, plaintext);
1032            }
1033        };
1034    }
1035
1036    macro_rules! test_raw_encryption {
1037        ($test_name:ident, $cipher_mode:expr, $key_gen:expr, $nonce_size:literal, $message:literal) => {
1038            #[test]
1039            fn $test_name() {
1040                let key = EncryptionKey::from_hex_string(&$key_gen()).unwrap();
1041                let ctx = EncryptionContext::new($cipher_mode, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1042                    .unwrap();
1043
1044                let plaintext = $message.as_bytes();
1045                let (ciphertext, nonce) = ctx.encrypt_raw(plaintext).unwrap();
1046
1047                assert_eq!(nonce.len(), $nonce_size);
1048                assert_ne!(ciphertext[..plaintext.len()], plaintext[..]);
1049
1050                let decrypted = ctx.decrypt_raw(&ciphertext, &nonce).unwrap();
1051                assert_eq!(decrypted, plaintext);
1052            }
1053        };
1054    }
1055
1056    fn generate_random_hex_key() -> String {
1057        let mut rng = rand::rng();
1058        let mut bytes = [0u8; 32];
1059        rng.fill(&mut bytes);
1060        hex::encode(bytes)
1061    }
1062
1063    fn generate_random_hex_key_128() -> String {
1064        let mut rng = rand::rng();
1065        let mut bytes = [0u8; 16];
1066        rng.fill(&mut bytes);
1067        hex::encode(bytes)
1068    }
1069
1070    fn create_test_page_1() -> Vec<u8> {
1071        let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1072        page[..SQLITE_HEADER.len()].copy_from_slice(SQLITE_HEADER);
1073        let mut rng = rand::rng();
1074        // 48 is the max reserved bytes we might need for metadata with any cipher
1075        rng.fill(&mut page[SQLITE_HEADER.len()..DEFAULT_ENCRYPTED_PAGE_SIZE - 48]);
1076        page
1077    }
1078
1079    test_aes_cipher_wrapper!(
1080        test_aes128gcm_cipher_wrapper,
1081        Aes128GcmCipher,
1082        generate_random_hex_key_128,
1083        12,
1084        "Hello, AES-128-GCM!"
1085    );
1086
1087    test_raw_encryption!(
1088        test_aes128gcm_raw_encryption,
1089        CipherMode::Aes128Gcm,
1090        generate_random_hex_key_128,
1091        12,
1092        "Hello, AES-128-GCM!"
1093    );
1094
1095    #[test]
1096    fn test_page_1_encrypt_decrypt_round_trip_with_ad() {
1097        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1098        let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1099            .unwrap();
1100
1101        let page_data = create_test_page_1();
1102        let encrypted = ctx.encrypt_page(&page_data, 1).unwrap();
1103        assert_ne!(
1104            &page_data[0..DatabaseHeader::SIZE],
1105            &encrypted[0..DatabaseHeader::SIZE],
1106            "Encrypted data should be different from the page data"
1107        );
1108        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1109
1110        // check that header is readable directly from disk (not encrypted)
1111        assert_eq!(&encrypted[..5], b"Turso");
1112        assert_eq!(encrypted[5], TURSO_VERSION);
1113        assert_eq!(encrypted[6], CipherMode::Aegis256.cipher_id());
1114
1115        // header should be unencrypted, but data after DatabaseHeader::SIZE should be different
1116        assert_eq!(&encrypted[16..100], &page_data[16..100]); // header portion
1117        assert_ne!(&encrypted[100..200], &page_data[100..200]); // some encrypted portion
1118
1119        // decrypt page 1
1120        let decrypted = ctx.decrypt_page(&encrypted, 1).unwrap();
1121        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1122
1123        // check that SQLite header was restored
1124        assert_eq!(&decrypted[..SQLITE_HEADER.len()], SQLITE_HEADER);
1125        assert_eq!(decrypted, page_data);
1126    }
1127
1128    #[test]
1129    fn test_turso_header_validation() {
1130        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1131        let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1132            .unwrap();
1133
1134        // test cipher_id conversion
1135        assert_eq!(CipherMode::Aes128Gcm.cipher_id(), 1);
1136        assert_eq!(CipherMode::Aes256Gcm.cipher_id(), 2);
1137        assert_eq!(CipherMode::Aegis256.cipher_id(), 3);
1138        assert_eq!(CipherMode::Aegis128L.cipher_id(), 6);
1139
1140        // test from_cipher_id conversion
1141        assert_eq!(
1142            CipherMode::from_cipher_id(1).unwrap(),
1143            CipherMode::Aes128Gcm
1144        );
1145        assert_eq!(CipherMode::from_cipher_id(3).unwrap(), CipherMode::Aegis256);
1146        assert!(CipherMode::from_cipher_id(99).is_err());
1147
1148        // test header creation
1149        let header = ctx.create_turso_header();
1150        assert_eq!(&header[..5], b"Turso");
1151        assert_eq!(header[5], TURSO_VERSION);
1152        assert_eq!(header[6], 3); // AEGIS-256
1153        assert_eq!(&header[7..], &[0u8; 9]); // unused bytes are zero
1154    }
1155
1156    #[test]
1157    fn test_invalid_turso_header_fails_decrypt() {
1158        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1159        let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1160            .unwrap();
1161
1162        let page_data = create_test_page_1();
1163        let encrypted = ctx.encrypt_page(&page_data, 1).unwrap();
1164
1165        // corrupt the header prefix
1166        let mut corrupted = encrypted.clone();
1167        corrupted[0] = b'V'; // make `Turso` to `Vurso`
1168        assert!(ctx.decrypt_page(&corrupted, 1).is_err());
1169
1170        // test with wrong cipher ID
1171        let mut wrong_cipher = encrypted;
1172        wrong_cipher[6] = 99; // invalid cipher ID
1173        assert!(ctx.decrypt_page(&wrong_cipher, 1).is_err());
1174    }
1175
1176    #[test]
1177    fn test_associated_data_validation() {
1178        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1179        let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1180            .unwrap();
1181
1182        let page_data = create_test_page_1();
1183        let encrypted = ctx.encrypt_page(&page_data, 1).unwrap();
1184
1185        // modify a byte in the preserved header portion (bytes 16-100)
1186        let mut corrupted_ad = encrypted;
1187        corrupted_ad[50] ^= 1; // flip one bit in the associated data portion
1188
1189        // this should fail decryption because associated data doesn't match
1190        let decrypt_result = ctx.decrypt_page(&corrupted_ad, 1);
1191        assert!(
1192            decrypt_result.is_err(),
1193            "Decryption should fail with corrupted associated data"
1194        );
1195    }
1196
1197    #[test]
1198    fn test_turso_header_corruption_detection() {
1199        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1200        let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1201            .unwrap();
1202
1203        let page_data = create_test_page_1();
1204        let encrypted = ctx.encrypt_page(&page_data, 1).unwrap();
1205
1206        let mut corrupted_turso_header = encrypted;
1207        corrupted_turso_header[7] ^= 1;
1208
1209        let decrypt_result = ctx.decrypt_page(&corrupted_turso_header, 1);
1210        assert!(
1211            decrypt_result.is_err(),
1212            "Decryption should fail with corrupted Turso header"
1213        );
1214    }
1215
1216    #[test]
1217    fn test_aes128gcm_encrypt_decrypt_round_trip() {
1218        let mut rng = rand::rng();
1219        let cipher_mode = CipherMode::Aes128Gcm;
1220        let metadata_size = cipher_mode.metadata_size();
1221        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1222
1223        let page_data = {
1224            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1225            page.iter_mut()
1226                .take(data_size)
1227                .for_each(|byte| *byte = rng.random());
1228            page
1229        };
1230
1231        let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap();
1232        let ctx = EncryptionContext::new(CipherMode::Aes128Gcm, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1233            .unwrap();
1234
1235        let page_id = 42;
1236        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1237        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1238        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1239        assert_ne!(&encrypted[..], &page_data[..]);
1240
1241        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1242        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1243        assert_eq!(decrypted, page_data);
1244    }
1245
1246    #[test]
1247    fn test_aes_encrypt_decrypt_round_trip() {
1248        let mut rng = rand::rng();
1249        let cipher_mode = CipherMode::Aes256Gcm;
1250        let metadata_size = cipher_mode.metadata_size();
1251        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1252
1253        let page_data = {
1254            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1255            page.iter_mut()
1256                .take(data_size)
1257                .for_each(|byte| *byte = rng.random());
1258            page
1259        };
1260
1261        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1262        let ctx = EncryptionContext::new(CipherMode::Aes256Gcm, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1263            .unwrap();
1264
1265        let page_id = 42;
1266        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1267        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1268        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1269        assert_ne!(&encrypted[..], &page_data[..]);
1270
1271        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1272        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1273        assert_eq!(decrypted, page_data);
1274    }
1275
1276    test_cipher_wrapper!(
1277        test_aegis256_cipher_wrapper,
1278        Aegis256Cipher,
1279        generate_random_hex_key,
1280        32,
1281        "Hello, AEGIS-256!"
1282    );
1283
1284    test_raw_encryption!(
1285        test_aegis256_raw_encryption,
1286        CipherMode::Aegis256,
1287        generate_random_hex_key,
1288        32,
1289        "Hello, AEGIS-256!"
1290    );
1291
1292    #[test]
1293    fn test_aegis256_encrypt_decrypt_round_trip() {
1294        let mut rng = rand::rng();
1295        let cipher_mode = CipherMode::Aegis256;
1296        let metadata_size = cipher_mode.metadata_size();
1297        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1298
1299        let page_data = {
1300            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1301            page.iter_mut()
1302                .take(data_size)
1303                .for_each(|byte| *byte = rng.random());
1304            page
1305        };
1306
1307        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1308        let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1309            .unwrap();
1310
1311        let page_id = 42;
1312        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1313        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1314        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1315
1316        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1317        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1318        assert_eq!(decrypted, page_data);
1319    }
1320
1321    test_cipher_wrapper!(
1322        test_aegis128x2_cipher_wrapper,
1323        Aegis128X2Cipher,
1324        generate_random_hex_key_128,
1325        16,
1326        "Hello, AEGIS-128X2!"
1327    );
1328
1329    test_raw_encryption!(
1330        test_aegis128x2_raw_encryption,
1331        CipherMode::Aegis128X2,
1332        generate_random_hex_key_128,
1333        16,
1334        "Hello, AEGIS-128X2!"
1335    );
1336
1337    #[test]
1338    fn test_aegis128x2_encrypt_decrypt_round_trip() {
1339        let mut rng = rand::rng();
1340        let cipher_mode = CipherMode::Aegis128X2;
1341        let metadata_size = cipher_mode.metadata_size();
1342        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1343
1344        let page_data = {
1345            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1346            page.iter_mut()
1347                .take(data_size)
1348                .for_each(|byte| *byte = rng.random());
1349            page
1350        };
1351
1352        let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap();
1353        let ctx = EncryptionContext::new(CipherMode::Aegis128X2, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1354            .unwrap();
1355
1356        let page_id = 42;
1357        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1358        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1359        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1360
1361        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1362        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1363        assert_eq!(decrypted, page_data);
1364    }
1365
1366    test_cipher_wrapper!(
1367        test_aegis128l_cipher_wrapper,
1368        Aegis128LCipher,
1369        generate_random_hex_key_128,
1370        16,
1371        "Hello, AEGIS-128L!"
1372    );
1373
1374    test_raw_encryption!(
1375        test_aegis128l_raw_encryption,
1376        CipherMode::Aegis128L,
1377        generate_random_hex_key_128,
1378        16,
1379        "Hello, AEGIS-128L!"
1380    );
1381
1382    #[test]
1383    fn test_aegis128l_encrypt_decrypt_round_trip() {
1384        let mut rng = rand::rng();
1385        let cipher_mode = CipherMode::Aegis128L;
1386        let metadata_size = cipher_mode.metadata_size();
1387        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1388
1389        let page_data = {
1390            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1391            page.iter_mut()
1392                .take(data_size)
1393                .for_each(|byte| *byte = rng.random());
1394            page
1395        };
1396
1397        let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap();
1398        let ctx = EncryptionContext::new(CipherMode::Aegis128L, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1399            .unwrap();
1400
1401        let page_id = 42;
1402        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1403        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1404        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1405
1406        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1407        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1408        assert_eq!(decrypted, page_data);
1409    }
1410
1411    test_cipher_wrapper!(
1412        test_aegis128x4_cipher_wrapper,
1413        Aegis128X4Cipher,
1414        generate_random_hex_key_128,
1415        16,
1416        "Hello, AEGIS-128X4!"
1417    );
1418
1419    test_raw_encryption!(
1420        test_aegis128x4_raw_encryption,
1421        CipherMode::Aegis128X4,
1422        generate_random_hex_key_128,
1423        16,
1424        "Hello, AEGIS-128X4!"
1425    );
1426
1427    #[test]
1428    fn test_aegis128x4_encrypt_decrypt_round_trip() {
1429        let mut rng = rand::rng();
1430        let cipher_mode = CipherMode::Aegis128X4;
1431        let metadata_size = cipher_mode.metadata_size();
1432        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1433
1434        let page_data = {
1435            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1436            page.iter_mut()
1437                .take(data_size)
1438                .for_each(|byte| *byte = rng.random());
1439            page
1440        };
1441
1442        let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap();
1443        let ctx = EncryptionContext::new(CipherMode::Aegis128X4, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1444            .unwrap();
1445
1446        let page_id = 42;
1447        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1448        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1449        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1450
1451        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1452        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1453        assert_eq!(decrypted, page_data);
1454    }
1455
1456    test_cipher_wrapper!(
1457        test_aegis256x2_cipher_wrapper,
1458        Aegis256X2Cipher,
1459        generate_random_hex_key,
1460        32,
1461        "Hello, AEGIS-256X2!"
1462    );
1463
1464    test_raw_encryption!(
1465        test_aegis256x2_raw_encryption,
1466        CipherMode::Aegis256X2,
1467        generate_random_hex_key,
1468        32,
1469        "Hello, AEGIS-256X2!"
1470    );
1471
1472    #[test]
1473    fn test_aegis256x2_encrypt_decrypt_round_trip() {
1474        let mut rng = rand::rng();
1475        let cipher_mode = CipherMode::Aegis256X2;
1476        let metadata_size = cipher_mode.metadata_size();
1477        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1478
1479        let page_data = {
1480            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1481            page.iter_mut()
1482                .take(data_size)
1483                .for_each(|byte| *byte = rng.random());
1484            page
1485        };
1486
1487        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1488        let ctx = EncryptionContext::new(CipherMode::Aegis256X2, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1489            .unwrap();
1490
1491        let page_id = 42;
1492        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1493        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1494        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1495
1496        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1497        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1498        assert_eq!(decrypted, page_data);
1499    }
1500
1501    test_cipher_wrapper!(
1502        test_aegis256x4_cipher_wrapper,
1503        Aegis256X4Cipher,
1504        generate_random_hex_key,
1505        32,
1506        "Hello, AEGIS-256X4!"
1507    );
1508
1509    test_raw_encryption!(
1510        test_aegis256x4_raw_encryption,
1511        CipherMode::Aegis256X4,
1512        generate_random_hex_key,
1513        32,
1514        "Hello, AEGIS-256X4!"
1515    );
1516
1517    #[test]
1518    fn test_aegis256x4_encrypt_decrypt_round_trip() {
1519        let mut rng = rand::rng();
1520        let cipher_mode = CipherMode::Aegis256X4;
1521        let metadata_size = cipher_mode.metadata_size();
1522        let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size;
1523
1524        let page_data = {
1525            let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE];
1526            page.iter_mut()
1527                .take(data_size)
1528                .for_each(|byte| *byte = rng.random());
1529            page
1530        };
1531
1532        let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap();
1533        let ctx = EncryptionContext::new(CipherMode::Aegis256X4, &key, DEFAULT_ENCRYPTED_PAGE_SIZE)
1534            .unwrap();
1535
1536        let page_id = 42;
1537        let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap();
1538        assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1539        assert_ne!(&encrypted[..data_size], &page_data[..data_size]);
1540
1541        let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap();
1542        assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE);
1543        assert_eq!(decrypted, page_data);
1544    }
1545
1546    #[test]
1547    fn test_cipher_mode_string_parsing() {
1548        // Test AES-128-GCM
1549        let mode = CipherMode::try_from("aes128gcm").unwrap();
1550        assert_eq!(mode, CipherMode::Aes128Gcm);
1551        assert_eq!(mode.to_string(), "aes128gcm");
1552        assert_eq!(mode.required_key_size(), 16);
1553        assert_eq!(mode.nonce_size(), 12);
1554        assert_eq!(mode.tag_size(), 16);
1555
1556        let mode = CipherMode::try_from("aes-128-gcm").unwrap();
1557        assert_eq!(mode, CipherMode::Aes128Gcm);
1558
1559        let mode = CipherMode::try_from("aes_128_gcm").unwrap();
1560        assert_eq!(mode, CipherMode::Aes128Gcm);
1561
1562        // Test AES-256-GCM
1563        let mode = CipherMode::try_from("aes256gcm").unwrap();
1564        assert_eq!(mode, CipherMode::Aes256Gcm);
1565        assert_eq!(mode.to_string(), "aes256gcm");
1566        assert_eq!(mode.required_key_size(), 32);
1567        assert_eq!(mode.nonce_size(), 12);
1568
1569        // Test that all AEGIS variants can be parsed from strings
1570        let mode = CipherMode::try_from("aegis128x2").unwrap();
1571        assert_eq!(mode, CipherMode::Aegis128X2);
1572        assert_eq!(mode.to_string(), "aegis128x2");
1573        assert_eq!(mode.required_key_size(), 16);
1574        assert_eq!(mode.nonce_size(), 16);
1575        assert_eq!(mode.tag_size(), 16);
1576
1577        let mode = CipherMode::try_from("aegis-128x2").unwrap();
1578        assert_eq!(mode, CipherMode::Aegis128X2);
1579
1580        let mode = CipherMode::try_from("aegis_128x2").unwrap();
1581        assert_eq!(mode, CipherMode::Aegis128X2);
1582
1583        // Test AEGIS-128L
1584        let mode = CipherMode::try_from("aegis128l").unwrap();
1585        assert_eq!(mode, CipherMode::Aegis128L);
1586        assert_eq!(mode.to_string(), "aegis128l");
1587        assert_eq!(mode.required_key_size(), 16);
1588        assert_eq!(mode.nonce_size(), 16);
1589
1590        // Test AEGIS-128X4
1591        let mode = CipherMode::try_from("aegis128x4").unwrap();
1592        assert_eq!(mode, CipherMode::Aegis128X4);
1593        assert_eq!(mode.to_string(), "aegis128x4");
1594        assert_eq!(mode.required_key_size(), 16);
1595        assert_eq!(mode.nonce_size(), 16);
1596
1597        // Test AEGIS-256X2
1598        let mode = CipherMode::try_from("aegis256x2").unwrap();
1599        assert_eq!(mode, CipherMode::Aegis256X2);
1600        assert_eq!(mode.to_string(), "aegis256x2");
1601        assert_eq!(mode.required_key_size(), 32);
1602        assert_eq!(mode.nonce_size(), 32);
1603
1604        // Test AEGIS-256X4
1605        let mode = CipherMode::try_from("aegis256x4").unwrap();
1606        assert_eq!(mode, CipherMode::Aegis256X4);
1607        assert_eq!(mode.to_string(), "aegis256x4");
1608        assert_eq!(mode.required_key_size(), 32);
1609        assert_eq!(mode.nonce_size(), 32);
1610    }
1611}