use std::time::Duration;
use eyeball::SharedObservable;
use futures_core::Stream;
use matrix_sdk_base::{
boxed_into_future,
crypto::types::{
SecretsBundle,
qr_login::{QrCodeData, QrCodeMode},
},
};
use oauth2::VerificationUriComplete;
use ruma::time::Instant;
use url::Url;
#[cfg(doc)]
use vodozemac::ecies::CheckCode;
use super::{
LoginProtocolType, QrAuthMessage,
secure_channel::{EstablishedSecureChannel, SecureChannel},
};
use crate::{
Client,
authentication::oauth::qrcode::{
CheckCodeSender, GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError,
QrProgress, SecureChannelError,
},
};
async fn export_secrets_bundle(client: &Client) -> Result<SecretsBundle, QRCodeGrantLoginError> {
let secrets_bundle = client
.olm_machine()
.await
.as_ref()
.ok_or_else(|| QRCodeGrantLoginError::MissingSecretsBackup(None))?
.store()
.export_secrets_bundle()
.await?;
Ok(secrets_bundle)
}
async fn finish_login_grant<Q>(
client: &Client,
channel: &mut EstablishedSecureChannel,
device_creation_timeout: Duration,
secrets_bundle: &SecretsBundle,
state: &SharedObservable<GrantLoginProgress<Q>>,
) -> Result<(), QRCodeGrantLoginError> {
let message = channel.receive_json().await?;
let QrAuthMessage::LoginProtocol { device_authorization_grant, protocol, device_id } = message
else {
return Err(QRCodeGrantLoginError::Unknown(
"Receiving unexpected message when expecting LoginProtocol".to_owned(),
));
};
if protocol != LoginProtocolType::DeviceAuthorizationGrant {
channel
.send_json(QrAuthMessage::LoginFailure {
reason: LoginFailureReason::UnsupportedProtocol,
homeserver: None,
})
.await?;
return Err(QRCodeGrantLoginError::UnsupportedProtocol(protocol));
}
if !matches!(client.device_exists(device_id.to_base64().into()).await, Ok(false)) {
channel
.send_json(QrAuthMessage::LoginFailure {
reason: LoginFailureReason::DeviceAlreadyExists,
homeserver: None,
})
.await?;
return Err(QRCodeGrantLoginError::DeviceIDAlreadyInUse);
}
let verification_uri = Url::parse(
device_authorization_grant
.verification_uri_complete
.map(VerificationUriComplete::into_secret)
.unwrap_or(device_authorization_grant.verification_uri.to_string())
.as_str(),
)
.map_err(|_| QRCodeGrantLoginError::UnableToCreateDevice)?;
state.set(GrantLoginProgress::WaitingForAuth { verification_uri });
let message = QrAuthMessage::LoginProtocolAccepted;
channel.send_json(&message).await?;
let message: QrAuthMessage = channel.receive_json().await?;
let QrAuthMessage::LoginSuccess = message else {
return Err(QRCodeGrantLoginError::Unknown(
"Receiving unexpected message when expecting LoginSuccess".to_owned(),
));
};
let deadline = Instant::now() + device_creation_timeout;
loop {
if matches!(client.device_exists(device_id.to_base64().into()).await, Ok(true)) {
break;
} else {
if Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
} else {
channel
.send_json(QrAuthMessage::LoginFailure {
reason: LoginFailureReason::DeviceNotFound,
homeserver: None,
})
.await?;
return Err(QRCodeGrantLoginError::DeviceIDAlreadyInUse);
}
}
}
state.set(GrantLoginProgress::SyncingSecrets);
let message = QrAuthMessage::LoginSecrets(secrets_bundle.clone());
channel.send_json(&message).await?;
state.set(GrantLoginProgress::Done);
Ok(())
}
#[derive(Clone, Debug, Default)]
pub enum GrantLoginProgress<Q> {
#[default]
Starting,
EstablishingSecureChannel(Q),
WaitingForAuth {
verification_uri: Url,
},
SyncingSecrets,
Done,
}
#[derive(Debug)]
pub struct GrantLoginWithScannedQrCode<'a> {
client: &'a Client,
qr_code_data: &'a QrCodeData,
device_creation_timeout: Duration,
state: SharedObservable<GrantLoginProgress<QrProgress>>,
}
impl<'a> GrantLoginWithScannedQrCode<'a> {
pub(crate) fn new(
client: &'a Client,
qr_code_data: &'a QrCodeData,
device_creation_timeout: Duration,
) -> GrantLoginWithScannedQrCode<'a> {
GrantLoginWithScannedQrCode {
client,
qr_code_data,
device_creation_timeout,
state: Default::default(),
}
}
}
impl GrantLoginWithScannedQrCode<'_> {
pub fn subscribe_to_progress(
&self,
) -> impl Stream<Item = GrantLoginProgress<QrProgress>> + use<> {
self.state.subscribe()
}
}
impl<'a> IntoFuture for GrantLoginWithScannedQrCode<'a> {
type Output = Result<(), QRCodeGrantLoginError>;
boxed_into_future!(extra_bounds: 'a);
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut channel = EstablishedSecureChannel::from_qr_code(
self.client.inner.http_client.inner.clone(),
self.qr_code_data,
QrCodeMode::Reciprocate,
)
.await?;
let check_code = channel.check_code().to_owned();
self.state
.set(GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }));
let message = QrAuthMessage::LoginProtocols {
protocols: vec![LoginProtocolType::DeviceAuthorizationGrant],
homeserver: self.client.homeserver(),
};
channel.send_json(message).await?;
finish_login_grant(
self.client,
&mut channel,
self.device_creation_timeout,
&export_secrets_bundle(self.client).await?,
&self.state,
)
.await
})
}
}
#[derive(Debug)]
pub struct GrantLoginWithGeneratedQrCode<'a> {
client: &'a Client,
device_creation_timeout: Duration,
state: SharedObservable<GrantLoginProgress<GeneratedQrProgress>>,
}
impl<'a> GrantLoginWithGeneratedQrCode<'a> {
pub(crate) fn new(
client: &'a Client,
device_creation_timeout: Duration,
) -> GrantLoginWithGeneratedQrCode<'a> {
GrantLoginWithGeneratedQrCode { client, device_creation_timeout, state: Default::default() }
}
}
impl GrantLoginWithGeneratedQrCode<'_> {
pub fn subscribe_to_progress(
&self,
) -> impl Stream<Item = GrantLoginProgress<GeneratedQrProgress>> + use<> {
self.state.subscribe()
}
}
impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> {
type Output = Result<(), QRCodeGrantLoginError>;
boxed_into_future!(extra_bounds: 'a);
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let homeserver_url = self.client.homeserver();
let http_client = self.client.inner.http_client.clone();
let channel = SecureChannel::reciprocate(http_client, &homeserver_url).await?;
self.state.set(GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(channel.qr_code_data().clone()),
));
let channel = channel.connect().await?;
let (tx, rx) = tokio::sync::oneshot::channel();
self.state.set(GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(CheckCodeSender::new(tx)),
));
let check_code = rx.await.map_err(|_| SecureChannelError::CannotReceiveCheckCode)?;
let mut channel = channel.confirm(check_code)?;
finish_login_grant(
self.client,
&mut channel,
self.device_creation_timeout,
&export_secrets_bundle(self.client).await?,
&self.state,
)
.await
})
}
}
#[cfg(all(test, not(target_family = "wasm")))]
mod test {
use assert_matches2::{assert_let, assert_matches};
use futures_util::StreamExt;
use matrix_sdk_base::crypto::types::SecretsBundle;
use matrix_sdk_common::executor::spawn;
use matrix_sdk_test::async_test;
use oauth2::{EndUserVerificationUrl, VerificationUriComplete};
use ruma::{owned_device_id, owned_user_id};
use tokio::sync::oneshot;
use tracing::debug;
use vodozemac::Curve25519PublicKey;
use super::*;
use crate::{
authentication::oauth::qrcode::{
LoginFailureReason, QrAuthMessage,
messages::{AuthorizationGrant, LoginProtocolType},
secure_channel::{EstablishedSecureChannel, test::MockedRendezvousServer},
},
http_client::HttpClient,
test_utils::mocks::MatrixMockServer,
};
enum BobBehaviour {
HappyPath,
UnexpectedMessageInsteadOfLoginProtocol,
DeviceAlreadyExists,
DeviceNotCreated,
}
#[allow(clippy::too_many_arguments)]
async fn request_login_with_scanned_qr_code(
behaviour: BobBehaviour,
qr_code_rx: oneshot::Receiver<QrCodeData>,
check_code_tx: oneshot::Sender<u8>,
server: MatrixMockServer,
_rendezvous_server: MockedRendezvousServer,
device_authorization_grant: Option<AuthorizationGrant>,
secrets_bundle: Option<SecretsBundle>,
) {
let qr_code_data = qr_code_rx.await.expect("Bob should receive the QR code");
let mut bob = EstablishedSecureChannel::from_qr_code(
reqwest::Client::new(),
&qr_code_data,
QrCodeMode::Login,
)
.await
.expect("Bob should be able to connect the secure channel");
check_code_tx
.send(bob.check_code().to_digit())
.expect("Bob should be able to send the checkcode");
match behaviour {
BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol => {
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
return;
}
BobBehaviour::DeviceAlreadyExists => {
server.mock_get_device().ok().expect(1..).named("get_device").mount().await;
let message = QrAuthMessage::LoginProtocol {
protocol: LoginProtocolType::DeviceAuthorizationGrant,
device_authorization_grant: device_authorization_grant
.expect("Bob needs the device authorization grant"),
device_id: Curve25519PublicKey::from_base64(
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
)
.unwrap(),
};
bob.send_json(message).await.unwrap();
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginFailure message from Alice");
assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message);
assert_matches!(reason, LoginFailureReason::DeviceAlreadyExists);
return; }
_ => {
let message = QrAuthMessage::LoginProtocol {
protocol: LoginProtocolType::DeviceAuthorizationGrant,
device_authorization_grant: device_authorization_grant
.expect("Bob needs the device authorization grant"),
device_id: Curve25519PublicKey::from_base64(
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
)
.unwrap(),
};
bob.send_json(message).await.unwrap();
}
}
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginProtocolAccepted message from Alice");
assert_let!(QrAuthMessage::LoginProtocolAccepted = message);
match behaviour {
BobBehaviour::DeviceNotCreated => {
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginFailure message from Alice");
assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message);
assert_matches!(reason, LoginFailureReason::DeviceNotFound);
return; }
_ => {
server.mock_get_device().ok().expect(1..).named("get_device").mount().await;
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
}
}
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginSecrets message from Alice");
assert_let!(QrAuthMessage::LoginSecrets(bundle) = message);
assert_eq!(
serde_json::to_value(&secrets_bundle).unwrap(),
serde_json::to_value(&bundle).unwrap()
);
}
#[allow(clippy::too_many_arguments)]
async fn request_login_with_generated_qr_code(
behaviour: BobBehaviour,
channel: SecureChannel,
check_code_rx: oneshot::Receiver<u8>,
server: MatrixMockServer,
_rendezvous_server: MockedRendezvousServer,
homeserver: Url,
device_authorization_grant: Option<AuthorizationGrant>,
secrets_bundle: Option<SecretsBundle>,
) {
let channel =
channel.connect().await.expect("Bob should be able to connect the secure channel");
let check_code = check_code_rx.await.expect("Bob should receive the checkcode");
let mut bob = channel
.confirm(check_code)
.expect("Bob should be able to confirm the channel is secure");
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginProtocolAccepted message from Alice");
assert_let!(
QrAuthMessage::LoginProtocols { protocols, homeserver: alice_homeserver } = message
);
assert_eq!(protocols, vec![LoginProtocolType::DeviceAuthorizationGrant]);
assert_eq!(alice_homeserver, homeserver);
match behaviour {
BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol => {
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
return;
}
BobBehaviour::DeviceAlreadyExists => {
server.mock_get_device().ok().expect(1..).named("get_device").mount().await;
let message = QrAuthMessage::LoginProtocol {
protocol: LoginProtocolType::DeviceAuthorizationGrant,
device_authorization_grant: device_authorization_grant
.expect("Bob needs the device authorization grant"),
device_id: Curve25519PublicKey::from_base64(
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
)
.unwrap(),
};
bob.send_json(message).await.unwrap();
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginFailure message from Alice");
assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message);
assert_matches!(reason, LoginFailureReason::DeviceAlreadyExists);
return; }
_ => {
let message = QrAuthMessage::LoginProtocol {
protocol: LoginProtocolType::DeviceAuthorizationGrant,
device_authorization_grant: device_authorization_grant
.expect("Bob needs the device authorization grant"),
device_id: Curve25519PublicKey::from_base64(
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
)
.unwrap(),
};
bob.send_json(message).await.unwrap();
}
}
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginProtocolAccepted message from Alice");
assert_let!(QrAuthMessage::LoginProtocolAccepted = message);
match behaviour {
BobBehaviour::DeviceNotCreated => {
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginFailure message from Alice");
assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message);
assert_matches!(reason, LoginFailureReason::DeviceNotFound);
return; }
_ => {
server.mock_get_device().ok().expect(1..).named("get_device").mount().await;
let message = QrAuthMessage::LoginSuccess;
bob.send_json(message).await.unwrap();
}
}
let message = bob
.receive_json()
.await
.expect("Bob should receive the LoginSecrets message from Alice");
assert_let!(QrAuthMessage::LoginSecrets(bundle) = message);
assert_eq!(
serde_json::to_value(&secrets_bundle).unwrap(),
serde_json::to_value(&bundle).unwrap()
);
}
#[async_test]
async fn test_grant_login_with_generated_qr_code() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.generate();
let secrets_bundle = export_secrets_bundle(&alice)
.await
.expect("Alice should be able to export the secrets bundle");
let (qr_code_tx, qr_code_rx) = oneshot::channel();
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut qr_code_tx = Some(qr_code_tx);
let mut checkcode_rx = Some(checkcode_rx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(qr_code_data),
) => {
assert_matches!(state, GrantLoginProgress::Starting);
qr_code_tx
.take()
.expect("The QR code should only be forwarded once")
.send(qr_code_data.clone())
.expect("Alice should be able to forward the QR code");
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(checkcode_sender),
) => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(_)
)
);
let checkcode = checkcode_rx
.take()
.expect("The checkcode should only be forwarded once")
.await
.expect("Alice should receive the checkcode");
checkcode_sender
.send(checkcode)
.await
.expect("Alice should be able to forward the checkcode");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(_)
)
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
GrantLoginProgress::SyncingSecrets => {
assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. });
}
GrantLoginProgress::Done => {
assert_matches!(state, GrantLoginProgress::SyncingSecrets);
break;
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_scanned_qr_code(
BobBehaviour::HappyPath,
qr_code_rx,
checkcode_tx,
server,
rendezvous_server,
Some(device_authorization_grant),
Some(secrets_bundle),
)
.await;
});
grant.await.expect("Alice should be able to grant the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let client = HttpClient::new(reqwest::Client::new(), Default::default());
let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url)
.await
.expect("Bob should be able to create a secure channel.");
let qr_code_data = channel.qr_code_data().clone();
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let secrets_bundle = export_secrets_bundle(&alice)
.await
.expect("Alice should be able to export the secrets bundle");
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut checkcode_tx = Some(checkcode_tx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
assert_matches!(state, GrantLoginProgress::Starting);
checkcode_tx
.take()
.expect("The checkcode should only be forwarded once")
.send(check_code.to_digit())
.expect("Alice should be able to forward the checkcode");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. })
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
GrantLoginProgress::SyncingSecrets => {
assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. });
}
GrantLoginProgress::Done => {
assert_matches!(state, GrantLoginProgress::SyncingSecrets);
break;
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::HappyPath,
channel,
checkcode_rx,
server,
rendezvous_server,
alice.homeserver(),
Some(device_authorization_grant),
Some(secrets_bundle),
)
.await;
});
grant.await.expect("Alice should be able to grant the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_with_homeserver_swap() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
let login_server = MatrixMockServer::new().await;
login_server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
login_server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
login_server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let client = HttpClient::new(reqwest::Client::new(), Default::default());
let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url)
.await
.expect("Bob should be able to create a secure channel.");
let qr_code_data = channel.qr_code_data().clone();
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = login_server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let secrets_bundle = export_secrets_bundle(&alice)
.await
.expect("Alice should be able to export the secrets bundle");
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut checkcode_tx = Some(checkcode_tx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
assert_matches!(state, GrantLoginProgress::Starting);
checkcode_tx
.take()
.expect("The checkcode should only be forwarded once")
.send(check_code.to_digit())
.expect("Alice should be able to forward the checkcode");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. })
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
GrantLoginProgress::SyncingSecrets => {
assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. });
}
GrantLoginProgress::Done => {
assert_matches!(state, GrantLoginProgress::SyncingSecrets);
break;
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::HappyPath,
channel,
checkcode_rx,
login_server,
rendezvous_server,
alice.homeserver(),
Some(device_authorization_grant),
Some(secrets_bundle),
)
.await;
});
grant.await.expect("Alice should be able to grant the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_generated_qr_code_unexpected_message_instead_of_login_protocol()
{
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.generate();
let (qr_code_tx, qr_code_rx) = oneshot::channel();
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut qr_code_tx = Some(qr_code_tx);
let mut checkcode_rx = Some(checkcode_rx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(qr_code_data),
) => {
assert_matches!(state, GrantLoginProgress::Starting);
qr_code_tx
.take()
.expect("The QR code should only be forwarded once")
.send(qr_code_data.clone())
.expect("Alice should be able to forward the QR code");
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(checkcode_sender),
) => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(_)
)
);
let checkcode = checkcode_rx
.take()
.expect("The checkcode should only be forwarded once")
.await
.expect("Alice should receive the checkcode");
checkcode_sender
.send(checkcode)
.await
.expect("Alice should be able to forward the checkcode");
break;
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_scanned_qr_code(
BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol,
qr_code_rx,
checkcode_tx,
server,
rendezvous_server,
None,
None,
)
.await;
});
grant.await.expect_err("Alice should abort the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_unexpected_message_instead_of_login_protocol() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let client = HttpClient::new(reqwest::Client::new(), Default::default());
let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url)
.await
.expect("Bob should be able to create a secure channel.");
let qr_code_data = channel.qr_code_data().clone();
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut checkcode_tx = Some(checkcode_tx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
assert_matches!(state, GrantLoginProgress::Starting);
checkcode_tx
.take()
.expect("The checkcode should only be forwarded once")
.send(check_code.to_digit())
.expect("Alice should be able to forward the checkcode");
break;
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol,
channel,
checkcode_rx,
server,
rendezvous_server,
alice.homeserver(),
None,
None,
)
.await;
});
grant.await.expect_err("Alice should abort the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_generated_qr_code_device_already_exists() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.generate();
let (qr_code_tx, qr_code_rx) = oneshot::channel();
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut qr_code_tx = Some(qr_code_tx);
let mut checkcode_rx = Some(checkcode_rx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(qr_code_data),
) => {
assert_matches!(state, GrantLoginProgress::Starting);
qr_code_tx
.take()
.expect("The QR code should only be forwarded once")
.send(qr_code_data.clone())
.expect("Alice should be able to forward the QR code");
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(checkcode_sender),
) => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(_)
)
);
let checkcode = checkcode_rx
.take()
.expect("The checkcode should only be forwarded once")
.await
.expect("Alice should receive the checkcode");
checkcode_sender
.send(checkcode)
.await
.expect("Alice should be able to forward the checkcode");
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_scanned_qr_code(
BobBehaviour::DeviceAlreadyExists,
qr_code_rx,
checkcode_tx,
server,
rendezvous_server,
Some(device_authorization_grant),
None,
)
.await;
});
grant.await.expect_err("Alice should abort the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_device_already_exists() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let client = HttpClient::new(reqwest::Client::new(), Default::default());
let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url)
.await
.expect("Bob should be able to create a secure channel.");
let qr_code_data = channel.qr_code_data().clone();
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut checkcode_tx = Some(checkcode_tx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
assert_matches!(state, GrantLoginProgress::Starting);
checkcode_tx
.take()
.expect("The checkcode should only be forwarded once")
.send(check_code.to_digit())
.expect("Alice should be able to forward the checkcode");
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::DeviceAlreadyExists,
channel,
checkcode_rx,
server,
rendezvous_server,
alice.homeserver(),
Some(device_authorization_grant),
None,
)
.await;
});
grant.await.expect_err("Alice should abort the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_generated_qr_code_device_not_created() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.generate();
let (qr_code_tx, qr_code_rx) = oneshot::channel();
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut qr_code_tx = Some(qr_code_tx);
let mut checkcode_rx = Some(checkcode_rx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(qr_code_data),
) => {
assert_matches!(state, GrantLoginProgress::Starting);
qr_code_tx
.take()
.expect("The QR code should only be forwarded once")
.send(qr_code_data.clone())
.expect("Alice should be able to forward the QR code");
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(checkcode_sender),
) => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(_)
)
);
let checkcode = checkcode_rx
.take()
.expect("The checkcode should only be forwarded once")
.await
.expect("Alice should receive the checkcode");
checkcode_sender
.send(checkcode)
.await
.expect("Alice should be able to forward the checkcode");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrScanned(_)
)
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_scanned_qr_code(
BobBehaviour::DeviceNotCreated,
qr_code_rx,
checkcode_tx,
server,
rendezvous_server,
Some(device_authorization_grant),
None,
)
.await;
});
grant.await.expect_err("Alice should abort the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_device_not_created() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
let device_authorization_grant = AuthorizationGrant {
verification_uri_complete: Some(VerificationUriComplete::new(
"https://id.matrix.org/device/abcde".to_owned(),
)),
verification_uri: EndUserVerificationUrl::new(
"https://id.matrix.org/device/abcde?code=ABCDE".to_owned(),
)
.unwrap(),
};
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let client = HttpClient::new(reqwest::Client::new(), Default::default());
let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url)
.await
.expect("Bob should be able to create a secure channel.");
let qr_code_data = channel.qr_code_data().clone();
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let (checkcode_tx, checkcode_rx) = oneshot::channel();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
let verification_uri_complete =
device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
let mut checkcode_tx = Some(checkcode_tx);
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
assert_matches!(state, GrantLoginProgress::Starting);
checkcode_tx
.take()
.expect("The checkcode should only be forwarded once")
.send(check_code.to_digit())
.expect("Alice should be able to forward the checkcode");
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
assert_matches!(
state,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. })
);
assert_eq!(verification_uri.as_str(), verification_uri_complete);
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
let bob_task = spawn(async move {
request_login_with_generated_qr_code(
BobBehaviour::DeviceNotCreated,
channel,
checkcode_rx,
server,
rendezvous_server,
alice.homeserver(),
Some(device_authorization_grant),
None,
)
.await;
});
grant.await.expect_err("Alice should abort the login");
updates_task.await.expect("Alice should run through all progress states");
bob_task.await.expect("Bob's task should finish");
}
#[async_test]
async fn test_grant_login_with_generated_qr_code_session_expired() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::from_secs(2))
.await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.generate();
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(
GeneratedQrProgress::QrReady(_),
) => {
assert_matches!(state, GrantLoginProgress::Starting);
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
assert_matches!(grant.await, Err(QRCodeGrantLoginError::NotFound));
updates_task.await.expect("Alice should run through all progress states");
}
#[async_test]
async fn test_grant_login_with_scanned_qr_code_session_expired() {
let server = MatrixMockServer::new().await;
let rendezvous_server =
MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::from_secs(2))
.await;
debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url);
server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await;
server
.mock_upload_cross_signing_keys()
.ok()
.expect(1)
.named("upload_xsigning_keys")
.mount()
.await;
server
.mock_upload_cross_signing_signatures()
.ok()
.expect(1)
.named("upload_xsigning_signatures")
.mount()
.await;
let client = HttpClient::new(reqwest::Client::new(), Default::default());
let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url)
.await
.expect("Bob should be able to create a secure channel.");
let qr_code_data = channel.qr_code_data().clone();
let user_id = owned_user_id!("@alice:example.org");
let device_id = owned_device_id!("ALICE_DEVICE");
let alice = server
.client_builder_for_crypto_end_to_end(&user_id, &device_id)
.logged_in_with_oauth()
.build()
.await;
alice
.encryption()
.bootstrap_cross_signing(None)
.await
.expect("Alice should be able to set up cross signing");
let oauth = alice.oauth();
let grant = oauth
.grant_login_with_qr_code()
.device_creation_timeout(Duration::from_secs(2))
.scan(&qr_code_data);
let mut updates = grant.subscribe_to_progress();
let mut state = grant.state.get();
assert_matches!(state.clone(), GrantLoginProgress::Starting);
let updates_task = spawn(async move {
while let Some(update) = updates.next().await {
match &update {
GrantLoginProgress::Starting => {
assert_matches!(state, GrantLoginProgress::Starting);
}
GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. }) => {
assert_matches!(state, GrantLoginProgress::Starting);
}
_ => {
panic!("Alice should abort the process");
}
}
state = update;
}
});
assert_matches!(grant.await, Err(QRCodeGrantLoginError::NotFound));
updates_task.await.expect("Alice should run through all progress states");
}
}