hive_btle/security/
mesh_key.rs

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