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_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}