Skip to main content

idb/innodb/
encryption.rs

1//! Tablespace encryption detection and encryption info parsing.
2//!
3//! Detects whether a tablespace is encrypted by inspecting FSP flags,
4//! and parses the encryption info structure from page 0 that contains
5//! the encrypted tablespace key and IV needed for decryption.
6//!
7//! MySQL uses bit 13 for tablespace-level encryption (AES). MariaDB does
8//! not have a tablespace-level encryption flag; encryption is per-page
9//! (page type 37401).
10
11use crate::innodb::constants::*;
12use crate::innodb::vendor::VendorInfo;
13use byteorder::{BigEndian, ByteOrder};
14use serde::Serialize;
15
16/// Encryption algorithm detected from FSP flags.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EncryptionAlgorithm {
19    None,
20    Aes,
21}
22
23/// Detect encryption from FSP space flags.
24///
25/// When `vendor_info` indicates MariaDB, returns `None` because MariaDB
26/// does not use a tablespace-level encryption flag. For MySQL/Percona,
27/// checks bit 13 of FSP flags.
28///
29/// # Examples
30///
31/// ```
32/// use idb::innodb::encryption::{detect_encryption, EncryptionAlgorithm};
33/// use idb::innodb::vendor::{VendorInfo, MariaDbFormat};
34///
35/// // No encryption flag set → None
36/// assert_eq!(detect_encryption(0, None), EncryptionAlgorithm::None);
37///
38/// // Bit 13 set → AES encryption detected
39/// assert_eq!(detect_encryption(1 << 13, None), EncryptionAlgorithm::Aes);
40///
41/// // MariaDB always returns None (encryption is per-page, not tablespace-level)
42/// let maria = VendorInfo::mariadb(MariaDbFormat::FullCrc32);
43/// assert_eq!(detect_encryption(1 << 13, Some(&maria)), EncryptionAlgorithm::None);
44/// ```
45pub fn detect_encryption(fsp_flags: u32, vendor_info: Option<&VendorInfo>) -> EncryptionAlgorithm {
46    // MariaDB: no tablespace-level encryption flag
47    if vendor_info.is_some_and(|v| v.vendor == crate::innodb::vendor::InnoDbVendor::MariaDB) {
48        return EncryptionAlgorithm::None;
49    }
50
51    if (fsp_flags >> 13) & 0x01 != 0 {
52        EncryptionAlgorithm::Aes
53    } else {
54        EncryptionAlgorithm::None
55    }
56}
57
58/// Check if a tablespace is encrypted based on its FSP flags.
59///
60/// This is a convenience wrapper around [`detect_encryption`] without
61/// vendor-specific handling. It checks whether bit 13 of the FSP flags
62/// is set (MySQL/Percona AES encryption).
63///
64/// # Examples
65///
66/// ```
67/// use idb::innodb::encryption::is_encrypted;
68///
69/// // No encryption
70/// assert!(!is_encrypted(0));
71///
72/// // Bit 13 set → encrypted
73/// assert!(is_encrypted(1 << 13));
74///
75/// // Other bits do not indicate encryption
76/// assert!(!is_encrypted(0xFF));
77/// ```
78pub fn is_encrypted(fsp_flags: u32) -> bool {
79    detect_encryption(fsp_flags, None) != EncryptionAlgorithm::None
80}
81
82/// Check if a page type indicates MariaDB page-level encryption.
83///
84/// MariaDB uses page type 37401 (`FIL_PAGE_PAGE_COMPRESSED_ENCRYPTED`)
85/// for pages that are both compressed and encrypted at the page level.
86///
87/// # Examples
88///
89/// ```
90/// use idb::innodb::encryption::is_mariadb_encrypted_page;
91///
92/// // MariaDB compressed+encrypted page type
93/// assert!(is_mariadb_encrypted_page(37401));
94///
95/// // Standard INDEX page type
96/// assert!(!is_mariadb_encrypted_page(17855));
97/// ```
98pub fn is_mariadb_encrypted_page(page_type: u16) -> bool {
99    page_type == crate::innodb::constants::FIL_PAGE_PAGE_COMPRESSED_ENCRYPTED
100}
101
102/// Read the encryption key version from a MariaDB encrypted page.
103///
104/// For page type 37401 (PAGE_COMPRESSED_ENCRYPTED), the key version
105/// is stored as a u32 at byte offset 26.
106///
107/// # Examples
108///
109/// ```
110/// use idb::innodb::encryption::mariadb_encryption_key_version;
111///
112/// // Build a minimal page with a key version at offset 26 (big-endian)
113/// let mut page = vec![0u8; 38];
114/// page[26] = 0x00;
115/// page[27] = 0x00;
116/// page[28] = 0x00;
117/// page[29] = 0x05;
118/// assert_eq!(mariadb_encryption_key_version(&page), Some(5));
119///
120/// // Too-short data returns None
121/// let short = vec![0u8; 10];
122/// assert_eq!(mariadb_encryption_key_version(&short), None);
123/// ```
124pub fn mariadb_encryption_key_version(page_data: &[u8]) -> Option<u32> {
125    if page_data.len() < 30 {
126        return None;
127    }
128    Some(BigEndian::read_u32(&page_data[26..]))
129}
130
131/// Parsed encryption info from page 0 of an encrypted tablespace.
132///
133/// Located after the XDES array on page 0, this structure contains the
134/// master key ID, server UUID, and the encrypted tablespace key+IV needed
135/// to decrypt individual pages.
136#[derive(Debug, Clone, Serialize)]
137pub struct EncryptionInfo {
138    /// Encryption info version (1 = `lCA`, 2 = `lCB`, 3 = `lCC`/MySQL 8.0.5+).
139    pub magic_version: u8,
140    /// Master key ID from the keyring.
141    pub master_key_id: u32,
142    /// Server UUID string (36 ASCII characters).
143    pub server_uuid: String,
144    /// Encrypted tablespace key (32 bytes) + IV (32 bytes), AES-256-ECB encrypted.
145    #[serde(skip)]
146    pub encrypted_key_iv: [u8; 64],
147    /// CRC32 checksum of the plaintext key+IV.
148    pub checksum: u32,
149}
150
151/// Compute the number of pages per extent for a given page size.
152fn pages_per_extent(page_size: u32) -> u32 {
153    if page_size <= 16384 {
154        1048576 / page_size // 1MB extents for page sizes <= 16K
155    } else {
156        64 // 64 pages per extent for larger page sizes
157    }
158}
159
160/// Compute the number of XDES entries on page 0 for a given page size.
161fn xdes_arr_size(page_size: u32) -> u32 {
162    page_size / pages_per_extent(page_size)
163}
164
165/// Compute the byte offset of the encryption info on page 0.
166///
167/// Layout: FIL_PAGE_DATA(38) + FSP_HEADER(112) + XDES_ARRAY(entries * 40)
168///
169/// # Examples
170///
171/// ```
172/// use idb::innodb::encryption::encryption_info_offset;
173///
174/// // For 16K pages: 38 + 112 + (256 * 40) = 10390
175/// assert_eq!(encryption_info_offset(16384), 10390);
176///
177/// // For 4K pages: 38 + 112 + (16 * 40) = 790
178/// assert_eq!(encryption_info_offset(4096), 790);
179/// ```
180pub fn encryption_info_offset(page_size: u32) -> usize {
181    let xdes_arr_offset = FIL_PAGE_DATA + FSP_HEADER_SIZE;
182    let xdes_entries = xdes_arr_size(page_size) as usize;
183    xdes_arr_offset + xdes_entries * XDES_SIZE
184}
185
186/// Parse encryption info from page 0 of a tablespace.
187///
188/// Returns `None` if the page does not contain valid encryption info
189/// (no magic marker found at the expected offset).
190///
191/// # Examples
192///
193/// ```
194/// use idb::innodb::encryption::{parse_encryption_info, encryption_info_offset};
195///
196/// // Build a synthetic 16K page with encryption info V3 (magic "lCC")
197/// let page_size = 16384u32;
198/// let mut page = vec![0u8; page_size as usize];
199/// let offset = encryption_info_offset(page_size);
200///
201/// // Write magic V3 marker
202/// page[offset..offset + 3].copy_from_slice(b"lCC");
203/// // Master key ID = 1 (big-endian u32 at offset+3)
204/// page[offset + 6] = 1;
205/// // Server UUID (36 ASCII bytes at offset+7)
206/// let uuid = b"01234567-89ab-cdef-0123-456789abcdef";
207/// page[offset + 7..offset + 7 + 36].copy_from_slice(uuid);
208///
209/// let info = parse_encryption_info(&page, page_size).unwrap();
210/// assert_eq!(info.magic_version, 3);
211/// assert_eq!(info.master_key_id, 1);
212/// assert_eq!(info.server_uuid, "01234567-89ab-cdef-0123-456789abcdef");
213///
214/// // No magic marker → returns None
215/// let empty_page = vec![0u8; page_size as usize];
216/// assert!(parse_encryption_info(&empty_page, page_size).is_none());
217/// ```
218pub fn parse_encryption_info(page0: &[u8], page_size: u32) -> Option<EncryptionInfo> {
219    let offset = encryption_info_offset(page_size);
220
221    if page0.len() < offset + ENCRYPTION_INFO_SIZE {
222        return None;
223    }
224
225    let magic = &page0[offset..offset + ENCRYPTION_MAGIC_SIZE];
226    let magic_version = if magic == ENCRYPTION_MAGIC_V1 {
227        1
228    } else if magic == ENCRYPTION_MAGIC_V2 {
229        2
230    } else if magic == ENCRYPTION_MAGIC_V3 {
231        3
232    } else {
233        return None;
234    };
235
236    let master_key_id = BigEndian::read_u32(&page0[offset + 3..]);
237    let uuid_bytes = &page0[offset + 7..offset + 7 + ENCRYPTION_SERVER_UUID_LEN];
238    let server_uuid = String::from_utf8_lossy(uuid_bytes).to_string();
239
240    let mut encrypted_key_iv = [0u8; 64];
241    encrypted_key_iv.copy_from_slice(&page0[offset + 43..offset + 43 + 64]);
242
243    let checksum = BigEndian::read_u32(&page0[offset + 107..]);
244
245    Some(EncryptionInfo {
246        magic_version,
247        master_key_id,
248        server_uuid,
249        encrypted_key_iv,
250        checksum,
251    })
252}
253
254impl std::fmt::Display for EncryptionAlgorithm {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            EncryptionAlgorithm::None => write!(f, "None"),
258            EncryptionAlgorithm::Aes => write!(f, "AES"),
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::innodb::vendor::MariaDbFormat;
267
268    #[test]
269    fn test_detect_encryption_mysql() {
270        assert_eq!(detect_encryption(0, None), EncryptionAlgorithm::None);
271        assert_eq!(detect_encryption(1 << 13, None), EncryptionAlgorithm::Aes);
272        assert_eq!(detect_encryption(0xFF, None), EncryptionAlgorithm::None);
273        assert_eq!(
274            detect_encryption(0xFF | (1 << 13), None),
275            EncryptionAlgorithm::Aes
276        );
277    }
278
279    #[test]
280    fn test_detect_encryption_mariadb_returns_none() {
281        let vendor = VendorInfo::mariadb(MariaDbFormat::FullCrc32);
282        // Even with bit 13 set, MariaDB returns None (no TS-level encryption)
283        assert_eq!(
284            detect_encryption(1 << 13, Some(&vendor)),
285            EncryptionAlgorithm::None
286        );
287    }
288
289    #[test]
290    fn test_is_encrypted() {
291        assert!(!is_encrypted(0));
292        assert!(is_encrypted(1 << 13));
293    }
294
295    #[test]
296    fn test_is_mariadb_encrypted_page() {
297        assert!(is_mariadb_encrypted_page(37401));
298        assert!(!is_mariadb_encrypted_page(17855));
299    }
300
301    #[test]
302    fn test_mariadb_encryption_key_version() {
303        let mut page = vec![0u8; 38];
304        BigEndian::write_u32(&mut page[26..], 42);
305        assert_eq!(mariadb_encryption_key_version(&page), Some(42));
306    }
307
308    #[test]
309    fn test_encryption_info_offset_16k() {
310        // FIL_PAGE_DATA(38) + FSP_HEADER_SIZE(112) + 256 * 40 = 10390
311        assert_eq!(encryption_info_offset(16384), 10390);
312    }
313
314    #[test]
315    fn test_encryption_info_offset_various() {
316        assert_eq!(encryption_info_offset(4096), 38 + 112 + 16 * 40); // 790
317        assert_eq!(encryption_info_offset(8192), 38 + 112 + 64 * 40); // 2710
318        assert_eq!(encryption_info_offset(32768), 38 + 112 + 512 * 40); // 20630
319    }
320
321    #[test]
322    fn test_parse_encryption_info_v3() {
323        let mut page = vec![0u8; 16384];
324        let offset = encryption_info_offset(16384);
325
326        // Write magic V3
327        page[offset..offset + 3].copy_from_slice(b"lCC");
328        // Master key ID
329        BigEndian::write_u32(&mut page[offset + 3..], 42);
330        // Server UUID (36 bytes)
331        let uuid = "12345678-1234-1234-1234-123456789abc";
332        page[offset + 7..offset + 7 + 36].copy_from_slice(uuid.as_bytes());
333        // Encrypted key+IV (64 bytes) — fill with pattern
334        for i in 0..64 {
335            page[offset + 43 + i] = i as u8;
336        }
337        // CRC32 checksum
338        BigEndian::write_u32(&mut page[offset + 107..], 0xDEADBEEF);
339
340        let info = parse_encryption_info(&page, 16384).unwrap();
341        assert_eq!(info.magic_version, 3);
342        assert_eq!(info.master_key_id, 42);
343        assert_eq!(info.server_uuid, uuid);
344        assert_eq!(info.checksum, 0xDEADBEEF);
345        assert_eq!(info.encrypted_key_iv[0], 0);
346        assert_eq!(info.encrypted_key_iv[63], 63);
347    }
348
349    #[test]
350    fn test_parse_encryption_info_v1() {
351        let mut page = vec![0u8; 16384];
352        let offset = encryption_info_offset(16384);
353        page[offset..offset + 3].copy_from_slice(b"lCA");
354        BigEndian::write_u32(&mut page[offset + 3..], 1);
355        let uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
356        page[offset + 7..offset + 7 + 36].copy_from_slice(uuid.as_bytes());
357        BigEndian::write_u32(&mut page[offset + 107..], 0x12345678);
358
359        let info = parse_encryption_info(&page, 16384).unwrap();
360        assert_eq!(info.magic_version, 1);
361        assert_eq!(info.master_key_id, 1);
362    }
363
364    #[test]
365    fn test_parse_encryption_info_no_magic() {
366        let page = vec![0u8; 16384];
367        assert!(parse_encryption_info(&page, 16384).is_none());
368    }
369
370    #[test]
371    fn test_parse_encryption_info_bad_magic() {
372        let mut page = vec![0u8; 16384];
373        let offset = encryption_info_offset(16384);
374        page[offset..offset + 3].copy_from_slice(b"lCD");
375        assert!(parse_encryption_info(&page, 16384).is_none());
376    }
377}