diff --git a/linux-rust/Cargo.lock b/linux-rust/Cargo.lock index 40a3922..1229475 100644 --- a/linux-rust/Cargo.lock +++ b/linux-rust/Cargo.lock @@ -24,6 +24,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -304,6 +315,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.50" @@ -365,6 +386,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -405,6 +435,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "custom_debug" version = "0.6.2" @@ -793,6 +833,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -931,6 +981,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1095,10 +1154,12 @@ name = "librepods-rust" version = "0.1.0" dependencies = [ "ab_glyph", + "aes", "bluer", "clap", "dbus", "env_logger", + "futures", "hex", "image", "imageproc", @@ -2232,6 +2293,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/linux-rust/Cargo.toml b/linux-rust/Cargo.toml index 611b261..029b65e 100644 --- a/linux-rust/Cargo.toml +++ b/linux-rust/Cargo.toml @@ -20,6 +20,8 @@ ab_glyph = "0.2.32" clap = { version = "4.5.50", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +aes = "0.8.4" +futures = "0.3.31" [profile.release] opt-level = "s" diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index 7aa320c..6141be6 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -8,12 +8,19 @@ use tokio::time::{sleep, Instant}; use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json; +use std::path::PathBuf; const PSM: u16 = 0x1001; 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 { + 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") +} + pub mod opcodes { pub const SET_FEATURE_FLAGS: u8 = 0x4D; pub const REQUEST_NOTIFICATIONS: u8 = 0x0F; @@ -253,7 +260,7 @@ struct AACPManagerState { impl AACPManagerState { fn new() -> Self { - let proximity_keys = std::fs::read_to_string("proximity_keys.json") + 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(); @@ -565,9 +572,16 @@ impl AACPManager { state.proximity_keys.insert(kt, key_data.clone()); } } - // Persist to file + let json = serde_json::to_string(&state.proximity_keys).unwrap(); - if let Err(e) = tokio::fs::write("proximity_keys.json", json).await { + let path = get_proximity_keys_path(); + 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 Err(e) = tokio::fs::write(&path, json).await { error!("Failed to save proximity keys: {}", e); } if let Some(ref tx) = state.event_tx { diff --git a/linux-rust/src/bluetooth/le.rs b/linux-rust/src/bluetooth/le.rs index e69de29..e51f52f 100644 --- a/linux-rust/src/bluetooth/le.rs +++ b/linux-rust/src/bluetooth/le.rs @@ -0,0 +1,130 @@ +use bluer::monitor::{Monitor, MonitorEvent, Pattern, RssiSamplingPeriod}; +use bluer::{Address, Session}; +use aes::Aes128; +use aes::cipher::{BlockEncrypt, KeyInit}; +use aes::cipher::generic_array::GenericArray; +use std::collections::{HashMap, HashSet}; +use log::{info, error, debug}; +use serde_json; +use crate::bluetooth::aacp::ProximityKeyType; +use futures::StreamExt; +use hex; +use std::time::Duration; +use std::path::PathBuf; + +fn get_proximity_keys_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") +} + +fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { + let mut swapped_key = *key; + swapped_key.reverse(); + let mut swapped_data = *data; + swapped_data.reverse(); + let cipher = Aes128::new(&GenericArray::from(swapped_key)); + let mut block = GenericArray::from(swapped_data); + cipher.encrypt_block(&mut block); + let mut result: [u8; 16] = block.into(); + result.reverse(); + result +} + +fn ah(k: &[u8; 16], r: &[u8; 3]) -> [u8; 3] { + let mut r_padded = [0u8; 16]; + r_padded[..3].copy_from_slice(r); + let encrypted = e(k, &r_padded); + let mut hash = [0u8; 3]; + hash.copy_from_slice(&encrypted[..3]); + hash +} + +fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { + let rpa: Vec = addr.split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect::>() + .into_iter() + .rev() + .collect(); + if rpa.len() != 6 { + return false; + } + let prand_slice = &rpa[3..6]; + let prand: [u8; 3] = prand_slice.try_into().unwrap(); + let hash_slice = &rpa[0..3]; + let hash: [u8; 3] = hash_slice.try_into().unwrap(); + let computed_hash = ah(irk, &prand); + hash == computed_hash +} + +pub async fn start_le_monitor() -> bluer::Result<()> { + let session = Session::new().await?; + let adapter = session.default_adapter().await?; + adapter.set_powered(true).await?; + + let proximity_keys: HashMap> = std::fs::read_to_string(get_proximity_keys_path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + let irk = proximity_keys.get(&ProximityKeyType::Irk) + .and_then(|v| if v.len() == 16 { Some(<[u8; 16]>::try_from(v.as_slice()).unwrap()) } else { None }); + let mut verified_macs: HashSet
= HashSet::new(); + + let pattern = Pattern { + data_type: 0xFF, // Manufacturer specific data + start_position: 0, + content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE + }; + + let mm = adapter.monitor().await?; + let mut monitor_handle = mm + .register(Monitor { + monitor_type: bluer::monitor::Type::OrPatterns, + rssi_low_threshold: None, + rssi_high_threshold: None, + rssi_low_timeout: None, + rssi_high_timeout: None, + rssi_sampling_period: Some(RssiSamplingPeriod::Period(Duration::from_millis(500))), + patterns: Some(vec![pattern]), + ..Default::default() + }) + .await?; + + while let Some(mevt) = monitor_handle.next().await { + if let MonitorEvent::DeviceFound(devid) = mevt { + let dev = adapter.device(devid.device)?; + let addr = dev.address(); + let addr_str = addr.to_string(); + + if !verified_macs.contains(&addr) { + if let Some(irk) = &irk { + if verify_rpa(&addr_str, irk) { + verified_macs.insert(addr); + info!("matched our device ({}) with the irk", addr); + } + } + } + + if verified_macs.contains(&addr) { + let mut events = dev.events().await?; + tokio::spawn(async move { + while let Some(ev) = events.next().await { + match ev { + bluer::DeviceEvent::PropertyChanged(prop) => { + match prop { + bluer::DeviceProperty::ManufacturerData(data) => { + info!("Manufacturer data from {}: {:?}", addr_str, data.iter().map(|(k, v)| (k, hex::encode(v))).collect::>()); + } + _ => {} + } + } + } + } + }); + } + } + } + + Ok(()) +} diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index db9241a..6da2a6d 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -16,6 +16,7 @@ use bluer::Address; use ksni::TrayMethods; use crate::ui::tray::MyTray; use clap::Parser; +use crate::bluetooth::le::start_le_monitor; #[derive(Parser)] struct Args { @@ -58,6 +59,14 @@ async fn main() -> bluer::Result<()> { let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; + // Start LE monitor for Apple devices + tokio::spawn(async { + info!("Starting LE monitor..."); + if let Err(e) = start_le_monitor().await { + log::error!("LE monitor error: {}", e); + } + }); + info!("Listening for new connections."); info!("Checking for connected devices..."); @@ -79,7 +88,7 @@ async fn main() -> bluer::Result<()> { if !path.contains("/org/bluez/hci") || !path.contains("/dev_") { return true; } - debug!("PropertiesChanged signal for path: {}", path); + // debug!("PropertiesChanged signal for path: {}", path); let Ok((iface, changed, _)) = msg.read3::>>, Vec>() else { return true; };