linux-rust: parse and store device info

This commit is contained in:
Kavish Devar
2025-10-28 17:42:59 +05:30
parent c5a824c384
commit fa8bc11060
2 changed files with 154 additions and 52 deletions

View File

@@ -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<AirPodsInformation>,
}
pub struct AACPManagerState {
pub sender: Option<mpsc::Sender<Vec<u8>>>,
pub control_command_status_list: Vec<ControlCommandStatus>,
@@ -255,16 +288,17 @@ pub struct AACPManagerState {
pub old_ear_detection_status: Vec<EarDetectionStatus>,
pub ear_detection_status: Vec<EarDetectionStatus>,
event_tx: Option<mpsc::UnboundedSender<AACPEvent>>,
proximity_keys: HashMap<ProximityKeyType, Vec<u8>>,
pub devices: HashMap<String, DeviceData>,
pub airpods_mac: Option<Address>,
}
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<String, DeviceData> =
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<String, HashMap<ProximityKeyType, Vec<u8>>> =
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."),

View File

@@ -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<ksni::Handle<MyTray>>) -> bluer::Result<()> {
let session = Session::new().await?;
let adapter = session.default_adapter().await?;
adapter.set_powered(true).await?;
let all_proximity_keys: HashMap<String, HashMap<ProximityKeyType, Vec<u8>>> =
std::fs::read_to_string(get_proximity_keys_path())
let all_devices: HashMap<String, DeviceData> =
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<ksni::Handle<MyTray>>) -> 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<ksni::Handle<MyTray>>) -> 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());
}
}
}
}