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 read_char_raw(
164 gatt: &dyn GattConnection,
165 secure: &Mutex<Secure>,
166 reviver: &Reviver,
167 uuid: &str,
168 iid: u64,
169 frag_size: usize,
170) -> Result<Vec<u8>> {
171 let iid16 = u16::try_from(iid).map_err(|_| BleError::CharacteristicNotFound { aid: 0, iid })?;
172 let mut s = secure.lock().await;
173 let mut attempts = 0;
174 loop {
175 revive_if_stale(gatt, &mut s, reviver).await?;
176 s.tid = s.tid.wrapping_add(1);
177 let tid = s.tid;
178 match pdu::request_secure(
179 gatt,
180 &mut s.session,
181 uuid,
182 OpCode::CharacteristicRead,
183 tid,
184 iid16,
185 &[],
186 frag_size,
187 )
188 .await
189 {
190 Ok(resp) => return pdu::value_param(&resp.body),
191 Err(e) => {
195 attempts += 1;
196 if attempts < MAX_REVIVE_RETRIES && gatt.generation().await > s.generation {
197 continue;
198 }
199 return Err(e);
200 }
201 }
202 }
203}
204
205pub struct BleAccessory {
209 gatt: Arc<dyn GattConnection>,
210 secure: Arc<Mutex<Secure>>,
211 reviver: Arc<Reviver>,
212 pairings: (String, u16),
214 frag_size: usize,
215 accessories: Vec<Accessory>,
216 chars: HashMap<(u64, u64), (String, CharFormat)>,
218 events_tx: tokio::sync::broadcast::Sender<CharacteristicEvent>,
219 tasks: Vec<tokio::task::JoinHandle<()>>,
221 last_gsn: Arc<Mutex<u16>>,
223 emitted: Arc<Mutex<HashMap<u64, u16>>>,
226 broadcast_key: hap_crypto::BroadcastKey,
228}
229
230impl Drop for BleAccessory {
231 fn drop(&mut self) {
232 for task in &self.tasks {
233 task.abort();
234 }
235 }
236}
237
238impl BleAccessory {
239 pub(crate) fn new(
246 gatt: Arc<dyn GattConnection>,
247 ctx: SecureContext,
248 frag_size: usize,
249 gatt_services: &[GattService],
250 accessories: Vec<Accessory>,
251 ) -> Self {
252 let (events_tx, _) = tokio::sync::broadcast::channel(64);
253 let mut uuid_by_iid: HashMap<u64, String> = HashMap::new();
257 for gs in gatt_services {
258 for gc in &gs.characteristics {
259 uuid_by_iid.insert(u64::from(gc.iid), gc.uuid.clone());
260 }
261 }
262 let mut chars = HashMap::new();
263 for acc in &accessories {
264 for svc in &acc.services {
265 for ch in &svc.characteristics {
266 if let Some(uuid) = uuid_by_iid.get(&ch.iid) {
267 chars.insert((acc.aid, ch.iid), (uuid.clone(), ch.format));
268 }
269 }
270 }
271 }
272 Self {
273 gatt,
274 secure: Arc::new(Mutex::new(Secure {
275 session: ctx.session,
276 tid: 0,
277 generation: ctx.session_generation,
278 })),
279 reviver: Arc::new(Reviver {
280 keypair: ctx.keypair,
281 pairing: ctx.pairing,
282 verify_char: ctx.verify_char,
283 verify_iid: ctx.verify_iid,
284 frag_size,
285 }),
286 pairings: (ctx.pairings_char, ctx.pairings_iid),
287 frag_size,
288 accessories,
289 chars,
290 events_tx,
291 tasks: Vec::new(),
292 last_gsn: Arc::new(Mutex::new(ctx.initial_gsn)),
293 emitted: Arc::new(Mutex::new(HashMap::new())),
294 broadcast_key: ctx.broadcast_key,
295 }
296 }
297
298 pub fn accessories(&self) -> &[Accessory] {
300 &self.accessories
301 }
302
303 pub async fn broadcast_state(&self) -> BleBroadcastState {
306 BleBroadcastState {
307 key: self.broadcast_key.clone(),
308 gsn: *self.last_gsn.lock().await,
309 }
310 }
311
312 #[allow(clippy::needless_pass_by_value)]
320 pub fn find(&self, svc: ServiceType, chr: CharacteristicType) -> Result<(u64, u64)> {
321 for acc in &self.accessories {
322 for service in &acc.services {
323 if service.service_type == svc {
324 for ch in &service.characteristics {
325 if ch.char_type == chr {
326 return Ok((acc.aid, ch.iid));
327 }
328 }
329 }
330 }
331 }
332 Err(BleError::CharacteristicNotFound { aid: 0, iid: 0 })
333 }
334
335 pub async fn read(&mut self, aid: u64, iid: u64) -> Result<CharValue> {
340 let (uuid, format) = self
341 .chars
342 .get(&(aid, iid))
343 .cloned()
344 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
345 let raw = read_char_raw(
346 self.gatt.as_ref(),
347 &self.secure,
348 &self.reviver,
349 &uuid,
350 iid,
351 self.frag_size,
352 )
353 .await?;
354 db::decode_value(format, &raw)
355 }
356
357 pub async fn remove_pairing(&mut self, controller_id: &str) -> Result<()> {
378 let (uuid, iid) = self.pairings.clone();
379 let removing_self = controller_id == self.reviver.keypair.id;
380 let tlv = encode_remove_pairing(controller_id);
381 let body = pdu::encode_write_body(&tlv);
382 let mut s = self.secure.lock().await;
383 revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
384 s.tid = s.tid.wrapping_add(1);
385 let tid = s.tid;
386 let result = pdu::request_secure(
387 self.gatt.as_ref(),
388 &mut s.session,
389 &uuid,
390 OpCode::CharacteristicWrite,
391 tid,
392 iid,
393 &body,
394 self.frag_size,
395 )
396 .await;
397 match result {
398 Ok(resp) if resp.status != 0 => Err(BleError::PairingRejected(resp.status)),
399 Ok(resp) => expect_remove_m2(&pdu::value_param(&resp.body)?),
400 Err(BleError::Disconnected | BleError::Crypto(_)) if removing_self => Ok(()),
404 Err(e) => Err(e),
405 }
406 }
407
408 pub async fn enable_broadcasts(&mut self, iids: &[u64]) -> Result<()> {
419 let mut s = self.secure.lock().await;
420 for &iid in iids {
421 let Some((uuid, _)) = self.chars.get(&(1, iid)).cloned() else {
422 continue;
423 };
424 let Ok(iid16) = u16::try_from(iid) else {
425 continue;
426 };
427 revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
428 s.tid = s.tid.wrapping_add(1);
429 let tid = s.tid;
430 let _ = pdu::request_secure(
431 self.gatt.as_ref(),
432 &mut s.session,
433 &uuid,
434 OpCode::CharacteristicConfig,
435 tid,
436 iid16,
437 &ENABLE_BROADCAST_BODY,
438 self.frag_size,
439 )
440 .await;
441 }
442 Ok(())
443 }
444
445 pub async fn subscribe(&mut self, aid: u64, iid: u64) -> Result<()> {
458 let (uuid, format) = self
459 .chars
460 .get(&(aid, iid))
461 .cloned()
462 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
463 let mut rx = self.gatt.subscribe(&uuid).await?;
464 let tx = self.events_tx.clone();
465 let gatt = self.gatt.clone();
466 let secure = self.secure.clone();
467 let reviver = self.reviver.clone();
468 let frag_size = self.frag_size;
469 let task = tokio::spawn(async move {
470 while rx.recv().await.is_some() {
472 if let Ok(raw) =
473 read_char_raw(gatt.as_ref(), &secure, &reviver, &uuid, iid, frag_size).await
474 {
475 if let Ok(value) = db::decode_value(format, &raw) {
476 let _ = tx.send(CharacteristicEvent { aid, iid, value });
477 }
478 }
479 }
480 });
481 self.tasks.push(task);
482 Ok(())
483 }
484
485 #[allow(clippy::too_many_lines)]
500 pub async fn watch_sleepy_events(
501 &mut self,
502 advert_source: Arc<dyn crate::gatt::AdvertSource>,
503 device_id: [u8; 6],
504 poll_iids: Vec<(u64, u64)>,
505 ) -> Result<()> {
506 let mut targets = Vec::new();
509 for (aid, iid) in poll_iids {
510 if let Some((uuid, format)) = self.chars.get(&(aid, iid)).cloned() {
511 targets.push((aid, iid, uuid, format));
512 }
513 }
514 let formats: std::collections::HashMap<u64, CharFormat> = self
516 .chars
517 .iter()
518 .map(|((_, iid), (_, f))| (*iid, *f))
519 .collect();
520 let broadcast_key = self.broadcast_key.clone();
521
522 let mut adverts = advert_source.watch_adverts().await?;
523 let tx = self.events_tx.clone();
524 let gatt = self.gatt.clone();
525 let secure = self.secure.clone();
526 let reviver = self.reviver.clone();
527 let frag = self.frag_size;
528 let last_gsn = self.last_gsn.clone();
529 let emitted = self.emitted.clone();
530 let task = tokio::spawn(async move {
531 while let Some(raw) = adverts.recv().await {
532 match crate::advert::HapAdvert::parse(&raw.manufacturer_data) {
533 Some(crate::advert::HapAdvert::Regular {
534 device_id: d, gsn, ..
535 }) => {
536 if d != device_id {
537 continue;
538 }
539 {
540 let mut lg = last_gsn.lock().await;
541 if !gsn_is_newer(gsn, *lg) {
542 continue;
543 }
544 *lg = gsn;
545 }
546 for (aid, iid, uuid, format) in &targets {
547 if let Ok(raw_val) =
548 read_char_raw(gatt.as_ref(), &secure, &reviver, uuid, *iid, frag)
549 .await
550 {
551 if let Ok(value) = db::decode_value(*format, &raw_val) {
552 let mut e = emitted.lock().await;
553 if e.get(iid).copied() != Some(gsn) {
554 e.insert(*iid, gsn);
555 drop(e);
556 let _ = tx.send(CharacteristicEvent {
557 aid: *aid,
558 iid: *iid,
559 value,
560 });
561 }
562 }
563 }
564 }
565 }
566 Some(crate::advert::HapAdvert::EncryptedNotification {
567 advertising_id,
568 payload,
569 }) => {
570 if advertising_id != device_id {
571 continue;
572 }
573 let start = *last_gsn.lock().await;
574 let candidates = std::iter::once(start.wrapping_add(1))
577 .chain(std::iter::once(start))
578 .chain((2..=100u16).map(|d| start.wrapping_add(d)));
579 for gsn in candidates {
580 let Ok(pt) = broadcast_key.open(gsn, &payload, &advertising_id) else {
581 continue;
582 };
583 if pt.len() < 12 {
584 continue;
585 }
586 if u16::from_le_bytes([pt[0], pt[1]]) != gsn {
587 continue;
588 }
589 if !gsn_is_newer(gsn, start) {
591 break;
592 }
593 let iid = u64::from(u16::from_le_bytes([pt[2], pt[3]]));
594 {
597 let mut lg = last_gsn.lock().await;
598 *lg = gsn;
599 }
600 let Some(format) = formats.get(&iid).copied() else {
601 break;
602 };
603 if let Ok(value) = db::decode_value(format, &pt[4..12]) {
604 let mut e = emitted.lock().await;
605 if e.get(&iid).copied() != Some(gsn) {
606 e.insert(iid, gsn);
607 drop(e);
608 let _ = tx.send(CharacteristicEvent { aid: 1, iid, value });
609 }
610 }
611 break;
612 }
613 }
614 _ => {}
615 }
616 }
617 });
618 self.tasks.push(task);
619 Ok(())
620 }
621
622 pub fn events(&self) -> impl tokio_stream::Stream<Item = CharacteristicEvent> {
625 tokio_stream::wrappers::BroadcastStream::new(self.events_tx.subscribe())
626 .filter_map(std::result::Result::ok)
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use crate::gatt::{GattCharacteristic, GattService, MockGatt};
634 use hap_crypto::SessionKeys;
635
636 #[allow(clippy::unwrap_used)]
637 fn on_le() -> Vec<u8> {
638 let hex = "00000025000010008000".to_string() + "0026bb765291";
639 let mut b: Vec<u8> = (0..16)
640 .map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap())
641 .collect();
642 b.reverse();
643 b
644 }
645
646 fn on_service() -> GattService {
647 GattService {
648 uuid: "00000043-0000-1000-8000-0026bb765291".into(), iid: 10,
650 characteristics: vec![GattCharacteristic {
651 uuid: "00000025-0000-1000-8000-0026bb765291".into(), iid: 11,
653 }],
654 }
655 }
656
657 #[allow(clippy::unwrap_used)]
658 fn sig_resp() -> Vec<u8> {
659 let mut body = Vec::new();
660 let mut w = hap_tlv8::Tlv8Writer::new(&mut body);
661 w.push(crate::pdu::param::CHAR_TYPE, &on_le());
662 w.push(crate::pdu::param::PROPERTIES, &0x0083u16.to_le_bytes()); w.push(
664 crate::pdu::param::PRESENTATION_FORMAT,
665 &[0x01, 0, 0, 0, 0, 0, 0],
666 );
667 let mut resp = vec![0x02, 0x01, 0x00];
668 resp.extend_from_slice(&u16::try_from(body.len()).unwrap().to_le_bytes());
669 resp.extend_from_slice(&body);
670 resp
671 }
672
673 #[allow(clippy::unwrap_used)]
674 async fn handle_with_db() -> (BleAccessory, Arc<MockGatt>) {
675 let gatt = Arc::new(MockGatt::new().with_services(vec![on_service()]));
676 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sig_resp());
677 let session = BleSession::new(SessionKeys {
678 read_key: [0; 32],
679 write_key: [0; 32],
680 });
681 let services = gatt.enumerate().await.unwrap();
682 let accessories = crate::db::build_db(gatt.as_ref(), &services, 512)
683 .await
684 .unwrap();
685 let ctx = SecureContext {
686 session,
687 session_generation: 0,
688 keypair: ControllerKeypair::generate("test-controller".into()),
689 pairing: AccessoryPairing {
690 pairing_id: "AE:EC:86:C0:BF:D7".into(),
691 ltpk: [0; 32],
692 },
693 verify_char: "0000004e-0000-1000-8000-0026bb765291".into(),
694 verify_iid: 1,
695 pairings_char: "00000050-0000-1000-8000-0026bb765291".into(),
696 pairings_iid: 2,
697 broadcast_key: hap_crypto::BroadcastKey::from_bytes([0u8; 32]),
698 initial_gsn: 0,
699 };
700 let h = BleAccessory::new(gatt.clone(), ctx, 512, &services, accessories);
701 (h, gatt)
702 }
703
704 #[tokio::test]
705 #[allow(clippy::unwrap_used)]
706 async fn find_locates_characteristic() {
707 let (h, _g) = handle_with_db().await;
708 let (aid, iid) = h
709 .find(ServiceType::LightBulb, CharacteristicType::On)
710 .unwrap();
711 assert_eq!((aid, iid), (1, 11));
712 }
713
714 #[tokio::test]
715 #[allow(clippy::unwrap_used)]
716 async fn find_missing_errors() {
717 let (h, _g) = handle_with_db().await;
718 let err = h
719 .find(ServiceType::LightBulb, CharacteristicType::Brightness)
720 .unwrap_err();
721 assert!(matches!(err, BleError::CharacteristicNotFound { .. }));
722 }
723
724 #[test]
725 fn encode_remove_pairing_matches_hap_layout() {
726 let tlv = encode_remove_pairing("c2");
728 assert_eq!(
729 tlv,
730 vec![0x06, 0x01, 0x01, 0x00, 0x01, 0x04, 0x01, 0x02, b'c', b'2']
731 );
732 }
733
734 #[test]
735 fn expect_remove_m2_accepts_m2_and_rejects_error() {
736 assert!(expect_remove_m2(&[0x06, 0x01, 0x02]).is_ok());
737 assert!(matches!(
739 expect_remove_m2(&[0x07, 0x01, 0x02]),
740 Err(BleError::PairingRejected(2))
741 ));
742 assert!(matches!(
744 expect_remove_m2(&[0x06, 0x01, 0x01]),
745 Err(BleError::MalformedPdu(_))
746 ));
747 }
748
749 #[tokio::test]
750 #[allow(clippy::unwrap_used)]
751 async fn remove_pairing_writes_request_and_accepts_m2() {
752 let (mut h, gatt) = handle_with_db().await;
753
754 let m2 = vec![0x06, 0x01, 0x02];
757 let vbody = crate::pdu::encode_value_param(&m2);
758 let mut plain = vec![0x02, 0x01, 0x00];
759 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
760 plain.extend_from_slice(&vbody);
761 let sealed =
762 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
763 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", sealed);
764
765 h.remove_pairing("AE:EC:86:C0:BF:D7").await.unwrap();
766 }
767
768 #[tokio::test]
769 #[allow(clippy::unwrap_used)]
770 async fn remove_own_pairing_tolerates_session_teardown() {
771 let (mut h, gatt) = handle_with_db().await;
773 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
777 h.remove_pairing("test-controller").await.unwrap();
778 }
779
780 #[tokio::test]
781 #[allow(clippy::unwrap_used)]
782 async fn remove_other_pairing_propagates_teardown_error() {
783 let (mut h, gatt) = handle_with_db().await;
786 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
787 let err = h.remove_pairing("some-other-controller").await.unwrap_err();
788 assert!(matches!(err, BleError::Crypto(_)));
789 }
790
791 #[tokio::test]
792 #[allow(clippy::unwrap_used)]
793 async fn subscribe_then_event_decodes_value() {
794 use tokio_stream::StreamExt as _;
795 let (mut h, gatt) = handle_with_db().await;
796
797 let mut plain = vec![0x02, 0x01, 0x00];
801 let vbody = crate::pdu::encode_value_param(&[0x01]); plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
803 plain.extend_from_slice(&vbody);
804 let sealed =
805 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
806 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
807
808 h.subscribe(1, 11).await.unwrap();
809 let mut events = h.events();
810
811 gatt.notifier("00000025-0000-1000-8000-0026bb765291")
813 .unwrap()
814 .send(Vec::new())
815 .await
816 .unwrap();
817
818 let ev = events.next().await.unwrap();
819 assert_eq!(ev.iid, 11);
820 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
821 }
822
823 #[tokio::test]
824 #[allow(clippy::unwrap_used)]
825 async fn gsn_bump_triggers_disconnected_event_read() {
826 use tokio_stream::StreamExt as _;
827 let (mut h, gatt) = handle_with_db().await;
828
829 let mut plain = vec![0x02, 0x01, 0x00];
832 let vbody = crate::pdu::encode_value_param(&[0x01]);
833 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
834 plain.extend_from_slice(&vbody);
835 let sealed =
836 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
837 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
838
839 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
840 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
841 .await
842 .unwrap();
843 let mut events = h.events();
844
845 gatt.advert_sender()
847 .send(crate::gatt::RawAdvert {
848 manufacturer_data: vec![
849 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
850 ],
851 })
852 .await
853 .unwrap();
854
855 let ev = events.next().await.unwrap();
856 assert_eq!(ev.iid, 11);
857 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
858 }
859
860 #[tokio::test]
861 #[allow(clippy::unwrap_used)]
862 async fn encrypted_broadcast_0x11_decrypts_and_emits_event() {
863 use tokio_stream::StreamExt as _;
864 let (mut h, gatt) = handle_with_db().await;
866
867 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
870 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
871 let mut pt = Vec::new();
872 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);
876
877 let mut mfg = vec![0x11u8, 0x00];
879 mfg.extend_from_slice(&aid_bytes);
880 mfg.extend_from_slice(&sealed);
881
882 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
883 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
885 .await
886 .unwrap();
887 let mut events = h.events();
888
889 gatt.advert_sender()
890 .send(crate::gatt::RawAdvert {
891 manufacturer_data: mfg,
892 })
893 .await
894 .unwrap();
895
896 let ev = events.next().await.unwrap();
897 assert_eq!(ev.aid, 1);
898 assert_eq!(ev.iid, 11);
899 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
900 }
901
902 #[test]
903 fn gsn_is_newer_handles_wraparound() {
904 assert!(gsn_is_newer(6, 5));
905 assert!(!gsn_is_newer(5, 5));
906 assert!(!gsn_is_newer(4, 5));
907 assert!(gsn_is_newer(1, 65535)); assert!(!gsn_is_newer(65535, 1)); }
910
911 #[tokio::test]
912 #[allow(clippy::unwrap_used)]
913 async fn same_change_via_poll_and_broadcast_emits_once() {
914 use tokio_stream::StreamExt as _;
915 let (mut h, gatt) = handle_with_db().await;
916
917 let mut plain = vec![0x02, 0x01, 0x00];
920 let vbody = crate::pdu::encode_value_param(&[0x01]);
921 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
922 plain.extend_from_slice(&vbody);
923 let sealed =
924 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
925 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
926
927 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
928 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
929 .await
930 .unwrap();
931 let mut events = h.events();
932
933 gatt.advert_sender()
935 .send(crate::gatt::RawAdvert {
936 manufacturer_data: vec![
937 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
938 ],
939 })
940 .await
941 .unwrap();
942
943 let ev = events.next().await.unwrap();
945 assert_eq!(ev.iid, 11);
946 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
947
948 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
950 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
951 let mut pt = Vec::new();
952 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);
956
957 let mut mfg = vec![0x11u8, 0x00];
958 mfg.extend_from_slice(&aid_bytes);
959 mfg.extend_from_slice(&sealed_bc);
960
961 gatt.advert_sender()
962 .send(crate::gatt::RawAdvert {
963 manufacturer_data: mfg,
964 })
965 .await
966 .unwrap();
967
968 let timeout_result =
970 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
971 assert!(
972 timeout_result.is_err(),
973 "expected dedup to suppress the duplicate 0x11 broadcast event, but got one"
974 );
975 }
976
977 #[tokio::test]
982 #[allow(clippy::unwrap_used)]
983 async fn foreign_device_advert_ignored() {
984 use tokio_stream::StreamExt as _;
985 let (mut h, gatt) = handle_with_db().await;
986
987 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
988 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
990 .await
991 .unwrap();
992 let mut events = h.events();
993
994 gatt.advert_sender()
996 .send(crate::gatt::RawAdvert {
997 manufacturer_data: vec![
998 0x06, 0x21, 0x01, 9, 9, 9, 9, 9, 9, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
999 ],
1000 })
1001 .await
1002 .unwrap();
1003
1004 let timeout_result =
1005 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1006 assert!(
1007 timeout_result.is_err(),
1008 "foreign device advert must not emit an event, but one was received"
1009 );
1010 }
1011
1012 #[tokio::test]
1015 #[allow(clippy::unwrap_used)]
1016 async fn stale_gsn_broadcast_ignored() {
1017 use tokio_stream::StreamExt as _;
1018 let (mut h, gatt) = handle_with_db().await;
1019
1020 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1021 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1022
1023 let mut pt = Vec::new();
1026 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);
1030
1031 let mut mfg = vec![0x11u8, 0x00];
1032 mfg.extend_from_slice(&aid_bytes);
1033 mfg.extend_from_slice(&sealed);
1034
1035 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1036 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
1037 .await
1038 .unwrap();
1039 let mut events = h.events();
1040
1041 gatt.advert_sender()
1043 .send(crate::gatt::RawAdvert {
1044 manufacturer_data: mfg.clone(),
1045 })
1046 .await
1047 .unwrap();
1048
1049 let ev = events.next().await.unwrap();
1050 assert_eq!(ev.iid, 11);
1051 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
1052
1053 gatt.advert_sender()
1055 .send(crate::gatt::RawAdvert {
1056 manufacturer_data: mfg,
1057 })
1058 .await
1059 .unwrap();
1060
1061 let timeout_result =
1062 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1063 assert!(
1064 timeout_result.is_err(),
1065 "duplicate GSN 5 broadcast must not emit a second event"
1066 );
1067 }
1068
1069 #[tokio::test]
1072 #[allow(clippy::unwrap_used)]
1073 async fn wrong_broadcast_key_ignored() {
1074 use tokio_stream::StreamExt as _;
1075 let (mut h, gatt) = handle_with_db().await;
1077
1078 let wrong_key = hap_crypto::BroadcastKey::from_bytes([0xFF; 32]);
1080 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1081
1082 let mut pt = Vec::new();
1083 pt.extend_from_slice(&1u16.to_le_bytes());
1084 pt.extend_from_slice(&11u16.to_le_bytes());
1085 pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]);
1086 let sealed = wrong_key.seal(1, &pt, &aid_bytes);
1087
1088 let mut mfg = vec![0x11u8, 0x00];
1089 mfg.extend_from_slice(&aid_bytes);
1090 mfg.extend_from_slice(&sealed);
1091
1092 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1093 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
1094 .await
1095 .unwrap();
1096 let mut events = h.events();
1097
1098 gatt.advert_sender()
1099 .send(crate::gatt::RawAdvert {
1100 manufacturer_data: mfg,
1101 })
1102 .await
1103 .unwrap();
1104
1105 let timeout_result =
1106 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1107 assert!(
1108 timeout_result.is_err(),
1109 "wrong-key broadcast must not emit any event (all candidate opens fail)"
1110 );
1111 }
1112
1113 #[tokio::test]
1117 #[allow(clippy::unwrap_used)]
1118 async fn malformed_0x11_advert_ignored() {
1119 use tokio_stream::StreamExt as _;
1120 let (mut h, gatt) = handle_with_db().await;
1121
1122 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1123 h.watch_sleepy_events(advert_source, [1, 2, 3, 4, 5, 6], vec![])
1124 .await
1125 .unwrap();
1126 let mut events = h.events();
1127
1128 let manufacturer_data = vec![0x11, 0x00, 1, 2, 3, 4, 5, 6, 0xAA, 0xBB];
1130 gatt.advert_sender()
1131 .send(crate::gatt::RawAdvert { manufacturer_data })
1132 .await
1133 .unwrap();
1134
1135 let timeout_result =
1136 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1137 assert!(
1138 timeout_result.is_err(),
1139 "malformed (too-short payload) 0x11 advert must not emit any event"
1140 );
1141 }
1142
1143 #[tokio::test]
1147 #[allow(clippy::unwrap_used)]
1148 async fn broadcast_value_self_inconsistent_gsn_ignored() {
1149 use tokio_stream::StreamExt as _;
1150 let (mut h, gatt) = handle_with_db().await;
1151
1152 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1153 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1154
1155 let mut pt = Vec::new();
1159 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];
1165 mfg.extend_from_slice(&aid_bytes);
1166 mfg.extend_from_slice(&sealed);
1167
1168 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1169 h.watch_sleepy_events(advert_source, aid_bytes, vec![])
1170 .await
1171 .unwrap();
1172 let mut events = h.events();
1173
1174 gatt.advert_sender()
1175 .send(crate::gatt::RawAdvert {
1176 manufacturer_data: mfg,
1177 })
1178 .await
1179 .unwrap();
1180
1181 let timeout_result =
1182 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1183 assert!(
1184 timeout_result.is_err(),
1185 "self-inconsistent GSN (embedded 3 != nonce 7) must not emit any event"
1186 );
1187 }
1188
1189 #[tokio::test]
1190 #[allow(clippy::unwrap_used)]
1191 async fn read_after_reconnect_re_verifies_before_using_session() {
1192 let (mut h, gatt) = handle_with_db().await;
1193
1194 let mut plain = vec![0x02, 0x01, 0x00];
1197 let vbody = crate::pdu::encode_value_param(&[0x01]);
1198 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1199 plain.extend_from_slice(&vbody);
1200 let sealed =
1201 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1202 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1203
1204 gatt.bump_generation();
1209 let err = h.read(1, 11).await.unwrap_err();
1210 assert!(
1211 !matches!(err, BleError::CharacteristicNotFound { .. }),
1212 "expected a verify/transport error from the re-verify attempt, got {err:?}"
1213 );
1214 }
1215}