matrix_sdk_crypto/types/
mod.rs

1// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Module containing customized types modeling Matrix keys and events.
16//!
17//! These types were mostly taken from the Ruma project. The types differ in a
18//! couple of important ways to the Ruma types of the same name:
19//!
20//! 1. They are using vodozemac types so we directly deserialize into a
21//!    vodozemac Curve25519 or Ed25519 key.
22//! 2. They support lossless serialization cycles in a canonical JSON supported
23//!    way, meaning the white-space and field order won't be preserved but the
24//!    data will.
25//! 3. Types containing secrets implement the [`Zeroize`] and [`ZeroizeOnDrop`]
26//!    traits to clear out any memory containing secret key material.
27
28use std::{
29    borrow::Borrow,
30    collections::{
31        btree_map::{IntoIter, Iter},
32        BTreeMap,
33    },
34};
35
36use as_variant::as_variant;
37use matrix_sdk_common::deserialized_responses::PrivOwnedStr;
38use ruma::{
39    events::AnyToDeviceEvent,
40    serde::{Raw, StringEnum},
41    DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, OwnedUserId, RoomId, UserId,
42};
43use serde::{Deserialize, Deserializer, Serialize, Serializer};
44use vodozemac::{Curve25519PublicKey, Ed25519PublicKey, Ed25519Signature, KeyError};
45use zeroize::{Zeroize, ZeroizeOnDrop};
46
47mod backup;
48mod cross_signing;
49mod device_keys;
50pub mod events;
51mod one_time_keys;
52pub mod qr_login;
53pub mod requests;
54pub mod room_history;
55
56pub use self::{backup::*, cross_signing::*, device_keys::*, one_time_keys::*};
57use crate::store::BackupDecryptionKey;
58
59macro_rules! from_base64 {
60    ($foo:ident, $name:ident) => {
61        pub(crate) fn $name<'de, D>(deserializer: D) -> Result<$foo, D::Error>
62        where
63            D: Deserializer<'de>,
64        {
65            let mut string = String::deserialize(deserializer)?;
66
67            let result = $foo::from_base64(&string);
68            string.zeroize();
69
70            result.map_err(serde::de::Error::custom)
71        }
72    };
73}
74
75macro_rules! to_base64 {
76    ($foo:ident, $name:ident) => {
77        pub(crate) fn $name<S>(v: &$foo, serializer: S) -> Result<S::Ok, S::Error>
78        where
79            S: Serializer,
80        {
81            let mut string = v.to_base64();
82            let ret = string.serialize(serializer);
83
84            string.zeroize();
85
86            ret
87        }
88    };
89}
90
91/// Struct containing the bundle of secrets to fully activate a new devices for
92/// end-to-end encryption.
93#[derive(Debug, Deserialize, Clone, Serialize, ZeroizeOnDrop)]
94pub struct SecretsBundle {
95    /// The cross-signing keys.
96    pub cross_signing: CrossSigningSecrets,
97    /// The backup key, if available.
98    pub backup: Option<BackupSecrets>,
99}
100
101/// Data for the secrets bundle containing the cross-signing keys.
102#[derive(Deserialize, Clone, Serialize, ZeroizeOnDrop)]
103pub struct CrossSigningSecrets {
104    /// The seed for the private part of the cross-signing master key, encoded
105    /// as base64.
106    pub master_key: String,
107    /// The seed for the private part of the cross-signing user-signing key,
108    /// encoded as base64.
109    pub user_signing_key: String,
110    /// The seed for the private part of the cross-signing self-signing key,
111    /// encoded as base64.
112    pub self_signing_key: String,
113}
114
115impl std::fmt::Debug for CrossSigningSecrets {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("CrossSigningSecrets")
118            .field("master_key", &"...")
119            .field("user_signing_key", &"...")
120            .field("self_signing_key", &"...")
121            .finish()
122    }
123}
124
125/// Data for the secrets bundle containing the secret and version for a
126/// `m.megolm_backup.v1.curve25519-aes-sha2` backup.
127#[derive(Debug, Deserialize, Clone, Serialize, ZeroizeOnDrop)]
128pub struct MegolmBackupV1Curve25519AesSha2Secrets {
129    /// The private half of the backup key, can be used to access and decrypt
130    /// room keys in the backup. Also called the recovery key in the
131    /// [spec](https://spec.matrix.org/v1.10/client-server-api/#recovery-key).
132    #[serde(serialize_with = "backup_key_to_base64", deserialize_with = "backup_key_from_base64")]
133    pub key: BackupDecryptionKey,
134    /// The backup version that is tied to the above backup key.
135    pub backup_version: String,
136}
137
138from_base64!(BackupDecryptionKey, backup_key_from_base64);
139to_base64!(BackupDecryptionKey, backup_key_to_base64);
140
141/// Enum for the algorithm-specific secrets for the room key backup.
142#[derive(Debug, Clone, ZeroizeOnDrop, Serialize, Deserialize)]
143#[serde(tag = "algorithm")]
144pub enum BackupSecrets {
145    /// Backup secrets for the `m.megolm_backup.v1.curve25519-aes-sha2` backup
146    /// algorithm.
147    #[serde(rename = "m.megolm_backup.v1.curve25519-aes-sha2")]
148    MegolmBackupV1Curve25519AesSha2(MegolmBackupV1Curve25519AesSha2Secrets),
149}
150
151impl BackupSecrets {
152    /// Get the algorithm of the secrets contained in the [`BackupSecrets`].
153    pub fn algorithm(&self) -> &str {
154        match &self {
155            BackupSecrets::MegolmBackupV1Curve25519AesSha2(_) => {
156                "m.megolm_backup.v1.curve25519-aes-sha2"
157            }
158        }
159    }
160}
161
162/// Represents a potentially decoded signature (but *not* a validated one).
163///
164/// There are two important cases here:
165///
166/// 1. If the claimed algorithm is supported *and* the payload has an expected
167///    format, the signature will be represent by the enum variant corresponding
168///    to that algorithm. For example, decodable Ed25519 signatures are
169///    represented as `Ed25519(...)`.
170/// 2. If the claimed algorithm is unsupported, the signature is represented as
171///    `Other(...)`.
172#[derive(Clone, Debug, PartialEq, Eq)]
173pub enum Signature {
174    /// A Ed25519 digital signature.
175    Ed25519(Ed25519Signature),
176    /// A digital signature in an unsupported algorithm. The raw signature bytes
177    /// are represented as a base64-encoded string.
178    Other(String),
179}
180
181/// Represents a signature that could not be decoded.
182///
183/// This will currently only hold invalid Ed25519 signatures.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct InvalidSignature {
186    /// The base64 encoded string that is claimed to contain a signature but
187    /// could not be decoded.
188    pub source: String,
189}
190
191impl Signature {
192    /// Get the Ed25519 signature, if this is one.
193    pub fn ed25519(&self) -> Option<Ed25519Signature> {
194        as_variant!(self, Self::Ed25519).copied()
195    }
196
197    /// Convert the signature to a base64 encoded string.
198    pub fn to_base64(&self) -> String {
199        match self {
200            Signature::Ed25519(s) => s.to_base64(),
201            Signature::Other(s) => s.to_owned(),
202        }
203    }
204}
205
206impl From<Ed25519Signature> for Signature {
207    fn from(signature: Ed25519Signature) -> Self {
208        Self::Ed25519(signature)
209    }
210}
211
212/// Signatures for a signed object.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct Signatures(
215    BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, Result<Signature, InvalidSignature>>>,
216);
217
218impl Signatures {
219    /// Create a new, empty, signatures collection.
220    pub fn new() -> Self {
221        Signatures(Default::default())
222    }
223
224    /// Add the given signature from the given signer and the given key_id to
225    /// the collection.
226    pub fn add_signature(
227        &mut self,
228        signer: OwnedUserId,
229        key_id: OwnedDeviceKeyId,
230        signature: Ed25519Signature,
231    ) -> Option<Result<Signature, InvalidSignature>> {
232        self.0.entry(signer).or_default().insert(key_id, Ok(signature.into()))
233    }
234
235    /// Try to find an Ed25519 signature from the given signer with the given
236    /// key id.
237    pub fn get_signature(&self, signer: &UserId, key_id: &DeviceKeyId) -> Option<Ed25519Signature> {
238        self.get(signer)?.get(key_id)?.as_ref().ok()?.ed25519()
239    }
240
241    /// Get the map of signatures that belong to the given user.
242    pub fn get(
243        &self,
244        signer: &UserId,
245    ) -> Option<&BTreeMap<OwnedDeviceKeyId, Result<Signature, InvalidSignature>>> {
246        self.0.get(signer)
247    }
248
249    /// Remove all the signatures we currently hold.
250    pub fn clear(&mut self) {
251        self.0.clear()
252    }
253
254    /// Do we hold any signatures or is our collection completely empty.
255    pub fn is_empty(&self) -> bool {
256        self.0.is_empty()
257    }
258
259    /// How many signatures do we currently hold.
260    pub fn signature_count(&self) -> usize {
261        self.0.values().map(|u| u.len()).sum()
262    }
263}
264
265impl Default for Signatures {
266    fn default() -> Self {
267        Self::new()
268    }
269}
270
271impl IntoIterator for Signatures {
272    type Item = (OwnedUserId, BTreeMap<OwnedDeviceKeyId, Result<Signature, InvalidSignature>>);
273
274    type IntoIter =
275        IntoIter<OwnedUserId, BTreeMap<OwnedDeviceKeyId, Result<Signature, InvalidSignature>>>;
276
277    fn into_iter(self) -> Self::IntoIter {
278        self.0.into_iter()
279    }
280}
281
282impl<'de> Deserialize<'de> for Signatures {
283    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284    where
285        D: Deserializer<'de>,
286    {
287        let map: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>> =
288            Deserialize::deserialize(deserializer)?;
289
290        let map = map
291            .into_iter()
292            .map(|(user, signatures)| {
293                let signatures = signatures
294                    .into_iter()
295                    .map(|(key_id, s)| {
296                        let algorithm = key_id.algorithm();
297                        let signature = match algorithm {
298                            DeviceKeyAlgorithm::Ed25519 => Ed25519Signature::from_base64(&s)
299                                .map(|s| s.into())
300                                .map_err(|_| InvalidSignature { source: s }),
301                            _ => Ok(Signature::Other(s)),
302                        };
303
304                        Ok((key_id, signature))
305                    })
306                    .collect::<Result<BTreeMap<_, _>, _>>()?;
307
308                Ok((user, signatures))
309            })
310            .collect::<Result<_, _>>()?;
311
312        Ok(Signatures(map))
313    }
314}
315
316impl Serialize for Signatures {
317    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
318    where
319        S: Serializer,
320    {
321        let signatures: BTreeMap<&OwnedUserId, BTreeMap<&OwnedDeviceKeyId, String>> = self
322            .0
323            .iter()
324            .map(|(u, m)| {
325                (
326                    u,
327                    m.iter()
328                        .map(|(d, s)| {
329                            (
330                                d,
331                                match s {
332                                    Ok(s) => s.to_base64(),
333                                    Err(i) => i.source.to_owned(),
334                                },
335                            )
336                        })
337                        .collect(),
338                )
339            })
340            .collect();
341
342        Serialize::serialize(&signatures, serializer)
343    }
344}
345
346/// A collection of signing keys, a map from the key id to the signing key.
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct SigningKeys<T: Ord>(BTreeMap<T, SigningKey>);
349
350impl<T: Ord> SigningKeys<T> {
351    /// Create a new, empty, `SigningKeys` collection.
352    pub fn new() -> Self {
353        Self(BTreeMap::new())
354    }
355
356    /// Insert a `SigningKey` into the collection.
357    pub fn insert(&mut self, key_id: T, key: SigningKey) -> Option<SigningKey> {
358        self.0.insert(key_id, key)
359    }
360
361    /// Get a `SigningKey` with the given `DeviceKeyId`.
362    pub fn get<Q>(&self, key_id: &Q) -> Option<&SigningKey>
363    where
364        T: Borrow<Q>,
365        Q: Ord + ?Sized,
366    {
367        self.0.get(key_id)
368    }
369
370    /// Create an iterator over the `SigningKey`s in this collection.
371    pub fn iter(&self) -> Iter<'_, T, SigningKey> {
372        self.0.iter()
373    }
374
375    /// Do we hold any keys in or is our collection completely empty.
376    pub fn is_empty(&self) -> bool {
377        self.0.is_empty()
378    }
379}
380
381impl<T: Ord> Default for SigningKeys<T> {
382    fn default() -> Self {
383        Self::new()
384    }
385}
386
387impl<T: Ord> IntoIterator for SigningKeys<T> {
388    type Item = (T, SigningKey);
389
390    type IntoIter = IntoIter<T, SigningKey>;
391
392    fn into_iter(self) -> Self::IntoIter {
393        self.0.into_iter()
394    }
395}
396
397impl<K: Ord> FromIterator<(K, SigningKey)> for SigningKeys<K> {
398    fn from_iter<T: IntoIterator<Item = (K, SigningKey)>>(iter: T) -> Self {
399        let map = BTreeMap::from_iter(iter);
400
401        Self(map)
402    }
403}
404
405impl<K: Ord, const N: usize> From<[(K, SigningKey); N]> for SigningKeys<K> {
406    fn from(v: [(K, SigningKey); N]) -> Self {
407        let map = BTreeMap::from(v);
408
409        Self(map)
410    }
411}
412
413// Helper trait to generalize between a `OwnedDeviceKeyId` and a
414// `DeviceKeyAlgorithm` so that we can support Deserialize for
415// `SigningKeys<T>`
416trait Algorithm {
417    fn algorithm(&self) -> DeviceKeyAlgorithm;
418}
419
420impl Algorithm for OwnedDeviceKeyId {
421    fn algorithm(&self) -> DeviceKeyAlgorithm {
422        DeviceKeyId::algorithm(self)
423    }
424}
425
426impl Algorithm for DeviceKeyAlgorithm {
427    fn algorithm(&self) -> DeviceKeyAlgorithm {
428        self.to_owned()
429    }
430}
431
432/// An encryption algorithm to be used to encrypt messages sent to a room.
433#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
434#[non_exhaustive]
435pub enum EventEncryptionAlgorithm {
436    /// Olm version 1 using Curve25519, AES-256, and SHA-256.
437    #[ruma_enum(rename = "m.olm.v1.curve25519-aes-sha2")]
438    OlmV1Curve25519AesSha2,
439
440    /// Olm version 2 using Curve25519, AES-256, and SHA-256.
441    #[cfg(feature = "experimental-algorithms")]
442    #[ruma_enum(rename = "m.olm.v2.curve25519-aes-sha2")]
443    OlmV2Curve25519AesSha2,
444
445    /// Megolm version 1 using AES-256 and SHA-256.
446    #[ruma_enum(rename = "m.megolm.v1.aes-sha2")]
447    MegolmV1AesSha2,
448
449    /// Megolm version 2 using AES-256 and SHA-256.
450    #[cfg(feature = "experimental-algorithms")]
451    #[ruma_enum(rename = "m.megolm.v2.aes-sha2")]
452    MegolmV2AesSha2,
453
454    #[doc(hidden)]
455    _Custom(PrivOwnedStr),
456}
457
458impl<T: Ord + Serialize> Serialize for SigningKeys<T> {
459    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
460    where
461        S: Serializer,
462    {
463        let keys: BTreeMap<&T, String> =
464            self.0.iter().map(|(key_id, key)| (key_id, key.to_base64())).collect();
465
466        keys.serialize(serializer)
467    }
468}
469
470impl<'de, T: Algorithm + Ord + Deserialize<'de>> Deserialize<'de> for SigningKeys<T> {
471    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
472    where
473        D: Deserializer<'de>,
474    {
475        let map: BTreeMap<T, String> = Deserialize::deserialize(deserializer)?;
476
477        let map: Result<_, _> = map
478            .into_iter()
479            .map(|(key_id, key)| {
480                let key = SigningKey::from_parts(&key_id.algorithm(), key)
481                    .map_err(serde::de::Error::custom)?;
482
483                Ok((key_id, key))
484            })
485            .collect();
486
487        Ok(SigningKeys(map?))
488    }
489}
490
491// Vodozemac serializes Curve25519 keys directly as a byteslice, while Matrix
492// likes to base64 encode all byte slices.
493//
494// This ensures that we serialize/deserialize in a Matrix-compatible way.
495from_base64!(Curve25519PublicKey, deserialize_curve_key);
496to_base64!(Curve25519PublicKey, serialize_curve_key);
497
498from_base64!(Ed25519PublicKey, deserialize_ed25519_key);
499to_base64!(Ed25519PublicKey, serialize_ed25519_key);
500
501pub(crate) fn deserialize_curve_key_vec<'de, D>(de: D) -> Result<Vec<Curve25519PublicKey>, D::Error>
502where
503    D: Deserializer<'de>,
504{
505    let keys: Vec<String> = Deserialize::deserialize(de)?;
506    let keys: Result<Vec<Curve25519PublicKey>, KeyError> =
507        keys.iter().map(|k| Curve25519PublicKey::from_base64(k)).collect();
508
509    keys.map_err(serde::de::Error::custom)
510}
511
512pub(crate) fn serialize_curve_key_vec<S>(
513    keys: &[Curve25519PublicKey],
514    s: S,
515) -> Result<S::Ok, S::Error>
516where
517    S: Serializer,
518{
519    let keys: Vec<String> = keys.iter().map(|k| k.to_base64()).collect();
520    keys.serialize(s)
521}
522
523#[cfg(test)]
524mod test {
525    use insta::{assert_debug_snapshot, assert_json_snapshot, with_settings};
526    use ruma::{device_id, user_id};
527    use serde_json::json;
528    use similar_asserts::assert_eq;
529
530    use super::*;
531
532    #[test]
533    fn serialize_secrets_bundle() {
534        let json = json!({
535            "cross_signing": {
536                "master_key": "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw",
537                "self_signing_key": "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk",
538                "user_signing_key": "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM",
539            },
540            "backup": {
541                "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
542                "backup_version": "2",
543                "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
544            },
545        });
546
547        let deserialized: SecretsBundle = serde_json::from_value(json.clone())
548            .expect("We should be able to deserialize the secrets bundle");
549
550        let serialized = serde_json::to_value(&deserialized)
551            .expect("We should be able to serialize a secrets bundle");
552
553        assert_eq!(json, serialized, "A serialization cycle should yield the same result");
554    }
555
556    #[test]
557    fn snapshot_backup_decryption_key() {
558        let decryption_key = BackupDecryptionKey { inner: Box::new([1u8; 32]) };
559        assert_json_snapshot!(decryption_key);
560
561        // should not log the key !
562        assert_debug_snapshot!(decryption_key);
563    }
564
565    #[test]
566    fn snapshot_signatures() {
567        let signatures = Signatures(BTreeMap::from([
568            (
569                user_id!("@alice:localhost").to_owned(),
570                BTreeMap::from([
571                    (
572                        DeviceKeyId::from_parts(
573                            DeviceKeyAlgorithm::Ed25519,
574                            device_id!("ABCDEFGH"),
575                        ),
576                        Ok(Signature::from(Ed25519Signature::from_slice(&[0u8; 64]).unwrap())),
577                    ),
578                    (
579                        DeviceKeyId::from_parts(
580                            DeviceKeyAlgorithm::Curve25519,
581                            device_id!("IJKLMNOP"),
582                        ),
583                        Ok(Signature::from(Ed25519Signature::from_slice(&[1u8; 64]).unwrap())),
584                    ),
585                ]),
586            ),
587            (
588                user_id!("@bob:localhost").to_owned(),
589                BTreeMap::from([(
590                    DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, device_id!("ABCDEFGH")),
591                    Err(InvalidSignature { source: "SOME+B64+SOME+B64+SOME+B64+==".to_owned() }),
592                )]),
593            ),
594        ]));
595
596        with_settings!({sort_maps =>true}, {
597            assert_json_snapshot!(signatures)
598        });
599    }
600
601    #[test]
602    fn snapshot_secret_bundle() {
603        let secret_bundle = SecretsBundle {
604            cross_signing: CrossSigningSecrets {
605                master_key: "MSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSK".to_owned(),
606                user_signing_key: "USKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSK".to_owned(),
607                self_signing_key: "SSKSSKSSKSSKSSKSSKSSKSSKSSKSSKSSK".to_owned(),
608            },
609            backup: Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(
610                MegolmBackupV1Curve25519AesSha2Secrets {
611                    key: BackupDecryptionKey::from_bytes(&[0u8; 32]),
612                    backup_version: "v1.1".to_owned(),
613                },
614            )),
615        };
616
617        assert_json_snapshot!(secret_bundle);
618
619        let secret_bundle = SecretsBundle {
620            cross_signing: CrossSigningSecrets {
621                master_key: "MSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSK".to_owned(),
622                user_signing_key: "USKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSK".to_owned(),
623                self_signing_key: "SSKSSKSSKSSKSSKSSKSSKSSKSSKSSKSSK".to_owned(),
624            },
625            backup: None,
626        };
627
628        assert_json_snapshot!(secret_bundle);
629    }
630}
631
632/// Represents a to-device event after it has been processed by the olm machine.
633#[derive(Clone, Debug, Serialize, Deserialize)]
634pub enum ProcessedToDeviceEvent {
635    /// A successfully-decrypted encrypted event.
636    /// Contains the raw decrypted event .
637    Decrypted(Raw<AnyToDeviceEvent>),
638
639    /// An encrypted event which could not be decrypted.
640    UnableToDecrypt(Raw<AnyToDeviceEvent>),
641
642    /// An unencrypted event.
643    PlainText(Raw<AnyToDeviceEvent>),
644
645    /// An invalid to device event that was ignored because it is missing some
646    /// required information to be processed (like no event `type` for
647    /// example)
648    Invalid(Raw<AnyToDeviceEvent>),
649}
650
651impl ProcessedToDeviceEvent {
652    /// Converts a ProcessedToDeviceEvent to the `Raw<AnyToDeviceEvent>` it
653    /// encapsulates
654    pub fn to_raw(&self) -> Raw<AnyToDeviceEvent> {
655        match self {
656            ProcessedToDeviceEvent::Decrypted(decrypted_event) => decrypted_event.clone(),
657            ProcessedToDeviceEvent::UnableToDecrypt(event) => event.clone(),
658            ProcessedToDeviceEvent::PlainText(event) => event.clone(),
659            ProcessedToDeviceEvent::Invalid(event) => event.clone(),
660        }
661    }
662}
663
664/// Trait to express the various room key export formats we have in a unified
665/// manner.
666pub trait RoomKeyExport {
667    /// The ID of the room where the exported room key was used.
668    fn room_id(&self) -> &RoomId;
669    /// The unique ID of the exported room key.
670    fn session_id(&self) -> &str;
671    /// The [Curve25519PublicKey] long-term identity key of the sender of this
672    /// room key.
673    fn sender_key(&self) -> Curve25519PublicKey;
674}