Skip to main content

hive_btle/security/
mesh_key.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//! Mesh encryption key derivation and cryptographic operations
17
18#[cfg(not(feature = "std"))]
19use alloc::vec::Vec;
20
21use chacha20poly1305::{
22    aead::{Aead, KeyInit, OsRng},
23    ChaCha20Poly1305, Nonce,
24};
25use hkdf::Hkdf;
26use rand_core::RngCore;
27use sha2::Sha256;
28
29/// Errors that can occur during encryption/decryption
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum EncryptionError {
32    /// Encryption operation failed
33    EncryptionFailed,
34    /// Decryption failed (wrong key or corrupted data)
35    DecryptionFailed,
36    /// Invalid encrypted document format
37    InvalidFormat,
38}
39
40impl core::fmt::Display for EncryptionError {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        match self {
43            Self::EncryptionFailed => write!(f, "encryption failed"),
44            Self::DecryptionFailed => write!(f, "decryption failed (wrong key or corrupted data)"),
45            Self::InvalidFormat => write!(f, "invalid encrypted document format"),
46        }
47    }
48}
49
50#[cfg(feature = "std")]
51impl std::error::Error for EncryptionError {}
52
53/// An encrypted HIVE document
54///
55/// Contains the nonce and ciphertext (which includes the 16-byte Poly1305 auth tag).
56#[derive(Debug, Clone)]
57pub struct EncryptedDocument {
58    /// 12-byte random nonce
59    pub nonce: [u8; 12],
60    /// Ciphertext with appended 16-byte auth tag
61    pub ciphertext: Vec<u8>,
62}
63
64impl EncryptedDocument {
65    /// Total overhead added by encryption (nonce + auth tag)
66    pub const OVERHEAD: usize = 12 + 16; // nonce + Poly1305 tag
67
68    /// Encode to bytes for wire transmission
69    ///
70    /// Format: nonce (12 bytes) || ciphertext (variable, includes tag)
71    pub fn encode(&self) -> Vec<u8> {
72        let mut buf = Vec::with_capacity(12 + self.ciphertext.len());
73        buf.extend_from_slice(&self.nonce);
74        buf.extend_from_slice(&self.ciphertext);
75        buf
76    }
77
78    /// Decode from bytes received over wire
79    ///
80    /// Returns None if data is too short (minimum: 12 nonce + 16 tag = 28 bytes)
81    pub fn decode(data: &[u8]) -> Option<Self> {
82        if data.len() < Self::OVERHEAD {
83            return None;
84        }
85
86        let mut nonce = [0u8; 12];
87        nonce.copy_from_slice(&data[..12]);
88        let ciphertext = data[12..].to_vec();
89
90        Some(Self { nonce, ciphertext })
91    }
92}
93
94/// Mesh-wide encryption key for HIVE documents
95///
96/// All nodes sharing the same formation secret derive the same key,
97/// enabling encrypted communication across the mesh.
98#[derive(Clone)]
99pub struct MeshEncryptionKey {
100    /// ChaCha20-Poly1305 256-bit key
101    key: [u8; 32],
102}
103
104impl MeshEncryptionKey {
105    /// HKDF info context for mesh encryption key derivation
106    const HKDF_INFO: &'static [u8] = b"HIVE-BTLE-mesh-encryption-v1";
107
108    /// Derive a mesh encryption key from a shared secret
109    ///
110    /// Uses HKDF-SHA256 with the mesh ID as salt and a fixed info string
111    /// to derive a unique 256-bit key for this mesh.
112    ///
113    /// # Arguments
114    /// * `mesh_id` - The mesh identifier (e.g., "DEMO", "ALPHA")
115    /// * `secret` - 32-byte shared secret known to all mesh participants
116    ///
117    /// # Example
118    /// ```ignore
119    /// let secret = [0x42u8; 32]; // In practice, a securely shared secret
120    /// let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
121    /// ```
122    pub fn from_shared_secret(mesh_id: &str, secret: &[u8; 32]) -> Self {
123        let hk = Hkdf::<Sha256>::new(Some(mesh_id.as_bytes()), secret);
124        let mut key = [0u8; 32];
125        hk.expand(Self::HKDF_INFO, &mut key)
126            .expect("32 bytes is valid output length for HKDF-SHA256");
127        Self { key }
128    }
129
130    /// Encrypt plaintext document bytes
131    ///
132    /// Generates a random 12-byte nonce and encrypts using ChaCha20-Poly1305.
133    /// The resulting ciphertext includes a 16-byte authentication tag.
134    ///
135    /// # Arguments
136    /// * `plaintext` - Raw document bytes to encrypt
137    ///
138    /// # Returns
139    /// * `Ok(EncryptedDocument)` - Encrypted document with nonce and ciphertext
140    /// * `Err(EncryptionError)` - If encryption fails (should not happen in practice)
141    pub fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedDocument, EncryptionError> {
142        let cipher = ChaCha20Poly1305::new_from_slice(&self.key)
143            .map_err(|_| EncryptionError::EncryptionFailed)?;
144
145        // Generate random nonce
146        let mut nonce_bytes = [0u8; 12];
147        OsRng.fill_bytes(&mut nonce_bytes);
148        let nonce = Nonce::from_slice(&nonce_bytes);
149
150        // Encrypt with authentication
151        let ciphertext = cipher
152            .encrypt(nonce, plaintext)
153            .map_err(|_| EncryptionError::EncryptionFailed)?;
154
155        Ok(EncryptedDocument {
156            nonce: nonce_bytes,
157            ciphertext,
158        })
159    }
160
161    /// Decrypt encrypted document bytes
162    ///
163    /// Verifies the authentication tag and decrypts the ciphertext.
164    ///
165    /// # Arguments
166    /// * `encrypted` - Encrypted document with nonce and ciphertext
167    ///
168    /// # Returns
169    /// * `Ok(Vec<u8>)` - Decrypted plaintext document bytes
170    /// * `Err(EncryptionError)` - If decryption fails (wrong key or corrupted data)
171    pub fn decrypt(&self, encrypted: &EncryptedDocument) -> Result<Vec<u8>, EncryptionError> {
172        let cipher = ChaCha20Poly1305::new_from_slice(&self.key)
173            .map_err(|_| EncryptionError::DecryptionFailed)?;
174
175        let nonce = Nonce::from_slice(&encrypted.nonce);
176
177        cipher
178            .decrypt(nonce, encrypted.ciphertext.as_ref())
179            .map_err(|_| EncryptionError::DecryptionFailed)
180    }
181
182    /// Encrypt and encode in one step
183    ///
184    /// Convenience method that encrypts plaintext and returns wire-format bytes.
185    pub fn encrypt_to_bytes(&self, plaintext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
186        let encrypted = self.encrypt(plaintext)?;
187        Ok(encrypted.encode())
188    }
189
190    /// Decode and decrypt in one step
191    ///
192    /// Convenience method that decodes wire-format bytes and decrypts.
193    pub fn decrypt_from_bytes(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
194        let encrypted = EncryptedDocument::decode(data).ok_or(EncryptionError::InvalidFormat)?;
195        self.decrypt(&encrypted)
196    }
197}
198
199impl core::fmt::Debug for MeshEncryptionKey {
200    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
201        // Don't expose key bytes in debug output
202        f.debug_struct("MeshEncryptionKey")
203            .field("key", &"[REDACTED]")
204            .finish()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_key_derivation_deterministic() {
214        let secret = [0x42u8; 32];
215        let key1 = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
216        let key2 = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
217
218        // Same inputs produce same key
219        assert_eq!(key1.key, key2.key);
220    }
221
222    #[test]
223    fn test_key_derivation_different_mesh_id() {
224        let secret = [0x42u8; 32];
225        let key1 = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
226        let key2 = MeshEncryptionKey::from_shared_secret("ALPHA", &secret);
227
228        // Different mesh IDs produce different keys
229        assert_ne!(key1.key, key2.key);
230    }
231
232    #[test]
233    fn test_key_derivation_different_secret() {
234        let secret1 = [0x42u8; 32];
235        let secret2 = [0x43u8; 32];
236        let key1 = MeshEncryptionKey::from_shared_secret("DEMO", &secret1);
237        let key2 = MeshEncryptionKey::from_shared_secret("DEMO", &secret2);
238
239        // Different secrets produce different keys
240        assert_ne!(key1.key, key2.key);
241    }
242
243    #[test]
244    fn test_encrypt_decrypt_roundtrip() {
245        let secret = [0x42u8; 32];
246        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
247
248        let plaintext = b"Hello, HIVE mesh!";
249        let encrypted = key.encrypt(plaintext).unwrap();
250        let decrypted = key.decrypt(&encrypted).unwrap();
251
252        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
253    }
254
255    #[test]
256    fn test_encrypt_decrypt_empty() {
257        let secret = [0x42u8; 32];
258        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
259
260        let plaintext = b"";
261        let encrypted = key.encrypt(plaintext).unwrap();
262        let decrypted = key.decrypt(&encrypted).unwrap();
263
264        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
265    }
266
267    #[test]
268    fn test_encrypt_produces_different_ciphertext() {
269        let secret = [0x42u8; 32];
270        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
271
272        let plaintext = b"Same message";
273        let encrypted1 = key.encrypt(plaintext).unwrap();
274        let encrypted2 = key.encrypt(plaintext).unwrap();
275
276        // Different nonces produce different ciphertext (probabilistic encryption)
277        assert_ne!(encrypted1.nonce, encrypted2.nonce);
278        assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
279
280        // But both decrypt to same plaintext
281        assert_eq!(key.decrypt(&encrypted1).unwrap(), plaintext.as_slice());
282        assert_eq!(key.decrypt(&encrypted2).unwrap(), plaintext.as_slice());
283    }
284
285    #[test]
286    fn test_wrong_key_fails() {
287        let secret1 = [0x42u8; 32];
288        let secret2 = [0x43u8; 32];
289        let key1 = MeshEncryptionKey::from_shared_secret("DEMO", &secret1);
290        let key2 = MeshEncryptionKey::from_shared_secret("DEMO", &secret2);
291
292        let plaintext = b"Secret message";
293        let encrypted = key1.encrypt(plaintext).unwrap();
294
295        // Wrong key fails to decrypt (authentication fails)
296        let result = key2.decrypt(&encrypted);
297        assert!(result.is_err());
298        assert_eq!(result.unwrap_err(), EncryptionError::DecryptionFailed);
299    }
300
301    #[test]
302    fn test_tampered_ciphertext_fails() {
303        let secret = [0x42u8; 32];
304        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
305
306        let plaintext = b"Authentic message";
307        let mut encrypted = key.encrypt(plaintext).unwrap();
308
309        // Tamper with ciphertext
310        if !encrypted.ciphertext.is_empty() {
311            encrypted.ciphertext[0] ^= 0xFF;
312        }
313
314        // Decryption fails (authentication fails)
315        let result = key.decrypt(&encrypted);
316        assert!(result.is_err());
317    }
318
319    #[test]
320    fn test_encrypted_document_encode_decode() {
321        let secret = [0x42u8; 32];
322        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
323
324        let plaintext = b"Wire format test";
325        let encrypted = key.encrypt(plaintext).unwrap();
326
327        // Encode to bytes
328        let wire_bytes = encrypted.encode();
329
330        // Decode from bytes
331        let decoded = EncryptedDocument::decode(&wire_bytes).unwrap();
332
333        assert_eq!(encrypted.nonce, decoded.nonce);
334        assert_eq!(encrypted.ciphertext, decoded.ciphertext);
335
336        // Decrypt decoded document
337        let decrypted = key.decrypt(&decoded).unwrap();
338        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
339    }
340
341    #[test]
342    fn test_convenience_methods() {
343        let secret = [0x42u8; 32];
344        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
345
346        let plaintext = b"Convenience test";
347
348        // encrypt_to_bytes / decrypt_from_bytes
349        let wire_bytes = key.encrypt_to_bytes(plaintext).unwrap();
350        let decrypted = key.decrypt_from_bytes(&wire_bytes).unwrap();
351
352        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
353    }
354
355    #[test]
356    fn test_encrypted_document_decode_too_short() {
357        // Less than 28 bytes (12 nonce + 16 tag minimum)
358        let short_data = [0u8; 27];
359        assert!(EncryptedDocument::decode(&short_data).is_none());
360
361        // Exactly 28 bytes is valid (empty plaintext)
362        let minimal_data = [0u8; 28];
363        assert!(EncryptedDocument::decode(&minimal_data).is_some());
364    }
365
366    #[test]
367    fn test_overhead_calculation() {
368        let secret = [0x42u8; 32];
369        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
370
371        let plaintext = b"Testing overhead";
372        let encrypted = key.encrypt(plaintext).unwrap();
373        let wire_bytes = encrypted.encode();
374
375        // Wire format: nonce (12) + ciphertext (plaintext.len() + 16 tag)
376        let expected_size = 12 + plaintext.len() + 16;
377        assert_eq!(wire_bytes.len(), expected_size);
378        assert_eq!(
379            wire_bytes.len() - plaintext.len(),
380            EncryptedDocument::OVERHEAD
381        );
382    }
383
384    #[test]
385    fn test_debug_redacts_key() {
386        let secret = [0x42u8; 32];
387        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
388
389        let debug_str = format!("{:?}", key);
390        assert!(debug_str.contains("REDACTED"));
391        assert!(!debug_str.contains("42")); // Key bytes not exposed
392    }
393
394    #[test]
395    fn test_realistic_document_size() {
396        let secret = [0x42u8; 32];
397        let key = MeshEncryptionKey::from_shared_secret("DEMO", &secret);
398
399        // Simulate a typical HIVE document (100 bytes)
400        let doc = vec![0xABu8; 100];
401        let encrypted = key.encrypt(&doc).unwrap();
402        let wire_bytes = encrypted.encode();
403
404        // 100 + 28 = 128 bytes
405        assert_eq!(wire_bytes.len(), 128);
406
407        // Well under BLE MTU (244 bytes) and MAX_DOCUMENT_SIZE (512 bytes)
408        assert!(wire_bytes.len() < 244);
409        assert!(wire_bytes.len() < 512);
410    }
411}