Skip to main content

matrix_sdk_crypto/store/
types.rs

1// Copyright 2020, 2026 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//! Data types for persistent storage.
16//!
17//! This module defines the data structures used by the crypto store to
18//! represent objects that are persisted in the database.
19
20use std::{
21    collections::{BTreeMap, HashMap, HashSet},
22    time::Duration,
23};
24
25use rand::Rng;
26use ruma::{
27    MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedRoomId, OwnedUserId,
28    events::secret::request::SecretName,
29};
30use serde::{Deserialize, Serialize};
31use vodozemac::{Curve25519PublicKey, base64_encode};
32use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
33
34use super::{DehydrationError, GossipRequest};
35#[cfg(feature = "experimental-push-secrets")]
36use crate::types::events::secret_push::SecretPushContent;
37use crate::{
38    Account, Device, DeviceData, GossippedSecret, Session, UserIdentity, UserIdentityData,
39    olm::{
40        InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity,
41        SenderData,
42    },
43    types::{
44        EventEncryptionAlgorithm,
45        events::{
46            room_key_bundle::RoomKeyBundleContent,
47            room_key_withheld::{RoomKeyWithheldContent, RoomKeyWithheldEvent},
48        },
49    },
50};
51
52/// Aggregated changes to be saved in the database.
53///
54/// This is an update version of `Changes` that will replace it as #2624
55/// progresses.
56// If you ever add a field here, make sure to update `Changes::is_empty` too.
57#[derive(Default, Debug)]
58#[allow(missing_docs)]
59pub struct PendingChanges {
60    pub account: Option<Account>,
61}
62
63impl PendingChanges {
64    /// Are there any changes stored or is this an empty `Changes` struct?
65    pub fn is_empty(&self) -> bool {
66        self.account.is_none()
67    }
68}
69
70/// Aggregated changes to be saved in the database.
71// If you ever add a field here, make sure to update `Changes::is_empty` too.
72#[derive(Default, Debug)]
73#[allow(missing_docs)]
74pub struct Changes {
75    pub private_identity: Option<PrivateCrossSigningIdentity>,
76    pub backup_version: Option<String>,
77    pub backup_decryption_key: Option<BackupDecryptionKey>,
78    pub dehydrated_device_pickle_key: Option<DehydratedDeviceKey>,
79    pub sessions: Vec<Session>,
80    pub message_hashes: Vec<OlmMessageHash>,
81    pub inbound_group_sessions: Vec<InboundGroupSession>,
82    pub outbound_group_sessions: Vec<OutboundGroupSession>,
83    pub key_requests: Vec<GossipRequest>,
84    pub identities: IdentityChanges,
85    pub devices: DeviceChanges,
86    /// Stores when a `m.room_key.withheld` is received
87    pub withheld_session_info: BTreeMap<OwnedRoomId, BTreeMap<String, RoomKeyWithheldEntry>>,
88    pub room_settings: HashMap<OwnedRoomId, RoomSettings>,
89    pub secrets: Vec<SecretsInboxItem>,
90    pub next_batch_token: Option<String>,
91
92    /// Historical room key history bundles that we have received and should
93    /// store.
94    pub received_room_key_bundles: Vec<StoredRoomKeyBundleData>,
95
96    /// The set of rooms for which we have requested all room keys from the
97    /// backup in advance of constructing a room key bundle.
98    pub room_key_backups_fully_downloaded: HashSet<OwnedRoomId>,
99
100    /// Updates to the list of rooms where we have recently accepted invites
101    /// (and should now accept a key bundle, if one arrives soon).
102    pub rooms_pending_key_bundle: HashMap<OwnedRoomId, Option<RoomPendingKeyBundleDetails>>,
103}
104
105/// A secret that was received via an `m.secret.send` or
106/// `io.element.msc4385.secret.push` event.
107#[derive(Clone)]
108pub struct SecretsInboxItem {
109    /// The name of the secret.
110    pub secret_name: SecretName,
111    /// The contents of the secret.
112    pub secret: Zeroizing<String>,
113}
114
115#[cfg(not(tarpaulin_include))]
116impl std::fmt::Debug for SecretsInboxItem {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        f.debug_tuple("SecretsInboxItem").field(&self.secret_name).finish()
119    }
120}
121
122impl From<GossippedSecret> for SecretsInboxItem {
123    fn from(secret: GossippedSecret) -> Self {
124        Self { secret_name: secret.secret_name, secret: secret.event.content.secret.clone().into() }
125    }
126}
127
128#[cfg(feature = "experimental-push-secrets")]
129impl From<SecretPushContent> for SecretsInboxItem {
130    fn from(secret: SecretPushContent) -> Self {
131        Self { secret_name: secret.name.clone(), secret: secret.secret.clone().into() }
132    }
133}
134
135/// Information about an [MSC4268] room key bundle.
136///
137/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
138#[derive(Clone, Debug, Serialize, Deserialize)]
139pub struct StoredRoomKeyBundleData {
140    /// The user that sent us this data.
141    pub sender_user: OwnedUserId,
142
143    /// The [`Curve25519PublicKey`] of the device that sent us this data.
144    pub sender_key: Curve25519PublicKey,
145
146    /// Information about the sender of this data and how much we trust that
147    /// information.
148    pub sender_data: SenderData,
149
150    /// The room key bundle data itself.
151    pub bundle_data: RoomKeyBundleContent,
152}
153
154/// A user for which we are tracking the list of devices.
155#[derive(Clone, Debug, Serialize, Deserialize)]
156pub struct TrackedUser {
157    /// The user ID of the user.
158    pub user_id: OwnedUserId,
159    /// The outdate/dirty flag of the user, remembers if the list of devices for
160    /// the user is considered to be out of date. If the list of devices is
161    /// out of date, a `/keys/query` request should be sent out for this
162    /// user.
163    pub dirty: bool,
164}
165
166impl Changes {
167    /// Are there any changes stored or is this an empty `Changes` struct?
168    pub fn is_empty(&self) -> bool {
169        self.private_identity.is_none()
170            && self.backup_version.is_none()
171            && self.backup_decryption_key.is_none()
172            && self.dehydrated_device_pickle_key.is_none()
173            && self.sessions.is_empty()
174            && self.message_hashes.is_empty()
175            && self.inbound_group_sessions.is_empty()
176            && self.outbound_group_sessions.is_empty()
177            && self.key_requests.is_empty()
178            && self.identities.is_empty()
179            && self.devices.is_empty()
180            && self.withheld_session_info.is_empty()
181            && self.room_settings.is_empty()
182            && self.secrets.is_empty()
183            && self.next_batch_token.is_none()
184            && self.received_room_key_bundles.is_empty()
185    }
186}
187
188/// This struct is used to remember whether an identity has undergone a change
189/// or remains the same as the one we already know about.
190///
191/// When the homeserver informs us of a potential change in a user's identity or
192/// device during a `/sync` response, it triggers a `/keys/query` request from
193/// our side. In response to this query, the server provides a comprehensive
194/// snapshot of all the user's devices and identities.
195///
196/// Our responsibility is to discern whether a device or identity is new,
197/// changed, or unchanged.
198#[derive(Debug, Clone, Default)]
199#[allow(missing_docs)]
200pub struct IdentityChanges {
201    pub new: Vec<UserIdentityData>,
202    pub changed: Vec<UserIdentityData>,
203    pub unchanged: Vec<UserIdentityData>,
204}
205
206impl IdentityChanges {
207    pub(super) fn is_empty(&self) -> bool {
208        self.new.is_empty() && self.changed.is_empty()
209    }
210
211    /// Convert the vectors contained in the [`IdentityChanges`] into
212    /// three maps from user id to user identity (new, updated, unchanged).
213    pub(super) fn into_maps(
214        self,
215    ) -> (
216        BTreeMap<OwnedUserId, UserIdentityData>,
217        BTreeMap<OwnedUserId, UserIdentityData>,
218        BTreeMap<OwnedUserId, UserIdentityData>,
219    ) {
220        let new: BTreeMap<_, _> = self
221            .new
222            .into_iter()
223            .map(|identity| (identity.user_id().to_owned(), identity))
224            .collect();
225
226        let changed: BTreeMap<_, _> = self
227            .changed
228            .into_iter()
229            .map(|identity| (identity.user_id().to_owned(), identity))
230            .collect();
231
232        let unchanged: BTreeMap<_, _> = self
233            .unchanged
234            .into_iter()
235            .map(|identity| (identity.user_id().to_owned(), identity))
236            .collect();
237
238        (new, changed, unchanged)
239    }
240}
241
242#[derive(Debug, Clone, Default)]
243#[allow(missing_docs)]
244pub struct DeviceChanges {
245    pub new: Vec<DeviceData>,
246    pub changed: Vec<DeviceData>,
247    pub deleted: Vec<DeviceData>,
248}
249
250/// Updates about [`Device`]s which got received over the `/keys/query`
251/// endpoint.
252#[derive(Clone, Debug, Default)]
253pub struct DeviceUpdates {
254    /// The list of newly discovered devices.
255    ///
256    /// A device being in this list does not necessarily mean that the device
257    /// was just created, it just means that it's the first time we're
258    /// seeing this device.
259    pub new: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, Device>>,
260    /// The list of changed devices.
261    pub changed: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, Device>>,
262}
263
264/// Updates about [`UserIdentity`]s which got received over the `/keys/query`
265/// endpoint.
266#[derive(Clone, Debug, Default)]
267pub struct IdentityUpdates {
268    /// The list of newly discovered user identities .
269    ///
270    /// A identity being in this list does not necessarily mean that the
271    /// identity was just created, it just means that it's the first time
272    /// we're seeing this identity.
273    pub new: BTreeMap<OwnedUserId, UserIdentity>,
274    /// The list of changed identities.
275    pub changed: BTreeMap<OwnedUserId, UserIdentity>,
276    /// The list of unchanged identities.
277    pub unchanged: BTreeMap<OwnedUserId, UserIdentity>,
278}
279
280/// The private part of a backup key.
281///
282/// The private part of the key is not used on a regular basis. Rather, it is
283/// used only when we need to *recover* the backup.
284///
285/// Typically, this private key is itself encrypted and stored in server-side
286/// secret storage (SSSS), whence it can be retrieved when it is needed for a
287/// recovery operation. Alternatively, the key can be "gossiped" between devices
288/// via "secret sharing".
289#[derive(Clone, Zeroize, ZeroizeOnDrop, Deserialize, Serialize)]
290#[serde(transparent)]
291pub struct BackupDecryptionKey {
292    pub(crate) inner: Box<[u8; BackupDecryptionKey::KEY_SIZE]>,
293}
294
295impl BackupDecryptionKey {
296    /// The number of bytes the decryption key will hold.
297    pub const KEY_SIZE: usize = 32;
298
299    /// Create a new random decryption key.
300    #[allow(clippy::new_without_default)]
301    pub fn new() -> Self {
302        let mut rng = rand::rng();
303
304        let mut key = Box::new([0u8; Self::KEY_SIZE]);
305        rng.fill_bytes(key.as_mut_slice());
306
307        Self { inner: key }
308    }
309
310    /// Export the [`BackupDecryptionKey`] as a base64 encoded string.
311    pub fn to_base64(&self) -> String {
312        base64_encode(self.inner.as_slice())
313    }
314}
315
316#[cfg(not(tarpaulin_include))]
317impl std::fmt::Debug for BackupDecryptionKey {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.debug_tuple("BackupDecryptionKey").field(&"...").finish()
320    }
321}
322
323/// The pickle key used to safely store the dehydrated device pickle.
324///
325/// This input key material will be expanded using HKDF into an AES key, MAC
326/// key, and an initialization vector (IV).
327#[derive(Clone, Zeroize, ZeroizeOnDrop, Deserialize, Serialize)]
328#[serde(transparent)]
329pub struct DehydratedDeviceKey {
330    pub(crate) inner: Box<[u8; DehydratedDeviceKey::KEY_SIZE]>,
331}
332
333impl DehydratedDeviceKey {
334    /// The number of bytes the encryption key will hold.
335    pub const KEY_SIZE: usize = 32;
336
337    /// Generates a new random pickle key.
338    #[allow(clippy::new_without_default)]
339    pub fn new() -> Self {
340        let mut rng = rand::rng();
341
342        let mut key = Box::new([0u8; Self::KEY_SIZE]);
343        rng.fill_bytes(key.as_mut_slice());
344
345        Self { inner: key }
346    }
347
348    /// Creates a new dehydration pickle key from the given slice.
349    ///
350    /// Fail if the slice length is not 32.
351    pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
352        if slice.len() == 32 {
353            let mut key = Box::new([0u8; 32]);
354            key.copy_from_slice(slice);
355            Ok(DehydratedDeviceKey { inner: key })
356        } else {
357            Err(DehydrationError::PickleKeyLength(slice.len()))
358        }
359    }
360
361    /// Creates a dehydration pickle key from the given bytes.
362    pub fn from_bytes(raw_key: &[u8; 32]) -> Self {
363        let mut inner = Box::new([0u8; Self::KEY_SIZE]);
364        inner.copy_from_slice(raw_key);
365
366        Self { inner }
367    }
368
369    /// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
370    pub fn to_base64(&self) -> String {
371        base64_encode(self.inner.as_slice())
372    }
373}
374
375impl From<&[u8; 32]> for DehydratedDeviceKey {
376    fn from(value: &[u8; 32]) -> Self {
377        DehydratedDeviceKey { inner: Box::new(*value) }
378    }
379}
380
381impl From<DehydratedDeviceKey> for Vec<u8> {
382    fn from(key: DehydratedDeviceKey) -> Self {
383        key.inner.to_vec()
384    }
385}
386
387#[cfg(not(tarpaulin_include))]
388impl std::fmt::Debug for DehydratedDeviceKey {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        f.debug_tuple("DehydratedDeviceKey").field(&"...").finish()
391    }
392}
393
394impl DeviceChanges {
395    /// Merge the given `DeviceChanges` into this instance of `DeviceChanges`.
396    pub fn extend(&mut self, other: DeviceChanges) {
397        self.new.extend(other.new);
398        self.changed.extend(other.changed);
399        self.deleted.extend(other.deleted);
400    }
401
402    /// Are there any changes is this an empty [`DeviceChanges`] struct?
403    pub fn is_empty(&self) -> bool {
404        self.new.is_empty() && self.changed.is_empty() && self.deleted.is_empty()
405    }
406}
407
408/// Struct holding info about how many room keys the store has.
409#[derive(Debug, Clone, Default)]
410pub struct RoomKeyCounts {
411    /// The total number of room keys the store has.
412    pub total: usize,
413    /// The number of backed up room keys the store has.
414    pub backed_up: usize,
415}
416
417/// Stored versions of the backup keys.
418#[derive(Default, Clone, Debug)]
419pub struct BackupKeys {
420    /// The key used to decrypt backed up room keys.
421    pub decryption_key: Option<BackupDecryptionKey>,
422    /// The version that we are using for backups.
423    pub backup_version: Option<String>,
424}
425
426/// A struct containing private cross signing keys that can be backed up or
427/// uploaded to the secret store.
428#[derive(Default, Zeroize, ZeroizeOnDrop)]
429pub struct CrossSigningKeyExport {
430    /// The seed of the master key encoded as unpadded base64.
431    pub master_key: Option<String>,
432    /// The seed of the self signing key encoded as unpadded base64.
433    pub self_signing_key: Option<String>,
434    /// The seed of the user signing key encoded as unpadded base64.
435    pub user_signing_key: Option<String>,
436}
437
438#[cfg(not(tarpaulin_include))]
439impl std::fmt::Debug for CrossSigningKeyExport {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        f.debug_struct("CrossSigningKeyExport")
442            .field("master_key", &self.master_key.is_some())
443            .field("self_signing_key", &self.self_signing_key.is_some())
444            .field("user_signing_key", &self.user_signing_key.is_some())
445            .finish_non_exhaustive()
446    }
447}
448
449/// Result type telling us if a `/keys/query` response was expected for a given
450/// user.
451#[derive(Clone, Copy, Debug, PartialEq, Eq)]
452pub(crate) enum UserKeyQueryResult {
453    WasPending,
454    WasNotPending,
455
456    /// A query was pending, but we gave up waiting
457    TimeoutExpired,
458}
459
460/// Room encryption settings which are modified by state events or user options
461#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
462pub struct RoomSettings {
463    /// The encryption algorithm that should be used in the room.
464    pub algorithm: EventEncryptionAlgorithm,
465
466    /// Whether state event encryption is enabled.
467    #[cfg(feature = "experimental-encrypted-state-events")]
468    #[serde(default)]
469    pub encrypt_state_events: bool,
470
471    /// Should untrusted devices receive the room key, or should they be
472    /// excluded from the conversation.
473    pub only_allow_trusted_devices: bool,
474
475    /// The maximum time an encryption session should be used for, before it is
476    /// rotated.
477    pub session_rotation_period: Option<Duration>,
478
479    /// The maximum number of messages an encryption session should be used for,
480    /// before it is rotated.
481    pub session_rotation_period_messages: Option<usize>,
482}
483
484impl Default for RoomSettings {
485    fn default() -> Self {
486        Self {
487            algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
488            #[cfg(feature = "experimental-encrypted-state-events")]
489            encrypt_state_events: false,
490            only_allow_trusted_devices: false,
491            session_rotation_period: None,
492            session_rotation_period_messages: None,
493        }
494    }
495}
496
497/// Information on a room key that has been received or imported.
498#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
499pub struct RoomKeyInfo {
500    /// The [messaging algorithm] that this key is used for. Will be one of the
501    /// `m.megolm.*` algorithms.
502    ///
503    /// [messaging algorithm]: https://spec.matrix.org/v1.6/client-server-api/#messaging-algorithms
504    pub algorithm: EventEncryptionAlgorithm,
505
506    /// The room where the key is used.
507    pub room_id: OwnedRoomId,
508
509    /// The Curve25519 key of the device which initiated the session originally.
510    pub sender_key: Curve25519PublicKey,
511
512    /// The ID of the session that the key is for.
513    pub session_id: String,
514}
515
516impl From<&InboundGroupSession> for RoomKeyInfo {
517    fn from(group_session: &InboundGroupSession) -> Self {
518        RoomKeyInfo {
519            algorithm: group_session.algorithm().clone(),
520            room_id: group_session.room_id().to_owned(),
521            sender_key: group_session.sender_key(),
522            session_id: group_session.session_id().to_owned(),
523        }
524    }
525}
526
527/// Information on a room key that has been withheld
528#[derive(Clone, Debug, Deserialize, Serialize)]
529pub struct RoomKeyWithheldInfo {
530    /// The room where the key is used.
531    pub room_id: OwnedRoomId,
532
533    /// The ID of the session that the key is for.
534    pub session_id: String,
535
536    /// The withheld entry from a `m.room_key.withheld` event or [MSC4268] room
537    /// key bundle.
538    ///
539    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
540    pub withheld_event: RoomKeyWithheldEntry,
541}
542
543/// Represents an entry for a withheld room key event, which can be either a
544/// to-device event or a bundle entry.
545#[derive(Clone, Debug, Serialize, Deserialize)]
546pub struct RoomKeyWithheldEntry {
547    /// The user ID responsible for this entry, either from a
548    /// `m.room_key.withheld` to-device event or an [MSC4268] room key bundle.
549    ///
550    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
551    pub sender: OwnedUserId,
552    /// The content of the entry, which provides details about the reason the
553    /// key was withheld.
554    pub content: RoomKeyWithheldContent,
555}
556
557impl From<RoomKeyWithheldEvent> for RoomKeyWithheldEntry {
558    fn from(value: RoomKeyWithheldEvent) -> Self {
559        Self { sender: value.sender, content: value.content }
560    }
561}
562
563/// Information about a received historic room key bundle.
564///
565/// This struct contains information needed to uniquely identify a room key
566/// bundle. Only a single bundle per sender for a given room is persisted at a
567/// time.
568///
569/// It is used to notify listeners about received room key bundles.
570#[derive(Debug, Clone)]
571pub struct RoomKeyBundleInfo {
572    /// The user ID of the person that sent us the historic room key bundle.
573    pub sender: OwnedUserId,
574
575    /// The [`Curve25519PublicKey`] of the device that sent us this data.
576    pub sender_key: Curve25519PublicKey,
577
578    /// The ID of the room the bundle should be used in.
579    pub room_id: OwnedRoomId,
580}
581
582impl From<&StoredRoomKeyBundleData> for RoomKeyBundleInfo {
583    fn from(value: &StoredRoomKeyBundleData) -> Self {
584        let StoredRoomKeyBundleData { sender_user, sender_data: _, bundle_data, sender_key } =
585            value;
586        let sender_key = *sender_key;
587
588        Self { sender: sender_user.clone(), room_id: bundle_data.room_id.clone(), sender_key }
589    }
590}
591
592/// A struct remembering details of a room that we recently joined following an
593/// invite (and so should accept a room key bundle if we receive one).
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct RoomPendingKeyBundleDetails {
596    /// The ID of the room associated with this entry, used to track and
597    /// enumerate all such rooms during the startup process. This enables the
598    /// client to resume importing after a crash.
599    pub room_id: OwnedRoomId,
600
601    /// A timestamp remembering when we observed the user accepting an invite
602    /// using this client.
603    pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
604
605    /// The user ID of the person that invited us.
606    pub inviter: OwnedUserId,
607}