mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-07 15:33:45 -04:00
... and add accessors instead. Give `TimelineEvent` the same treatment we just gave `SyncTimelineEvent`: make the fields private, and use accessors where we previously used direct access.
969 lines
35 KiB
Rust
969 lines
35 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
|
|
use anyhow::Result;
|
|
use assert_matches::assert_matches;
|
|
use assert_matches2::assert_let;
|
|
use assign::assign;
|
|
use matrix_sdk::{
|
|
crypto::{format_emojis, SasState},
|
|
encryption::{
|
|
backups::BackupState,
|
|
recovery::RecoveryState,
|
|
verification::{
|
|
QrVerificationData, QrVerificationState, Verification, VerificationRequestState,
|
|
},
|
|
BackupDownloadStrategy, EncryptionSettings, LocalTrust,
|
|
},
|
|
ruma::{
|
|
api::client::room::create_room::v3::Request as CreateRoomRequest,
|
|
events::{
|
|
key::verification::{request::ToDeviceKeyVerificationRequestEvent, VerificationMethod},
|
|
room::message::{
|
|
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
|
|
SyncRoomMessageEvent,
|
|
},
|
|
OriginalSyncMessageLikeEvent,
|
|
},
|
|
},
|
|
Client,
|
|
};
|
|
use similar_asserts::assert_eq;
|
|
use tracing::warn;
|
|
|
|
use crate::helpers::{SyncTokenAwareClient, TestClientBuilder};
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_mutual_sas_verification() -> Result<()> {
|
|
let encryption_settings =
|
|
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
|
|
let alice = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("alice")
|
|
.use_sqlite()
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
let bob = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("bob")
|
|
.use_sqlite()
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
|
|
warn!("alice's device: {}", alice.device_id().unwrap());
|
|
warn!("bob's device: {}", bob.device_id().unwrap());
|
|
|
|
let invite = vec![bob.user_id().unwrap().to_owned()];
|
|
let request = assign!(CreateRoomRequest::new(), {
|
|
invite,
|
|
is_direct: true,
|
|
});
|
|
|
|
let alice_room = alice.create_room(request).await?;
|
|
alice_room.enable_encryption().await?;
|
|
let room_id = alice_room.room_id();
|
|
|
|
warn!("alice has created and enabled encryption in the room");
|
|
|
|
bob.sync_once().await?;
|
|
bob.get_room(room_id).unwrap().join().await?;
|
|
|
|
alice.sync_once().await?;
|
|
|
|
warn!("alice and bob are both aware of each other in the e2ee room");
|
|
|
|
// Bob adds the verification listeners.
|
|
let bob_verification_request = Arc::new(Mutex::new(None));
|
|
{
|
|
let bvr = bob_verification_request.clone();
|
|
bob.add_event_handler(
|
|
|ev: ToDeviceKeyVerificationRequestEvent, client: Client| async move {
|
|
let request = client
|
|
.encryption()
|
|
.get_verification_request(&ev.sender, &ev.content.transaction_id)
|
|
.await
|
|
.expect("Request object wasn't created");
|
|
*bvr.lock().unwrap() = Some(request);
|
|
},
|
|
);
|
|
|
|
let bvr = bob_verification_request.clone();
|
|
bob.add_event_handler(|ev: OriginalSyncRoomMessageEvent, client: Client| async move {
|
|
if let MessageType::VerificationRequest(_) = &ev.content.msgtype {
|
|
let request = client
|
|
.encryption()
|
|
.get_verification_request(&ev.sender, &ev.event_id)
|
|
.await
|
|
.expect("Request object wasn't created");
|
|
*bvr.lock().unwrap() = Some(request);
|
|
}
|
|
});
|
|
}
|
|
|
|
warn!("bob has set up verification listeners");
|
|
|
|
let alice_bob_identity = alice
|
|
.encryption()
|
|
.get_user_identity(bob.user_id().unwrap())
|
|
.await?
|
|
.expect("alice knows bob's identity");
|
|
|
|
warn!("alice has found bob's identity");
|
|
|
|
let alice_verification_request = alice_bob_identity.request_verification().await?;
|
|
|
|
assert!(!alice_verification_request.is_passive());
|
|
assert!(alice_verification_request.we_started());
|
|
assert_eq!(alice_verification_request.room_id(), Some(room_id));
|
|
|
|
warn!("alice has started verification");
|
|
|
|
bob.sync_once().await?;
|
|
let bob_verification_request = bob_verification_request
|
|
.lock()
|
|
.unwrap()
|
|
.take()
|
|
.expect("bob received a verification request");
|
|
|
|
warn!("bob has received the verification request");
|
|
|
|
assert_eq!(bob_verification_request.room_id(), Some(room_id));
|
|
|
|
assert!(!bob_verification_request.is_done());
|
|
|
|
assert!(!bob_verification_request.is_cancelled());
|
|
assert!(bob_verification_request.cancel_info().is_none());
|
|
|
|
assert!(!bob_verification_request.is_ready());
|
|
assert!(!bob_verification_request.is_passive());
|
|
assert!(!bob_verification_request.we_started());
|
|
|
|
assert_eq!(bob_verification_request.own_user_id(), bob.user_id().unwrap());
|
|
assert_eq!(bob_verification_request.other_user_id(), alice.user_id().unwrap());
|
|
assert!(!bob_verification_request.is_self_verification());
|
|
|
|
assert_matches!(bob_verification_request.state(), VerificationRequestState::Requested { .. });
|
|
let _flow_id = bob_verification_request.flow_id();
|
|
|
|
// Bob notifies Alice he accepts the verification process.
|
|
bob_verification_request.accept().await.unwrap();
|
|
let our_methods = assert_matches!(bob_verification_request.state(), VerificationRequestState::Ready { our_methods, .. } => our_methods);
|
|
assert!(bob_verification_request.is_ready());
|
|
|
|
assert_eq!(bob_verification_request.their_supported_methods(), Some(our_methods));
|
|
|
|
warn!("bob has accepted the verification request");
|
|
|
|
// Alice receives the accept, and moves to the ready state.
|
|
assert_matches!(alice_verification_request.state(), VerificationRequestState::Created { .. });
|
|
alice.sync_once().await.unwrap();
|
|
assert_matches!(alice_verification_request.state(), VerificationRequestState::Ready { .. });
|
|
|
|
let alice_sas =
|
|
alice_verification_request.start_sas().await?.expect("must have a sas verification");
|
|
|
|
assert_eq!(alice_sas.own_user_id(), alice.user_id().unwrap());
|
|
assert_eq!(alice_sas.other_user_id(), bob.user_id().unwrap());
|
|
assert_eq!(alice_sas.room_id(), Some(room_id));
|
|
|
|
assert!(alice_sas.started_from_request());
|
|
assert!(!alice_sas.is_self_verification());
|
|
assert!(alice_sas.we_started());
|
|
assert!(!alice_sas.is_done());
|
|
assert!(!alice_sas.is_cancelled());
|
|
assert!(alice_sas.cancel_info().is_none());
|
|
assert!(!alice_sas.supports_emoji());
|
|
assert!(alice_sas.emoji().is_none());
|
|
assert!(alice_sas.decimals().is_none());
|
|
|
|
bob.sync_once().await?;
|
|
|
|
let bob_verification = assert_matches!(bob_verification_request.state(), VerificationRequestState::Transitioned { verification } => verification);
|
|
|
|
assert!(!bob_verification.is_done());
|
|
assert!(!bob_verification.is_cancelled());
|
|
assert!(!bob_verification.is_self_verification());
|
|
assert!(!bob_verification.we_started());
|
|
assert!(bob_verification.cancel_info().is_none());
|
|
assert_eq!(bob_verification.own_user_id(), bob.user_id().unwrap());
|
|
assert_eq!(bob_verification.other_user_id(), alice.user_id().unwrap());
|
|
assert_eq!(bob_verification.room_id(), Some(room_id));
|
|
|
|
let bob_sas = bob_verification.sas().unwrap();
|
|
|
|
assert_matches!(alice_sas.state(), SasState::Created { .. });
|
|
assert_matches!(bob_sas.state(), SasState::Started { .. });
|
|
|
|
bob_sas.accept().await?;
|
|
assert_matches!(bob_sas.state(), SasState::Accepted { .. });
|
|
alice.sync_once().await?;
|
|
assert_matches!(alice_sas.state(), SasState::Accepted { .. });
|
|
assert!(alice_sas.supports_emoji());
|
|
|
|
assert!(!alice_sas.can_be_presented());
|
|
assert!(!bob_sas.can_be_presented());
|
|
|
|
// Let a little crypto messages dance happen.
|
|
alice.sync_once().await?;
|
|
bob.sync_once().await?;
|
|
assert_matches!(alice_sas.state(), SasState::Accepted { .. });
|
|
assert_matches!(bob_sas.state(), SasState::KeysExchanged { .. });
|
|
|
|
alice.sync_once().await?;
|
|
let alice_emojis =
|
|
assert_matches!(alice_sas.state(), SasState::KeysExchanged { emojis, .. } => emojis)
|
|
.expect("alice received emojis");
|
|
let bob_emojis =
|
|
assert_matches!(bob_sas.state(), SasState::KeysExchanged { emojis, .. } => emojis)
|
|
.expect("bob received emojis");
|
|
|
|
assert!(alice_sas.can_be_presented());
|
|
assert!(bob_sas.can_be_presented());
|
|
|
|
assert_eq!(format_emojis(alice_emojis.emojis), format_emojis(bob_emojis.emojis));
|
|
|
|
alice_sas.confirm().await?;
|
|
bob_sas.confirm().await?;
|
|
assert_matches!(alice_sas.state(), SasState::Confirmed);
|
|
assert_matches!(bob_sas.state(), SasState::Confirmed);
|
|
|
|
// Moar crypto dancing.
|
|
alice.sync_once().await?;
|
|
bob.sync_once().await?;
|
|
assert_matches!(alice_sas.state(), SasState::Confirmed);
|
|
assert_matches!(bob_sas.state(), SasState::Done { .. });
|
|
|
|
alice.sync_once().await?;
|
|
assert_matches!(alice_sas.state(), SasState::Done { .. });
|
|
assert_matches!(bob_sas.state(), SasState::Done { .. });
|
|
|
|
// Wait for remote echos for verification status requests.
|
|
alice.sync_once().await?;
|
|
bob.sync_once().await?;
|
|
|
|
assert!(!bob_verification_request.is_cancelled());
|
|
assert!(!alice_verification_request.is_cancelled());
|
|
|
|
assert!(bob_verification_request.is_done());
|
|
assert!(alice_verification_request.is_done());
|
|
assert!(bob_sas.is_done());
|
|
assert!(alice_sas.is_done());
|
|
|
|
// Both users appear as verified to each other.
|
|
let alice_bob_ident =
|
|
alice.encryption().get_user_identity(bob.user_id().unwrap()).await?.unwrap();
|
|
assert!(alice_bob_ident.is_verified());
|
|
|
|
let bob_alice_ident =
|
|
bob.encryption().get_user_identity(alice.user_id().unwrap()).await?.unwrap();
|
|
assert!(bob_alice_ident.is_verified());
|
|
|
|
// Both user devices appear as verified to the other user.
|
|
let alice_bob_device = alice_sas.other_device();
|
|
assert_eq!(alice_bob_device.user_id(), bob.user_id().unwrap());
|
|
assert_eq!(alice_bob_device.device_id(), bob.device_id().unwrap());
|
|
assert_eq!(alice_bob_device.local_trust_state(), LocalTrust::Unset);
|
|
|
|
let alice_bob_device = alice
|
|
.encryption()
|
|
.get_device(bob.user_id().unwrap(), bob.device_id().unwrap())
|
|
.await?
|
|
.unwrap();
|
|
assert!(alice_bob_device.is_verified());
|
|
assert_eq!(alice_bob_device.local_trust_state(), LocalTrust::Verified);
|
|
assert!(alice_bob_device.is_locally_trusted());
|
|
assert!(!alice_bob_device.is_blacklisted());
|
|
|
|
let bob_alice_device = bob_sas.other_device();
|
|
assert_eq!(bob_alice_device.user_id(), alice.user_id().unwrap());
|
|
assert_eq!(bob_alice_device.device_id(), alice.device_id().unwrap());
|
|
assert_eq!(bob_alice_device.local_trust_state(), LocalTrust::Unset);
|
|
|
|
let bob_alice_device = bob
|
|
.encryption()
|
|
.get_device(alice.user_id().unwrap(), alice.device_id().unwrap())
|
|
.await?
|
|
.unwrap();
|
|
assert!(bob_alice_device.is_verified());
|
|
assert_eq!(bob_alice_device.local_trust_state(), LocalTrust::Verified);
|
|
assert!(bob_alice_device.is_locally_trusted());
|
|
assert!(!bob_alice_device.is_blacklisted());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_mutual_qrcode_verification() -> Result<()> {
|
|
let encryption_settings =
|
|
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
|
|
let alice = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("alice")
|
|
.use_sqlite()
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
let bob = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("bob")
|
|
.use_sqlite()
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
|
|
warn!("alice's device: {}", alice.device_id().unwrap());
|
|
warn!("bob's device: {}", bob.device_id().unwrap());
|
|
|
|
let invite = vec![bob.user_id().unwrap().to_owned()];
|
|
let request = assign!(CreateRoomRequest::new(), {
|
|
invite,
|
|
is_direct: true,
|
|
});
|
|
|
|
let alice_room = alice.create_room(request).await?;
|
|
alice_room.enable_encryption().await?;
|
|
let room_id = alice_room.room_id();
|
|
|
|
warn!("alice has created and enabled encryption in the room");
|
|
|
|
bob.sync_once().await?;
|
|
bob.get_room(room_id).unwrap().join().await?;
|
|
|
|
alice.sync_once().await?;
|
|
|
|
warn!("alice and bob are both aware of each other in the e2ee room");
|
|
|
|
// Bob adds the verification listeners.
|
|
let bob_verification_request = Arc::new(Mutex::new(None));
|
|
{
|
|
let bvr = bob_verification_request.clone();
|
|
bob.add_event_handler(
|
|
|ev: ToDeviceKeyVerificationRequestEvent, client: Client| async move {
|
|
let request = client
|
|
.encryption()
|
|
.get_verification_request(&ev.sender, &ev.content.transaction_id)
|
|
.await
|
|
.expect("Request object wasn't created");
|
|
*bvr.lock().unwrap() = Some(request);
|
|
},
|
|
);
|
|
|
|
let bvr = bob_verification_request.clone();
|
|
bob.add_event_handler(|ev: OriginalSyncRoomMessageEvent, client: Client| async move {
|
|
if let MessageType::VerificationRequest(_) = &ev.content.msgtype {
|
|
let request = client
|
|
.encryption()
|
|
.get_verification_request(&ev.sender, &ev.event_id)
|
|
.await
|
|
.expect("Request object wasn't created");
|
|
*bvr.lock().unwrap() = Some(request);
|
|
}
|
|
});
|
|
}
|
|
|
|
warn!("bob has set up verification listeners");
|
|
|
|
let alice_bob_identity = alice
|
|
.encryption()
|
|
.get_user_identity(bob.user_id().unwrap())
|
|
.await?
|
|
.expect("alice knows bob's identity");
|
|
|
|
warn!("alice has found bob's identity");
|
|
|
|
let alice_verification_request = alice_bob_identity
|
|
.request_verification_with_methods(vec![
|
|
VerificationMethod::SasV1,
|
|
VerificationMethod::QrCodeScanV1,
|
|
VerificationMethod::QrCodeShowV1,
|
|
VerificationMethod::ReciprocateV1,
|
|
])
|
|
.await?;
|
|
|
|
assert!(!alice_verification_request.is_passive());
|
|
assert!(alice_verification_request.we_started());
|
|
assert_eq!(alice_verification_request.room_id(), Some(room_id));
|
|
|
|
warn!("alice has started verification");
|
|
|
|
bob.sync_once().await?;
|
|
let bob_verification_request = bob_verification_request
|
|
.lock()
|
|
.unwrap()
|
|
.take()
|
|
.expect("bob received a verification request");
|
|
|
|
warn!("bob has received the verification request");
|
|
|
|
assert_eq!(bob_verification_request.room_id(), Some(room_id));
|
|
|
|
assert!(!bob_verification_request.is_done());
|
|
|
|
assert!(!bob_verification_request.is_cancelled());
|
|
assert!(bob_verification_request.cancel_info().is_none());
|
|
|
|
assert!(!bob_verification_request.is_ready());
|
|
assert!(!bob_verification_request.is_passive());
|
|
assert!(!bob_verification_request.we_started());
|
|
|
|
assert_eq!(bob_verification_request.own_user_id(), bob.user_id().unwrap());
|
|
assert_eq!(bob_verification_request.other_user_id(), alice.user_id().unwrap());
|
|
assert!(!bob_verification_request.is_self_verification());
|
|
|
|
assert_matches!(bob_verification_request.state(), VerificationRequestState::Requested { .. });
|
|
let _flow_id = bob_verification_request.flow_id();
|
|
|
|
// Bob notifies Alice he accepts the verification process.
|
|
bob_verification_request.accept().await.unwrap();
|
|
assert_matches!(bob_verification_request.state(), VerificationRequestState::Ready { .. });
|
|
assert!(bob_verification_request.is_ready());
|
|
|
|
warn!("bob has accepted the verification request");
|
|
|
|
// Alice receives the accept, and moves to the ready state.
|
|
assert_matches!(alice_verification_request.state(), VerificationRequestState::Created { .. });
|
|
alice.sync_once().await.unwrap();
|
|
assert_matches!(alice_verification_request.state(), VerificationRequestState::Ready { .. });
|
|
|
|
let bob_qr =
|
|
bob_verification_request.generate_qr_code().await?.expect("must have a qr verification");
|
|
|
|
assert_matches!(bob_qr.state(), QrVerificationState::Started);
|
|
|
|
warn!("bob has generated the QR code");
|
|
|
|
let bob_verification = assert_matches!(bob_verification_request.state(), VerificationRequestState::Transitioned { verification } => verification);
|
|
|
|
assert!(!bob_verification.is_done());
|
|
assert!(!bob_verification.is_cancelled());
|
|
assert!(!bob_verification.is_self_verification());
|
|
assert!(!bob_verification.we_started());
|
|
assert!(bob_verification.cancel_info().is_none());
|
|
assert_eq!(bob_verification.own_user_id(), bob.user_id().unwrap());
|
|
assert_eq!(bob_verification.other_user_id(), alice.user_id().unwrap());
|
|
assert_eq!(bob_verification.room_id(), Some(room_id));
|
|
|
|
let qr_code_bytes = bob_qr.to_bytes()?;
|
|
let alice_qr = alice_verification_request
|
|
.scan_qr_code(QrVerificationData::from_bytes(qr_code_bytes)?)
|
|
.await?
|
|
.expect("must have a qr verification");
|
|
|
|
warn!("alice has scanned the QR code");
|
|
|
|
assert_eq!(alice_qr.own_user_id(), alice.user_id().unwrap());
|
|
assert_eq!(alice_qr.other_user_id(), bob.user_id().unwrap());
|
|
assert_eq!(alice_qr.room_id(), Some(room_id));
|
|
|
|
assert!(!alice_qr.is_self_verification());
|
|
assert!(alice_qr.we_started());
|
|
assert!(!alice_qr.is_done());
|
|
assert!(!alice_qr.is_cancelled());
|
|
assert!(alice_qr.cancel_info().is_none());
|
|
|
|
bob.sync_once().await?;
|
|
|
|
assert_matches!(alice_qr.state(), QrVerificationState::Reciprocated);
|
|
assert_matches!(bob_qr.state(), QrVerificationState::Scanned);
|
|
|
|
bob_qr.confirm().await?;
|
|
|
|
warn!("bob has confirmed the QR code scanning");
|
|
|
|
alice.sync_once().await?;
|
|
|
|
assert_matches!(bob_qr.state(), QrVerificationState::Confirmed);
|
|
assert_matches!(alice_qr.state(), QrVerificationState::Done { .. });
|
|
|
|
// Crypto dancing.
|
|
alice.sync_once().await?;
|
|
bob.sync_once().await?;
|
|
assert_matches!(bob_qr.state(), QrVerificationState::Done { .. });
|
|
assert_matches!(alice_qr.state(), QrVerificationState::Done { .. });
|
|
|
|
// Wait for remote echos for verification status requests.
|
|
alice.sync_once().await?;
|
|
bob.sync_once().await?;
|
|
|
|
assert!(!bob_verification_request.is_cancelled());
|
|
assert!(!alice_verification_request.is_cancelled());
|
|
|
|
assert!(bob_verification_request.is_done());
|
|
assert!(alice_verification_request.is_done());
|
|
assert!(bob_qr.is_done());
|
|
assert!(alice_qr.is_done());
|
|
|
|
// Both users appear as verified to each other.
|
|
let alice_bob_ident =
|
|
alice.encryption().get_user_identity(bob.user_id().unwrap()).await?.unwrap();
|
|
assert!(alice_bob_ident.is_verified());
|
|
|
|
let bob_alice_ident =
|
|
bob.encryption().get_user_identity(alice.user_id().unwrap()).await?.unwrap();
|
|
assert!(bob_alice_ident.is_verified());
|
|
|
|
// Both user devices appear as verified to the other user.
|
|
let alice_bob_device = alice_qr.other_device();
|
|
assert_eq!(alice_bob_device.user_id(), bob.user_id().unwrap());
|
|
assert_eq!(alice_bob_device.device_id(), bob.device_id().unwrap());
|
|
assert_eq!(alice_bob_device.local_trust_state(), LocalTrust::Unset);
|
|
|
|
let alice_bob_device = alice
|
|
.encryption()
|
|
.get_device(bob.user_id().unwrap(), bob.device_id().unwrap())
|
|
.await?
|
|
.unwrap();
|
|
assert!(alice_bob_device.is_verified());
|
|
assert!(!alice_bob_device.is_blacklisted());
|
|
|
|
let bob_alice_device = bob_qr.other_device();
|
|
assert_eq!(bob_alice_device.user_id(), alice.user_id().unwrap());
|
|
assert_eq!(bob_alice_device.device_id(), alice.device_id().unwrap());
|
|
|
|
let bob_alice_device = bob
|
|
.encryption()
|
|
.get_device(alice.user_id().unwrap(), alice.device_id().unwrap())
|
|
.await?
|
|
.unwrap();
|
|
assert!(bob_alice_device.is_verified());
|
|
assert!(!bob_alice_device.is_blacklisted());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_encryption_missing_member_keys() -> Result<()> {
|
|
let alice =
|
|
SyncTokenAwareClient::new(TestClientBuilder::new("alice").use_sqlite().build().await?);
|
|
let bob = SyncTokenAwareClient::new(TestClientBuilder::new("bob").use_sqlite().build().await?);
|
|
|
|
let invite = vec![bob.user_id().unwrap().to_owned()];
|
|
let request = assign!(CreateRoomRequest::new(), {
|
|
invite,
|
|
is_direct: true,
|
|
});
|
|
|
|
let alice_room = alice.create_room(request).await?;
|
|
alice_room.enable_encryption().await?;
|
|
alice.sync_once().await?;
|
|
|
|
warn!("alice has created and enabled encryption in the room");
|
|
|
|
bob.sync_once().await?;
|
|
bob.get_room(alice_room.room_id()).unwrap().join().await?;
|
|
|
|
warn!("bob has joined");
|
|
|
|
// New person joins the room.
|
|
let carl =
|
|
SyncTokenAwareClient::new(TestClientBuilder::new("carl").use_sqlite().build().await?);
|
|
alice_room.invite_user_by_id(carl.user_id().unwrap()).await?;
|
|
|
|
carl.sync_once().await?;
|
|
carl.get_room(alice_room.room_id()).unwrap().join().await?;
|
|
carl.sync_once().await?;
|
|
|
|
warn!("carl has joined");
|
|
|
|
// Bob sends message WITHOUT syncing.
|
|
warn!("bob sends message...");
|
|
let bob_room = bob.get_room(alice_room.room_id()).unwrap();
|
|
let message = "Hello world!";
|
|
let bob_message_content = Arc::new(Mutex::new(message));
|
|
bob_room.send(RoomMessageEventContent::text_plain(message)).await?;
|
|
warn!("bob is done sending the message");
|
|
|
|
// Alice was in the room when Bob sent the message, so they'll see it.
|
|
let alice_found_event = Arc::new(Mutex::new(false));
|
|
{
|
|
warn!("alice is looking for the decrypted message");
|
|
|
|
let found_event_handler = alice_found_event.clone();
|
|
let bob_message_content = bob_message_content.clone();
|
|
alice.add_event_handler(move |event: SyncRoomMessageEvent| async move {
|
|
warn!("Found a message \\o/ {event:?}");
|
|
let MessageType::Text(text_content) = &event.as_original().unwrap().content.msgtype
|
|
else {
|
|
return;
|
|
};
|
|
if text_content.body == *bob_message_content.lock().unwrap() {
|
|
*found_event_handler.lock().unwrap() = true;
|
|
}
|
|
});
|
|
|
|
alice.sync_once().await?;
|
|
|
|
let found = *alice_found_event.lock().unwrap();
|
|
assert!(found, "event has not been found for alice");
|
|
}
|
|
|
|
// Bob wasn't aware of Carl's presence (no sync() since Carl joined), so Carl
|
|
// won't see it first.
|
|
let carl_found_event = Arc::new(Mutex::new(false));
|
|
{
|
|
warn!("carl is looking for the decrypted message");
|
|
|
|
let found_event_handler = carl_found_event.clone();
|
|
let bob_message_content = bob_message_content.clone();
|
|
carl.add_event_handler(move |event: SyncRoomMessageEvent| async move {
|
|
warn!("Found a message \\o/ {event:?}");
|
|
let MessageType::Text(text_content) = &event.as_original().unwrap().content.msgtype
|
|
else {
|
|
return;
|
|
};
|
|
if text_content.body == *bob_message_content.lock().unwrap() {
|
|
*found_event_handler.lock().unwrap() = true;
|
|
}
|
|
});
|
|
|
|
carl.sync_once().await?;
|
|
|
|
let found = *carl_found_event.lock().unwrap();
|
|
assert!(!found, "event has been unexpectedly found for carl");
|
|
}
|
|
|
|
// Now Bob syncs, thus notices the presence of Carl.
|
|
bob.sync_once().await?;
|
|
|
|
warn!("bob sends another message...");
|
|
let bob_room = bob.get_room(alice_room.room_id()).unwrap();
|
|
let message = "Wassup";
|
|
*bob_message_content.lock().unwrap() = message;
|
|
bob_room.send(RoomMessageEventContent::text_plain(message)).await?;
|
|
warn!("bob is done sending another message");
|
|
|
|
{
|
|
*alice_found_event.lock().unwrap() = false;
|
|
alice.sync_once().await?;
|
|
let found = *alice_found_event.lock().unwrap();
|
|
assert!(found, "second message has not been found for alice");
|
|
}
|
|
|
|
{
|
|
*carl_found_event.lock().unwrap() = false;
|
|
carl.sync_once().await?;
|
|
let found = *carl_found_event.lock().unwrap();
|
|
assert!(found, "second message has not been found for carl");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_failed_members_response() -> Result<()> {
|
|
let alice =
|
|
SyncTokenAwareClient::new(TestClientBuilder::new("alice").use_sqlite().build().await?);
|
|
let bob = SyncTokenAwareClient::new(TestClientBuilder::new("bob").use_sqlite().build().await?);
|
|
|
|
let invite = vec![bob.user_id().unwrap().to_owned()];
|
|
let request = assign!(CreateRoomRequest::new(), {
|
|
invite,
|
|
is_direct: true,
|
|
});
|
|
|
|
let alice_room = alice.create_room(request).await?;
|
|
alice_room.enable_encryption().await?;
|
|
alice.sync_once().await?;
|
|
|
|
warn!("alice has created and enabled encryption in the room");
|
|
|
|
bob.sync_once().await?;
|
|
|
|
// Cause a failure of a sync_members request by asking for members before
|
|
// joining. Since this is a private DM room, it will fail with a 401, as
|
|
// we're not authorized to look at state history.
|
|
let result = bob.get_room(alice_room.room_id()).unwrap().sync_members().await;
|
|
assert!(result.is_err());
|
|
|
|
bob.get_room(alice_room.room_id()).unwrap().join().await?;
|
|
|
|
warn!("bob has joined");
|
|
|
|
// Bob sends message WITHOUT syncing.
|
|
warn!("bob sends message...");
|
|
let bob_room = bob.get_room(alice_room.room_id()).unwrap();
|
|
let message = "Hello world!";
|
|
let bob_message_content = Arc::new(Mutex::new(message));
|
|
bob_room.send(RoomMessageEventContent::text_plain(message)).await?;
|
|
warn!("bob is done sending the message");
|
|
|
|
// Alice sees the message.
|
|
let alice_found_event = Arc::new(Mutex::new(false));
|
|
warn!("alice is looking for the decrypted message");
|
|
|
|
let found_event_handler = alice_found_event.clone();
|
|
let bob_message_content = bob_message_content.clone();
|
|
alice.add_event_handler(move |event: SyncRoomMessageEvent| async move {
|
|
warn!("Found a message \\o/ {event:?}");
|
|
let MessageType::Text(text_content) = &event.as_original().unwrap().content.msgtype else {
|
|
return;
|
|
};
|
|
if text_content.body == *bob_message_content.lock().unwrap() {
|
|
*found_event_handler.lock().unwrap() = true;
|
|
}
|
|
});
|
|
|
|
alice.sync_once().await?;
|
|
|
|
let found = *alice_found_event.lock().unwrap();
|
|
assert!(found, "event has not been found for alice");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_backup_enable_new_user() -> Result<()> {
|
|
let encryption_settings =
|
|
EncryptionSettings { auto_enable_backups: true, ..Default::default() };
|
|
|
|
let alice = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("alice")
|
|
.use_sqlite()
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
|
|
alice.encryption().wait_for_e2ee_initialization_tasks().await;
|
|
|
|
assert!(
|
|
alice.encryption().backups().are_enabled().await,
|
|
"Backups should have been enabled automatically."
|
|
);
|
|
|
|
assert_eq!(
|
|
alice.encryption().backups().state(),
|
|
BackupState::Enabled,
|
|
"The backup state should now be BackupState::Enabled"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_cross_signing_bootstrap() -> Result<()> {
|
|
let encryption_settings =
|
|
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
|
|
|
|
let alice = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("alice")
|
|
.use_sqlite()
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
|
|
alice.encryption().wait_for_e2ee_initialization_tasks().await;
|
|
|
|
let status = alice
|
|
.encryption()
|
|
.cross_signing_status()
|
|
.await
|
|
.expect("We should know our cross-signing status by now.");
|
|
|
|
assert!(status.is_complete(), "We should have all private cross-signing keys available.");
|
|
|
|
// We need to sync to get the remote echo of the upload of the device keys.
|
|
alice.sync_once().await?;
|
|
|
|
let own_device = alice.encryption().get_own_device().await?.unwrap();
|
|
|
|
assert!(
|
|
own_device.is_cross_signed_by_owner(),
|
|
"Since we bootstrapped cross-signing, our own device should have been \
|
|
signed by the cross-signing keys."
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn test_secret_gossip_after_interactive_verification() -> Result<()> {
|
|
let encryption_settings = EncryptionSettings {
|
|
auto_enable_cross_signing: true,
|
|
auto_enable_backups: true,
|
|
backup_download_strategy: BackupDownloadStrategy::OneShot,
|
|
};
|
|
|
|
let first_client = SyncTokenAwareClient::new(
|
|
TestClientBuilder::new("alice_gossip_test")
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
|
|
let user_id = first_client.user_id().expect("We should have access to the user id now");
|
|
|
|
let request = CreateRoomRequest::new();
|
|
let room_first_client = first_client.create_room(request).await?;
|
|
room_first_client.enable_encryption().await?;
|
|
first_client.sync_once().await?;
|
|
|
|
first_client.encryption().recovery().enable().await?;
|
|
|
|
assert_eq!(first_client.encryption().recovery().state(), RecoveryState::Enabled);
|
|
|
|
let response = room_first_client
|
|
.send(RoomMessageEventContent::text_plain("It's a secret to everybody"))
|
|
.await?;
|
|
|
|
let event_id = response.event_id;
|
|
|
|
warn!("The first device has created and enabled encryption in the room and sent an event");
|
|
|
|
let second_client = SyncTokenAwareClient::new(
|
|
TestClientBuilder::with_exact_username(user_id.localpart().to_owned())
|
|
.encryption_settings(encryption_settings)
|
|
.build()
|
|
.await?,
|
|
);
|
|
|
|
second_client.encryption().wait_for_e2ee_initialization_tasks().await;
|
|
|
|
// The second client doesn't have access to the backup, nor is recovery in the
|
|
// enabled state.
|
|
assert_eq!(second_client.encryption().recovery().state(), RecoveryState::Incomplete);
|
|
assert_eq!(second_client.encryption().backups().state(), BackupState::Unknown);
|
|
|
|
warn!("The first device: {}", first_client.device_id().unwrap());
|
|
warn!("The second device: {}", second_client.device_id().unwrap());
|
|
assert_ne!(first_client.device_id().unwrap(), second_client.device_id().unwrap());
|
|
|
|
second_client.sync_once().await?;
|
|
|
|
let seconds_first_device = second_client
|
|
.encryption()
|
|
.get_device(second_client.user_id().unwrap(), first_client.device_id().unwrap())
|
|
.await?
|
|
.expect("We should have access to the first device once we have synced");
|
|
|
|
// The first client is not verified from the point of view of the second client.
|
|
assert!(!seconds_first_device.is_verified());
|
|
|
|
// Let's send out a request to verify with each other.
|
|
let seconds_verification_request = seconds_first_device.request_verification().await?;
|
|
let flow_id = seconds_verification_request.flow_id();
|
|
|
|
first_client.sync_once().await?;
|
|
let firsts_verification_request = first_client
|
|
.encryption()
|
|
.get_verification_request(user_id, flow_id)
|
|
.await
|
|
.expect("The verification should have been requested");
|
|
|
|
assert_matches!(seconds_verification_request.state(), VerificationRequestState::Created { .. });
|
|
assert_matches!(
|
|
firsts_verification_request.state(),
|
|
VerificationRequestState::Requested { .. }
|
|
);
|
|
warn!("The first device is accepting the verification request");
|
|
firsts_verification_request.accept().await?;
|
|
|
|
first_client.sync_once().await?;
|
|
|
|
assert_matches!(firsts_verification_request.state(), VerificationRequestState::Ready { .. });
|
|
let firsts_sas = firsts_verification_request
|
|
.start_sas()
|
|
.await?
|
|
.expect("We should be able to start the SAS verification");
|
|
|
|
second_client.sync_once().await?;
|
|
assert_let!(
|
|
VerificationRequestState::Transitioned { verification: Verification::SasV1(seconds_sas) } =
|
|
seconds_verification_request.state()
|
|
);
|
|
|
|
seconds_sas.accept().await?;
|
|
|
|
// We need to sync a couple of times so the clients exchange the shared secret.
|
|
first_client.sync_once().await?;
|
|
second_client.sync_once().await?;
|
|
first_client.sync_once().await?;
|
|
|
|
assert_eq!(
|
|
firsts_sas.emoji().expect("The firsts sas should be presentable"),
|
|
seconds_sas.emoji().expect("The seconds sas should be presentable"),
|
|
"The emojis should match"
|
|
);
|
|
|
|
// Confirm that the emojis match.
|
|
firsts_sas.confirm().await?;
|
|
seconds_sas.confirm().await?;
|
|
|
|
// After both sides confirm, we need a couple more syncs to exchange the final
|
|
// verification events.
|
|
second_client.sync_once().await?;
|
|
first_client.sync_once().await?;
|
|
second_client.sync_once().await?;
|
|
|
|
// And we're done, the verification dance is completed.
|
|
assert!(seconds_sas.is_done());
|
|
assert!(firsts_sas.is_done());
|
|
|
|
let second_device = second_client.encryption().get_own_device().await?.unwrap();
|
|
|
|
// The first device has signed the second one.
|
|
assert!(second_device.is_cross_signed_by_owner());
|
|
assert!(
|
|
!second_client.encryption().cross_signing_status().await.unwrap().is_complete(),
|
|
"We should not have received our cross-signing keys yet."
|
|
);
|
|
assert_eq!(
|
|
second_client.encryption().backups().state(),
|
|
BackupState::Unknown,
|
|
"The backup should not have been enabled yet."
|
|
);
|
|
assert_eq!(
|
|
second_client.encryption().recovery().state(),
|
|
RecoveryState::Incomplete,
|
|
"The recovery state should be in the Incomplete state, since we have not yet received all secrets"
|
|
);
|
|
|
|
// We still need to gossip the secrets from one device to the other, the first
|
|
// device syncs to receive the gossip requests, then the second device syncs
|
|
// to receive the secrets.
|
|
first_client.sync_once().await?;
|
|
warn!("The second client is doing its final sync");
|
|
second_client.sync_once().await?;
|
|
|
|
assert!(
|
|
second_client.encryption().cross_signing_status().await.unwrap().is_complete(),
|
|
"We should have received all the cross-signing keys from the first device"
|
|
);
|
|
assert_eq!(
|
|
second_client.encryption().backups().state(),
|
|
BackupState::Enabled,
|
|
"We should have enabled the backup after we received the backup key from the first device"
|
|
);
|
|
assert_eq!(
|
|
second_client.encryption().recovery().state(),
|
|
RecoveryState::Enabled,
|
|
"The recovery state should be in the Enabled state, since we have all the secrets"
|
|
);
|
|
|
|
// Let's now check if we can decrypt the event that was sent before our
|
|
// device was created.
|
|
let room = second_client
|
|
.get_room(room_first_client.room_id())
|
|
.expect("The second client should know about the room as well");
|
|
|
|
let timeline_event = room.event(&event_id, None).await?;
|
|
timeline_event
|
|
.encryption_info()
|
|
.expect("The event should have been encrypted and successfully decrypted.");
|
|
|
|
let event: OriginalSyncMessageLikeEvent<RoomMessageEventContent> =
|
|
timeline_event.raw().deserialize_as()?;
|
|
let message = event.content.msgtype;
|
|
|
|
assert_let!(MessageType::Text(message) = message);
|
|
|
|
assert_eq!(
|
|
message.body, "It's a secret to everybody",
|
|
"The decrypted message should match the text we encrypted."
|
|
);
|
|
|
|
Ok(())
|
|
}
|