use std::collections::HashMap;
#[derive(Debug)]
pub struct KeyDb {
pub device_keys: Vec<DeviceKey>,
pub processing_keys: Vec<[u8; 16]>,
pub host_certs: Vec<HostCert>,
pub disc_entries: HashMap<String, DiscEntry>,
}
#[derive(Debug, Clone)]
pub struct DeviceKey {
pub key: [u8; 16],
pub node: u16,
pub uv: u32,
pub u_mask_shift: u8,
}
#[derive(Debug, Clone)]
pub struct HostCert {
pub private_key: [u8; 20],
pub certificate: Vec<u8>,
pub private_key_v2: Option<[u8; 32]>,
pub certificate_v2: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct DiscEntry {
pub disc_hash: String,
pub title: String,
pub media_key: Option<[u8; 16]>,
pub disc_id: Option<[u8; 16]>,
pub vuk: Option<[u8; 16]>,
pub unit_keys: Vec<(u32, [u8; 16])>,
}
pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
if s.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(s.len() / 2);
for i in (0..s.len()).step_by(2) {
out.push(u8::from_str_radix(&s[i..i + 2], 16).ok()?);
}
Some(out)
}
pub(crate) fn parse_hex16(s: &str) -> Option<[u8; 16]> {
let v = parse_hex(s)?;
if v.len() != 16 {
return None;
}
let mut out = [0u8; 16];
out.copy_from_slice(&v);
Some(out)
}
pub(crate) fn parse_hex20(s: &str) -> Option<[u8; 20]> {
let v = parse_hex(s)?;
if v.len() != 20 {
return None;
}
let mut out = [0u8; 20];
out.copy_from_slice(&v);
Some(out)
}
impl KeyDb {
pub fn parse(data: &str) -> Self {
let mut db = KeyDb {
device_keys: Vec::new(),
processing_keys: Vec::new(),
host_certs: Vec::new(),
disc_entries: HashMap::new(),
};
for line in data.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
if line.starts_with("| DK") {
if let Some(dk) = Self::parse_device_key(line) {
db.device_keys.push(dk);
}
continue;
}
if line.starts_with("| PK") {
if let Some(pk) = Self::parse_processing_key(line) {
db.processing_keys.push(pk);
}
continue;
}
if line.starts_with("| HC2") {
if let Some(hc) = db.host_certs.last_mut() {
if let Some((pk, cert)) = Self::parse_host_cert_v2(line) {
hc.private_key_v2 = Some(pk);
hc.certificate_v2 = Some(cert);
}
}
continue;
}
if line.starts_with("| HC") {
if let Some(hc) = Self::parse_host_cert(line) {
db.host_certs.push(hc);
}
continue;
}
if line.starts_with("0x") && line.contains(" = ") {
if let Some(entry) = Self::parse_disc_entry(line) {
db.disc_entries.insert(entry.disc_hash.clone(), entry);
}
}
}
db
}
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
let data = std::fs::read_to_string(path)?;
Ok(Self::parse(&data))
}
pub fn find_vuk(&self, disc_hash: &str) -> Option<[u8; 16]> {
let hash = disc_hash
.trim()
.to_lowercase()
.trim_start_matches("0x")
.to_string();
self.disc_entries
.get(&format!("0x{hash}"))
.or_else(|| self.disc_entries.get(&hash))
.and_then(|e| e.vuk)
}
pub fn find_disc(&self, disc_hash: &str) -> Option<&DiscEntry> {
let hash = disc_hash
.trim()
.to_lowercase()
.trim_start_matches("0x")
.to_string();
self.disc_entries
.get(&format!("0x{hash}"))
.or_else(|| self.disc_entries.get(&hash))
}
fn parse_device_key(line: &str) -> Option<DeviceKey> {
let key_str = line.split("DEVICE_KEY").nth(1)?.split('|').next()?.trim();
let node_str = line.split("DEVICE_NODE").nth(1)?.split('|').next()?.trim();
let uv_str = line.split("KEY_UV").nth(1)?.split('|').next()?.trim();
let shift_str = line
.split("KEY_U_MASK_SHIFT")
.nth(1)?
.split(';')
.next()?
.split('|')
.next()?
.trim();
Some(DeviceKey {
key: parse_hex16(key_str)?,
node: u16::from_str_radix(node_str.trim_start_matches("0x"), 16).ok()?,
uv: u32::from_str_radix(uv_str.trim_start_matches("0x"), 16).ok()?,
u_mask_shift: u8::from_str_radix(shift_str.trim_start_matches("0x"), 16).ok()?,
})
}
fn parse_processing_key(line: &str) -> Option<[u8; 16]> {
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 3 {
let key_str = parts[2].split(';').next()?.trim();
return parse_hex16(key_str);
}
None
}
fn parse_host_cert(line: &str) -> Option<HostCert> {
let priv_str = line
.split("HOST_PRIV_KEY")
.nth(1)?
.split('|')
.next()?
.trim();
let cert_str = line
.split("HOST_CERT")
.nth(1)?
.split(';')
.next()?
.split('|')
.next()?
.trim();
Some(HostCert {
private_key: parse_hex20(priv_str)?,
certificate: parse_hex(cert_str)?,
private_key_v2: None,
certificate_v2: None,
})
}
fn parse_host_cert_v2(line: &str) -> Option<([u8; 32], Vec<u8>)> {
let priv_str = line
.split("HOST_PRIV_KEY")
.nth(1)?
.split('|')
.next()?
.trim();
let cert_str = line
.split("HOST_CERT")
.nth(1)?
.split(';')
.next()?
.split('|')
.next()?
.trim();
let priv_bytes = parse_hex(priv_str)?;
if priv_bytes.len() != 32 {
return None;
}
let mut pk = [0u8; 32];
pk.copy_from_slice(&priv_bytes);
let cert = parse_hex(cert_str)?;
if cert.len() < 132 {
return None;
}
Some((pk, cert))
}
fn parse_disc_entry(line: &str) -> Option<DiscEntry> {
let (hash_part, rest) = line.split_once(" = ")?;
let disc_hash = hash_part.trim().to_lowercase();
let title_part = rest.split(" | ").next().unwrap_or("").trim();
let title = if let Some(start) = title_part.find('(') {
if let Some(end) = title_part.rfind(')') {
title_part[start + 1..end].to_string()
} else {
title_part.to_string()
}
} else {
title_part.to_string()
};
let mut media_key = None;
let mut disc_id = None;
let mut vuk = None;
let mut unit_keys = Vec::new();
let parts: Vec<&str> = rest.split(" | ").collect();
let mut i = 0;
while i < parts.len() {
match parts[i].trim() {
"M" => {
if i + 1 < parts.len() {
media_key = parse_hex16(parts[i + 1].trim());
i += 1;
}
}
"I" => {
if i + 1 < parts.len() {
disc_id = parse_hex16(parts[i + 1].trim());
i += 1;
}
}
"V" => {
if i + 1 < parts.len() {
vuk = parse_hex16(parts[i + 1].trim());
i += 1;
}
}
"U" => {
if i + 1 < parts.len() {
let uk_str = parts[i + 1].split(';').next().unwrap_or("").trim();
for uk in uk_str.split(' ') {
let uk = uk.trim();
if let Some((num, key)) = uk.split_once('-') {
if let Ok(n) = num.parse::<u32>() {
if let Some(k) = parse_hex16(key) {
unit_keys.push((n, k));
}
}
}
}
i += 1;
}
}
_ => {}
}
i += 1;
}
Some(DiscEntry {
disc_hash,
title,
media_key,
disc_id,
vuk,
unit_keys,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn keydb_path() -> Option<std::path::PathBuf> {
let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?);
if path.exists() {
Some(path)
} else {
None
}
}
#[test]
fn test_parse_disc_entry() {
let line = r#"0x1C620AB48AEA23F3440F1189D268F3D24F61C007 = DUNE_PART_TWO (Dune: Part Two) | D | 2024-04-02 | M | 0x252FB636E883529E119AB715F4EB1640 | I | 0xA13CBE2CE40565D104B53E768C700E30 | V | 0x1114360B10EE6EAC78AA4AC0B752EAEB | U | 1-0x9E5D1310337443E811A52EBBEAE0470F ; MKBv77"#;
let entry = KeyDb::parse_disc_entry(line).unwrap();
assert_eq!(entry.title, "Dune: Part Two");
assert!(entry.media_key.is_some());
assert!(entry.vuk.is_some());
assert_eq!(entry.unit_keys.len(), 1);
assert_eq!(entry.unit_keys[0].0, 1);
}
#[test]
fn test_parse_device_key() {
let line = "| DK | DEVICE_KEY 0x5FB86EF127C19C171E799F61C27BDC2A | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; MKBv01-MKBv48";
let dk = KeyDb::parse_device_key(line).unwrap();
assert_eq!(dk.node, 0x0800);
assert_eq!(dk.u_mask_shift, 0x17);
}
#[test]
fn test_parse_host_cert() {
let line = "| HC | HOST_PRIV_KEY 0x909250D0C7FC2EE0F0383409D896993B723FA965 | HOST_CERT 0x0203005CFFFF800001C100003A5907E685E4CBA2A8CD5616665DFAA74421A14F6020D4CFC9847C23107697C39F9D109C8B2D5B93280499661AAE588AD3BF887C48DE144D48226ABC2C7ADAD0030893D1F3F1832B61B8D82D1FAFFF81 ; Revoked";
let hc = KeyDb::parse_host_cert(line).unwrap();
assert_eq!(hc.private_key[0], 0x90);
assert_eq!(hc.certificate.len(), 92);
}
#[test]
fn test_parse_full_keydb() {
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
assert_eq!(db.device_keys.len(), 4);
assert_eq!(db.processing_keys.len(), 3);
assert!(!db.host_certs.is_empty());
assert!(db.disc_entries.len() > 170000);
let dune = db
.disc_entries
.values()
.find(|e| e.title.contains("Dune: Part Two") && e.vuk.is_some())
.expect("Dune: Part Two not found");
assert!(dune.media_key.is_some());
assert!(dune.vuk.is_some());
assert!(!dune.unit_keys.is_empty());
eprintln!(
"Parsed {} disc entries, {} DK, {} PK",
db.disc_entries.len(),
db.device_keys.len(),
db.processing_keys.len()
);
}
}