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},
24};
25use serde::{Deserialize, Serialize};
26use serde_json::Value as JsonValue;
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, Deserialize)]
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 From<MegolmBackupV1Curve25519AesSha2AuthData> for BackupAlgorithm {
86    fn from(value: MegolmBackupV1Curve25519AesSha2AuthData) -> Self {
87        Self::MegolmBackupV1Curve25519AesSha2(value)
88    }
89}
90
91/// The data for the `m.megolm_backup.v1.curve25519-aes-sha2` backup algorithm.
92#[derive(Clone, Debug, Serialize, Deserialize)]
93#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
94pub struct MegolmBackupV1Curve25519AesSha2AuthData {
95    /// The curve25519 public key used to encrypt the backups, encoded in unpadded base64.
96    pub public_key: Base64,
97
98    /// Signatures of the auth_data as Signed JSON.
99    pub signatures: CrossSigningOrDeviceSignatures,
100}
101
102impl MegolmBackupV1Curve25519AesSha2AuthData {
103    /// Construct a new `MegolmBackupV1Curve25519AesSha2BackupAlgorithm` using the given public key.
104    pub fn new(public_key: Base64) -> Self {
105        Self { public_key, signatures: Default::default() }
106    }
107}
108
109/// The payload for a custom backup algorithm.
110#[doc(hidden)]
111#[derive(Clone, Debug, Deserialize, Serialize)]
112pub struct CustomBackupAlgorithm {
113    /// The backup algorithm.
114    algorithm: String,
115
116    /// The data of the algorithm.
117    auth_data: JsonObject,
118}
119
120/// Information about the backup key.
121///
122/// To create an instance of this type, first create a [`KeyBackupDataInit`] and convert it via
123/// `KeyBackupData::from` / `.into()`.
124#[derive(Clone, Debug, Serialize, Deserialize)]
125#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
126pub struct KeyBackupData {
127    /// The index of the first message in the session that the key can decrypt.
128    pub first_message_index: UInt,
129
130    /// The number of times this key has been forwarded via key-sharing between devices.
131    pub forwarded_count: UInt,
132
133    /// Whether the device backing up the key verified the device that the key is from.
134    pub is_verified: bool,
135
136    /// Encrypted data about the session.
137    pub session_data: EncryptedSessionData,
138}
139
140/// Information about the backup key.
141///
142/// This struct will not be updated even if additional fields are added to [`KeyBackupData`] in a
143/// new (non-breaking) release of the Matrix specification.
144#[derive(Debug)]
145#[allow(clippy::exhaustive_structs)]
146pub struct KeyBackupDataInit {
147    /// The index of the first message in the session that the key can decrypt.
148    pub first_message_index: UInt,
149
150    /// The number of times this key has been forwarded via key-sharing between devices.
151    pub forwarded_count: UInt,
152
153    /// Whether the device backing up the key verified the device that the key is from.
154    pub is_verified: bool,
155
156    /// Encrypted data about the session.
157    pub session_data: EncryptedSessionData,
158}
159
160impl From<KeyBackupDataInit> for KeyBackupData {
161    fn from(init: KeyBackupDataInit) -> Self {
162        let KeyBackupDataInit { first_message_index, forwarded_count, is_verified, session_data } =
163            init;
164        Self { first_message_index, forwarded_count, is_verified, session_data }
165    }
166}
167
168/// The encrypted algorithm-dependent data for backups.
169///
170/// To create an instance of this type, first create an [`EncryptedSessionDataInit`] and convert it
171/// via `EncryptedSessionData::from` / `.into()`.
172#[derive(Clone, Debug, Serialize, Deserialize)]
173#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
174pub struct EncryptedSessionData {
175    /// Unpadded base64-encoded public half of the ephemeral key.
176    pub ephemeral: Base64,
177
178    /// Ciphertext, encrypted using AES-CBC-256 with PKCS#7 padding, encoded in base64.
179    pub ciphertext: Base64,
180
181    /// First 8 bytes of MAC key, encoded in base64.
182    pub mac: Base64,
183}
184
185/// The encrypted algorithm-dependent data for backups.
186///
187/// This struct will not be updated even if additional fields are added to [`EncryptedSessionData`]
188/// in a new (non-breaking) release of the Matrix specification.
189#[derive(Debug)]
190#[allow(clippy::exhaustive_structs)]
191pub struct EncryptedSessionDataInit {
192    /// Unpadded base64-encoded public half of the ephemeral key.
193    pub ephemeral: Base64,
194
195    /// Ciphertext, encrypted using AES-CBC-256 with PKCS#7 padding, encoded in base64.
196    pub ciphertext: Base64,
197
198    /// First 8 bytes of MAC key, encoded in base64.
199    pub mac: Base64,
200}
201
202impl From<EncryptedSessionDataInit> for EncryptedSessionData {
203    fn from(init: EncryptedSessionDataInit) -> Self {
204        let EncryptedSessionDataInit { ephemeral, ciphertext, mac } = init;
205        Self { ephemeral, ciphertext, mac }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use std::borrow::Cow;
212
213    use assert_matches2::assert_matches;
214    use ruma_common::{
215        SigningKeyAlgorithm, SigningKeyId, canonical_json::assert_to_canonical_json_eq,
216        owned_user_id, serde::Base64,
217    };
218    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
219
220    use super::{BackupAlgorithm, MegolmBackupV1Curve25519AesSha2AuthData};
221
222    #[test]
223    fn megolm_v1_backup_algorithm_serialize_roundtrip() {
224        let json = json!({
225            "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
226            "auth_data": {
227                "public_key": "YWJjZGVm",
228                "signatures": {
229                    "@alice:example.org": {
230                        "ed25519:DEVICEID": "signature",
231                    },
232                },
233            },
234        });
235
236        let mut backup_algorithm =
237            MegolmBackupV1Curve25519AesSha2AuthData::new(Base64::new(b"abcdef".to_vec()));
238        backup_algorithm.signatures.insert_signature(
239            owned_user_id!("@alice:example.org"),
240            SigningKeyId::from_parts(SigningKeyAlgorithm::Ed25519, "DEVICEID".into()),
241            "signature".to_owned(),
242        );
243        assert_to_canonical_json_eq!(BackupAlgorithm::from(backup_algorithm), json.clone());
244
245        assert_matches!(
246            from_json_value(json),
247            Ok(BackupAlgorithm::MegolmBackupV1Curve25519AesSha2(auth_data))
248        );
249        assert_eq!(auth_data.public_key.as_bytes(), b"abcdef");
250        assert_matches!(
251            auth_data.signatures.get(&owned_user_id!("@alice:example.org")),
252            Some(user_signatures)
253        );
254
255        let mut user_signatures_iter = user_signatures.iter();
256        assert_matches!(user_signatures_iter.next(), Some((key_id, signature)));
257        assert_eq!(key_id, "ed25519:DEVICEID");
258        assert_eq!(signature, "signature");
259        assert_matches!(user_signatures_iter.next(), None);
260    }
261
262    #[test]
263    fn custom_backup_algorithm_serialize_roundtrip() {
264        let json = json!({
265            "algorithm": "local.dev.unknown_algorithm",
266            "auth_data": {
267                "foo": "bar",
268                "signatures": {
269                    "ed25519:DEVICEID": "signature",
270                },
271            },
272        });
273
274        assert_matches!(from_json_value::<BackupAlgorithm>(json.clone()), Ok(backup_algorithm));
275        assert_eq!(backup_algorithm.algorithm(), "local.dev.unknown_algorithm");
276        assert_matches!(backup_algorithm.auth_data(), Cow::Borrowed(auth_data));
277
278        assert_matches!(auth_data.get("foo"), Some(JsonValue::String(foo)));
279        assert_eq!(foo, "bar");
280        assert_matches!(auth_data.get("signatures"), Some(JsonValue::Object(signatures)));
281        assert_matches!(signatures.get("ed25519:DEVICEID"), Some(JsonValue::String(signature)));
282        assert_eq!(signature, "signature");
283
284        assert_to_canonical_json_eq!(backup_algorithm, json);
285    }
286}