diff --git a/bindings/matrix-sdk-crypto-ffi/src/verification.rs b/bindings/matrix-sdk-crypto-ffi/src/verification.rs index 86561706e..2b28e5713 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/verification.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/verification.rs @@ -550,26 +550,6 @@ pub enum VerificationRequestState { }, } -impl From for VerificationRequestState { - fn from(value: RustVerificationRequestState) -> Self { - match value { - // The clients do not need to distinguish `Created` and `Requested` state - RustVerificationRequestState::Created { .. } => Self::Requested, - RustVerificationRequestState::Requested { .. } => Self::Requested, - RustVerificationRequestState::Ready { - their_methods, - our_methods, - other_device_id: _, - } => Self::Ready { - their_methods: their_methods.iter().map(|m| m.to_string()).collect(), - our_methods: our_methods.iter().map(|m| m.to_string()).collect(), - }, - RustVerificationRequestState::Done => Self::Done, - RustVerificationRequestState::Cancelled(c) => Self::Cancelled { cancel_info: c.into() }, - } - } -} - /// The verificatoin request object which then can transition into some concrete /// verification method #[derive(uniffi::Object)] @@ -747,17 +727,57 @@ impl VerificationRequest { pub fn set_changes_listener(&self, listener: Box) { let stream = self.inner.changes(); - self.runtime.spawn(Self::changes_listener(stream, listener)); + self.runtime.spawn(Self::changes_listener(self.inner.to_owned(), stream, listener)); } /// Get the current state of the verification request. pub fn state(&self) -> VerificationRequestState { - self.inner.state().into() + Self::convert_verification_request(&self.inner, self.inner.state()) } } impl VerificationRequest { + fn convert_verification_request( + request: &InnerVerificationRequest, + value: RustVerificationRequestState, + ) -> VerificationRequestState { + match value { + // The clients do not need to distinguish `Created` and `Requested` state + RustVerificationRequestState::Created { .. } => VerificationRequestState::Requested, + RustVerificationRequestState::Requested { .. } => VerificationRequestState::Requested, + RustVerificationRequestState::Ready { + their_methods, + our_methods, + other_device_id: _, + } => VerificationRequestState::Ready { + their_methods: their_methods.iter().map(|m| m.to_string()).collect(), + our_methods: our_methods.iter().map(|m| m.to_string()).collect(), + }, + RustVerificationRequestState::Done => VerificationRequestState::Done, + RustVerificationRequestState::Transitioned { .. } => { + let their_methods = request + .their_supported_methods() + .expect("The transitioned state should know the other side's methods") + .into_iter() + .map(|m| m.to_string()) + .collect(); + let our_methods = request + .our_supported_methods() + .expect("The transitioned state should know our own supported methods") + .iter() + .map(|m| m.to_string()) + .collect(); + VerificationRequestState::Ready { their_methods, our_methods } + } + + RustVerificationRequestState::Cancelled(c) => { + VerificationRequestState::Cancelled { cancel_info: c.into() } + } + } + } + async fn changes_listener( + request: InnerVerificationRequest, mut stream: impl Stream + std::marker::Unpin, listener: Box, ) { @@ -771,7 +791,9 @@ impl VerificationRequest { | RustVerificationRequestState::Cancelled { .. } ); - listener.on_change(state.into()); + let state = Self::convert_verification_request(&request, state); + + listener.on_change(state); if should_break { break; diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 51bfb5fa7..5095e3b7f 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -1,5 +1,10 @@ # v0.7.0 +- Add a new variant to the `VerificationRequestState` enum called + `Transitioned`. This enum variant is used when a `VerificationRequest` + transitions into a concrete `Verification` object. The concrete `Verification` + object is given as associated data in the `Transitioned` enum variant. + - Replace the libolm backup encryption code with a native Rust version. This adds WASM support to the backups_v1 feature. diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index e51b86a91..5f53f2963 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -14,7 +14,7 @@ use std::{sync::Arc, time::Duration}; -use eyeball::shared::{Observable as SharedObservable, ObservableWriteGuard}; +use eyeball::shared::{Observable as SharedObservable, ObservableWriteGuard, WeakObservable}; use futures_core::Stream; use futures_util::StreamExt; use matrix_sdk_common::instant::Instant; @@ -48,7 +48,7 @@ use super::{ event_enums::{ CancelContent, DoneContent, OutgoingContent, ReadyContent, RequestContent, StartContent, }, - CancelInfo, Cancelled, FlowId, IdentitiesBeingVerified, Verification, VerificationStore, + CancelInfo, Cancelled, FlowId, Verification, VerificationStore, }; use crate::{ olm::ReadOnlyAccount, CryptoStoreError, OutgoingVerificationRequest, RoomMessageRequest, Sas, @@ -93,6 +93,14 @@ pub enum VerificationRequestState { /// request. other_device_id: OwnedDeviceId, }, + /// The verification request has transitioned into a concrete verification + /// flow. For example it transitioned into the emoji based SAS + /// verification. + Transitioned { + /// The concrete [`Verification`] object the verification request + /// transitioned into. + verification: Verification, + }, /// The verification flow that was started with this request has finished. Done, /// The verification process has been cancelled. @@ -114,6 +122,9 @@ impl From<&InnerRequest> for VerificationRequestState { our_methods: s.state.our_methods.to_owned(), other_device_id: s.state.other_device_id.to_owned(), }, + InnerRequest::Transitioned(s) => { + Self::Transitioned { verification: s.state.verification.to_owned() } + } InnerRequest::Passive(_) => { Self::Cancelled(Cancelled::new(true, CancelCode::Accepted).into()) } @@ -150,22 +161,27 @@ pub struct VerificationRequest { /// Cancelling a QR code verification should also cancel the request. This /// `RequestHandle` allows the QR code verification object to cancel the parent /// `VerificationRequest` object. -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub(crate) struct RequestHandle { - inner: SharedObservable, + inner: WeakObservable, } impl RequestHandle { pub fn cancel_with_code(&self, cancel_code: &CancelCode) { - let mut guard = self.inner.write(); - if let Some(updated) = guard.cancel(true, cancel_code) { - ObservableWriteGuard::set(&mut guard, updated); + if let Some(observable) = self.inner.upgrade() { + let mut guard = observable.write(); + + if let Some(updated) = guard.cancel(true, cancel_code) { + ObservableWriteGuard::set(&mut guard, updated); + } } } } impl From> for RequestHandle { fn from(inner: SharedObservable) -> Self { + let inner = inner.downgrade(); + Self { inner } } } @@ -264,8 +280,9 @@ impl VerificationRequest { /// The id of the other device that is participating in this verification. pub fn other_device_id(&self) -> Option { match &*self.inner.read() { - InnerRequest::Requested(r) => Some(r.state.other_device_id.clone()), - InnerRequest::Ready(r) => Some(r.state.other_device_id.clone()), + InnerRequest::Requested(r) => Some(r.state.other_device_id.to_owned()), + InnerRequest::Ready(r) => Some(r.state.other_device_id.to_owned()), + InnerRequest::Transitioned(r) => Some(r.state.ready.other_device_id.to_owned()), InnerRequest::Created(_) | InnerRequest::Passive(_) | InnerRequest::Done(_) @@ -314,6 +331,7 @@ impl VerificationRequest { match &*self.inner.read() { InnerRequest::Requested(r) => Some(r.state.their_methods.clone()), InnerRequest::Ready(r) => Some(r.state.their_methods.clone()), + InnerRequest::Transitioned(r) => Some(r.state.ready.their_methods.clone()), InnerRequest::Created(_) | InnerRequest::Passive(_) | InnerRequest::Done(_) @@ -329,6 +347,7 @@ impl VerificationRequest { match &*self.inner.read() { InnerRequest::Created(r) => Some(r.state.our_methods.clone()), InnerRequest::Ready(r) => Some(r.state.our_methods.clone()), + InnerRequest::Transitioned(r) => Some(r.state.ready.our_methods.clone()), InnerRequest::Requested(_) | InnerRequest::Passive(_) | InnerRequest::Done(_) @@ -367,7 +386,19 @@ impl VerificationRequest { #[cfg(feature = "qrcode")] pub async fn generate_qr_code(&self) -> Result, CryptoStoreError> { let inner = self.inner.get(); - inner.generate_qr_code(self.we_started, self.inner.clone().into()).await + + let ret = if let Some((state, verification)) = + inner.generate_qr_code(self.we_started, self.inner.clone().into()).await? + { + let mut inner = self.inner.write(); + ObservableWriteGuard::set(&mut inner, InnerRequest::Transitioned(state)); + + Some(verification) + } else { + None + }; + + Ok(ret) } /// Start a QR code verification by providing a scanned QR code for this @@ -382,22 +413,25 @@ impl VerificationRequest { &self, data: QrVerificationData, ) -> Result, ScanError> { - let future = if let InnerRequest::Ready(r) = &*self.inner.read() { - QrVerification::from_scan( - r.store.clone(), - r.other_user_id.clone(), - r.state.other_device_id.clone(), - r.flow_id.as_ref().to_owned(), - data, - self.we_started, - Some(self.inner.clone().into()), - ) - } else { - return Ok(None); - }; + let inner = self.inner.read().to_owned(); - // await future after self.inner read guard is released - let qr_verification = future.await?; + let (new_state, qr_verification) = match inner { + InnerRequest::Ready(r) => { + scan_qr_code(data, &r, &r.state, self.we_started, self.inner.to_owned().into()) + .await? + } + InnerRequest::Transitioned(r) => { + scan_qr_code( + data, + &r, + &r.state.ready, + self.we_started, + self.inner.to_owned().into(), + ) + .await? + } + _ => return Ok(None), + }; // We may have previously started our own QR verification (e.g. two devices // displaying QR code at the same time), so we need to replace it with the newly @@ -422,6 +456,9 @@ impl VerificationRequest { self.verification_cache.insert_qr(qr_verification.clone()); } + let mut guard = self.inner.write(); + ObservableWriteGuard::set(&mut guard, InnerRequest::Transitioned(new_state)); + Ok(Some(qr_verification)) } @@ -650,6 +687,7 @@ impl VerificationRequest { } } InnerRequest::Ready(_) + | InnerRequest::Transitioned(_) | InnerRequest::Passive(_) | InnerRequest::Done(_) | InnerRequest::Cancelled(_) => {} @@ -663,16 +701,52 @@ impl VerificationRequest { ) -> Result<(), CryptoStoreError> { let inner = self.inner.get(); - let InnerRequest::Ready(s) = inner else { - warn!( - sender = sender.as_str(), - device_id = content.from_device().as_str(), - "Received a key verification start event but we're not yet in the ready state" - ); - return Ok(()); - }; + match &inner { + InnerRequest::Created(_) + | InnerRequest::Requested(_) + | InnerRequest::Passive(_) + | InnerRequest::Done(_) + | InnerRequest::Cancelled(_) => { + warn!( + sender = sender.as_str(), + device_id = content.from_device().as_str(), + "Received a key verification start event but we're not yet in the ready state" + ); + Ok(()) + } + InnerRequest::Ready(s) => { + let s = s.clone(); - s.receive_start(sender, content, self.we_started, self.inner.clone().into()).await + if let Some(new_state) = s + .receive_start(sender, content, self.we_started, self.inner.clone().into()) + .await? + { + let mut inner = self.inner.write(); + ObservableWriteGuard::set(&mut inner, InnerRequest::Transitioned(new_state)); + } + + Ok(()) + } + InnerRequest::Transitioned(s) => { + // This is the same as the `Ready` state. We need to support this in the case + // someone tries QR code verification and notices that they can't scan the QR + // code for one reason or the other, in that case they are able + // to transition into the emoji based SAS verification. + // + // In this case we're going to from one `Transitioned` state into another. + let s = s.clone(); + + if let Some(new_state) = s + .receive_start(sender, content, self.we_started, self.inner.clone().into()) + .await? + { + let mut inner = self.inner.write(); + ObservableWriteGuard::set(&mut inner, InnerRequest::Transitioned(new_state)); + } + + Ok(()) + } + } } pub(crate) fn receive_done(&self, sender: &UserId, content: &DoneContent<'_>) { @@ -714,51 +788,73 @@ impl VerificationRequest { } } + fn start_sas_helper( + &self, + new_state: RequestState, + sas: Sas, + content: OutgoingContent, + other_device_id: DeviceIdOrAllDevices, + ) -> Option<(Sas, OutgoingVerificationRequest)> { + // We may have previously started QR verification and generated a QR code. If we + // now switch to SAS flow, the previous verification has to be replaced + cfg_if::cfg_if! { + if #[cfg(feature = "qrcode")] { + if self.verification_cache.get_qr(sas.other_user_id(), sas.flow_id().as_str()).is_some() { + debug!( + user_id = ?self.other_user(), + flow_id = self.flow_id().as_str(), + "We have an ongoing QR verification, replacing with SAS" + ); + self.verification_cache.replace(sas.clone().into()) + } else { + self.verification_cache.insert_sas(sas.clone()); + } + } else { + self.verification_cache.insert_sas(sas.clone()); + } + } + + let request = match content { + OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id( + self.other_user(), + other_device_id, + &content, + TransactionId::new(), + ) + .into(), + OutgoingContent::Room(room_id, content) => { + RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into() + } + }; + + let mut guard = self.inner.write(); + ObservableWriteGuard::set(&mut guard, InnerRequest::Transitioned(new_state)); + + Some((sas, request)) + } + /// Transition from this verification request into a SAS verification flow. pub async fn start_sas( &self, ) -> Result, CryptoStoreError> { let inner = self.inner.get(); + let other_device_id = inner.other_device_id(); Ok(match &inner { InnerRequest::Ready(s) => { - if let Some((sas, content)) = - s.clone().start_sas(self.we_started, self.inner.clone().into()).await? + if let Some((new_state, sas, content)) = + s.start_sas(self.we_started, self.inner.clone().into()).await? { - // We may have previously started QR verification and generated a QR code. If we - // now switch to SAS flow, the previous verification has to be replaced - cfg_if::cfg_if! { - if #[cfg(feature = "qrcode")] { - if self.verification_cache.get_qr(sas.other_user_id(), sas.flow_id().as_str()).is_some() { - debug!( - user_id = ?self.other_user(), - flow_id = self.flow_id().as_str(), - "We have an ongoing QR verification, replacing with SAS" - ); - self.verification_cache.replace(sas.clone().into()) - } else { - self.verification_cache.insert_sas(sas.clone()); - } - } else { - self.verification_cache.insert_sas(sas.clone()); - } - } - - let request = match content { - OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id( - self.other_user(), - inner.other_device_id(), - &content, - TransactionId::new(), - ) - .into(), - OutgoingContent::Room(room_id, content) => { - RoomMessageRequest { room_id, txn_id: TransactionId::new(), content } - .into() - } - }; - - Some((sas, request)) + self.start_sas_helper(new_state, sas, content, other_device_id) + } else { + None + } + } + InnerRequest::Transitioned(s) => { + if let Some((new_state, sas, content)) = + s.start_sas(self.we_started, self.inner.clone().into()).await? + { + self.start_sas_helper(new_state, sas, content, other_device_id) } else { None } @@ -789,6 +885,7 @@ enum InnerRequest { Created(RequestState), Requested(RequestState), Ready(RequestState), + Transitioned(RequestState), Passive(RequestState), Done(RequestState), Cancelled(RequestState), @@ -802,6 +899,9 @@ impl InnerRequest { InnerRequest::Ready(r) => { DeviceIdOrAllDevices::DeviceId(r.state.other_device_id.to_owned()) } + InnerRequest::Transitioned(r) => { + DeviceIdOrAllDevices::DeviceId(r.state.ready.other_device_id.to_owned()) + } InnerRequest::Passive(_) => DeviceIdOrAllDevices::AllDevices, InnerRequest::Done(_) => DeviceIdOrAllDevices::AllDevices, InnerRequest::Cancelled(_) => DeviceIdOrAllDevices::AllDevices, @@ -817,9 +917,10 @@ impl InnerRequest { fn receive_done(&self, content: &DoneContent<'_>) -> Option { let state = InnerRequest::Done(match self { - InnerRequest::Ready(s) => s.clone().into_done(content), + InnerRequest::Transitioned(s) => s.clone().into_done(content), InnerRequest::Passive(s) => s.clone().into_done(content), InnerRequest::Done(_) + | InnerRequest::Ready(_) | InnerRequest::Created(_) | InnerRequest::Requested(_) | InnerRequest::Cancelled(_) => return None, @@ -850,6 +951,10 @@ impl InnerRequest { print_info(); s.clone().into_canceled(cancelled_by_us, cancel_code) } + InnerRequest::Transitioned(s) => { + print_info(); + s.clone().into_canceled(cancelled_by_us, cancel_code) + } InnerRequest::Passive(_) | InnerRequest::Done(_) | InnerRequest::Cancelled(_) => { return None } @@ -863,14 +968,15 @@ impl InnerRequest { &self, we_started: bool, request_handle: RequestHandle, - ) -> Result, CryptoStoreError> { + ) -> Result, QrVerification)>, CryptoStoreError> { match self { - InnerRequest::Created(_) => Ok(None), - InnerRequest::Requested(_) => Ok(None), + InnerRequest::Created(_) + | InnerRequest::Requested(_) + | InnerRequest::Passive(_) + | InnerRequest::Done(_) + | InnerRequest::Cancelled(_) => Ok(None), InnerRequest::Ready(s) => s.generate_qr_code(we_started, request_handle).await, - InnerRequest::Passive(_) => Ok(None), - InnerRequest::Done(_) => Ok(None), - InnerRequest::Cancelled(_) => Ok(None), + InnerRequest::Transitioned(s) => s.generate_qr_code(we_started, request_handle).await, } } } @@ -1046,150 +1152,357 @@ struct Ready { pub other_device_id: OwnedDeviceId, } -impl RequestState { - fn to_started_sas( - &self, - content: &StartContent<'_>, - identities: IdentitiesBeingVerified, - we_started: bool, - request_handle: RequestHandle, - ) -> Result { - Sas::from_start_event( - (*self.flow_id).to_owned(), - content, - identities, - Some(request_handle), - we_started, - ) +#[cfg(feature = "qrcode")] +async fn scan_qr_code( + data: QrVerificationData, + request_state: &RequestState, + state: &Ready, + we_started: bool, + request_handle: RequestHandle, +) -> Result<(RequestState, QrVerification), ScanError> { + let verification = QrVerification::from_scan( + request_state.store.to_owned(), + request_state.other_user_id.to_owned(), + state.other_device_id.to_owned(), + request_state.flow_id.as_ref().to_owned(), + data, + we_started, + Some(request_handle), + ) + .await?; + + let new_state = RequestState { + verification_cache: request_state.verification_cache.to_owned(), + store: request_state.store.to_owned(), + flow_id: request_state.flow_id.to_owned(), + other_user_id: request_state.other_user_id.to_owned(), + state: Transitioned { + ready: state.to_owned(), + verification: verification.to_owned().into(), + }, + }; + + Ok((new_state, verification)) +} + +#[cfg(feature = "qrcode")] +async fn generate_qr_code( + request_state: &RequestState, + state: &Ready, + we_started: bool, + request_handle: RequestHandle, +) -> Result, QrVerification)>, CryptoStoreError> { + use crate::ReadOnlyUserIdentities; + + // If we didn't state that we support showing QR codes or if the other + // side doesn't support scanning QR codes bail early. + if !state.our_methods.contains(&VerificationMethod::QrCodeShowV1) + || !state.their_methods.contains(&VerificationMethod::QrCodeScanV1) + { + return Ok(None); } - #[cfg(feature = "qrcode")] - async fn generate_qr_code( - &self, - we_started: bool, - request_handle: RequestHandle, - ) -> Result, CryptoStoreError> { - use crate::ReadOnlyUserIdentities; - - // If we didn't state that we support showing QR codes or if the other - // side doesn't support scanning QR codes bail early. - if !self.state.our_methods.contains(&VerificationMethod::QrCodeShowV1) - || !self.state.their_methods.contains(&VerificationMethod::QrCodeScanV1) - { - return Ok(None); - } - - let Some(device) = - self.store.get_device(&self.other_user_id, &self.state.other_device_id).await? else { + let Some(device) = + request_state.store.get_device(&request_state.other_user_id, &state.other_device_id).await? else { warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), "Can't create a QR code, the device that accepted the \ verification doesn't exist" ); return Ok(None); }; - let identities = self.store.get_identities(device).await?; + let identities = request_state.store.get_identities(device).await?; - let verification = if let Some(identity) = &identities.identity_being_verified { - match &identity { - ReadOnlyUserIdentities::Own(i) => { - if let Some(master_key) = i.master_key().get_first_key() { - if identities.can_sign_devices().await { - if let Some(device_key) = identities.other_device().ed25519_key() { - Some(QrVerification::new_self( - self.flow_id.as_ref().to_owned(), - master_key.to_owned(), - device_key.to_owned(), - identities, - we_started, - Some(request_handle), - )) - } else { - warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), - "Can't create a QR code, the other device \ - doesn't have a valid device key" - ); - None - } - } else { - Some(QrVerification::new_self_no_master( - self.store.clone(), - self.flow_id.as_ref().to_owned(), + let verification = if let Some(identity) = &identities.identity_being_verified { + match &identity { + ReadOnlyUserIdentities::Own(i) => { + if let Some(master_key) = i.master_key().get_first_key() { + if identities.can_sign_devices().await { + if let Some(device_key) = identities.other_device().ed25519_key() { + Some(QrVerification::new_self( + request_state.flow_id.as_ref().to_owned(), master_key.to_owned(), - identities, - we_started, - Some(request_handle), - )) - } - } else { - warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), - "Can't create a QR code, our cross signing identity \ - doesn't contain a valid master key" - ); - None - } - } - ReadOnlyUserIdentities::Other(i) => { - if let Some(other_master) = i.master_key().get_first_key() { - // TODO we can get the master key from the public - // identity if we don't have the private one and we - // trust the public one. - if let Some(own_master) = identities - .private_identity - .master_public_key() - .await - .and_then(|m| m.get_first_key().map(|m| m.to_owned())) - { - Some(QrVerification::new_cross( - self.flow_id.as_ref().to_owned(), - own_master, - other_master.to_owned(), + device_key.to_owned(), identities, we_started, Some(request_handle), )) } else { warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), - "Can't create a QR code, we don't trust our own \ - master key" + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), + "Can't create a QR code, the other device doesn't have a valid device key" ); None } + } else { + Some(QrVerification::new_self_no_master( + request_state.store.clone(), + request_state.flow_id.as_ref().to_owned(), + master_key.to_owned(), + identities, + we_started, + Some(request_handle), + )) + } + } else { + warn!( + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), + "Can't create a QR code, our cross signing identity \ + doesn't contain a valid master key" + ); + None + } + } + ReadOnlyUserIdentities::Other(i) => { + if let Some(other_master) = i.master_key().get_first_key() { + // TODO we can get the master key from the public + // identity if we don't have the private one and we + // trust the public one. + if let Some(own_master) = identities + .private_identity + .master_public_key() + .await + .and_then(|m| m.get_first_key().map(|m| m.to_owned())) + { + Some(QrVerification::new_cross( + request_state.flow_id.as_ref().to_owned(), + own_master, + other_master.to_owned(), + identities, + we_started, + Some(request_handle), + )) } else { warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), - "Can't create a QR code, the user's identity \ - doesn't have a valid master key" + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), + "Can't create a QR code, we don't trust our own \ + master key" ); None } + } else { + warn!( + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), + "Can't create a QR code, the user's identity \ + doesn't have a valid master key" + ); + None } } - } else { - warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), - "Can't create a QR code, the user doesn't have a valid cross \ + } + } else { + warn!( + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), + "Can't create a QR code, the user doesn't have a valid cross \ signing identity." - ); + ); - None + None + }; + + if let Some(verification) = verification { + let new_state = RequestState { + verification_cache: request_state.verification_cache.to_owned(), + store: request_state.store.to_owned(), + flow_id: request_state.flow_id.to_owned(), + other_user_id: request_state.other_user_id.to_owned(), + state: Transitioned { + ready: state.to_owned(), + verification: verification.to_owned().into(), + }, }; - if let Some(verification) = &verification { - self.verification_cache.insert_qr(verification.clone()); - } + request_state.verification_cache.insert_qr(verification.to_owned()); - Ok(verification) + Ok(Some((new_state, verification))) + } else { + Ok(None) + } +} + +async fn receive_start( + sender: &UserId, + content: &StartContent<'_>, + we_started: bool, + request_handle: RequestHandle, + request_state: &RequestState, + state: &Ready, +) -> Result>, CryptoStoreError> { + info!( + sender = sender.as_str(), + device = content.from_device().as_str(), + "Received a new verification start event", + ); + + let Some(device) = request_state.store.get_device(sender, content.from_device()).await? else { + warn!( + sender = sender.as_str(), + device = content.from_device().as_str(), + "Received a key verification start event from an unknown device", + ); + + return Ok(None); + }; + + let identities = request_state.store.get_identities(device.clone()).await?; + let own_user_id = request_state.store.account.user_id(); + let own_device_id = request_state.store.account.device_id(); + + match content.method() { + StartMethod::SasV1(_) => { + match Sas::from_start_event( + (*request_state.flow_id).to_owned(), + content, + identities, + Some(request_handle), + we_started, + ) { + Ok(new) => { + if let Some(Verification::SasV1(_old)) = + request_state.verification_cache.get(sender, request_state.flow_id.as_str()) + { + // If there is already a SAS verification, i.e. we already started one + // before the other side tried to do the same; ignore it if we did and + // we're the lexicographically smaller user ID (or device ID if equal). + use std::cmp::Ordering; + if !matches!( + (sender.cmp(own_user_id), device.device_id().cmp(own_device_id)), + (Ordering::Greater, _) | (Ordering::Equal, Ordering::Greater) + ) { + info!( + "Started a new SAS verification, replacing an already started one." + ); + request_state.verification_cache.replace_sas(new.to_owned()); + + Ok(Some(state.to_transitioned(request_state, new.into()))) + } else { + Ok(None) + } + } else { + info!("Started a new SAS verification."); + request_state.verification_cache.insert_sas(new.to_owned()); + Ok(Some(state.to_transitioned(request_state, new.into()))) + } + } + Err(c) => { + warn!( + user_id = ?device.user_id(), + device_id = ?device.device_id(), + content = ?c, + "Can't start key verification, canceling.", + ); + request_state.verification_cache.queue_up_content( + device.user_id(), + device.device_id(), + c, + None, + ); + + Ok(None) + } + } + } + #[cfg(feature = "qrcode")] + StartMethod::ReciprocateV1(_) => { + if let Some(qr_verification) = + request_state.verification_cache.get_qr(sender, content.flow_id()) + { + if let Some(request) = qr_verification.receive_reciprocation(content) { + request_state.verification_cache.add_request(request.into()) + } + trace!( + sender = ?identities.device_being_verified.user_id(), + device_id = ?identities.device_being_verified.device_id(), + verification = ?qr_verification, + "Received a QR code reciprocation" + ); + + Ok(None) + } else { + Ok(None) + } + } + m => { + warn!(method = ?m, "Received a key verification start event with an unsupported method"); + Ok(None) + } + } +} + +async fn start_sas( + request_state: &RequestState, + state: &Ready, + we_started: bool, + request_handle: RequestHandle, +) -> Result, Sas, OutgoingContent)>, CryptoStoreError> { + if !state.their_methods.contains(&VerificationMethod::SasV1) { + return Ok(None); + } + + // TODO signal why starting the sas flow doesn't work? + let Some(device) = + request_state.store.get_device(&request_state.other_user_id, &state.other_device_id).await? else { + warn!( + user_id = request_state.other_user_id.as_str(), + device_id = state.other_device_id.as_str(), + "Can't start the SAS verification flow, the device that \ + accepted the verification doesn't exist" + ); + return Ok(None); + }; + + let identities = request_state.store.get_identities(device).await?; + + let (state, sas, content) = match request_state.flow_id.as_ref() { + FlowId::ToDevice(t) => { + let (sas, content) = + Sas::start(identities, t.to_owned(), we_started, Some(request_handle)); + + let state = + Transitioned { ready: state.to_owned(), verification: sas.to_owned().into() }; + + (state, sas, content) + } + FlowId::InRoom(r, e) => { + let (sas, content) = Sas::start_in_room( + e.to_owned(), + r.to_owned(), + identities, + we_started, + request_handle, + ); + let state = + Transitioned { ready: state.to_owned(), verification: sas.to_owned().into() }; + (state, sas, content) + } + }; + + let state = RequestState { + verification_cache: request_state.verification_cache.to_owned(), + store: request_state.store.to_owned(), + flow_id: request_state.flow_id.to_owned(), + other_user_id: request_state.other_user_id.to_owned(), + state, + }; + + Ok(Some((state, sas, content))) +} + +impl RequestState { + #[cfg(feature = "qrcode")] + async fn generate_qr_code( + &self, + we_started: bool, + request_handle: RequestHandle, + ) -> Result, QrVerification)>, CryptoStoreError> { + generate_qr_code(self, &self.state, we_started, request_handle).await } async fn receive_start( @@ -1198,131 +1511,67 @@ impl RequestState { content: &StartContent<'_>, we_started: bool, request_handle: RequestHandle, - ) -> Result<(), CryptoStoreError> { - info!( - sender = sender.as_str(), - device = content.from_device().as_str(), - "Received a new verification start event", - ); - - let Some(device) = self.store.get_device(sender, content.from_device()).await? else { - warn!( - sender = sender.as_str(), - device = content.from_device().as_str(), - "Received a key verification start event from an unknown device", - ); - - return Ok(()); - }; - - let identities = self.store.get_identities(device.clone()).await?; - let own_user_id = self.store.account.user_id(); - let own_device_id = self.store.account.device_id(); - - match content.method() { - StartMethod::SasV1(_) => { - match self.to_started_sas(content, identities, we_started, request_handle) { - Ok(s) => { - let start_new = if let Some(Verification::SasV1(_sas)) = - self.verification_cache.get(sender, self.flow_id.as_str()) - { - // If there is already a SAS verification, i.e. we already started one - // before the other side tried to do the same; ignore it if we did and - // we're the lexicographically smaller user ID (or device ID if equal). - use std::cmp::Ordering; - !matches!( - (sender.cmp(own_user_id), device.device_id().cmp(own_device_id)), - (Ordering::Greater, _) | (Ordering::Equal, Ordering::Greater) - ) - } else { - true - }; - - if start_new { - info!("Started a new SAS verification."); - self.verification_cache.replace_sas(s); - } - } - Err(c) => { - warn!( - user_id = ?device.user_id(), - device_id = ?device.device_id(), - content = ?c, - "Can't start key verification, canceling.", - ); - self.verification_cache.queue_up_content( - device.user_id(), - device.device_id(), - c, - None, - ) - } - } - } - #[cfg(feature = "qrcode")] - StartMethod::ReciprocateV1(_) => { - if let Some(qr_verification) = - self.verification_cache.get_qr(sender, content.flow_id()) - { - if let Some(request) = qr_verification.receive_reciprocation(content) { - self.verification_cache.add_request(request.into()) - } - trace!( - sender = ?identities.device_being_verified.user_id(), - device_id = ?identities.device_being_verified.device_id(), - verification = ?qr_verification, - "Received a QR code reciprocation" - ) - } - } - m => { - warn!(method = ?m, "Received a key verification start event with an unsupported method") - } - } - - Ok(()) + ) -> Result>, CryptoStoreError> { + receive_start(sender, content, we_started, request_handle, self, &self.state).await } async fn start_sas( - self, + &self, we_started: bool, request_handle: RequestHandle, - ) -> Result, CryptoStoreError> { - if !self.state.their_methods.contains(&VerificationMethod::SasV1) { - return Ok(None); + ) -> Result, Sas, OutgoingContent)>, CryptoStoreError> { + start_sas(self, &self.state, we_started, request_handle).await + } +} + +impl Ready { + fn to_transitioned( + &self, + request_state: &RequestState, + verification: Verification, + ) -> RequestState { + RequestState { + verification_cache: request_state.verification_cache.to_owned(), + store: request_state.store.to_owned(), + flow_id: request_state.flow_id.to_owned(), + other_user_id: request_state.other_user_id.to_owned(), + state: Transitioned { ready: self.clone(), verification }, } + } +} - // TODO signal why starting the sas flow doesn't work? - let Some(device) = - self.store.get_device(&self.other_user_id, &self.state.other_device_id).await? else { - warn!( - user_id = self.other_user_id.as_str(), - device_id = self.state.other_device_id.as_str(), - "Can't start the SAS verification flow, the device that \ - accepted the verification doesn't exist" - ); - return Ok(None); - }; +#[derive(Clone, Debug)] +struct Transitioned { + ready: Ready, + verification: Verification, +} - let identities = self.store.get_identities(device).await?; +impl RequestState { + #[cfg(feature = "qrcode")] + async fn generate_qr_code( + &self, + we_started: bool, + request_handle: RequestHandle, + ) -> Result, QrVerification)>, CryptoStoreError> { + generate_qr_code(self, &self.state.ready, we_started, request_handle).await + } - Ok(Some(match self.flow_id.as_ref() { - FlowId::ToDevice(t) => { - let (sas, content) = - Sas::start(identities, t.to_owned(), we_started, Some(request_handle)); - (sas, content) - } - FlowId::InRoom(r, e) => { - let (sas, content) = Sas::start_in_room( - e.to_owned(), - r.to_owned(), - identities, - we_started, - request_handle, - ); - (sas, content) - } - })) + async fn receive_start( + &self, + sender: &UserId, + content: &StartContent<'_>, + we_started: bool, + request_handle: RequestHandle, + ) -> Result>, CryptoStoreError> { + receive_start(sender, content, we_started, request_handle, self, &self.state.ready).await + } + + async fn start_sas( + &self, + we_started: bool, + request_handle: RequestHandle, + ) -> Result, Sas, OutgoingContent)>, CryptoStoreError> { + start_sas(self, &self.state.ready, we_started, request_handle).await } } @@ -1355,7 +1604,7 @@ mod tests { cache::VerificationCache, event_enums::{OutgoingContent, ReadyContent, RequestContent, StartContent}, test::{alice_id, bob_id, setup_stores}, - FlowId, + FlowId, Verification, }, ReadOnlyDevice, VerificationRequestState, }; @@ -1460,6 +1709,15 @@ mod tests { let alice_sas = alice_request.verification_cache.get_sas(bob_device.user_id(), &flow_id).unwrap(); + assert_matches!( + alice_request.state(), + VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + ); + assert_matches!( + bob_request.state(), + VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + ); + assert!(!bob_sas.is_cancelled()); assert!(!alice_sas.is_cancelled()); } @@ -1510,6 +1768,15 @@ mod tests { let alice_sas = alice_request.verification_cache.get_sas(bob_device.user_id(), &flow_id).unwrap(); + assert_matches!( + alice_request.state(), + VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + ); + assert_matches!( + bob_request.state(), + VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + ); + assert!(!bob_sas.is_cancelled()); assert!(!alice_sas.is_cancelled()); assert!(alice_sas.started_from_request()); @@ -1563,13 +1830,29 @@ mod tests { let alice_verification = alice_request.generate_qr_code().await.unwrap(); let bob_verification = bob_request.generate_qr_code().await.unwrap(); + assert_matches!( + alice_request.state(), + VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + ); + assert_matches!( + bob_request.state(), + VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + ); + assert!(alice_verification.is_some()); assert!(bob_verification.is_some()); // Now only Alice scans Bob's code let bob_qr_code = bob_verification.unwrap().to_bytes().unwrap(); let bob_qr_code = QrVerificationData::from_bytes(bob_qr_code).unwrap(); - let alice_verification = alice_request.scan_qr_code(bob_qr_code).await.unwrap().unwrap(); + let _ = alice_request.scan_qr_code(bob_qr_code).await.unwrap().unwrap(); + + let alice_verification = assert_matches!( + alice_request.state(), + VerificationRequestState::Transitioned { + verification: Verification::QrV1(v) + } => v + ); // Finally we assert that the verification has been reciprocated rather than // cancelled due to a duplicate verification flow @@ -1628,12 +1911,21 @@ mod tests { let alice_verification = alice_request.generate_qr_code().await.unwrap(); let bob_verification = bob_request.generate_qr_code().await.unwrap(); + assert_matches!( + alice_request.state(), + VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + ); + assert!(alice_verification.is_some()); assert!(bob_verification.is_some()); // Alice can now start SAS verification flow instead of QR without cancelling // the request let (sas, _) = alice_request.start_sas().await.unwrap().unwrap(); + assert_matches!( + alice_request.state(), + VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + ); assert!(!sas.is_cancelled()); } }