1use std::path::Path;
9
10use sha2::{Digest, Sha256};
11
12use crate::IdbError;
13
14const OBFUSCATE_KEY: &[u8] = b"*305=Ljt0*!@$Hnm(*-9-w;:";
16
17#[derive(Debug, Clone)]
19pub struct KeyringEntry {
20 pub key_id: String,
22 pub key_type: String,
24 pub user_id: String,
26 pub key_data: Vec<u8>,
28}
29
30#[derive(Debug)]
32pub struct Keyring {
33 entries: Vec<KeyringEntry>,
34}
35
36impl Keyring {
37 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 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 let entries = parse_entries(content)?;
69
70 Ok(Keyring { entries })
71 }
72
73 pub fn find_key(&self, key_id: &str) -> Option<&KeyringEntry> {
75 self.entries.iter().find(|e| e.key_id == key_id)
76 }
77
78 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 pub fn len(&self) -> usize {
89 self.entries.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.entries.is_empty()
95 }
96}
97
98fn 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
106fn read_le_u64(data: &[u8]) -> u64 {
108 u64::from_le_bytes(data[..8].try_into().unwrap())
109}
110
111fn 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 break;
119 }
120
121 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 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 assert_ne!(data, original);
176 deobfuscate(&mut data);
178 assert_eq!(data, original);
179 }
180
181 #[test]
182 fn test_deobfuscate_wraps_key() {
183 let mut data = vec![0u8; OBFUSCATE_KEY.len() * 2 + 5];
185 deobfuscate(&mut data);
186 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 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}