Skip to main content

lxmf_core/message/
wire.rs

1use crate::error::LxmfError;
2use crate::message::{MessageContainer, MessageState, Payload, TransportMethod};
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 rns_core::ratchets::decrypt_with_identity;
14use serde::Deserialize;
15use serde_bytes::ByteBuf;
16use sha2::{Digest, Sha256};
17use x25519_dalek::{EphemeralSecret, PublicKey};
18
19pub const SIGNATURE_LENGTH: usize = ed25519_dalek::SIGNATURE_LENGTH;
20pub const LXM_URI_PREFIX: &str = "lxm://";
21const STORAGE_MAGIC: &[u8; 8] = b"LXMFSTR0";
22const STORAGE_VERSION: u8 = 1;
23const STORAGE_FLAG_HAS_SIGNATURE: u8 = 0x01;
24
25#[derive(Debug, Deserialize)]
26struct PythonStorageContainer {
27    lxmf_bytes: serde_bytes::ByteBuf,
28}
29
30#[derive(Debug, Clone)]
31pub struct WireMessage {
32    pub destination: [u8; 16],
33    pub source: [u8; 16],
34    pub signature: Option<[u8; SIGNATURE_LENGTH]>,
35    pub payload: Payload,
36}
37
38impl WireMessage {
39    pub fn new(destination: [u8; 16], source: [u8; 16], payload: Payload) -> Self {
40        Self { destination, source, signature: None, payload }
41    }
42
43    pub fn message_id(&self) -> [u8; 32] {
44        self.try_message_id()
45            .expect("serializing an in-memory LXMF payload for hashing must succeed")
46    }
47
48    pub fn try_message_id(&self) -> Result<[u8; 32], LxmfError> {
49        let mut hasher = Sha256::new();
50        hasher.update(self.destination);
51        hasher.update(self.source);
52        hasher.update(self.payload.to_msgpack_without_stamp()?);
53        let bytes = hasher.finalize();
54        let mut out = [0u8; 32];
55        out.copy_from_slice(&bytes);
56        Ok(out)
57    }
58
59    pub fn sign(&mut self, signer: &PrivateIdentity) -> Result<(), LxmfError> {
60        let payload = self.payload.to_msgpack_without_stamp()?;
61        let mut data = Vec::with_capacity(16 + 16 + payload.len() + 32);
62        data.extend_from_slice(&self.destination);
63        data.extend_from_slice(&self.source);
64        data.extend_from_slice(&payload);
65        data.extend_from_slice(&self.try_message_id()?);
66
67        let signature = signer.sign(&data);
68        self.signature = Some(signature.to_bytes());
69        Ok(())
70    }
71
72    pub fn verify(&self, identity: &Identity) -> Result<bool, LxmfError> {
73        let Some(sig_bytes) = self.signature else {
74            return Ok(false);
75        };
76        let signature = Signature::from_slice(&sig_bytes)
77            .map_err(|e: ed25519_dalek::SignatureError| LxmfError::Decode(e.to_string()))?;
78
79        let payload = self.payload.to_msgpack_without_stamp()?;
80        let mut data = Vec::with_capacity(16 + 16 + payload.len() + 32);
81        data.extend_from_slice(&self.destination);
82        data.extend_from_slice(&self.source);
83        data.extend_from_slice(&payload);
84        data.extend_from_slice(&self.try_message_id()?);
85
86        Ok(identity.verify(&data, &signature).is_ok())
87    }
88
89    pub fn pack(&self) -> Result<Vec<u8>, LxmfError> {
90        let signature =
91            self.signature.ok_or_else(|| LxmfError::Encode("missing signature".into()))?;
92        let mut out = Vec::new();
93        out.extend_from_slice(&self.destination);
94        out.extend_from_slice(&self.source);
95        out.extend_from_slice(&signature);
96        let payload = self.payload.to_msgpack()?;
97        out.extend_from_slice(&payload);
98        Ok(out)
99    }
100
101    pub fn pack_storage(&self) -> Result<Vec<u8>, LxmfError> {
102        self.pack_storage_container(MessageState::Outbound, TransportMethod::Direct, false, None)
103    }
104
105    pub fn pack_storage_container(
106        &self,
107        state: MessageState,
108        method: TransportMethod,
109        transport_encrypted: bool,
110        transport_encryption: Option<String>,
111    ) -> Result<Vec<u8>, LxmfError> {
112        let container = MessageContainer {
113            state: state.as_u8(),
114            lxmf_bytes: ByteBuf::from(self.pack()?),
115            transport_encrypted,
116            transport_encryption,
117            method: method.as_u8(),
118        };
119        container.to_msgpack()
120    }
121
122    pub fn unpack(bytes: &[u8]) -> Result<Self, LxmfError> {
123        let min_len = 16 + 16 + SIGNATURE_LENGTH;
124        if bytes.len() < min_len {
125            return Err(LxmfError::Decode("wire message too short".into()));
126        }
127        let mut dest = [0u8; 16];
128        let mut src = [0u8; 16];
129        let mut signature = [0u8; SIGNATURE_LENGTH];
130        dest.copy_from_slice(&bytes[0..16]);
131        src.copy_from_slice(&bytes[16..32]);
132        signature.copy_from_slice(&bytes[32..32 + SIGNATURE_LENGTH]);
133        let payload = Payload::from_msgpack(&bytes[32 + SIGNATURE_LENGTH..])?;
134        Ok(Self { destination: dest, source: src, signature: Some(signature), payload })
135    }
136
137    #[cfg(feature = "std")]
138    pub fn unpack_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, LxmfError> {
139        let bytes = std::fs::read(path).map_err(|e| LxmfError::Io(e.to_string()))?;
140        Self::unpack(&bytes)
141    }
142
143    pub fn unpack_storage(bytes: &[u8]) -> Result<Self, LxmfError> {
144        let magic_len = STORAGE_MAGIC.len();
145        if bytes.len() >= magic_len && bytes.starts_with(STORAGE_MAGIC) {
146            if bytes.len() < magic_len + 1 + 1 + 16 + 16 {
147                return Err(LxmfError::Decode("storage message too short".into()));
148            }
149            let version = bytes[magic_len];
150            if version != STORAGE_VERSION {
151                return Err(LxmfError::Decode("unsupported storage version".into()));
152            }
153            let flags = bytes[magic_len + 1];
154            let mut idx = magic_len + 2;
155            let mut dest = [0u8; 16];
156            let mut src = [0u8; 16];
157            dest.copy_from_slice(&bytes[idx..idx + 16]);
158            idx += 16;
159            src.copy_from_slice(&bytes[idx..idx + 16]);
160            idx += 16;
161            let signature = if flags & STORAGE_FLAG_HAS_SIGNATURE != 0 {
162                if bytes.len() < idx + SIGNATURE_LENGTH {
163                    return Err(LxmfError::Decode("storage signature missing".into()));
164                }
165                let mut sig = [0u8; SIGNATURE_LENGTH];
166                sig.copy_from_slice(&bytes[idx..idx + SIGNATURE_LENGTH]);
167                idx += SIGNATURE_LENGTH;
168                Some(sig)
169            } else {
170                None
171            };
172            let payload = Payload::from_msgpack(&bytes[idx..])?;
173            return Ok(Self { destination: dest, source: src, signature, payload });
174        }
175
176        if let Ok(container) = rmp_serde::from_slice::<PythonStorageContainer>(bytes) {
177            return Self::unpack(container.lxmf_bytes.as_ref());
178        }
179
180        Self::unpack(bytes)
181    }
182
183    #[cfg(feature = "std")]
184    pub fn unpack_storage_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, LxmfError> {
185        let bytes = std::fs::read(path).map_err(|e| LxmfError::Io(e.to_string()))?;
186        Self::unpack_storage(&bytes)
187    }
188
189    #[cfg(feature = "std")]
190    pub fn pack_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), LxmfError> {
191        let bytes = self.pack()?;
192        std::fs::write(path, bytes).map_err(|e| LxmfError::Io(e.to_string()))
193    }
194
195    #[cfg(feature = "std")]
196    pub fn pack_storage_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), LxmfError> {
197        let bytes = self.pack_storage()?;
198        std::fs::write(path, bytes).map_err(|e| LxmfError::Io(e.to_string()))
199    }
200
201    pub fn pack_propagation_with_rng<R: CryptoRngCore + Copy>(
202        &self,
203        destination: &Identity,
204        timestamp: f64,
205        rng: R,
206    ) -> Result<Vec<u8>, LxmfError> {
207        let (envelope, _) =
208            self.pack_propagation_with_options_and_rng(destination, timestamp, None, rng)?;
209        Ok(envelope)
210    }
211
212    /// Packs the propagation envelope for `destination`.
213    ///
214    /// `propagation_stamp` is the LXMF proof-of-work anti-spam stamp
215    /// appended in cleartext after the encrypted payload — it is **not**
216    /// a salt or nonce and plays no role in the cryptographic envelope
217    /// (issue #519 audit). Propagation nodes validate it against the
218    /// transient id (see `crate::stamp`); a fixed or absent stamp will be
219    /// rejected by nodes enforcing a stamp cost, so callers should
220    /// generate one with `stamp::generate_propagation_stamp`.
221    pub fn pack_propagation_with_options_and_rng<R: CryptoRngCore + Copy>(
222        &self,
223        destination: &Identity,
224        timestamp: f64,
225        propagation_stamp: Option<&[u8]>,
226        rng: R,
227    ) -> Result<(Vec<u8>, [u8; 32]), LxmfError> {
228        let (lxmf_data, transient_id) =
229            self.pack_propagation_transient_with_rng(destination, rng)?;
230        let packed = Self::pack_propagation_envelope(timestamp, &lxmf_data, propagation_stamp)?;
231        Ok((packed, transient_id))
232    }
233
234    pub fn pack_propagation_transient_with_rng<R: CryptoRngCore + Copy>(
235        &self,
236        destination: &Identity,
237        rng: R,
238    ) -> Result<(Vec<u8>, [u8; 32]), LxmfError> {
239        let packed = self.pack()?;
240        let encrypted = encrypt_for_identity(destination, &packed[16..], rng)?;
241
242        let mut lxmf_data = Vec::with_capacity(16 + encrypted.len());
243        lxmf_data.extend_from_slice(&packed[..16]);
244        lxmf_data.extend_from_slice(&encrypted);
245        let transient_id = Sha256::digest(&lxmf_data);
246        let mut transient_id_bytes = [0u8; 32];
247        transient_id_bytes.copy_from_slice(transient_id.as_slice());
248        Ok((lxmf_data, transient_id_bytes))
249    }
250
251    pub fn pack_propagation_envelope(
252        timestamp: f64,
253        lxmf_data: &[u8],
254        propagation_stamp: Option<&[u8]>,
255    ) -> Result<Vec<u8>, LxmfError> {
256        let mut transient_payload = Vec::with_capacity(
257            lxmf_data.len() + propagation_stamp.map(|stamp| stamp.len()).unwrap_or(0),
258        );
259        transient_payload.extend_from_slice(lxmf_data);
260        if let Some(stamp) = propagation_stamp {
261            transient_payload.extend_from_slice(stamp);
262        }
263
264        let envelope = (timestamp, vec![serde_bytes::ByteBuf::from(transient_payload)]);
265        rmp_serde::to_vec(&envelope).map_err(|e| LxmfError::Encode(e.to_string()))
266    }
267
268    pub fn pack_paper_with_rng<R: CryptoRngCore + Copy>(
269        &self,
270        destination: &Identity,
271        rng: R,
272    ) -> Result<Vec<u8>, LxmfError> {
273        let packed = self.pack()?;
274        let encrypted = encrypt_for_identity(destination, &packed[16..], rng)?;
275        let mut out = Vec::with_capacity(16 + encrypted.len());
276        out.extend_from_slice(&packed[..16]);
277        out.extend_from_slice(&encrypted);
278        Ok(out)
279    }
280
281    pub fn pack_paper_uri_with_rng<R: CryptoRngCore + Copy>(
282        &self,
283        destination: &Identity,
284        rng: R,
285    ) -> Result<String, LxmfError> {
286        let packed = self.pack_paper_with_rng(destination, rng)?;
287        Ok(Self::encode_lxm_uri(&packed))
288    }
289
290    pub fn encode_lxm_uri(paper_bytes: &[u8]) -> String {
291        let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(paper_bytes);
292        format!("{LXM_URI_PREFIX}{encoded}")
293    }
294
295    pub fn decode_lxm_uri(uri: &str) -> Result<Vec<u8>, LxmfError> {
296        let encoded = uri
297            .strip_prefix(LXM_URI_PREFIX)
298            .ok_or_else(|| LxmfError::Decode("invalid lxm uri prefix".into()))?;
299
300        base64::engine::general_purpose::URL_SAFE_NO_PAD
301            .decode(encoded)
302            .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(encoded))
303            .map_err(|e| LxmfError::Decode(format!("invalid lxm uri payload: {e}")))
304    }
305
306    pub fn unpack_paper(
307        paper_bytes: &[u8],
308        recipient: &PrivateIdentity,
309    ) -> Result<Self, LxmfError> {
310        if paper_bytes.len() <= 16 + PUBLIC_KEY_LENGTH {
311            return Err(LxmfError::Decode("paper message too short".into()));
312        }
313
314        let mut destination = [0u8; 16];
315        destination.copy_from_slice(&paper_bytes[..16]);
316
317        let decrypted = decrypt_with_identity(
318            recipient,
319            recipient.address_hash().as_slice(),
320            &paper_bytes[16..],
321        )
322        .map_err(|err| LxmfError::Decode(format!("paper message decrypt failed: {err:?}")))?;
323        let mut wire = Vec::with_capacity(16 + decrypted.len());
324        wire.extend_from_slice(&destination);
325        wire.extend_from_slice(&decrypted);
326        Self::unpack(&wire)
327    }
328
329    pub fn unpack_paper_uri(uri: &str, recipient: &PrivateIdentity) -> Result<Self, LxmfError> {
330        let paper_bytes = Self::decode_lxm_uri(uri)?;
331        Self::unpack_paper(&paper_bytes, recipient)
332    }
333}
334
335fn encrypt_for_identity<R: CryptoRngCore + Copy>(
336    destination: &Identity,
337    plaintext: &[u8],
338    rng: R,
339) -> Result<Vec<u8>, LxmfError> {
340    let secret = EphemeralSecret::random_from_rng(rng);
341    let ephemeral_public = PublicKey::from(&secret);
342    let shared = secret.diffie_hellman(&destination.public_key);
343    let derived = DerivedKey::new(&shared, Some(destination.address_hash.as_slice()));
344    let key_bytes = derived.as_bytes();
345    let split = key_bytes.len() / 2;
346
347    let fernet = Fernet::new_from_slices(&key_bytes[..split], &key_bytes[split..], rng);
348    // Use shared Fernet bounds from reticulum-rs to avoid token sizing drift.
349    let token_capacity = plaintext.len() + FERNET_OVERHEAD_SIZE + FERNET_MAX_PADDING_SIZE;
350    let mut out = vec![0u8; PUBLIC_KEY_LENGTH + token_capacity];
351    out[..PUBLIC_KEY_LENGTH].copy_from_slice(ephemeral_public.as_bytes());
352    let token = fernet
353        .encrypt(PlainText::from(plaintext), &mut out[PUBLIC_KEY_LENGTH..])
354        .map_err(|e| LxmfError::Encode(format!("{e:?}")))?;
355    let total = PUBLIC_KEY_LENGTH + token.len();
356    out.truncate(total);
357    Ok(out)
358}
359
360#[cfg(test)]
361mod tests {
362    include!("wire_tests.rs");
363}