Skip to main content

ruma_events/secret_storage/
key.rs

1//! Types for the [`m.secret_storage.key.*`] event.
2//!
3//! [`m.secret_storage.key.*`]: https://spec.matrix.org/v1.18/client-server-api/#key-storage
4
5use std::borrow::Cow;
6
7use js_int::{UInt, uint};
8use ruma_common::{
9    KeyDerivationAlgorithm,
10    serde::{Base64, JsonObject},
11};
12use serde::{Deserialize, Serialize};
13use serde_json::Value as JsonValue;
14
15mod secret_encryption_algorithm_serde;
16
17use crate::macros::EventContent;
18
19/// A passphrase from which a key is to be derived.
20#[derive(Clone, Debug, Deserialize, Serialize)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22pub struct PassPhrase {
23    /// The algorithm to use to generate the key from the passphrase.
24    ///
25    /// Must be `m.pbkdf2`.
26    pub algorithm: KeyDerivationAlgorithm,
27
28    /// The salt used in PBKDF2.
29    pub salt: String,
30
31    /// The number of iterations to use in PBKDF2.
32    pub iterations: UInt,
33
34    /// The number of bits to generate for the key.
35    ///
36    /// Defaults to 256
37    #[serde(default = "default_bits", skip_serializing_if = "is_default_bits")]
38    pub bits: UInt,
39}
40
41impl PassPhrase {
42    /// Creates a new `PassPhrase` with a given salt and number of iterations.
43    pub fn new(salt: String, iterations: UInt) -> Self {
44        Self { algorithm: KeyDerivationAlgorithm::Pbkfd2, salt, iterations, bits: default_bits() }
45    }
46}
47
48fn default_bits() -> UInt {
49    uint!(256)
50}
51
52fn is_default_bits(val: &UInt) -> bool {
53    *val == default_bits()
54}
55
56/// A key description encrypted using a specified algorithm.
57///
58/// The only algorithm currently specified is `m.secret_storage.v1.aes-hmac-sha2`, so this
59/// essentially represents `AesHmacSha2KeyDescription` in the
60/// [spec](https://spec.matrix.org/v1.18/client-server-api/#msecret_storagev1aes-hmac-sha2).
61#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
62#[derive(Clone, Debug, Serialize, EventContent)]
63#[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)]
64pub struct SecretStorageKeyEventContent {
65    /// The ID of the key.
66    #[ruma_event(type_fragment)]
67    #[serde(skip)]
68    pub key_id: String,
69
70    /// The name of the key.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub name: Option<String>,
73
74    /// The encryption algorithm used for this key.
75    ///
76    /// Currently, only `m.secret_storage.v1.aes-hmac-sha2` is supported.
77    #[serde(flatten)]
78    pub algorithm: SecretStorageEncryptionAlgorithm,
79
80    /// The passphrase from which to generate the key.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub passphrase: Option<PassPhrase>,
83}
84
85impl SecretStorageKeyEventContent {
86    /// Creates a `KeyDescription` with the given name.
87    pub fn new(key_id: String, algorithm: SecretStorageEncryptionAlgorithm) -> Self {
88        Self { key_id, name: None, algorithm, passphrase: None }
89    }
90}
91
92/// An algorithm and its properties, used to encrypt a secret.
93#[derive(Debug, Clone)]
94#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
95pub enum SecretStorageEncryptionAlgorithm {
96    /// Encrypted using the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
97    ///
98    /// Secrets using this method are encrypted using AES-CTR-256 and authenticated using
99    /// HMAC-SHA-256.
100    V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties),
101
102    /// Encrypted using a custom algorithm.
103    #[doc(hidden)]
104    _Custom(CustomSecretEncryptionAlgorithm),
105}
106
107impl SecretStorageEncryptionAlgorithm {
108    /// The `algorithm` string.
109    pub fn algorithm(&self) -> &str {
110        match self {
111            Self::V1AesHmacSha2(_) => "m.secret_storage.v1.aes-hmac-sha2",
112            Self::_Custom(c) => &c.algorithm,
113        }
114    }
115
116    /// The algorithm-specific properties.
117    ///
118    /// The returned JSON object won't contain the `algorithm` field, use [`Self::algorithm()`] to
119    /// access it.
120    ///
121    /// Prefer to use the public variants of `SecretStorageEncryptionAlgorithm` where possible; this
122    /// method is meant to be used for custom algorithms only.
123    pub fn properties(&self) -> Cow<'_, JsonObject> {
124        fn serialize<T: Serialize>(obj: &T) -> JsonObject {
125            match serde_json::to_value(obj).expect("secret properties serialization to succeed") {
126                JsonValue::Object(obj) => obj,
127                _ => panic!("all secret properties must serialize to objects"),
128            }
129        }
130
131        match self {
132            Self::V1AesHmacSha2(p) => Cow::Owned(serialize(p)),
133            Self::_Custom(c) => Cow::Borrowed(&c.properties),
134        }
135    }
136}
137
138/// The key properties for the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
139///
140/// Corresponds to the AES-specific properties of `AesHmacSha2KeyDescription` in the
141/// [spec](https://spec.matrix.org/v1.18/client-server-api/#msecret_storagev1aes-hmac-sha2).
142#[derive(Debug, Clone, Deserialize, Serialize)]
143#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
144pub struct SecretStorageV1AesHmacSha2Properties {
145    /// The 16-byte initialization vector, encoded as base64.
146    pub iv: Option<Base64>,
147
148    /// The MAC, encoded as base64.
149    pub mac: Option<Base64>,
150}
151
152impl SecretStorageV1AesHmacSha2Properties {
153    /// Creates a new `SecretStorageV1AesHmacSha2Properties` with the given
154    /// initialization vector and MAC.
155    pub fn new(iv: Option<Base64>, mac: Option<Base64>) -> Self {
156        Self { iv, mac }
157    }
158}
159
160/// The payload for a custom secret encryption algorithm.
161#[doc(hidden)]
162#[derive(Clone, Debug, Serialize)]
163pub struct CustomSecretEncryptionAlgorithm {
164    /// The encryption algorithm to be used for the key.
165    algorithm: String,
166
167    /// Algorithm-specific properties.
168    #[serde(flatten)]
169    properties: JsonObject,
170}
171
172#[cfg(test)]
173mod tests {
174    use assert_matches2::{assert_let, assert_matches};
175    use js_int::uint;
176    use ruma_common::{
177        KeyDerivationAlgorithm, canonical_json::assert_to_canonical_json_eq, serde::Base64,
178    };
179    use serde_json::{
180        from_value as from_json_value, json, value::to_raw_value as to_raw_json_value,
181    };
182
183    use super::{
184        PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
185        SecretStorageV1AesHmacSha2Properties,
186    };
187    use crate::{AnyGlobalAccountDataEvent, EventContentFromType, GlobalAccountDataEvent};
188
189    #[test]
190    fn key_description_serialization() {
191        let mut content = SecretStorageKeyEventContent::new(
192            "my_key".into(),
193            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
194                iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
195                mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
196            }),
197        );
198        content.name = Some("my_key".to_owned());
199
200        assert_to_canonical_json_eq!(
201            content,
202            json!({
203                "name": "my_key",
204                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
205                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
206                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
207            }),
208        );
209    }
210
211    #[test]
212    fn key_description_deserialization() {
213        let json = to_raw_json_value(&json!({
214            "name": "my_key",
215            "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
216            "iv": "YWJjZGVmZ2hpamtsbW5vcA",
217            "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
218        }))
219        .unwrap();
220
221        let content =
222            SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
223        assert_eq!(content.name.unwrap(), "my_key");
224        assert_matches!(content.passphrase, None);
225
226        assert_matches!(
227            content.algorithm,
228            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
229                iv: Some(iv),
230                mac: Some(mac)
231            })
232        );
233
234        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
235        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
236    }
237
238    #[test]
239    fn key_description_deserialization_without_name() {
240        let json = to_raw_json_value(&json!({
241            "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
242            "iv": "YWJjZGVmZ2hpamtsbW5vcA",
243            "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
244        }))
245        .unwrap();
246
247        let content =
248            SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
249        assert!(content.name.is_none());
250        assert_matches!(content.passphrase, None);
251
252        assert_matches!(
253            content.algorithm,
254            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
255                iv: Some(iv),
256                mac: Some(mac)
257            })
258        );
259        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
260        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
261    }
262
263    #[test]
264    fn key_description_with_passphrase_serialization() {
265        let mut content = SecretStorageKeyEventContent {
266            passphrase: Some(PassPhrase::new("rocksalt".into(), uint!(8))),
267            ..SecretStorageKeyEventContent::new(
268                "my_key".into(),
269                SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
270                    SecretStorageV1AesHmacSha2Properties {
271                        iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
272                        mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
273                    },
274                ),
275            )
276        };
277        content.name = Some("my_key".to_owned());
278
279        assert_to_canonical_json_eq!(
280            content,
281            json!({
282                "name": "my_key",
283                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
284                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
285                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
286                "passphrase": {
287                    "algorithm": "m.pbkdf2",
288                    "salt": "rocksalt",
289                    "iterations": 8,
290                },
291            }),
292        );
293    }
294
295    #[test]
296    fn key_description_with_passphrase_deserialization() {
297        let json = to_raw_json_value(&json!({
298            "name": "my_key",
299            "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
300            "iv": "YWJjZGVmZ2hpamtsbW5vcA",
301            "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
302            "passphrase": {
303                "algorithm": "m.pbkdf2",
304                "salt": "rocksalt",
305                "iterations": 8,
306                "bits": 256
307            }
308        }))
309        .unwrap();
310
311        let content =
312            SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
313        assert_eq!(content.name.unwrap(), "my_key");
314
315        let passphrase = content.passphrase.unwrap();
316        assert_eq!(passphrase.algorithm, KeyDerivationAlgorithm::Pbkfd2);
317        assert_eq!(passphrase.salt, "rocksalt");
318        assert_eq!(passphrase.iterations, uint!(8));
319        assert_eq!(passphrase.bits, uint!(256));
320
321        assert_matches!(
322            content.algorithm,
323            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
324                iv: Some(iv),
325                mac: Some(mac)
326            })
327        );
328        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
329        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
330    }
331
332    #[test]
333    fn event_content_serialization() {
334        let mut content = SecretStorageKeyEventContent::new(
335            "my_key_id".into(),
336            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
337                iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
338                mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
339            }),
340        );
341        content.name = Some("my_key".to_owned());
342
343        assert_to_canonical_json_eq!(
344            content,
345            json!({
346                "name": "my_key",
347                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
348                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
349                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
350            }),
351        );
352    }
353
354    #[test]
355    fn event_serialization() {
356        let mut content = SecretStorageKeyEventContent::new(
357            "my_key_id".into(),
358            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
359                iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
360                mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
361            }),
362        );
363        content.name = Some("my_key".to_owned());
364        let event = GlobalAccountDataEvent { content };
365
366        assert_to_canonical_json_eq!(
367            event,
368            json!({
369                "type": "m.secret_storage.key.my_key_id",
370                "content": {
371                    "name": "my_key",
372                    "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
373                    "iv": "YWJjZGVmZ2hpamtsbW5vcA",
374                    "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
375                },
376            }),
377        );
378    }
379
380    #[test]
381    fn event_deserialization() {
382        let json = json!({
383            "type": "m.secret_storage.key.my_key_id",
384            "content": {
385                "name": "my_key",
386                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
387                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
388                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
389            }
390        });
391
392        let any_ev = from_json_value::<AnyGlobalAccountDataEvent>(json).unwrap();
393        assert_matches!(any_ev, AnyGlobalAccountDataEvent::SecretStorageKey(ev));
394        assert_eq!(ev.content.key_id, "my_key_id");
395        assert_eq!(ev.content.name.unwrap(), "my_key");
396        assert_matches!(ev.content.passphrase, None);
397
398        assert_matches!(
399            ev.content.algorithm,
400            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
401                iv: Some(iv),
402                mac: Some(mac)
403            })
404        );
405        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
406        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
407    }
408
409    #[test]
410    fn custom_algorithm_serialization_roundtrip() {
411        let content_json = json!({
412            "name": "my_key",
413            "algorithm": "io.ruma.custom_alg",
414            "io.ruma.custom_prop1": "YWJjZGVmZ2hpamtsbW5vcA",
415            "io.ruma.custom_prop2": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
416        });
417        let event_json = json!({
418            "type": "m.secret_storage.key.my_key_id",
419            "content": content_json,
420        });
421
422        assert_let!(
423            Ok(AnyGlobalAccountDataEvent::SecretStorageKey(event)) = from_json_value(event_json)
424        );
425        let content = &event.content;
426
427        assert_eq!(content.key_id, "my_key_id");
428        assert_eq!(content.name.as_deref(), Some("my_key"));
429        assert_matches!(content.passphrase.as_ref(), None);
430        assert_eq!(content.algorithm.algorithm(), "io.ruma.custom_alg");
431        let properties = &*content.algorithm.properties();
432        assert_eq!(properties.len(), 2);
433        assert_eq!(
434            properties.get("io.ruma.custom_prop1").unwrap().as_str(),
435            Some("YWJjZGVmZ2hpamtsbW5vcA")
436        );
437        assert_eq!(
438            properties.get("io.ruma.custom_prop2").unwrap().as_str(),
439            Some("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U")
440        );
441
442        assert_to_canonical_json_eq!(content, content_json);
443    }
444}