hive_btle/security/
mesh_key.rs1#[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#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum EncryptionError {
17 EncryptionFailed,
19 DecryptionFailed,
21 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#[derive(Debug, Clone)]
42pub struct EncryptedDocument {
43 pub nonce: [u8; 12],
45 pub ciphertext: Vec<u8>,
47}
48
49impl EncryptedDocument {
50 pub const OVERHEAD: usize = 12 + 16; 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 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#[derive(Clone)]
84pub struct MeshEncryptionKey {
85 key: [u8; 32],
87}
88
89impl MeshEncryptionKey {
90 const HKDF_INFO: &'static [u8] = b"HIVE-BTLE-mesh-encryption-v1";
92
93 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 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 let mut nonce_bytes = [0u8; 12];
132 OsRng.fill_bytes(&mut nonce_bytes);
133 let nonce = Nonce::from_slice(&nonce_bytes);
134
135 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 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 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 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 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 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 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 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 assert_ne!(encrypted1.nonce, encrypted2.nonce);
263 assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
264
265 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 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 if !encrypted.ciphertext.is_empty() {
296 encrypted.ciphertext[0] ^= 0xFF;
297 }
298
299 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 let wire_bytes = encrypted.encode();
314
315 let decoded = EncryptedDocument::decode(&wire_bytes).unwrap();
317
318 assert_eq!(encrypted.nonce, decoded.nonce);
319 assert_eq!(encrypted.ciphertext, decoded.ciphertext);
320
321 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 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 let short_data = [0u8; 27];
344 assert!(EncryptedDocument::decode(&short_data).is_none());
345
346 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 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")); }
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 let doc = vec![0xABu8; 100];
386 let encrypted = key.encrypt(&doc).unwrap();
387 let wire_bytes = encrypted.encode();
388
389 assert_eq!(wire_bytes.len(), 128);
391
392 assert!(wire_bytes.len() < 244);
394 assert!(wire_bytes.len() < 512);
395 }
396}