1use std::collections::HashMap;
4
5#[derive(Debug)]
7pub struct KeyDb {
8 pub device_keys: Vec<DeviceKey>,
10 pub processing_keys: Vec<[u8; 16]>,
12 pub host_certs: Vec<HostCert>,
14 pub disc_entries: HashMap<String, DiscEntry>,
16}
17
18#[derive(Debug, Clone)]
20pub struct DeviceKey {
21 pub key: [u8; 16],
22 pub node: u16,
23 pub uv: u32,
24 pub u_mask_shift: u8,
25}
26
27#[derive(Debug, Clone)]
29pub struct HostCert {
30 pub private_key: [u8; 20],
32 pub certificate: Vec<u8>,
34 pub private_key_v2: Option<[u8; 32]>,
36 pub certificate_v2: Option<Vec<u8>>,
38}
39
40#[derive(Debug, Clone)]
42pub struct DiscEntry {
43 pub disc_hash: String,
45 pub title: String,
47 pub media_key: Option<[u8; 16]>,
49 pub disc_id: Option<[u8; 16]>,
51 pub vuk: Option<[u8; 16]>,
53 pub unit_keys: Vec<(u32, [u8; 16])>,
55}
56
57pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
59 let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
60 if s.len() % 2 != 0 {
61 return None;
62 }
63 let mut out = Vec::with_capacity(s.len() / 2);
64 for i in (0..s.len()).step_by(2) {
65 out.push(u8::from_str_radix(&s[i..i + 2], 16).ok()?);
66 }
67 Some(out)
68}
69
70pub(crate) fn parse_hex16(s: &str) -> Option<[u8; 16]> {
72 let v = parse_hex(s)?;
73 if v.len() != 16 {
74 return None;
75 }
76 let mut out = [0u8; 16];
77 out.copy_from_slice(&v);
78 Some(out)
79}
80
81pub(crate) fn parse_hex20(s: &str) -> Option<[u8; 20]> {
82 let v = parse_hex(s)?;
83 if v.len() != 20 {
84 return None;
85 }
86 let mut out = [0u8; 20];
87 out.copy_from_slice(&v);
88 Some(out)
89}
90
91impl KeyDb {
92 pub fn parse(data: &str) -> Self {
94 let mut db = KeyDb {
95 device_keys: Vec::new(),
96 processing_keys: Vec::new(),
97 host_certs: Vec::new(),
98 disc_entries: HashMap::new(),
99 };
100
101 for line in data.lines() {
102 let line = line.trim();
103
104 if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
106 continue;
107 }
108
109 if line.starts_with("| DK") {
111 if let Some(dk) = Self::parse_device_key(line) {
112 db.device_keys.push(dk);
113 }
114 continue;
115 }
116
117 if line.starts_with("| PK") {
119 if let Some(pk) = Self::parse_processing_key(line) {
120 db.processing_keys.push(pk);
121 }
122 continue;
123 }
124
125 if line.starts_with("| HC2") {
127 if let Some(hc) = db.host_certs.last_mut() {
128 if let Some((pk, cert)) = Self::parse_host_cert_v2(line) {
129 hc.private_key_v2 = Some(pk);
130 hc.certificate_v2 = Some(cert);
131 }
132 }
133 continue;
134 }
135
136 if line.starts_with("| HC") {
138 if let Some(hc) = Self::parse_host_cert(line) {
139 db.host_certs.push(hc);
140 }
141 continue;
142 }
143
144 if line.starts_with("0x") && line.contains(" = ") {
146 if let Some(entry) = Self::parse_disc_entry(line) {
147 db.disc_entries.insert(entry.disc_hash.clone(), entry);
148 }
149 }
150 }
151
152 db
153 }
154
155 pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
157 let data = std::fs::read_to_string(path)?;
158 Ok(Self::parse(&data))
159 }
160
161 pub fn find_vuk(&self, disc_hash: &str) -> Option<[u8; 16]> {
163 let hash = disc_hash
164 .trim()
165 .to_lowercase()
166 .trim_start_matches("0x")
167 .to_string();
168 self.disc_entries
170 .get(&format!("0x{hash}"))
171 .or_else(|| self.disc_entries.get(&hash))
172 .and_then(|e| e.vuk)
173 }
174
175 pub fn find_disc(&self, disc_hash: &str) -> Option<&DiscEntry> {
177 let hash = disc_hash
178 .trim()
179 .to_lowercase()
180 .trim_start_matches("0x")
181 .to_string();
182 self.disc_entries
183 .get(&format!("0x{hash}"))
184 .or_else(|| self.disc_entries.get(&hash))
185 }
186
187 fn parse_device_key(line: &str) -> Option<DeviceKey> {
190 let key_str = line.split("DEVICE_KEY").nth(1)?.split('|').next()?.trim();
192 let node_str = line.split("DEVICE_NODE").nth(1)?.split('|').next()?.trim();
193 let uv_str = line.split("KEY_UV").nth(1)?.split('|').next()?.trim();
194 let shift_str = line
195 .split("KEY_U_MASK_SHIFT")
196 .nth(1)?
197 .split(';')
198 .next()?
199 .split('|')
200 .next()?
201 .trim();
202
203 Some(DeviceKey {
204 key: parse_hex16(key_str)?,
205 node: u16::from_str_radix(node_str.trim_start_matches("0x"), 16).ok()?,
206 uv: u32::from_str_radix(uv_str.trim_start_matches("0x"), 16).ok()?,
207 u_mask_shift: u8::from_str_radix(shift_str.trim_start_matches("0x"), 16).ok()?,
208 })
209 }
210
211 fn parse_processing_key(line: &str) -> Option<[u8; 16]> {
212 let parts: Vec<&str> = line.split('|').collect();
214 if parts.len() >= 3 {
215 let key_str = parts[2].split(';').next()?.trim();
216 return parse_hex16(key_str);
217 }
218 None
219 }
220
221 fn parse_host_cert(line: &str) -> Option<HostCert> {
222 let priv_str = line
224 .split("HOST_PRIV_KEY")
225 .nth(1)?
226 .split('|')
227 .next()?
228 .trim();
229 let cert_str = line
230 .split("HOST_CERT")
231 .nth(1)?
232 .split(';')
233 .next()?
234 .split('|')
235 .next()?
236 .trim();
237
238 Some(HostCert {
239 private_key: parse_hex20(priv_str)?,
240 certificate: parse_hex(cert_str)?,
241 private_key_v2: None,
242 certificate_v2: None,
243 })
244 }
245
246 fn parse_host_cert_v2(line: &str) -> Option<([u8; 32], Vec<u8>)> {
248 let priv_str = line
249 .split("HOST_PRIV_KEY")
250 .nth(1)?
251 .split('|')
252 .next()?
253 .trim();
254 let cert_str = line
255 .split("HOST_CERT")
256 .nth(1)?
257 .split(';')
258 .next()?
259 .split('|')
260 .next()?
261 .trim();
262
263 let priv_bytes = parse_hex(priv_str)?;
264 if priv_bytes.len() != 32 {
265 return None;
266 }
267 let mut pk = [0u8; 32];
268 pk.copy_from_slice(&priv_bytes);
269
270 let cert = parse_hex(cert_str)?;
271 if cert.len() < 132 {
272 return None;
273 }
274
275 Some((pk, cert))
276 }
277
278 fn parse_disc_entry(line: &str) -> Option<DiscEntry> {
279 let (hash_part, rest) = line.split_once(" = ")?;
281 let disc_hash = hash_part.trim().to_lowercase();
282
283 let title_part = rest.split(" | ").next().unwrap_or("").trim();
285 let title = if let Some(start) = title_part.find('(') {
287 if let Some(end) = title_part.rfind(')') {
288 title_part[start + 1..end].to_string()
289 } else {
290 title_part.to_string()
291 }
292 } else {
293 title_part.to_string()
294 };
295
296 let mut media_key = None;
298 let mut disc_id = None;
299 let mut vuk = None;
300 let mut unit_keys = Vec::new();
301
302 let parts: Vec<&str> = rest.split(" | ").collect();
303 let mut i = 0;
304 while i < parts.len() {
305 match parts[i].trim() {
306 "M" => {
307 if i + 1 < parts.len() {
308 media_key = parse_hex16(parts[i + 1].trim());
309 i += 1;
310 }
311 }
312 "I" => {
313 if i + 1 < parts.len() {
314 disc_id = parse_hex16(parts[i + 1].trim());
315 i += 1;
316 }
317 }
318 "V" => {
319 if i + 1 < parts.len() {
320 vuk = parse_hex16(parts[i + 1].trim());
321 i += 1;
322 }
323 }
324 "U" => {
325 if i + 1 < parts.len() {
326 let uk_str = parts[i + 1].split(';').next().unwrap_or("").trim();
328 for uk in uk_str.split(' ') {
329 let uk = uk.trim();
330 if let Some((num, key)) = uk.split_once('-') {
331 if let Ok(n) = num.parse::<u32>() {
332 if let Some(k) = parse_hex16(key) {
333 unit_keys.push((n, k));
334 }
335 }
336 }
337 }
338 i += 1;
339 }
340 }
341 _ => {}
342 }
343 i += 1;
344 }
345
346 Some(DiscEntry {
347 disc_hash,
348 title,
349 media_key,
350 disc_id,
351 vuk,
352 unit_keys,
353 })
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn keydb_path() -> Option<std::path::PathBuf> {
363 let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?);
364 if path.exists() { Some(path) } else { None }
365 }
366
367 #[test]
368 fn test_parse_disc_entry() {
369 let line = r#"0x1C620AB48AEA23F3440F1189D268F3D24F61C007 = DUNE_PART_TWO (Dune: Part Two) | D | 2024-04-02 | M | 0x252FB636E883529E119AB715F4EB1640 | I | 0xA13CBE2CE40565D104B53E768C700E30 | V | 0x1114360B10EE6EAC78AA4AC0B752EAEB | U | 1-0x9E5D1310337443E811A52EBBEAE0470F ; MKBv77"#;
370 let entry = KeyDb::parse_disc_entry(line).unwrap();
371 assert_eq!(entry.title, "Dune: Part Two");
372 assert!(entry.media_key.is_some());
373 assert!(entry.vuk.is_some());
374 assert_eq!(entry.unit_keys.len(), 1);
375 assert_eq!(entry.unit_keys[0].0, 1);
376 }
377
378 #[test]
379 fn test_parse_device_key() {
380 let line = "| DK | DEVICE_KEY 0x5FB86EF127C19C171E799F61C27BDC2A | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; MKBv01-MKBv48";
381 let dk = KeyDb::parse_device_key(line).unwrap();
382 assert_eq!(dk.node, 0x0800);
383 assert_eq!(dk.u_mask_shift, 0x17);
384 }
385
386 #[test]
387 fn test_parse_host_cert() {
388 let line = "| HC | HOST_PRIV_KEY 0x909250D0C7FC2EE0F0383409D896993B723FA965 | HOST_CERT 0x0203005CFFFF800001C100003A5907E685E4CBA2A8CD5616665DFAA74421A14F6020D4CFC9847C23107697C39F9D109C8B2D5B93280499661AAE588AD3BF887C48DE144D48226ABC2C7ADAD0030893D1F3F1832B61B8D82D1FAFFF81 ; Revoked";
389 let hc = KeyDb::parse_host_cert(line).unwrap();
390 assert_eq!(hc.private_key[0], 0x90);
391 assert_eq!(hc.certificate.len(), 92);
392 }
393
394 #[test]
395 fn test_parse_full_keydb() {
396 let path = match keydb_path() {
397 Some(p) => p,
398 None => return,
399 }; let db = KeyDb::load(&path).unwrap();
402
403 assert_eq!(db.device_keys.len(), 4);
404 assert_eq!(db.processing_keys.len(), 3);
405 assert!(!db.host_certs.is_empty());
406 assert!(db.disc_entries.len() > 170000);
407
408 let dune = db
410 .disc_entries
411 .values()
412 .find(|e| e.title.contains("Dune: Part Two") && e.vuk.is_some())
413 .expect("Dune: Part Two not found");
414 assert!(dune.media_key.is_some());
415 assert!(dune.vuk.is_some());
416 assert!(!dune.unit_keys.is_empty());
417
418 eprintln!(
419 "Parsed {} disc entries, {} DK, {} PK",
420 db.disc_entries.len(),
421 db.device_keys.len(),
422 db.processing_keys.len()
423 );
424 }
425}