diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index 4324aa8..a0c6fdf 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -15,10 +15,10 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const POLL_INTERVAL: Duration = Duration::from_millis(200); const HEADER_BYTES: [u8; 4] = [0x04, 0x00, 0x04, 0x00]; -fn get_proximity_keys_path() -> PathBuf { +fn get_devices_path() -> PathBuf { let data_dir = std::env::var("XDG_DATA_HOME") .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); - PathBuf::from(data_dir).join("librepods").join("proximity_keys.json") + PathBuf::from(data_dir).join("librepods").join("devices.json") } pub mod opcodes { @@ -28,7 +28,7 @@ pub mod opcodes { pub const CONTROL_COMMAND: u8 = 0x09; pub const EAR_DETECTION: u8 = 0x06; pub const CONVERSATION_AWARENESS: u8 = 0x4B; - pub const DEVICE_METADATA: u8 = 0x1D; + pub const INFORMATION: u8 = 0x1D; pub const RENAME: u8 = 0x1E; pub const PROXIMITY_KEYS_REQ: u8 = 0x30; pub const PROXIMITY_KEYS_RSP: u8 = 0x31; @@ -242,6 +242,39 @@ pub enum AACPEvent { OwnershipToFalseRequest, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DeviceType { + AirPods, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LEData { + pub irk: String, + pub enc_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AirPodsInformation { + pub name: String, + pub model_number: String, + pub manufacturer: String, + pub serial_number: String, + pub version1: String, + pub version2: String, + pub hardware_revision: String, + pub updater_identifier: String, + pub left_serial_number: String, + pub right_serial_number: String, + pub version3: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeviceData { + pub type_: DeviceType, + pub le: LEData, + pub information: Option, +} + pub struct AACPManagerState { pub sender: Option>>, pub control_command_status_list: Vec, @@ -255,16 +288,17 @@ pub struct AACPManagerState { pub old_ear_detection_status: Vec, pub ear_detection_status: Vec, event_tx: Option>, - proximity_keys: HashMap>, + pub devices: HashMap, pub airpods_mac: Option
, } impl AACPManagerState { fn new() -> Self { - let proximity_keys = std::fs::read_to_string(get_proximity_keys_path()) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); + let devices: HashMap = + std::fs::read_to_string(get_devices_path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); AACPManagerState { sender: None, control_command_status_list: Vec::new(), @@ -278,7 +312,7 @@ impl AACPManagerState { old_ear_detection_status: Vec::new(), ear_detection_status: Vec::new(), event_tx: None, - proximity_keys, + devices, airpods_mac: None, } } @@ -542,7 +576,66 @@ impl AACPManager { info!("Received Conversation Awareness packet with unexpected length: {}", packet.len()); } } - opcodes::DEVICE_METADATA => info!("Received Device Metadata packet."), + opcodes::INFORMATION => { + if payload.len() < 6 { + error!("Information packet too short: {}", hex::encode(payload)); + return; + } + let data = &payload[4..]; + let mut index = 0; + while index < data.len() && data[index] != 0x00 { + index += 1; + } + let mut strings = Vec::new(); + while index < data.len() { + while index < data.len() && data[index] == 0x00 { + index += 1; + } + if index >= data.len() { + break; + } + let start = index; + while index < data.len() && data[index] != 0x00 { + index += 1; + } + let str_bytes = &data[start..index]; + if let Ok(s) = std::str::from_utf8(str_bytes) { + strings.push(s.to_string()); + } + } + strings.remove(0); // Remove the first empty string as per comment + let info = AirPodsInformation { + name: strings.get(0).cloned().unwrap_or_default(), + model_number: strings.get(1).cloned().unwrap_or_default(), + manufacturer: strings.get(2).cloned().unwrap_or_default(), + serial_number: strings.get(3).cloned().unwrap_or_default(), + version1: strings.get(4).cloned().unwrap_or_default(), + version2: strings.get(5).cloned().unwrap_or_default(), + hardware_revision: strings.get(6).cloned().unwrap_or_default(), + updater_identifier: strings.get(7).cloned().unwrap_or_default(), + left_serial_number: strings.get(8).cloned().unwrap_or_default(), + right_serial_number: strings.get(9).cloned().unwrap_or_default(), + version3: strings.get(10).cloned().unwrap_or_default(), + }; + let mut state = self.state.lock().await; + if let Some(mac) = state.airpods_mac { + if let Some(device_data) = state.devices.get_mut(&mac.to_string()) { + device_data.information = Some(info.clone()); + } + } + let json = serde_json::to_string(&state.devices).unwrap(); + if let Some(parent) = get_devices_path().parent() { + if let Err(e) = tokio::fs::create_dir_all(&parent).await { + error!("Failed to create directory for devices: {}", e); + return; + } + } + if let Err(e) = tokio::fs::write(&get_devices_path(), json).await { + error!("Failed to save devices: {}", e); + } + info!("Received Information: {:?}", info); + }, + opcodes::PROXIMITY_KEYS_RSP => { if payload.len() < 4 { error!("Proximity Keys Response packet too short: {}", hex::encode(payload)); @@ -572,34 +665,29 @@ impl AACPManager { let mut state = self.state.lock().await; for (key_type, key_data) in &keys { if let Some(kt) = ProximityKeyType::from_u8(*key_type) { - state.proximity_keys.insert(kt, key_data.clone()); - } - } - - if let Some(mac) = state.airpods_mac { - let path = get_proximity_keys_path(); - let mut all_keys: HashMap>> = - std::fs::read_to_string(&path) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - - all_keys.insert(mac.to_string(), state.proximity_keys.clone()); - - let json = serde_json::to_string(&all_keys).unwrap(); - if let Some(parent) = path.parent() { - if let Err(e) = tokio::fs::create_dir_all(&parent).await { - error!("Failed to create directory for proximity keys: {}", e); - return; + if let Some(mac) = state.airpods_mac { + let mac_str = mac.to_string(); + let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData { + type_: DeviceType::AirPods, + le: LEData { irk: "".to_string(), enc_key: "".to_string() }, + information: None, + }); + match kt { + ProximityKeyType::Irk => device_data.le.irk = hex::encode(key_data), + ProximityKeyType::EncKey => device_data.le.enc_key = hex::encode(key_data), + } } } - if let Err(e) = tokio::fs::write(&path, json).await { - error!("Failed to save proximity keys: {}", e); + } + let json = serde_json::to_string(&state.devices).unwrap(); + if let Some(parent) = get_devices_path().parent() { + if let Err(e) = tokio::fs::create_dir_all(&parent).await { + error!("Failed to create directory for devices: {}", e); + return; } } - - if let Some(ref tx) = state.event_tx { - let _ = tx.send(AACPEvent::ProximityKeys(keys)); + if let Err(e) = tokio::fs::write(&get_devices_path(), json).await { + error!("Failed to save devices: {}", e); } }, opcodes::STEM_PRESS => info!("Received Stem Press packet."), diff --git a/linux-rust/src/bluetooth/le.rs b/linux-rust/src/bluetooth/le.rs index 1674ac8..da6f40e 100644 --- a/linux-rust/src/bluetooth/le.rs +++ b/linux-rust/src/bluetooth/le.rs @@ -1,3 +1,4 @@ +use std::cmp::PartialEq; use bluer::monitor::{Monitor, MonitorEvent, Pattern}; use bluer::{Address, Session}; use aes::Aes128; @@ -15,11 +16,12 @@ use std::sync::Arc; use tokio::sync::Mutex; use crate::bluetooth::aacp::BatteryStatus; use crate::ui::tray::MyTray; +use crate::bluetooth::aacp::{DeviceData, LEData, DeviceType}; -fn get_proximity_keys_path() -> PathBuf { +fn get_devices_path() -> PathBuf { let data_dir = std::env::var("XDG_DATA_HOME") .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); - PathBuf::from(data_dir).join("librepods").join("proximity_keys.json") + PathBuf::from(data_dir).join("librepods").join("devices.json") } fn get_preferences_path() -> PathBuf { @@ -76,13 +78,21 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { hash == computed_hash } +impl PartialEq for DeviceType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (DeviceType::AirPods, DeviceType::AirPods) => true + } + } +} + pub async fn start_le_monitor(tray_handle: Option>) -> bluer::Result<()> { let session = Session::new().await?; let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; - let all_proximity_keys: HashMap>> = - std::fs::read_to_string(get_proximity_keys_path()) + let all_devices: HashMap = + std::fs::read_to_string(get_devices_path()) .ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default(); @@ -130,16 +140,18 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue } else { debug!("Checking RPA for device: {}", addr_str); let mut found_mac = None; - for (airpods_mac, keys) in &all_proximity_keys { - if let Some(irk_vec) = keys.get(&ProximityKeyType::Irk) { - if irk_vec.len() == 16 { - let irk: [u8; 16] = irk_vec.as_slice().try_into().unwrap(); - debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, hex::encode(irk)); - if verify_rpa(&addr_str, &irk) { - info!("Matched our device ({}) with the irk for {}", addr, airpods_mac); - verified_macs.insert(addr, airpods_mac.clone()); - found_mac = Some(airpods_mac.clone()); - break; + for (airpods_mac, device_data) in &all_devices { + if device_data.type_ == DeviceType::AirPods { + if let Ok(irk_bytes) = hex::decode(&device_data.le.irk) { + if irk_bytes.len() == 16 { + let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap(); + debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, device_data.le.irk); + if verify_rpa(&addr_str, &irk) { + info!("Matched our device ({}) with the irk for {}", addr, airpods_mac); + verified_macs.insert(addr, airpods_mac.clone()); + found_mac = Some(airpods_mac.clone()); + break; + } } } } @@ -155,10 +167,12 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue } if let Some(ref mac) = matched_airpods_mac { - if let Some(keys) = all_proximity_keys.get(mac) { - if let Some(enc_key_vec) = keys.get(&ProximityKeyType::EncKey) { - if enc_key_vec.len() == 16 { - matched_enc_key = Some(enc_key_vec.as_slice().try_into().unwrap()); + if let Some(device_data) = all_devices.get(mac) { + if !device_data.le.enc_key.is_empty() { + if let Ok(enc_key_bytes) = hex::decode(&device_data.le.enc_key) { + if enc_key_bytes.len() == 16 { + matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap()); + } } } }