Skip to main content

ruma_client_api/
backup.rs

1//! Endpoints for server-side key backups.
2
3pub mod add_backup_keys;
4pub mod add_backup_keys_for_room;
5pub mod add_backup_keys_for_session;
6pub mod create_backup_version;
7pub mod delete_backup_keys;
8pub mod delete_backup_keys_for_room;
9pub mod delete_backup_keys_for_session;
10pub mod delete_backup_version;
11pub mod get_backup_info;
12pub mod get_backup_keys;
13pub mod get_backup_keys_for_room;
14pub mod get_backup_keys_for_session;
15pub mod get_latest_backup_info;
16pub mod update_backup_version;
17
18use std::{borrow::Cow, collections::BTreeMap};
19
20use js_int::UInt;
21use ruma_common::{
22    CrossSigningOrDeviceSignatures,
23    serde::{Base64, JsonObject, Raw, from_raw_json_value},
24};
25use serde::{Deserialize, Deserializer, Serialize};
26use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
27
28/// A wrapper around a mapping of session IDs to key data.
29#[derive(Clone, Debug, Serialize, Deserialize)]
30#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
31pub struct RoomKeyBackup {
32    /// A map of session IDs to key data.
33    pub sessions: BTreeMap<String, Raw<KeyBackupData>>,
34}
35
36impl RoomKeyBackup {
37    /// Creates a new `RoomKeyBackup` with the given sessions.
38    pub fn new(sessions: BTreeMap<String, Raw<KeyBackupData>>) -> Self {
39        Self { sessions }
40    }
41}
42
43/// The algorithm used for storing backups.
44#[derive(Clone, Debug, Serialize)]
45#[serde(tag = "algorithm", content = "auth_data")]
46#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
47pub enum BackupAlgorithm {
48    /// `m.megolm_backup.v1.curve25519-aes-sha2` backup algorithm.
49    #[serde(rename = "m.megolm_backup.v1.curve25519-aes-sha2")]
50    MegolmBackupV1Curve25519AesSha2(MegolmBackupV1Curve25519AesSha2AuthData),
51
52    #[doc(hidden)]
53    #[serde(untagged)]
54    _Custom(CustomBackupAlgorithm),
55}
56
57impl BackupAlgorithm {
58    /// Returns a reference to the `algorithm` string.
59    pub fn algorithm(&self) -> &str {
60        match self {
61            Self::MegolmBackupV1Curve25519AesSha2(_) => "m.megolm_backup.v1.curve25519-aes-sha2",
62            Self::_Custom(c) => &c.algorithm,
63        }
64    }
65
66    /// Returns the data of the algorithm.
67    ///
68    /// Prefer to use the public variants of `BackupAlgorithm` where possible; this method is meant
69    /// to be used for custom algorithms only.
70    pub fn auth_data(&self) -> Cow<'_, JsonObject> {
71        fn serialize<T: Serialize>(obj: &T) -> JsonObject {
72            match serde_json::to_value(obj).expect("backup data serialization to succeed") {
73                JsonValue::Object(obj) => obj,
74                _ => panic!("all backup data types must serialize to objects"),
75            }
76        }
77
78        match self {
79            Self::MegolmBackupV1Curve25519AesSha2(d) => Cow::Owned(serialize(d)),
80            Self::_Custom(c) => Cow::Borrowed(&c.auth_data),
81        }
82    }
83}
84
85impl<'de> Deserialize<'de> for BackupAlgorithm {
86    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87    where
88        D: Deserializer<'de>,
89    {
90        #[derive(Deserialize)]
91        struct BackupAlgorithmDeHelper {
92            algorithm: String,
93            auth_data: Box<RawJsonValue>,
94        }
95
96        let BackupAlgorithmDeHelper { algorithm, auth_data } =
97            BackupAlgorithmDeHelper::deserialize(deserializer)?;
98
99        Ok(match algorithm.as_ref() {
100            "m.megolm_backup.v1.curve25519-aes-sha2" => {
101                Self::MegolmBackupV1Curve25519AesSha2(from_raw_json_value(&auth_data)?)
102            }
103            _ => Self::_Custom(CustomBackupAlgorithm {
104                algorithm,
105                auth_data: from_raw_json_value(&auth_data)?,
106            }),
107        })
108    }
109}
110
111impl From<MegolmBackupV1Curve25519AesSha2AuthData> for BackupAlgorithm {
112    fn from(value: MegolmBackupV1Curve25519AesSha2AuthData) -> Self {
113        Self::MegolmBackupV1Curve25519AesSha2(value)
114    }
115}
116
117/// The data for the `m.megolm_backup.v1.curve25519-aes-sha2` backup algorithm.
118#[derive(Clone, Debug, Serialize, Deserialize)]
119#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
120pub struct MegolmBackupV1Curve25519AesSha2AuthData {
121    /// The curve25519 public key used to encrypt the backups, encoded in unpadded base64.
122    pub public_key: Base64,
123
124    /// Signatures of the auth_data as Signed JSON.
125    pub signatures: CrossSigningOrDeviceSignatures,
126}
127
128impl MegolmBackupV1Curve25519AesSha2AuthData {
129    /// Construct a new `MegolmBackupV1Curve25519AesSha2BackupAlgorithm` using the given public key.
130    pub fn new(public_key: Base64) -> Self {
131        Self { public_key, signatures: Default::default() }
132    }
133}
134
135/// The payload for a custom backup algorithm.
136#[doc(hidden)]
137#[derive(Clone, Debug, Serialize)]
138pub struct CustomBackupAlgorithm {
139    /// The backup algorithm.
140    algorithm: String,
141
142    /// The data of the algorithm.
143    auth_data: JsonObject,
144}
145
146/// Information about the backup key.
147///
148/// To create an instance of this type, first create a [`KeyBackupDataInit`] and convert it via
149/// `KeyBackupData::from` / `.into()`.
150#[derive(Clone, Debug, Serialize, Deserialize)]
151#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
152pub struct KeyBackupData {
153    /// The index of the first message in the session that the key can decrypt.
154    pub first_message_index: UInt,
155
156    /// The number of times this key has been forwarded via key-sharing between devices.
157    pub forwarded_count: UInt,
158
159    /// Whether the device backing up the key verified the device that the key is from.
160    pub is_verified: bool,
161
162    /// Encrypted data about the session.
163    pub session_data: EncryptedSessionData,
164}
165
166/// Information about the backup key.
167///
168/// This struct will not be updated even if additional fields are added to [`KeyBackupData`] in a
169/// new (non-breaking) release of the Matrix specification.
170#[derive(Debug)]
171#[allow(clippy::exhaustive_structs)]
172pub struct KeyBackupDataInit {
173    /// The index of the first message in the session that the key can decrypt.
174    pub first_message_index: UInt,
175
176    /// The number of times this key has been forwarded via key-sharing between devices.
177    pub forwarded_count: UInt,
178
179    /// Whether the device backing up the key verified the device that the key is from.
180    pub is_verified: bool,
181
182    /// Encrypted data about the session.
183    pub session_data: EncryptedSessionData,
184}
185
186impl From<KeyBackupDataInit> for KeyBackupData {
187    fn from(init: KeyBackupDataInit) -> Self {
188        let KeyBackupDataInit { first_message_index, forwarded_count, is_verified, session_data } =
189            init;
190        Self { first_message_index, forwarded_count, is_verified, session_data }
191    }
192}
193
194/// The encrypted algorithm-dependent data for backups.
195///
196/// To create an instance of this type, first create an [`EncryptedSessionDataInit`] and convert it
197/// via `EncryptedSessionData::from` / `.into()`.
198#[derive(Clone, Debug, Serialize, Deserialize)]
199#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
200pub struct EncryptedSessionData {
201    /// Unpadded base64-encoded public half of the ephemeral key.
202    pub ephemeral: Base64,
203
204    /// Ciphertext, encrypted using AES-CBC-256 with PKCS#7 padding, encoded in base64.
205    pub ciphertext: Base64,
206
207    /// First 8 bytes of MAC key, encoded in base64.
208    pub mac: Base64,
209}
210
211/// The encrypted algorithm-dependent data for backups.
212///
213/// This struct will not be updated even if additional fields are added to [`EncryptedSessionData`]
214/// in a new (non-breaking) release of the Matrix specification.
215#[derive(Debug)]
216#[allow(clippy::exhaustive_structs)]
217pub struct EncryptedSessionDataInit {
218    /// Unpadded base64-encoded public half of the ephemeral key.
219    pub ephemeral: Base64,
220
221    /// Ciphertext, encrypted using AES-CBC-256 with PKCS#7 padding, encoded in base64.
222    pub ciphertext: Base64,
223
224    /// First 8 bytes of MAC key, encoded in base64.
225    pub mac: Base64,
226}
227
228impl From<EncryptedSessionDataInit> for EncryptedSessionData {
229    fn from(init: EncryptedSessionDataInit) -> Self {
230        let EncryptedSessionDataInit { ephemeral, ciphertext, mac } = init;
231        Self { ephemeral, ciphertext, mac }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use std::borrow::Cow;
238
239    use assert_matches2::{assert_let, assert_matches};
240    use ruma_common::{
241        SigningKeyAlgorithm, SigningKeyId, canonical_json::assert_to_canonical_json_eq,
242        owned_user_id, serde::Base64,
243    };
244    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
245
246    use super::{BackupAlgorithm, MegolmBackupV1Curve25519AesSha2AuthData};
247
248    #[test]
249    fn megolm_v1_backup_algorithm_serialize_roundtrip() {
250        let json = json!({
251            "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
252            "auth_data": {
253                "public_key": "YWJjZGVm",
254                "signatures": {
255                    "@alice:example.org": {
256                        "ed25519:DEVICEID": "signature",
257                    },
258                },
259            },
260        });
261
262        let mut backup_algorithm =
263            MegolmBackupV1Curve25519AesSha2AuthData::new(Base64::new(b"abcdef".to_vec()));
264        backup_algorithm.signatures.insert_signature(
265            owned_user_id!("@alice:example.org"),
266            SigningKeyId::from_parts(SigningKeyAlgorithm::Ed25519, "DEVICEID".into()),
267            "signature".to_owned(),
268        );
269        assert_to_canonical_json_eq!(BackupAlgorithm::from(backup_algorithm), json.clone());
270
271        assert_let!(
272            Ok(BackupAlgorithm::MegolmBackupV1Curve25519AesSha2(auth_data)) = from_json_value(json)
273        );
274        assert_eq!(auth_data.public_key.as_bytes(), b"abcdef");
275        let user_signatures =
276            auth_data.signatures.get(&owned_user_id!("@alice:example.org")).unwrap();
277
278        let mut user_signatures_iter = user_signatures.iter();
279        let (key_id, signature) = user_signatures_iter.next().unwrap();
280        assert_eq!(key_id, "ed25519:DEVICEID");
281        assert_eq!(signature, "signature");
282        assert_matches!(user_signatures_iter.next(), None);
283    }
284
285    #[test]
286    fn custom_backup_algorithm_serialize_roundtrip() {
287        let json = json!({
288            "algorithm": "local.dev.unknown_algorithm",
289            "auth_data": {
290                "foo": "bar",
291                "signatures": {
292                    "ed25519:DEVICEID": "signature",
293                },
294            },
295        });
296
297        let backup_algorithm = from_json_value::<BackupAlgorithm>(json.clone()).unwrap();
298        assert_eq!(backup_algorithm.algorithm(), "local.dev.unknown_algorithm");
299        assert_let!(Cow::Borrowed(auth_data) = backup_algorithm.auth_data());
300
301        assert_let!(Some(JsonValue::String(foo)) = auth_data.get("foo"));
302        assert_eq!(foo, "bar");
303        assert_let!(Some(JsonValue::Object(signatures)) = auth_data.get("signatures"));
304        assert_let!(Some(JsonValue::String(signature)) = signatures.get("ed25519:DEVICEID"));
305        assert_eq!(signature, "signature");
306
307        assert_to_canonical_json_eq!(backup_algorithm, json);
308    }
309}