hap_ble/controller.rs
1//! The BLE controller entry point: own the controller identity, scan, pair, and
2//! connect.
3
4use crate::accessory::BleAccessory;
5use crate::broadcast_state::BleBroadcastState;
6use crate::discovery::DiscoveredBleAccessory;
7use crate::error::Result;
8use crate::pairing;
9use hap_crypto::AccessoryPairing;
10use hap_crypto::ControllerKeypair;
11use std::sync::Arc;
12
13/// The HAP Pairing-Service characteristic UUIDs (HAP-defined, fixed).
14const PAIR_SETUP_CHAR: &str = "0000004c-0000-1000-8000-0026bb765291";
15const PAIR_VERIFY_CHAR: &str = "0000004e-0000-1000-8000-0026bb765291";
16const PAIRINGS_CHAR: &str = "00000050-0000-1000-8000-0026bb765291";
17/// The HAP Service-Signature characteristic (one appears in *every* service).
18/// The generate-broadcast-key request must target the one in the Protocol
19/// Information service specifically (see `protocol_info_signature_iid`).
20const SERVICE_SIGNATURE_CHAR: &str = "000000a5-0000-1000-8000-0026bb765291";
21/// The HAP Protocol Information service — its Service-Signature characteristic is
22/// where the Protocol-Configuration "generate broadcast key" request is written.
23const PROTOCOL_INFO_SERVICE: &str = "000000a2-0000-1000-8000-0026bb765291";
24/// Protocol-Configuration TLV body that asks the accessory to generate a
25/// broadcast encryption key (type `GenerateBroadcastEncryptionKey` = 0x01, len 0).
26const GENERATE_BROADCAST_KEY_BODY: [u8; 2] = [0x01, 0x00];
27
28/// The result of a successful BLE pairing.
29pub struct Paired {
30 /// The connected accessory handle.
31 pub accessory: BleAccessory,
32 /// The long-term pairing — persist this.
33 pub pairing: AccessoryPairing,
34 /// Broadcast material — persist this to resume broadcasts across restarts.
35 pub broadcast: BleBroadcastState,
36}
37
38/// A BLE HAP controller: holds the long-term controller identity used for
39/// pairing and verification.
40pub struct BleController {
41 keypair: ControllerKeypair,
42}
43
44impl BleController {
45 /// Create a controller from a long-term identity.
46 pub fn new(keypair: ControllerKeypair) -> Self {
47 Self { keypair }
48 }
49
50 /// Generate a fresh controller identity with the given pairing id.
51 pub fn generate(id: String) -> Self {
52 Self {
53 keypair: ControllerKeypair::generate(id),
54 }
55 }
56
57 /// The controller's pairing identity.
58 pub fn keypair(&self) -> &ControllerKeypair {
59 &self.keypair
60 }
61
62 /// Pair with a discovered accessory: run Pair Setup, then Pair Verify, then
63 /// build the attribute database. Returns a [`Paired`] containing the ready
64 /// accessory handle, the persisted [`AccessoryPairing`], and initial
65 /// broadcast material.
66 ///
67 /// # Errors
68 /// Propagates connection, pairing, and model errors.
69 pub async fn pair(
70 &self,
71 gatt: Arc<dyn crate::gatt::GattConnection>,
72 _accessory: &DiscoveredBleAccessory,
73 setup_code: &str,
74 ) -> Result<Paired> {
75 // Pair first (reading only the Pair-Setup characteristic's iid, one
76 // descriptor read) — the long database sweep must not run before the
77 // stateful Pair Setup handshake, which can't survive a mid-handshake
78 // reconnect.
79 let frag = gatt.max_write().await;
80 let setup_iid = gatt.instance_id(PAIR_SETUP_CHAR).await?;
81 let pairing = pairing::pair_setup(
82 gatt.as_ref(),
83 PAIR_SETUP_CHAR,
84 setup_iid,
85 setup_code,
86 self.keypair.clone(),
87 frag,
88 )
89 .await?;
90 let accessory = self.verify_and_build(gatt, &pairing, 0).await?;
91 let broadcast = accessory.broadcast_state().await;
92 Ok(Paired {
93 accessory,
94 pairing,
95 broadcast,
96 })
97 }
98
99 /// Connect to an already-paired accessory via Pair Verify, then build the DB.
100 ///
101 /// `broadcast` is optional previously-persisted broadcast state. Its `gsn`
102 /// seeds `last_gsn` so the accessory handle does not re-emit already-seen
103 /// events after a restart. The key in `broadcast` is the previously-persisted
104 /// one — Pair Verify derives a fresh per-session broadcast key, which becomes
105 /// the accessory's current key.
106 ///
107 /// # NOTE
108 /// Decrypting pre-connect broadcasts with the persisted key (vs the fresh
109 /// per-session key derived here) is a documented follow-up task — the fresh
110 /// key covers forward broadcasts.
111 ///
112 /// # Errors
113 /// Propagates connection, verify, and model errors.
114 pub async fn connect(
115 &self,
116 gatt: Arc<dyn crate::gatt::GattConnection>,
117 pairing: &AccessoryPairing,
118 broadcast: Option<BleBroadcastState>,
119 ) -> Result<BleAccessory> {
120 let initial_gsn = broadcast.as_ref().map_or(0, |b| b.gsn);
121 self.verify_and_build(gatt, pairing, initial_gsn).await
122 }
123
124 async fn verify_and_build(
125 &self,
126 gatt: Arc<dyn crate::gatt::GattConnection>,
127 pairing: &AccessoryPairing,
128 initial_gsn: u16,
129 ) -> Result<BleAccessory> {
130 // After pairing, walk the full tree (resilient) for iids, then build the
131 // typed database from UNENCRYPTED characteristic-signature reads — HAP
132 // reads the database structure after Pair Setup but before Pair Verify
133 // (no secure session yet). The resilient GattConnection reconnects +
134 // resumes through the accessory's periodic disconnects.
135 let frag = gatt.max_write().await;
136 let services = gatt.enumerate().await?;
137 let accessories = crate::db::build_db(gatt.as_ref(), &services, frag).await?;
138
139 // Now establish the secure session for value reads / events.
140 let verify_iid = iid_of(&services, PAIR_VERIFY_CHAR)?;
141 let (mut session, broadcast_key) = pairing::pair_verify(
142 gatt.as_ref(),
143 PAIR_VERIFY_CHAR,
144 verify_iid,
145 &self.keypair,
146 pairing,
147 frag,
148 )
149 .await?;
150
151 // Best-effort: ask the accessory to generate its broadcast encryption key
152 // so it emits encrypted broadcast notifications while disconnected. An
153 // accessory that doesn't support broadcasts (or whose Service-Signature
154 // characteristic we can't address) just won't broadcast — the
155 // disconnected-event poll still delivers durable events. Failure here must
156 // not abort pairing, so it is ignored.
157 if let Some(sig_iid) = protocol_info_signature_iid(&services) {
158 let _ = crate::pdu::request_secure(
159 gatt.as_ref(),
160 &mut session,
161 SERVICE_SIGNATURE_CHAR,
162 crate::pdu::OpCode::ProtocolConfig,
163 1,
164 sig_iid,
165 &GENERATE_BROADCAST_KEY_BODY,
166 frag,
167 )
168 .await;
169 }
170 // The generation the session was minted at — a later reconnect past this
171 // means the accessory dropped the session and the BleAccessory must
172 // re-verify before its next encrypted op (events surviving a reconnect).
173 let session_generation = gatt.generation().await;
174 let pairings_iid = iid_of(&services, PAIRINGS_CHAR)?;
175 let ctx = crate::accessory::SecureContext {
176 session,
177 session_generation,
178 keypair: self.keypair.clone(),
179 pairing: pairing.clone(),
180 verify_char: PAIR_VERIFY_CHAR.to_string(),
181 verify_iid,
182 pairings_char: PAIRINGS_CHAR.to_string(),
183 pairings_iid,
184 broadcast_key,
185 initial_gsn,
186 };
187 Ok(BleAccessory::new(gatt, ctx, frag, &services, accessories))
188 }
189}
190
191/// The Service-Signature characteristic's iid within the Protocol Information
192/// service — the correct target for the generate-broadcast-key request (every
193/// service has a Service-Signature char, so we must scope to this service).
194fn protocol_info_signature_iid(services: &[crate::gatt::GattService]) -> Option<u16> {
195 let svc = services
196 .iter()
197 .find(|s| s.uuid.eq_ignore_ascii_case(PROTOCOL_INFO_SERVICE))?;
198 svc.characteristics
199 .iter()
200 .find(|c| c.uuid.eq_ignore_ascii_case(SERVICE_SIGNATURE_CHAR))
201 .map(|c| c.iid)
202}
203
204/// Find a characteristic's HAP instance id by UUID in an enumerated GATT tree.
205fn iid_of(services: &[crate::gatt::GattService], char_uuid: &str) -> Result<u16> {
206 services
207 .iter()
208 .flat_map(|s| &s.characteristics)
209 .find(|c| c.uuid.eq_ignore_ascii_case(char_uuid))
210 .map(|c| c.iid)
211 .ok_or(crate::error::BleError::CharacteristicNotFound { aid: 0, iid: 0 })
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn generate_sets_identity() {
220 let c = BleController::generate("11:22:33:44:55:66".into());
221 assert_eq!(c.keypair().id, "11:22:33:44:55:66");
222 }
223}