Skip to main content

hap_ble/
advert.rs

1//! Typed HAP-BLE advertisement parsing (manufacturer-data, company id 0x004C).
2//! Two relevant types: regular HAP advert (0x06) and encrypted notification
3//! (0x11). The 0x06 device id and 0x11 advertising id are the stable HAP Device
4//! ID we match accessories by (NOT the platform BLE address).
5
6/// A parsed HAP advertisement of interest to the controller.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum HapAdvert {
9    /// Regular HAP advertisement (type 0x06): carries the device id and GSN.
10    Regular {
11        /// The accessory's 6-byte HAP Device ID.
12        device_id: [u8; 6],
13        /// Global State Number (bumps on every event while paired).
14        gsn: u16,
15        /// True if the accessory reports itself paired (status bit0 clear).
16        paired: bool,
17    },
18    /// Encrypted broadcast notification (type 0x11): an encrypted value change.
19    EncryptedNotification {
20        /// The 6-byte advertising identifier (also the AEAD AAD).
21        advertising_id: [u8; 6],
22        /// Ciphertext with the 16-byte Poly1305 tag appended.
23        payload: Vec<u8>,
24    },
25}
26
27impl HapAdvert {
28    /// Parse Apple (0x004C) HAP manufacturer-data. Returns `None` for non-HAP or
29    /// malformed/too-short input.
30    #[must_use]
31    pub fn parse(mfg: &[u8]) -> Option<Self> {
32        match mfg.first()? {
33            0x06 if mfg.len() >= 13 => {
34                let mut device_id = [0u8; 6];
35                device_id.copy_from_slice(&mfg[3..9]);
36                let gsn = u16::from_le_bytes([mfg[11], mfg[12]]);
37                let paired = mfg[2] & 0x01 == 0;
38                Some(Self::Regular {
39                    device_id,
40                    gsn,
41                    paired,
42                })
43            }
44            0x11 if mfg.len() >= 8 => {
45                let mut advertising_id = [0u8; 6];
46                advertising_id.copy_from_slice(&mfg[2..8]);
47                Some(Self::EncryptedNotification {
48                    advertising_id,
49                    payload: mfg[8..].to_vec(),
50                })
51            }
52            _ => None,
53        }
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    #[test]
61    #[allow(clippy::unwrap_used)]
62    fn parses_regular_0x06_advert() {
63        let mfg = [
64            0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x05, 0x00, 0x01, 0x00,
65        ];
66        let a = HapAdvert::parse(&mfg).unwrap();
67        match a {
68            HapAdvert::Regular {
69                device_id,
70                gsn,
71                paired,
72            } => {
73                assert_eq!(device_id, [1, 2, 3, 4, 5, 6]);
74                assert_eq!(gsn, 5);
75                assert!(!paired);
76            }
77            HapAdvert::EncryptedNotification { .. } => panic!("expected Regular"),
78        }
79    }
80    #[test]
81    #[allow(clippy::unwrap_used)]
82    fn parses_encrypted_0x11_advert() {
83        let mfg = [0x11, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 9, 9, 9];
84        let a = HapAdvert::parse(&mfg).unwrap();
85        match a {
86            HapAdvert::EncryptedNotification {
87                advertising_id,
88                payload,
89            } => {
90                assert_eq!(advertising_id, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
91                assert_eq!(payload, vec![9, 9, 9]);
92            }
93            HapAdvert::Regular { .. } => panic!("expected EncryptedNotification"),
94        }
95    }
96    #[test]
97    fn rejects_short_advert() {
98        assert!(HapAdvert::parse(&[0x06, 0x21]).is_none());
99    }
100}