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::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}