Skip to main content

ruma_signatures/
hash.rs

1use base64::{Engine, alphabet};
2use ruma_common::{
3    CanonicalJsonObject, CanonicalJsonValue,
4    canonical_json::{CanonicalJsonObjectExt, RedactingSerializer},
5    room_version_rules::{EventIdFormatVersion, RoomVersionRules},
6    serde::{Base64, base64::Standard},
7};
8use sha2::{Digest, Sha256};
9
10use crate::JsonError;
11
12/// The [maximum size allowed] for a PDU.
13///
14/// [maximum size allowed]: https://spec.matrix.org/v1.18/client-server-api/#size-limits
15const MAX_PDU_BYTES: usize = 65_535;
16
17/// The fields to remove from a JSON object when creating a content hash of an event.
18static CONTENT_HASH_FIELDS_TO_REMOVE: &[&str] = &["hashes", "signatures", "unsigned"];
19
20/// The fields to remove from a JSON object when creating a reference hash of an event.
21static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["signatures", "unsigned"];
22
23/// Compute and add the [content hash] to the given event.
24///
25/// This adds or overwrites the `sha256` key in the `hashes` object of the event.
26///
27/// This should only be called when creating a new event.
28///
29/// # Parameters
30///
31/// * `object`: A JSON object to be hashed according to the Matrix specification.
32///
33/// # Errors
34///
35/// Returns an error if the `hashes` key is present and is not an object.
36///
37/// # Examples
38///
39/// ```
40/// use ruma_common::CanonicalJsonObject;
41/// use ruma_signatures::add_content_hash_to_event;
42///
43/// // Deserialize an event from JSON.
44/// let mut event = serde_json::from_str(
45///     r#"{
46///         "room_id": "!x:domain",
47///         "sender": "@a:domain",
48///         "origin": "domain",
49///         "origin_server_ts": 1000000,
50///         "type": "X",
51///         "content": {},
52///         "prev_events": [],
53///         "auth_events": [],
54///         "depth": 3
55///     }"#,
56/// )?;
57///
58/// // Hash the JSON.
59/// add_content_hash_to_event(&mut event)?;
60///
61/// // The hash was added.
62/// assert_eq!(
63///     event,
64///     serde_json::from_str::<CanonicalJsonObject>(
65///         r#"{
66///             "room_id": "!x:domain",
67///             "sender": "@a:domain",
68///             "origin": "domain",
69///             "origin_server_ts": 1000000,
70///             "type": "X",
71///             "content": {},
72///             "prev_events": [],
73///             "auth_events": [],
74///             "depth": 3,
75///             "hashes": {
76///                 "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
77///             }
78///         }"#,
79///     )?
80/// );
81/// # Ok::<(), Box<dyn std::error::Error>>(())
82/// ```
83///
84/// [content hash]: https://spec.matrix.org/v1.18/server-server-api/#calculating-the-content-hash-for-an-event
85pub fn add_content_hash_to_event(object: &mut CanonicalJsonObject) -> Result<(), JsonError> {
86    let hash = content_hash(object)?;
87
88    let hashes = object.get_as_object_or_insert_default("hashes", "hashes")?;
89    hashes.insert("sha256".into(), CanonicalJsonValue::String(hash.encode()));
90
91    Ok(())
92}
93
94/// Computes the [content hash] of the given event.
95///
96/// The content hash of an event covers the complete event including the unredacted contents. It is
97/// used during federation and is described in the Matrix server-server specification.
98///
99/// # Parameters
100///
101/// * `object`: A JSON object to generate a content hash for.
102///
103/// # Errors
104///
105/// Returns an error if the event is too large.
106///
107/// [content hash]: https://spec.matrix.org/v1.18/server-server-api/#calculating-the-content-hash-for-an-event
108pub fn content_hash(object: &CanonicalJsonObject) -> Result<Base64<Standard, [u8; 32]>, JsonError> {
109    let json = RedactingSerializer::new()
110        .custom_redacted_root_fields(CONTENT_HASH_FIELDS_TO_REMOVE)
111        .serialize(object)?;
112
113    if json.len() > MAX_PDU_BYTES {
114        return Err(JsonError::PduTooLarge);
115    }
116
117    let hash = Sha256::digest(json.as_bytes());
118
119    Ok(Base64::new(hash.into()))
120}
121
122/// Computes the [reference hash] of the given event.
123///
124/// The reference hash of an event covers the essential fields of an event, including content
125/// hashes.
126///
127/// When creating a new event, [`add_content_hash_to_event()`] must be called before this function
128/// to add the content hash.
129///
130/// Returns the hash as a base64-encoded string, without padding. The correct character set is used
131/// depending on the room version:
132///
133/// * For room versions 1 and 2, the standard character set is used for sending the reference hash
134///   of the `auth_events` and `prev_events`.
135/// * For room version 3, the standard character set is used for using the reference hash as the
136///   event ID.
137/// * For newer versions, the URL-safe character set is used for using the reference hash as the
138///   event ID.
139///
140/// # Parameters
141///
142/// * `object`: A JSON object to generate a reference hash for.
143/// * `rules`: The rules of the version of the current room.
144///
145/// # Errors
146///
147/// Returns an error if the event is too large or redaction fails.
148///
149/// [reference hash]: https://spec.matrix.org/v1.18/server-server-api#calculating-the-reference-hash-for-an-event
150pub fn reference_hash(
151    object: &CanonicalJsonObject,
152    rules: &RoomVersionRules,
153) -> Result<String, JsonError> {
154    let json = RedactingSerializer::new()
155        .rules(&rules.redaction)
156        .custom_redacted_root_fields(REFERENCE_HASH_FIELDS_TO_REMOVE)
157        .serialize(object)?;
158
159    if json.len() > MAX_PDU_BYTES {
160        return Err(JsonError::PduTooLarge);
161    }
162
163    let hash = Sha256::digest(json.as_bytes());
164
165    let base64_alphabet = match rules.event_id_format {
166        EventIdFormatVersion::V1 | EventIdFormatVersion::V2 => alphabet::STANDARD,
167        // Room versions higher than version 3 are URL-safe base64 encoded
168        _ => alphabet::URL_SAFE,
169    };
170    let base64_engine = base64::engine::GeneralPurpose::new(
171        &base64_alphabet,
172        base64::engine::general_purpose::NO_PAD,
173    );
174
175    Ok(base64_engine.encode(hash))
176}