1use 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#[derive(Clone, Debug, Deserialize, Serialize)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22pub struct PassPhrase {
23 pub algorithm: KeyDerivationAlgorithm,
27
28 pub salt: String,
30
31 pub iterations: UInt,
33
34 #[serde(default = "default_bits", skip_serializing_if = "is_default_bits")]
38 pub bits: UInt,
39}
40
41impl PassPhrase {
42 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#[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 #[ruma_event(type_fragment)]
67 #[serde(skip)]
68 pub key_id: String,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub name: Option<String>,
73
74 #[serde(flatten)]
78 pub algorithm: SecretStorageEncryptionAlgorithm,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub passphrase: Option<PassPhrase>,
83}
84
85impl SecretStorageKeyEventContent {
86 pub fn new(key_id: String, algorithm: SecretStorageEncryptionAlgorithm) -> Self {
88 Self { key_id, name: None, algorithm, passphrase: None }
89 }
90}
91
92#[derive(Debug, Clone)]
94#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
95pub enum SecretStorageEncryptionAlgorithm {
96 V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties),
101
102 #[doc(hidden)]
104 _Custom(CustomSecretEncryptionAlgorithm),
105}
106
107impl SecretStorageEncryptionAlgorithm {
108 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 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#[derive(Debug, Clone, Deserialize, Serialize)]
143#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
144pub struct SecretStorageV1AesHmacSha2Properties {
145 pub iv: Option<Base64>,
147
148 pub mac: Option<Base64>,
150}
151
152impl SecretStorageV1AesHmacSha2Properties {
153 pub fn new(iv: Option<Base64>, mac: Option<Base64>) -> Self {
156 Self { iv, mac }
157 }
158}
159
160#[doc(hidden)]
162#[derive(Clone, Debug, Serialize)]
163pub struct CustomSecretEncryptionAlgorithm {
164 algorithm: String,
166
167 #[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}