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.19/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.19/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.19/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_matches::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    use strass::assert_let;
183
184    use super::{
185        PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
186        SecretStorageV1AesHmacSha2Properties,
187    };
188    use crate::{AnyGlobalAccountDataEvent, EventContentFromType, GlobalAccountDataEvent};
189
190    #[test]
191    fn key_description_serialization() {
192        let mut content = SecretStorageKeyEventContent::new(
193            "my_key".into(),
194            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
195                iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
196                mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
197            }),
198        );
199        content.name = Some("my_key".to_owned());
200
201        assert_to_canonical_json_eq!(
202            content,
203            json!({
204                "name": "my_key",
205                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
206                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
207                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
208            }),
209        );
210    }
211
212    #[test]
213    fn key_description_deserialization() {
214        let json = to_raw_json_value(&json!({
215            "name": "my_key",
216            "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
217            "iv": "YWJjZGVmZ2hpamtsbW5vcA",
218            "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
219        }))
220        .unwrap();
221
222        let content =
223            SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
224        assert_eq!(content.name.unwrap(), "my_key");
225        assert_matches!(content.passphrase, None);
226
227        assert_let!(
228            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
229                SecretStorageV1AesHmacSha2Properties { iv: Some(iv), mac: Some(mac) }
230            ) = content.algorithm
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_let!(
252            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
253                SecretStorageV1AesHmacSha2Properties { iv: Some(iv), mac: Some(mac) }
254            ) = content.algorithm
255        );
256        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
257        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
258    }
259
260    #[test]
261    fn key_description_with_passphrase_serialization() {
262        let mut content = SecretStorageKeyEventContent {
263            passphrase: Some(PassPhrase::new("rocksalt".into(), uint!(8))),
264            ..SecretStorageKeyEventContent::new(
265                "my_key".into(),
266                SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
267                    SecretStorageV1AesHmacSha2Properties {
268                        iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
269                        mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
270                    },
271                ),
272            )
273        };
274        content.name = Some("my_key".to_owned());
275
276        assert_to_canonical_json_eq!(
277            content,
278            json!({
279                "name": "my_key",
280                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
281                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
282                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
283                "passphrase": {
284                    "algorithm": "m.pbkdf2",
285                    "salt": "rocksalt",
286                    "iterations": 8,
287                },
288            }),
289        );
290    }
291
292    #[test]
293    fn key_description_with_passphrase_deserialization() {
294        let json = to_raw_json_value(&json!({
295            "name": "my_key",
296            "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
297            "iv": "YWJjZGVmZ2hpamtsbW5vcA",
298            "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
299            "passphrase": {
300                "algorithm": "m.pbkdf2",
301                "salt": "rocksalt",
302                "iterations": 8,
303                "bits": 256
304            }
305        }))
306        .unwrap();
307
308        let content =
309            SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
310        assert_eq!(content.name.unwrap(), "my_key");
311
312        let passphrase = content.passphrase.unwrap();
313        assert_eq!(passphrase.algorithm, KeyDerivationAlgorithm::Pbkfd2);
314        assert_eq!(passphrase.salt, "rocksalt");
315        assert_eq!(passphrase.iterations, uint!(8));
316        assert_eq!(passphrase.bits, uint!(256));
317
318        assert_let!(
319            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
320                SecretStorageV1AesHmacSha2Properties { iv: Some(iv), mac: Some(mac) }
321            ) = content.algorithm
322        );
323        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
324        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
325    }
326
327    #[test]
328    fn event_content_serialization() {
329        let mut content = SecretStorageKeyEventContent::new(
330            "my_key_id".into(),
331            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
332                iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
333                mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
334            }),
335        );
336        content.name = Some("my_key".to_owned());
337
338        assert_to_canonical_json_eq!(
339            content,
340            json!({
341                "name": "my_key",
342                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
343                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
344                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
345            }),
346        );
347    }
348
349    #[test]
350    fn event_serialization() {
351        let mut content = SecretStorageKeyEventContent::new(
352            "my_key_id".into(),
353            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
354                iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
355                mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
356            }),
357        );
358        content.name = Some("my_key".to_owned());
359        let event = GlobalAccountDataEvent { content };
360
361        assert_to_canonical_json_eq!(
362            event,
363            json!({
364                "type": "m.secret_storage.key.my_key_id",
365                "content": {
366                    "name": "my_key",
367                    "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
368                    "iv": "YWJjZGVmZ2hpamtsbW5vcA",
369                    "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
370                },
371            }),
372        );
373    }
374
375    #[test]
376    fn event_deserialization() {
377        let json = json!({
378            "type": "m.secret_storage.key.my_key_id",
379            "content": {
380                "name": "my_key",
381                "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
382                "iv": "YWJjZGVmZ2hpamtsbW5vcA",
383                "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
384            }
385        });
386
387        let any_ev = from_json_value::<AnyGlobalAccountDataEvent>(json).unwrap();
388        assert_let!(AnyGlobalAccountDataEvent::SecretStorageKey(ev) = any_ev);
389        assert_eq!(ev.content.key_id, "my_key_id");
390        assert_eq!(ev.content.name.unwrap(), "my_key");
391        assert_matches!(ev.content.passphrase, None);
392
393        assert_let!(
394            SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
395                SecretStorageV1AesHmacSha2Properties { iv: Some(iv), mac: Some(mac) }
396            ) = ev.content.algorithm
397        );
398        assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
399        assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
400    }
401
402    #[test]
403    fn custom_algorithm_serialization_roundtrip() {
404        let content_json = json!({
405            "name": "my_key",
406            "algorithm": "io.ruma.custom_alg",
407            "io.ruma.custom_prop1": "YWJjZGVmZ2hpamtsbW5vcA",
408            "io.ruma.custom_prop2": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U",
409        });
410        let event_json = json!({
411            "type": "m.secret_storage.key.my_key_id",
412            "content": content_json,
413        });
414
415        assert_let!(
416            Ok(AnyGlobalAccountDataEvent::SecretStorageKey(event)) = from_json_value(event_json)
417        );
418        let content = &event.content;
419
420        assert_eq!(content.key_id, "my_key_id");
421        assert_eq!(content.name.as_deref(), Some("my_key"));
422        assert_matches!(content.passphrase.as_ref(), None);
423        assert_eq!(content.algorithm.algorithm(), "io.ruma.custom_alg");
424        let properties = &*content.algorithm.properties();
425        assert_eq!(properties.len(), 2);
426        assert_eq!(
427            properties.get("io.ruma.custom_prop1").unwrap().as_str(),
428            Some("YWJjZGVmZ2hpamtsbW5vcA")
429        );
430        assert_eq!(
431            properties.get("io.ruma.custom_prop2").unwrap().as_str(),
432            Some("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U")
433        );
434
435        assert_to_canonical_json_eq!(content, content_json);
436    }
437}