Allow room key forwarding to be enabled and disabled (#1365)

Co-authored-by: Ivan Enderlin <ivan@mnt.io>
This commit is contained in:
Damir Jelić 2023-03-14 18:23:39 +01:00 committed by GitHub
parent eb2bf61236
commit 516d849ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 68 deletions

View File

@ -42,11 +42,38 @@ uniffi = { workspace = true }
url = "2.2.2"
zeroize = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
log-panics = { version = "2", features = ["with-backtrace"]}
tracing-android = "0.2.0"
matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["anyhow", "experimental-timeline", "e2e-encryption", "sled", "markdown", "experimental-sliding-sync", "socks", "rustls-tls"], version = "0.6.0" }
[target.'cfg(not(target_os = "android"))'.dependencies]
matrix-sdk = { path = "../../crates/matrix-sdk", features = ["anyhow", "experimental-timeline", "markdown", "experimental-sliding-sync", "socks"], version = "0.6.0" }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
path = "../../crates/matrix-sdk"
default-features = false
features = [
"anyhow",
"experimental-sliding-sync",
"experimental-timeline",
"e2e-encryption",
"markdown",
"sled",
"socks",
"rustls-tls",
]
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
path = "../../crates/matrix-sdk"
default-features = false
features = [
"anyhow",
"experimental-sliding-sync",
"experimental-timeline",
"e2e-encryption",
"markdown",
"native-tls",
"sled",
"socks",
]

View File

@ -20,6 +20,7 @@ default = []
e2e-encryption = ["dep:matrix-sdk-crypto"]
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
experimental-sliding-sync = ["ruma/unstable-msc3575"]
# helpers for testing features build upon this
@ -35,7 +36,7 @@ futures-core = "0.3.21"
futures-util = { workspace = true }
http = { workspace = true, optional = true }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true }
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true, default-features = false }
matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" }
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", optional = true }
once_cell = { workspace = true }

View File

@ -15,7 +15,8 @@ version = "0.6.0"
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
default = ["automatic-room-key-forwarding"]
automatic-room-key-forwarding = []
js = ["ruma/js", "vodozemac/js"]
qrcode = ["dep:matrix-sdk-qrcode"]
backups_v1 = ["dep:olm-rs", "dep:bs58"]

View File

@ -20,8 +20,12 @@
// If we don't trust the device store an object that remembers the request and
// let the users introspect that object.
use std::{collections::BTreeMap, sync::Arc};
use std::{
collections::BTreeMap,
sync::{atomic::AtomicBool, Arc},
};
use atomic::Ordering;
use dashmap::{mapref::entry::Entry, DashMap, DashSet};
use ruma::{
api::client::keys::claim_keys::v3::Request as KeysClaimRequest,
@ -34,10 +38,10 @@ use ruma::{
use tracing::{debug, info, trace, warn};
use vodozemac::{megolm::SessionOrdering, Curve25519PublicKey};
use super::{GossipRequest, KeyForwardDecision, RequestEvent, RequestInfo, SecretInfo, WaitQueue};
use super::{GossipRequest, RequestEvent, RequestInfo, SecretInfo, WaitQueue};
use crate::{
error::{EventError, OlmError, OlmResult},
olm::{InboundGroupSession, Session, ShareState},
olm::{InboundGroupSession, Session},
requests::{OutgoingRequest, ToDeviceRequest},
session_manager::GroupSessionCache,
store::{Changes, CryptoStoreError, SecretImportError, Store},
@ -45,7 +49,7 @@ use crate::{
forwarded_room_key::ForwardedRoomKeyContent,
olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent},
room::encrypted::EncryptedEvent,
room_key_request::{Action, RequestedKeyInfo, RoomKeyRequestEvent},
room_key_request::RoomKeyRequestEvent,
secret_send::SecretSendContent,
EventType,
},
@ -57,11 +61,13 @@ pub(crate) struct GossipMachine {
user_id: Arc<UserId>,
device_id: Arc<DeviceId>,
store: Store,
#[cfg(feature = "automatic-room-key-forwarding")]
outbound_group_sessions: GroupSessionCache,
outgoing_requests: Arc<DashMap<OwnedTransactionId, OutgoingRequest>>,
incoming_key_requests: Arc<DashMap<RequestInfo, RequestEvent>>,
wait_queue: WaitQueue,
users_for_key_claim: Arc<DashMap<OwnedUserId, DashSet<OwnedDeviceId>>>,
room_key_forwarding_enabled: Arc<AtomicBool>,
}
impl GossipMachine {
@ -69,21 +75,35 @@ impl GossipMachine {
user_id: Arc<UserId>,
device_id: Arc<DeviceId>,
store: Store,
outbound_group_sessions: GroupSessionCache,
#[allow(unused)] outbound_group_sessions: GroupSessionCache,
users_for_key_claim: Arc<DashMap<OwnedUserId, DashSet<OwnedDeviceId>>>,
) -> Self {
let room_key_forwarding_enabled =
AtomicBool::new(cfg!(feature = "automatic-room-key-forwarding")).into();
Self {
user_id,
device_id,
store,
#[cfg(feature = "automatic-room-key-forwarding")]
outbound_group_sessions,
outgoing_requests: Default::default(),
incoming_key_requests: Default::default(),
wait_queue: WaitQueue::new(),
users_for_key_claim,
room_key_forwarding_enabled,
}
}
#[cfg(feature = "automatic-room-key-forwarding")]
pub fn toggle_room_key_forwarding(&self, enabled: bool) {
self.room_key_forwarding_enabled.store(enabled, Ordering::SeqCst)
}
pub fn is_room_key_forwarding_enabled(&self) -> bool {
self.room_key_forwarding_enabled.load(Ordering::SeqCst)
}
/// Load stored outgoing requests that were not yet sent out.
async fn load_outgoing_requests(&self) -> Result<Vec<OutgoingRequest>, CryptoStoreError> {
Ok(self
@ -171,8 +191,11 @@ impl GossipMachine {
let event = item.value();
if let Some(s) = match event {
#[cfg(feature = "automatic-room-key-forwarding")]
RequestEvent::KeyShare(e) => self.handle_key_request(e).await?,
RequestEvent::Secret(e) => self.handle_secret_request(e).await?,
#[cfg(not(feature = "automatic-room-key-forwarding"))]
_ => None,
} {
changed_sessions.push(s);
}
@ -312,6 +335,7 @@ impl GossipMachine {
/// given `Device`, in that case we're going to queue up an
/// `/keys/claim` request to be sent out and retry once the 1-to-1 Olm
/// session has been established.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn try_to_forward_room_key(
&self,
event: &RoomKeyRequestEvent,
@ -358,11 +382,14 @@ impl GossipMachine {
/// Answer a room key request after we found the matching
/// `InboundGroupSession`.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn answer_room_key_request(
&self,
event: &RoomKeyRequestEvent,
session: InboundGroupSession,
) -> OlmResult<Option<Session>> {
use super::KeyForwardDecision;
let device =
self.store.get_device(&event.sender, &event.content.requesting_device_id).await?;
@ -403,6 +430,7 @@ impl GossipMachine {
}
}
#[cfg(feature = "automatic-room-key-forwarding")]
async fn handle_supported_key_request(
&self,
event: &RoomKeyRequestEvent,
@ -427,27 +455,38 @@ impl GossipMachine {
}
/// Handle a single incoming key request.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn handle_key_request(&self, event: &RoomKeyRequestEvent) -> OlmResult<Option<Session>> {
match &event.content.action {
Action::Request(info) => match info {
RequestedKeyInfo::MegolmV1AesSha2(i) => {
self.handle_supported_key_request(event, &i.room_id, &i.session_id).await
}
#[cfg(feature = "experimental-algorithms")]
RequestedKeyInfo::MegolmV2AesSha2(i) => {
self.handle_supported_key_request(event, &i.room_id, &i.session_id).await
}
RequestedKeyInfo::Unknown(i) => {
debug!(
sender = ?event.sender,
algorithm = ?i.algorithm,
"Received a room key request for a unsupported algorithm"
);
Ok(None)
}
},
// We ignore cancellations here since there's nothing to serve.
Action::Cancellation => Ok(None),
use crate::types::events::room_key_request::{Action, RequestedKeyInfo};
if self.room_key_forwarding_enabled.load(Ordering::SeqCst) {
match &event.content.action {
Action::Request(info) => match info {
RequestedKeyInfo::MegolmV1AesSha2(i) => {
self.handle_supported_key_request(event, &i.room_id, &i.session_id).await
}
#[cfg(feature = "experimental-algorithms")]
RequestedKeyInfo::MegolmV2AesSha2(i) => {
self.handle_supported_key_request(event, &i.room_id, &i.session_id).await
}
RequestedKeyInfo::Unknown(i) => {
debug!(
sender = ?event.sender,
algorithm = ?i.algorithm,
"Received a room key request for a unsupported algorithm"
);
Ok(None)
}
},
// We ignore cancellations here since there's nothing to serve.
Action::Cancellation => Ok(None),
}
} else {
debug!(
sender = ?event.sender,
"Received a room key request, but room key forwarding has been turned off"
);
Ok(None)
}
}
@ -476,6 +515,7 @@ impl GossipMachine {
Ok(used_session)
}
#[cfg(feature = "automatic-room-key-forwarding")]
async fn forward_room_key(
&self,
session: &InboundGroupSession,
@ -533,11 +573,16 @@ impl GossipMachine {
/// i.
/// - `Err(x)`: Should *refuse* to share the session. `x` is the reason for
/// the refusal.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_share_key(
&self,
device: &Device,
session: &InboundGroupSession,
) -> Result<Option<u32>, KeyForwardDecision> {
) -> Result<Option<u32>, super::KeyForwardDecision> {
use super::KeyForwardDecision;
use crate::olm::ShareState;
let outbound_session = self
.outbound_group_sessions
.get_with_id(session.room_id(), session.session_id())
@ -575,20 +620,21 @@ impl GossipMachine {
///
/// * `key_info` - The info of our key request containing information about
/// the key we wish to request.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_request_key(&self, key_info: &SecretInfo) -> Result<bool, CryptoStoreError> {
let request = self.store.get_secret_request_by_info(key_info).await?;
if self.room_key_forwarding_enabled.load(Ordering::SeqCst) {
let request = self.store.get_secret_request_by_info(key_info).await?;
// Don't send out duplicate requests, users can re-request them if they
// think a second request might succeed.
if request.is_none() {
let devices = self.store.get_user_devices(self.user_id()).await?;
// Don't send out duplicate requests, users can re-request them if they
// think a second request might succeed.
if request.is_none() {
let devices = self.store.get_user_devices(self.user_id()).await?;
// Devices will only respond to key requests if the devices are
// verified, if the device isn't verified by us it's unlikely that
// we're verified by them either. Don't request keys if there isn't
// at least one verified device.
if devices.is_any_verified() {
Ok(true)
// Devices will only respond to key requests if the devices are
// verified, if the device isn't verified by us it's unlikely that
// we're verified by them either. Don't request keys if there isn't
// at least one verified device.
Ok(devices.is_any_verified())
} else {
Ok(false)
}
@ -681,6 +727,7 @@ impl GossipMachine {
/// * `room_id` - The id of the room where the key is used in.
///
/// * `event` - The event for which we would like to request the room key.
#[cfg(feature = "automatic-room-key-forwarding")]
pub async fn create_outgoing_key_request(
&self,
room_id: &RoomId,
@ -989,6 +1036,7 @@ impl GossipMachine {
mod tests {
use std::sync::Arc;
#[cfg(feature = "automatic-room-key-forwarding")]
use assert_matches::assert_matches;
use dashmap::DashMap;
use matrix_sdk_common::locks::Mutex;
@ -997,35 +1045,40 @@ mod tests {
device_id, event_id,
events::{
secret::request::{RequestAction, SecretName, ToDeviceSecretRequestEventContent},
AnyToDeviceEventContent, ToDeviceEvent as RumaToDeviceEvent,
ToDeviceEvent as RumaToDeviceEvent,
},
room_id,
serde::Raw,
user_id, DeviceId, RoomId, UserId,
};
#[cfg(feature = "automatic-room-key-forwarding")]
use serde::{de::DeserializeOwned, Serialize};
use serde_json::json;
use super::{GossipMachine, KeyForwardDecision};
use super::GossipMachine;
#[cfg(feature = "automatic-room-key-forwarding")]
use crate::{
identities::{LocalTrust, ReadOnlyDevice},
olm::{Account, OutboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount},
session_manager::GroupSessionCache,
store::{Changes, IntoCryptoStore, MemoryStore, Store},
gossiping::KeyForwardDecision,
olm::OutboundGroupSession,
store::Changes,
types::{
events::{
forwarded_room_key::ForwardedRoomKeyContent,
olm_v1::{AnyDecryptedOlmEvent, DecryptedOlmV1Event},
room::encrypted::{
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
},
EventType, ToDeviceEvent,
forwarded_room_key::ForwardedRoomKeyContent, olm_v1::AnyDecryptedOlmEvent,
olm_v1::DecryptedOlmV1Event, room::encrypted::EncryptedToDeviceEvent, EventType,
ToDeviceEvent,
},
EventEncryptionAlgorithm,
},
verification::VerificationMachine,
EncryptionSettings, OutgoingRequest, OutgoingRequests,
};
use crate::{
identities::{LocalTrust, ReadOnlyDevice},
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
session_manager::GroupSessionCache,
store::{IntoCryptoStore, MemoryStore, Store},
types::events::room::encrypted::{EncryptedEvent, RoomEncryptedEventContent},
verification::VerificationMachine,
};
fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
@ -1063,6 +1116,7 @@ mod tests {
ReadOnlyAccount::new(alice_id(), alice2_device_id())
}
#[cfg(feature = "automatic-room-key-forwarding")]
fn test_gossip_machine(user_id: &UserId) -> GossipMachine {
let user_id = Arc::from(user_id);
let device_id = DeviceId::new();
@ -1107,6 +1161,7 @@ mod tests {
)
}
#[cfg(feature = "automatic-room-key-forwarding")]
async fn machines_for_key_share(
other_machine_owner: &UserId,
create_sessions: bool,
@ -1148,9 +1203,10 @@ mod tests {
.await
.unwrap();
bob_machine.store.save_inbound_group_sessions(&[inbound_group_session]).await.unwrap();
let content = group_session.encrypt(json!({}), "m.dummy").await;
let event = wrap_encrypted_content(bob_machine.user_id(), content);
bob_machine.store.save_inbound_group_sessions(&[inbound_group_session]).await.unwrap();
// Alice wants to request the outbound group session from bob.
assert!(
@ -1172,10 +1228,11 @@ mod tests {
(alice_machine, alice_account, group_session, bob_machine)
}
#[cfg(feature = "automatic-room-key-forwarding")]
fn extract_content<'a>(
recipient: &UserId,
request: &'a OutgoingRequest,
) -> &'a Raw<AnyToDeviceEventContent> {
) -> &'a Raw<ruma::events::AnyToDeviceEventContent> {
request
.request()
.to_device()
@ -1204,6 +1261,7 @@ mod tests {
}
}
#[cfg(feature = "automatic-room-key-forwarding")]
fn request_to_event<C>(
recipient: &UserId,
sender: &UserId,
@ -1250,6 +1308,7 @@ mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn create_key_request() {
let machine = get_machine().await;
let account = account();
@ -1281,6 +1340,7 @@ mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn receive_forwarded_key() {
let machine = get_machine().await;
let account = account();
@ -1383,6 +1443,7 @@ mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_share_key_test() {
let machine = get_machine().await;
let account = account();
@ -1497,6 +1558,7 @@ mod tests {
assert_matches!(machine.should_share_key(&own_device, &other_inbound).await, Ok(None));
}
#[cfg(feature = "automatic-room-key-forwarding")]
async fn key_share_cycle(algorithm: EventEncryptionAlgorithm) {
let (alice_machine, alice_account, group_session, bob_machine) =
machines_for_key_share(alice_id(), true, algorithm).await;
@ -1556,6 +1618,7 @@ mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn reject_forward_from_another_user() {
let (alice_machine, alice_account, group_session, bob_machine) =
machines_for_key_share(bob_id(), true, EventEncryptionAlgorithm::MegolmV1AesSha2).await;
@ -1605,12 +1668,13 @@ mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn key_share_cycle_megolm_v1() {
key_share_cycle(EventEncryptionAlgorithm::MegolmV1AesSha2).await;
}
#[cfg(feature = "experimental-algorithms")]
#[async_test]
#[cfg(all(feature = "experimental-algorithms", feature = "automatic-room-key-forwarding"))]
async fn key_share_cycle_megolm_v2() {
key_share_cycle(EventEncryptionAlgorithm::MegolmV2AesSha2).await;
}
@ -1686,6 +1750,7 @@ mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn key_share_cycle_without_session() {
let (alice_machine, alice_account, group_session, bob_machine) =
machines_for_key_share(alice_id(), false, EventEncryptionAlgorithm::MegolmV1AesSha2)

View File

@ -32,8 +32,6 @@ use ruma::{
DeviceId, OwnedDeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::error;
use crate::{
requests::{OutgoingRequest, ToDeviceRequest},
@ -44,7 +42,8 @@ use crate::{
};
/// An error describing why a key share request won't be honored.
#[derive(Debug, Clone, Error, PartialEq, Eq)]
#[cfg(feature = "automatic-room-key-forwarding")]
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum KeyForwardDecision {
/// The key request is from a device that we don't own, we're only sharing
/// sessions that we know the requesting device already was supposed to get.
@ -302,7 +301,7 @@ impl WaitQueue {
}
}
#[cfg(test)]
#[cfg(all(test, feature = "automatic-room-key-forwarding"))]
fn is_empty(&self) -> bool {
self.requests_ids_waiting.is_empty() && self.requests_waiting_for_session.is_empty()
}

View File

@ -47,10 +47,7 @@ use tracing::{
field::{debug, display},
info, instrument, warn, Span,
};
use vodozemac::{
megolm::{DecryptionError, SessionOrdering},
Curve25519PublicKey, Ed25519Signature,
};
use vodozemac::{megolm::SessionOrdering, Curve25519PublicKey, Ed25519Signature};
#[cfg(feature = "backups_v1")]
use crate::backups::BackupMachine;
@ -326,6 +323,21 @@ impl OlmMachine {
self.store.tracked_users().await
}
/// Enable or disable room key forwarding.
///
/// Room key forwarding allows the device to request room keys that it might
/// have missend in the original share using `m.room_key_request`
/// events.
#[cfg(feature = "automatic-room-key-forwarding")]
pub fn toggle_room_key_forwarding(&self, enable: bool) {
self.key_request_machine.toggle_room_key_forwarding(enable)
}
/// Is room key forwarding enabled?
pub fn is_room_key_forwarding_enabled(&self) -> bool {
self.key_request_machine.is_room_key_forwarding_enabled()
}
/// Get the outgoing requests that need to be sent out.
///
/// This returns a list of [`OutgoingRequest`]. Those requests need to be
@ -1227,9 +1239,12 @@ impl OlmMachine {
let result = self.decrypt_megolm_events(room_id, &event, &content).await;
if let Err(e) = &result {
#[cfg(feature = "automatic-room-key-forwarding")]
match e {
MegolmError::MissingRoomKey
| MegolmError::Decryption(DecryptionError::UnknownMessageIndex(_, _)) => {
| MegolmError::Decryption(
vodozemac::megolm::DecryptionError::UnknownMessageIndex(_, _),
) => {
self.key_request_machine.create_outgoing_key_request(room_id, &event).await?;
}
_ => {}
@ -2131,6 +2146,7 @@ pub(crate) mod tests {
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn test_query_ratcheted_key() {
let (alice, bob) = get_machine_pair_with_setup_sessions().await;
let room_id = room_id!("!test:example.org");
@ -2185,7 +2201,7 @@ pub(crate) mod tests {
let decrypt_error = bob.decrypt_room_event(&room_event, room_id).await.unwrap_err();
if let MegolmError::Decryption(vodo_error) = decrypt_error {
if let crate::MegolmError::Decryption(vodo_error) = decrypt_error {
if let vodozemac::megolm::DecryptionError::UnknownMessageIndex(_, _) = vodo_error {
// check that key has been requested
let outgoing_to_devices =

View File

@ -95,6 +95,7 @@ impl GroupSessionCache {
///
/// This is the same as [get_or_load()](#method.get_or_load) but it will
/// filter out the session if it doesn't match the given session id.
#[cfg(feature = "automatic-room-key-forwarding")]
pub async fn get_with_id(
&self,
room_id: &RoomId,

View File

@ -18,6 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
default = [
"e2e-encryption",
"automatic-room-key-forwarding",
"sled",
"native-tls",
]
@ -25,6 +26,7 @@ testing = []
e2e-encryption = [
"matrix-sdk-base/e2e-encryption",
"matrix-sdk-base/automatic-room-key-forwarding",
"matrix-sdk-sled?/crypto-store", # activate crypto-store on sled if given
"matrix-sdk-indexeddb?/e2e-encryption", # activate on indexeddb if given
]
@ -34,6 +36,7 @@ sled = ["dep:matrix-sdk-sled", "matrix-sdk-sled?/state-store"]
indexeddb = ["dep:matrix-sdk-indexeddb"]
qrcode = ["e2e-encryption", "matrix-sdk-base/qrcode"]
automatic-room-key-forwarding = ["e2e-encryption", "matrix-sdk-base/automatic-room-key-forwarding"]
markdown = ["ruma/markdown"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]

View File

@ -235,6 +235,7 @@ fn run_crypto_tests() -> Result<()> {
"rustup run stable cargo clippy -p matrix-sdk-crypto --features=backups_v1 -- -D warnings"
)
.run()?;
cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --no-default-features").run()?;
cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --features=backups_v1").run()?;
cmd!("rustup run stable cargo test --doc -p matrix-sdk-crypto --features=backups_v1").run()?;
cmd!(