diff --git a/.typos.toml b/.typos.toml index 4f4ad0c08..e274b4d9a 100644 --- a/.typos.toml +++ b/.typos.toml @@ -7,6 +7,7 @@ UE = "UE" Ure = "Ure" OFO = "OFO" Ot = "Ot" +ket = "ket" # This is the thead html tag, remove this once typos is updated in the github # action. 1.3.1 seems to work correctly, while 1.11.0 on the CI seems to get # this wrong diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 3f872ede9..aad28d805 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -28,7 +28,7 @@ pub use error::{ use js_int::UInt; pub use logger::{set_logger, Logger}; pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification}; -use matrix_sdk_common::deserialized_responses::VerificationState; +use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState; use matrix_sdk_crypto::{ backups::SignatureState, olm::{IdentityKeys, InboundGroupSession, Session}, @@ -643,6 +643,7 @@ impl From for RustEncryptionSettings { } /// An event that was successfully decrypted. +#[derive(uniffi::Record)] pub struct DecryptedEvent { /// The decrypted version of the event. pub clear_event: String, @@ -654,10 +655,48 @@ pub struct DecryptedEvent { /// key to us. Is empty if the key came directly from the sender of the /// event. pub forwarding_curve25519_chain: Vec, - /// The verification state of the device that sent us the event, note this - /// is the state of the device at the time of decryption. It may change in - /// the future if a device gets verified or deleted. - pub verification_state: VerificationState, + /// The shield state (color and message to display to user) for the event, + /// representing the event's authenticity. Computed from the properties of + /// the sender user identity and their Olm device. + /// + /// Note that this is computed at time of decryption, so the value reflects + /// the computed event authenticity at that time. Authenticity-related + /// properties can change later on, such as when a user identity is + /// subsequently verified or a device is deleted. + pub shield_state: ShieldState, +} + +/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`] +/// for more info. +#[allow(missing_docs)] +#[derive(uniffi::Enum)] +pub enum ShieldColor { + Red, + Grey, + None, +} + +/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`] +/// for more info. +#[derive(uniffi::Record)] +#[allow(missing_docs)] +pub struct ShieldState { + color: ShieldColor, + message: Option, +} + +impl From for ShieldState { + fn from(value: RustShieldState) -> Self { + match value { + RustShieldState::Red { message } => { + Self { color: ShieldColor::Red, message: Some(message.to_owned()) } + } + RustShieldState::Grey { message } => { + Self { color: ShieldColor::Grey, message: Some(message.to_owned()) } + } + RustShieldState::None => Self { color: ShieldColor::None, message: None }, + } + } } /// Struct representing the state of our private cross signing keys, it shows @@ -812,7 +851,7 @@ mod uniffi_types { RequestVerificationResult, StartSasResult, Verification, VerificationRequest, }, BackupKeys, CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, EncryptionSettings, - EventEncryptionAlgorithm, RoomKeyCounts, RoomSettings, + EventEncryptionAlgorithm, RoomKeyCounts, RoomSettings, ShieldColor, ShieldState, }; } diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index bc218f42b..bd9b476ac 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -147,10 +147,7 @@ impl OlmMachine { HashMap::from([("ed25519".to_owned(), ed25519_key), ("curve25519".to_owned(), curve_key)]) } -} -#[uniffi::export] -impl OlmMachine { /// Get the status of the private cross signing keys. /// /// This can be used to check which private cross signing keys we have @@ -753,11 +750,16 @@ impl OlmMachine { /// * `event` - The serialized encrypted version of the event. /// /// * `room_id` - The unique id of the room where the event was sent to. + /// + /// * `strict_shields` - If `true`, messages will be decorated with strict + /// warnings (use `false` to match legacy behaviour where unsafe keys have + /// lower severity warnings and unverified identities are not decorated). pub fn decrypt_room_event( &self, event: String, room_id: String, handle_verification_events: bool, + strict_shields: bool, ) -> Result { // Element Android wants only the content and the type and will create a // decrypted event with those two itself, this struct makes sure we @@ -808,7 +810,11 @@ impl OlmMachine { .get(&DeviceKeyAlgorithm::Ed25519) .cloned(), forwarding_curve25519_chain: vec![], - verification_state: encryption_info.verification_state, + shield_state: if strict_shields { + encryption_info.verification_state.to_shield_state_strict().into() + } else { + encryption_info.verification_state.to_shield_state_lax().into() + }, } } }) diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 9f5e5fc88..a175051e4 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -83,14 +83,6 @@ dictionary KeysImportResult { record>> keys; }; -dictionary DecryptedEvent { - string clear_event; - string sender_curve25519_key; - string? claimed_ed25519_key; - sequence forwarding_curve25519_chain; - VerificationState verification_state; -}; - dictionary Device { string user_id; string device_id; @@ -320,11 +312,6 @@ enum LocalTrust { "Unset", }; -enum VerificationState { - "Trusted", - "Untrusted", - "UnknownDevice", -}; enum EventEncryptionAlgorithm { "OlmV1Curve25519AesSha2", diff --git a/bindings/matrix-sdk-crypto-js/src/encryption.rs b/bindings/matrix-sdk-crypto-js/src/encryption.rs index aa4d1c65a..72cc1e4af 100644 --- a/bindings/matrix-sdk-crypto-js/src/encryption.rs +++ b/bindings/matrix-sdk-crypto-js/src/encryption.rs @@ -2,6 +2,7 @@ use std::time::Duration; +use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState; use wasm_bindgen::prelude::*; use crate::events; @@ -108,28 +109,48 @@ impl From for EncryptionAlgo } } -/// The verification state of the device that sent an event to us. +/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`] +/// for more info. #[wasm_bindgen] -#[derive(Debug)] -pub enum VerificationState { - /// The device is trusted. - Trusted, - - /// The device is not trusted. - Untrusted, - - /// The device is not known to us. - UnknownDevice, +#[derive(Debug, Clone, Copy)] +pub enum ShieldColor { + /// Important warning + Red, + /// Low warning + Grey, + /// No warning + None, } -impl From<&matrix_sdk_common::deserialized_responses::VerificationState> for VerificationState { - fn from(value: &matrix_sdk_common::deserialized_responses::VerificationState) -> Self { - use matrix_sdk_common::deserialized_responses::VerificationState::*; +/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`] +/// for more info. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct ShieldState { + /// The shield color + pub color: ShieldColor, + message: Option, +} +#[wasm_bindgen] +impl ShieldState { + /// Error message that can be displayed as a tooltip + #[wasm_bindgen(getter)] + pub fn message(&self) -> Option { + self.message.clone() + } +} + +impl From for ShieldState { + fn from(value: RustShieldState) -> Self { match value { - Trusted => Self::Trusted, - Untrusted => Self::Untrusted, - UnknownDevice => Self::UnknownDevice, + RustShieldState::Red { message } => { + Self { color: ShieldColor::Red, message: Some(message.to_owned()) } + } + RustShieldState::Grey { message } => { + Self { color: ShieldColor::Grey, message: Some(message.to_owned()) } + } + RustShieldState::None => Self { color: ShieldColor::None, message: None }, } } } diff --git a/bindings/matrix-sdk-crypto-js/src/responses.rs b/bindings/matrix-sdk-crypto-js/src/responses.rs index 24d51cd48..2602a43d4 100644 --- a/bindings/matrix-sdk-crypto-js/src/responses.rs +++ b/bindings/matrix-sdk-crypto-js/src/responses.rs @@ -1,7 +1,5 @@ //! Types related to responses. -use std::borrow::Borrow; - use js_sys::{Array, JsString}; use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo}; use matrix_sdk_crypto::IncomingResponse; @@ -190,9 +188,15 @@ impl DecryptedRoomEvent { /// note this is the state of the device at the time of /// decryption. It may change in the future if a device gets /// verified or deleted. - #[wasm_bindgen(getter, js_name = "verificationState")] - pub fn verification_state(&self) -> Option { - Some((self.encryption_info.as_ref()?.verification_state.borrow()).into()) + #[wasm_bindgen(js_name = "shieldState")] + pub fn shield_state(&self, strict: bool) -> Option { + let state = &self.encryption_info.as_ref()?.verification_state; + + if strict { + Some(state.to_shield_state_strict().into()) + } else { + Some(state.to_shield_state_lax().into()) + } } } diff --git a/bindings/matrix-sdk-crypto-js/tests/encryption.test.js b/bindings/matrix-sdk-crypto-js/tests/encryption.test.js index e564e6d26..2edccdbba 100644 --- a/bindings/matrix-sdk-crypto-js/tests/encryption.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/encryption.test.js @@ -33,11 +33,3 @@ describe(EncryptionSettings.name, () => { }).toThrow(); }); }); - -describe("VerificationState", () => { - test("has the correct variant values", () => { - expect(VerificationState.Trusted).toStrictEqual(0); - expect(VerificationState.Untrusted).toStrictEqual(1); - expect(VerificationState.UnknownDevice).toStrictEqual(2); - }); -}); diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 95c8ba241..32a0ffa0d 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -21,7 +21,7 @@ const { UserId, UserIdentity, VerificationRequest, - VerificationState, + ShieldColor, } = require("../pkg/matrix_sdk_crypto_js"); const { addMachineToMachine } = require("./helper"); require("fake-indexeddb/auto"); @@ -497,7 +497,8 @@ describe(OlmMachine.name, () => { expect(decrypted.senderCurve25519Key).toBeDefined(); expect(decrypted.senderClaimedEd25519Key).toBeDefined(); expect(decrypted.forwardingCurve25519KeyChain).toHaveLength(0); - expect(decrypted.verificationState).toStrictEqual(VerificationState.Trusted); + expect(decrypted.shieldState(true).color).toStrictEqual(ShieldColor.Red); + expect(decrypted.shieldState(false).color).toStrictEqual(ShieldColor.Red); }); }); diff --git a/bindings/matrix-sdk-crypto-nodejs/.gitignore b/bindings/matrix-sdk-crypto-nodejs/.gitignore index f1fbf26ec..4dc1f7592 100644 --- a/bindings/matrix-sdk-crypto-nodejs/.gitignore +++ b/bindings/matrix-sdk-crypto-nodejs/.gitignore @@ -4,4 +4,4 @@ /index.d.ts /matrix-sdk-crypto.*.node /docs/* -*.tgz \ No newline at end of file +*.tgz diff --git a/bindings/matrix-sdk-crypto-nodejs/src/encryption.rs b/bindings/matrix-sdk-crypto-nodejs/src/encryption.rs index 8b30a3bd6..240a21729 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/encryption.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/encryption.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState; use napi::bindgen_prelude::{BigInt, ToNapiValue}; use napi_derive::*; @@ -107,27 +108,33 @@ impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings { } } -/// The verification state of the device that sent an event to us. +/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`] +/// for more info. #[napi] -pub enum VerificationState { - /// The device is trusted. - Trusted, - - /// The device is not trusted. - Untrusted, - - /// The device is not known to us. - UnknownDevice, +pub enum ShieldColor { + Red, + Grey, + None, } -impl From<&matrix_sdk_common::deserialized_responses::VerificationState> for VerificationState { - fn from(value: &matrix_sdk_common::deserialized_responses::VerificationState) -> Self { - use matrix_sdk_common::deserialized_responses::VerificationState::*; +/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`] +/// for more info. +#[napi] +pub struct ShieldState { + pub color: ShieldColor, + pub message: Option<&'static str>, +} +impl From for ShieldState { + fn from(value: RustShieldState) -> Self { match value { - Trusted => Self::Trusted, - Untrusted => Self::Untrusted, - UnknownDevice => Self::UnknownDevice, + RustShieldState::Red { message } => { + ShieldState { color: ShieldColor::Red, message: Some(message) } + } + RustShieldState::Grey { message } => { + ShieldState { color: ShieldColor::Grey, message: Some(message) } + } + RustShieldState::None => ShieldState { color: ShieldColor::None, message: None }, } } } diff --git a/bindings/matrix-sdk-crypto-nodejs/src/responses.rs b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs index d6488f528..14eb94c89 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/responses.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs @@ -1,5 +1,3 @@ -use std::borrow::Borrow; - use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo}; use matrix_sdk_crypto::IncomingResponse; use napi_derive::*; @@ -186,9 +184,14 @@ impl DecryptedRoomEvent { /// note this is the state of the device at the time of /// decryption. It may change in the future if a device gets /// verified or deleted. - #[napi(getter)] - pub fn verification_state(&self) -> Option { - Some(self.encryption_info.as_ref()?.verification_state.borrow().into()) + #[napi] + pub fn shield_state(&self, strict: bool) -> Option { + let state = &self.encryption_info.as_ref()?.verification_state; + if strict { + Some(state.to_shield_state_strict().into()) + } else { + Some(state.to_shield_state_lax().into()) + } } } diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/encryption.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/encryption.test.js index 14a7f15d1..aac973150 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/encryption.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/encryption.test.js @@ -28,11 +28,3 @@ describe(EncryptionSettings.name, () => { }).toThrow(); }); }); - -describe("VerificationState", () => { - test("has the correct variant values", () => { - expect(VerificationState.Trusted).toStrictEqual(0); - expect(VerificationState.Untrusted).toStrictEqual(1); - expect(VerificationState.UnknownDevice).toStrictEqual(2); - }); -}); diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index 74611aaf9..90ed77f19 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -15,6 +15,7 @@ const { CrossSigningStatus, MaybeSignature, StoreType, + ShieldColor, } = require("../"); const path = require("path"); const os = require("os"); @@ -410,7 +411,8 @@ describe(OlmMachine.name, () => { expect(decrypted.senderCurve25519Key).toBeDefined(); expect(decrypted.senderClaimedEd25519Key).toBeDefined(); expect(decrypted.forwardingCurve25519KeyChain).toHaveLength(0); - expect(decrypted.verificationState).toStrictEqual(VerificationState.Trusted); + expect(decrypted.shieldState(true).color).toStrictEqual(ShieldColor.Red); + expect(decrypted.shieldState(false).color).toStrictEqual(ShieldColor.Red); }); }); diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index d8244e20c..e7a91c864 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -8,15 +8,139 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; -/// The verification state of the device that sent an event to us. -#[derive(Clone, Debug, Deserialize, Serialize)] +const AUTHENTICITY_NOT_GUARANTEED: &str = + "The authenticity of this encrypted message can't be guaranteed on this device."; +const UNVERIFIED_IDENTITY: &str = "Encrypted by an unverified user."; +const UNSIGNED_DEVICE: &str = "Encrypted by a device not verified by its owner."; +const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device."; + +/// Represents the state of verification for a decrypted message sent by a +/// device. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub enum VerificationState { - /// The device is trusted. - Trusted, - /// The device is not trusted. - Untrusted, - /// The device is not known to us. - UnknownDevice, + /// This message is guaranteed to be authentic as it is coming from a device + /// belonging to a user that we have verified. + /// + /// This is the only state where authenticity can be guaranteed. + Verified, + + /// The message could not be linked to a verified device. + /// + /// For more detailed information on why the message is considered + /// unverified, refer to the VerificationLevel sub-enum. + Unverified(VerificationLevel), +} + +impl VerificationState { + /// Convert the `VerificationState` into a `ShieldState` which can be + /// directly used to decorate messages in the recommended way. + /// + /// This method decorates messages using a strict ruleset, for a more lax + /// variant of this method take a look at + /// [`VerificationState::to_shield_state_lax()`]. + pub fn to_shield_state_strict(&self) -> ShieldState { + match self { + VerificationState::Verified => ShieldState::None, + VerificationState::Unverified(level) => { + let message = match level { + VerificationLevel::UnverifiedIdentity | VerificationLevel::UnsignedDevice => { + UNVERIFIED_IDENTITY + } + VerificationLevel::None(link) => match link { + DeviceLinkProblem::MissingDevice => UNKNOWN_DEVICE, + DeviceLinkProblem::InsecureSource => AUTHENTICITY_NOT_GUARANTEED, + }, + }; + + ShieldState::Red { message } + } + } + } + + /// Convert the `VerificationState` into a `ShieldState` which can be used + /// to decorate messages in the recommended way. + /// + /// This implements a legacy, lax decoration mode. + /// + /// For a more strict variant of this method take a look at + /// [`VerificationState::to_shield_state_strict()`]. + pub fn to_shield_state_lax(&self) -> ShieldState { + match self { + VerificationState::Verified => ShieldState::None, + VerificationState::Unverified(level) => match level { + VerificationLevel::UnverifiedIdentity => { + // If you didn't show interest in verifying that user we don't + // nag you with an error message. + // TODO: We should detect identity rotation of a previously trusted identity and + // then warn see https://github.com/matrix-org/matrix-rust-sdk/issues/1129 + ShieldState::None + } + VerificationLevel::UnsignedDevice => { + // This is a high warning. The sender hasn't verified his own device. + ShieldState::Red { message: UNSIGNED_DEVICE } + } + VerificationLevel::None(link) => match link { + DeviceLinkProblem::MissingDevice => { + // Have to warn as it could have been a temporary injected device. + // Notice that the device might just not be known at this time, so callers + // should retry when there is a device change for that user. + ShieldState::Red { message: UNKNOWN_DEVICE } + } + DeviceLinkProblem::InsecureSource => { + // In legacy mode, we tone down this warning as it is quite common and + // mostly noise (due to legacy backup and lack of trusted forwards). + ShieldState::Grey { message: AUTHENTICITY_NOT_GUARANTEED } + } + }, + }, + } + } +} + +/// The sub-enum containing detailed information on why a message is considered +/// to be unverified. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum VerificationLevel { + /// The message was sent by a user identity we have not verified. + UnverifiedIdentity, + + /// The message was sent by a device not linked to (signed by) any user + /// identity. + UnsignedDevice, + + /// We weren't able to link the message back to any device. This might be + /// because the message claims to have been sent by a device which we have + /// not been able to obtain (for example, because the device was since + /// deleted) or because the key to decrypt the message was obtained from + /// an insecure source. + None(DeviceLinkProblem), +} + +/// The sub-enum containing detailed information on why we were not able to link +/// a message back to a device. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum DeviceLinkProblem { + /// The device is missing, either because it was deleted, or you haven't + /// yet downoaled it or the server is erroneously omitting it (federation + /// lag). + MissingDevice, + /// The key was obtained from an insecure source: imported from a file, + /// obtained from a legacy (asymmetric) backup, unsafe key forward, etc. + InsecureSource, +} + +/// Recommended decorations for decrypted messages, representing the message's +/// authenticity properties. +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub enum ShieldState { + /// A red shield with a tooltip containing the associated message should be + /// presented. + Red { message: &'static str }, + /// A grey shield with a tooltip containing the associated message should be + /// presented. + Grey { message: &'static str }, + /// No shield should be presented. + None, } /// The algorithm specific information of a decrypted event. @@ -38,16 +162,19 @@ pub enum AlgorithmInfo { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct EncryptionInfo { /// The user ID of the event sender, note this is untrusted data unless the - /// `verification_state` is as well trusted. + /// `verification_state` is `Verified` as well. pub sender: OwnedUserId, /// The device ID of the device that sent us the event, note this is - /// untrusted data unless `verification_state` is as well trusted. + /// untrusted data unless `verification_state` is `Verified` as well. pub sender_device: Option, /// Information about the algorithm that was used to encrypt the event. pub algorithm_info: AlgorithmInfo, /// The verification state of the device that sent us the event, note this /// is the state of the device at the time of decryption. It may change in /// the future if a device gets verified or deleted. + /// + /// Callers that persist this should mark the state as dirty when a device + /// change is received down the sync. pub verification_state: VerificationState, } @@ -126,7 +253,7 @@ mod tests { #[test] fn room_event_to_sync_room_event() { - let event = json! ({ + let event = json!({ "content": RoomMessageEventContent::text_plain("foobar"), "type": "m.room.message", "event_id": "$xxxxx:example.org", diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index f0a093257..7d2ea5c12 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -259,6 +259,40 @@ impl Device { } } + /// Is this device cross signed by its owner? + pub fn is_cross_signed_by_owner(&self) -> bool { + self.device_owner_identity + .as_ref() + .map(|device_identity| match device_identity { + // If it's one of our own devices, just check that + // we signed the device. + ReadOnlyUserIdentities::Own(identity) => { + identity.is_device_signed(&self.inner).is_ok() + } + // If it's a device from someone else, check + // if the other user has signed this device. + ReadOnlyUserIdentities::Other(device_identity) => { + device_identity.is_device_signed(&self.inner).is_ok() + } + }) + .unwrap_or(false) + } + + /// Is the device owner verified by us? + pub fn is_device_owner_verified(&self) -> bool { + self.device_owner_identity + .as_ref() + .map(|id| match id { + ReadOnlyUserIdentities::Own(own_identity) => own_identity.is_verified(), + ReadOnlyUserIdentities::Other(other_identity) => self + .own_identity + .as_ref() + .map(|oi| oi.is_verified() && oi.is_identity_signed(other_identity).is_ok()) + .unwrap_or(false), + }) + .unwrap_or(false) + } + /// Request an interactive verification with this `Device`. /// /// Returns a `VerificationRequest` object and a to-device request that diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 275e1caed..68660fe29 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -20,7 +20,10 @@ use std::{ use dashmap::DashMap; use matrix_sdk_common::{ - deserialized_responses::{AlgorithmInfo, EncryptionInfo, TimelineEvent, VerificationState}, + deserialized_responses::{ + AlgorithmInfo, DeviceLinkProblem, EncryptionInfo, TimelineEvent, VerificationLevel, + VerificationState, + }, locks::Mutex, }; use ruma::{ @@ -595,7 +598,7 @@ impl OlmMachine { } #[instrument( - skip_all, + skip_all, // This function is only ever called by add_room_key via // handle_decrypted_to_device_event, so sender, sender_key, and algorithm are // already recorded. @@ -1113,36 +1116,63 @@ impl OlmMachine { session: &InboundGroupSession, sender: &UserId, ) -> MegolmResult<(VerificationState, Option)> { - Ok( - // First find the device corresponding to the Curve25519 identity - // key that sent us the session (recorded upon successful - // decryption of the `m.room_key` to-device message). - if let Some(device) = self - .get_user_devices(sender, None) - .await? - .devices() - .find(|d| d.curve25519_key() == Some(session.sender_key())) - { - // If the `Device` is confirmed to be the owner of the - // `InboundGroupSession` we will consider the session (i.e. - // "room key"), and by extension any events that are encrypted - // using this session, trusted if either: - // - // a) This is our own device, or - // b) The device itself is considered to be trusted. - if device.is_owner_of_session(session)? - && (device.is_our_own_device() || device.is_verified()) - { - (VerificationState::Trusted, Some(device.device_id().to_owned())) + let claimed_device = self + .get_user_devices(sender, None) + .await? + .devices() + .find(|d| d.curve25519_key() == Some(session.sender_key())); + + Ok(match claimed_device { + None => { + // We didn't find a device, no way to know if we should trust the + // `InboundGroupSession` or not. + + let link_problem = if session.has_been_imported() { + DeviceLinkProblem::InsecureSource } else { - (VerificationState::Untrusted, Some(device.device_id().to_owned())) + DeviceLinkProblem::MissingDevice + }; + + (VerificationState::Unverified(VerificationLevel::None(link_problem)), None) + } + Some(device) => { + let device_id = device.device_id().to_owned(); + + // We found a matching device, let's check if it owns the session. + if !(device.is_owner_of_session(session)?) { + // The key cannot be linked to an owning device. + ( + VerificationState::Unverified(VerificationLevel::None( + DeviceLinkProblem::InsecureSource, + )), + Some(device_id), + ) + } else { + // We only consider cross trust and not local trust. If your own device is not + // signed and send a message, it will be seen as Unverified. + if device.is_cross_signed_by_owner() { + // The device is cross signed by this owner Meaning that the user did self + // verify it properly. Let's check if we trust the identity. + if device.is_device_owner_verified() { + (VerificationState::Verified, Some(device_id)) + } else { + ( + VerificationState::Unverified( + VerificationLevel::UnverifiedIdentity, + ), + Some(device_id), + ) + } + } else { + // The device owner hasn't self-verified its device. + ( + VerificationState::Unverified(VerificationLevel::UnsignedDevice), + Some(device_id), + ) + } } - } else { - // We didn't find a device, no way to know if we should trust - // the `InboundGroupSession` or not. - (VerificationState::UnknownDevice, None) - }, - ) + } + }) } /// Get some metadata pertaining to a given group session. @@ -1638,11 +1668,16 @@ pub(crate) mod tests { use std::{collections::BTreeMap, iter, sync::Arc}; use assert_matches::assert_matches; + use matrix_sdk_common::deserialized_responses::{ + DeviceLinkProblem, ShieldState, VerificationLevel, VerificationState, + }; use matrix_sdk_test::{async_test, test_json}; use ruma::{ api::{ client::{ - keys::{claim_keys, get_keys, upload_keys}, + keys::{ + claim_keys, get_keys, get_keys::v3::Response as KeyQueryResponse, upload_keys, + }, sync::sync_events::DeviceLists, to_device::send_event_to_device::v3::Response as ToDeviceResponse, }, @@ -1665,24 +1700,25 @@ pub(crate) mod tests { use serde_json::json; use vodozemac::{ megolm::{GroupSession, SessionConfig}, - Ed25519PublicKey, + Curve25519PublicKey, Ed25519PublicKey, }; use super::testing::response_from_file; use crate::{ error::EventError, machine::OlmMachine, - olm::VerifyJson, + olm::{InboundGroupSession, OutboundGroupSession, VerifyJson}, types::{ events::{ room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent}, ToDeviceEvent, }, - DeviceKeys, SignedKey, SigningKeys, + CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, SignedKey, SigningKeys, }, utilities::json_convert, verification::tests::{outgoing_request_to_event, request_to_event}, - EncryptionSettings, MegolmError, OlmError, ReadOnlyDevice, ToDeviceRequest, + EncryptionSettings, LocalTrust, MegolmError, OlmError, ReadOnlyDevice, ToDeviceRequest, + UserIdentities, }; /// These keys need to be periodically uploaded to the server. @@ -1696,6 +1732,10 @@ pub(crate) mod tests { device_id!("JLAFKJWSCS") } + fn bob_device_id() -> &'static DeviceId { + device_id!("NTHHPZDPRN") + } + fn user_id() -> &'static UserId { user_id!("@bob:example.com") } @@ -1730,7 +1770,7 @@ pub(crate) mod tests { } pub(crate) async fn get_prepared_machine() -> (OlmMachine, OneTimeKeys) { - let machine = OlmMachine::new(user_id(), alice_device_id()).await; + let machine = OlmMachine::new(user_id(), bob_device_id()).await; machine.account.inner.update_uploaded_key_count(0); let request = machine.keys_for_upload().await.expect("Can't prepare initial key upload"); let response = keys_upload_response(); @@ -2145,6 +2185,399 @@ pub(crate) mod tests { } } + #[async_test] + async fn test_decryption_verification_state() { + macro_rules! assert_shield { + ($foo: ident, $strict: ident, $lax: ident) => { + let lax = $foo.verification_state.to_shield_state_lax(); + let strict = $foo.verification_state.to_shield_state_strict(); + + assert_matches!(lax, ShieldState::$lax { .. }); + assert_matches!(strict, ShieldState::$strict { .. }); + }; + } + let (alice, bob) = get_machine_pair_with_setup_sessions().await; + let room_id = room_id!("!test:example.org"); + + let to_device_requests = alice + .share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default()) + .await + .unwrap(); + + let event = ToDeviceEvent::new( + alice.user_id().to_owned(), + to_device_requests_to_content(to_device_requests), + ); + + let group_session = + bob.decrypt_to_device_event(&event).await.unwrap().inbound_group_session; + + let export = group_session.as_ref().unwrap().clone().export().await; + + bob.store.save_inbound_group_sessions(&[group_session.unwrap()]).await.unwrap(); + + let plaintext = "It is a secret to everybody"; + + let content = RoomMessageEventContent::text_plain(plaintext); + + let encrypted_content = alice + .encrypt_room_event(room_id, AnyMessageLikeEventContent::RoomMessage(content.clone())) + .await + .unwrap(); + + let event = json!({ + "event_id": "$xxxxx:example.org", + "origin_server_ts": MilliSecondsSinceUnixEpoch::now(), + "sender": alice.user_id(), + "type": "m.room.encrypted", + "content": encrypted_content, + }); + + let event = json_convert(&event).unwrap(); + + let encryption_info = + bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + + assert_eq!( + VerificationState::Unverified(VerificationLevel::UnsignedDevice), + encryption_info.verification_state + ); + + assert_shield!(encryption_info, Red, Red); + + // Local trust state has no effect + bob.get_device(alice.user_id(), alice_device_id(), None) + .await + .unwrap() + .unwrap() + .set_trust_state(LocalTrust::Verified); + + let encryption_info = + bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + + assert_eq!( + VerificationState::Unverified(VerificationLevel::UnsignedDevice), + encryption_info.verification_state + ); + assert_shield!(encryption_info, Red, Red); + + setup_cross_signing_for_machine(&alice, &bob).await; + let bob_id_from_alice = alice.get_identity(bob.user_id(), None).await.unwrap(); + assert_matches!(bob_id_from_alice, Some(UserIdentities::Other(_))); + let alice_id_from_bob = bob.get_identity(alice.user_id(), None).await.unwrap(); + assert_matches!(alice_id_from_bob, Some(UserIdentities::Other(_))); + + // we setup cross signing but nothing is signed yet + let encryption_info = + bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + + assert_eq!( + VerificationState::Unverified(VerificationLevel::UnsignedDevice), + encryption_info.verification_state + ); + assert_shield!(encryption_info, Red, Red); + + // Let alice sign her device + sign_alice_device_for_machine(&alice, &bob).await; + + let encryption_info = + bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + + assert_eq!( + VerificationState::Unverified(VerificationLevel::UnverifiedIdentity), + encryption_info.verification_state + ); + + assert_shield!(encryption_info, Red, None); + + mark_alice_identity_as_verified(&alice, &bob).await; + let encryption_info = + bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + assert_eq!(VerificationState::Verified, encryption_info.verification_state); + assert_shield!(encryption_info, None, None); + + // Simulate an imported session, to change verification state + let imported = InboundGroupSession::from_export(&export).unwrap(); + bob.store.save_inbound_group_sessions(&[imported]).await.unwrap(); + + let encryption_info = + bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + + // As soon as the key source is unsafe the verification state (or existence) of + // the device is meaningless + assert_eq!( + VerificationState::Unverified(VerificationLevel::None( + DeviceLinkProblem::InsecureSource + )), + encryption_info.verification_state + ); + + assert_shield!(encryption_info, Red, Grey); + } + + async fn setup_cross_signing_for_machine(alice: &OlmMachine, bob: &OlmMachine) { + let (alice_upload_signing, _) = + alice.bootstrap_cross_signing(false).await.expect("Expect Alice x-signing key request"); + + let (bob_upload_signing, _) = + bob.bootstrap_cross_signing(false).await.expect("Expect Bob x-signing key request"); + + let bob_device_keys = bob + .get_device(bob.user_id(), bob.device_id(), None) + .await + .unwrap() + .unwrap() + .as_device_keys() + .to_owned(); + + let alice_device_keys = alice + .get_device(alice.user_id(), alice.device_id(), None) + .await + .unwrap() + .unwrap() + .as_device_keys() + .to_owned(); + + // We only want to setup cross signing we don't actually sign the current + // devices. so we ignore the new device signatures + let json = json!({ + "device_keys": { + bob.user_id() : { bob.device_id() : bob_device_keys}, + alice.user_id() : { alice.device_id(): alice_device_keys } + }, + "failures": {}, + "master_keys": { + bob.user_id() : bob_upload_signing.master_key.unwrap(), + alice.user_id() : alice_upload_signing.master_key.unwrap() + }, + "user_signing_keys": { + bob.user_id() : bob_upload_signing.user_signing_key.unwrap(), + alice.user_id() : alice_upload_signing.user_signing_key.unwrap() + }, + "self_signing_keys": { + bob.user_id() : bob_upload_signing.self_signing_key.unwrap(), + alice.user_id() : alice_upload_signing.self_signing_key.unwrap() + }, + } + ); + + let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json)) + .expect("Can't parse the keys upload response"); + + alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + } + + async fn sign_alice_device_for_machine(alice: &OlmMachine, bob: &OlmMachine) { + let (upload_signing, upload_signature) = + alice.bootstrap_cross_signing(false).await.expect("Expect Alice x-signing key request"); + + let mut device_keys = alice + .get_device(alice.user_id(), alice.device_id(), None) + .await + .unwrap() + .unwrap() + .as_device_keys() + .to_owned(); + + let raw_extracted = upload_signature + .signed_keys + .get(alice.user_id()) + .unwrap() + .iter() + .next() + .unwrap() + .1 + .get(); + + let new_signature: DeviceKeys = serde_json::from_str(raw_extracted).unwrap(); + + let self_sign_key_id = upload_signing + .self_signing_key + .as_ref() + .unwrap() + .get_first_key_and_id() + .unwrap() + .0 + .to_owned(); + + device_keys.signatures.add_signature( + alice.user_id().to_owned(), + self_sign_key_id.to_owned(), + new_signature.signatures.get_signature(alice.user_id(), &self_sign_key_id).unwrap(), + ); + + let updated_keys_with_x_signing = json!({ device_keys.device_id.to_string(): device_keys }); + + let json = json!({ + "device_keys": { + alice.user_id() : updated_keys_with_x_signing + }, + "failures": {}, + "master_keys": { + alice.user_id() : upload_signing.master_key.unwrap(), + }, + "user_signing_keys": { + alice.user_id() : upload_signing.user_signing_key.unwrap(), + }, + "self_signing_keys": { + alice.user_id() : upload_signing.self_signing_key.unwrap(), + }, + } + ); + + let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json)) + .expect("Can't parse the keys upload response"); + + alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + } + + async fn mark_alice_identity_as_verified(alice: &OlmMachine, bob: &OlmMachine) { + let alice_device = + bob.get_device(alice.user_id(), alice.device_id(), None).await.unwrap().unwrap(); + + let alice_identity = + bob.get_identity(alice.user_id(), None).await.unwrap().unwrap().other().unwrap(); + let upload_request = alice_identity.verify().await.unwrap(); + + let raw_extracted = + upload_request.signed_keys.get(alice.user_id()).unwrap().iter().next().unwrap().1.get(); + + let new_signature: CrossSigningKey = serde_json::from_str(raw_extracted).unwrap(); + + let user_key_id = bob + .bootstrap_cross_signing(false) + .await + .expect("Expect Alice x-signing key request") + .0 + .user_signing_key + .unwrap() + .get_first_key_and_id() + .unwrap() + .0 + .to_owned(); + + // add the new signature to alice msk + let mut alice_updated_msk = + alice_device.device_owner_identity.as_ref().unwrap().master_key().as_ref().to_owned(); + + alice_updated_msk.signatures.add_signature( + bob.user_id().to_owned(), + user_key_id.to_owned(), + new_signature.signatures.get_signature(bob.user_id(), &user_key_id).unwrap(), + ); + + let alice_x_keys = alice + .bootstrap_cross_signing(false) + .await + .expect("Expect Alice x-signing key request") + .0; + + let json = json!({ + "device_keys": { + alice.user_id() : { alice.device_id(): alice_device.as_device_keys().to_owned() } + }, + "failures": {}, + "master_keys": { + alice.user_id() : alice_updated_msk, + }, + "user_signing_keys": { + alice.user_id() : alice_x_keys.user_signing_key.unwrap(), + }, + "self_signing_keys": { + alice.user_id() : alice_x_keys.self_signing_key.unwrap(), + }, + } + ); + + let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json)) + .expect("Can't parse the keys upload response"); + + alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + + // so alice identity should be now trusted + + assert!(bob + .get_identity(alice.user_id(), None) + .await + .unwrap() + .unwrap() + .other() + .unwrap() + .is_verified()); + } + + #[async_test] + async fn test_verication_states_multiple_device() { + let (bob, _) = get_prepared_machine().await; + + let other_user_id = user_id!("@web2:localhost:8482"); + + let data = response_from_file(&test_json::KEYS_QUERY_TWO_DEVICES_ONE_SIGNED); + let response = get_keys::v3::Response::try_from_http_response(data) + .expect("Can't parse the keys upload response"); + + let (device_change, identity_change) = + bob.receive_keys_query_response(&TransactionId::new(), &response).await.unwrap(); + assert_eq!(device_change.new.len(), 2); + assert_eq!(identity_change.new.len(), 1); + // + let devices = bob.store.get_user_devices(other_user_id).await.unwrap(); + assert_eq!(devices.devices().count(), 2); + + let fake_room_id = room_id!("!roomid:example.com"); + + // We just need a fake session to export it + // We will use the export to create various inbounds with other claimed + // ownership + let id_keys = bob.identity_keys(); + let fake_device_id = bob.device_id.clone(); + let olm = OutboundGroupSession::new( + fake_device_id, + Arc::new(id_keys), + fake_room_id, + EncryptionSettings::default(), + ) + .unwrap() + .session_key() + .await; + + let web_unverified_inbound_session = InboundGroupSession::new( + Curve25519PublicKey::from_base64("LTpv2DGMhggPAXO02+7f68CNEp6A40F0Yl8B094Y8gc") + .unwrap(), + Ed25519PublicKey::from_base64("loz5i40dP+azDtWvsD0L/xpnCjNkmrcvtXVXzCHX8Vw").unwrap(), + fake_room_id, + &olm, + EventEncryptionAlgorithm::MegolmV1AesSha2, + None, + ) + .unwrap(); + + let (state, _) = bob + .get_verification_state(&web_unverified_inbound_session, other_user_id) + .await + .unwrap(); + assert_eq!(VerificationState::Unverified(VerificationLevel::UnsignedDevice), state); + + let web_signed_inbound_session = InboundGroupSession::new( + Curve25519PublicKey::from_base64("XJixbpnfIk+RqcK5T6moqVY9d9Q1veR8WjjSlNiQNT0") + .unwrap(), + Ed25519PublicKey::from_base64("48f3WQAMGwYLBg5M5qUhqnEVA8yeibjZpPsShoWMFT8").unwrap(), + fake_room_id, + &olm, + EventEncryptionAlgorithm::MegolmV1AesSha2, + None, + ) + .unwrap(); + + let (state, _) = + bob.get_verification_state(&web_signed_inbound_session, other_user_id).await.unwrap(); + + assert_eq!(VerificationState::Unverified(VerificationLevel::UnverifiedIdentity), state); + } + #[async_test] #[cfg(feature = "automatic-room-key-forwarding")] async fn test_query_ratcheted_key() { diff --git a/testing/matrix-sdk-test/src/test_json/api_responses.rs b/testing/matrix-sdk-test/src/test_json/api_responses.rs index 3405e8038..d53469771 100644 --- a/testing/matrix-sdk-test/src/test_json/api_responses.rs +++ b/testing/matrix-sdk-test/src/test_json/api_responses.rs @@ -68,6 +68,91 @@ pub static KEYS_QUERY: Lazy = Lazy::new(|| { }) }); +/// `POST /_matrix/client/v3/keys/query` +/// For a set of 2 devices own by a user named web2. +/// First device is unsigned, second one is signed +pub static KEYS_QUERY_TWO_DEVICES_ONE_SIGNED: Lazy = Lazy::new(|| { + json!({ + "device_keys":{ + "@web2:localhost:8482":{ + "AVXFQWJUQA":{ + "algorithms":[ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id":"AVXFQWJUQA", + "keys":{ + "curve25519:AVXFQWJUQA":"LTpv2DGMhggPAXO02+7f68CNEp6A40F0Yl8B094Y8gc", + "ed25519:AVXFQWJUQA":"loz5i40dP+azDtWvsD0L/xpnCjNkmrcvtXVXzCHX8Vw" + }, + "signatures":{ + "@web2:localhost:8482":{ + "ed25519:AVXFQWJUQA":"BmdzjXMwZaZ0ZK8T6h3pkTA+gZbD34Bzf8FNazBdAIE16fxVzrlSJkLfXnjdBqRO0Dlda5vKgGpqJazZP6obDw" + } + }, + "user_id":"@web2:localhost:8482" + }, + "JERTCKWUWG":{ + "algorithms":[ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id":"JERTCKWUWG", + "keys":{ + "curve25519:JERTCKWUWG":"XJixbpnfIk+RqcK5T6moqVY9d9Q1veR8WjjSlNiQNT0", + "ed25519:JERTCKWUWG":"48f3WQAMGwYLBg5M5qUhqnEVA8yeibjZpPsShoWMFT8" + }, + "signatures":{ + "@web2:localhost:8482":{ + "ed25519:JERTCKWUWG":"Wc67XYem4IKCpshcslQ6ketCE5otubpX+Bh01OB8ghLxl1d6exlZsgaRA57N8RJ0EMvbeTWCweHXXC/UeeQ4DQ", + "ed25519:uXOM0Xlfts9SGysk/yNr0Vn9rgv1Ifh3R8oPhtic4BM":"dto9VPhhJbNw62j8NQyjnwukMd1NtYnDYSoUOzD5dABq1u2Kt/ZdthcTO42HyxG/3/hZdno8XPfJ47l1ZxuXBA" + } + }, + "user_id":"@web2:localhost:8482" + } + } + }, + "failures":{ + + }, + "master_keys":{ + "@web2:localhost:8482":{ + "user_id":"@web2:localhost:8482", + "usage":[ + "master" + ], + "keys":{ + "ed25519:Ct4QR+aXrzW4iYIgH1B/56NkPEtSPoN+h2TGoQ0xxYI":"Ct4QR+aXrzW4iYIgH1B/56NkPEtSPoN+h2TGoQ0xxYI" + }, + "signatures":{ + "@web2:localhost:8482":{ + "ed25519:JERTCKWUWG":"H9hEsUJ+alB5XAboDzU4loVb+SZajC4tsQzGaeU/FHMFAnWeVarTMCR+NmPSGsZfvPrNz2WVS2G7FIH5yhJfBg" + } + } + } + }, + "self_signing_keys":{ + "@web2:localhost:8482":{ + "user_id":"@web2:localhost:8482", + "usage":[ + "self_signing" + ], + "keys":{ + "ed25519:uXOM0Xlfts9SGysk/yNr0Vn9rgv1Ifh3R8oPhtic4BM":"uXOM0Xlfts9SGysk/yNr0Vn9rgv1Ifh3R8oPhtic4BM" + }, + "signatures":{ + "@web2:localhost:8482":{ + "ed25519:Ct4QR+aXrzW4iYIgH1B/56NkPEtSPoN+h2TGoQ0xxYI":"YbD6gTEwY078nllTxmlyea2VNvAElQ/ig7aPsyhA3h1gGwFvPdtyDbomjdIphUF/lXQ+Eyz4SzlUWeghr1b3BA" + } + } + } + }, + "user_signing_keys":{ + + } + }) +}); + /// `` pub static KEYS_UPLOAD: Lazy = Lazy::new(|| { json!({ diff --git a/testing/matrix-sdk-test/src/test_json/mod.rs b/testing/matrix-sdk-test/src/test_json/mod.rs index b7472d36a..90c56413f 100644 --- a/testing/matrix-sdk-test/src/test_json/mod.rs +++ b/testing/matrix-sdk-test/src/test_json/mod.rs @@ -14,10 +14,10 @@ pub mod sync; pub mod sync_events; pub use api_responses::{ - DEVICES, GET_ALIAS, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGIN_TYPES, - LOGIN_WITH_DISCOVERY, LOGIN_WITH_REFRESH_TOKEN, NOT_FOUND, PUBLIC_ROOMS, REFRESH_TOKEN, - REFRESH_TOKEN_WITH_REFRESH_TOKEN, REGISTRATION_RESPONSE_ERR, UNKNOWN_TOKEN_SOFT_LOGOUT, - VERSIONS, WELL_KNOWN, WHOAMI, + DEVICES, GET_ALIAS, KEYS_QUERY, KEYS_QUERY_TWO_DEVICES_ONE_SIGNED, KEYS_UPLOAD, LOGIN, + LOGIN_RESPONSE_ERR, LOGIN_TYPES, LOGIN_WITH_DISCOVERY, LOGIN_WITH_REFRESH_TOKEN, NOT_FOUND, + PUBLIC_ROOMS, REFRESH_TOKEN, REFRESH_TOKEN_WITH_REFRESH_TOKEN, REGISTRATION_RESPONSE_ERR, + UNKNOWN_TOKEN_SOFT_LOGOUT, VERSIONS, WELL_KNOWN, WHOAMI, }; pub use members::MEMBERS; pub use messages::{ROOM_MESSAGES, ROOM_MESSAGES_BATCH_1, ROOM_MESSAGES_BATCH_2};