#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::NodeId;
pub const ENCRYPTED_BEACON_VERSION: u8 = 0x02;
pub const ENCRYPTED_BEACON_SIZE: usize = 21;
const ENCRYPTED_IDENTITY_SIZE: usize = 8;
const MAC_SIZE: usize = 4;
const NONCE_SIZE: usize = 4;
#[derive(Clone)]
pub struct BeaconKey {
key: [u8; 32],
}
impl BeaconKey {
pub fn from_base(base: &[u8; 32]) -> Self {
Self { key: *base }
}
#[cfg(test)]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.key
}
fn derive_keystream(&self, nonce: &[u8; NONCE_SIZE]) -> [u8; ENCRYPTED_IDENTITY_SIZE] {
let mut input = [0u8; 36];
input[..32].copy_from_slice(&self.key);
input[32..].copy_from_slice(nonce);
let hash = blake3::hash(&input);
let mut keystream = [0u8; ENCRYPTED_IDENTITY_SIZE];
keystream.copy_from_slice(&hash.as_bytes()[..ENCRYPTED_IDENTITY_SIZE]);
keystream
}
fn compute_mac(
&self,
nonce: &[u8; NONCE_SIZE],
encrypted: &[u8; ENCRYPTED_IDENTITY_SIZE],
) -> [u8; MAC_SIZE] {
let hash = blake3::keyed_hash(
&self.key,
&[nonce.as_slice(), encrypted.as_slice()].concat(),
);
let mut mac = [0u8; MAC_SIZE];
mac.copy_from_slice(&hash.as_bytes()[..MAC_SIZE]);
mac
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedBeacon {
pub node_id: NodeId,
pub capabilities: u16,
pub hierarchy_level: u8,
pub battery_percent: u8,
}
impl EncryptedBeacon {
pub fn new(
node_id: NodeId,
capabilities: u16,
hierarchy_level: u8,
battery_percent: u8,
) -> Self {
Self {
node_id,
capabilities,
hierarchy_level,
battery_percent,
}
}
pub fn encrypt(&self, key: &BeaconKey, mesh_id_bytes: &[u8; 4]) -> Vec<u8> {
let mut buf = Vec::with_capacity(ENCRYPTED_BEACON_SIZE);
buf.push(ENCRYPTED_BEACON_VERSION);
let mut nonce = [0u8; NONCE_SIZE];
rand_core::OsRng.fill_bytes(&mut nonce);
buf.extend_from_slice(&nonce);
let mut plaintext = [0u8; ENCRYPTED_IDENTITY_SIZE];
plaintext[..4].copy_from_slice(mesh_id_bytes);
plaintext[4..].copy_from_slice(&self.node_id.as_u32().to_be_bytes());
let keystream = key.derive_keystream(&nonce);
let mut encrypted = [0u8; ENCRYPTED_IDENTITY_SIZE];
for i in 0..ENCRYPTED_IDENTITY_SIZE {
encrypted[i] = plaintext[i] ^ keystream[i];
}
buf.extend_from_slice(&encrypted);
let mac = key.compute_mac(&nonce, &encrypted);
buf.extend_from_slice(&mac);
buf.extend_from_slice(&self.capabilities.to_be_bytes());
buf.push(self.hierarchy_level);
buf.push(self.battery_percent);
buf
}
pub fn decrypt(data: &[u8], key: &BeaconKey) -> Option<(Self, [u8; 4])> {
if data.len() < ENCRYPTED_BEACON_SIZE {
return None;
}
if data[0] != ENCRYPTED_BEACON_VERSION {
return None;
}
let mut nonce = [0u8; NONCE_SIZE];
nonce.copy_from_slice(&data[1..5]);
let mut encrypted = [0u8; ENCRYPTED_IDENTITY_SIZE];
encrypted.copy_from_slice(&data[5..13]);
let mut received_mac = [0u8; MAC_SIZE];
received_mac.copy_from_slice(&data[13..17]);
let expected_mac = key.compute_mac(&nonce, &encrypted);
if received_mac != expected_mac {
return None;
}
let keystream = key.derive_keystream(&nonce);
let mut plaintext = [0u8; ENCRYPTED_IDENTITY_SIZE];
for i in 0..ENCRYPTED_IDENTITY_SIZE {
plaintext[i] = encrypted[i] ^ keystream[i];
}
let mut mesh_id_bytes = [0u8; 4];
mesh_id_bytes.copy_from_slice(&plaintext[..4]);
let node_id = NodeId::new(u32::from_be_bytes([
plaintext[4],
plaintext[5],
plaintext[6],
plaintext[7],
]));
let capabilities = u16::from_be_bytes([data[17], data[18]]);
let hierarchy_level = data[19];
let battery_percent = data[20];
Some((
Self {
node_id,
capabilities,
hierarchy_level,
battery_percent,
},
mesh_id_bytes,
))
}
pub fn is_encrypted_beacon(data: &[u8]) -> bool {
data.len() >= ENCRYPTED_BEACON_SIZE && data[0] == ENCRYPTED_BEACON_VERSION
}
}
pub fn mesh_id_to_bytes(mesh_id: &str) -> [u8; 4] {
let hash = blake3::hash(mesh_id.as_bytes());
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&hash.as_bytes()[..4]);
bytes
}
pub const ENCRYPTED_DEVICE_NAME: &str = "PEAT";
use rand_core::RngCore;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_roundtrip() {
let key = BeaconKey::from_base(&[0x42; 32]);
let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
let node_id = NodeId::new(0x12345678);
let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
let encrypted = beacon.encrypt(&key, &mesh_id_bytes);
assert_eq!(encrypted.len(), ENCRYPTED_BEACON_SIZE);
assert_eq!(encrypted[0], ENCRYPTED_BEACON_VERSION);
let (decrypted, decrypted_mesh_id) = EncryptedBeacon::decrypt(&encrypted, &key).unwrap();
assert_eq!(decrypted.node_id, node_id);
assert_eq!(decrypted.capabilities, 0x0F00);
assert_eq!(decrypted.hierarchy_level, 2);
assert_eq!(decrypted.battery_percent, 85);
assert_eq!(decrypted_mesh_id, mesh_id_bytes);
}
#[test]
fn test_wrong_key_fails() {
let key1 = BeaconKey::from_base(&[0x42; 32]);
let key2 = BeaconKey::from_base(&[0x99; 32]);
let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
let node_id = NodeId::new(0x12345678);
let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
let encrypted = beacon.encrypt(&key1, &mesh_id_bytes);
assert!(EncryptedBeacon::decrypt(&encrypted, &key2).is_none());
}
#[test]
fn test_tampered_data_fails() {
let key = BeaconKey::from_base(&[0x42; 32]);
let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
let node_id = NodeId::new(0x12345678);
let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
let mut encrypted = beacon.encrypt(&key, &mesh_id_bytes);
encrypted[7] ^= 0xFF;
assert!(EncryptedBeacon::decrypt(&encrypted, &key).is_none());
}
#[test]
fn test_different_nonces_produce_different_ciphertext() {
let key = BeaconKey::from_base(&[0x42; 32]);
let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
let node_id = NodeId::new(0x12345678);
let beacon = EncryptedBeacon::new(node_id, 0x0F00, 2, 85);
let encrypted1 = beacon.encrypt(&key, &mesh_id_bytes);
let encrypted2 = beacon.encrypt(&key, &mesh_id_bytes);
assert_ne!(&encrypted1[1..5], &encrypted2[1..5]);
assert_ne!(&encrypted1[5..13], &encrypted2[5..13]);
assert!(EncryptedBeacon::decrypt(&encrypted1, &key).is_some());
assert!(EncryptedBeacon::decrypt(&encrypted2, &key).is_some());
}
#[test]
fn test_is_encrypted_beacon() {
let key = BeaconKey::from_base(&[0x42; 32]);
let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
let beacon = EncryptedBeacon::new(NodeId::new(1), 0, 0, 0);
let encrypted = beacon.encrypt(&key, &mesh_id_bytes);
assert!(EncryptedBeacon::is_encrypted_beacon(&encrypted));
assert!(!EncryptedBeacon::is_encrypted_beacon(&[0x01; 21])); assert!(!EncryptedBeacon::is_encrypted_beacon(&[0x02; 10])); }
#[test]
fn test_mesh_id_to_bytes_deterministic() {
let bytes1 = mesh_id_to_bytes("ALPHA-TEAM");
let bytes2 = mesh_id_to_bytes("ALPHA-TEAM");
let bytes3 = mesh_id_to_bytes("BRAVO-TEAM");
assert_eq!(bytes1, bytes2);
assert_ne!(bytes1, bytes3);
}
}