feat: Expose a verification state listener directly from Encryption

This commit is contained in:
Stefan Ceriu 2024-03-06 17:41:15 +02:00 committed by Damir Jelić
parent a6c2719976
commit eea475854c
5 changed files with 172 additions and 9 deletions

View File

@ -1,7 +1,10 @@
use std::sync::Arc;
use futures_util::StreamExt;
use matrix_sdk::encryption::{backups, recovery};
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use thiserror::Error;
use zeroize::Zeroize;
@ -34,6 +37,11 @@ pub trait RecoveryStateListener: Sync + Send {
fn on_update(&self, status: RecoveryState);
}
#[uniffi::export(callback_interface)]
pub trait VerificationStateListener: Sync + Send {
fn on_update(&self, status: VerificationState);
}
#[derive(uniffi::Enum)]
pub enum BackupUploadState {
Waiting,
@ -186,6 +194,23 @@ impl From<recovery::EnableProgress> for EnableRecoveryProgress {
}
}
#[derive(uniffi::Enum)]
pub enum VerificationState {
Unknown,
Verified,
Unverified,
}
impl From<encryption::VerificationState> for VerificationState {
fn from(value: encryption::VerificationState) -> Self {
match &value {
encryption::VerificationState::Unknown => Self::Unknown,
encryption::VerificationState::Verified => Self::Verified,
encryption::VerificationState::Unverified => Self::Unverified,
}
}
}
#[uniffi::export(async_runtime = "tokio")]
impl Encryption {
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
@ -326,4 +351,20 @@ impl Encryption {
Ok(result?)
}
pub fn verification_state(&self) -> VerificationState {
self.inner.verification_state().get().into()
}
pub fn verification_state_listener(
self: Arc<Self>,
listener: Box<dyn VerificationStateListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.verification_state();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
while let Some(verification_state) = subscriber.next().await {
listener.on_update(verification_state.into());
}
})))
}
}

View File

@ -90,7 +90,7 @@ use crate::{
};
#[cfg(feature = "e2e-encryption")]
use crate::{
encryption::{Encryption, EncryptionData, EncryptionSettings},
encryption::{Encryption, EncryptionData, EncryptionSettings, VerificationState},
store_locks::CrossProcessStoreLock,
};
@ -279,6 +279,10 @@ pub(crate) struct ClientInner {
/// End-to-end encryption related state.
#[cfg(feature = "e2e-encryption")]
pub(crate) e2ee: EncryptionData,
/// The verification state of our own device.
#[cfg(feature = "e2e-encryption")]
pub(crate) verification_state: SharedObservable<VerificationState>,
}
impl ClientInner {
@ -322,6 +326,8 @@ impl ClientInner {
event_cache,
#[cfg(feature = "e2e-encryption")]
e2ee: EncryptionData::new(encryption_settings),
#[cfg(feature = "e2e-encryption")]
verification_state: SharedObservable::new(VerificationState::Unknown),
};
#[allow(clippy::let_and_return)]

View File

@ -24,7 +24,7 @@ use std::{
sync::{Arc, Mutex as StdMutex},
};
use eyeball::SharedObservable;
use eyeball::{SharedObservable, Subscriber};
use futures_core::Stream;
use futures_util::{
future::try_join,
@ -186,6 +186,21 @@ pub enum BackupDownloadStrategy {
Manual,
}
/// The verification state of our own device
///
/// This enum tells us if our own user identity trusts these devices, in other
/// words it tells us if the user identity has signed the device.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VerificationState {
/// The verification state is unknown for now.
Unknown,
/// The device is considered to be verified, it has been signed by its user
/// identity.
Verified,
/// The device is unverified.
Unverified,
}
impl Client {
pub(crate) async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
self.base_client().olm_machine().await
@ -222,7 +237,7 @@ impl Client {
let response = self.send(request, None).await?;
self.mark_request_as_sent(request_id, &response).await?;
self.encryption().recovery().update_state_after_keys_query(&response).await;
self.encryption().update_state_after_keys_query(&response).await;
Ok(response)
}
@ -611,6 +626,32 @@ impl Encryption {
}
}
/// Get a [`Subscriber`] for the [`VerificationState`].
///
/// # Examples
///
/// ```no_run
/// use matrix_sdk::{encryption, Client};
/// use url::Url;
///
/// # async {
/// let homeserver = Url::parse("http://example.com")?;
/// let client = Client::new(homeserver).await?;
/// let mut subscriber = client.encryption().verification_state();
///
/// let current_value = subscriber.get();
///
/// println!("The current verification state is: {current_value:?}");
///
/// if let Some(verification_state) = subscriber.next().await {
/// println!("Received verification state update {:?}", verification_state)
/// }
/// # anyhow::Ok(()) };
/// ```
pub fn verification_state(&self) -> Subscriber<VerificationState> {
self.client.inner.verification_state.subscribe()
}
/// Get a verification object with the given flow id.
pub async fn get_verification(&self, user_id: &UserId, flow_id: &str) -> Option<Verification> {
let olm = self.client.olm_machine().await;
@ -1171,7 +1212,7 @@ impl Encryption {
if prev_holder == lock_value {
return Ok(());
}
warn!("recreating cross-process store lock with a different holder value: prev was {prev_holder}, new is {lock_value}");
warn!("Recreating cross-process store lock with a different holder value: prev was {prev_holder}, new is {lock_value}");
}
let olm_machine = self.client.base_client().olm_machine().await;
@ -1309,6 +1350,8 @@ impl Encryption {
if let Err(e) = this.recovery().setup().await {
error!("Couldn't setup and resume recovery {e:?}");
}
this.update_verification_state().await;
}));
Ok(())
@ -1321,7 +1364,43 @@ impl Encryption {
if let Some(task) = task {
if let Err(err) = task.await {
warn!("error when initializing backups: {err}");
warn!("Error when initializing backups: {err}");
}
}
}
pub(crate) async fn update_state_after_keys_query(&self, response: &get_keys::v3::Response) {
self.recovery().update_state_after_keys_query(response).await;
// Only update the verification_state if our own devices changed
if let Some(user_id) = self.client.user_id() {
let contains_own_device = response.device_keys.contains_key(user_id);
if contains_own_device {
self.update_verification_state().await;
}
}
}
async fn update_verification_state(&self) {
match self.get_own_device().await {
Ok(device) => {
if let Some(device) = device {
let is_verified = device.is_cross_signed_by_owner();
if is_verified {
self.client.inner.verification_state.set(VerificationState::Verified);
} else {
self.client.inner.verification_state.set(VerificationState::Unverified);
}
} else {
warn!("Couldn't find out own device in the store.");
self.client.inner.verification_state.set(VerificationState::Unknown);
}
}
Err(error) => {
warn!("Failed retrieving own device: {error}");
self.client.inner.verification_state.set(VerificationState::Unknown);
}
}
}

View File

@ -3,9 +3,11 @@ use std::{
sync::{Arc, Mutex},
};
use futures_util::FutureExt;
use imbl::HashSet;
use matrix_sdk::{
config::RequestConfig,
encryption::VerificationState,
matrix_auth::{MatrixSession, MatrixSessionTokens},
Client,
};
@ -336,16 +338,28 @@ async fn test_own_verification() {
.await
.unwrap();
// Subscribe to verification state updates
let mut verification_state_subscriber = alice.encryption().verification_state();
assert_eq!(alice.encryption().verification_state().get(), VerificationState::Unknown);
server.add_known_device(&device_id);
// Have Alice bootstrap cross-signing.
bootstrap_cross_signing(&alice).await;
// The local device is considered verified by default.
// The local device is considered verified by default, we need a keys query to
// run
let own_device = alice.encryption().get_device(&user_id, &device_id).await.unwrap().unwrap();
assert!(own_device.is_verified());
assert!(!own_device.is_deleted());
// The device is not considered cross signed yet
assert_eq!(
verification_state_subscriber.next().now_or_never().flatten().unwrap(),
VerificationState::Unverified
);
assert_eq!(alice.encryption().verification_state().get(), VerificationState::Unverified);
// Manually re-verifying doesn't change the outcome.
own_device.verify().await.unwrap();
assert!(own_device.is_verified());
@ -364,6 +378,22 @@ async fn test_own_verification() {
// Manually re-verifying doesn't change the outcome.
user_identity.verify().await.unwrap();
assert!(user_identity.is_verified());
// Force a keys query to pick up the cross-signing state
let mut sync_response_builder = SyncResponseBuilder::new();
sync_response_builder.add_change_device(&user_id);
{
mock_sync(&server.server, sync_response_builder.build_json_sync_response(), None).await;
alice.sync_once(Default::default()).await.unwrap();
}
// The device should now be cross-signed
assert_eq!(
verification_state_subscriber.next().now_or_never().unwrap().unwrap(),
VerificationState::Verified
);
assert_eq!(alice.encryption().verification_state().get(), VerificationState::Verified);
}
#[async_test]

View File

@ -10,7 +10,7 @@ use ruma::{
},
events::{presence::PresenceEvent, AnyGlobalAccountDataEvent},
serde::Raw,
OwnedRoomId,
OwnedRoomId, OwnedUserId, UserId,
};
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
@ -52,6 +52,8 @@ pub struct SyncResponseBuilder {
/// Internal counter to enable the `prev_batch` and `next_batch` of each
/// sync response to vary.
batch_counter: i64,
/// The device lists of the user.
changed_device_lists: Vec<OwnedUserId>,
}
impl SyncResponseBuilder {
@ -136,6 +138,11 @@ impl SyncResponseBuilder {
self
}
pub fn add_change_device(&mut self, user_id: &UserId) -> &mut Self {
self.changed_device_lists.push(user_id.to_owned());
self
}
/// Builds a sync response as a JSON Value containing the events we queued
/// so far.
///
@ -155,7 +162,7 @@ impl SyncResponseBuilder {
"device_one_time_keys_count": {},
"next_batch": next_batch,
"device_lists": {
"changed": [],
"changed": self.changed_device_lists,
"left": [],
},
"rooms": {