1use crate::broadcast_state::BleBroadcastState;
5use crate::db;
6use crate::error::{BleError, Result};
7use crate::gatt::{GattConnection, GattService};
8use crate::pairing;
9use crate::pdu::{self, OpCode};
10use crate::session::BleSession;
11use hap_crypto::{AccessoryPairing, ControllerKeypair};
12use hap_model::format::{CharFormat, CharValue};
13use hap_model::tree::Accessory;
14use hap_model::{CharacteristicType, ServiceType};
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::Mutex;
18use tokio_stream::StreamExt as _;
19
20fn gsn_is_newer(new: u16, last: u16) -> bool {
24 let diff = new.wrapping_sub(last);
25 diff != 0 && diff < 0x8000
26}
27
28const MAX_REVIVE_RETRIES: u32 = 3;
31
32const ENABLE_BROADCAST_BODY: [u8; 7] = [0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01];
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct CharacteristicEvent {
39 pub aid: u64,
41 pub iid: u64,
43 pub value: CharValue,
45}
46
47struct Secure {
50 session: BleSession,
51 tid: u8,
52 generation: u64,
56}
57
58struct Reviver {
61 keypair: ControllerKeypair,
62 pairing: AccessoryPairing,
63 verify_char: String,
64 verify_iid: u16,
65 frag_size: usize,
66}
67
68pub(crate) struct SecureContext {
73 pub session: BleSession,
75 pub session_generation: u64,
77 pub keypair: ControllerKeypair,
79 pub pairing: AccessoryPairing,
81 pub verify_char: String,
83 pub verify_iid: u16,
84 pub pairings_char: String,
86 pub pairings_iid: u16,
87 pub broadcast_key: hap_crypto::BroadcastKey,
89 pub initial_gsn: u16,
91}
92
93async fn revive_if_stale(
97 gatt: &dyn GattConnection,
98 s: &mut Secure,
99 reviver: &Reviver,
100) -> Result<()> {
101 if gatt.generation().await <= s.generation {
102 return Ok(());
103 }
104 let (session, _bkey) = pairing::pair_verify(
105 gatt,
106 &reviver.verify_char,
107 reviver.verify_iid,
108 &reviver.keypair,
109 &reviver.pairing,
110 reviver.frag_size,
111 )
112 .await?;
113 s.session = session;
114 s.tid = 0;
115 s.generation = gatt.generation().await;
118 Ok(())
119}
120
121mod pairings_tlv {
123 pub(super) const STATE: u8 = 0x06;
124 pub(super) const METHOD: u8 = 0x00;
125 pub(super) const IDENTIFIER: u8 = 0x01;
126 pub(super) const ERROR: u8 = 0x07;
127 pub(super) const STATE_M1: u8 = 0x01;
128 pub(super) const STATE_M2: u8 = 0x02;
129 pub(super) const METHOD_REMOVE: u8 = 0x04;
130}
131
132fn encode_remove_pairing(controller_id: &str) -> Vec<u8> {
135 let mut out = Vec::new();
136 let mut w = hap_tlv8::Tlv8Writer::new(&mut out);
137 w.push_u8(pairings_tlv::STATE, pairings_tlv::STATE_M1);
138 w.push_u8(pairings_tlv::METHOD, pairings_tlv::METHOD_REMOVE);
139 w.push(pairings_tlv::IDENTIFIER, controller_id.as_bytes());
140 out
141}
142
143fn expect_remove_m2(tlv: &[u8]) -> Result<()> {
146 let map = hap_tlv8::Tlv8Map::parse(tlv)?;
147 if let Some(err) = map.get(pairings_tlv::ERROR) {
148 return Err(BleError::PairingRejected(err.first().copied().unwrap_or(1)));
149 }
150 match map
151 .get(pairings_tlv::STATE)
152 .and_then(|s| s.first().copied())
153 {
154 Some(pairings_tlv::STATE_M2) => Ok(()),
155 _ => Err(BleError::MalformedPdu("remove-pairing reply not state M2")),
156 }
157}
158
159async fn dedup_should_emit(emitted: &Mutex<HashMap<u64, u16>>, iid: u64, gsn: u16) -> bool {
168 let mut e = emitted.lock().await;
169 let prev = e.get(&iid).copied();
170 if prev == Some(gsn) {
171 return false;
172 }
173 if prev.is_none_or(|p| gsn_is_newer(gsn, p)) {
174 e.insert(iid, gsn);
175 }
176 true
177}
178
179async fn read_char_raw(
184 gatt: &dyn GattConnection,
185 secure: &Mutex<Secure>,
186 reviver: &Reviver,
187 uuid: &str,
188 iid: u64,
189 frag_size: usize,
190) -> Result<Vec<u8>> {
191 let iid16 = u16::try_from(iid).map_err(|_| BleError::CharacteristicNotFound { aid: 0, iid })?;
192 let mut s = secure.lock().await;
193 let mut attempts = 0;
194 loop {
195 revive_if_stale(gatt, &mut s, reviver).await?;
196 s.tid = s.tid.wrapping_add(1);
197 let tid = s.tid;
198 match pdu::request_secure(
199 gatt,
200 &mut s.session,
201 uuid,
202 OpCode::CharacteristicRead,
203 tid,
204 iid16,
205 &[],
206 frag_size,
207 )
208 .await
209 {
210 Ok(resp) => return pdu::value_param(&resp.body),
211 Err(e) => {
215 attempts += 1;
216 if attempts < MAX_REVIVE_RETRIES && gatt.generation().await > s.generation {
217 continue;
218 }
219 return Err(e);
220 }
221 }
222 }
223}
224
225pub struct BleAccessory {
229 gatt: Arc<dyn GattConnection>,
230 secure: Arc<Mutex<Secure>>,
231 reviver: Arc<Reviver>,
232 pairings: (String, u16),
234 frag_size: usize,
235 accessories: Vec<Accessory>,
236 chars: HashMap<(u64, u64), (String, CharFormat)>,
238 events_tx: tokio::sync::broadcast::Sender<CharacteristicEvent>,
239 tasks: Vec<tokio::task::JoinHandle<()>>,
241 last_gsn: Arc<Mutex<u16>>,
243 emitted: Arc<Mutex<HashMap<u64, u16>>>,
246 broadcast_key: hap_crypto::BroadcastKey,
248}
249
250impl Drop for BleAccessory {
251 fn drop(&mut self) {
252 for task in &self.tasks {
253 task.abort();
254 }
255 }
256}
257
258impl BleAccessory {
259 pub(crate) fn new(
266 gatt: Arc<dyn GattConnection>,
267 ctx: SecureContext,
268 frag_size: usize,
269 gatt_services: &[GattService],
270 accessories: Vec<Accessory>,
271 ) -> Self {
272 let (events_tx, _) = tokio::sync::broadcast::channel(64);
273 let mut uuid_by_iid: HashMap<u64, String> = HashMap::new();
277 for gs in gatt_services {
278 for gc in &gs.characteristics {
279 uuid_by_iid.insert(u64::from(gc.iid), gc.uuid.clone());
280 }
281 }
282 let mut chars = HashMap::new();
283 for acc in &accessories {
284 for svc in &acc.services {
285 for ch in &svc.characteristics {
286 if let Some(uuid) = uuid_by_iid.get(&ch.iid) {
287 chars.insert((acc.aid, ch.iid), (uuid.clone(), ch.format));
288 }
289 }
290 }
291 }
292 Self {
293 gatt,
294 secure: Arc::new(Mutex::new(Secure {
295 session: ctx.session,
296 tid: 0,
297 generation: ctx.session_generation,
298 })),
299 reviver: Arc::new(Reviver {
300 keypair: ctx.keypair,
301 pairing: ctx.pairing,
302 verify_char: ctx.verify_char,
303 verify_iid: ctx.verify_iid,
304 frag_size,
305 }),
306 pairings: (ctx.pairings_char, ctx.pairings_iid),
307 frag_size,
308 accessories,
309 chars,
310 events_tx,
311 tasks: Vec::new(),
312 last_gsn: Arc::new(Mutex::new(ctx.initial_gsn)),
313 emitted: Arc::new(Mutex::new(HashMap::new())),
314 broadcast_key: ctx.broadcast_key,
315 }
316 }
317
318 pub fn accessories(&self) -> &[Accessory] {
320 &self.accessories
321 }
322
323 pub async fn broadcast_state(&self) -> BleBroadcastState {
326 BleBroadcastState {
327 key: self.broadcast_key.clone(),
328 gsn: *self.last_gsn.lock().await,
329 }
330 }
331
332 #[allow(clippy::needless_pass_by_value)]
340 pub fn find(&self, svc: ServiceType, chr: CharacteristicType) -> Result<(u64, u64)> {
341 for acc in &self.accessories {
342 for service in &acc.services {
343 if service.service_type == svc {
344 for ch in &service.characteristics {
345 if ch.char_type == chr {
346 return Ok((acc.aid, ch.iid));
347 }
348 }
349 }
350 }
351 }
352 Err(BleError::CharacteristicNotFound { aid: 0, iid: 0 })
353 }
354
355 pub async fn read(&mut self, aid: u64, iid: u64) -> Result<CharValue> {
360 let (uuid, format) = self
361 .chars
362 .get(&(aid, iid))
363 .cloned()
364 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
365 let raw = read_char_raw(
366 self.gatt.as_ref(),
367 &self.secure,
368 &self.reviver,
369 &uuid,
370 iid,
371 self.frag_size,
372 )
373 .await?;
374 db::decode_value(format, &raw)
375 }
376
377 pub async fn remove_pairing(&mut self, controller_id: &str) -> Result<()> {
398 let (uuid, iid) = self.pairings.clone();
399 let removing_self = controller_id == self.reviver.keypair.id;
400 let tlv = encode_remove_pairing(controller_id);
401 let body = pdu::encode_write_body(&tlv);
402 let mut s = self.secure.lock().await;
403 revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
404 s.tid = s.tid.wrapping_add(1);
405 let tid = s.tid;
406 let result = pdu::request_secure(
407 self.gatt.as_ref(),
408 &mut s.session,
409 &uuid,
410 OpCode::CharacteristicWrite,
411 tid,
412 iid,
413 &body,
414 self.frag_size,
415 )
416 .await;
417 match result {
418 Ok(resp) if resp.status != 0 => Err(BleError::PairingRejected(resp.status)),
419 Ok(resp) => expect_remove_m2(&pdu::value_param(&resp.body)?),
420 Err(BleError::Disconnected | BleError::Crypto(_)) if removing_self => Ok(()),
424 Err(e) => Err(e),
425 }
426 }
427
428 pub async fn enable_broadcasts(&mut self, iids: &[u64]) -> Result<()> {
439 let mut s = self.secure.lock().await;
440 for &iid in iids {
441 let Some((uuid, _)) = self.chars.get(&(1, iid)).cloned() else {
442 continue;
443 };
444 let Ok(iid16) = u16::try_from(iid) else {
445 continue;
446 };
447 revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
448 s.tid = s.tid.wrapping_add(1);
449 let tid = s.tid;
450 let _ = pdu::request_secure(
451 self.gatt.as_ref(),
452 &mut s.session,
453 &uuid,
454 OpCode::CharacteristicConfig,
455 tid,
456 iid16,
457 &ENABLE_BROADCAST_BODY,
458 self.frag_size,
459 )
460 .await;
461 }
462 Ok(())
463 }
464
465 pub async fn subscribe(&mut self, aid: u64, iid: u64) -> Result<()> {
478 let (uuid, format) = self
479 .chars
480 .get(&(aid, iid))
481 .cloned()
482 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
483 let mut rx = self.gatt.subscribe(&uuid).await?;
484 let tx = self.events_tx.clone();
485 let gatt = self.gatt.clone();
486 let secure = self.secure.clone();
487 let reviver = self.reviver.clone();
488 let frag_size = self.frag_size;
489 let task = tokio::spawn(async move {
490 while rx.recv().await.is_some() {
492 if let Ok(raw) =
493 read_char_raw(gatt.as_ref(), &secure, &reviver, &uuid, iid, frag_size).await
494 {
495 if let Ok(value) = db::decode_value(format, &raw) {
496 let _ = tx.send(CharacteristicEvent { aid, iid, value });
497 }
498 }
499 }
500 });
501 self.tasks.push(task);
502 Ok(())
503 }
504
505 #[allow(clippy::too_many_lines)]
521 pub async fn watch_sleepy_events(
522 &mut self,
523 advert_source: Arc<dyn crate::gatt::AdvertSource>,
524 device_id: [u8; 6],
525 poll_iids: Vec<(u64, u64)>,
526 ) -> Result<()> {
527 let mut targets = Vec::new();
530 for (aid, iid) in poll_iids {
531 if let Some((uuid, format)) = self.chars.get(&(aid, iid)).cloned() {
532 targets.push((aid, iid, uuid, format));
533 }
534 }
535 let formats: std::collections::HashMap<u64, CharFormat> = self
537 .chars
538 .iter()
539 .map(|((_, iid), (_, f))| (*iid, *f))
540 .collect();
541 let broadcast_key = self.broadcast_key.clone();
542
543 let mut adverts = advert_source.watch_adverts().await?;
544
545 let (poll_tx, mut poll_rx) = tokio::sync::watch::channel(0u16);
550 if !targets.is_empty() {
551 let gatt = self.gatt.clone();
552 let secure = self.secure.clone();
553 let reviver = self.reviver.clone();
554 let frag = self.frag_size;
555 let poll_events = self.events_tx.clone();
556 let poll_emitted = self.emitted.clone();
557 let poll_task = tokio::spawn(async move {
558 while poll_rx.changed().await.is_ok() {
559 let gsn = *poll_rx.borrow_and_update();
560 for (aid, iid, uuid, format) in &targets {
561 if let Ok(raw_val) =
562 read_char_raw(gatt.as_ref(), &secure, &reviver, uuid, *iid, frag).await
563 {
564 if let Ok(value) = db::decode_value(*format, &raw_val) {
565 if dedup_should_emit(&poll_emitted, *iid, gsn).await {
566 let _ = poll_events.send(CharacteristicEvent {
567 aid: *aid,
568 iid: *iid,
569 value,
570 });
571 }
572 }
573 }
574 }
575 }
576 });
577 self.tasks.push(poll_task);
578 }
579
580 let tx = self.events_tx.clone();
581 let last_gsn = self.last_gsn.clone();
582 let emitted = self.emitted.clone();
583 let advert_task = tokio::spawn(async move {
584 while let Some(raw) = adverts.recv().await {
585 match crate::advert::HapAdvert::parse(&raw.manufacturer_data) {
586 Some(crate::advert::HapAdvert::Regular {
587 device_id: d, gsn, ..
588 }) => {
589 if d != device_id {
590 continue;
591 }
592 {
593 let mut lg = last_gsn.lock().await;
594 if !gsn_is_newer(gsn, *lg) {
595 continue;
596 }
597 *lg = gsn;
598 }
599 let _ = poll_tx.send(gsn);
602 }
603 Some(crate::advert::HapAdvert::EncryptedNotification {
604 advertising_id,
605 payload,
606 }) => {
607 if advertising_id != device_id {
608 continue;
609 }
610 let start = *last_gsn.lock().await;
611 let candidates = std::iter::once(start.wrapping_add(1))
614 .chain(std::iter::once(start))
615 .chain((2..=100u16).map(|d| start.wrapping_add(d)));
616 for gsn in candidates {
617 let Ok(pt) = broadcast_key.open(gsn, &payload, &advertising_id) else {
618 continue;
619 };
620 if pt.len() < 12 {
621 continue;
622 }
623 if u16::from_le_bytes([pt[0], pt[1]]) != gsn {
624 continue;
625 }
626 if !gsn_is_newer(gsn, start) {
628 break;
629 }
630 let iid = u64::from(u16::from_le_bytes([pt[2], pt[3]]));
631 {
635 let mut lg = last_gsn.lock().await;
636 *lg = gsn;
637 }
638 let Some(format) = formats.get(&iid).copied() else {
639 break;
640 };
641 if let Ok(value) = db::decode_value(format, &pt[4..12]) {
642 if dedup_should_emit(&emitted, iid, gsn).await {
643 let _ = tx.send(CharacteristicEvent { aid: 1, iid, value });
644 }
645 }
646 break;
647 }
648 }
649 _ => {}
650 }
651 }
652 });
653 self.tasks.push(advert_task);
654 Ok(())
655 }
656
657 pub fn events(&self) -> impl tokio_stream::Stream<Item = CharacteristicEvent> {
660 tokio_stream::wrappers::BroadcastStream::new(self.events_tx.subscribe())
661 .filter_map(std::result::Result::ok)
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::gatt::{GattCharacteristic, GattService, MockGatt};
669 use hap_crypto::SessionKeys;
670
671 #[allow(clippy::unwrap_used)]
672 fn on_le() -> Vec<u8> {
673 let hex = "00000025000010008000".to_string() + "0026bb765291";
674 let mut b: Vec<u8> = (0..16)
675 .map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap())
676 .collect();
677 b.reverse();
678 b
679 }
680
681 fn on_service() -> GattService {
682 GattService {
683 uuid: "00000043-0000-1000-8000-0026bb765291".into(), iid: 10,
685 characteristics: vec![GattCharacteristic {
686 uuid: "00000025-0000-1000-8000-0026bb765291".into(), iid: 11,
688 }],
689 }
690 }
691
692 #[allow(clippy::unwrap_used)]
693 fn sig_resp() -> Vec<u8> {
694 let mut body = Vec::new();
695 let mut w = hap_tlv8::Tlv8Writer::new(&mut body);
696 w.push(crate::pdu::param::CHAR_TYPE, &on_le());
697 w.push(crate::pdu::param::PROPERTIES, &0x0083u16.to_le_bytes()); w.push(
699 crate::pdu::param::PRESENTATION_FORMAT,
700 &[0x01, 0, 0, 0, 0, 0, 0],
701 );
702 let mut resp = vec![0x02, 0x01, 0x00];
703 resp.extend_from_slice(&u16::try_from(body.len()).unwrap().to_le_bytes());
704 resp.extend_from_slice(&body);
705 resp
706 }
707
708 #[allow(clippy::unwrap_used)]
709 async fn handle_with_db() -> (BleAccessory, Arc<MockGatt>) {
710 let gatt = Arc::new(MockGatt::new().with_services(vec![on_service()]));
711 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sig_resp());
712 let session = BleSession::new(SessionKeys {
713 read_key: [0; 32],
714 write_key: [0; 32],
715 });
716 let services = gatt.enumerate().await.unwrap();
717 let accessories = crate::db::build_db(gatt.as_ref(), &services, 512)
718 .await
719 .unwrap();
720 let ctx = SecureContext {
721 session,
722 session_generation: 0,
723 keypair: ControllerKeypair::generate("test-controller".into()),
724 pairing: AccessoryPairing {
725 pairing_id: "AE:EC:86:C0:BF:D7".into(),
726 ltpk: [0; 32],
727 },
728 verify_char: "0000004e-0000-1000-8000-0026bb765291".into(),
729 verify_iid: 1,
730 pairings_char: "00000050-0000-1000-8000-0026bb765291".into(),
731 pairings_iid: 2,
732 broadcast_key: hap_crypto::BroadcastKey::from_bytes([0u8; 32]),
733 initial_gsn: 0,
734 };
735 let h = BleAccessory::new(gatt.clone(), ctx, 512, &services, accessories);
736 (h, gatt)
737 }
738
739 #[tokio::test]
740 #[allow(clippy::unwrap_used)]
741 async fn find_locates_characteristic() {
742 let (h, _g) = handle_with_db().await;
743 let (aid, iid) = h
744 .find(ServiceType::LightBulb, CharacteristicType::On)
745 .unwrap();
746 assert_eq!((aid, iid), (1, 11));
747 }
748
749 #[tokio::test]
750 #[allow(clippy::unwrap_used)]
751 async fn find_missing_errors() {
752 let (h, _g) = handle_with_db().await;
753 let err = h
754 .find(ServiceType::LightBulb, CharacteristicType::Brightness)
755 .unwrap_err();
756 assert!(matches!(err, BleError::CharacteristicNotFound { .. }));
757 }
758
759 #[test]
760 fn encode_remove_pairing_matches_hap_layout() {
761 let tlv = encode_remove_pairing("c2");
763 assert_eq!(
764 tlv,
765 vec![0x06, 0x01, 0x01, 0x00, 0x01, 0x04, 0x01, 0x02, b'c', b'2']
766 );
767 }
768
769 #[test]
770 fn expect_remove_m2_accepts_m2_and_rejects_error() {
771 assert!(expect_remove_m2(&[0x06, 0x01, 0x02]).is_ok());
772 assert!(matches!(
774 expect_remove_m2(&[0x07, 0x01, 0x02]),
775 Err(BleError::PairingRejected(2))
776 ));
777 assert!(matches!(
779 expect_remove_m2(&[0x06, 0x01, 0x01]),
780 Err(BleError::MalformedPdu(_))
781 ));
782 }
783
784 #[tokio::test]
785 #[allow(clippy::unwrap_used)]
786 async fn remove_pairing_writes_request_and_accepts_m2() {
787 let (mut h, gatt) = handle_with_db().await;
788
789 let m2 = vec![0x06, 0x01, 0x02];
792 let vbody = crate::pdu::encode_value_param(&m2);
793 let mut plain = vec![0x02, 0x01, 0x00];
794 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
795 plain.extend_from_slice(&vbody);
796 let sealed =
797 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
798 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", sealed);
799
800 h.remove_pairing("AE:EC:86:C0:BF:D7").await.unwrap();
801 }
802
803 #[tokio::test]
804 #[allow(clippy::unwrap_used)]
805 async fn remove_own_pairing_tolerates_session_teardown() {
806 let (mut h, gatt) = handle_with_db().await;
808 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
812 h.remove_pairing("test-controller").await.unwrap();
813 }
814
815 #[tokio::test]
816 #[allow(clippy::unwrap_used)]
817 async fn remove_other_pairing_propagates_teardown_error() {
818 let (mut h, gatt) = handle_with_db().await;
821 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
822 let err = h.remove_pairing("some-other-controller").await.unwrap_err();
823 assert!(matches!(err, BleError::Crypto(_)));
824 }
825
826 #[tokio::test]
827 #[allow(clippy::unwrap_used)]
828 async fn subscribe_then_event_decodes_value() {
829 use tokio_stream::StreamExt as _;
830 let (mut h, gatt) = handle_with_db().await;
831
832 let mut plain = vec![0x02, 0x01, 0x00];
836 let vbody = crate::pdu::encode_value_param(&[0x01]); plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
838 plain.extend_from_slice(&vbody);
839 let sealed =
840 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
841 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
842
843 h.subscribe(1, 11).await.unwrap();
844 let mut events = h.events();
845
846 gatt.notifier("00000025-0000-1000-8000-0026bb765291")
848 .unwrap()
849 .send(Vec::new())
850 .await
851 .unwrap();
852
853 let ev = events.next().await.unwrap();
854 assert_eq!(ev.iid, 11);
855 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
856 }
857
858 #[tokio::test]
859 #[allow(clippy::unwrap_used)]
860 async fn gsn_bump_triggers_disconnected_event_read() {
861 use tokio_stream::StreamExt as _;
862 let (mut h, gatt) = handle_with_db().await;
863
864 let mut plain = vec![0x02, 0x01, 0x00];
867 let vbody = crate::pdu::encode_value_param(&[0x01]);
868 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
869 plain.extend_from_slice(&vbody);
870 let sealed =
871 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
872 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
873
874 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
875 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
876 .await
877 .unwrap();
878 let mut events = h.events();
879
880 gatt.advert_sender()
882 .send(crate::gatt::RawAdvert {
883 manufacturer_data: vec![
884 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
885 ],
886 })
887 .await
888 .unwrap();
889
890 let ev = events.next().await.unwrap();
891 assert_eq!(ev.iid, 11);
892 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
893 }
894
895 #[tokio::test]
896 #[allow(clippy::unwrap_used)]
897 async fn encrypted_broadcast_0x11_decrypts_and_emits_event() {
898 use tokio_stream::StreamExt as _;
899 let (mut h, gatt) = handle_with_db().await;
901
902 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
905 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
906 let mut pt = Vec::new();
907 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);
911
912 let mut mfg = vec![0x11u8, 0x00];
914 mfg.extend_from_slice(&aid_bytes);
915 mfg.extend_from_slice(&sealed);
916
917 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
918 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
920 .await
921 .unwrap();
922 let mut events = h.events();
923
924 gatt.advert_sender()
925 .send(crate::gatt::RawAdvert {
926 manufacturer_data: mfg,
927 })
928 .await
929 .unwrap();
930
931 let ev = events.next().await.unwrap();
932 assert_eq!(ev.aid, 1);
933 assert_eq!(ev.iid, 11);
934 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
935 }
936
937 #[test]
938 fn gsn_is_newer_handles_wraparound() {
939 assert!(gsn_is_newer(6, 5));
940 assert!(!gsn_is_newer(5, 5));
941 assert!(!gsn_is_newer(4, 5));
942 assert!(gsn_is_newer(1, 65535)); assert!(!gsn_is_newer(65535, 1)); }
945
946 #[tokio::test]
947 #[allow(clippy::unwrap_used)]
948 async fn same_change_via_poll_and_broadcast_emits_once() {
949 use tokio_stream::StreamExt as _;
950 let (mut h, gatt) = handle_with_db().await;
951
952 let mut plain = vec![0x02, 0x01, 0x00];
955 let vbody = crate::pdu::encode_value_param(&[0x01]);
956 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
957 plain.extend_from_slice(&vbody);
958 let sealed =
959 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
960 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
961
962 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
963 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
964 .await
965 .unwrap();
966 let mut events = h.events();
967
968 gatt.advert_sender()
970 .send(crate::gatt::RawAdvert {
971 manufacturer_data: vec![
972 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
973 ],
974 })
975 .await
976 .unwrap();
977
978 let ev = events.next().await.unwrap();
980 assert_eq!(ev.iid, 11);
981 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
982
983 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
985 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
986 let mut pt = Vec::new();
987 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);
991
992 let mut mfg = vec![0x11u8, 0x00];
993 mfg.extend_from_slice(&aid_bytes);
994 mfg.extend_from_slice(&sealed_bc);
995
996 gatt.advert_sender()
997 .send(crate::gatt::RawAdvert {
998 manufacturer_data: mfg,
999 })
1000 .await
1001 .unwrap();
1002
1003 let timeout_result =
1005 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1006 assert!(
1007 timeout_result.is_err(),
1008 "expected dedup to suppress the duplicate 0x11 broadcast event, but got one"
1009 );
1010 }
1011
1012 #[tokio::test]
1017 #[allow(clippy::unwrap_used)]
1018 async fn broadcast_delivered_while_poll_read_blocked() {
1019 use tokio_stream::StreamExt as _;
1020 let (mut h, gatt) = handle_with_db().await;
1021
1022 let release = gatt.block_next_read("00000025-0000-1000-8000-0026bb765291");
1025 let mut plain = vec![0x02, 0x01, 0x00];
1026 let vbody = crate::pdu::encode_value_param(&[0x01]);
1027 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1028 plain.extend_from_slice(&vbody);
1029 let sealed =
1030 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1031 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1032
1033 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1034 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
1035 .await
1036 .unwrap();
1037 let mut events = h.events();
1038
1039 gatt.advert_sender()
1041 .send(crate::gatt::RawAdvert {
1042 manufacturer_data: vec![
1043 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
1044 ],
1045 })
1046 .await
1047 .unwrap();
1048
1049 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1052 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1053 let mut pt = Vec::new();
1054 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);
1058 let mut mfg = vec![0x11u8, 0x00];
1059 mfg.extend_from_slice(&aid_bytes);
1060 mfg.extend_from_slice(&sealed_bc);
1061 gatt.advert_sender()
1062 .send(crate::gatt::RawAdvert {
1063 manufacturer_data: mfg,
1064 })
1065 .await
1066 .unwrap();
1067
1068 let ev = tokio::time::timeout(std::time::Duration::from_secs(2), events.next())
1072 .await
1073 .unwrap()
1074 .unwrap();
1075 assert_eq!(ev.iid, 11);
1076 assert_eq!(ev.value, hap_model::format::CharValue::Bool(false));
1077
1078 release.notify_one();
1080 let ev2 = tokio::time::timeout(std::time::Duration::from_secs(2), events.next())
1081 .await
1082 .unwrap()
1083 .unwrap();
1084 assert_eq!(ev2.iid, 11);
1085 assert_eq!(ev2.value, hap_model::format::CharValue::Bool(true));
1086 }
1087
1088 #[tokio::test]
1093 #[allow(clippy::unwrap_used)]
1094 async fn foreign_device_advert_ignored() {
1095 use tokio_stream::StreamExt as _;
1096 let (mut h, gatt) = handle_with_db().await;
1097
1098 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1099 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
1101 .await
1102 .unwrap();
1103 let mut events = h.events();
1104
1105 gatt.advert_sender()
1107 .send(crate::gatt::RawAdvert {
1108 manufacturer_data: vec![
1109 0x06, 0x21, 0x01, 9, 9, 9, 9, 9, 9, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
1110 ],
1111 })
1112 .await
1113 .unwrap();
1114
1115 let timeout_result =
1116 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1117 assert!(
1118 timeout_result.is_err(),
1119 "foreign device advert must not emit an event, but one was received"
1120 );
1121 }
1122
1123 #[tokio::test]
1126 #[allow(clippy::unwrap_used)]
1127 async fn stale_gsn_broadcast_ignored() {
1128 use tokio_stream::StreamExt as _;
1129 let (mut h, gatt) = handle_with_db().await;
1130
1131 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1132 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1133
1134 let mut pt = Vec::new();
1137 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);
1141
1142 let mut mfg = vec![0x11u8, 0x00];
1143 mfg.extend_from_slice(&aid_bytes);
1144 mfg.extend_from_slice(&sealed);
1145
1146 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1147 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
1148 .await
1149 .unwrap();
1150 let mut events = h.events();
1151
1152 gatt.advert_sender()
1154 .send(crate::gatt::RawAdvert {
1155 manufacturer_data: mfg.clone(),
1156 })
1157 .await
1158 .unwrap();
1159
1160 let ev = events.next().await.unwrap();
1161 assert_eq!(ev.iid, 11);
1162 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
1163
1164 gatt.advert_sender()
1166 .send(crate::gatt::RawAdvert {
1167 manufacturer_data: mfg,
1168 })
1169 .await
1170 .unwrap();
1171
1172 let timeout_result =
1173 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1174 assert!(
1175 timeout_result.is_err(),
1176 "duplicate GSN 5 broadcast must not emit a second event"
1177 );
1178 }
1179
1180 #[tokio::test]
1183 #[allow(clippy::unwrap_used)]
1184 async fn wrong_broadcast_key_ignored() {
1185 use tokio_stream::StreamExt as _;
1186 let (mut h, gatt) = handle_with_db().await;
1188
1189 let wrong_key = hap_crypto::BroadcastKey::from_bytes([0xFF; 32]);
1191 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1192
1193 let mut pt = Vec::new();
1194 pt.extend_from_slice(&1u16.to_le_bytes());
1195 pt.extend_from_slice(&11u16.to_le_bytes());
1196 pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]);
1197 let sealed = wrong_key.seal(1, &pt, &aid_bytes);
1198
1199 let mut mfg = vec![0x11u8, 0x00];
1200 mfg.extend_from_slice(&aid_bytes);
1201 mfg.extend_from_slice(&sealed);
1202
1203 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1204 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
1205 .await
1206 .unwrap();
1207 let mut events = h.events();
1208
1209 gatt.advert_sender()
1210 .send(crate::gatt::RawAdvert {
1211 manufacturer_data: mfg,
1212 })
1213 .await
1214 .unwrap();
1215
1216 let timeout_result =
1217 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1218 assert!(
1219 timeout_result.is_err(),
1220 "wrong-key broadcast must not emit any event (all candidate opens fail)"
1221 );
1222 }
1223
1224 #[tokio::test]
1228 #[allow(clippy::unwrap_used)]
1229 async fn malformed_0x11_advert_ignored() {
1230 use tokio_stream::StreamExt as _;
1231 let (mut h, gatt) = handle_with_db().await;
1232
1233 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1234 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![])
1235 .await
1236 .unwrap();
1237 let mut events = h.events();
1238
1239 let manufacturer_data = vec![0x11, 0x00, 1, 2, 3, 4, 5, 6, 0xAA, 0xBB];
1241 gatt.advert_sender()
1242 .send(crate::gatt::RawAdvert { manufacturer_data })
1243 .await
1244 .unwrap();
1245
1246 let timeout_result =
1247 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1248 assert!(
1249 timeout_result.is_err(),
1250 "malformed (too-short payload) 0x11 advert must not emit any event"
1251 );
1252 }
1253
1254 #[tokio::test]
1258 #[allow(clippy::unwrap_used)]
1259 async fn broadcast_value_self_inconsistent_gsn_ignored() {
1260 use tokio_stream::StreamExt as _;
1261 let (mut h, gatt) = handle_with_db().await;
1262
1263 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1264 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1265
1266 let mut pt = Vec::new();
1270 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];
1276 mfg.extend_from_slice(&aid_bytes);
1277 mfg.extend_from_slice(&sealed);
1278
1279 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1280 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
1281 .await
1282 .unwrap();
1283 let mut events = h.events();
1284
1285 gatt.advert_sender()
1286 .send(crate::gatt::RawAdvert {
1287 manufacturer_data: mfg,
1288 })
1289 .await
1290 .unwrap();
1291
1292 let timeout_result =
1293 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1294 assert!(
1295 timeout_result.is_err(),
1296 "self-inconsistent GSN (embedded 3 != nonce 7) must not emit any event"
1297 );
1298 }
1299
1300 #[tokio::test]
1301 #[allow(clippy::unwrap_used)]
1302 async fn read_after_reconnect_re_verifies_before_using_session() {
1303 let (mut h, gatt) = handle_with_db().await;
1304
1305 let mut plain = vec![0x02, 0x01, 0x00];
1308 let vbody = crate::pdu::encode_value_param(&[0x01]);
1309 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1310 plain.extend_from_slice(&vbody);
1311 let sealed =
1312 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1313 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1314
1315 gatt.bump_generation();
1320 let err = h.read(1, 11).await.unwrap_err();
1321 assert!(
1322 !matches!(err, BleError::CharacteristicNotFound { .. }),
1323 "expected a verify/transport error from the re-verify attempt, got {err:?}"
1324 );
1325 }
1326
1327 #[tokio::test]
1328 async fn dedup_emits_once_per_gsn_and_never_downgrades() {
1329 let emitted = Mutex::new(HashMap::new());
1330 assert!(dedup_should_emit(&emitted, 11, 9).await);
1332 assert!(!dedup_should_emit(&emitted, 11, 9).await);
1334 assert!(dedup_should_emit(&emitted, 11, 10).await);
1336 assert!(dedup_should_emit(&emitted, 11, 9).await);
1339 assert!(!dedup_should_emit(&emitted, 11, 10).await);
1341 assert!(dedup_should_emit(&emitted, 12, 65535).await);
1343 assert!(dedup_should_emit(&emitted, 12, 1).await);
1344 assert!(!dedup_should_emit(&emitted, 12, 1).await);
1345 }
1346}