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::gatt::{PROTOCOL_INFO_SERVICE, SERVICE_SIGNATURE_CHAR};
9use crate::pairing;
10use hap_crypto::AccessoryPairing;
11use hap_crypto::ControllerKeypair;
12use std::sync::Arc;
13
14/// The HAP Pairing-Service characteristic UUIDs (HAP-defined, fixed).
15const PAIR_SETUP_CHAR: &str = "0000004c-0000-1000-8000-0026bb765291";
16const PAIR_VERIFY_CHAR: &str = "0000004e-0000-1000-8000-0026bb765291";
17const PAIRINGS_CHAR: &str = "00000050-0000-1000-8000-0026bb765291";
18/// Protocol-Configuration TLV body that asks the accessory to generate a
19/// broadcast encryption key (type `GenerateBroadcastEncryptionKey` = 0x01, len 0).
20const GENERATE_BROADCAST_KEY_BODY: [u8; 2] = [0x01, 0x00];
21
22/// The result of a successful BLE pairing.
23pub struct Paired {
24 /// The connected accessory handle.
25 pub accessory: BleAccessory,
26 /// The long-term pairing — persist this.
27 pub pairing: AccessoryPairing,
28 /// Broadcast material — persist this to resume broadcasts across restarts.
29 pub broadcast: BleBroadcastState,
30}
31
32/// A BLE HAP controller: holds the long-term controller identity used for
33/// pairing and verification.
34pub struct BleController {
35 keypair: ControllerKeypair,
36}
37
38impl BleController {
39 /// Create a controller from a long-term identity.
40 pub fn new(keypair: ControllerKeypair) -> Self {
41 Self { keypair }
42 }
43
44 /// Generate a fresh controller identity with the given pairing id.
45 pub fn generate(id: String) -> Self {
46 Self {
47 keypair: ControllerKeypair::generate(id),
48 }
49 }
50
51 /// The controller's pairing identity.
52 pub fn keypair(&self) -> &ControllerKeypair {
53 &self.keypair
54 }
55
56 /// Pair with a discovered accessory: run Pair Setup, then Pair Verify, then
57 /// build the attribute database. Returns a [`Paired`] containing the ready
58 /// accessory handle, the persisted [`AccessoryPairing`], and initial
59 /// broadcast material.
60 ///
61 /// # Errors
62 /// Propagates connection, pairing, and model errors.
63 pub async fn pair(
64 &self,
65 gatt: Arc<dyn crate::gatt::GattConnection>,
66 _accessory: &DiscoveredBleAccessory,
67 setup_code: &str,
68 ) -> Result<Paired> {
69 // Pair first (reading only the Pair-Setup characteristic's iid, one
70 // descriptor read) — the long database sweep must not run before the
71 // stateful Pair Setup handshake, which can't survive a mid-handshake
72 // reconnect.
73 let frag = gatt.max_write().await;
74 let setup_iid = gatt.instance_id(PAIR_SETUP_CHAR).await?;
75 let pairing = pairing::pair_setup(
76 gatt.as_ref(),
77 PAIR_SETUP_CHAR,
78 setup_iid,
79 setup_code,
80 self.keypair.clone(),
81 frag,
82 )
83 .await?;
84 let accessory = self.verify_and_build(gatt, &pairing, 0).await?;
85 let broadcast = accessory.broadcast_state().await;
86 Ok(Paired {
87 accessory,
88 pairing,
89 broadcast,
90 })
91 }
92
93 /// Connect to an already-paired accessory via Pair Verify, then build the DB.
94 ///
95 /// `broadcast` is optional previously-persisted broadcast state. Its `gsn`
96 /// seeds `last_gsn` so the accessory handle does not re-emit already-seen
97 /// events after a restart. The key in `broadcast` is the previously-persisted
98 /// one — Pair Verify derives a fresh per-session broadcast key, which becomes
99 /// the accessory's current key.
100 ///
101 /// # NOTE
102 /// Decrypting pre-connect broadcasts with the persisted key (vs the fresh
103 /// per-session key derived here) is a documented follow-up task — the fresh
104 /// key covers forward broadcasts.
105 ///
106 /// # Errors
107 /// Propagates connection, verify, and model errors.
108 pub async fn connect(
109 &self,
110 gatt: Arc<dyn crate::gatt::GattConnection>,
111 pairing: &AccessoryPairing,
112 broadcast: Option<BleBroadcastState>,
113 ) -> Result<BleAccessory> {
114 let initial_gsn = broadcast.as_ref().map_or(0, |b| b.gsn);
115 self.verify_and_build(gatt, pairing, initial_gsn).await
116 }
117
118 async fn verify_and_build(
119 &self,
120 gatt: Arc<dyn crate::gatt::GattConnection>,
121 pairing: &AccessoryPairing,
122 initial_gsn: u16,
123 ) -> Result<BleAccessory> {
124 // After pairing, walk the full tree (resilient) for iids, then build the
125 // typed database from UNENCRYPTED characteristic-signature reads — HAP
126 // reads the database structure after Pair Setup but before Pair Verify
127 // (no secure session yet). The resilient GattConnection reconnects +
128 // resumes through the accessory's periodic disconnects.
129 let frag = gatt.max_write().await;
130 let services = gatt.enumerate().await?;
131 let accessories = crate::db::build_db(gatt.as_ref(), &services, frag).await?;
132
133 // Now establish the secure session for value reads / events.
134 let verify_iid = iid_of(&services, PAIR_VERIFY_CHAR)?;
135 let (mut session, broadcast_key) = pairing::pair_verify(
136 gatt.as_ref(),
137 PAIR_VERIFY_CHAR,
138 verify_iid,
139 &self.keypair,
140 pairing,
141 frag,
142 )
143 .await?;
144
145 // Best-effort: ask the accessory to generate its broadcast encryption key
146 // so it emits encrypted broadcast notifications while disconnected. An
147 // accessory that doesn't support broadcasts (or whose Service-Signature
148 // characteristic we can't address) just won't broadcast — the
149 // disconnected-event poll still delivers durable events.
150 // The outcome is logged (not propagated) so pairing stays unaffected;
151 // enable `hap_ble=debug` to see whether the accessory generated its
152 // broadcast key. A rejected/absent key means no 0x11 broadcasts will
153 // ever flow regardless of per-characteristic enable — the first thing to
154 // check when broadcast notifications don't appear.
155 if let Some(service_iid) = protocol_info_service_iid(&services) {
156 match crate::pdu::request_secure(
157 gatt.as_ref(),
158 &mut session,
159 SERVICE_SIGNATURE_CHAR,
160 crate::pdu::OpCode::ProtocolConfig,
161 1,
162 service_iid,
163 &GENERATE_BROADCAST_KEY_BODY,
164 frag,
165 )
166 .await
167 {
168 Ok(r) if r.status == 0 => {
169 tracing::debug!(
170 iid = service_iid,
171 "generate-broadcast-key accepted by accessory"
172 );
173 }
174 Ok(r) => {
175 tracing::debug!(
176 iid = service_iid,
177 status = r.status,
178 "generate-broadcast-key rejected by accessory (non-zero HAP status)"
179 );
180 }
181 Err(e) => {
182 tracing::debug!(iid = service_iid, error = %e, "generate-broadcast-key write failed");
183 }
184 }
185 } else {
186 tracing::debug!(
187 "no Protocol-Information Service-Signature characteristic — cannot request a \
188 broadcast key; this accessory will not emit 0x11 broadcasts"
189 );
190 }
191 // The generation the session was minted at — a later reconnect past this
192 // means the accessory dropped the session and the BleAccessory must
193 // re-verify before its next encrypted op (events surviving a reconnect).
194 let session_generation = gatt.generation().await;
195 let pairings_iid = iid_of(&services, PAIRINGS_CHAR)?;
196 let ctx = crate::accessory::SecureContext {
197 session,
198 session_generation,
199 keypair: self.keypair.clone(),
200 pairing: pairing.clone(),
201 verify_char: PAIR_VERIFY_CHAR.to_string(),
202 verify_iid,
203 pairings_char: PAIRINGS_CHAR.to_string(),
204 pairings_iid,
205 broadcast_key,
206 initial_gsn,
207 };
208 Ok(BleAccessory::new(gatt, ctx, frag, &services, accessories))
209 }
210}
211
212/// The Service-Signature characteristic's iid within the Protocol Information
213/// service — the correct target for the generate-broadcast-key request (every
214/// service has a Service-Signature char, so we must scope to this service).
215fn protocol_info_service_iid(services: &[crate::gatt::GattService]) -> Option<u16> {
216 let svc = services
217 .iter()
218 .find(|s| s.uuid.eq_ignore_ascii_case(PROTOCOL_INFO_SERVICE))?;
219 // The generate-broadcast-key request is written to the Service-Signature
220 // characteristic's GATT handle, but the PDU carries the Protocol-Information
221 // *service's* instance id — not the characteristic's own iid (aiohomekit
222 // uses `hap_char.service.iid`; an accessory rejects the characteristic iid
223 // with HAP status 4, "invalid instance id"). We still require the
224 // Service-Signature characteristic to be present, since its absence means
225 // the accessory does not implement encrypted broadcasts at all.
226 let has_signature = svc
227 .characteristics
228 .iter()
229 .any(|c| c.uuid.eq_ignore_ascii_case(SERVICE_SIGNATURE_CHAR));
230 has_signature.then_some(svc.iid)
231}
232
233/// Find a characteristic's HAP instance id by UUID in an enumerated GATT tree.
234fn iid_of(services: &[crate::gatt::GattService], char_uuid: &str) -> Result<u16> {
235 services
236 .iter()
237 .flat_map(|s| &s.characteristics)
238 .find(|c| c.uuid.eq_ignore_ascii_case(char_uuid))
239 .map(|c| c.iid)
240 .ok_or(crate::error::BleError::CharacteristicNotFound { aid: 0, iid: 0 })
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn generate_sets_identity() {
249 let c = BleController::generate("11:22:33:44:55:66".into());
250 assert_eq!(c.keypair().id, "11:22:33:44:55:66");
251 }
252
253 #[test]
254 fn returns_protocol_info_service_iid_when_signature_present() {
255 use crate::gatt::{GattCharacteristic, GattService};
256 let services = vec![
257 // A decoy service that also carries a Service-Signature char (they
258 // share one UUID) is listed FIRST; the lookup must scope to the
259 // Protocol-Information service and return ITS service iid (16), not
260 // the decoy's (and not any characteristic iid).
261 GattService {
262 uuid: "00000055-0000-1000-8000-0026bb765291".into(),
263 iid: 80,
264 characteristics: vec![GattCharacteristic {
265 uuid: SERVICE_SIGNATURE_CHAR.into(),
266 iid: 99,
267 }],
268 },
269 GattService {
270 uuid: PROTOCOL_INFO_SERVICE.into(),
271 iid: 16, // the service instance id — the request target
272 characteristics: vec![
273 GattCharacteristic {
274 uuid: SERVICE_SIGNATURE_CHAR.into(),
275 iid: 17, // the characteristic iid — NOT what we send
276 },
277 GattCharacteristic {
278 uuid: "00000037-0000-1000-8000-0026bb765291".into(),
279 iid: 18,
280 },
281 ],
282 },
283 ];
284 assert_eq!(protocol_info_service_iid(&services), Some(16));
285 }
286
287 #[test]
288 fn missing_protocol_info_signature_is_none() {
289 use crate::gatt::GattService;
290 // Protocol-Information service present but with no Service-Signature char
291 // ⇒ the accessory does not implement encrypted broadcasts ⇒ None (a
292 // best-effort skip, not a hard error).
293 let services = vec![GattService {
294 uuid: PROTOCOL_INFO_SERVICE.into(),
295 iid: 16,
296 characteristics: vec![],
297 }];
298 assert_eq!(protocol_info_service_iid(&services), None);
299 }
300}