1use crate::error::AacsError;
27use crate::vuk::Vuk;
28use std::collections::BTreeMap;
29use std::path::Path;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct KeyDbEntry {
34 pub disc_id: [u8; 20],
36 pub vuk: Vuk,
38 pub label: Option<String>,
40 pub unit_keys: Vec<(u16, [u8; 16])>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct DeviceKeyRecord {
56 pub device_key: [u8; 16],
58 pub device_node: [u8; 2],
60 pub key_uv: [u8; 4],
62 pub key_u_mask_shift: u8,
64 pub comment: Option<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ProcessingKey {
75 pub processing_key: [u8; 16],
77 pub comment: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct HostCertRecord {
85 pub host_priv_key: [u8; 20],
87 pub host_cert: Vec<u8>,
92 pub comment: Option<String>,
94}
95
96impl HostCertRecord {
97 pub fn host_id(&self) -> Option<[u8; 6]> {
101 if self.host_cert.len() < 14 {
102 return None;
103 }
104 let mut out = [0u8; 6];
105 out.copy_from_slice(&self.host_cert[8..14]);
106 Some(out)
107 }
108
109 pub fn cert_type(&self) -> Option<u8> {
111 self.host_cert.first().copied()
112 }
113
114 pub fn declared_length(&self) -> Option<u16> {
117 if self.host_cert.len() < 4 {
118 return None;
119 }
120 Some(u16::from_be_bytes([self.host_cert[2], self.host_cert[3]]))
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct DriveCertRecord {
128 pub drive_priv_key: [u8; 20],
130 pub drive_cert: Vec<u8>,
132 pub comment: Option<String>,
134}
135
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
140pub struct DiscRecords {
141 pub disc_id: [u8; 20],
143 pub vid: Option<[u8; 16]>,
145 pub vuk: Option<Vuk>,
147 pub mek: Option<[u8; 16]>,
149 pub title_keys: Vec<[u8; 16]>,
151 pub kcd: Option<Vec<u8>>,
153 pub label: Option<String>,
155}
156
157#[derive(Debug, Default, Clone)]
164pub struct KeyDb {
165 by_disc_id: BTreeMap<[u8; 20], KeyDbEntry>,
166 device_keys: Vec<DeviceKeyRecord>,
167 processing_keys: Vec<ProcessingKey>,
168 host_certs: Vec<HostCertRecord>,
169 drive_certs: Vec<DriveCertRecord>,
170 disc_records: BTreeMap<[u8; 20], DiscRecords>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct SkippedLine {
182 pub line_number: usize,
184 pub snippet: String,
187 pub reason: String,
189}
190
191#[derive(Debug, Clone, Default, PartialEq, Eq)]
201pub struct ParseReport {
202 pub skipped: Vec<SkippedLine>,
204}
205
206impl ParseReport {
207 pub fn is_clean(&self) -> bool {
210 self.skipped.is_empty()
211 }
212
213 pub fn skipped_count(&self) -> usize {
215 self.skipped.len()
216 }
217}
218
219impl KeyDb {
220 pub fn parse(text: &str) -> Result<Self, AacsError> {
238 let (db, _report) = Self::parse_with_report(text)?;
239 Ok(db)
240 }
241
242 pub fn parse_with_report(text: &str) -> Result<(Self, ParseReport), AacsError> {
257 let debug = std::env::var_os("OXIDEAV_AACS_DEBUG").is_some();
258 let mut out = Self::default();
259 let mut report = ParseReport::default();
260 let mut current_discid: Option<[u8; 20]> = None;
263 for (line_idx, raw) in text.lines().enumerate() {
266 let line_number = line_idx + 1;
267 let (body, comment) = match raw.find(';') {
269 Some(i) => (&raw[..i], Some(raw[i + 1..].trim().to_string())),
270 None => (raw, None),
271 };
272 let body = body.trim();
273 if body.is_empty() {
274 continue;
275 }
276 let comment_owned = comment.filter(|s| !s.is_empty());
277 let res = if body.starts_with('|') {
279 parse_pipe_record(
280 body,
281 comment_owned.as_deref(),
282 &mut out,
283 &mut current_discid,
284 )
285 } else {
286 parse_legacy_line(body).map(|entry| {
287 out.by_disc_id.insert(entry.disc_id, entry);
288 })
289 };
290 if let Err(e) = res {
291 if debug {
292 eprintln!("oxideav-aacs: KEYDB.cfg line {line_number} skipped — {e}");
293 }
294 report.skipped.push(SkippedLine {
295 line_number,
296 snippet: truncate_excerpt(body, 80),
297 reason: e.to_string(),
298 });
299 }
300 }
301 if debug {
302 eprintln!(
303 "oxideav-aacs: KEYDB.cfg parse — kept {} per-disc + {} DK + {} PK + {} HC + {} DC + {} DISCID-scoped, skipped {} unparseable lines",
304 out.by_disc_id.len(),
305 out.device_keys.len(),
306 out.processing_keys.len(),
307 out.host_certs.len(),
308 out.drive_certs.len(),
309 out.disc_records.len(),
310 report.skipped.len()
311 );
312 }
313 Ok((out, report))
314 }
315
316 pub fn load_from(path: impl AsRef<Path>) -> Result<Self, AacsError> {
318 let text = std::fs::read_to_string(path.as_ref())?;
319 Self::parse(&text)
320 }
321
322 pub fn load_from_with_report(path: impl AsRef<Path>) -> Result<(Self, ParseReport), AacsError> {
328 let text = std::fs::read_to_string(path.as_ref())?;
329 Self::parse_with_report(&text)
330 }
331
332 pub fn load_default() -> Result<Self, AacsError> {
346 for path in default_search_paths() {
347 if path.exists() {
348 return Self::load_from(path);
349 }
350 }
351 Err(AacsError::MissingDiscFile("KEYDB.cfg"))
352 }
353
354 pub fn vuk_for_disc(&self, disc_id: &[u8; 20]) -> Option<Vuk> {
360 if let Some(e) = self.by_disc_id.get(disc_id) {
361 return Some(e.vuk);
362 }
363 self.disc_records.get(disc_id).and_then(|r| r.vuk)
364 }
365
366 pub fn entry_for_disc(&self, disc_id: &[u8; 20]) -> Option<&KeyDbEntry> {
368 self.by_disc_id.get(disc_id)
369 }
370
371 pub fn entries(&self) -> impl Iterator<Item = &KeyDbEntry> {
373 self.by_disc_id.values()
374 }
375
376 pub fn device_keys(&self) -> &[DeviceKeyRecord] {
378 &self.device_keys
379 }
380
381 pub fn processing_keys(&self) -> &[ProcessingKey] {
383 &self.processing_keys
384 }
385
386 pub fn host_certs(&self) -> &[HostCertRecord] {
388 &self.host_certs
389 }
390
391 pub fn drive_certs(&self) -> &[DriveCertRecord] {
393 &self.drive_certs
394 }
395
396 pub fn disc_records(&self) -> &BTreeMap<[u8; 20], DiscRecords> {
399 &self.disc_records
400 }
401
402 pub fn disc_record(&self, disc_id: &[u8; 20]) -> Option<&DiscRecords> {
404 self.disc_records.get(disc_id)
405 }
406
407 pub fn len(&self) -> usize {
410 self.by_disc_id.len()
411 }
412
413 pub fn is_empty(&self) -> bool {
415 self.by_disc_id.is_empty()
416 && self.device_keys.is_empty()
417 && self.processing_keys.is_empty()
418 && self.host_certs.is_empty()
419 && self.drive_certs.is_empty()
420 && self.disc_records.is_empty()
421 }
422}
423
424fn default_search_paths() -> Vec<std::path::PathBuf> {
425 use std::path::PathBuf;
426 let mut out = Vec::new();
427 if let Ok(p) = std::env::var("OXIDEAV_AACS_KEYDB") {
428 if !p.is_empty() {
429 out.push(PathBuf::from(p));
430 }
431 }
432 #[cfg(target_os = "macos")]
433 if let Ok(home) = std::env::var("HOME") {
434 if !home.is_empty() {
435 out.push(
436 PathBuf::from(&home)
437 .join("Library")
438 .join("Preferences")
439 .join("aacs")
440 .join("KEYDB.cfg"),
441 );
442 }
443 }
444 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
445 if !xdg.is_empty() {
446 out.push(PathBuf::from(xdg).join("aacs").join("KEYDB.cfg"));
447 }
448 }
449 if let Ok(dirs) = std::env::var("XDG_CONFIG_DIRS") {
450 for d in dirs.split(':') {
451 if !d.is_empty() {
452 out.push(PathBuf::from(d).join("aacs").join("KEYDB.cfg"));
453 }
454 }
455 }
456 if let Ok(home) = std::env::var("HOME") {
457 if !home.is_empty() {
458 out.push(
459 PathBuf::from(home)
460 .join(".config")
461 .join("aacs")
462 .join("KEYDB.cfg"),
463 );
464 }
465 }
466 out
467}
468
469fn parse_legacy_line(line: &str) -> Result<KeyDbEntry, AacsError> {
475 let (disc_id_text, rhs) = match line.split_once('=') {
476 Some(parts) => parts,
477 None => return Err(make_legacy_err(line)),
478 };
479 let disc_id_text = strip_hex_prefix(disc_id_text.trim());
480 let disc_id = parse_hex_array_20_legacy(disc_id_text)?;
481
482 let pipe_tokens: Vec<&str> = rhs.split('|').map(str::trim).collect();
483 let mut vuk_bytes: Option<[u8; 16]> = None;
484 let mut unit_keys: Vec<(u16, [u8; 16])> = Vec::new();
485 let mut label_parts: Vec<String> = Vec::new();
486 let mut current_flag: Option<char> = None;
487 for ptok in pipe_tokens {
488 if ptok.is_empty() {
489 continue;
490 }
491 fn is_flag_word(s: &str) -> bool {
492 s.len() == 1
493 && matches!(
494 s.as_bytes()[0],
495 b'D' | b'M' | b'I' | b'V' | b'U' | b'd' | b'm' | b'i' | b'v' | b'u'
496 )
497 }
498 let (head, value): (&str, &str) = if let Some(idx) = ptok.find(char::is_whitespace) {
499 let candidate = &ptok[..idx];
500 if is_flag_word(candidate) {
501 (candidate, ptok[idx..].trim())
502 } else {
503 ("", ptok)
504 }
505 } else if is_flag_word(ptok) {
506 (ptok, "")
507 } else {
508 ("", ptok)
509 };
510 if !head.is_empty() {
511 current_flag = head.chars().next().map(|c| c.to_ascii_uppercase());
512 }
513 if value.is_empty() {
514 continue;
515 }
516 match current_flag {
517 Some('V') => {
518 let raw = strip_hex_prefix(value);
519 if raw.len() == 32 {
520 vuk_bytes = Some(parse_hex_array_16_legacy(raw)?);
521 }
522 current_flag = None;
523 }
524 Some('U') => {
525 if let Some((id_str, key_str)) = value.split_once('-') {
526 let key_str = strip_hex_prefix(key_str.trim());
527 if let (Ok(id), Ok(key)) = (
528 id_str.trim().parse::<u16>(),
529 parse_hex_array_16_legacy(key_str),
530 ) {
531 unit_keys.push((id, key));
532 }
533 }
534 }
535 Some(_) => {
536 current_flag = None;
537 }
538 None => {
539 label_parts.push(value.to_string());
540 }
541 }
542 }
543
544 let vuk_bytes = vuk_bytes.ok_or_else(|| make_legacy_err(line))?;
545 let label = if label_parts.is_empty() {
546 None
547 } else {
548 Some(label_parts.join(" | "))
549 };
550
551 Ok(KeyDbEntry {
552 disc_id,
553 vuk: Vuk::from_bytes(vuk_bytes),
554 label,
555 unit_keys,
556 })
557}
558
559fn pipe_tokenize(body: &str) -> Vec<&str> {
567 body.split('|').map(str::trim).collect()
568}
569
570fn split_named(field: &str) -> (&str, &str) {
574 if let Some(idx) = field.find(char::is_whitespace) {
575 let head = field[..idx].trim();
576 let rest = field[idx..].trim();
577 if head.starts_with("0x") || head.starts_with("0X") {
581 ("", field.trim())
582 } else {
583 (head, rest)
584 }
585 } else {
586 ("", field.trim())
587 }
588}
589
590fn parse_hex_fixed(value: &str, expected_len: usize, field_name: &str) -> Result<Vec<u8>, String> {
594 let v = value.trim();
595 let stripped = v
596 .strip_prefix("0x")
597 .or_else(|| v.strip_prefix("0X"))
598 .ok_or_else(|| format!("{field_name}: missing 0x prefix"))?;
599 if stripped.len() != expected_len {
600 return Err(format!(
601 "{field_name}: expected {expected_len} hex chars, got {}",
602 stripped.len()
603 ));
604 }
605 parse_hex_bytes(stripped).ok_or_else(|| format!("{field_name}: non-hex character"))
606}
607
608fn parse_hex_var(value: &str, field_name: &str) -> Result<Vec<u8>, String> {
612 let v = value.trim();
613 let stripped = v
614 .strip_prefix("0x")
615 .or_else(|| v.strip_prefix("0X"))
616 .ok_or_else(|| format!("{field_name}: missing 0x prefix"))?;
617 if stripped.is_empty() || stripped.len() % 2 != 0 {
618 return Err(format!(
619 "{field_name}: hex length must be a positive even number, got {}",
620 stripped.len()
621 ));
622 }
623 parse_hex_bytes(stripped).ok_or_else(|| format!("{field_name}: non-hex character"))
624}
625
626fn parse_hex_bytes(hex: &str) -> Option<Vec<u8>> {
627 if hex.len() % 2 != 0 {
628 return None;
629 }
630 let mut out = Vec::with_capacity(hex.len() / 2);
631 let b = hex.as_bytes();
632 let mut i = 0;
633 while i < b.len() {
634 let hi = hex_digit(b[i])?;
635 let lo = hex_digit(b[i + 1])?;
636 out.push((hi << 4) | lo);
637 i += 2;
638 }
639 Some(out)
640}
641
642fn hex_digit(b: u8) -> Option<u8> {
643 match b {
644 b'0'..=b'9' => Some(b - b'0'),
645 b'a'..=b'f' => Some(b - b'a' + 10),
646 b'A'..=b'F' => Some(b - b'A' + 10),
647 _ => None,
648 }
649}
650
651fn find_named<'a>(tokens: &'a [&'a str], name: &str) -> Option<&'a str> {
657 for tok in tokens {
658 let (n, v) = split_named(tok);
659 if !n.is_empty() && n.eq_ignore_ascii_case(name) {
660 return Some(v);
661 }
662 }
663 None
664}
665
666fn positional_values<'a>(tokens: &'a [&'a str]) -> Vec<&'a str> {
669 tokens
670 .iter()
671 .filter_map(|tok| {
672 let (n, v) = split_named(tok);
673 if n.is_empty() && !v.is_empty() {
674 Some(v)
675 } else {
676 None
677 }
678 })
679 .collect()
680}
681
682fn parse_pipe_record(
686 body: &str,
687 comment: Option<&str>,
688 db: &mut KeyDb,
689 current_discid: &mut Option<[u8; 20]>,
690) -> Result<(), AacsError> {
691 let tokens_full = pipe_tokenize(body);
692 if tokens_full.first().copied() != Some("") {
698 return Err(header_err(
699 body,
700 "line must start with `|` (no leader `|` found)",
701 ));
702 }
703 let mut tokens: Vec<&str> = tokens_full
705 .iter()
706 .filter(|t| !t.is_empty())
707 .copied()
708 .collect();
709 if tokens.is_empty() {
710 return Err(header_err(body, "empty record"));
711 }
712 let leader = tokens.remove(0);
713 let fields: Vec<&str> = tokens;
715
716 match leader.to_ascii_uppercase().as_str() {
717 "DK" => parse_dk(&fields, comment, body, db),
718 "PK" => parse_pk(&fields, comment, body, db),
719 "HC" => parse_hc(&fields, comment, body, db),
720 "DC" => parse_dc(&fields, comment, body, db),
721 "DISCID" => parse_discid(&fields, comment, body, db, current_discid),
722 "VID" => parse_vid(&fields, body, db, current_discid),
723 "VUK" => parse_vuk(&fields, body, db, current_discid),
724 "MEK" => parse_mek(&fields, body, db, current_discid),
725 "TK" => parse_tk(&fields, body, db, current_discid),
726 "KCD" => parse_kcd(&fields, body, db, current_discid),
727 other => Err(header_err(
728 body,
729 &format!("unrecognised record leader `{other}`"),
730 )),
731 }
732}
733
734fn parse_dk(
735 fields: &[&str],
736 comment: Option<&str>,
737 body: &str,
738 db: &mut KeyDb,
739) -> Result<(), AacsError> {
740 let dk_hex = find_named(fields, "DEVICE_KEY")
741 .ok_or_else(|| header_err(body, "DK: missing DEVICE_KEY field"))?;
742 let node_hex = find_named(fields, "DEVICE_NODE")
743 .ok_or_else(|| header_err(body, "DK: missing DEVICE_NODE field"))?;
744 let uv_hex =
745 find_named(fields, "KEY_UV").ok_or_else(|| header_err(body, "DK: missing KEY_UV field"))?;
746 let shift_hex = find_named(fields, "KEY_U_MASK_SHIFT")
747 .ok_or_else(|| header_err(body, "DK: missing KEY_U_MASK_SHIFT field"))?;
748
749 let dk = parse_hex_fixed(dk_hex, 32, "DEVICE_KEY").map_err(|m| header_err(body, &m))?;
750 let node = parse_hex_fixed(node_hex, 4, "DEVICE_NODE").map_err(|m| header_err(body, &m))?;
751 let uv = parse_hex_fixed(uv_hex, 8, "KEY_UV").map_err(|m| header_err(body, &m))?;
752 let shift =
753 parse_hex_fixed(shift_hex, 2, "KEY_U_MASK_SHIFT").map_err(|m| header_err(body, &m))?;
754
755 let mut device_key = [0u8; 16];
756 device_key.copy_from_slice(&dk);
757 let mut device_node = [0u8; 2];
758 device_node.copy_from_slice(&node);
759 let mut key_uv = [0u8; 4];
760 key_uv.copy_from_slice(&uv);
761
762 db.device_keys.push(DeviceKeyRecord {
763 device_key,
764 device_node,
765 key_uv,
766 key_u_mask_shift: shift[0],
767 comment: comment.map(str::to_string),
768 });
769 Ok(())
770}
771
772fn parse_pk(
773 fields: &[&str],
774 comment: Option<&str>,
775 body: &str,
776 db: &mut KeyDb,
777) -> Result<(), AacsError> {
778 let positionals = positional_values(fields);
780 if positionals.len() != 1 {
781 return Err(header_err(
782 body,
783 &format!(
784 "PK: expected exactly 1 positional value, got {}",
785 positionals.len()
786 ),
787 ));
788 }
789 let pk =
790 parse_hex_fixed(positionals[0], 32, "PROCESSING_KEY").map_err(|m| header_err(body, &m))?;
791 let mut processing_key = [0u8; 16];
792 processing_key.copy_from_slice(&pk);
793 db.processing_keys.push(ProcessingKey {
794 processing_key,
795 comment: comment.map(str::to_string),
796 });
797 Ok(())
798}
799
800fn parse_hc(
801 fields: &[&str],
802 comment: Option<&str>,
803 body: &str,
804 db: &mut KeyDb,
805) -> Result<(), AacsError> {
806 let priv_hex = find_named(fields, "HOST_PRIV_KEY")
807 .ok_or_else(|| header_err(body, "HC: missing HOST_PRIV_KEY field"))?;
808 let cert_hex = find_named(fields, "HOST_CERT")
809 .ok_or_else(|| header_err(body, "HC: missing HOST_CERT field"))?;
810
811 let priv_bytes =
812 parse_hex_fixed(priv_hex, 40, "HOST_PRIV_KEY").map_err(|m| header_err(body, &m))?;
813 let cert_bytes = parse_hex_var(cert_hex, "HOST_CERT").map_err(|m| header_err(body, &m))?;
814
815 if cert_bytes.len() >= 4 {
819 let declared = u16::from_be_bytes([cert_bytes[2], cert_bytes[3]]) as usize;
820 if declared != cert_bytes.len() {
821 return Err(header_err(
822 body,
823 &format!(
824 "HC: HOST_CERT internal length field {declared} != buffer length {}",
825 cert_bytes.len()
826 ),
827 ));
828 }
829 }
830
831 let mut host_priv_key = [0u8; 20];
832 host_priv_key.copy_from_slice(&priv_bytes);
833 db.host_certs.push(HostCertRecord {
834 host_priv_key,
835 host_cert: cert_bytes,
836 comment: comment.map(str::to_string),
837 });
838 Ok(())
839}
840
841fn parse_dc(
842 fields: &[&str],
843 comment: Option<&str>,
844 body: &str,
845 db: &mut KeyDb,
846) -> Result<(), AacsError> {
847 let priv_hex = find_named(fields, "DRIVE_PRIV_KEY")
848 .ok_or_else(|| header_err(body, "DC: missing DRIVE_PRIV_KEY field"))?;
849 let cert_hex = find_named(fields, "DRIVE_CERT")
850 .ok_or_else(|| header_err(body, "DC: missing DRIVE_CERT field"))?;
851
852 let priv_bytes =
853 parse_hex_fixed(priv_hex, 40, "DRIVE_PRIV_KEY").map_err(|m| header_err(body, &m))?;
854 let cert_bytes = parse_hex_var(cert_hex, "DRIVE_CERT").map_err(|m| header_err(body, &m))?;
855
856 let mut drive_priv_key = [0u8; 20];
857 drive_priv_key.copy_from_slice(&priv_bytes);
858 db.drive_certs.push(DriveCertRecord {
859 drive_priv_key,
860 drive_cert: cert_bytes,
861 comment: comment.map(str::to_string),
862 });
863 Ok(())
864}
865
866fn parse_discid(
867 fields: &[&str],
868 comment: Option<&str>,
869 body: &str,
870 db: &mut KeyDb,
871 current_discid: &mut Option<[u8; 20]>,
872) -> Result<(), AacsError> {
873 let positionals = positional_values(fields);
875 if positionals.is_empty() {
876 return Err(header_err(body, "DISCID: missing disc-id positional value"));
877 }
878 let id_bytes =
879 parse_hex_fixed(positionals[0], 40, "DISCID").map_err(|m| header_err(body, &m))?;
880 let mut disc_id = [0u8; 20];
881 disc_id.copy_from_slice(&id_bytes);
882 *current_discid = Some(disc_id);
883
884 let label = if positionals.len() > 1 {
886 Some(positionals[1..].join(" | "))
887 } else {
888 None
889 };
890
891 let rec = db
892 .disc_records
893 .entry(disc_id)
894 .or_insert_with(|| DiscRecords {
895 disc_id,
896 ..DiscRecords::default()
897 });
898 if rec.label.is_none() {
899 rec.label = label;
900 }
901 if rec.label.is_none() {
902 rec.label = comment.map(str::to_string);
903 }
904 Ok(())
905}
906
907fn require_discid(
908 current_discid: &Option<[u8; 20]>,
909 leader: &str,
910 body: &str,
911) -> Result<[u8; 20], AacsError> {
912 current_discid.ok_or_else(|| {
913 header_err(
914 body,
915 &format!("{leader}: must be preceded by a `| DISCID |` row"),
916 )
917 })
918}
919
920fn parse_vid(
921 fields: &[&str],
922 body: &str,
923 db: &mut KeyDb,
924 current_discid: &Option<[u8; 20]>,
925) -> Result<(), AacsError> {
926 let did = require_discid(current_discid, "VID", body)?;
927 let value = positional_or_named(fields, "VID", body)?;
928 let bytes = parse_hex_fixed(value, 32, "VID").map_err(|m| header_err(body, &m))?;
929 let mut vid = [0u8; 16];
930 vid.copy_from_slice(&bytes);
931 let rec = db.disc_records.entry(did).or_insert_with(|| DiscRecords {
932 disc_id: did,
933 ..DiscRecords::default()
934 });
935 rec.vid = Some(vid);
936 Ok(())
937}
938
939fn parse_vuk(
940 fields: &[&str],
941 body: &str,
942 db: &mut KeyDb,
943 current_discid: &Option<[u8; 20]>,
944) -> Result<(), AacsError> {
945 let did = require_discid(current_discid, "VUK", body)?;
946 let value = positional_or_named(fields, "VUK", body)?;
947 let bytes = parse_hex_fixed(value, 32, "VUK").map_err(|m| header_err(body, &m))?;
948 let mut v = [0u8; 16];
949 v.copy_from_slice(&bytes);
950 let rec = db.disc_records.entry(did).or_insert_with(|| DiscRecords {
951 disc_id: did,
952 ..DiscRecords::default()
953 });
954 rec.vuk = Some(Vuk::from_bytes(v));
955 Ok(())
956}
957
958fn parse_mek(
959 fields: &[&str],
960 body: &str,
961 db: &mut KeyDb,
962 current_discid: &Option<[u8; 20]>,
963) -> Result<(), AacsError> {
964 let did = require_discid(current_discid, "MEK", body)?;
965 let value = positional_or_named(fields, "MEK", body)?;
966 let bytes = parse_hex_fixed(value, 32, "MEK").map_err(|m| header_err(body, &m))?;
967 let mut mek = [0u8; 16];
968 mek.copy_from_slice(&bytes);
969 let rec = db.disc_records.entry(did).or_insert_with(|| DiscRecords {
970 disc_id: did,
971 ..DiscRecords::default()
972 });
973 rec.mek = Some(mek);
974 Ok(())
975}
976
977fn parse_tk(
978 fields: &[&str],
979 body: &str,
980 db: &mut KeyDb,
981 current_discid: &Option<[u8; 20]>,
982) -> Result<(), AacsError> {
983 let did = require_discid(current_discid, "TK", body)?;
984 let value = positional_or_named(fields, "TK", body)?;
985 let bytes = parse_hex_fixed(value, 32, "TK").map_err(|m| header_err(body, &m))?;
986 let mut tk = [0u8; 16];
987 tk.copy_from_slice(&bytes);
988 let rec = db.disc_records.entry(did).or_insert_with(|| DiscRecords {
989 disc_id: did,
990 ..DiscRecords::default()
991 });
992 rec.title_keys.push(tk);
993 Ok(())
994}
995
996fn parse_kcd(
997 fields: &[&str],
998 body: &str,
999 db: &mut KeyDb,
1000 current_discid: &Option<[u8; 20]>,
1001) -> Result<(), AacsError> {
1002 let did = require_discid(current_discid, "KCD", body)?;
1003 let value = positional_or_named(fields, "KCD", body)?;
1004 let bytes = parse_hex_var(value, "KCD").map_err(|m| header_err(body, &m))?;
1005 let rec = db.disc_records.entry(did).or_insert_with(|| DiscRecords {
1006 disc_id: did,
1007 ..DiscRecords::default()
1008 });
1009 rec.kcd = Some(bytes);
1010 Ok(())
1011}
1012
1013fn positional_or_named<'a>(
1019 fields: &'a [&'a str],
1020 name: &str,
1021 body: &str,
1022) -> Result<&'a str, AacsError> {
1023 if let Some(v) = find_named(fields, name) {
1024 return Ok(v);
1025 }
1026 let positionals = positional_values(fields);
1027 if positionals.len() == 1 {
1028 return Ok(positionals[0]);
1029 }
1030 Err(header_err(
1031 body,
1032 &format!(
1033 "{name}: expected `{name} 0xVALUE` or `0xVALUE`, got {} positionals + no named match",
1034 positionals.len()
1035 ),
1036 ))
1037}
1038
1039fn strip_hex_prefix(s: &str) -> &str {
1042 s.strip_prefix("0x")
1043 .or_else(|| s.strip_prefix("0X"))
1044 .unwrap_or(s)
1045}
1046
1047fn parse_hex_array_20_legacy(text: &str) -> Result<[u8; 20], AacsError> {
1048 if text.len() != 40 {
1049 return Err(make_legacy_err(text));
1050 }
1051 let mut out = [0u8; 20];
1052 for (i, byte) in out.iter_mut().enumerate() {
1053 let pair = &text[i * 2..i * 2 + 2];
1054 *byte = u8::from_str_radix(pair, 16).map_err(|_| make_legacy_err(text))?;
1055 }
1056 Ok(out)
1057}
1058
1059fn parse_hex_array_16_legacy(text: &str) -> Result<[u8; 16], AacsError> {
1060 if text.len() != 32 {
1061 return Err(make_legacy_err(text));
1062 }
1063 let mut out = [0u8; 16];
1064 for (i, byte) in out.iter_mut().enumerate() {
1065 let pair = &text[i * 2..i * 2 + 2];
1066 *byte = u8::from_str_radix(pair, 16).map_err(|_| make_legacy_err(text))?;
1067 }
1068 Ok(out)
1069}
1070
1071fn make_legacy_err(snippet: &str) -> AacsError {
1072 AacsError::KeyDbParseError(truncate_excerpt(snippet, 80))
1073}
1074
1075fn truncate_excerpt(snippet: &str, max_bytes: usize) -> String {
1079 if snippet.len() <= max_bytes {
1080 return snippet.to_string();
1081 }
1082 let cut = snippet
1083 .char_indices()
1084 .take_while(|(i, c)| i + c.len_utf8() <= max_bytes)
1085 .last()
1086 .map(|(i, c)| i + c.len_utf8())
1087 .unwrap_or(0);
1088 snippet[..cut].to_string()
1089}
1090
1091fn header_err(snippet: &str, msg: &str) -> AacsError {
1092 let excerpt = truncate_excerpt(snippet, 80);
1093 AacsError::HeaderParseError(format!("{msg} (near {excerpt:?})"))
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098 use super::*;
1099
1100 #[test]
1103 fn parses_canonical_line() {
1104 let text = "0123456789ABCDEF0123456789ABCDEF01234567 = V 0102030405060708090A0B0C0D0E0F10 | Test Disc";
1105 let db = KeyDb::parse(text).unwrap();
1106 assert_eq!(db.len(), 1);
1107 let id = parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1108 let entry = db.entry_for_disc(&id).unwrap();
1109 assert_eq!(entry.label.as_deref(), Some("Test Disc"));
1110 assert_eq!(entry.vuk.as_bytes()[0], 0x01);
1111 }
1112
1113 #[test]
1114 fn parses_lowercase_hex() {
1115 let text = "abcdef0123456789abcdef0123456789abcdef01 = v fedcba9876543210fedcba9876543210";
1116 let db = KeyDb::parse(text).unwrap();
1117 assert_eq!(db.len(), 1);
1118 let id = parse_hex_array_20_legacy("ABCDEF0123456789ABCDEF0123456789ABCDEF01").unwrap();
1119 assert!(db.entry_for_disc(&id).is_some());
1120 }
1121
1122 #[test]
1123 fn ignores_blank_lines_and_comments() {
1124 let text = r#"
1125; this is a comment
1126;another comment
1127
11280123456789ABCDEF0123456789ABCDEF01234567 = V 0102030405060708090A0B0C0D0E0F10 ; trailing comment
1129"#;
1130 let db = KeyDb::parse(text).unwrap();
1131 assert_eq!(db.len(), 1);
1132 }
1133
1134 #[test]
1139 fn skips_malformed_lines_without_failing_the_load() {
1140 let text = r#"
1141; banner
114200 = V 0102030405060708090A0B0C0D0E0F10
11430123456789ABCDEF0123456789ABCDEF01234567 = X 0102030405060708090A0B0C0D0E0F10
11440123456789ABCDEF0123456789ABCDEF01234567 = V 0102
11450123456789ABCDEF0123456789ABCDEF01234567 = V CAFEBABE0102030405060708090A0B0C | OK
1146"#;
1147 let db = KeyDb::parse(text).unwrap();
1148 assert_eq!(db.len(), 1);
1149 let id = parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1150 let entry = db.entry_for_disc(&id).unwrap();
1151 assert_eq!(entry.vuk.as_bytes()[0], 0xCA);
1152 assert_eq!(entry.label.as_deref(), Some("OK"));
1153 }
1154
1155 #[test]
1159 fn parses_extended_pipe_tokenised_per_disc_form() {
1160 let text = "0x0123456789ABCDEF0123456789ABCDEF01234567 = Test Title \
1161 | D | 2017-10-12 \
1162 | M | 0x6D6284E100C23949F40559732EA541CE \
1163 | I | 0x3E91BD640F849EA14131E70B818A5182 \
1164 | V | 0xD8C278536EE614B877FCF3E4DD631091 \
1165 | U | 1-0xC8702051C53A11F873EF5851737E6B75 \
1166 ; trailing comment";
1167 let db = KeyDb::parse(text).unwrap();
1168 assert_eq!(db.len(), 1);
1169 let id = parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1170 let entry = db.entry_for_disc(&id).unwrap();
1171 assert_eq!(entry.vuk.as_bytes()[0], 0xD8);
1172 assert_eq!(entry.vuk.as_bytes()[15], 0x91);
1173 assert_eq!(entry.unit_keys.len(), 1);
1174 assert_eq!(entry.unit_keys[0].0, 1);
1175 assert_eq!(entry.unit_keys[0].1[0], 0xC8);
1176 assert_eq!(entry.unit_keys[0].1[15], 0x75);
1177 assert_eq!(entry.label.as_deref(), Some("Test Title"));
1178 }
1179
1180 #[test]
1181 fn parses_extended_with_multiple_unit_keys() {
1182 let text = "0x0123456789ABCDEF0123456789ABCDEF01234567 = X \
1183 | V | 0x0102030405060708090A0B0C0D0E0F10 \
1184 | U | 1-0x11111111111111111111111111111111 \
1185 | 2-0x22222222222222222222222222222222 \
1186 | 3-0x33333333333333333333333333333333";
1187 let db = KeyDb::parse(text).unwrap();
1188 let id = parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1189 let entry = db.entry_for_disc(&id).unwrap();
1190 assert_eq!(entry.unit_keys.len(), 3);
1191 assert_eq!(entry.unit_keys[0], (1, [0x11; 16]));
1192 assert_eq!(entry.unit_keys[1], (2, [0x22; 16]));
1193 assert_eq!(entry.unit_keys[2], (3, [0x33; 16]));
1194 }
1195
1196 #[cfg(target_os = "macos")]
1197 #[test]
1198 fn macos_library_preferences_is_in_search_path() {
1199 let saved_home = std::env::var_os("HOME");
1200 std::env::set_var("HOME", "/Users/oxideav-test");
1201 let saved_env = std::env::var_os("OXIDEAV_AACS_KEYDB");
1202 std::env::remove_var("OXIDEAV_AACS_KEYDB");
1203
1204 let paths = default_search_paths();
1205 let want =
1206 std::path::PathBuf::from("/Users/oxideav-test/Library/Preferences/aacs/KEYDB.cfg");
1207 assert!(
1208 paths.contains(&want),
1209 "macOS search path missing Library/Preferences entry: {paths:?}",
1210 );
1211
1212 match saved_home {
1213 Some(v) => std::env::set_var("HOME", v),
1214 None => std::env::remove_var("HOME"),
1215 }
1216 if let Some(v) = saved_env {
1217 std::env::set_var("OXIDEAV_AACS_KEYDB", v);
1218 }
1219 }
1220
1221 #[test]
1225 fn parses_dk_record() {
1226 let line = "| DK | DEVICE_KEY 0x000102030405060708090A0B0C0D0E0F \
1227 | DEVICE_NODE 0x0800 \
1228 | KEY_UV 0x00000400 \
1229 | KEY_U_MASK_SHIFT 0x17 \
1230 ; MKBv01-MKBv48";
1231 let db = KeyDb::parse(line).unwrap();
1232 assert_eq!(db.device_keys().len(), 1);
1233 let dk = &db.device_keys()[0];
1234 assert_eq!(
1235 dk.device_key,
1236 [
1237 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
1238 0x0E, 0x0F,
1239 ]
1240 );
1241 assert_eq!(dk.device_node, [0x08, 0x00]);
1242 assert_eq!(dk.key_uv, [0x00, 0x00, 0x04, 0x00]);
1243 assert_eq!(dk.key_u_mask_shift, 0x17);
1244 assert_eq!(dk.comment.as_deref(), Some("MKBv01-MKBv48"));
1245 }
1246
1247 #[test]
1249 fn parses_pk_record() {
1250 let line = "| PK | 0xAABBCCDDEEFF00112233445566778899 ; MKBv12";
1251 let db = KeyDb::parse(line).unwrap();
1252 assert_eq!(db.processing_keys().len(), 1);
1253 let pk = &db.processing_keys()[0];
1254 assert_eq!(pk.processing_key[0], 0xAA);
1255 assert_eq!(pk.processing_key[15], 0x99);
1256 assert_eq!(pk.comment.as_deref(), Some("MKBv12"));
1257 }
1258
1259 #[test]
1262 fn parses_hc_record() {
1263 let mut cert = vec![0x02u8, 0x03, 0x00, 0x5C];
1266 for i in 4..92u8 {
1267 cert.push(i);
1268 }
1269 assert_eq!(cert.len(), 92);
1270 let cert_hex: String = cert.iter().map(|b| format!("{b:02X}")).collect();
1271 let priv_hex = "0102030405060708090A0B0C0D0E0F1011121314";
1273 let line = format!("| HC | HOST_PRIV_KEY 0x{priv_hex} | HOST_CERT 0x{cert_hex} ; valid");
1274 let db = KeyDb::parse(&line).unwrap();
1275 assert_eq!(db.host_certs().len(), 1);
1276 let hc = &db.host_certs()[0];
1277 assert_eq!(hc.host_priv_key[0], 0x01);
1278 assert_eq!(hc.host_priv_key[19], 0x14);
1279 assert_eq!(hc.host_cert.len(), 92);
1280 assert_eq!(hc.cert_type(), Some(0x02));
1281 assert_eq!(hc.declared_length(), Some(92));
1282 assert_eq!(hc.host_id(), Some([8, 9, 10, 11, 12, 13]));
1284 assert_eq!(hc.comment.as_deref(), Some("valid"));
1285 }
1286
1287 #[test]
1289 fn parses_dc_record() {
1290 let priv_hex = "1112131415161718191A1B1C1D1E1F2021222324";
1291 let cert_hex = "DEADBEEFCAFEBABE";
1292 let line = format!("| DC | DRIVE_PRIV_KEY 0x{priv_hex} | DRIVE_CERT 0x{cert_hex}");
1293 let db = KeyDb::parse(&line).unwrap();
1294 assert_eq!(db.drive_certs().len(), 1);
1295 let dc = &db.drive_certs()[0];
1296 assert_eq!(dc.drive_priv_key[0], 0x11);
1297 assert_eq!(
1298 dc.drive_cert,
1299 vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]
1300 );
1301 }
1302
1303 #[test]
1306 fn parses_disc_scoped_records() {
1307 let text = "\
1308| DISCID | 0x0123456789ABCDEF0123456789ABCDEF01234567 \n\
1309| VID | 0xAABBCCDDEEFF00112233445566778899 \n\
1310| VUK | 0xD8C278536EE614B877FCF3E4DD631091 \n\
1311| MEK | 0x11111111111111111111111111111111 \n\
1312| TK | 0x22222222222222222222222222222222 \n\
1313| TK | 0x33333333333333333333333333333333 \n\
1314";
1315 let db = KeyDb::parse(text).unwrap();
1316 let id = parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1317 let rec = db.disc_record(&id).unwrap();
1318 assert_eq!(rec.disc_id, id);
1319 assert_eq!(rec.vid.unwrap()[0], 0xAA);
1320 assert_eq!(rec.vuk.unwrap().as_bytes()[15], 0x91);
1321 assert_eq!(rec.mek.unwrap(), [0x11; 16]);
1322 assert_eq!(rec.title_keys.len(), 2);
1323 assert_eq!(rec.title_keys[0], [0x22; 16]);
1324 assert_eq!(rec.title_keys[1], [0x33; 16]);
1325
1326 assert_eq!(db.vuk_for_disc(&id).unwrap().as_bytes()[0], 0xD8);
1328 }
1329
1330 #[test]
1332 fn parses_kcd_record() {
1333 let text = "\
1334| DISCID | 0x0123456789ABCDEF0123456789ABCDEF01234567 \n\
1335| KCD | 0xABCDEF0123 \n\
1336";
1337 let db = KeyDb::parse(text).unwrap();
1338 let id = parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1339 let rec = db.disc_record(&id).unwrap();
1340 assert_eq!(
1341 rec.kcd.as_deref(),
1342 Some(&[0xAB, 0xCD, 0xEF, 0x01, 0x23][..])
1343 );
1344 }
1345
1346 #[test]
1351 fn rejects_dk_with_bad_device_key_length() {
1352 let line = "| DK | DEVICE_KEY 0x0001 | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17";
1353 let db = KeyDb::parse(line).unwrap();
1354 assert!(db.device_keys().is_empty());
1355 }
1356
1357 #[test]
1359 fn rejects_dk_missing_required_field() {
1360 let line = "| DK | DEVICE_KEY 0x000102030405060708090A0B0C0D0E0F | DEVICE_NODE 0x0800 | KEY_U_MASK_SHIFT 0x17";
1361 let db = KeyDb::parse(line).unwrap();
1362 assert!(db.device_keys().is_empty());
1363 }
1364
1365 #[test]
1367 fn rejects_pk_with_bad_length() {
1368 let line = "| PK | 0xAABBCC";
1369 let db = KeyDb::parse(line).unwrap();
1370 assert!(db.processing_keys().is_empty());
1371 }
1372
1373 #[test]
1376 fn rejects_hc_with_mismatched_internal_length() {
1377 let line = "| HC | HOST_PRIV_KEY 0x0102030405060708090A0B0C0D0E0F1011121314 | HOST_CERT 0x0203006401020304";
1379 let db = KeyDb::parse(line).unwrap();
1380 assert!(db.host_certs().is_empty());
1381 }
1382
1383 #[test]
1385 fn rejects_vid_without_discid() {
1386 let line = "| VID | 0xAABBCCDDEEFF00112233445566778899";
1387 let db = KeyDb::parse(line).unwrap();
1388 assert!(db.disc_records().is_empty());
1389 }
1390
1391 #[test]
1393 fn rejects_unknown_leader() {
1394 let text = "| WHAT | 0x00 \n| PK | 0x00112233445566778899AABBCCDDEEFF";
1395 let db = KeyDb::parse(text).unwrap();
1396 assert_eq!(db.processing_keys().len(), 1);
1398 }
1399
1400 #[test]
1405 fn parses_mixed_keydb_file() {
1406 let text = "\
1407; AACS keydb.cfg — synthetic mixed test\n\
1408\n\
14090000000000000000000000000000000000000001 = V 0102030405060708090A0B0C0D0E0F10 | Legacy Disc A\n\
1410\n\
1411| DK | DEVICE_KEY 0x000102030405060708090A0B0C0D0E0F | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; MKBv01-MKBv48\n\
1412| DK | DEVICE_KEY 0x101112131415161718191A1B1C1D1E1F | DEVICE_NODE 0x0C00 | KEY_UV 0x00000A00 | KEY_U_MASK_SHIFT 0x0B ; MKBv49-MKBv71\n\
1413\n\
1414| PK | 0xAABBCCDDEEFF00112233445566778899 ; MKBv12\n\
1415| PK | 0xBBCCDDEEFF0011223344556677889900 ; MKBv24-MKBv48\n\
1416\n\
1417| DISCID | 0x0123456789ABCDEF0123456789ABCDEF01234567 \n\
1418| VUK | 0xD8C278536EE614B877FCF3E4DD631091 \n\
1419| TK | 0x22222222222222222222222222222222 \n\
1420";
1421 let db = KeyDb::parse(text).unwrap();
1422 let legacy_id =
1424 parse_hex_array_20_legacy("0000000000000000000000000000000000000001").unwrap();
1425 assert_eq!(
1426 db.entry_for_disc(&legacy_id).unwrap().label.as_deref(),
1427 Some("Legacy Disc A")
1428 );
1429 assert_eq!(db.len(), 1);
1430 assert_eq!(db.device_keys().len(), 2);
1432 assert_eq!(db.processing_keys().len(), 2);
1433 assert_eq!(db.device_keys()[0].key_u_mask_shift, 0x17);
1434 assert_eq!(db.device_keys()[1].key_u_mask_shift, 0x0B);
1435 let scoped_id =
1437 parse_hex_array_20_legacy("0123456789ABCDEF0123456789ABCDEF01234567").unwrap();
1438 let rec = db.disc_record(&scoped_id).unwrap();
1439 assert_eq!(rec.vuk.unwrap().as_bytes()[0], 0xD8);
1440 assert_eq!(rec.title_keys, vec![[0x22; 16]]);
1441 assert_eq!(db.vuk_for_disc(&legacy_id).unwrap().as_bytes()[0], 0x01);
1443 assert_eq!(db.vuk_for_disc(&scoped_id).unwrap().as_bytes()[0], 0xD8);
1444 }
1445
1446 #[test]
1450 fn legacy_only_file_unchanged() {
1451 let text = "\
1452; legacy-only fixture\n\
14530000000000000000000000000000000000000001 = V 0102030405060708090A0B0C0D0E0F10 | Synthetic A\n\
14540000000000000000000000000000000000000002 = V 1112131415161718191A1B1C1D1E1F20 ; trailing comment\n\
14550000000000000000000000000000000000000003 = V 2122232425262728292A2B2C2D2E2F30 | Disc with | pipes | in label\n\
1456";
1457 let db = KeyDb::parse(text).unwrap();
1458 assert_eq!(db.len(), 3);
1459 assert!(db.device_keys().is_empty());
1460 assert!(db.processing_keys().is_empty());
1461 assert!(db.host_certs().is_empty());
1462 assert!(db.drive_certs().is_empty());
1463 assert!(db.disc_records().is_empty());
1464 }
1465}