diff --git a/asahi-btsync/src/main.rs b/asahi-btsync/src/main.rs index fe4501d..6740610 100644 --- a/asahi-btsync/src/main.rs +++ b/asahi-btsync/src/main.rs @@ -56,6 +56,7 @@ fn real_main() -> Result<()> { let matches = clap::command!() .arg(clap::arg!(-d --device [DEVICE] "Path to the nvram device.")) .subcommand(clap::Command::new("list").about("Parse shared Bluetooth keys from nvram")) + .subcommand(clap::Command::new("list2").about("Parse shared Bluetooth keys from nvram")) .subcommand( clap::Command::new("sync") .about("Sync Bluetooth device information from nvram") @@ -83,13 +84,21 @@ fn real_main() -> Result<()> { .get_variable(bt_var.as_bytes(), VarType::System) .ok_or(Error::VariableNotFound)?; + let bt_var2 = "BluetoothInfo"; + let bt_devs2 = active + .get_variable(bt_var2.as_bytes(), VarType::System) + .ok_or(Error::VariableNotFound)?; + match matches.subcommand() { + Some(("list2", _args)) => { + print_btkeys2(bt_devs2).expect("Failed to parse bt device info"); + } Some(("list", _args)) => { print_btkeys(bt_devs).expect("Failed to parse bt device info"); } Some(("sync", args)) => { sync_btkeys( - bt_devs, + bt_devs, bt_devs2, args.get_one::("config").unwrap_or(&default_config), ) .expect("Failed to sync bt device info"); @@ -113,9 +122,14 @@ struct BtDevice { mac: [u8; 6], class: u16, name: String, - vendor_id: u16, - product_id: u16, - pairing_key: [u8; 16], + vendor_id: Option, + product_id: Option, + irk: Option<[u8; 16]>, // IdentityResolvingKey.Key + remote_ltk: Option<[u8; 16]>, // LongTermKey.Key + remote_rand: Option, // LongTermKey.Rand + peripheral_ltk: Option<[u8; 16]>, // PeripheralLongTermKey.Key, SlaveLongTermKey.Key + ediv: Option, // LongTermKey.EDiv + pairing_key: Option<[u8; 16]>, } struct BtInfo { @@ -129,6 +143,96 @@ fn read_le_u16(input: &mut &[u8]) -> Result { Ok(u16::from_le_bytes(int_bytes.try_into()?)) } +fn parse_bt_device2(input: &mut &[u8]) -> Result { + let mut name: Option = None; + let mut product_id: Option = None; + let mut vendor_id: Option = None; + let mut mac: Option<[u8; 6]> = None; + let mut irk: Option<[u8; 16]> = None; + //let mut local_ltk: Option<[u8; 16]> = None; + let mut remote_ltk: Option<[u8; 16]> = None; + let mut remote_rand: Option = None; + let mut peripheral_ltk: Option<[u8; 16]> = None; + let mut ediv: Option = None; + + while input.len() != 0 { + let field_type= input[0]; + let field_len = input[1] as usize; + *input = &input[2..]; + let (field_bytes, remain) = input.split_at(field_len); + *input = remain; + match field_type { + 0x02 => { // Name + name = Some(String::from_utf8_lossy(&field_bytes[..field_len]).to_string()); + } + 0x04 => { // Product + product_id = Some(u16::from_le_bytes(field_bytes.try_into().expect("unexpected Product length"))); + } + 0x05 => { // Vendor + vendor_id = Some(u16::from_le_bytes(field_bytes.try_into().expect("unexpected Vendor length"))); + } + 0x08 => { // IdentityResolvingKey.Key + irk = Some(field_bytes.try_into().expect("unexpected IRK length")); + } + 0x09 => { + // Remote long term key, with one extra byte in front (perhaps a + // type of some sort). + remote_ltk = Some(field_bytes[1..].try_into().expect("unexpected Remote LTK length")); + } + 0x0a => { // LongTermKey.EDiv + ediv = Some((field_bytes[0] as u16) | (field_bytes[1] as u16) << 8); + } + 0x0b => { // LongTermKey.Rand + remote_rand = Some(u64::from_le_bytes(field_bytes.try_into().expect("unexpected Rand length"))); + } + 0x0e => { + // Mac address, with one extra byte in the beginning. + // My guess is that this first byte indicates the address type + // (static or public), we currently assume static addresses. + mac = Some(field_bytes[1..].try_into().expect("unexpected MAC length")); + } + 0x0f => { + // Seems to be the end of the device list? + println!("---"); + break + } + 0x10 => { + // Peripheral/slave long term key, similar to the remote long + // term key. + // When the first byte is 1, the key is all zeroes. When it is + // 2, it has a value. + if field_bytes[0] == 2 { + peripheral_ltk = Some(field_bytes[1..].try_into().expect("unexpected Peripheral LTK length")); + } + } + _ => { + println!("found field: {} {} {:?}", field_type, field_len, field_bytes); + } + } + } + + // TODO + //let irk = None + //let mac: [u8; 6] = [0; 6]; + let class: u16 = 0; + //let vendor_id: u16 = 0; + //let product_id: u16 = 0; + + Ok(BtDevice { + mac: mac.unwrap_or([0; 6]), + vendor_id, + product_id, + class, + name: name.unwrap_or("".to_string()), + irk, + remote_ltk, + remote_rand, + peripheral_ltk, + pairing_key: None, + ediv, + }) +} + fn parse_bt_device(input: &mut &[u8]) -> Result { // parse MAC let (mac_bytes, remain) = input.split_at(6_usize); @@ -162,9 +266,14 @@ fn parse_bt_device(input: &mut &[u8]) -> Result { mac, class, name, - vendor_id, - product_id, - pairing_key: key, + vendor_id: Some(vendor_id), + product_id: Some(product_id), + irk: None, + pairing_key: Some(key), + remote_ltk: None, + remote_rand: None, + peripheral_ltk: None, + ediv: None, }) } @@ -189,6 +298,29 @@ fn parse_bt_info(var: &dyn Variable) -> Result { }) } +fn parse_bt_info2(var: &dyn Variable) -> Result { + let data = var.value(); + + assert!(data.len() >= 8); + //let adapter_mac: [u8; 6] = data[0..6].try_into()?; + let adapter_mac: [u8; 6] = [0; 6]; + //let num_devices = data[0]; + //assert!(data[7] == 0x04); + + let mut dev_data = &data[0..]; + + let mut devices: Vec = Vec::new(); + for _n in 0..2 { + devices.push(parse_bt_device2(&mut dev_data)?); + //break + } + + Ok(BtInfo { + mac: adapter_mac, + devices, + }) +} + fn format_mac(mac: &[u8; 6]) -> Result { Ok(mac .iter() @@ -197,18 +329,52 @@ fn format_mac(mac: &[u8; 6]) -> Result { .join(":")) } -fn format_key(key: &[u8; 16]) -> Result { +fn format_irk(irk: &Option<[u8; 16]>) -> String { + match irk { + Some(irk) => { + irk + .iter() + .map(|x| format!("{x:02X}")) + .collect::>() + .join(":") + }, + None => String::from("None"), + } +} + +fn format_key_reversed(key: &[u8; 16]) -> Result { Ok(key.iter().map(|x| format!("{x:02X}")).rev().collect()) } +fn format_key(key: &[u8; 16]) -> Result { + Ok(key.iter().map(|x| format!("{x:02X}")).collect()) +} + +fn print_btkeys2(var: &dyn Variable) -> Result<()> { + let info = parse_bt_info2(var)?; + + for dev in info.devices { + println!( + "ID {:04x}:{:04x} {} ({}) IRK={} LongTermKey.EDiv={}", + dev.vendor_id.unwrap_or(0), + dev.product_id.unwrap_or(0), + dev.name, + format_mac(&dev.mac)?, + format_irk(&dev.irk), + dev.ediv.unwrap_or(0) + ); + } + Ok(()) +} + fn print_btkeys(var: &dyn Variable) -> Result<()> { let info = parse_bt_info(var)?; for dev in info.devices { println!( "ID {:04x}:{:04x} {} ({})", - dev.vendor_id, - dev.product_id, + dev.vendor_id.unwrap_or(0), + dev.product_id.unwrap_or(0), dev.name, format_mac(&dev.mac)? ); @@ -216,14 +382,14 @@ fn print_btkeys(var: &dyn Variable) -> Result<()> { Ok(()) } -fn sync_btkeys(var: &dyn Variable, config: &String) -> Result<()> { +fn sync_btkeys(var: &dyn Variable, var2: &dyn Variable, config: &String) -> Result<()> { let config_path = Path::new(config); if !config_path.is_dir() { return Err(Error::BluezConfigDirNotFound); } - let info = parse_bt_info(var)?; + let info = parse_bt_info2(var2)?; let adapter_path = config_path.join(format_mac(&info.mac)?); @@ -246,17 +412,42 @@ fn sync_btkeys(var: &dyn Variable, config: &String) -> Result<()> { let mut info = Ini::new(); + // The ini format is documented here: + // https://github.com/bluez/bluez/blob/master/doc/settings-storage.txt + info.with_section(Some("General")) .set("Name", dev.name) - .set("Class", format!("{:#08X}", dev.class)) + .set("AddressType", "static") // TODO: read from NVRAM + .set("SupportedTechnologies", "LE;") .set("Trusted", "true") .set("Blocked", "false") .set("WakeAllowed", "true"); - info.with_section(Some("LinkKey")) - .set("Key", format_key(&dev.pairing_key)?); - info.with_section(Some("DeviceID")) - .set("Vendor", format!("{}", dev.vendor_id)) - .set("Product", format!("{}", dev.product_id)); + info.with_section(Some("IdentityResolvingKey")) + .set("Key", format_key(&dev.irk.unwrap())?); + if dev.remote_ltk.is_some() { + info.with_section(Some("LongTermKey")) + .set("Key", format_key(&dev.remote_ltk.unwrap())?) + .set("Authenticated", "1") + .set("EncSize", "16") + .set("EDiv", format!("{}", dev.ediv.unwrap())) + .set("Rand", format!("{}", dev.remote_rand.unwrap())); + } + if dev.peripheral_ltk.is_some() { + // There's also SlaveLongTermKey but it got deprecated in favor of + // PeripheralLongTermKey. See: + // https://github.com/bluez/bluez/commit/1a04dc35b3b2896b398d4352a34d5ae6db04e4f8 + info.with_section(Some("PeripheralLongTermKey")) + .set("Key", format_key(&dev.peripheral_ltk.unwrap())?) + .set("Authenticated", "2") + .set("EncSize", "16") + .set("EDiv", "0") + .set("Rand", "0"); + } + if dev.vendor_id.is_some() { + info.with_section(Some("DeviceID")) + .set("Vendor", format!("{}", dev.vendor_id.unwrap())) + .set("Product", format!("{}", dev.product_id.unwrap())); + } info.write_to_file(info_file)?; println!("{}", format_mac(&dev.mac)?);