Extended verification states
Co-authored-by: Damir Jelić <poljar@termina.org.uk> Co-authored-by: Denis Kasak <dkasak@termina.org.uk>pull/1671/head
parent
1e1e0d7e6d
commit
248b8db309
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
/index.d.ts
|
||||
/matrix-sdk-crypto.*.node
|
||||
/docs/*
|
||||
*.tgz
|
||||
*.tgz
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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!({
|
||||
|
|
|
@ -14,10 +14,10 @@ pub mod sync;
|
|||
pub mod sync_events;
|