hive_btle/security/
peer_key.rs

1//! X25519 key exchange for per-peer E2EE
2//!
3//! Provides Diffie-Hellman key exchange using Curve25519 (X25519) to establish
4//! unique shared secrets between specific peer pairs. Combined with ChaCha20-Poly1305,
5//! this enables end-to-end encryption where only the sender and recipient can
6//! decrypt messages - even other mesh members with the formation key cannot read them.
7
8#[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
18/// HKDF info context for per-peer session key derivation
19const PEER_E2EE_HKDF_INFO: &[u8] = b"HIVE-peer-e2ee-v1";
20
21/// A long-term X25519 keypair for peer identity
22///
23/// Used to establish E2EE sessions with other peers. The public key can be
24/// shared freely; the secret key must be kept private.
25#[derive(Clone)]
26pub struct PeerIdentityKey {
27    secret: StaticSecret,
28    public: PublicKey,
29}
30
31impl PeerIdentityKey {
32    /// Generate a new random identity keypair
33    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    /// Create from an existing secret key bytes
40    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    /// Get the public key bytes (safe to share)
47    pub fn public_key_bytes(&self) -> [u8; 32] {
48        self.public.to_bytes()
49    }
50
51    /// Get a reference to the public key
52    pub fn public_key(&self) -> &PublicKey {
53        &self.public
54    }
55
56    /// Perform X25519 key exchange with a peer's public key
57    ///
58    /// Returns a shared secret that both parties will derive identically.
59    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    /// Perform key exchange with peer's public key bytes
67    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
82/// An ephemeral X25519 keypair for forward secrecy
83///
84/// Used for a single key exchange and then discarded. Provides forward secrecy:
85/// if the long-term key is compromised, past sessions remain secure.
86pub struct EphemeralKey {
87    secret: EphemeralSecret,
88    public: PublicKey,
89}
90
91impl EphemeralKey {
92    /// Generate a new random ephemeral keypair
93    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    /// Get the public key bytes (safe to share)
100    pub fn public_key_bytes(&self) -> [u8; 32] {
101        self.public.to_bytes()
102    }
103
104    /// Perform X25519 key exchange (consumes the ephemeral secret)
105    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    /// Perform key exchange with peer's public key bytes (consumes self)
113    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
127/// Raw shared secret from X25519 key exchange
128///
129/// This should be processed through HKDF to derive the actual session key.
130/// Never use the raw shared secret directly for encryption.
131pub struct SharedSecret {
132    bytes: [u8; 32],
133}
134
135impl SharedSecret {
136    /// Derive a session key for peer E2EE communication
137    ///
138    /// Uses HKDF-SHA256 with the node IDs as salt to bind the key to this
139    /// specific peer pair. The node IDs are sorted to ensure both peers
140    /// derive the same key regardless of who initiated.
141    ///
142    /// # Arguments
143    /// * `our_node_id` - Our node identifier
144    /// * `peer_node_id` - Peer's node identifier
145    ///
146    /// # Returns
147    /// A 32-byte session key suitable for ChaCha20-Poly1305
148    pub fn derive_session_key(&self, our_node_id: NodeId, peer_node_id: NodeId) -> PeerSessionKey {
149        // Sort node IDs to ensure both peers derive the same key
150        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        // Create salt from sorted node IDs
157        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        // Derive session key using HKDF
162        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/// Session key for per-peer E2EE encryption
180///
181/// Derived from the X25519 shared secret via HKDF. Used with ChaCha20-Poly1305
182/// for authenticated encryption of peer-to-peer messages.
183#[derive(Clone)]
184pub struct PeerSessionKey {
185    key: [u8; 32],
186}
187
188impl PeerSessionKey {
189    /// Get the raw key bytes for use with ChaCha20-Poly1305
190    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/// Key exchange message sent to initiate or respond to E2EE session
204#[derive(Debug, Clone)]
205pub struct KeyExchangeMessage {
206    /// Sender's node ID
207    pub sender_node_id: NodeId,
208    /// Sender's public key (32 bytes)
209    pub public_key: [u8; 32],
210    /// Whether this is using an ephemeral key (for forward secrecy)
211    pub is_ephemeral: bool,
212}
213
214impl KeyExchangeMessage {
215    /// Create a new key exchange message
216    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    /// Encode to bytes for transmission
225    ///
226    /// Format: sender_node_id(4) | flags(1) | public_key(32) = 37 bytes
227    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    /// Decode from bytes
236    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
254/// Helper to format bytes as short hex for debug output
255fn 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
267// We need hex for debug formatting
268mod 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        // Different keys should have different public keys
284        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        // Same secret should produce same public key
295        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        // Alice computes shared secret with Bob's public key
304        let alice_shared = alice.exchange(bob.public_key());
305
306        // Bob computes shared secret with Alice's public key
307        let bob_shared = bob.exchange(alice.public_key());
308
309        // Both should have the same shared secret
310        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        // Derive session keys (note: different order of node IDs)
325        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        // Both should derive the same session key
329        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        // Alice-Bob session
343        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        // Alice-Charlie session
347        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        // Different peer pairs should have different session keys
352        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        // Alice uses Bob's ephemeral public key
363        let alice_shared = alice_static.exchange_with_bytes(&bob_public_bytes);
364
365        // Bob uses Alice's static public key (consumes ephemeral)
366        let bob_shared = bob_ephemeral.exchange(alice_static.public_key());
367
368        // Both should have the same shared secret
369        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, // static key
393        );
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]; // Need 37 bytes
404        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        // Should not contain raw key bytes
414    }
415}