Skip to main content

lxmf_core/message/
wire.rs

1use crate::error::LxmfError;
2use crate::message::Payload;
3use alloc::format;
4use alloc::string::String;
5use alloc::string::ToString;
6use alloc::vec;
7use alloc::vec::Vec;
8use base64::Engine;
9use ed25519_dalek::Signature;
10use rand_core::CryptoRngCore;
11use rns_core::crypt::fernet::{Fernet, PlainText, FERNET_MAX_PADDING_SIZE, FERNET_OVERHEAD_SIZE};
12use rns_core::identity::{DerivedKey, Identity, PrivateIdentity, PUBLIC_KEY_LENGTH};
13use serde::Deserialize;
14use sha2::{Digest, Sha256};
15use x25519_dalek::{EphemeralSecret, PublicKey};
16
17pub const SIGNATURE_LENGTH: usize = ed25519_dalek::SIGNATURE_LENGTH;
18pub const LXM_URI_PREFIX: &str = "lxm://";
19const STORAGE_MAGIC: &[u8; 8] = b"LXMFSTR0";
20const STORAGE_VERSION: u8 = 1;
21const STORAGE_FLAG_HAS_SIGNATURE: u8 = 0x01;
22
23#[derive(Debug, Deserialize)]
24struct PythonStorageContainer {
25    lxmf_bytes: serde_bytes::ByteBuf,
26}
27
28#[derive(Debug, Clone)]
29pub struct WireMessage {
30    pub destination: [u8; 16],
31    pub source: [u8; 16],
32    pub signature: Option<[u8; SIGNATURE_LENGTH]>,
33    pub payload: Payload,
34}
35
36impl WireMessage {
37    pub fn new(destination: [u8; 16], source: [u8; 16], payload: Payload) -> Self {
38        Self { destination, source, signature: None, payload }
39    }
40
41    pub fn message_id(&self) -> [u8; 32] {
42        let mut hasher = Sha256::new();
43        hasher.update(self.destination);
44        hasher.update(self.source);
45        hasher.update(self.payload.to_msgpack_without_stamp().unwrap_or_default());
46        let bytes = hasher.finalize();
47        let mut out = [0u8; 32];
48        out.copy_from_slice(&bytes);
49        out
50    }
51
52    pub fn sign(&mut self, signer: &PrivateIdentity) -> Result<(), LxmfError> {
53        let payload = self.payload.to_msgpack_without_stamp()?;
54        let mut data = Vec::with_capacity(16 + 16 + payload.len() + 32);
55        data.extend_from_slice(&self.destination);
56        data.extend_from_slice(&self.source);
57        data.extend_from_slice(&payload);
58        data.extend_from_slice(&self.message_id());
59
60        let signature = signer.sign(&data);
61        self.signature = Some(signature.to_bytes());
62        Ok(())
63    }
64
65    pub fn verify(&self, identity: &Identity) -> Result<bool, LxmfError> {
66        let Some(sig_bytes) = self.signature else {
67            return Ok(false);
68        };
69        let signature = Signature::from_slice(&sig_bytes)
70            .map_err(|e: ed25519_dalek::SignatureError| LxmfError::Decode(e.to_string()))?;
71
72        let payload = self.payload.to_msgpack_without_stamp()?;
73        let mut data = Vec::with_capacity(16 + 16 + payload.len() + 32);
74        data.extend_from_slice(&self.destination);
75        data.extend_from_slice(&self.source);
76        data.extend_from_slice(&payload);
77        data.extend_from_slice(&self.message_id());
78
79        Ok(identity.verify(&data, &signature).is_ok())
80    }
81
82    pub fn pack(&self) -> Result<Vec<u8>, LxmfError> {
83        let signature =
84            self.signature.ok_or_else(|| LxmfError::Encode("missing signature".into()))?;
85        let mut out = Vec::new();
86        out.extend_from_slice(&self.destination);
87        out.extend_from_slice(&self.source);
88        out.extend_from_slice(&signature);
89        let payload = self.payload.to_msgpack()?;
90        out.extend_from_slice(&payload);
91        Ok(out)
92    }
93
94    pub fn pack_storage(&self) -> Result<Vec<u8>, LxmfError> {
95        let payload = self.payload.to_msgpack()?;
96        let mut out = Vec::with_capacity(
97            STORAGE_MAGIC.len()
98                + 1
99                + 1
100                + 16
101                + 16
102                + self.signature.map(|_| SIGNATURE_LENGTH).unwrap_or(0)
103                + payload.len(),
104        );
105        out.extend_from_slice(STORAGE_MAGIC);
106        out.push(STORAGE_VERSION);
107        let mut flags = 0u8;
108        if self.signature.is_some() {
109            flags |= STORAGE_FLAG_HAS_SIGNATURE;
110        }
111        out.push(flags);
112        out.extend_from_slice(&self.destination);
113        out.extend_from_slice(&self.source);
114        if let Some(signature) = self.signature {
115            out.extend_from_slice(&signature);
116        }
117        out.extend_from_slice(&payload);
118        Ok(out)
119    }
120
121    pub fn unpack(bytes: &[u8]) -> Result<Self, LxmfError> {
122        let min_len = 16 + 16 + SIGNATURE_LENGTH;
123        if bytes.len() < min_len {
124            return Err(LxmfError::Decode("wire message too short".into()));
125        }
126        let mut dest = [0u8; 16];
127        let mut src = [0u8; 16];
128        let mut signature = [0u8; SIGNATURE_LENGTH];
129        dest.copy_from_slice(&bytes[0..16]);
130        src.copy_from_slice(&bytes[16..32]);
131        signature.copy_from_slice(&bytes[32..32 + SIGNATURE_LENGTH]);
132        let payload = Payload::from_msgpack(&bytes[32 + SIGNATURE_LENGTH..])?;
133        Ok(Self { destination: dest, source: src, signature: Some(signature), payload })
134    }
135
136    #[cfg(feature = "std")]
137    pub fn unpack_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, LxmfError> {
138        let bytes = std::fs::read(path).map_err(|e| LxmfError::Io(e.to_string()))?;
139        Self::unpack(&bytes)
140    }
141
142    pub fn unpack_storage(bytes: &[u8]) -> Result<Self, LxmfError> {
143        let magic_len = STORAGE_MAGIC.len();
144        if bytes.len() >= magic_len && bytes.starts_with(STORAGE_MAGIC) {
145            if bytes.len() < magic_len + 1 + 1 + 16 + 16 {
146                return Err(LxmfError::Decode("storage message too short".into()));
147            }
148            let version = bytes[magic_len];
149            if version != STORAGE_VERSION {
150                return Err(LxmfError::Decode("unsupported storage version".into()));
151            }
152            let flags = bytes[magic_len + 1];
153            let mut idx = magic_len + 2;
154            let mut dest = [0u8; 16];
155            let mut src = [0u8; 16];
156            dest.copy_from_slice(&bytes[idx..idx + 16]);
157            idx += 16;
158            src.copy_from_slice(&bytes[idx..idx + 16]);
159            idx += 16;
160            let signature = if flags & STORAGE_FLAG_HAS_SIGNATURE != 0 {
161                if bytes.len() < idx + SIGNATURE_LENGTH {
162                    return Err(LxmfError::Decode("storage signature missing".into()));
163                }
164                let mut sig = [0u8; SIGNATURE_LENGTH];
165                sig.copy_from_slice(&bytes[idx..idx + SIGNATURE_LENGTH]);
166                idx += SIGNATURE_LENGTH;
167                Some(sig)
168            } else {
169                None
170            };
171            let payload = Payload::from_msgpack(&bytes[idx..])?;
172            return Ok(Self { destination: dest, source: src, signature, payload });
173        }
174
175        if let Ok(container) = rmp_serde::from_slice::<PythonStorageContainer>(bytes) {
176            return Self::unpack(container.lxmf_bytes.as_ref());
177        }
178
179        Self::unpack(bytes)
180    }
181
182    #[cfg(feature = "std")]
183    pub fn unpack_storage_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, LxmfError> {
184        let bytes = std::fs::read(path).map_err(|e| LxmfError::Io(e.to_string()))?;
185        Self::unpack_storage(&bytes)
186    }
187
188    #[cfg(feature = "std")]
189    pub fn pack_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), LxmfError> {
190        let bytes = self.pack()?;
191        std::fs::write(path, bytes).map_err(|e| LxmfError::Io(e.to_string()))
192    }
193
194    #[cfg(feature = "std")]
195    pub fn pack_storage_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), LxmfError> {
196        let bytes = self.pack_storage()?;
197        std::fs::write(path, bytes).map_err(|e| LxmfError::Io(e.to_string()))
198    }
199
200    pub fn pack_propagation_with_rng<R: CryptoRngCore + Copy>(
201        &self,
202        destination: &Identity,
203        timestamp: f64,
204        rng: R,
205    ) -> Result<Vec<u8>, LxmfError> {
206        let (envelope, _) =
207            self.pack_propagation_with_options_and_rng(destination, timestamp, None, rng)?;
208        Ok(envelope)
209    }
210
211    pub fn pack_propagation_with_options_and_rng<R: CryptoRngCore + Copy>(
212        &self,
213        destination: &Identity,
214        timestamp: f64,
215        propagation_stamp: Option<&[u8]>,
216        rng: R,
217    ) -> Result<(Vec<u8>, [u8; 32]), LxmfError> {
218        let (lxmf_data, transient_id) =
219            self.pack_propagation_transient_with_rng(destination, rng)?;
220        let packed = Self::pack_propagation_envelope(timestamp, &lxmf_data, propagation_stamp)?;
221        Ok((packed, transient_id))
222    }
223
224    pub fn pack_propagation_transient_with_rng<R: CryptoRngCore + Copy>(
225        &self,
226        destination: &Identity,
227        rng: R,
228    ) -> Result<(Vec<u8>, [u8; 32]), LxmfError> {
229        let packed = self.pack()?;
230        let encrypted = encrypt_for_identity(destination, &packed[16..], rng)?;
231
232        let mut lxmf_data = Vec::with_capacity(16 + encrypted.len());
233        lxmf_data.extend_from_slice(&packed[..16]);
234        lxmf_data.extend_from_slice(&encrypted);
235        let transient_id = Sha256::digest(&lxmf_data);
236        let mut transient_id_bytes = [0u8; 32];
237        transient_id_bytes.copy_from_slice(transient_id.as_slice());
238        Ok((lxmf_data, transient_id_bytes))
239    }
240
241    pub fn pack_propagation_envelope(
242        timestamp: f64,
243        lxmf_data: &[u8],
244        propagation_stamp: Option<&[u8]>,
245    ) -> Result<Vec<u8>, LxmfError> {
246        let mut transient_payload = Vec::with_capacity(
247            lxmf_data.len() + propagation_stamp.map(|stamp| stamp.len()).unwrap_or(0),
248        );
249        transient_payload.extend_from_slice(lxmf_data);
250        if let Some(stamp) = propagation_stamp {
251            transient_payload.extend_from_slice(stamp);
252        }
253
254        let envelope = (timestamp, vec![serde_bytes::ByteBuf::from(transient_payload)]);
255        rmp_serde::to_vec(&envelope).map_err(|e| LxmfError::Encode(e.to_string()))
256    }
257
258    pub fn pack_paper_with_rng<R: CryptoRngCore + Copy>(
259        &self,
260        destination: &Identity,
261        rng: R,
262    ) -> Result<Vec<u8>, LxmfError> {
263        let packed = self.pack()?;
264        let encrypted = encrypt_for_identity(destination, &packed[16..], rng)?;
265        let mut out = Vec::with_capacity(16 + encrypted.len());
266        out.extend_from_slice(&packed[..16]);
267        out.extend_from_slice(&encrypted);
268        Ok(out)
269    }
270
271    pub fn pack_paper_uri_with_rng<R: CryptoRngCore + Copy>(
272        &self,
273        destination: &Identity,
274        rng: R,
275    ) -> Result<String, LxmfError> {
276        let packed = self.pack_paper_with_rng(destination, rng)?;
277        Ok(Self::encode_lxm_uri(&packed))
278    }
279
280    pub fn encode_lxm_uri(paper_bytes: &[u8]) -> String {
281        let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(paper_bytes);
282        format!("{LXM_URI_PREFIX}{encoded}")
283    }
284
285    pub fn decode_lxm_uri(uri: &str) -> Result<Vec<u8>, LxmfError> {
286        let encoded = uri
287            .strip_prefix(LXM_URI_PREFIX)
288            .ok_or_else(|| LxmfError::Decode("invalid lxm uri prefix".into()))?;
289
290        base64::engine::general_purpose::URL_SAFE_NO_PAD
291            .decode(encoded)
292            .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(encoded))
293            .map_err(|e| LxmfError::Decode(format!("invalid lxm uri payload: {e}")))
294    }
295}
296
297fn encrypt_for_identity<R: CryptoRngCore + Copy>(
298    destination: &Identity,
299    plaintext: &[u8],
300    rng: R,
301) -> Result<Vec<u8>, LxmfError> {
302    let secret = EphemeralSecret::random_from_rng(rng);
303    let ephemeral_public = PublicKey::from(&secret);
304    let shared = secret.diffie_hellman(&destination.public_key);
305    let derived = DerivedKey::new(&shared, Some(destination.address_hash.as_slice()));
306    let key_bytes = derived.as_bytes();
307    let split = key_bytes.len() / 2;
308
309    let fernet = Fernet::new_from_slices(&key_bytes[..split], &key_bytes[split..], rng);
310    // Use shared Fernet bounds from reticulum-rs to avoid token sizing drift.
311    let token_capacity = plaintext.len() + FERNET_OVERHEAD_SIZE + FERNET_MAX_PADDING_SIZE;
312    let mut out = vec![0u8; PUBLIC_KEY_LENGTH + token_capacity];
313    out[..PUBLIC_KEY_LENGTH].copy_from_slice(ephemeral_public.as_bytes());
314    let token = fernet
315        .encrypt(PlainText::from(plaintext), &mut out[PUBLIC_KEY_LENGTH..])
316        .map_err(|e| LxmfError::Encode(format!("{e:?}")))?;
317    let total = PUBLIC_KEY_LENGTH + token.len();
318    out.truncate(total);
319    Ok(out)
320}
321
322#[cfg(test)]
323mod tests {
324    use super::WireMessage;
325    use crate::message::Payload;
326    use rand_core::OsRng;
327    use rns_core::identity::{DecryptIdentity, PrivateIdentity, PUBLIC_KEY_LENGTH};
328    use serde_bytes::ByteBuf;
329    use sha2::{Digest, Sha256};
330    use x25519_dalek::PublicKey;
331
332    fn address_hash_bytes(identity: &PrivateIdentity) -> [u8; 16] {
333        let mut out = [0u8; 16];
334        out.copy_from_slice(identity.address_hash().as_slice());
335        out
336    }
337
338    #[test]
339    fn propagation_pack_derives_transient_id_from_lxm_data() {
340        let sender = PrivateIdentity::new_from_name("propagation-pack-sender");
341        let receiver = PrivateIdentity::new_from_name("propagation-pack-receiver");
342        let payload =
343            Payload::new(1.0, Some(b"content".to_vec()), Some(b"title".to_vec()), None, None);
344        let mut wire =
345            WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
346        wire.sign(&sender).expect("sign");
347
348        let (envelope, transient_id) = wire
349            .pack_propagation_with_options_and_rng(receiver.as_identity(), 2.0, None, OsRng)
350            .expect("pack propagation");
351        let (_timestamp, entries): (f64, Vec<ByteBuf>) =
352            rmp_serde::from_slice(&envelope).expect("decode propagation envelope");
353        assert_eq!(entries.len(), 1);
354
355        let expected = Sha256::digest(entries[0].as_ref());
356        assert_eq!(transient_id.as_slice(), expected.as_slice());
357    }
358
359    #[test]
360    fn propagation_pack_appends_optional_stamp_after_lxm_data() {
361        let sender = PrivateIdentity::new_from_name("propagation-pack-stamp-sender");
362        let receiver = PrivateIdentity::new_from_name("propagation-pack-stamp-receiver");
363        let payload = Payload::new(1.0, Some(vec![0x55; 48]), Some(b"title".to_vec()), None, None);
364        let mut wire =
365            WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
366        wire.sign(&sender).expect("sign");
367
368        let propagation_stamp = vec![0xAB; 32];
369        let (envelope, transient_id) = wire
370            .pack_propagation_with_options_and_rng(
371                receiver.as_identity(),
372                3.0,
373                Some(propagation_stamp.as_slice()),
374                OsRng,
375            )
376            .expect("pack propagation with stamp");
377        let (_timestamp, entries): (f64, Vec<ByteBuf>) =
378            rmp_serde::from_slice(&envelope).expect("decode propagation envelope");
379        let transient_payload = entries[0].as_ref();
380
381        assert!(transient_payload.ends_with(propagation_stamp.as_slice()));
382        let lxm_data = &transient_payload[..transient_payload.len() - propagation_stamp.len()];
383        let expected = Sha256::digest(lxm_data);
384        assert_eq!(transient_id.as_slice(), expected.as_slice());
385    }
386
387    #[test]
388    fn propagation_transient_helper_matches_envelope_transient_id() {
389        let sender = PrivateIdentity::new_from_name("propagation-pack-helper-sender");
390        let receiver = PrivateIdentity::new_from_name("propagation-pack-helper-receiver");
391        let payload = Payload::new(1.0, Some(vec![0x11; 32]), Some(b"title".to_vec()), None, None);
392        let mut wire =
393            WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
394        wire.sign(&sender).expect("sign");
395
396        let (lxmf_data, transient_id) = wire
397            .pack_propagation_transient_with_rng(receiver.as_identity(), OsRng)
398            .expect("pack propagation transient");
399        let propagation_stamp = vec![0xCD; 32];
400        let envelope = WireMessage::pack_propagation_envelope(
401            4.0,
402            &lxmf_data,
403            Some(propagation_stamp.as_slice()),
404        )
405        .expect("pack propagation envelope");
406        let (_timestamp, entries): (f64, Vec<ByteBuf>) =
407            rmp_serde::from_slice(&envelope).expect("decode propagation envelope");
408        let transient_payload = entries[0].as_ref();
409
410        assert!(transient_payload.ends_with(propagation_stamp.as_slice()));
411        assert_eq!(
412            &transient_payload[..transient_payload.len() - propagation_stamp.len()],
413            lxmf_data.as_slice()
414        );
415        let expected = Sha256::digest(&lxmf_data);
416        assert_eq!(transient_id.as_slice(), expected.as_slice());
417    }
418
419    #[test]
420    fn propagation_transient_can_be_decrypted_by_recipient_identity() {
421        let sender = PrivateIdentity::new_from_name("propagation-pack-decrypt-sender");
422        let receiver = PrivateIdentity::new_from_name("propagation-pack-decrypt-receiver");
423        let payload =
424            Payload::new(1.0, Some(b"content".to_vec()), Some(b"title".to_vec()), None, None);
425        let mut wire =
426            WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
427        wire.sign(&sender).expect("sign");
428
429        let packed = wire.pack().expect("pack");
430        let (lxmf_data, _transient_id) = wire
431            .pack_propagation_transient_with_rng(receiver.as_identity(), OsRng)
432            .expect("pack propagation transient");
433        let encrypted = &lxmf_data[16..];
434        let mut ephemeral_pub = [0u8; PUBLIC_KEY_LENGTH];
435        ephemeral_pub.copy_from_slice(&encrypted[..PUBLIC_KEY_LENGTH]);
436        let derived = receiver
437            .derive_key(&PublicKey::from(ephemeral_pub), Some(receiver.address_hash().as_slice()));
438        let mut plaintext = vec![0u8; packed.len()];
439        let decrypted = receiver
440            .decrypt(OsRng, &encrypted[PUBLIC_KEY_LENGTH..], &derived, &mut plaintext)
441            .expect("decrypt propagation payload");
442
443        assert_eq!(&lxmf_data[..16], &packed[..16]);
444        assert_eq!(decrypted, &packed[16..]);
445    }
446
447    #[test]
448    fn unpack_storage_accepts_python_msgpack_container() {
449        let sender = PrivateIdentity::new_from_name("python-storage-sender");
450        let receiver = PrivateIdentity::new_from_name("python-storage-receiver");
451        let payload = Payload::new(
452            1_773_999_123.25,
453            Some(b"content".to_vec()),
454            Some(b"title".to_vec()),
455            None,
456            None,
457        );
458        let mut wire =
459            WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
460        wire.sign(&sender).expect("sign");
461
462        let packed_wire = wire.pack().expect("pack");
463        let python_container = rmp_serde::to_vec(&rmpv::Value::Map(vec![
464            (rmpv::Value::String("state".into()), rmpv::Value::Integer(4_i64.into())),
465            (rmpv::Value::String("lxmf_bytes".into()), rmpv::Value::Binary(packed_wire.clone())),
466            (rmpv::Value::String("transport_encrypted".into()), rmpv::Value::Boolean(true)),
467            (
468                rmpv::Value::String("transport_encryption".into()),
469                rmpv::Value::String("Curve25519".into()),
470            ),
471            (rmpv::Value::String("method".into()), rmpv::Value::Integer(2_i64.into())),
472        ]))
473        .expect("pack python container");
474
475        let unpacked = WireMessage::unpack_storage(&python_container).expect("unpack storage");
476        assert_eq!(unpacked.destination, wire.destination);
477        assert_eq!(unpacked.source, wire.source);
478        assert_eq!(unpacked.signature, wire.signature);
479        assert_eq!(
480            unpacked.payload.to_msgpack().expect("payload msgpack"),
481            wire.payload.to_msgpack().expect("payload msgpack")
482        );
483    }
484}