Skip to main content

idb/innodb/
decryption.rs

1//! Tablespace page decryption using AES-256-CBC.
2//!
3//! Provides [`DecryptionContext`] which holds the per-tablespace key and IV
4//! derived from the keyring master key and the encryption info on page 0.
5//! Pages with encrypted page types (15, 16, 17) are decrypted in-place
6//! by [`DecryptionContext::decrypt_page`].
7
8use aes::cipher::block_padding::NoPadding;
9use aes::cipher::{BlockDecryptMut, KeyInit, KeyIvInit};
10use aes::Aes256;
11use byteorder::{BigEndian, ByteOrder};
12
13use crate::innodb::constants::*;
14use crate::innodb::encryption::EncryptionInfo;
15use crate::innodb::keyring::Keyring;
16use crate::innodb::page_types::PageType;
17use crate::IdbError;
18
19type Aes256CbcDec = cbc::Decryptor<Aes256>;
20type Aes256EcbDec = ecb::Decryptor<Aes256>;
21
22/// Holds the decrypted per-tablespace key and IV for page decryption.
23#[derive(Debug)]
24pub struct DecryptionContext {
25    /// Decrypted 32-byte tablespace key for AES-256-CBC.
26    tablespace_key: [u8; 32],
27    /// Decrypted 32-byte IV (first 16 bytes used as AES-CBC IV).
28    tablespace_iv: [u8; 32],
29}
30
31impl DecryptionContext {
32    /// Build a decryption context from encryption info and a keyring.
33    ///
34    /// Looks up the master key in the keyring using the server UUID and
35    /// master key ID from the encryption info, then decrypts the tablespace
36    /// key+IV using AES-256-ECB, and verifies the CRC32 checksum.
37    pub fn from_encryption_info(
38        info: &EncryptionInfo,
39        keyring: &Keyring,
40    ) -> Result<Self, IdbError> {
41        let master_key = keyring
42            .find_innodb_master_key(&info.server_uuid, info.master_key_id)
43            .ok_or_else(|| {
44                IdbError::Parse(format!(
45                    "Master key not found in keyring: INNODBKey-{}-{}",
46                    info.server_uuid, info.master_key_id
47                ))
48            })?;
49
50        if master_key.len() != 32 {
51            return Err(IdbError::Parse(format!(
52                "Master key has wrong length: expected 32, got {}",
53                master_key.len()
54            )));
55        }
56
57        // Decrypt the tablespace key+IV using AES-256-ECB with the master key
58        let mut decrypted = info.encrypted_key_iv;
59        let decryptor = Aes256EcbDec::new_from_slice(master_key)
60            .map_err(|e| IdbError::Parse(format!("AES-256-ECB init failed: {}", e)))?;
61        decryptor
62            .decrypt_padded_mut::<NoPadding>(&mut decrypted)
63            .map_err(|e| IdbError::Parse(format!("AES-256-ECB decrypt failed: {}", e)))?;
64
65        // Verify CRC32 checksum of the decrypted key+IV
66        let computed_crc = crc32c::crc32c(&decrypted);
67        if computed_crc != info.checksum {
68            return Err(IdbError::Parse(format!(
69                "Failed to decrypt tablespace key: CRC32 checksum mismatch \
70                 (computed=0x{:08X}, expected=0x{:08X}). Wrong keyring?",
71                computed_crc, info.checksum
72            )));
73        }
74
75        let mut tablespace_key = [0u8; 32];
76        let mut tablespace_iv = [0u8; 32];
77        tablespace_key.copy_from_slice(&decrypted[..32]);
78        tablespace_iv.copy_from_slice(&decrypted[32..64]);
79
80        Ok(DecryptionContext {
81            tablespace_key,
82            tablespace_iv,
83        })
84    }
85
86    /// Decrypt an encrypted page in-place.
87    ///
88    /// Decrypts bytes [38..page_size-8) using AES-256-CBC, then restores
89    /// the original page type from the FIL header byte 26 (where MySQL
90    /// saves it before overwriting with the encrypted page type).
91    ///
92    /// Returns `Ok(true)` if the page was decrypted, `Ok(false)` if the
93    /// page type is not an encrypted type and no decryption was needed.
94    pub fn decrypt_page(&self, page_data: &mut [u8], page_size: usize) -> Result<bool, IdbError> {
95        if page_data.len() < page_size {
96            return Err(IdbError::Parse(
97                "Page data too short for decryption".to_string(),
98            ));
99        }
100
101        // Check if this page has an encrypted page type
102        let page_type_raw = BigEndian::read_u16(&page_data[FIL_PAGE_TYPE..]);
103        let page_type = PageType::from_u16(page_type_raw);
104
105        if !matches!(
106            page_type,
107            PageType::Encrypted | PageType::CompressedEncrypted | PageType::EncryptedRtree
108        ) {
109            return Ok(false);
110        }
111
112        // Read the original page type stored at offset 26 (FIL_PAGE_FILE_FLUSH_LSN)
113        // MySQL saves the original type here before encrypting
114        let original_type = BigEndian::read_u16(&page_data[FIL_PAGE_ORIGINAL_TYPE_V1..]);
115
116        // Encrypted range: [38..page_size-8)
117        let encrypt_start = SIZE_FIL_HEAD;
118        let encrypt_end = page_size - SIZE_FIL_TRAILER;
119        let encrypt_len = encrypt_end - encrypt_start;
120
121        // AES block size is 16 bytes; MySQL handles the tail specially
122        let aes_block_size = 16;
123
124        if encrypt_len < aes_block_size {
125            return Err(IdbError::Parse(
126                "Encrypted page body too small for AES decryption".to_string(),
127            ));
128        }
129
130        // Use first 16 bytes of the 32-byte IV
131        let iv: [u8; 16] = self.tablespace_iv[..16].try_into().unwrap();
132
133        // Decrypt the block-aligned portion of the page body.
134        // For standard 16K pages, the body is 16338 bytes (remainder of 2 bytes
135        // after block alignment). The trailing non-aligned bytes are left as-is;
136        // they are not significant for page structure parsing.
137        let main_len = (encrypt_len / aes_block_size) * aes_block_size;
138
139        if main_len > 0 {
140            let main_end = encrypt_start + main_len;
141            let decryptor = Aes256CbcDec::new_from_slices(&self.tablespace_key, &iv)
142                .map_err(|e| IdbError::Parse(format!("AES-256-CBC init failed: {}", e)))?;
143            decryptor
144                .decrypt_padded_mut::<NoPadding>(&mut page_data[encrypt_start..main_end])
145                .map_err(|e| IdbError::Parse(format!("AES-256-CBC decrypt failed: {}", e)))?;
146        }
147
148        // Restore the original page type
149        BigEndian::write_u16(&mut page_data[FIL_PAGE_TYPE..], original_type);
150
151        Ok(true)
152    }
153
154    /// Check if a page has an encrypted page type.
155    pub fn is_encrypted_page(page_data: &[u8]) -> bool {
156        if page_data.len() < SIZE_FIL_HEAD {
157            return false;
158        }
159        let page_type = PageType::from_u16(BigEndian::read_u16(&page_data[FIL_PAGE_TYPE..]));
160        matches!(
161            page_type,
162            PageType::Encrypted | PageType::CompressedEncrypted | PageType::EncryptedRtree
163        )
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use aes::cipher::BlockEncryptMut;
171
172    type Aes256CbcEnc = cbc::Encryptor<Aes256>;
173    type Aes256EcbEnc = ecb::Encryptor<Aes256>;
174
175    /// Build a synthetic encrypted page for testing.
176    fn build_encrypted_page(
177        page_num: u32,
178        space_id: u32,
179        original_type: u16,
180        key: &[u8; 32],
181        iv: &[u8; 32],
182        page_size: usize,
183    ) -> Vec<u8> {
184        let mut page = vec![0u8; page_size];
185
186        // FIL header
187        BigEndian::write_u32(&mut page[FIL_PAGE_OFFSET..], page_num);
188        BigEndian::write_u32(&mut page[FIL_PAGE_PREV..], FIL_NULL);
189        BigEndian::write_u32(&mut page[FIL_PAGE_NEXT..], FIL_NULL);
190        BigEndian::write_u64(&mut page[FIL_PAGE_LSN..], 5000);
191        // Save original type at offset 26
192        BigEndian::write_u16(&mut page[FIL_PAGE_ORIGINAL_TYPE_V1..], original_type);
193        BigEndian::write_u32(&mut page[FIL_PAGE_SPACE_ID..], space_id);
194
195        // Write some recognizable data in the page body
196        for i in SIZE_FIL_HEAD..page_size - SIZE_FIL_TRAILER {
197            page[i] = ((i * 7 + 13) & 0xFF) as u8;
198        }
199
200        // Encrypt body: [38..page_size-8)
201        let encrypt_start = SIZE_FIL_HEAD;
202        let encrypt_end = page_size - SIZE_FIL_TRAILER;
203        let encrypt_len = encrypt_end - encrypt_start;
204        let aes_block_size = 16;
205        let main_len = (encrypt_len / aes_block_size) * aes_block_size;
206
207        let cbc_iv: [u8; 16] = iv[..16].try_into().unwrap();
208        let encryptor = Aes256CbcEnc::new_from_slices(key, &cbc_iv).unwrap();
209        encryptor
210            .encrypt_padded_mut::<NoPadding>(
211                &mut page[encrypt_start..encrypt_start + main_len],
212                main_len,
213            )
214            .unwrap();
215
216        // Set encrypted page type
217        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 15); // Encrypted
218
219        // Trailer
220        let trailer = page_size - SIZE_FIL_TRAILER;
221        BigEndian::write_u32(&mut page[trailer + 4..], (5000u64 & 0xFFFFFFFF) as u32);
222
223        page
224    }
225
226    #[test]
227    fn test_decrypt_page_roundtrip() {
228        let key: [u8; 32] = [0x42; 32];
229        let iv: [u8; 32] = [0x13; 32];
230        let page_size = 16384;
231
232        // Build a reference page with original content
233        let mut reference = vec![0u8; page_size];
234        for i in SIZE_FIL_HEAD..page_size - SIZE_FIL_TRAILER {
235            reference[i] = ((i * 7 + 13) & 0xFF) as u8;
236        }
237
238        // Build encrypted version
239        let mut encrypted = build_encrypted_page(1, 1, 17855, &key, &iv, page_size);
240
241        // Verify it's marked encrypted
242        let pt = BigEndian::read_u16(&encrypted[FIL_PAGE_TYPE..]);
243        assert_eq!(pt, 15);
244
245        // Decrypt
246        let ctx = DecryptionContext {
247            tablespace_key: key,
248            tablespace_iv: iv,
249        };
250        let decrypted = ctx.decrypt_page(&mut encrypted, page_size).unwrap();
251        assert!(decrypted);
252
253        // Page type should be restored to INDEX (17855)
254        let restored_type = BigEndian::read_u16(&encrypted[FIL_PAGE_TYPE..]);
255        assert_eq!(restored_type, 17855);
256
257        // Body content should match reference
258        assert_eq!(
259            &encrypted[SIZE_FIL_HEAD..page_size - SIZE_FIL_TRAILER],
260            &reference[SIZE_FIL_HEAD..page_size - SIZE_FIL_TRAILER]
261        );
262    }
263
264    #[test]
265    fn test_decrypt_non_encrypted_page_is_noop() {
266        let key: [u8; 32] = [0x42; 32];
267        let iv: [u8; 32] = [0x13; 32];
268
269        let mut page = vec![0u8; 16384];
270        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 17855); // INDEX, not encrypted
271
272        let ctx = DecryptionContext {
273            tablespace_key: key,
274            tablespace_iv: iv,
275        };
276        let result = ctx.decrypt_page(&mut page, 16384).unwrap();
277        assert!(!result);
278    }
279
280    #[test]
281    fn test_is_encrypted_page() {
282        let mut page = vec![0u8; 38];
283        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 15);
284        assert!(DecryptionContext::is_encrypted_page(&page));
285
286        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 16);
287        assert!(DecryptionContext::is_encrypted_page(&page));
288
289        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 17);
290        assert!(DecryptionContext::is_encrypted_page(&page));
291
292        BigEndian::write_u16(&mut page[FIL_PAGE_TYPE..], 17855);
293        assert!(!DecryptionContext::is_encrypted_page(&page));
294    }
295
296    #[test]
297    fn test_from_encryption_info() {
298        use crate::innodb::keyring::Keyring;
299        use sha2::{Digest, Sha256};
300
301        // Generate known keys
302        let master_key: [u8; 32] = [0xAA; 32];
303        let ts_key: [u8; 32] = [0xBB; 32];
304        let ts_iv: [u8; 32] = [0xCC; 32];
305
306        // Encrypt ts_key+iv with AES-256-ECB using master key
307        let mut key_iv_data = [0u8; 64];
308        key_iv_data[..32].copy_from_slice(&ts_key);
309        key_iv_data[32..].copy_from_slice(&ts_iv);
310
311        let crc = crc32c::crc32c(&key_iv_data);
312
313        let encryptor = Aes256EcbEnc::new_from_slice(&master_key).unwrap();
314        let mut encrypted_key_iv = key_iv_data;
315        encryptor
316            .encrypt_padded_mut::<NoPadding>(&mut encrypted_key_iv, 64)
317            .unwrap();
318
319        let uuid = "12345678-1234-1234-1234-123456789abc";
320        let info = EncryptionInfo {
321            magic_version: 3,
322            master_key_id: 1,
323            server_uuid: uuid.to_string(),
324            encrypted_key_iv,
325            checksum: crc,
326        };
327
328        // Build a keyring file with the master key
329        let obfuscate_key = b"*305=Ljt0*!@$Hnm(*-9-w;:";
330        let key_id = format!("INNODBKey-{}-1", uuid);
331        let mut obfuscated_master = master_key.to_vec();
332        for (i, byte) in obfuscated_master.iter_mut().enumerate() {
333            *byte ^= obfuscate_key[i % obfuscate_key.len()];
334        }
335
336        let mut entry = Vec::new();
337        let pod_size = 40 + key_id.len() + 3 + 0 + 32;
338        entry.extend_from_slice(&(pod_size as u64).to_le_bytes());
339        entry.extend_from_slice(&(key_id.len() as u64).to_le_bytes());
340        entry.extend_from_slice(&(3u64).to_le_bytes()); // "AES"
341        entry.extend_from_slice(&(0u64).to_le_bytes()); // ""
342        entry.extend_from_slice(&(32u64).to_le_bytes());
343        entry.extend_from_slice(key_id.as_bytes());
344        entry.extend_from_slice(b"AES");
345        entry.extend_from_slice(&obfuscated_master);
346
347        let mut file_data = entry;
348        let mut hasher = Sha256::new();
349        hasher.update(&file_data);
350        let hash = hasher.finalize();
351        file_data.extend_from_slice(&hash);
352
353        let tmp = tempfile::NamedTempFile::new().unwrap();
354        std::fs::write(tmp.path(), &file_data).unwrap();
355
356        let keyring = Keyring::load(tmp.path()).unwrap();
357        let ctx = DecryptionContext::from_encryption_info(&info, &keyring).unwrap();
358
359        assert_eq!(ctx.tablespace_key, ts_key);
360        assert_eq!(ctx.tablespace_iv, ts_iv);
361    }
362
363    #[test]
364    fn test_from_encryption_info_wrong_key() {
365        use crate::innodb::keyring::Keyring;
366        use sha2::{Digest, Sha256};
367
368        let master_key: [u8; 32] = [0xAA; 32];
369        let wrong_master: [u8; 32] = [0xDD; 32];
370        let ts_key: [u8; 32] = [0xBB; 32];
371        let ts_iv: [u8; 32] = [0xCC; 32];
372
373        let mut key_iv_data = [0u8; 64];
374        key_iv_data[..32].copy_from_slice(&ts_key);
375        key_iv_data[32..].copy_from_slice(&ts_iv);
376        let crc = crc32c::crc32c(&key_iv_data);
377
378        // Encrypt with the correct master key
379        let encryptor = Aes256EcbEnc::new_from_slice(&master_key).unwrap();
380        let mut encrypted_key_iv = key_iv_data;
381        encryptor
382            .encrypt_padded_mut::<NoPadding>(&mut encrypted_key_iv, 64)
383            .unwrap();
384
385        let uuid = "12345678-1234-1234-1234-123456789abc";
386        let info = EncryptionInfo {
387            magic_version: 3,
388            master_key_id: 1,
389            server_uuid: uuid.to_string(),
390            encrypted_key_iv,
391            checksum: crc,
392        };
393
394        // Build keyring with WRONG master key
395        let obfuscate_key = b"*305=Ljt0*!@$Hnm(*-9-w;:";
396        let key_id = format!("INNODBKey-{}-1", uuid);
397        let mut obfuscated = wrong_master.to_vec();
398        for (i, byte) in obfuscated.iter_mut().enumerate() {
399            *byte ^= obfuscate_key[i % obfuscate_key.len()];
400        }
401
402        let mut entry = Vec::new();
403        let pod_size = 40 + key_id.len() + 3 + 0 + 32;
404        entry.extend_from_slice(&(pod_size as u64).to_le_bytes());
405        entry.extend_from_slice(&(key_id.len() as u64).to_le_bytes());
406        entry.extend_from_slice(&(3u64).to_le_bytes());
407        entry.extend_from_slice(&(0u64).to_le_bytes());
408        entry.extend_from_slice(&(32u64).to_le_bytes());
409        entry.extend_from_slice(key_id.as_bytes());
410        entry.extend_from_slice(b"AES");
411        entry.extend_from_slice(&obfuscated);
412
413        let mut file_data = entry;
414        let mut hasher = Sha256::new();
415        hasher.update(&file_data);
416        let hash = hasher.finalize();
417        file_data.extend_from_slice(&hash);
418
419        let tmp = tempfile::NamedTempFile::new().unwrap();
420        std::fs::write(tmp.path(), &file_data).unwrap();
421
422        let keyring = Keyring::load(tmp.path()).unwrap();
423        let result = DecryptionContext::from_encryption_info(&info, &keyring);
424        assert!(result.is_err());
425        assert!(result
426            .unwrap_err()
427            .to_string()
428            .contains("CRC32 checksum mismatch"));
429    }
430}