1use sha2::{Digest, Sha256};
9
10use crate::IdbError;
11
12const OBFUSCATE_KEY: &[u8] = b"*305=Ljt0*!@$Hnm(*-9-w;:";
14
15#[derive(Debug, Clone)]
17pub struct KeyringEntry {
18 pub key_id: String,
20 pub key_type: String,
22 pub user_id: String,
24 pub key_data: Vec<u8>,
26}
27
28#[derive(Debug)]
30pub struct Keyring {
31 entries: Vec<KeyringEntry>,
32}
33
34impl Keyring {
35 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 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 #[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 pub fn find_key(&self, key_id: &str) -> Option<&KeyringEntry> {
84 self.entries.iter().find(|e| e.key_id == key_id)
85 }
86
87 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 pub fn len(&self) -> usize {
98 self.entries.len()
99 }
100
101 pub fn is_empty(&self) -> bool {
103 self.entries.is_empty()
104 }
105}
106
107fn 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
115fn read_le_u64(data: &[u8]) -> u64 {
117 u64::from_le_bytes(data[..8].try_into().unwrap())
118}
119
120fn 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 break;
128 }
129
130 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 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 assert_ne!(data, original);
185 deobfuscate(&mut data);
187 assert_eq!(data, original);
188 }
189
190 #[test]
191 fn test_deobfuscate_wraps_key() {
192 let mut data = vec![0u8; OBFUSCATE_KEY.len() * 2 + 5];
194 deobfuscate(&mut data);
195 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 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}