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