feat: Expose a verification state listener directly from Encryption
This commit is contained in:
parent
a6c2719976
commit
eea475854c
|
@ -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());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue