Extended verification states

Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
This commit is contained in:
Valere 2023-03-15 18:16:31 +01:00 committed by GitHub
parent 1e1e0d7e6d
commit 248b8db309
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 871 additions and 137 deletions

View File

@ -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

View File

@ -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<EncryptionSettings> 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<String>,
/// 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<String>,
}
impl From<RustShieldState> 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,
};
}

View File

@ -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<DecryptedEvent, DecryptionError> {
// 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()
},
}
}
})

View File

@ -83,14 +83,6 @@ dictionary KeysImportResult {
record<DOMString, record<DOMString, sequence<string>>> keys;
};
dictionary DecryptedEvent {
string clear_event;
string sender_curve25519_key;
string? claimed_ed25519_key;
sequence<string> 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",

View File

@ -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<matrix_sdk_crypto::types::EventEncryptionAlgorithm> 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<String>,
}
#[wasm_bindgen]
impl ShieldState {
/// Error message that can be displayed as a tooltip
#[wasm_bindgen(getter)]
pub fn message(&self) -> Option<String> {
self.message.clone()
}
}
impl From<RustShieldState> 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 },
}
}
}

View File

@ -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<encryption::VerificationState> {
Some((self.encryption_info.as_ref()?.verification_state.borrow()).into())
#[wasm_bindgen(js_name = "shieldState")]
pub fn shield_state(&self, strict: bool) -> Option<encryption::ShieldState> {
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())
}
}
}

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -4,4 +4,4 @@
/index.d.ts
/matrix-sdk-crypto.*.node
/docs/*
*.tgz
*.tgz

View File

@ -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<RustShieldState> 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 },
}
}
}

View File

@ -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<encryption::VerificationState> {
Some(self.encryption_info.as_ref()?.verification_state.borrow().into())
#[napi]
pub fn shield_state(&self, strict: bool) -> Option<encryption::ShieldState> {
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())
}
}
}

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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<OwnedDeviceId>,
/// 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",

View File

@ -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

View File

@ -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<OwnedDeviceId>)> {
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() {

View File

@ -68,6 +68,91 @@ pub static KEYS_QUERY: Lazy<JsonValue> = 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<JsonValue> = 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<JsonValue> = Lazy::new(|| {
json!({

View File

@ -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};