hive_btle/security/
peer_key.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! X25519 key exchange for per-peer E2EE
17//!
18//! Provides Diffie-Hellman key exchange using Curve25519 (X25519) to establish
19//! unique shared secrets between specific peer pairs. Combined with ChaCha20-Poly1305,
20//! this enables end-to-end encryption where only the sender and recipient can
21//! decrypt messages - even other mesh members with the formation key cannot read them.
22
23#[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
33/// HKDF info context for per-peer session key derivation
34const PEER_E2EE_HKDF_INFO: &[u8] = b"HIVE-peer-e2ee-v1";
35
36/// A long-term X25519 keypair for peer identity
37///
38/// Used to establish E2EE sessions with other peers. The public key can be
39/// shared freely; the secret key must be kept private.
40#[derive(Clone)]
41pub struct PeerIdentityKey {
42    secret: StaticSecret,
43    public: PublicKey,
44}
45
46impl PeerIdentityKey {
47    /// Generate a new random identity keypair
48    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    /// Create from an existing secret key bytes
55    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    /// Get the public key bytes (safe to share)
62    pub fn public_key_bytes(&self) -> [u8; 32] {
63        self.public.to_bytes()
64    }
65
66    /// Get a reference to the public key
67    pub fn public_key(&self) -> &PublicKey {
68        &self.public
69    }
70
71    /// Perform X25519 key exchange with a peer's public key
72    ///
73    /// Returns a shared secret that both parties will derive identically.
74    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    /// Perform key exchange with peer's public key bytes
82    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
97/// An ephemeral X25519 keypair for forward secrecy
98///
99/// Used for a single key exchange and then discarded. Provides forward secrecy:
100/// if the long-term key is compromised, past sessions remain secure.
101pub struct EphemeralKey {
102    secret: EphemeralSecret,
103    public: PublicKey,
104}
105
106impl EphemeralKey {
107    /// Generate a new random ephemeral keypair
108    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    /// Get the public key bytes (safe to share)
115    pub fn public_key_bytes(&self) -> [u8; 32] {
116        self.public.to_bytes()
117    }
118
119    /// Perform X25519 key exchange (consumes the ephemeral secret)
120    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    /// Perform key exchange with peer's public key bytes (consumes self)
128    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
142/// Raw shared secret from X25519 key exchange
143///
144/// This should be processed through HKDF to derive the actual session key.
145/// Never use the raw shared secret directly for encryption.
146pub struct SharedSecret {
147    bytes: [u8; 32],
148}
149
150impl SharedSecret {
151    /// Derive a session key for peer E2EE communication
152    ///
153    /// Uses HKDF-SHA256 with the node IDs as salt to bind the key to this
154    /// specific peer pair. The node IDs are sorted to ensure both peers
155    /// derive the same key regardless of who initiated.
156    ///
157    /// # Arguments
158    /// * `our_node_id` - Our node identifier
159    /// * `peer_node_id` - Peer's node identifier
160    ///
161    /// # Returns
162    /// A 32-byte session key suitable for ChaCha20-Poly1305
163    pub fn derive_session_key(&self, our_node_id: NodeId, peer_node_id: NodeId) -> PeerSessionKey {
164        // Sort node IDs to ensure both peers derive the same key
165        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        // Create salt from sorted node IDs
172        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        // Derive session key using HKDF
177        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/// Session key for per-peer E2EE encryption
195///
196/// Derived from the X25519 shared secret via HKDF. Used with ChaCha20-Poly1305
197/// for authenticated encryption of peer-to-peer messages.
198#[derive(Clone)]
199pub struct PeerSessionKey {
200    key: [u8; 32],
201}
202
203impl PeerSessionKey {
204    /// Get the raw key bytes for use with ChaCha20-Poly1305
205    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/// Key exchange message sent to initiate or respond to E2EE session
219#[derive(Debug, Clone)]
220pub struct KeyExchangeMessage {
221    /// Sender's node ID
222    pub sender_node_id: NodeId,
223    /// Sender's public key (32 bytes)
224    pub public_key: [u8; 32],
225    /// Whether this is using an ephemeral key (for forward secrecy)
226    pub is_ephemeral: bool,
227}
228
229impl KeyExchangeMessage {
230    /// Create a new key exchange message
231    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    /// Encode to bytes for transmission
240    ///
241    /// Format: sender_node_id(4) | flags(1) | public_key(32) = 37 bytes
242    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    /// Decode from bytes
251    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
269/// Helper to format bytes as short hex for debug output
270fn 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
282// We need hex for debug formatting
283mod 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        // Different keys should have different public keys
299        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        // Same secret should produce same public key
310        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        // Alice computes shared secret with Bob's public key
319        let alice_shared = alice.exchange(bob.public_key());
320
321        // Bob computes shared secret with Alice's public key
322        let bob_shared = bob.exchange(alice.public_key());
323
324        // Both should have the same shared secret
325        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        // Derive session keys (note: different order of node IDs)
340        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        // Both should derive the same session key
344        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        // Alice-Bob session
358        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        // Alice-Charlie session
362        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        // Different peer pairs should have different session keys
367        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        // Alice uses Bob's ephemeral public key
378        let alice_shared = alice_static.exchange_with_bytes(&bob_public_bytes);
379
380        // Bob uses Alice's static public key (consumes ephemeral)
381        let bob_shared = bob_ephemeral.exchange(alice_static.public_key());
382
383        // Both should have the same shared secret
384        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, // static key
408        );
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]; // Need 37 bytes
419        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        // Should not contain raw key bytes
429    }
430}