#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use hkdf::Hkdf;
use rand_core::OsRng;
use sha2::Sha256;
use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
use crate::NodeId;
const PEER_E2EE_HKDF_INFO: &[u8] = b"PEAT-peer-e2ee-v1";
#[derive(Clone)]
pub struct PeerIdentityKey {
secret: StaticSecret,
public: PublicKey,
}
impl PeerIdentityKey {
pub fn generate() -> Self {
let secret = StaticSecret::random_from_rng(OsRng);
let public = PublicKey::from(&secret);
Self { secret, public }
}
pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
let secret = StaticSecret::from(bytes);
let public = PublicKey::from(&secret);
Self { secret, public }
}
pub fn public_key_bytes(&self) -> [u8; 32] {
self.public.to_bytes()
}
pub fn public_key(&self) -> &PublicKey {
&self.public
}
pub fn exchange(&self, peer_public: &PublicKey) -> SharedSecret {
let shared = self.secret.diffie_hellman(peer_public);
SharedSecret {
bytes: shared.to_bytes(),
}
}
pub fn exchange_with_bytes(&self, peer_public_bytes: &[u8; 32]) -> SharedSecret {
let peer_public = PublicKey::from(*peer_public_bytes);
self.exchange(&peer_public)
}
}
impl core::fmt::Debug for PeerIdentityKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PeerIdentityKey")
.field("public", &hex_short(&self.public.to_bytes()))
.field("secret", &"[REDACTED]")
.finish()
}
}
pub struct EphemeralKey {
secret: EphemeralSecret,
public: PublicKey,
}
impl EphemeralKey {
pub fn generate() -> Self {
let secret = EphemeralSecret::random_from_rng(OsRng);
let public = PublicKey::from(&secret);
Self { secret, public }
}
pub fn public_key_bytes(&self) -> [u8; 32] {
self.public.to_bytes()
}
pub fn exchange(self, peer_public: &PublicKey) -> SharedSecret {
let shared = self.secret.diffie_hellman(peer_public);
SharedSecret {
bytes: shared.to_bytes(),
}
}
pub fn exchange_with_bytes(self, peer_public_bytes: &[u8; 32]) -> SharedSecret {
let peer_public = PublicKey::from(*peer_public_bytes);
self.exchange(&peer_public)
}
}
impl core::fmt::Debug for EphemeralKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("EphemeralKey")
.field("public", &hex_short(&self.public.to_bytes()))
.finish()
}
}
pub struct SharedSecret {
bytes: [u8; 32],
}
impl SharedSecret {
pub fn derive_session_key(&self, our_node_id: NodeId, peer_node_id: NodeId) -> PeerSessionKey {
let (id1, id2) = if our_node_id.as_u32() < peer_node_id.as_u32() {
(our_node_id.as_u32(), peer_node_id.as_u32())
} else {
(peer_node_id.as_u32(), our_node_id.as_u32())
};
let mut salt = [0u8; 8];
salt[..4].copy_from_slice(&id1.to_le_bytes());
salt[4..].copy_from_slice(&id2.to_le_bytes());
let hk = Hkdf::<Sha256>::new(Some(&salt), &self.bytes);
let mut key = [0u8; 32];
hk.expand(PEER_E2EE_HKDF_INFO, &mut key)
.expect("32 bytes is valid output length for HKDF-SHA256");
PeerSessionKey { key }
}
}
impl core::fmt::Debug for SharedSecret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SharedSecret")
.field("bytes", &"[REDACTED]")
.finish()
}
}
#[derive(Clone)]
pub struct PeerSessionKey {
key: [u8; 32],
}
impl PeerSessionKey {
pub fn as_bytes(&self) -> &[u8; 32] {
&self.key
}
}
impl core::fmt::Debug for PeerSessionKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PeerSessionKey")
.field("key", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone)]
pub struct KeyExchangeMessage {
pub sender_node_id: NodeId,
pub public_key: [u8; 32],
pub is_ephemeral: bool,
}
impl KeyExchangeMessage {
pub fn new(sender_node_id: NodeId, public_key: [u8; 32], is_ephemeral: bool) -> Self {
Self {
sender_node_id,
public_key,
is_ephemeral,
}
}
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(37);
buf.extend_from_slice(&self.sender_node_id.as_u32().to_le_bytes());
buf.push(if self.is_ephemeral { 0x01 } else { 0x00 });
buf.extend_from_slice(&self.public_key);
buf
}
pub fn decode(data: &[u8]) -> Option<Self> {
if data.len() < 37 {
return None;
}
let sender_node_id = NodeId::new(u32::from_le_bytes([data[0], data[1], data[2], data[3]]));
let is_ephemeral = data[4] & 0x01 != 0;
let mut public_key = [0u8; 32];
public_key.copy_from_slice(&data[5..37]);
Some(Self {
sender_node_id,
public_key,
is_ephemeral,
})
}
}
fn hex_short(bytes: &[u8]) -> String {
if bytes.len() <= 4 {
hex::encode(bytes)
} else {
format!(
"{}..{}",
hex::encode(&bytes[..2]),
hex::encode(&bytes[bytes.len() - 2..])
)
}
}
mod hex {
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_key_generation() {
let key1 = PeerIdentityKey::generate();
let key2 = PeerIdentityKey::generate();
assert_ne!(key1.public_key_bytes(), key2.public_key_bytes());
}
#[test]
fn test_identity_key_from_bytes() {
let key1 = PeerIdentityKey::generate();
let secret_bytes = key1.secret.to_bytes();
let key2 = PeerIdentityKey::from_secret_bytes(secret_bytes);
assert_eq!(key1.public_key_bytes(), key2.public_key_bytes());
}
#[test]
fn test_key_exchange_produces_same_shared_secret() {
let alice = PeerIdentityKey::generate();
let bob = PeerIdentityKey::generate();
let alice_shared = alice.exchange(bob.public_key());
let bob_shared = bob.exchange(alice.public_key());
assert_eq!(alice_shared.bytes, bob_shared.bytes);
}
#[test]
fn test_session_key_derivation_is_symmetric() {
let alice = PeerIdentityKey::generate();
let bob = PeerIdentityKey::generate();
let alice_node = NodeId::new(0x11111111);
let bob_node = NodeId::new(0x22222222);
let alice_shared = alice.exchange(bob.public_key());
let bob_shared = bob.exchange(alice.public_key());
let alice_session = alice_shared.derive_session_key(alice_node, bob_node);
let bob_session = bob_shared.derive_session_key(bob_node, alice_node);
assert_eq!(alice_session.key, bob_session.key);
}
#[test]
fn test_different_peers_get_different_session_keys() {
let alice = PeerIdentityKey::generate();
let bob = PeerIdentityKey::generate();
let charlie = PeerIdentityKey::generate();
let alice_node = NodeId::new(0x11111111);
let bob_node = NodeId::new(0x22222222);
let charlie_node = NodeId::new(0x33333333);
let alice_bob_shared = alice.exchange(bob.public_key());
let alice_bob_session = alice_bob_shared.derive_session_key(alice_node, bob_node);
let alice_charlie_shared = alice.exchange(charlie.public_key());
let alice_charlie_session =
alice_charlie_shared.derive_session_key(alice_node, charlie_node);
assert_ne!(alice_bob_session.key, alice_charlie_session.key);
}
#[test]
fn test_ephemeral_key_exchange() {
let alice_static = PeerIdentityKey::generate();
let bob_ephemeral = EphemeralKey::generate();
let bob_public_bytes = bob_ephemeral.public_key_bytes();
let alice_shared = alice_static.exchange_with_bytes(&bob_public_bytes);
let bob_shared = bob_ephemeral.exchange(alice_static.public_key());
assert_eq!(alice_shared.bytes, bob_shared.bytes);
}
#[test]
fn test_key_exchange_message_encode_decode() {
let key = PeerIdentityKey::generate();
let msg = KeyExchangeMessage::new(NodeId::new(0x12345678), key.public_key_bytes(), true);
let encoded = msg.encode();
assert_eq!(encoded.len(), 37);
let decoded = KeyExchangeMessage::decode(&encoded).unwrap();
assert_eq!(decoded.sender_node_id.as_u32(), 0x12345678);
assert_eq!(decoded.public_key, key.public_key_bytes());
assert!(decoded.is_ephemeral);
}
#[test]
fn test_key_exchange_message_static_flag() {
let key = PeerIdentityKey::generate();
let msg = KeyExchangeMessage::new(
NodeId::new(0xAABBCCDD),
key.public_key_bytes(),
false, );
let encoded = msg.encode();
let decoded = KeyExchangeMessage::decode(&encoded).unwrap();
assert!(!decoded.is_ephemeral);
}
#[test]
fn test_key_exchange_message_decode_too_short() {
let short_data = [0u8; 36]; assert!(KeyExchangeMessage::decode(&short_data).is_none());
}
#[test]
fn test_debug_redacts_secrets() {
let key = PeerIdentityKey::generate();
let debug_str = format!("{:?}", key);
assert!(debug_str.contains("REDACTED"));
}
}