lxmf-wire 0.6.0

Core LXMF wire format, message primitives, and identity helpers for LXMF-rs.
Documentation
use super::WireMessage;
use crate::message::{MessageContainer, MessageState, Payload, TransportMethod};
use rand_core::OsRng;
use rns_core::identity::{DecryptIdentity, PrivateIdentity, PUBLIC_KEY_LENGTH};
use serde_bytes::ByteBuf;
use sha2::{Digest, Sha256};
use x25519_dalek::PublicKey;

fn address_hash_bytes(identity: &PrivateIdentity) -> [u8; 16] {
    let mut out = [0u8; 16];
    out.copy_from_slice(identity.address_hash().as_slice());
    out
}

#[test]
fn propagation_pack_derives_transient_id_from_lxm_data() {
    let sender = PrivateIdentity::new_from_name("propagation-pack-sender");
    let receiver = PrivateIdentity::new_from_name("propagation-pack-receiver");
    let payload = Payload::new(1.0, Some(b"content".to_vec()), Some(b"title".to_vec()), None, None);
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let (envelope, transient_id) = wire
        .pack_propagation_with_options_and_rng(receiver.as_identity(), 2.0, None, OsRng)
        .expect("pack propagation");
    let (_timestamp, entries): (f64, Vec<ByteBuf>) =
        rmp_serde::from_slice(&envelope).expect("decode propagation envelope");
    assert_eq!(entries.len(), 1);

    let expected = Sha256::digest(entries[0].as_ref());
    assert_eq!(transient_id.as_slice(), expected.as_slice());
}

#[test]
fn propagation_pack_appends_optional_stamp_after_lxm_data() {
    let sender = PrivateIdentity::new_from_name("propagation-pack-stamp-sender");
    let receiver = PrivateIdentity::new_from_name("propagation-pack-stamp-receiver");
    let payload = Payload::new(1.0, Some(vec![0x55; 48]), Some(b"title".to_vec()), None, None);
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let propagation_stamp = vec![0xAB; 32];
    let (envelope, transient_id) = wire
        .pack_propagation_with_options_and_rng(
            receiver.as_identity(),
            3.0,
            Some(propagation_stamp.as_slice()),
            OsRng,
        )
        .expect("pack propagation with stamp");
    let (_timestamp, entries): (f64, Vec<ByteBuf>) =
        rmp_serde::from_slice(&envelope).expect("decode propagation envelope");
    let transient_payload = entries[0].as_ref();

    assert!(transient_payload.ends_with(propagation_stamp.as_slice()));
    let lxm_data = &transient_payload[..transient_payload.len() - propagation_stamp.len()];
    let expected = Sha256::digest(lxm_data);
    assert_eq!(transient_id.as_slice(), expected.as_slice());
}

#[test]
fn propagation_transient_helper_matches_envelope_transient_id() {
    let sender = PrivateIdentity::new_from_name("propagation-pack-helper-sender");
    let receiver = PrivateIdentity::new_from_name("propagation-pack-helper-receiver");
    let payload = Payload::new(1.0, Some(vec![0x11; 32]), Some(b"title".to_vec()), None, None);
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let (lxmf_data, transient_id) = wire
        .pack_propagation_transient_with_rng(receiver.as_identity(), OsRng)
        .expect("pack propagation transient");
    let propagation_stamp = vec![0xCD; 32];
    let envelope = WireMessage::pack_propagation_envelope(
        4.0,
        &lxmf_data,
        Some(propagation_stamp.as_slice()),
    )
    .expect("pack propagation envelope");
    let (_timestamp, entries): (f64, Vec<ByteBuf>) =
        rmp_serde::from_slice(&envelope).expect("decode propagation envelope");
    let transient_payload = entries[0].as_ref();

    assert!(transient_payload.ends_with(propagation_stamp.as_slice()));
    assert_eq!(
        &transient_payload[..transient_payload.len() - propagation_stamp.len()],
        lxmf_data.as_slice()
    );
    let expected = Sha256::digest(&lxmf_data);
    assert_eq!(transient_id.as_slice(), expected.as_slice());
}

#[test]
fn propagation_transient_can_be_decrypted_by_recipient_identity() {
    let sender = PrivateIdentity::new_from_name("propagation-pack-decrypt-sender");
    let receiver = PrivateIdentity::new_from_name("propagation-pack-decrypt-receiver");
    let payload = Payload::new(1.0, Some(b"content".to_vec()), Some(b"title".to_vec()), None, None);
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let packed = wire.pack().expect("pack");
    let (lxmf_data, _transient_id) = wire
        .pack_propagation_transient_with_rng(receiver.as_identity(), OsRng)
        .expect("pack propagation transient");
    let encrypted = &lxmf_data[16..];
    let mut ephemeral_pub = [0u8; PUBLIC_KEY_LENGTH];
    ephemeral_pub.copy_from_slice(&encrypted[..PUBLIC_KEY_LENGTH]);
    let derived =
        receiver.derive_key(&PublicKey::from(ephemeral_pub), Some(receiver.address_hash().as_slice()));
    let mut plaintext = vec![0u8; packed.len()];
    let decrypted = receiver
        .decrypt(OsRng, &encrypted[PUBLIC_KEY_LENGTH..], &derived, &mut plaintext)
        .expect("decrypt propagation payload");

    assert_eq!(&lxmf_data[..16], &packed[..16]);
    assert_eq!(decrypted, &packed[16..]);
}

#[test]
fn paper_uri_roundtrip_keeps_delivery_hash_separate_from_identity_hash() {
    let sender = PrivateIdentity::new_from_name("paper-pack-sender");
    let receiver = PrivateIdentity::new_from_name("paper-pack-receiver");
    let mut delivery_hash = [0x42u8; 16];
    assert_ne!(delivery_hash.as_slice(), receiver.address_hash().as_slice());
    let payload = Payload::new(1.0, Some(b"content".to_vec()), Some(b"title".to_vec()), None, None);
    let mut wire = WireMessage::new(delivery_hash, address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let uri = wire.pack_paper_uri_with_rng(receiver.as_identity(), OsRng).expect("pack paper uri");
    let unpacked = WireMessage::unpack_paper_uri(&uri, &receiver).expect("unpack paper uri");

    assert_eq!(unpacked.destination, delivery_hash);
    assert_eq!(unpacked.source, wire.source);
    assert_eq!(
        unpacked.payload.to_msgpack().expect("payload msgpack"),
        wire.payload.to_msgpack().expect("payload msgpack")
    );
    delivery_hash[0] ^= 0x01;
    assert_ne!(unpacked.destination, delivery_hash);
}

#[test]
fn unpack_storage_accepts_python_msgpack_container() {
    let sender = PrivateIdentity::new_from_name("python-storage-sender");
    let receiver = PrivateIdentity::new_from_name("python-storage-receiver");
    let payload = Payload::new(
        1_773_999_123.25,
        Some(b"content".to_vec()),
        Some(b"title".to_vec()),
        None,
        None,
    );
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let packed_wire = wire.pack().expect("pack");
    let python_container = rmp_serde::to_vec(&rmpv::Value::Map(vec![
        (rmpv::Value::String("state".into()), rmpv::Value::Integer(4_i64.into())),
        (rmpv::Value::String("lxmf_bytes".into()), rmpv::Value::Binary(packed_wire.clone())),
        (rmpv::Value::String("transport_encrypted".into()), rmpv::Value::Boolean(true)),
        (
            rmpv::Value::String("transport_encryption".into()),
            rmpv::Value::String("Curve25519".into()),
        ),
        (rmpv::Value::String("method".into()), rmpv::Value::Integer(2_i64.into())),
    ]))
    .expect("pack python container");

    let unpacked = WireMessage::unpack_storage(&python_container).expect("unpack storage");
    assert_eq!(unpacked.destination, wire.destination);
    assert_eq!(unpacked.source, wire.source);
    assert_eq!(unpacked.signature, wire.signature);
    assert_eq!(
        unpacked.payload.to_msgpack().expect("payload msgpack"),
        wire.payload.to_msgpack().expect("payload msgpack")
    );
}

#[test]
fn pack_storage_emits_python_msgpack_container() {
    let sender = PrivateIdentity::new_from_name("pack-storage-python-sender");
    let receiver = PrivateIdentity::new_from_name("pack-storage-python-receiver");
    let payload = Payload::new(
        1_774_000_123.5,
        Some(b"content".to_vec()),
        Some(b"title".to_vec()),
        None,
        None,
    );
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let storage = wire.pack_storage().expect("pack storage");
    assert!(!storage.starts_with(b"LXMFSTR0"));

    let container = MessageContainer::from_msgpack(&storage).expect("decode container");
    assert_eq!(container.state, MessageState::Outbound.as_u8());
    assert_eq!(container.method, TransportMethod::Direct.as_u8());
    assert!(!container.transport_encrypted);
    assert_eq!(container.transport_encryption, None);
    assert_eq!(container.lxmf_bytes.as_ref(), wire.pack().expect("pack").as_slice());

    let unpacked = WireMessage::unpack_storage(&storage).expect("unpack storage");
    assert_eq!(unpacked.destination, wire.destination);
    assert_eq!(unpacked.source, wire.source);
    assert_eq!(unpacked.signature, wire.signature);
    assert_eq!(
        unpacked.payload.to_msgpack().expect("payload msgpack"),
        wire.payload.to_msgpack().expect("payload msgpack")
    );
}

#[test]
fn pack_storage_container_preserves_python_metadata() {
    let sender = PrivateIdentity::new_from_name("pack-storage-metadata-sender");
    let receiver = PrivateIdentity::new_from_name("pack-storage-metadata-receiver");
    let payload = Payload::new(
        1_774_000_456.75,
        Some(b"content".to_vec()),
        Some(b"title".to_vec()),
        None,
        None,
    );
    let mut wire =
        WireMessage::new(address_hash_bytes(&receiver), address_hash_bytes(&sender), payload);
    wire.sign(&sender).expect("sign");

    let storage = wire
        .pack_storage_container(
            MessageState::Delivered,
            TransportMethod::Propagated,
            true,
            Some("Curve25519".to_string()),
        )
        .expect("pack storage container");
    let container = MessageContainer::from_msgpack(&storage).expect("decode container");

    assert_eq!(container.state, MessageState::Delivered.as_u8());
    assert_eq!(container.method, TransportMethod::Propagated.as_u8());
    assert!(container.transport_encrypted);
    assert_eq!(container.transport_encryption.as_deref(), Some("Curve25519"));
    assert_eq!(container.lxmf_bytes.as_ref(), wire.pack().expect("pack").as_slice());
    assert!(WireMessage::unpack_storage(&storage).is_ok());
}