Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP syncing Bluetooth 5.1 keys #20

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 209 additions & 18 deletions asahi-btsync/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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::<String>("config").unwrap_or(&default_config),
)
.expect("Failed to sync bt device info");
Expand All @@ -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<u16>,
product_id: Option<u16>,
irk: Option<[u8; 16]>, // IdentityResolvingKey.Key
remote_ltk: Option<[u8; 16]>, // LongTermKey.Key
remote_rand: Option<u64>, // LongTermKey.Rand
peripheral_ltk: Option<[u8; 16]>, // PeripheralLongTermKey.Key, SlaveLongTermKey.Key
ediv: Option<u16>, // LongTermKey.EDiv
pairing_key: Option<[u8; 16]>,
}

struct BtInfo {
Expand All @@ -129,6 +143,96 @@ fn read_le_u16(input: &mut &[u8]) -> Result<u16> {
Ok(u16::from_le_bytes(int_bytes.try_into()?))
}

fn parse_bt_device2(input: &mut &[u8]) -> Result<BtDevice> {
let mut name: Option<String> = None;
let mut product_id: Option<u16> = None;
let mut vendor_id: Option<u16> = 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<u64> = None;
let mut peripheral_ltk: Option<[u8; 16]> = None;
let mut ediv: Option<u16> = 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<BtDevice> {
// parse MAC
let (mac_bytes, remain) = input.split_at(6_usize);
Expand Down Expand Up @@ -162,9 +266,14 @@ fn parse_bt_device(input: &mut &[u8]) -> Result<BtDevice> {
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,
})
}

Expand All @@ -189,6 +298,29 @@ fn parse_bt_info(var: &dyn Variable) -> Result<BtInfo> {
})
}

fn parse_bt_info2(var: &dyn Variable) -> Result<BtInfo> {
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<BtDevice> = 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<String> {
Ok(mac
.iter()
Expand All @@ -197,33 +329,67 @@ fn format_mac(mac: &[u8; 6]) -> Result<String> {
.join(":"))
}

fn format_key(key: &[u8; 16]) -> Result<String> {
fn format_irk(irk: &Option<[u8; 16]>) -> String {
match irk {
Some(irk) => {
irk
.iter()
.map(|x| format!("{x:02X}"))
.collect::<Vec<String>>()
.join(":")
},
None => String::from("None"),
}
}

fn format_key_reversed(key: &[u8; 16]) -> Result<String> {
Ok(key.iter().map(|x| format!("{x:02X}")).rev().collect())
}

fn format_key(key: &[u8; 16]) -> Result<String> {
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)?
);
}
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)?);

Expand All @@ -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)?);
Expand Down