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.
156 // The outcome is logged (not propagated) so pairing stays unaffected;
157 // enable `hap_ble=debug` to see whether the accessory generated its
158 // broadcast key. A rejected/absent key means no 0x11 broadcasts will
159 // ever flow regardless of per-characteristic enable — the first thing to
160 // check when broadcast notifications don't appear.
161 if let Some(sig_iid) = protocol_info_signature_iid(&services) {
162 match crate::pdu::request_secure(
163 gatt.as_ref(),
164 &mut session,
165 SERVICE_SIGNATURE_CHAR,
166 crate::pdu::OpCode::ProtocolConfig,
167 1,
168 sig_iid,
169 &GENERATE_BROADCAST_KEY_BODY,
170 frag,
171 )
172 .await
173 {
174 Ok(r) if r.status == 0 => {
175 tracing::debug!(
176 iid = sig_iid,
177 "generate-broadcast-key accepted by accessory"
178 );
179 }
180 Ok(r) => {
181 tracing::debug!(
182 iid = sig_iid,
183 status = r.status,
184 "generate-broadcast-key rejected by accessory (non-zero HAP status)"
185 );
186 }
187 Err(e) => {
188 tracing::debug!(iid = sig_iid, error = %e, "generate-broadcast-key write failed");
189 }
190 }
191 } else {
192 tracing::debug!(
193 "no Protocol-Information Service-Signature characteristic — cannot request a \
194 broadcast key; this accessory will not emit 0x11 broadcasts"
195 );
196 }
197 // The generation the session was minted at — a later reconnect past this
198 // means the accessory dropped the session and the BleAccessory must
199 // re-verify before its next encrypted op (events surviving a reconnect).
200 let session_generation = gatt.generation().await;
201 let pairings_iid = iid_of(&services, PAIRINGS_CHAR)?;
202 let ctx = crate::accessory::SecureContext {
203 session,
204 session_generation,
205 keypair: self.keypair.clone(),
206 pairing: pairing.clone(),
207 verify_char: PAIR_VERIFY_CHAR.to_string(),
208 verify_iid,
209 pairings_char: PAIRINGS_CHAR.to_string(),
210 pairings_iid,
211 broadcast_key,
212 initial_gsn,
213 };
214 Ok(BleAccessory::new(gatt, ctx, frag, &services, accessories))
215 }
216}
217
218/// The Service-Signature characteristic's iid within the Protocol Information
219/// service — the correct target for the generate-broadcast-key request (every
220/// service has a Service-Signature char, so we must scope to this service).
221fn protocol_info_signature_iid(services: &[crate::gatt::GattService]) -> Option<u16> {
222 let svc = services
223 .iter()
224 .find(|s| s.uuid.eq_ignore_ascii_case(PROTOCOL_INFO_SERVICE))?;
225 svc.characteristics
226 .iter()
227 .find(|c| c.uuid.eq_ignore_ascii_case(SERVICE_SIGNATURE_CHAR))
228 .map(|c| c.iid)
229}
230
231/// Find a characteristic's HAP instance id by UUID in an enumerated GATT tree.
232fn iid_of(services: &[crate::gatt::GattService], char_uuid: &str) -> Result<u16> {
233 services
234 .iter()
235 .flat_map(|s| &s.characteristics)
236 .find(|c| c.uuid.eq_ignore_ascii_case(char_uuid))
237 .map(|c| c.iid)
238 .ok_or(crate::error::BleError::CharacteristicNotFound { aid: 0, iid: 0 })
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn generate_sets_identity() {
247 let c = BleController::generate("11:22:33:44:55:66".into());
248 assert_eq!(c.keypair().id, "11:22:33:44:55:66");
249 }
250}