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.
28pub fn detect_encryption(fsp_flags: u32, vendor_info: Option<&VendorInfo>) -> EncryptionAlgorithm {
29    // MariaDB: no tablespace-level encryption flag
30    if vendor_info.is_some_and(|v| v.vendor == crate::innodb::vendor::InnoDbVendor::MariaDB) {
31        return EncryptionAlgorithm::None;
32    }
33
34    if (fsp_flags >> 13) & 0x01 != 0 {
35        EncryptionAlgorithm::Aes
36    } else {
37        EncryptionAlgorithm::None
38    }
39}
40
41/// Check if a tablespace is encrypted based on its FSP flags.
42pub fn is_encrypted(fsp_flags: u32) -> bool {
43    detect_encryption(fsp_flags, None) != EncryptionAlgorithm::None
44}
45
46/// Check if a page type indicates MariaDB page-level encryption.
47pub fn is_mariadb_encrypted_page(page_type: u16) -> bool {
48    page_type == crate::innodb::constants::FIL_PAGE_PAGE_COMPRESSED_ENCRYPTED
49}
50
51/// Read the encryption key version from a MariaDB encrypted page.
52///
53/// For page type 37401 (PAGE_COMPRESSED_ENCRYPTED), the key version
54/// is stored as a u32 at byte offset 26.
55pub fn mariadb_encryption_key_version(page_data: &[u8]) -> Option<u32> {
56    if page_data.len() < 30 {
57        return None;
58    }
59    Some(BigEndian::read_u32(&page_data[26..]))
60}
61
62/// Parsed encryption info from page 0 of an encrypted tablespace.
63///
64/// Located after the XDES array on page 0, this structure contains the
65/// master key ID, server UUID, and the encrypted tablespace key+IV needed
66/// to decrypt individual pages.
67#[derive(Debug, Clone, Serialize)]
68pub struct EncryptionInfo {
69    /// Encryption info version (1 = `lCA`, 2 = `lCB`, 3 = `lCC`/MySQL 8.0.5+).
70    pub magic_version: u8,
71    /// Master key ID from the keyring.
72    pub master_key_id: u32,
73    /// Server UUID string (36 ASCII characters).
74    pub server_uuid: String,
75    /// Encrypted tablespace key (32 bytes) + IV (32 bytes), AES-256-ECB encrypted.
76    #[serde(skip)]
77    pub encrypted_key_iv: [u8; 64],
78    /// CRC32 checksum of the plaintext key+IV.
79    pub checksum: u32,
80}
81
82/// Compute the number of pages per extent for a given page size.
83fn pages_per_extent(page_size: u32) -> u32 {
84    if page_size <= 16384 {
85        1048576 / page_size // 1MB extents for page sizes <= 16K
86    } else {
87        64 // 64 pages per extent for larger page sizes
88    }
89}
90
91/// Compute the number of XDES entries on page 0 for a given page size.
92fn xdes_arr_size(page_size: u32) -> u32 {
93    page_size / pages_per_extent(page_size)
94}
95
96/// Compute the byte offset of the encryption info on page 0.
97///
98/// Layout: FIL_PAGE_DATA(38) + FSP_HEADER(112) + XDES_ARRAY(entries * 40)
99pub fn encryption_info_offset(page_size: u32) -> usize {
100    let xdes_arr_offset = FIL_PAGE_DATA + FSP_HEADER_SIZE;
101    let xdes_entries = xdes_arr_size(page_size) as usize;
102    xdes_arr_offset + xdes_entries * XDES_SIZE
103}
104
105/// Parse encryption info from page 0 of a tablespace.
106///
107/// Returns `None` if the page does not contain valid encryption info
108/// (no magic marker found at the expected offset).
109pub fn parse_encryption_info(page0: &[u8], page_size: u32) -> Option<EncryptionInfo> {
110    let offset = encryption_info_offset(page_size);
111
112    if page0.len() < offset + ENCRYPTION_INFO_SIZE {
113        return None;
114    }
115
116    let magic = &page0[offset..offset + ENCRYPTION_MAGIC_SIZE];
117    let magic_version = if magic == ENCRYPTION_MAGIC_V1 {
118        1
119    } else if magic == ENCRYPTION_MAGIC_V2 {
120        2
121    } else if magic == ENCRYPTION_MAGIC_V3 {
122        3
123    } else {
124        return None;
125    };
126
127    let master_key_id = BigEndian::read_u32(&page0[offset + 3..]);
128    let uuid_bytes = &page0[offset + 7..offset + 7 + ENCRYPTION_SERVER_UUID_LEN];
129    let server_uuid = String::from_utf8_lossy(uuid_bytes).to_string();
130
131    let mut encrypted_key_iv = [0u8; 64];
132    encrypted_key_iv.copy_from_slice(&page0[offset + 43..offset + 43 + 64]);
133
134    let checksum = BigEndian::read_u32(&page0[offset + 107..]);
135
136    Some(EncryptionInfo {
137        magic_version,
138        master_key_id,
139        server_uuid,
140        encrypted_key_iv,
141        checksum,
142    })
143}
144
145impl std::fmt::Display for EncryptionAlgorithm {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match self {
148            EncryptionAlgorithm::None => write!(f, "None"),
149            EncryptionAlgorithm::Aes => write!(f, "AES"),
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::innodb::vendor::MariaDbFormat;
158
159    #[test]
160    fn test_detect_encryption_mysql() {
161        assert_eq!(detect_encryption(0, None), EncryptionAlgorithm::None);
162        assert_eq!(detect_encryption(1 << 13, None), EncryptionAlgorithm::Aes);
163        assert_eq!(detect_encryption(0xFF, None), EncryptionAlgorithm::None);
164        assert_eq!(
165            detect_encryption(0xFF | (1 << 13), None),
166            EncryptionAlgorithm::Aes
167        );
168    }
169
170    #[test]
171    fn test_detect_encryption_mariadb_returns_none() {
172        let vendor = VendorInfo::mariadb(MariaDbFormat::FullCrc32);
173        // Even with bit 13 set, MariaDB returns None (no TS-level encryption)
174        assert_eq!(
175            detect_encryption(1 << 13, Some(&vendor)),
176            EncryptionAlgorithm::None
177        );
178    }
179
180    #[test]
181    fn test_is_encrypted() {
182        assert!(!is_encrypted(0));
183        assert!(is_encrypted(1 << 13));
184    }
185
186    #[test]
187    fn test_is_mariadb_encrypted_page() {
188        assert!(is_mariadb_encrypted_page(37401));
189        assert!(!is_mariadb_encrypted_page(17855));
190    }
191
192    #[test]
193    fn test_mariadb_encryption_key_version() {
194        let mut page = vec![0u8; 38];
195        BigEndian::write_u32(&mut page[26..], 42);
196        assert_eq!(mariadb_encryption_key_version(&page), Some(42));
197    }
198
199    #[test]
200    fn test_encryption_info_offset_16k() {
201        // FIL_PAGE_DATA(38) + FSP_HEADER_SIZE(112) + 256 * 40 = 10390
202        assert_eq!(encryption_info_offset(16384), 10390);
203    }
204
205    #[test]
206    fn test_encryption_info_offset_various() {
207        assert_eq!(encryption_info_offset(4096), 38 + 112 + 16 * 40); // 790
208        assert_eq!(encryption_info_offset(8192), 38 + 112 + 64 * 40); // 2710
209        assert_eq!(encryption_info_offset(32768), 38 + 112 + 512 * 40); // 20630
210    }
211
212    #[test]
213    fn test_parse_encryption_info_v3() {
214        let mut page = vec![0u8; 16384];
215        let offset = encryption_info_offset(16384);
216
217        // Write magic V3
218        page[offset..offset + 3].copy_from_slice(b"lCC");
219        // Master key ID
220        BigEndian::write_u32(&mut page[offset + 3..], 42);
221        // Server UUID (36 bytes)
222        let uuid = "12345678-1234-1234-1234-123456789abc";
223        page[offset + 7..offset + 7 + 36].copy_from_slice(uuid.as_bytes());
224        // Encrypted key+IV (64 bytes) — fill with pattern
225        for i in 0..64 {
226            page[offset + 43 + i] = i as u8;
227        }
228        // CRC32 checksum
229        BigEndian::write_u32(&mut page[offset + 107..], 0xDEADBEEF);
230
231        let info = parse_encryption_info(&page, 16384).unwrap();
232        assert_eq!(info.magic_version, 3);
233        assert_eq!(info.master_key_id, 42);
234        assert_eq!(info.server_uuid, uuid);
235        assert_eq!(info.checksum, 0xDEADBEEF);
236        assert_eq!(info.encrypted_key_iv[0], 0);
237        assert_eq!(info.encrypted_key_iv[63], 63);
238    }
239
240    #[test]
241    fn test_parse_encryption_info_v1() {
242        let mut page = vec![0u8; 16384];
243        let offset = encryption_info_offset(16384);
244        page[offset..offset + 3].copy_from_slice(b"lCA");
245        BigEndian::write_u32(&mut page[offset + 3..], 1);
246        let uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
247        page[offset + 7..offset + 7 + 36].copy_from_slice(uuid.as_bytes());
248        BigEndian::write_u32(&mut page[offset + 107..], 0x12345678);
249
250        let info = parse_encryption_info(&page, 16384).unwrap();
251        assert_eq!(info.magic_version, 1);
252        assert_eq!(info.master_key_id, 1);
253    }
254
255    #[test]
256    fn test_parse_encryption_info_no_magic() {
257        let page = vec![0u8; 16384];
258        assert!(parse_encryption_info(&page, 16384).is_none());
259    }
260
261    #[test]
262    fn test_parse_encryption_info_bad_magic() {
263        let mut page = vec![0u8; 16384];
264        let offset = encryption_info_offset(16384);
265        page[offset..offset + 3].copy_from_slice(b"lCD");
266        assert!(parse_encryption_info(&page, 16384).is_none());
267    }
268}