hive_btle/security/
peer_key.rs1#[cfg(not(feature = "std"))]
24use alloc::vec::Vec;
25
26use hkdf::Hkdf;
27use rand_core::OsRng;
28use sha2::Sha256;
29use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
30
31use crate::NodeId;
32
33const PEER_E2EE_HKDF_INFO: &[u8] = b"HIVE-peer-e2ee-v1";
35
36#[derive(Clone)]
41pub struct PeerIdentityKey {
42 secret: StaticSecret,
43 public: PublicKey,
44}
45
46impl PeerIdentityKey {
47 pub fn generate() -> Self {
49 let secret = StaticSecret::random_from_rng(OsRng);
50 let public = PublicKey::from(&secret);
51 Self { secret, public }
52 }
53
54 pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
56 let secret = StaticSecret::from(bytes);
57 let public = PublicKey::from(&secret);
58 Self { secret, public }
59 }
60
61 pub fn public_key_bytes(&self) -> [u8; 32] {
63 self.public.to_bytes()
64 }
65
66 pub fn public_key(&self) -> &PublicKey {
68 &self.public
69 }
70
71 pub fn exchange(&self, peer_public: &PublicKey) -> SharedSecret {
75 let shared = self.secret.diffie_hellman(peer_public);
76 SharedSecret {
77 bytes: shared.to_bytes(),
78 }
79 }
80
81 pub fn exchange_with_bytes(&self, peer_public_bytes: &[u8; 32]) -> SharedSecret {
83 let peer_public = PublicKey::from(*peer_public_bytes);
84 self.exchange(&peer_public)
85 }
86}
87
88impl core::fmt::Debug for PeerIdentityKey {
89 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90 f.debug_struct("PeerIdentityKey")
91 .field("public", &hex_short(&self.public.to_bytes()))
92 .field("secret", &"[REDACTED]")
93 .finish()
94 }
95}
96
97pub struct EphemeralKey {
102 secret: EphemeralSecret,
103 public: PublicKey,
104}
105
106impl EphemeralKey {
107 pub fn generate() -> Self {
109 let secret = EphemeralSecret::random_from_rng(OsRng);
110 let public = PublicKey::from(&secret);
111 Self { secret, public }
112 }
113
114 pub fn public_key_bytes(&self) -> [u8; 32] {
116 self.public.to_bytes()
117 }
118
119 pub fn exchange(self, peer_public: &PublicKey) -> SharedSecret {
121 let shared = self.secret.diffie_hellman(peer_public);
122 SharedSecret {
123 bytes: shared.to_bytes(),
124 }
125 }
126
127 pub fn exchange_with_bytes(self, peer_public_bytes: &[u8; 32]) -> SharedSecret {
129 let peer_public = PublicKey::from(*peer_public_bytes);
130 self.exchange(&peer_public)
131 }
132}
133
134impl core::fmt::Debug for EphemeralKey {
135 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136 f.debug_struct("EphemeralKey")
137 .field("public", &hex_short(&self.public.to_bytes()))
138 .finish()
139 }
140}
141
142pub struct SharedSecret {
147 bytes: [u8; 32],
148}
149
150impl SharedSecret {
151 pub fn derive_session_key(&self, our_node_id: NodeId, peer_node_id: NodeId) -> PeerSessionKey {
164 let (id1, id2) = if our_node_id.as_u32() < peer_node_id.as_u32() {
166 (our_node_id.as_u32(), peer_node_id.as_u32())
167 } else {
168 (peer_node_id.as_u32(), our_node_id.as_u32())
169 };
170
171 let mut salt = [0u8; 8];
173 salt[..4].copy_from_slice(&id1.to_le_bytes());
174 salt[4..].copy_from_slice(&id2.to_le_bytes());
175
176 let hk = Hkdf::<Sha256>::new(Some(&salt), &self.bytes);
178 let mut key = [0u8; 32];
179 hk.expand(PEER_E2EE_HKDF_INFO, &mut key)
180 .expect("32 bytes is valid output length for HKDF-SHA256");
181
182 PeerSessionKey { key }
183 }
184}
185
186impl core::fmt::Debug for SharedSecret {
187 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
188 f.debug_struct("SharedSecret")
189 .field("bytes", &"[REDACTED]")
190 .finish()
191 }
192}
193
194#[derive(Clone)]
199pub struct PeerSessionKey {
200 key: [u8; 32],
201}
202
203impl PeerSessionKey {
204 pub fn as_bytes(&self) -> &[u8; 32] {
206 &self.key
207 }
208}
209
210impl core::fmt::Debug for PeerSessionKey {
211 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212 f.debug_struct("PeerSessionKey")
213 .field("key", &"[REDACTED]")
214 .finish()
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct KeyExchangeMessage {
221 pub sender_node_id: NodeId,
223 pub public_key: [u8; 32],
225 pub is_ephemeral: bool,
227}
228
229impl KeyExchangeMessage {
230 pub fn new(sender_node_id: NodeId, public_key: [u8; 32], is_ephemeral: bool) -> Self {
232 Self {
233 sender_node_id,
234 public_key,
235 is_ephemeral,
236 }
237 }
238
239 pub fn encode(&self) -> Vec<u8> {
243 let mut buf = Vec::with_capacity(37);
244 buf.extend_from_slice(&self.sender_node_id.as_u32().to_le_bytes());
245 buf.push(if self.is_ephemeral { 0x01 } else { 0x00 });
246 buf.extend_from_slice(&self.public_key);
247 buf
248 }
249
250 pub fn decode(data: &[u8]) -> Option<Self> {
252 if data.len() < 37 {
253 return None;
254 }
255
256 let sender_node_id = NodeId::new(u32::from_le_bytes([data[0], data[1], data[2], data[3]]));
257 let is_ephemeral = data[4] & 0x01 != 0;
258 let mut public_key = [0u8; 32];
259 public_key.copy_from_slice(&data[5..37]);
260
261 Some(Self {
262 sender_node_id,
263 public_key,
264 is_ephemeral,
265 })
266 }
267}
268
269fn hex_short(bytes: &[u8]) -> String {
271 if bytes.len() <= 4 {
272 hex::encode(bytes)
273 } else {
274 format!(
275 "{}..{}",
276 hex::encode(&bytes[..2]),
277 hex::encode(&bytes[bytes.len() - 2..])
278 )
279 }
280}
281
282mod hex {
284 pub fn encode(bytes: &[u8]) -> String {
285 bytes.iter().map(|b| format!("{:02x}", b)).collect()
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_identity_key_generation() {
295 let key1 = PeerIdentityKey::generate();
296 let key2 = PeerIdentityKey::generate();
297
298 assert_ne!(key1.public_key_bytes(), key2.public_key_bytes());
300 }
301
302 #[test]
303 fn test_identity_key_from_bytes() {
304 let key1 = PeerIdentityKey::generate();
305 let secret_bytes = key1.secret.to_bytes();
306
307 let key2 = PeerIdentityKey::from_secret_bytes(secret_bytes);
308
309 assert_eq!(key1.public_key_bytes(), key2.public_key_bytes());
311 }
312
313 #[test]
314 fn test_key_exchange_produces_same_shared_secret() {
315 let alice = PeerIdentityKey::generate();
316 let bob = PeerIdentityKey::generate();
317
318 let alice_shared = alice.exchange(bob.public_key());
320
321 let bob_shared = bob.exchange(alice.public_key());
323
324 assert_eq!(alice_shared.bytes, bob_shared.bytes);
326 }
327
328 #[test]
329 fn test_session_key_derivation_is_symmetric() {
330 let alice = PeerIdentityKey::generate();
331 let bob = PeerIdentityKey::generate();
332
333 let alice_node = NodeId::new(0x11111111);
334 let bob_node = NodeId::new(0x22222222);
335
336 let alice_shared = alice.exchange(bob.public_key());
337 let bob_shared = bob.exchange(alice.public_key());
338
339 let alice_session = alice_shared.derive_session_key(alice_node, bob_node);
341 let bob_session = bob_shared.derive_session_key(bob_node, alice_node);
342
343 assert_eq!(alice_session.key, bob_session.key);
345 }
346
347 #[test]
348 fn test_different_peers_get_different_session_keys() {
349 let alice = PeerIdentityKey::generate();
350 let bob = PeerIdentityKey::generate();
351 let charlie = PeerIdentityKey::generate();
352
353 let alice_node = NodeId::new(0x11111111);
354 let bob_node = NodeId::new(0x22222222);
355 let charlie_node = NodeId::new(0x33333333);
356
357 let alice_bob_shared = alice.exchange(bob.public_key());
359 let alice_bob_session = alice_bob_shared.derive_session_key(alice_node, bob_node);
360
361 let alice_charlie_shared = alice.exchange(charlie.public_key());
363 let alice_charlie_session =
364 alice_charlie_shared.derive_session_key(alice_node, charlie_node);
365
366 assert_ne!(alice_bob_session.key, alice_charlie_session.key);
368 }
369
370 #[test]
371 fn test_ephemeral_key_exchange() {
372 let alice_static = PeerIdentityKey::generate();
373 let bob_ephemeral = EphemeralKey::generate();
374
375 let bob_public_bytes = bob_ephemeral.public_key_bytes();
376
377 let alice_shared = alice_static.exchange_with_bytes(&bob_public_bytes);
379
380 let bob_shared = bob_ephemeral.exchange(alice_static.public_key());
382
383 assert_eq!(alice_shared.bytes, bob_shared.bytes);
385 }
386
387 #[test]
388 fn test_key_exchange_message_encode_decode() {
389 let key = PeerIdentityKey::generate();
390 let msg = KeyExchangeMessage::new(NodeId::new(0x12345678), key.public_key_bytes(), true);
391
392 let encoded = msg.encode();
393 assert_eq!(encoded.len(), 37);
394
395 let decoded = KeyExchangeMessage::decode(&encoded).unwrap();
396 assert_eq!(decoded.sender_node_id.as_u32(), 0x12345678);
397 assert_eq!(decoded.public_key, key.public_key_bytes());
398 assert!(decoded.is_ephemeral);
399 }
400
401 #[test]
402 fn test_key_exchange_message_static_flag() {
403 let key = PeerIdentityKey::generate();
404 let msg = KeyExchangeMessage::new(
405 NodeId::new(0xAABBCCDD),
406 key.public_key_bytes(),
407 false, );
409
410 let encoded = msg.encode();
411 let decoded = KeyExchangeMessage::decode(&encoded).unwrap();
412
413 assert!(!decoded.is_ephemeral);
414 }
415
416 #[test]
417 fn test_key_exchange_message_decode_too_short() {
418 let short_data = [0u8; 36]; assert!(KeyExchangeMessage::decode(&short_data).is_none());
420 }
421
422 #[test]
423 fn test_debug_redacts_secrets() {
424 let key = PeerIdentityKey::generate();
425 let debug_str = format!("{:?}", key);
426
427 assert!(debug_str.contains("REDACTED"));
428 }
430}