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