Skip to main content

hive_btle/discovery/
encrypted_beacon.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Encrypted BLE Advertisement Beacons
17//!
18//! Protects mesh and node identity from passive observers while allowing
19//! mesh members to identify each other. Non-members see random data.
20//!
21//! # Privacy Properties
22//!
23//! - Device name is generic ("HIVE") - no identifying information
24//! - mesh_id and node_id are encrypted in service data
25//! - Nonce rotates to prevent tracking across advertisements
26//! - Only nodes with beacon_key can decrypt
27//!
28//! # Wire Format
29//!
30//! ```text
31//! Encrypted Beacon (21 bytes):
32//! ┌─────────┬─────────┬──────────────────┬─────┬──────┬─────┬─────┐
33//! │ Version │  Nonce  │ Encrypted Identity│ MAC │ Caps │Hier │ Bat │
34//! │ 1 byte  │ 4 bytes │     8 bytes      │4 byt│2 byt │1 byt│1 byt│
35//! └─────────┴─────────┴──────────────────┴─────┴──────┴─────┴─────┘
36//!
37//! Encrypted Identity = XOR(mesh_id[4] || node_id[4], keystream)
38//! MAC = BLAKE3(beacon_key || nonce || encrypted)[0..4]
39//! ```
40//!
41//! # Example
42//!
43//! ```ignore
44//! use hive_btle::discovery::{EncryptedBeacon, BeaconKey};
45//!
46//! // Derive beacon key from genesis
47//! let beacon_key = BeaconKey::from_base(&genesis.beacon_key_base());
48//!
49//! // Create and encrypt beacon
50//! let beacon = EncryptedBeacon::new(node_id, capabilities, hierarchy, battery);
51//! let encrypted = beacon.encrypt(&beacon_key, &mesh_id_bytes);
52//!
53//! // Decrypt received beacon
54//! if let Some((beacon, mesh_id)) = EncryptedBeacon::decrypt(&encrypted, &beacon_key) {
55//!     println!("Received from mesh {:08X}, node {:08X}", mesh_id, beacon.node_id);
56//! }
57//! ```
58
59#[cfg(not(feature = "std"))]
60use alloc::vec::Vec;
61
62use crate::NodeId;
63
64/// Version byte for encrypted beacon format
65pub const ENCRYPTED_BEACON_VERSION: u8 = 0x02;
66
67/// Size of encrypted beacon in bytes
68pub const ENCRYPTED_BEACON_SIZE: usize = 21;
69
70/// Size of the encrypted identity portion
71const ENCRYPTED_IDENTITY_SIZE: usize = 8;
72
73/// Size of the MAC
74const MAC_SIZE: usize = 4;
75
76/// Size of the nonce
77const NONCE_SIZE: usize = 4;
78
79/// Beacon encryption key derived from mesh genesis
80#[derive(Clone)]
81pub struct BeaconKey {
82    /// The 32-byte key used for encryption and MAC
83    key: [u8; 32],
84}
85
86impl BeaconKey {
87    /// Create a beacon key from the base key (from MeshGenesis::beacon_key_base())
88    pub fn from_base(base: &[u8; 32]) -> Self {
89        Self { key: *base }
90    }
91
92    /// Get the raw key bytes (for testing)
93    #[cfg(test)]
94    pub fn as_bytes(&self) -> &[u8; 32] {
95        &self.key
96    }
97
98    /// Derive keystream for XOR encryption
99    fn derive_keystream(&self, nonce: &[u8; NONCE_SIZE]) -> [u8; ENCRYPTED_IDENTITY_SIZE] {
100        // Use BLAKE3 keyed hash to derive keystream
101        let mut input = [0u8; 36];
102        input[..32].copy_from_slice(&self.key);
103        input[32..].copy_from_slice(nonce);
104
105        let hash = blake3::hash(&input);
106        let mut keystream = [0u8; ENCRYPTED_IDENTITY_SIZE];
107        keystream.copy_from_slice(&hash.as_bytes()[..ENCRYPTED_IDENTITY_SIZE]);
108        keystream
109    }
110
111    /// Compute truncated MAC over nonce and encrypted data
112    fn compute_mac(
113        &self,
114        nonce: &[u8; NONCE_SIZE],
115        encrypted: &[u8; ENCRYPTED_IDENTITY_SIZE],
116    ) -> [u8; MAC_SIZE] {
117        let hash = blake3::keyed_hash(
118            &self.key,
119            &[nonce.as_slice(), encrypted.as_slice()].concat(),
120        );
121        let mut mac = [0u8; MAC_SIZE];
122        mac.copy_from_slice(&hash.as_bytes()[..MAC_SIZE]);
123        mac
124    }
125}
126
127/// Encrypted beacon for privacy-preserving advertisements
128///
129/// Contains node identification and status that can only be read
130/// by mesh members with the beacon key.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct EncryptedBeacon {
133    /// Node identifier (encrypted in wire format)
134    pub node_id: NodeId,
135
136    /// Node capabilities bitmap (public)
137    pub capabilities: u16,
138
139    /// Hierarchy level (public, for parent selection)
140    pub hierarchy_level: u8,
141
142    /// Battery percentage 0-100 (public)
143    pub battery_percent: u8,
144}
145
146impl EncryptedBeacon {
147    /// Create a new beacon with the given parameters
148    pub fn new(
149        node_id: NodeId,
150        capabilities: u16,
151        hierarchy_level: u8,
152        battery_percent: u8,
153    ) -> Self {
154        Self {
155            node_id,
156            capabilities,
157            hierarchy_level,
158            battery_percent,
159        }
160    }
161
162    /// Encrypt the beacon for transmission
163    ///
164    /// # Arguments
165    /// * `key` - Beacon encryption key from mesh genesis
166    /// * `mesh_id_bytes` - First 4 bytes of mesh_id hash (for identification)
167    ///
168    /// # Returns
169    /// 21-byte encrypted beacon ready for BLE service data
170    pub fn encrypt(&self, key: &BeaconKey, mesh_id_bytes: &[u8; 4]) -> Vec<u8> {
171        let mut buf = Vec::with_capacity(ENCRYPTED_BEACON_SIZE);
172
173        // Version
174        buf.push(ENCRYPTED_BEACON_VERSION);
175
176        // Generate random nonce
177        let mut nonce = [0u8; NONCE_SIZE];
178        rand_core::OsRng.fill_bytes(&mut nonce);
179        buf.extend_from_slice(&nonce);
180
181        // Build plaintext: mesh_id[4] || node_id[4]
182        let mut plaintext = [0u8; ENCRYPTED_IDENTITY_SIZE];
183        plaintext[..4].copy_from_slice(mesh_id_bytes);
184        plaintext[4..].copy_from_slice(&self.node_id.as_u32().to_be_bytes());
185
186        // Encrypt with XOR keystream
187        let keystream = key.derive_keystream(&nonce);
188        let mut encrypted = [0u8; ENCRYPTED_IDENTITY_SIZE];
189        for i in 0..ENCRYPTED_IDENTITY_SIZE {
190            encrypted[i] = plaintext[i] ^ keystream[i];
191        }
192        buf.extend_from_slice(&encrypted);
193
194        // MAC
195        let mac = key.compute_mac(&nonce, &encrypted);
196        buf.extend_from_slice(&mac);
197
198        // Public fields (not encrypted, needed for filtering)
199        buf.extend_from_slice(&self.capabilities.to_be_bytes());
200        buf.push(self.hierarchy_level);
201        buf.push(self.battery_percent);
202
203        buf
204    }
205
206    /// Attempt to decrypt a beacon
207    ///
208    /// # Arguments
209    /// * `data` - Raw encrypted beacon bytes (21 bytes)
210    /// * `key` - Beacon encryption key to try
211    ///
212    /// # Returns
213    /// * `Some((beacon, mesh_id_bytes))` if decryption succeeds and MAC is valid
214    /// * `None` if data is invalid or MAC doesn't match (wrong mesh)
215    pub fn decrypt(data: &[u8], key: &BeaconKey) -> Option<(Self, [u8; 4])> {
216        if data.len() < ENCRYPTED_BEACON_SIZE {
217            return None;
218        }
219
220        // Check version
221        if data[0] != ENCRYPTED_BEACON_VERSION {
222            return None;
223        }
224
225        // Extract components
226        let mut nonce = [0u8; NONCE_SIZE];
227        nonce.copy_from_slice(&data[1..5]);
228
229        let mut encrypted = [0u8; ENCRYPTED_IDENTITY_SIZE];
230        encrypted.copy_from_slice(&data[5..13]);
231
232        let mut received_mac = [0u8; MAC_SIZE];
233        received_mac.copy_from_slice(&data[13..17]);
234
235        // Verify MAC first (quick rejection for wrong mesh)
236        let expected_mac = key.compute_mac(&nonce, &encrypted);
237        if received_mac != expected_mac {
238            return None;
239        }
240
241        // Decrypt
242        let keystream = key.derive_keystream(&nonce);
243        let mut plaintext = [0u8; ENCRYPTED_IDENTITY_SIZE];
244        for i in 0..ENCRYPTED_IDENTITY_SIZE {
245            plaintext[i] = encrypted[i] ^ keystream[i];
246        }
247
248        // Extract mesh_id and node_id
249        let mut mesh_id_bytes = [0u8; 4];
250        mesh_id_bytes.copy_from_slice(&plaintext[..4]);
251
252        let node_id = NodeId::new(u32::from_be_bytes([
253            plaintext[4],
254            plaintext[5],
255            plaintext[6],
256            plaintext[7],
257        ]));
258
259        // Extract public fields
260        let capabilities = u16::from_be_bytes([data[17], data[18]]);
261        let hierarchy_level = data[19];
262        let battery_percent = data[20];
263
264        Some((
265            Self {
266                node_id,
267                capabilities,
268                hierarchy_level,
269                battery_percent,
270            },
271            mesh_id_bytes,
272        ))
273    }
274
275    /// Check if data looks like an encrypted beacon (quick check)
276    pub fn is_encrypted_beacon(data: &[u8]) -> bool {
277        data.len() >= ENCRYPTED_BEACON_SIZE && data[0] == ENCRYPTED_BEACON_VERSION
278    }
279}
280
281/// Convert mesh_id string to 4-byte identifier for beacon
282///
283/// Uses first 4 bytes of BLAKE3 hash of the mesh_id string.
284pub fn mesh_id_to_bytes(mesh_id: &str) -> [u8; 4] {
285    let hash = blake3::hash(mesh_id.as_bytes());
286    let mut bytes = [0u8; 4];
287    bytes.copy_from_slice(&hash.as_bytes()[..4]);
288    bytes
289}
290
291/// Generic device name for encrypted beacons
292pub const ENCRYPTED_DEVICE_NAME: &str = "HIVE";
293
294use rand_core::RngCore;
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_encrypt_decrypt_roundtrip() {
302        let key = BeaconKey::from_base(&[0x42; 32]);
303        let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
304        let node_id = NodeId::new(0x12345678);
305
306        let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
307        let encrypted = beacon.encrypt(&key, &mesh_id_bytes);
308
309        assert_eq!(encrypted.len(), ENCRYPTED_BEACON_SIZE);
310        assert_eq!(encrypted[0], ENCRYPTED_BEACON_VERSION);
311
312        let (decrypted, decrypted_mesh_id) = EncryptedBeacon::decrypt(&encrypted, &key).unwrap();
313
314        assert_eq!(decrypted.node_id, node_id);
315        assert_eq!(decrypted.capabilities, 0x0F00);
316        assert_eq!(decrypted.hierarchy_level, 2);
317        assert_eq!(decrypted.battery_percent, 85);
318        assert_eq!(decrypted_mesh_id, mesh_id_bytes);
319    }
320
321    #[test]
322    fn test_wrong_key_fails() {
323        let key1 = BeaconKey::from_base(&[0x42; 32]);
324        let key2 = BeaconKey::from_base(&[0x99; 32]);
325        let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
326        let node_id = NodeId::new(0x12345678);
327
328        let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
329        let encrypted = beacon.encrypt(&key1, &mesh_id_bytes);
330
331        // Decryption with wrong key should fail (MAC mismatch)
332        assert!(EncryptedBeacon::decrypt(&encrypted, &key2).is_none());
333    }
334
335    #[test]
336    fn test_tampered_data_fails() {
337        let key = BeaconKey::from_base(&[0x42; 32]);
338        let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
339        let node_id = NodeId::new(0x12345678);
340
341        let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
342        let mut encrypted = beacon.encrypt(&key, &mesh_id_bytes);
343
344        // Tamper with encrypted data
345        encrypted[7] ^= 0xFF;
346
347        // Should fail MAC check
348        assert!(EncryptedBeacon::decrypt(&encrypted, &key).is_none());
349    }
350
351    #[test]
352    fn test_different_nonces_produce_different_ciphertext() {
353        let key = BeaconKey::from_base(&[0x42; 32]);
354        let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
355        let node_id = NodeId::new(0x12345678);
356
357        let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
358        let encrypted1 = beacon.encrypt(&key, &mesh_id_bytes);
359        let encrypted2 = beacon.encrypt(&key, &mesh_id_bytes);
360
361        // Nonces should differ (bytes 1-4)
362        assert_ne!(&encrypted1[1..5], &encrypted2[1..5]);
363
364        // Encrypted portions should differ
365        assert_ne!(&encrypted1[5..13], &encrypted2[5..13]);
366
367        // Both should decrypt correctly
368        assert!(EncryptedBeacon::decrypt(&encrypted1, &key).is_some());
369        assert!(EncryptedBeacon::decrypt(&encrypted2, &key).is_some());
370    }
371
372    #[test]
373    fn test_is_encrypted_beacon() {
374        let key = BeaconKey::from_base(&[0x42; 32]);
375        let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
376        let beacon = EncryptedBeacon::new(NodeId::new(1), 0, 0, 0);
377        let encrypted = beacon.encrypt(&key, &mesh_id_bytes);
378
379        assert!(EncryptedBeacon::is_encrypted_beacon(&encrypted));
380        assert!(!EncryptedBeacon::is_encrypted_beacon(&[0x01; 21])); // Wrong version
381        assert!(!EncryptedBeacon::is_encrypted_beacon(&[0x02; 10])); // Too short
382    }
383
384    #[test]
385    fn test_mesh_id_to_bytes_deterministic() {
386        let bytes1 = mesh_id_to_bytes("ALPHA-TEAM");
387        let bytes2 = mesh_id_to_bytes("ALPHA-TEAM");
388        let bytes3 = mesh_id_to_bytes("BRAVO-TEAM");
389
390        assert_eq!(bytes1, bytes2);
391        assert_ne!(bytes1, bytes3);
392    }
393}