hive_btle/security/
peer_key.rs1#[cfg(not(feature = "std"))]
9use alloc::vec::Vec;
10
11use hkdf::Hkdf;
12use rand_core::OsRng;
13use sha2::Sha256;
14use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
15
16use crate::NodeId;
17
18const PEER_E2EE_HKDF_INFO: &[u8] = b"HIVE-peer-e2ee-v1";
20
21#[derive(Clone)]
26pub struct PeerIdentityKey {
27 secret: StaticSecret,
28 public: PublicKey,
29}
30
31impl PeerIdentityKey {
32 pub fn generate() -> Self {
34 let secret = StaticSecret::random_from_rng(OsRng);
35 let public = PublicKey::from(&secret);
36 Self { secret, public }
37 }
38
39 pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
41 let secret = StaticSecret::from(bytes);
42 let public = PublicKey::from(&secret);
43 Self { secret, public }
44 }
45
46 pub fn public_key_bytes(&self) -> [u8; 32] {
48 self.public.to_bytes()
49 }
50
51 pub fn public_key(&self) -> &PublicKey {
53 &self.public
54 }
55
56 pub fn exchange(&self, peer_public: &PublicKey) -> SharedSecret {
60 let shared = self.secret.diffie_hellman(peer_public);
61 SharedSecret {
62 bytes: shared.to_bytes(),
63 }
64 }
65
66 pub fn exchange_with_bytes(&self, peer_public_bytes: &[u8; 32]) -> SharedSecret {
68 let peer_public = PublicKey::from(*peer_public_bytes);
69 self.exchange(&peer_public)
70 }
71}
72
73impl core::fmt::Debug for PeerIdentityKey {
74 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75 f.debug_struct("PeerIdentityKey")
76 .field("public", &hex_short(&self.public.to_bytes()))
77 .field("secret", &"[REDACTED]")
78 .finish()
79 }
80}
81
82pub struct EphemeralKey {
87 secret: EphemeralSecret,
88 public: PublicKey,
89}
90
91impl EphemeralKey {
92 pub fn generate() -> Self {
94 let secret = EphemeralSecret::random_from_rng(OsRng);
95 let public = PublicKey::from(&secret);
96 Self { secret, public }
97 }
98
99 pub fn public_key_bytes(&self) -> [u8; 32] {
101 self.public.to_bytes()
102 }
103
104 pub fn exchange(self, peer_public: &PublicKey) -> SharedSecret {
106 let shared = self.secret.diffie_hellman(peer_public);
107 SharedSecret {
108 bytes: shared.to_bytes(),
109 }
110 }
111
112 pub fn exchange_with_bytes(self, peer_public_bytes: &[u8; 32]) -> SharedSecret {
114 let peer_public = PublicKey::from(*peer_public_bytes);
115 self.exchange(&peer_public)
116 }
117}
118
119impl core::fmt::Debug for EphemeralKey {
120 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121 f.debug_struct("EphemeralKey")
122 .field("public", &hex_short(&self.public.to_bytes()))
123 .finish()
124 }
125}
126
127pub struct SharedSecret {
132 bytes: [u8; 32],
133}
134
135impl SharedSecret {
136 pub fn derive_session_key(&self, our_node_id: NodeId, peer_node_id: NodeId) -> PeerSessionKey {
149 let (id1, id2) = if our_node_id.as_u32() < peer_node_id.as_u32() {
151 (our_node_id.as_u32(), peer_node_id.as_u32())
152 } else {
153 (peer_node_id.as_u32(), our_node_id.as_u32())
154 };
155
156 let mut salt = [0u8; 8];
158 salt[..4].copy_from_slice(&id1.to_le_bytes());
159 salt[4..].copy_from_slice(&id2.to_le_bytes());
160
161 let hk = Hkdf::<Sha256>::new(Some(&salt), &self.bytes);
163 let mut key = [0u8; 32];
164 hk.expand(PEER_E2EE_HKDF_INFO, &mut key)
165 .expect("32 bytes is valid output length for HKDF-SHA256");
166
167 PeerSessionKey { key }
168 }
169}
170
171impl core::fmt::Debug for SharedSecret {
172 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
173 f.debug_struct("SharedSecret")
174 .field("bytes", &"[REDACTED]")
175 .finish()
176 }
177}
178
179#[derive(Clone)]
184pub struct PeerSessionKey {
185 key: [u8; 32],
186}
187
188impl PeerSessionKey {
189 pub fn as_bytes(&self) -> &[u8; 32] {
191 &self.key
192 }
193}
194
195impl core::fmt::Debug for PeerSessionKey {
196 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
197 f.debug_struct("PeerSessionKey")
198 .field("key", &"[REDACTED]")
199 .finish()
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct KeyExchangeMessage {
206 pub sender_node_id: NodeId,
208 pub public_key: [u8; 32],
210 pub is_ephemeral: bool,
212}
213
214impl KeyExchangeMessage {
215 pub fn new(sender_node_id: NodeId, public_key: [u8; 32], is_ephemeral: bool) -> Self {
217 Self {
218 sender_node_id,
219 public_key,
220 is_ephemeral,
221 }
222 }
223
224 pub fn encode(&self) -> Vec<u8> {
228 let mut buf = Vec::with_capacity(37);
229 buf.extend_from_slice(&self.sender_node_id.as_u32().to_le_bytes());
230 buf.push(if self.is_ephemeral { 0x01 } else { 0x00 });
231 buf.extend_from_slice(&self.public_key);
232 buf
233 }
234
235 pub fn decode(data: &[u8]) -> Option<Self> {
237 if data.len() < 37 {
238 return None;
239 }
240
241 let sender_node_id = NodeId::new(u32::from_le_bytes([data[0], data[1], data[2], data[3]]));
242 let is_ephemeral = data[4] & 0x01 != 0;
243 let mut public_key = [0u8; 32];
244 public_key.copy_from_slice(&data[5..37]);
245
246 Some(Self {
247 sender_node_id,
248 public_key,
249 is_ephemeral,
250 })
251 }
252}
253
254fn hex_short(bytes: &[u8]) -> String {
256 if bytes.len() <= 4 {
257 hex::encode(bytes)
258 } else {
259 format!(
260 "{}..{}",
261 hex::encode(&bytes[..2]),
262 hex::encode(&bytes[bytes.len() - 2..])
263 )
264 }
265}
266
267mod hex {
269 pub fn encode(bytes: &[u8]) -> String {
270 bytes.iter().map(|b| format!("{:02x}", b)).collect()
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_identity_key_generation() {
280 let key1 = PeerIdentityKey::generate();
281 let key2 = PeerIdentityKey::generate();
282
283 assert_ne!(key1.public_key_bytes(), key2.public_key_bytes());
285 }
286
287 #[test]
288 fn test_identity_key_from_bytes() {
289 let key1 = PeerIdentityKey::generate();
290 let secret_bytes = key1.secret.to_bytes();
291
292 let key2 = PeerIdentityKey::from_secret_bytes(secret_bytes);
293
294 assert_eq!(key1.public_key_bytes(), key2.public_key_bytes());
296 }
297
298 #[test]
299 fn test_key_exchange_produces_same_shared_secret() {
300 let alice = PeerIdentityKey::generate();
301 let bob = PeerIdentityKey::generate();
302
303 let alice_shared = alice.exchange(bob.public_key());
305
306 let bob_shared = bob.exchange(alice.public_key());
308
309 assert_eq!(alice_shared.bytes, bob_shared.bytes);
311 }
312
313 #[test]
314 fn test_session_key_derivation_is_symmetric() {
315 let alice = PeerIdentityKey::generate();
316 let bob = PeerIdentityKey::generate();
317
318 let alice_node = NodeId::new(0x11111111);
319 let bob_node = NodeId::new(0x22222222);
320
321 let alice_shared = alice.exchange(bob.public_key());
322 let bob_shared = bob.exchange(alice.public_key());
323
324 let alice_session = alice_shared.derive_session_key(alice_node, bob_node);
326 let bob_session = bob_shared.derive_session_key(bob_node, alice_node);
327
328 assert_eq!(alice_session.key, bob_session.key);
330 }
331
332 #[test]
333 fn test_different_peers_get_different_session_keys() {
334 let alice = PeerIdentityKey::generate();
335 let bob = PeerIdentityKey::generate();
336 let charlie = PeerIdentityKey::generate();
337
338 let alice_node = NodeId::new(0x11111111);
339 let bob_node = NodeId::new(0x22222222);
340 let charlie_node = NodeId::new(0x33333333);
341
342 let alice_bob_shared = alice.exchange(bob.public_key());
344 let alice_bob_session = alice_bob_shared.derive_session_key(alice_node, bob_node);
345
346 let alice_charlie_shared = alice.exchange(charlie.public_key());
348 let alice_charlie_session =
349 alice_charlie_shared.derive_session_key(alice_node, charlie_node);
350
351 assert_ne!(alice_bob_session.key, alice_charlie_session.key);
353 }
354
355 #[test]
356 fn test_ephemeral_key_exchange() {
357 let alice_static = PeerIdentityKey::generate();
358 let bob_ephemeral = EphemeralKey::generate();
359
360 let bob_public_bytes = bob_ephemeral.public_key_bytes();
361
362 let alice_shared = alice_static.exchange_with_bytes(&bob_public_bytes);
364
365 let bob_shared = bob_ephemeral.exchange(alice_static.public_key());
367
368 assert_eq!(alice_shared.bytes, bob_shared.bytes);
370 }
371
372 #[test]
373 fn test_key_exchange_message_encode_decode() {
374 let key = PeerIdentityKey::generate();
375 let msg = KeyExchangeMessage::new(NodeId::new(0x12345678), key.public_key_bytes(), true);
376
377 let encoded = msg.encode();
378 assert_eq!(encoded.len(), 37);
379
380 let decoded = KeyExchangeMessage::decode(&encoded).unwrap();
381 assert_eq!(decoded.sender_node_id.as_u32(), 0x12345678);
382 assert_eq!(decoded.public_key, key.public_key_bytes());
383 assert!(decoded.is_ephemeral);
384 }
385
386 #[test]
387 fn test_key_exchange_message_static_flag() {
388 let key = PeerIdentityKey::generate();
389 let msg = KeyExchangeMessage::new(
390 NodeId::new(0xAABBCCDD),
391 key.public_key_bytes(),
392 false, );
394
395 let encoded = msg.encode();
396 let decoded = KeyExchangeMessage::decode(&encoded).unwrap();
397
398 assert!(!decoded.is_ephemeral);
399 }
400
401 #[test]
402 fn test_key_exchange_message_decode_too_short() {
403 let short_data = [0u8; 36]; assert!(KeyExchangeMessage::decode(&short_data).is_none());
405 }
406
407 #[test]
408 fn test_debug_redacts_secrets() {
409 let key = PeerIdentityKey::generate();
410 let debug_str = format!("{:?}", key);
411
412 assert!(debug_str.contains("REDACTED"));
413 }
415}