Skip to main content

xmtp_mls/groups/
message_id.rs

1use xmtp_db::group_intent::IntentKind;
2
3use crate::groups::intents::{IntentError, SendMessageIntentData};
4use prost::Message;
5use xmtp_proto::xmtp::mls::message_contents::PlaintextEnvelope;
6use xmtp_proto::xmtp::mls::message_contents::plaintext_envelope::Content;
7
8/// A message id is a SHA-256 digest: 32 bytes, 64 hex characters.
9const MESSAGE_ID_HEX_LEN: usize = 64;
10
11/// Decodes a message id that a reply, reaction, or deletion references.
12/// Anything other than 64 hex characters, in either case, is not an id.
13/// `hex::decode` strips a `0x` prefix, so the decoded length is checked too.
14pub fn decode_message_reference(reference: &str) -> Option<Vec<u8>> {
15    if reference.len() != MESSAGE_ID_HEX_LEN {
16        return None;
17    }
18    hex::decode(reference)
19        .ok()
20        .filter(|id| id.len() == MESSAGE_ID_HEX_LEN / 2)
21}
22
23/// Relies on a client-created idempotency_key (which could be a timestamp)
24// implements: SEND-002
25pub fn calculate_message_id(
26    group_id: impl AsRef<[u8]>,
27    decrypted_message_bytes: &[u8],
28    idempotency_key: &str,
29) -> Vec<u8> {
30    use sha2::{Digest, Sha256};
31    // Hash the parts in order instead of joining them, so the content,
32    // which can hold a device-sync HMAC root key, is never copied.
33    let separator = b"\t";
34    Sha256::new()
35        .chain_update(group_id.as_ref())
36        .chain_update(separator)
37        .chain_update(idempotency_key.as_bytes())
38        .chain_update(separator)
39        .chain_update(decrypted_message_bytes)
40        .finalize()
41        .to_vec()
42}
43
44/// Calculate the message id for this intent.
45///
46/// # Note
47/// This functions deserializes and decodes a [`PlaintextEnvelope`] from encoded bytes.
48/// It would be costly to call this method while pulling extra data from a
49/// [`PlaintextEnvelope`] elsewhere. The caller should consider combining implementations.
50///
51/// # Returns
52/// Returns [`Option::None`] if `StoredGroupIntent` is not [`IntentKind::SendMessage`] or if
53/// an error occurs during decoding of intent data for [`IntentKind::SendMessage`].
54pub fn calculate_message_id_for_intent(
55    intent: &xmtp_db::group_intent::StoredGroupIntent,
56) -> Result<Option<Vec<u8>>, IntentError> {
57    if intent.kind != IntentKind::SendMessage {
58        return Ok(None);
59    }
60
61    let data = SendMessageIntentData::from_bytes(intent.data.as_slice())?;
62    let envelope: PlaintextEnvelope = PlaintextEnvelope::decode(data.message.as_slice())?;
63
64    // optimistic message should always have a plaintext envelope. Its
65    // content can hold a device-sync HMAC root key; the envelope wipes it
66    // on drop.
67    let Some(Content::V1(v1)) = &envelope.content else {
68        return Ok(None);
69    };
70    Ok(Some(calculate_message_id(
71        intent.group_id,
72        &v1.content,
73        &v1.idempotency_key,
74    )))
75}