Skip to main content

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}