use crate::broadcast_state::BleBroadcastState;
use crate::db;
use crate::error::{BleError, Result};
use crate::gatt::{GattConnection, GattService};
use crate::pairing;
use crate::pdu::{self, OpCode};
use crate::session::BleSession;
use hap_crypto::{AccessoryPairing, ControllerKeypair};
use hap_model::format::{CharFormat, CharValue};
use hap_model::tree::Accessory;
use hap_model::{CharacteristicType, ServiceType};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_stream::StreamExt as _;
fn gsn_is_newer(new: u16, last: u16) -> bool {
let diff = new.wrapping_sub(last);
diff != 0 && diff < 0x8000
}
const MAX_REVIVE_RETRIES: u32 = 3;
const ENABLE_BROADCAST_BODY: [u8; 7] = [0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01];
#[derive(Debug, Clone, PartialEq)]
pub struct CharacteristicEvent {
pub aid: u64,
pub iid: u64,
pub value: CharValue,
}
struct Secure {
session: BleSession,
tid: u8,
generation: u64,
}
struct Reviver {
keypair: ControllerKeypair,
pairing: AccessoryPairing,
verify_char: String,
verify_iid: u16,
frag_size: usize,
}
pub(crate) struct SecureContext {
pub session: BleSession,
pub session_generation: u64,
pub keypair: ControllerKeypair,
pub pairing: AccessoryPairing,
pub verify_char: String,
pub verify_iid: u16,
pub pairings_char: String,
pub pairings_iid: u16,
pub broadcast_key: hap_crypto::BroadcastKey,
pub initial_gsn: u16,
}
async fn revive_if_stale(
gatt: &dyn GattConnection,
s: &mut Secure,
reviver: &Reviver,
) -> Result<()> {
if gatt.generation().await <= s.generation {
return Ok(());
}
let (session, _bkey) = pairing::pair_verify(
gatt,
&reviver.verify_char,
reviver.verify_iid,
&reviver.keypair,
&reviver.pairing,
reviver.frag_size,
)
.await?;
s.session = session;
s.tid = 0;
s.generation = gatt.generation().await;
Ok(())
}
mod pairings_tlv {
pub(super) const STATE: u8 = 0x06;
pub(super) const METHOD: u8 = 0x00;
pub(super) const IDENTIFIER: u8 = 0x01;
pub(super) const ERROR: u8 = 0x07;
pub(super) const STATE_M1: u8 = 0x01;
pub(super) const STATE_M2: u8 = 0x02;
pub(super) const METHOD_REMOVE: u8 = 0x04;
}
fn encode_remove_pairing(controller_id: &str) -> Vec<u8> {
let mut out = Vec::new();
let mut w = hap_tlv8::Tlv8Writer::new(&mut out);
w.push_u8(pairings_tlv::STATE, pairings_tlv::STATE_M1);
w.push_u8(pairings_tlv::METHOD, pairings_tlv::METHOD_REMOVE);
w.push(pairings_tlv::IDENTIFIER, controller_id.as_bytes());
out
}
fn expect_remove_m2(tlv: &[u8]) -> Result<()> {
let map = hap_tlv8::Tlv8Map::parse(tlv)?;
if let Some(err) = map.get(pairings_tlv::ERROR) {
return Err(BleError::PairingRejected(err.first().copied().unwrap_or(1)));
}
match map
.get(pairings_tlv::STATE)
.and_then(|s| s.first().copied())
{
Some(pairings_tlv::STATE_M2) => Ok(()),
_ => Err(BleError::MalformedPdu("remove-pairing reply not state M2")),
}
}
async fn dedup_should_emit(emitted: &Mutex<HashMap<u64, u16>>, iid: u64, gsn: u16) -> bool {
let mut e = emitted.lock().await;
let prev = e.get(&iid).copied();
if prev == Some(gsn) {
return false;
}
if prev.is_none_or(|p| gsn_is_newer(gsn, p)) {
e.insert(iid, gsn);
}
true
}
async fn read_char_raw(
gatt: &dyn GattConnection,
secure: &Mutex<Secure>,
reviver: &Reviver,
uuid: &str,
iid: u64,
frag_size: usize,
) -> Result<Vec<u8>> {
let iid16 = u16::try_from(iid).map_err(|_| BleError::CharacteristicNotFound { aid: 0, iid })?;
let mut s = secure.lock().await;
let mut attempts = 0;
loop {
revive_if_stale(gatt, &mut s, reviver).await?;
s.tid = s.tid.wrapping_add(1);
let tid = s.tid;
match pdu::request_secure(
gatt,
&mut s.session,
uuid,
OpCode::CharacteristicRead,
tid,
iid16,
&[],
frag_size,
)
.await
{
Ok(resp) => return pdu::value_param(&resp.body),
Err(e) => {
attempts += 1;
if attempts < MAX_REVIVE_RETRIES && gatt.generation().await > s.generation {
continue;
}
return Err(e);
}
}
}
}
async fn write_char_raw(
gatt: &dyn GattConnection,
secure: &Mutex<Secure>,
reviver: &Reviver,
uuid: &str,
iid: u64,
value_bytes: &[u8],
frag_size: usize,
) -> Result<()> {
let iid16 = u16::try_from(iid).map_err(|_| BleError::CharacteristicNotFound { aid: 0, iid })?;
let body = pdu::encode_write_body(value_bytes);
let mut s = secure.lock().await;
let mut attempts = 0;
loop {
revive_if_stale(gatt, &mut s, reviver).await?;
s.tid = s.tid.wrapping_add(1);
let tid = s.tid;
match pdu::request_secure(
gatt,
&mut s.session,
uuid,
OpCode::CharacteristicWrite,
tid,
iid16,
&body,
frag_size,
)
.await
{
Ok(resp) if resp.status != 0 => return Err(BleError::RequestRejected(resp.status)),
Ok(_) => return Ok(()),
Err(e) => {
attempts += 1;
if attempts < MAX_REVIVE_RETRIES && gatt.generation().await > s.generation {
continue;
}
return Err(e);
}
}
}
}
pub struct BleAccessory {
gatt: Arc<dyn GattConnection>,
secure: Arc<Mutex<Secure>>,
reviver: Arc<Reviver>,
pairings: (String, u16),
frag_size: usize,
accessories: Vec<Accessory>,
chars: HashMap<(u64, u64), (String, CharFormat)>,
events_tx: tokio::sync::broadcast::Sender<CharacteristicEvent>,
tasks: Vec<tokio::task::JoinHandle<()>>,
last_gsn: Arc<Mutex<u16>>,
emitted: Arc<Mutex<HashMap<u64, u16>>>,
broadcast_key: hap_crypto::BroadcastKey,
}
impl Drop for BleAccessory {
fn drop(&mut self) {
for task in &self.tasks {
task.abort();
}
}
}
impl BleAccessory {
pub(crate) fn new(
gatt: Arc<dyn GattConnection>,
ctx: SecureContext,
frag_size: usize,
gatt_services: &[GattService],
accessories: Vec<Accessory>,
) -> Self {
let (events_tx, _) = tokio::sync::broadcast::channel(64);
let mut uuid_by_iid: HashMap<u64, String> = HashMap::new();
for gs in gatt_services {
for gc in &gs.characteristics {
uuid_by_iid.insert(u64::from(gc.iid), gc.uuid.clone());
}
}
let mut chars = HashMap::new();
for acc in &accessories {
for svc in &acc.services {
for ch in &svc.characteristics {
if let Some(uuid) = uuid_by_iid.get(&ch.iid) {
chars.insert((acc.aid, ch.iid), (uuid.clone(), ch.format));
}
}
}
}
Self {
gatt,
secure: Arc::new(Mutex::new(Secure {
session: ctx.session,
tid: 0,
generation: ctx.session_generation,
})),
reviver: Arc::new(Reviver {
keypair: ctx.keypair,
pairing: ctx.pairing,
verify_char: ctx.verify_char,
verify_iid: ctx.verify_iid,
frag_size,
}),
pairings: (ctx.pairings_char, ctx.pairings_iid),
frag_size,
accessories,
chars,
events_tx,
tasks: Vec::new(),
last_gsn: Arc::new(Mutex::new(ctx.initial_gsn)),
emitted: Arc::new(Mutex::new(HashMap::new())),
broadcast_key: ctx.broadcast_key,
}
}
pub fn accessories(&self) -> &[Accessory] {
&self.accessories
}
pub async fn broadcast_state(&self) -> BleBroadcastState {
BleBroadcastState {
key: self.broadcast_key.clone(),
gsn: *self.last_gsn.lock().await,
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn find(&self, svc: ServiceType, chr: CharacteristicType) -> Result<(u64, u64)> {
for acc in &self.accessories {
for service in &acc.services {
if service.service_type == svc {
for ch in &service.characteristics {
if ch.char_type == chr {
return Ok((acc.aid, ch.iid));
}
}
}
}
}
Err(BleError::CharacteristicNotFound { aid: 0, iid: 0 })
}
pub async fn read(&mut self, aid: u64, iid: u64) -> Result<CharValue> {
let (uuid, format) = self
.chars
.get(&(aid, iid))
.cloned()
.ok_or(BleError::CharacteristicNotFound { aid, iid })?;
let raw = read_char_raw(
self.gatt.as_ref(),
&self.secure,
&self.reviver,
&uuid,
iid,
self.frag_size,
)
.await?;
db::decode_value(format, &raw)
}
pub async fn remove_pairing(&mut self, controller_id: &str) -> Result<()> {
let (uuid, iid) = self.pairings.clone();
let removing_self = controller_id == self.reviver.keypair.id;
let tlv = encode_remove_pairing(controller_id);
let body = pdu::encode_write_body(&tlv);
let mut s = self.secure.lock().await;
revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
s.tid = s.tid.wrapping_add(1);
let tid = s.tid;
let result = pdu::request_secure(
self.gatt.as_ref(),
&mut s.session,
&uuid,
OpCode::CharacteristicWrite,
tid,
iid,
&body,
self.frag_size,
)
.await;
match result {
Ok(resp) if resp.status != 0 => Err(BleError::PairingRejected(resp.status)),
Ok(resp) => expect_remove_m2(&pdu::value_param(&resp.body)?),
Err(BleError::Disconnected | BleError::Crypto(_)) if removing_self => Ok(()),
Err(e) => Err(e),
}
}
pub async fn write(&mut self, aid: u64, iid: u64, value: CharValue) -> Result<()> {
let (uuid, format) = self
.chars
.get(&(aid, iid))
.cloned()
.ok_or(BleError::CharacteristicNotFound { aid, iid })?;
let bytes = db::encode_value(format, &value)?;
write_char_raw(
self.gatt.as_ref(),
&self.secure,
&self.reviver,
&uuid,
iid,
&bytes,
self.frag_size,
)
.await
}
#[must_use]
pub fn pairing_id(&self) -> &str {
&self.reviver.pairing.pairing_id
}
pub async fn enable_broadcasts(&mut self, iids: &[u64]) -> Result<()> {
let mut s = self.secure.lock().await;
for &iid in iids {
let Some((uuid, _)) = self.chars.get(&(1, iid)).cloned() else {
continue;
};
let Ok(iid16) = u16::try_from(iid) else {
continue;
};
revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
s.tid = s.tid.wrapping_add(1);
let tid = s.tid;
let _ = pdu::request_secure(
self.gatt.as_ref(),
&mut s.session,
&uuid,
OpCode::CharacteristicConfig,
tid,
iid16,
&ENABLE_BROADCAST_BODY,
self.frag_size,
)
.await;
}
Ok(())
}
pub async fn subscribe(&mut self, aid: u64, iid: u64) -> Result<()> {
let (uuid, format) = self
.chars
.get(&(aid, iid))
.cloned()
.ok_or(BleError::CharacteristicNotFound { aid, iid })?;
let mut rx = self.gatt.subscribe(&uuid).await?;
let tx = self.events_tx.clone();
let gatt = self.gatt.clone();
let secure = self.secure.clone();
let reviver = self.reviver.clone();
let frag_size = self.frag_size;
let task = tokio::spawn(async move {
while rx.recv().await.is_some() {
if let Ok(raw) =
read_char_raw(gatt.as_ref(), &secure, &reviver, &uuid, iid, frag_size).await
{
if let Ok(value) = db::decode_value(format, &raw) {
let _ = tx.send(CharacteristicEvent { aid, iid, value });
}
}
}
});
self.tasks.push(task);
Ok(())
}
#[allow(clippy::too_many_lines)]
pub async fn watch_sleepy_events(
&mut self,
advert_source: Arc<dyn crate::gatt::AdvertSource>,
device_id: [u8; 6],
poll_iids: Vec<(u64, u64)>,
) -> Result<()> {
let mut targets = Vec::new();
for (aid, iid) in poll_iids {
if let Some((uuid, format)) = self.chars.get(&(aid, iid)).cloned() {
targets.push((aid, iid, uuid, format));
}
}
let formats: std::collections::HashMap<u64, CharFormat> = self
.chars
.iter()
.map(|((_, iid), (_, f))| (*iid, *f))
.collect();
let broadcast_key = self.broadcast_key.clone();
let mut adverts = advert_source.watch_adverts().await?;
let (poll_tx, mut poll_rx) = tokio::sync::watch::channel(0u16);
if !targets.is_empty() {
let gatt = self.gatt.clone();
let secure = self.secure.clone();
let reviver = self.reviver.clone();
let frag = self.frag_size;
let poll_events = self.events_tx.clone();
let poll_emitted = self.emitted.clone();
let poll_task = tokio::spawn(async move {
while poll_rx.changed().await.is_ok() {
let gsn = *poll_rx.borrow_and_update();
for (aid, iid, uuid, format) in &targets {
if let Ok(raw_val) =
read_char_raw(gatt.as_ref(), &secure, &reviver, uuid, *iid, frag).await
{
if let Ok(value) = db::decode_value(*format, &raw_val) {
if dedup_should_emit(&poll_emitted, *iid, gsn).await {
let _ = poll_events.send(CharacteristicEvent {
aid: *aid,
iid: *iid,
value,
});
}
}
}
}
}
});
self.tasks.push(poll_task);
}
let tx = self.events_tx.clone();
let last_gsn = self.last_gsn.clone();
let emitted = self.emitted.clone();
let advert_task = tokio::spawn(async move {
while let Some(raw) = adverts.recv().await {
match crate::advert::HapAdvert::parse(&raw.manufacturer_data) {
Some(crate::advert::HapAdvert::Regular {
device_id: d, gsn, ..
}) => {
if d != device_id {
continue;
}
{
let mut lg = last_gsn.lock().await;
if !gsn_is_newer(gsn, *lg) {
continue;
}
*lg = gsn;
}
let _ = poll_tx.send(gsn);
}
Some(crate::advert::HapAdvert::EncryptedNotification {
advertising_id,
payload,
}) => {
if advertising_id != device_id {
continue;
}
let start = *last_gsn.lock().await;
let candidates = std::iter::once(start.wrapping_add(1))
.chain(std::iter::once(start))
.chain((2..=100u16).map(|d| start.wrapping_add(d)));
for gsn in candidates {
let Ok(pt) = broadcast_key.open(gsn, &payload, &advertising_id) else {
continue;
};
if pt.len() < 12 {
continue;
}
if u16::from_le_bytes([pt[0], pt[1]]) != gsn {
continue;
}
if !gsn_is_newer(gsn, start) {
break;
}
let iid = u64::from(u16::from_le_bytes([pt[2], pt[3]]));
{
let mut lg = last_gsn.lock().await;
*lg = gsn;
}
let Some(format) = formats.get(&iid).copied() else {
break;
};
if let Ok(value) = db::decode_value(format, &pt[4..12]) {
if dedup_should_emit(&emitted, iid, gsn).await {
let _ = tx.send(CharacteristicEvent { aid: 1, iid, value });
}
}
break;
}
}
_ => {}
}
}
});
self.tasks.push(advert_task);
Ok(())
}
pub fn events(&self) -> impl tokio_stream::Stream<Item = CharacteristicEvent> {
tokio_stream::wrappers::BroadcastStream::new(self.events_tx.subscribe())
.filter_map(std::result::Result::ok)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::ble_accessory_with_db;
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn find_locates_characteristic() {
let (h, _g) = ble_accessory_with_db().await;
let (aid, iid) = h
.find(ServiceType::LightBulb, CharacteristicType::On)
.unwrap();
assert_eq!((aid, iid), (1, 11));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn find_missing_errors() {
let (h, _g) = ble_accessory_with_db().await;
let err = h
.find(ServiceType::LightBulb, CharacteristicType::Brightness)
.unwrap_err();
assert!(matches!(err, BleError::CharacteristicNotFound { .. }));
}
#[test]
fn encode_remove_pairing_matches_hap_layout() {
let tlv = encode_remove_pairing("c2");
assert_eq!(
tlv,
vec![0x06, 0x01, 0x01, 0x00, 0x01, 0x04, 0x01, 0x02, b'c', b'2']
);
}
#[test]
fn expect_remove_m2_accepts_m2_and_rejects_error() {
assert!(expect_remove_m2(&[0x06, 0x01, 0x02]).is_ok());
assert!(matches!(
expect_remove_m2(&[0x07, 0x01, 0x02]),
Err(BleError::PairingRejected(2))
));
assert!(matches!(
expect_remove_m2(&[0x06, 0x01, 0x01]),
Err(BleError::MalformedPdu(_))
));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn remove_pairing_writes_request_and_accepts_m2() {
let (mut h, gatt) = ble_accessory_with_db().await;
let m2 = vec![0x06, 0x01, 0x02];
let vbody = crate::pdu::encode_value_param(&m2);
let mut plain = vec![0x02, 0x01, 0x00];
plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
plain.extend_from_slice(&vbody);
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000050-0000-1000-8000-0026bb765291", sealed);
h.remove_pairing("AE:EC:86:C0:BF:D7").await.unwrap();
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn remove_own_pairing_tolerates_session_teardown() {
let (mut h, gatt) = ble_accessory_with_db().await;
gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
h.remove_pairing("test-controller").await.unwrap();
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn remove_other_pairing_propagates_teardown_error() {
let (mut h, gatt) = ble_accessory_with_db().await;
gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
let err = h.remove_pairing("some-other-controller").await.unwrap_err();
assert!(matches!(err, BleError::Crypto(_)));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn subscribe_then_event_decodes_value() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let mut plain = vec![0x02, 0x01, 0x00];
let vbody = crate::pdu::encode_value_param(&[0x01]); plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
plain.extend_from_slice(&vbody);
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
h.subscribe(1, 11).await.unwrap();
let mut events = h.events();
gatt.notifier("00000025-0000-1000-8000-0026bb765291")
.unwrap()
.send(Vec::new())
.await
.unwrap();
let ev = events.next().await.unwrap();
assert_eq!(ev.iid, 11);
assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn gsn_bump_triggers_disconnected_event_read() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let mut plain = vec![0x02, 0x01, 0x00];
let vbody = crate::pdu::encode_value_param(&[0x01]);
plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
plain.extend_from_slice(&vbody);
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: vec![
0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
],
})
.await
.unwrap();
let ev = events.next().await.unwrap();
assert_eq!(ev.iid, 11);
assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn encrypted_broadcast_0x11_decrypts_and_emits_event() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut pt = Vec::new();
pt.extend_from_slice(&1u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed = key.seal(1, &pt, &aid_bytes);
let mut mfg = vec![0x11u8, 0x00];
mfg.extend_from_slice(&aid_bytes);
mfg.extend_from_slice(&sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, aid_bytes, vec![])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg,
})
.await
.unwrap();
let ev = events.next().await.unwrap();
assert_eq!(ev.aid, 1);
assert_eq!(ev.iid, 11);
assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
}
#[test]
fn gsn_is_newer_handles_wraparound() {
assert!(gsn_is_newer(6, 5));
assert!(!gsn_is_newer(5, 5));
assert!(!gsn_is_newer(4, 5));
assert!(gsn_is_newer(1, 65535)); assert!(!gsn_is_newer(65535, 1)); }
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn same_change_via_poll_and_broadcast_emits_once() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let mut plain = vec![0x02, 0x01, 0x00];
let vbody = crate::pdu::encode_value_param(&[0x01]);
plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
plain.extend_from_slice(&vbody);
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: vec![
0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
],
})
.await
.unwrap();
let ev = events.next().await.unwrap();
assert_eq!(ev.iid, 11);
assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut pt = Vec::new();
pt.extend_from_slice(&9u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed_bc = key.seal(9, &pt, &aid_bytes);
let mut mfg = vec![0x11u8, 0x00];
mfg.extend_from_slice(&aid_bytes);
mfg.extend_from_slice(&sealed_bc);
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg,
})
.await
.unwrap();
let timeout_result =
tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
assert!(
timeout_result.is_err(),
"expected dedup to suppress the duplicate 0x11 broadcast event, but got one"
);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn broadcast_delivered_while_poll_read_blocked() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let release = gatt.block_next_read("00000025-0000-1000-8000-0026bb765291");
let mut plain = vec![0x02, 0x01, 0x00];
let vbody = crate::pdu::encode_value_param(&[0x01]);
plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
plain.extend_from_slice(&vbody);
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: vec![
0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
],
})
.await
.unwrap();
let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut pt = Vec::new();
pt.extend_from_slice(&10u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x00, 0, 0, 0, 0, 0, 0, 0]); let sealed_bc = key.seal(10, &pt, &aid_bytes);
let mut mfg = vec![0x11u8, 0x00];
mfg.extend_from_slice(&aid_bytes);
mfg.extend_from_slice(&sealed_bc);
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg,
})
.await
.unwrap();
let ev = tokio::time::timeout(std::time::Duration::from_secs(2), events.next())
.await
.unwrap()
.unwrap();
assert_eq!(ev.iid, 11);
assert_eq!(ev.value, hap_model::format::CharValue::Bool(false));
release.notify_one();
let ev2 = tokio::time::timeout(std::time::Duration::from_secs(2), events.next())
.await
.unwrap()
.unwrap();
assert_eq!(ev2.iid, 11);
assert_eq!(ev2.value, hap_model::format::CharValue::Bool(true));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn foreign_device_advert_ignored() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: vec![
0x06, 0x21, 0x01, 9, 9, 9, 9, 9, 9, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
],
})
.await
.unwrap();
let timeout_result =
tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
assert!(
timeout_result.is_err(),
"foreign device advert must not emit an event, but one was received"
);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn stale_gsn_broadcast_ignored() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut pt = Vec::new();
pt.extend_from_slice(&5u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed = key.seal(5, &pt, &aid_bytes);
let mut mfg = vec![0x11u8, 0x00];
mfg.extend_from_slice(&aid_bytes);
mfg.extend_from_slice(&sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, aid_bytes, vec![])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg.clone(),
})
.await
.unwrap();
let ev = events.next().await.unwrap();
assert_eq!(ev.iid, 11);
assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg,
})
.await
.unwrap();
let timeout_result =
tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
assert!(
timeout_result.is_err(),
"duplicate GSN 5 broadcast must not emit a second event"
);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn wrong_broadcast_key_ignored() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let wrong_key = hap_crypto::BroadcastKey::from_bytes([0xFF; 32]);
let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut pt = Vec::new();
pt.extend_from_slice(&1u16.to_le_bytes());
pt.extend_from_slice(&11u16.to_le_bytes());
pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]);
let sealed = wrong_key.seal(1, &pt, &aid_bytes);
let mut mfg = vec![0x11u8, 0x00];
mfg.extend_from_slice(&aid_bytes);
mfg.extend_from_slice(&sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, aid_bytes, vec![])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg,
})
.await
.unwrap();
let timeout_result =
tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
assert!(
timeout_result.is_err(),
"wrong-key broadcast must not emit any event (all candidate opens fail)"
);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn malformed_0x11_advert_ignored() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![])
.await
.unwrap();
let mut events = h.events();
let manufacturer_data = vec![0x11, 0x00, 1, 2, 3, 4, 5, 6, 0xAA, 0xBB];
gatt.advert_sender()
.send(crate::gatt::RawAdvert { manufacturer_data })
.await
.unwrap();
let timeout_result =
tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
assert!(
timeout_result.is_err(),
"malformed (too-short payload) 0x11 advert must not emit any event"
);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn broadcast_value_self_inconsistent_gsn_ignored() {
use tokio_stream::StreamExt as _;
let (mut h, gatt) = ble_accessory_with_db().await;
let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut pt = Vec::new();
pt.extend_from_slice(&3u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed = key.seal(7, &pt, &aid_bytes);
let mut mfg = vec![0x11u8, 0x00];
mfg.extend_from_slice(&aid_bytes);
mfg.extend_from_slice(&sealed);
let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
h.watch_sleepy_events(advert_source, aid_bytes, vec![])
.await
.unwrap();
let mut events = h.events();
gatt.advert_sender()
.send(crate::gatt::RawAdvert {
manufacturer_data: mfg,
})
.await
.unwrap();
let timeout_result =
tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
assert!(
timeout_result.is_err(),
"self-inconsistent GSN (embedded 3 != nonce 7) must not emit any event"
);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn read_after_reconnect_re_verifies_before_using_session() {
let (mut h, gatt) = ble_accessory_with_db().await;
let mut plain = vec![0x02, 0x01, 0x00];
let vbody = crate::pdu::encode_value_param(&[0x01]);
plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
plain.extend_from_slice(&vbody);
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
gatt.bump_generation();
let err = h.read(1, 11).await.unwrap_err();
assert!(
!matches!(err, BleError::CharacteristicNotFound { .. }),
"expected a verify/transport error from the re-verify attempt, got {err:?}"
);
}
#[tokio::test]
async fn dedup_emits_once_per_gsn_and_never_downgrades() {
let emitted = Mutex::new(HashMap::new());
assert!(dedup_should_emit(&emitted, 11, 9).await);
assert!(!dedup_should_emit(&emitted, 11, 9).await);
assert!(dedup_should_emit(&emitted, 11, 10).await);
assert!(dedup_should_emit(&emitted, 11, 9).await);
assert!(!dedup_should_emit(&emitted, 11, 10).await);
assert!(dedup_should_emit(&emitted, 12, 65535).await);
assert!(dedup_should_emit(&emitted, 12, 1).await);
assert!(!dedup_should_emit(&emitted, 12, 1).await);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn write_sends_secure_pdu_and_accepts_success() {
let (mut h, gatt) = ble_accessory_with_db().await;
let plain = vec![0x02, 0x01, 0x00];
let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
h.write(1, 11, hap_model::format::CharValue::Bool(true))
.await
.unwrap();
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn write_surfaces_nonzero_pdu_status() {
let (mut h, gatt) = ble_accessory_with_db().await;
let plain = vec![0x02, 0x01, 0x06]; let sealed =
hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
let err = h
.write(1, 11, hap_model::format::CharValue::Bool(true))
.await
.unwrap_err();
assert!(matches!(err, BleError::RequestRejected(6)));
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn pairing_id_exposes_the_stored_pairing() {
let (h, _g) = ble_accessory_with_db().await;
assert_eq!(h.pairing_id(), "AE:EC:86:C0:BF:D7");
}
}