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