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