hive_btle/security/
mesh_key.rs1#[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#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum EncryptionError {
32 EncryptionFailed,
34 DecryptionFailed,
36 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#[derive(Debug, Clone)]
57pub struct EncryptedDocument {
58 pub nonce: [u8; 12],
60 pub ciphertext: Vec<u8>,
62}
63
64impl EncryptedDocument {
65 pub const OVERHEAD: usize = 12 + 16; 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 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#[derive(Clone)]
99pub struct MeshEncryptionKey {
100 key: [u8; 32],
102}
103
104impl MeshEncryptionKey {
105 const HKDF_INFO: &'static [u8] = b"HIVE-BTLE-mesh-encryption-v1";
107
108 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 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 let mut nonce_bytes = [0u8; 12];
147 OsRng.fill_bytes(&mut nonce_bytes);
148 let nonce = Nonce::from_slice(&nonce_bytes);
149
150 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 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 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 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 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 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 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 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 assert_ne!(encrypted1.nonce, encrypted2.nonce);
278 assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
279
280 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 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 if !encrypted.ciphertext.is_empty() {
311 encrypted.ciphertext[0] ^= 0xFF;
312 }
313
314 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 let wire_bytes = encrypted.encode();
329
330 let decoded = EncryptedDocument::decode(&wire_bytes).unwrap();
332
333 assert_eq!(encrypted.nonce, decoded.nonce);
334 assert_eq!(encrypted.ciphertext, decoded.ciphertext);
335
336 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 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 let short_data = [0u8; 27];
359 assert!(EncryptedDocument::decode(&short_data).is_none());
360
361 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 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")); }
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 let doc = vec![0xABu8; 100];
401 let encrypted = key.encrypt(&doc).unwrap();
402 let wire_bytes = encrypted.encode();
403
404 assert_eq!(wire_bytes.len(), 128);
406
407 assert!(wire_bytes.len() < 244);
409 assert!(wire_bytes.len() < 512);
410 }
411}