ping-openmls-sdk-core 0.4.1

Platform-agnostic OpenMLS-based messaging engine
Documentation
//! Wire envelope shared by every transport.

use serde::{Deserialize, Serialize};

use crate::{clock::Hlc, conversation::ConversationId, device::DeviceId, WIRE_VERSION};

/// What an envelope carries. Application bytes are opaque; handshake variants are MLS payloads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum MessageKind {
    Application = 1,
    Commit = 2,
    Welcome = 3,
    Proposal = 4,
    KeyPackage = 5,
}

/// On-the-wire envelope. CBOR-encoded.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageEnvelope {
    pub v: u8,
    pub conversation_id: ConversationId,
    pub epoch: u64,
    pub kind: MessageKind,
    pub sender_device: DeviceId,
    pub seq: u64,
    pub hlc: Hlc,
    #[serde(with = "serde_bytes")]
    pub payload: Vec<u8>,
    pub content_hash: [u8; 32],
}

impl MessageEnvelope {
    /// Build an envelope for a handshake message (`Commit` / `Welcome` / `Proposal` /
    /// `KeyPackage`). The `content_hash` is computed over `SHA-256(kind || payload)` —
    /// unchanged across the v=1 → v=2 transition because handshake payloads have no
    /// "plaintext vs ciphertext" distinction.
    ///
    /// For `MessageKind::Application` use [`new_application`](Self::new_application)
    /// instead; passing an application kind here is a bug and tagged by debug_assert.
    pub fn new(
        conversation_id: ConversationId,
        epoch: u64,
        kind: MessageKind,
        sender_device: DeviceId,
        seq: u64,
        hlc: Hlc,
        payload: Vec<u8>,
    ) -> Self {
        debug_assert!(
            !matches!(kind, MessageKind::Application),
            "use MessageEnvelope::new_application for Application kind (CR-6 hashes plaintext)"
        );
        let content_hash = hash_handshake(kind, &payload);
        Self {
            v: WIRE_VERSION,
            conversation_id,
            epoch,
            kind,
            sender_device,
            seq,
            hlc,
            payload,
            content_hash,
        }
    }

    /// Build an envelope for an application message ([CR-6]).
    ///
    /// `content_hash = SHA-256(plaintext)` — keyed to the *application bytes*, not the MLS
    /// ciphertext. This is what makes rebase-on-409 clean: a message that gets resent
    /// against a new epoch keeps the same `content_hash` so app-layer dedup tables remain
    /// consistent. It also gives every binding identical hashes for the same
    /// `AppEvent` because CBOR encoding is canonical.
    ///
    /// `payload` is the MLS ciphertext (what goes on the wire); `plaintext` is the
    /// application-defined bytes that were encrypted. The constructor uses `plaintext`
    /// only for hashing and `payload` for the envelope body.
    pub fn new_application(
        conversation_id: ConversationId,
        epoch: u64,
        sender_device: DeviceId,
        seq: u64,
        hlc: Hlc,
        payload: Vec<u8>,
        plaintext: &[u8],
    ) -> Self {
        let content_hash = hash_application_plaintext(plaintext);
        Self {
            v: WIRE_VERSION,
            conversation_id,
            epoch,
            kind: MessageKind::Application,
            sender_device,
            seq,
            hlc,
            payload,
            content_hash,
        }
    }
}

/// SHA-256 over `kind_byte || payload` — the v=1 hash, used by handshake kinds in both
/// v=1 and v=2 envelopes.
pub fn hash_handshake(kind: MessageKind, payload: &[u8]) -> [u8; 32] {
    use sha2::{Digest, Sha256};
    let mut h = Sha256::new();
    h.update([kind as u8]);
    h.update(payload);
    h.finalize().into()
}

/// SHA-256 over the raw application plaintext — the v=2 hash for `MessageKind::Application`
/// per [CR-6]. No kind byte, no envelope framing; cross-binding parity comes from
/// canonical CBOR of the inner `AppEvent`.
pub fn hash_application_plaintext(plaintext: &[u8]) -> [u8; 32] {
    use sha2::{Digest, Sha256};
    let mut h = Sha256::new();
    h.update(plaintext);
    h.finalize().into()
}

/// Application message handed to the host on receive.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IncomingMessage {
    pub conversation_id: ConversationId,
    pub sender_device: DeviceId,
    pub epoch: u64,
    pub hlc: Hlc,
    /// Application-defined plaintext.
    #[serde(with = "serde_bytes")]
    pub plaintext: Vec<u8>,
    pub content_hash: [u8; 32],
}

/// Application message handed to the SDK to send.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutgoingMessage {
    /// Application-defined plaintext.
    #[serde(with = "serde_bytes")]
    pub plaintext: Vec<u8>,
}