Skip to main content

idb/innodb/
keyring.rs

1//! MySQL `keyring_file` plugin binary format reader.
2//!
3//! Parses the legacy binary keyring file format used by the `keyring_file`
4//! MySQL plugin (MySQL 5.7.11+). Each key entry is serialized with length
5//! prefixes and the key data is XOR-obfuscated. The file ends with a
6//! SHA-256 digest over all preceding bytes for integrity verification.
7
8use std::path::Path;
9
10use sha2::{Digest, Sha256};
11
12use crate::IdbError;
13
14/// XOR obfuscation key used by MySQL's `keyring_file` plugin.
15const OBFUSCATE_KEY: &[u8] = b"*305=Ljt0*!@$Hnm(*-9-w;:";
16
17/// A single entry from a MySQL keyring file.
18#[derive(Debug, Clone)]
19pub struct KeyringEntry {
20    /// Key identifier (e.g., `INNODBKey-{uuid}-{id}`).
21    pub key_id: String,
22    /// Key type (e.g., `AES`).
23    pub key_type: String,
24    /// User ID associated with the key.
25    pub user_id: String,
26    /// De-obfuscated key data.
27    pub key_data: Vec<u8>,
28}
29
30/// A parsed MySQL keyring file.
31#[derive(Debug)]
32pub struct Keyring {
33    entries: Vec<KeyringEntry>,
34}
35
36impl Keyring {
37    /// Load and parse a MySQL `keyring_file` from disk.
38    ///
39    /// Reads the binary file, verifies the trailing SHA-256 checksum,
40    /// and parses all key entries with XOR de-obfuscation.
41    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, IdbError> {
42        let path = path.as_ref();
43        let data = std::fs::read(path)
44            .map_err(|e| IdbError::Io(format!("Cannot read keyring file {}: {}", path.display(), e)))?;
45
46        if data.len() < 32 {
47            return Err(IdbError::Parse(
48                "Keyring file too small (must contain at least SHA-256 digest)".to_string(),
49            ));
50        }
51
52        // Verify SHA-256 checksum (last 32 bytes)
53        let content_len = data.len() - 32;
54        let content = &data[..content_len];
55        let stored_hash = &data[content_len..];
56
57        let mut hasher = Sha256::new();
58        hasher.update(content);
59        let computed_hash = hasher.finalize();
60
61        if computed_hash.as_slice() != stored_hash {
62            return Err(IdbError::Parse(
63                "Keyring file SHA-256 checksum mismatch (file may be corrupt)".to_string(),
64            ));
65        }
66
67        // Parse entries from content
68        let entries = parse_entries(content)?;
69
70        Ok(Keyring { entries })
71    }
72
73    /// Find a key entry by its full key ID string.
74    pub fn find_key(&self, key_id: &str) -> Option<&KeyringEntry> {
75        self.entries.iter().find(|e| e.key_id == key_id)
76    }
77
78    /// Find the InnoDB master key for a given server UUID and key ID number.
79    ///
80    /// Constructs the key ID as `INNODBKey-{server_uuid}-{key_id}` and
81    /// looks it up in the keyring.
82    pub fn find_innodb_master_key(&self, server_uuid: &str, key_id: u32) -> Option<&[u8]> {
83        let full_id = format!("INNODBKey-{}-{}", server_uuid, key_id);
84        self.find_key(&full_id).map(|e| e.key_data.as_slice())
85    }
86
87    /// Returns the number of entries in the keyring.
88    pub fn len(&self) -> usize {
89        self.entries.len()
90    }
91
92    /// Returns true if the keyring contains no entries.
93    pub fn is_empty(&self) -> bool {
94        self.entries.is_empty()
95    }
96}
97
98/// XOR de-obfuscate key data using MySQL's obfuscation key.
99fn deobfuscate(data: &mut [u8]) {
100    let key_len = OBFUSCATE_KEY.len();
101    for (i, byte) in data.iter_mut().enumerate() {
102        *byte ^= OBFUSCATE_KEY[i % key_len];
103    }
104}
105
106/// Read a little-endian u64 from a byte slice.
107fn read_le_u64(data: &[u8]) -> u64 {
108    u64::from_le_bytes(data[..8].try_into().unwrap())
109}
110
111/// Parse all keyring entries from the content portion of the file.
112fn parse_entries(mut data: &[u8]) -> Result<Vec<KeyringEntry>, IdbError> {
113    let mut entries = Vec::new();
114
115    while !data.is_empty() {
116        if data.len() < 40 {
117            // Need at least 5 * 8 bytes for the length headers
118            break;
119        }
120
121        // Each entry: [pod_size(8)][key_id_len(8)][key_type_len(8)][user_id_len(8)][key_len(8)]
122        //             [key_id][key_type][user_id][key_data]
123        let pod_size = read_le_u64(&data[0..8]) as usize;
124        let key_id_len = read_le_u64(&data[8..16]) as usize;
125        let key_type_len = read_le_u64(&data[16..24]) as usize;
126        let user_id_len = read_le_u64(&data[24..32]) as usize;
127        let key_len = read_le_u64(&data[32..40]) as usize;
128
129        let header_size = 40;
130        let total_data = key_id_len + key_type_len + user_id_len + key_len;
131        let entry_size = header_size + total_data;
132
133        // Validate sizes
134        if pod_size == 0 || entry_size > data.len() {
135            break;
136        }
137
138        let mut offset = header_size;
139
140        let key_id = String::from_utf8_lossy(&data[offset..offset + key_id_len]).to_string();
141        offset += key_id_len;
142
143        let key_type = String::from_utf8_lossy(&data[offset..offset + key_type_len]).to_string();
144        offset += key_type_len;
145
146        let user_id = String::from_utf8_lossy(&data[offset..offset + user_id_len]).to_string();
147        offset += user_id_len;
148
149        let mut key_data = data[offset..offset + key_len].to_vec();
150        deobfuscate(&mut key_data);
151
152        entries.push(KeyringEntry {
153            key_id,
154            key_type,
155            user_id,
156            key_data,
157        });
158
159        data = &data[entry_size..];
160    }
161
162    Ok(entries)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_deobfuscate_roundtrip() {
171        let original = vec![0x41, 0x42, 0x43, 0x44];
172        let mut data = original.clone();
173        deobfuscate(&mut data);
174        // After one XOR, should differ
175        assert_ne!(data, original);
176        // After second XOR, should be back to original
177        deobfuscate(&mut data);
178        assert_eq!(data, original);
179    }
180
181    #[test]
182    fn test_deobfuscate_wraps_key() {
183        // Data longer than OBFUSCATE_KEY should wrap
184        let mut data = vec![0u8; OBFUSCATE_KEY.len() * 2 + 5];
185        deobfuscate(&mut data);
186        // First and (key_len+1)th bytes should use same XOR key byte
187        assert_eq!(data[0], data[OBFUSCATE_KEY.len()]);
188    }
189
190    fn build_keyring_entry(key_id: &str, key_type: &str, user_id: &str, key_data: &[u8]) -> Vec<u8> {
191        let mut obfuscated = key_data.to_vec();
192        deobfuscate(&mut obfuscated);
193
194        let pod_size = 40 + key_id.len() + key_type.len() + user_id.len() + key_data.len();
195        let mut entry = Vec::new();
196        entry.extend_from_slice(&(pod_size as u64).to_le_bytes());
197        entry.extend_from_slice(&(key_id.len() as u64).to_le_bytes());
198        entry.extend_from_slice(&(key_type.len() as u64).to_le_bytes());
199        entry.extend_from_slice(&(user_id.len() as u64).to_le_bytes());
200        entry.extend_from_slice(&(key_data.len() as u64).to_le_bytes());
201        entry.extend_from_slice(key_id.as_bytes());
202        entry.extend_from_slice(key_type.as_bytes());
203        entry.extend_from_slice(user_id.as_bytes());
204        entry.extend_from_slice(&obfuscated);
205        entry
206    }
207
208    fn build_keyring_file(entries: &[Vec<u8>]) -> Vec<u8> {
209        let mut data = Vec::new();
210        for entry in entries {
211            data.extend_from_slice(entry);
212        }
213        let mut hasher = Sha256::new();
214        hasher.update(&data);
215        let hash = hasher.finalize();
216        data.extend_from_slice(&hash);
217        data
218    }
219
220    #[test]
221    fn test_parse_single_entry() {
222        let key_data = vec![0x01, 0x02, 0x03, 0x04];
223        let entry = build_keyring_entry("test-key", "AES", "user1", &key_data);
224        let file_data = build_keyring_file(&[entry]);
225
226        let tmp = tempfile::NamedTempFile::new().unwrap();
227        std::fs::write(tmp.path(), &file_data).unwrap();
228
229        let keyring = Keyring::load(tmp.path()).unwrap();
230        assert_eq!(keyring.len(), 1);
231        let e = keyring.find_key("test-key").unwrap();
232        assert_eq!(e.key_type, "AES");
233        assert_eq!(e.user_id, "user1");
234        assert_eq!(e.key_data, key_data);
235    }
236
237    #[test]
238    fn test_parse_multiple_entries() {
239        let key1 = vec![0xAA; 32];
240        let key2 = vec![0xBB; 32];
241        let entry1 = build_keyring_entry("INNODBKey-uuid-1", "AES", "", &key1);
242        let entry2 = build_keyring_entry("INNODBKey-uuid-2", "AES", "", &key2);
243        let file_data = build_keyring_file(&[entry1, entry2]);
244
245        let tmp = tempfile::NamedTempFile::new().unwrap();
246        std::fs::write(tmp.path(), &file_data).unwrap();
247
248        let keyring = Keyring::load(tmp.path()).unwrap();
249        assert_eq!(keyring.len(), 2);
250        assert_eq!(keyring.find_key("INNODBKey-uuid-1").unwrap().key_data, key1);
251        assert_eq!(keyring.find_key("INNODBKey-uuid-2").unwrap().key_data, key2);
252    }
253
254    #[test]
255    fn test_find_innodb_master_key() {
256        let key_data = vec![0xCC; 32];
257        let entry = build_keyring_entry(
258            "INNODBKey-12345678-1234-1234-1234-123456789abc-1",
259            "AES",
260            "",
261            &key_data,
262        );
263        let file_data = build_keyring_file(&[entry]);
264
265        let tmp = tempfile::NamedTempFile::new().unwrap();
266        std::fs::write(tmp.path(), &file_data).unwrap();
267
268        let keyring = Keyring::load(tmp.path()).unwrap();
269        let found = keyring
270            .find_innodb_master_key("12345678-1234-1234-1234-123456789abc", 1)
271            .unwrap();
272        assert_eq!(found, &key_data[..]);
273    }
274
275    #[test]
276    fn test_find_innodb_master_key_not_found() {
277        let entry = build_keyring_entry("INNODBKey-uuid-1", "AES", "", &[0u8; 32]);
278        let file_data = build_keyring_file(&[entry]);
279
280        let tmp = tempfile::NamedTempFile::new().unwrap();
281        std::fs::write(tmp.path(), &file_data).unwrap();
282
283        let keyring = Keyring::load(tmp.path()).unwrap();
284        assert!(keyring.find_innodb_master_key("other-uuid", 1).is_none());
285    }
286
287    #[test]
288    fn test_bad_checksum_rejected() {
289        let entry = build_keyring_entry("key", "AES", "", &[0u8; 16]);
290        let mut file_data = build_keyring_file(&[entry]);
291        // Corrupt the SHA-256 digest
292        let len = file_data.len();
293        file_data[len - 1] ^= 0xFF;
294
295        let tmp = tempfile::NamedTempFile::new().unwrap();
296        std::fs::write(tmp.path(), &file_data).unwrap();
297
298        let result = Keyring::load(tmp.path());
299        assert!(result.is_err());
300        assert!(result.unwrap_err().to_string().contains("checksum mismatch"));
301    }
302
303    #[test]
304    fn test_empty_keyring() {
305        let file_data = build_keyring_file(&[]);
306
307        let tmp = tempfile::NamedTempFile::new().unwrap();
308        std::fs::write(tmp.path(), &file_data).unwrap();
309
310        let keyring = Keyring::load(tmp.path()).unwrap();
311        assert!(keyring.is_empty());
312        assert_eq!(keyring.len(), 0);
313    }
314}