From 9155989060e6456754dd32a4ed5d79971fb87b52 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 29 Aug 2022 14:54:03 +0200 Subject: [PATCH 01/37] feat(crypto): Simplify code and add documentation. --- .../matrix-sdk-crypto/src/verification/mod.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 0412dbbae..6eb7b01c5 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -417,44 +417,52 @@ impl Cancelled { } } +/// A key verification can be requested and started by a to-device +/// request or a room event. `FlowId` helps to represent both +/// usecases. #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd)] pub enum FlowId { + /// The flow ID comes from a to-device request. ToDevice(OwnedTransactionId), + + /// The flow ID comes from a room event. InRoom(OwnedRoomId, OwnedEventId), } impl FlowId { + /// Get the room ID if the flow ID comes from a room event. pub fn room_id(&self) -> Option<&RoomId> { - if let FlowId::InRoom(room_id, _) = &self { + if let Self::InRoom(room_id, _) = &self { Some(room_id) } else { None } } + /// Get the ID a string. pub fn as_str(&self) -> &str { match self { - FlowId::InRoom(_, event_id) => event_id.as_str(), - FlowId::ToDevice(transaction_id) => transaction_id.as_str(), + Self::InRoom(_, event_id) => event_id.as_str(), + Self::ToDevice(transaction_id) => transaction_id.as_str(), } } } impl From for FlowId { fn from(transaction_id: OwnedTransactionId) -> Self { - FlowId::ToDevice(transaction_id) + Self::ToDevice(transaction_id) } } impl From<(OwnedRoomId, OwnedEventId)> for FlowId { fn from(ids: (OwnedRoomId, OwnedEventId)) -> Self { - FlowId::InRoom(ids.0, ids.1) + Self::InRoom(ids.0, ids.1) } } impl From<(&RoomId, &EventId)> for FlowId { fn from(ids: (&RoomId, &EventId)) -> Self { - FlowId::InRoom(ids.0.to_owned(), ids.1.to_owned()) + Self::InRoom(ids.0.to_owned(), ids.1.to_owned()) } } From 53e21e0c26516f2087bd3c0c0ce3c02d8786bc25 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 29 Aug 2022 14:54:33 +0200 Subject: [PATCH 02/37] feat(crypto-js): Start implementation key verification API. --- Cargo.lock | 1 + bindings/matrix-sdk-crypto-js/Cargo.toml | 3 +- bindings/matrix-sdk-crypto-js/src/lib.rs | 1 + bindings/matrix-sdk-crypto-js/src/machine.rs | 63 +- .../matrix-sdk-crypto-js/src/verification.rs | 899 ++++++++++++++++++ 5 files changed, 965 insertions(+), 2 deletions(-) create mode 100644 bindings/matrix-sdk-crypto-js/src/verification.rs diff --git a/Cargo.lock b/Cargo.lock index 7800accee..84122cfc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2303,6 +2303,7 @@ dependencies = [ "matrix-sdk-common", "matrix-sdk-crypto", "matrix-sdk-indexeddb", + "matrix-sdk-qrcode", "ruma", "serde_json", "tracing", diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 447ff6cb2..c0f32df9a 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -22,13 +22,14 @@ crate-type = ["cdylib"] [features] default = ["tracing"] -qrcode = ["matrix-sdk-crypto/qrcode"] +qrcode = ["matrix-sdk-crypto/qrcode", "matrix-sdk-qrcode"] tracing = [] [dependencies] matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" } matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" } matrix-sdk-indexeddb = { version = "0.1.0", path = "../../crates/matrix-sdk-indexeddb" } +matrix-sdk-qrcode = { version = "0.3.0", path = "../../crates/matrix-sdk-qrcode", optional = true } ruma = { version = "0.7.0", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" diff --git a/bindings/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs index f9c43183e..3d826e979 100644 --- a/bindings/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -30,6 +30,7 @@ pub mod sync_events; mod tracing; pub mod types; pub mod vodozemac; +pub mod verification; use js_sys::{Object, Reflect}; use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*}; diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index cd7d29e7a..fdb6abd47 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -13,7 +13,7 @@ use crate::{ identifiers, olm, requests, requests::OutgoingRequest, responses::{self, response_from_string}, - sync_events, types, vodozemac, + sync_events, types, verification, vodozemac, }; /// State machine implementation of the Olm/Megolm encryption protocol @@ -453,4 +453,65 @@ impl OlmMachine { } })) } + /// Get a verification object for the given user ID with the given + /// flow ID (a to-device request ID if the verification has been + /// requested by a to-device request, or a room event ID if the + /// verification has been requested by a room event). + /// + /// It returns a `Verification` object. + #[wasm_bindgen(js_name = "getVerification")] + pub fn get_verification( + &self, + user_id: &identifiers::UserId, + flow_id: &str, + ) -> Result { + self.inner + .get_verification(&user_id.inner, flow_id) + .map(verification::Verification) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + } + + /// Get a verification request object with the given flow ID. + #[wasm_bindgen(js_name = "getVerificationRequest")] + pub fn get_verification_request( + &self, + user_id: &identifiers::UserId, + flow_id: &str, + ) -> Option { + self.inner.get_verification_request(&user_id.inner, flow_id).map(Into::into) + } + + /// Get all the verification requests of a given user. + #[wasm_bindgen(js_name = "getVerificationRequests")] + pub fn get_verification_requests(&self, user_id: &identifiers::UserId) -> Array { + self.inner + .get_verification_requests(&user_id.inner) + .into_iter() + .map(verification::VerificationRequest::from) + .map(JsValue::from) + .collect() + } + + /// Receive an unencrypted verification event. + /// + /// This method can be used to pass verification events that are + /// happening in unencrypted rooms to the `OlmMachine`. + /// + /// Note: This does not need to be called for encrypted events + /// since those will get passed to the `OlmMachine` during + /// decryption. + #[wasm_bindgen(js_name = "receiveUnencryptedVerificationEvent")] + pub fn receive_unencrypted_verification_event(&self, event: &str) -> Result { + let event: ruma::events::AnyMessageLikeEvent = serde_json::from_str(event)?; + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + Ok(me + .receive_unencrypted_verification_event(&event) + .await + .map(|_| JsValue::UNDEFINED)?) + })) + } } diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs new file mode 100644 index 000000000..f261fa98c --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -0,0 +1,899 @@ +//! Different verification types. + +use js_sys::{Array, JsString}; +use ruma::events::key::verification::{ + cancel::CancelCode as RumaCancelCode, VerificationMethod as RumaVerificationMethod, +}; +use wasm_bindgen::prelude::*; + +use crate::{ + identifiers::{DeviceId, RoomId, UserId}, + requests, +}; + +/// List of available verification methods. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub enum VerificationMethod { + /// The `m.sas.v1` verification method. + /// + /// SAS means Short Authentication String. + SasV1 = 0, + + /// The `m.qr_code.scan.v1` verification method. + QrCodeScanV1 = 1, + + /// The `m.qr_code.show.v1` verification method. + QrCodeShowV1 = 2, + + /// The `m.reciprocate.v1` verification method. + ReciprocateV1 = 3, +} + +impl From for JsValue { + fn from(value: VerificationMethod) -> Self { + use VerificationMethod::*; + + match value { + SasV1 => JsValue::from(0), + QrCodeScanV1 => JsValue::from(1), + QrCodeShowV1 => JsValue::from(2), + ReciprocateV1 => JsValue::from(3), + } + } +} + +impl TryFrom for VerificationMethod { + type Error = JsError; + + fn try_from(value: JsValue) -> Result { + let value = value.as_f64().ok_or_else(|| { + JsError::new(&format!("Expect a `number`, received a `{:?}`", value.js_typeof())) + })? as u32; + + Ok(match value { + 0 => Self::SasV1, + 1 => Self::QrCodeScanV1, + 2 => Self::QrCodeShowV1, + 3 => Self::ReciprocateV1, + _ => { + return Err(JsError::new(&format!( + "Unknown verification method (received `{:?}`)", + value + ))) + } + }) + } +} + +impl From for RumaVerificationMethod { + fn from(value: VerificationMethod) -> Self { + use VerificationMethod::*; + + match value { + SasV1 => Self::SasV1, + QrCodeScanV1 => Self::QrCodeScanV1, + QrCodeShowV1 => Self::QrCodeShowV1, + ReciprocateV1 => Self::ReciprocateV1, + } + } +} + +impl TryFrom for VerificationMethod { + type Error = JsError; + + fn try_from(value: RumaVerificationMethod) -> Result { + use RumaVerificationMethod::*; + + Ok(match value { + SasV1 => Self::SasV1, + QrCodeScanV1 => Self::QrCodeScanV1, + QrCodeShowV1 => Self::QrCodeShowV1, + ReciprocateV1 => Self::ReciprocateV1, + _ => { + return Err(JsError::new(&format!( + "Unknown verification method (received `{:?}`)", + value + ))) + } + }) + } +} + +pub(crate) struct Verification(pub(crate) matrix_sdk_crypto::Verification); + +impl TryFrom for JsValue { + type Error = JsError; + + fn try_from(verification: Verification) -> Result { + use matrix_sdk_crypto::Verification::*; + + Ok(match verification.0 { + SasV1(sas) => JsValue::from(Sas { inner: sas }), + + #[cfg(feature = "qrcode")] + QrV1(qr) => JsValue::from(Qr { inner: qr }), + + _ => { + return Err(JsError::new( + "Unknown verification type, expect `m.sas.v1` only for now", + )) + } + }) + } +} + +/// Short Authentication String (SAS) verification. +#[wasm_bindgen] +#[derive(Debug)] +pub struct Sas { + inner: matrix_sdk_crypto::Sas, +} + +#[wasm_bindgen] +impl Sas { + /// Get our own user ID. + #[wasm_bindgen(getter, js_name = "userId")] + pub fn user_id(&self) -> UserId { + self.inner.user_id().to_owned().into() + } + + /// Get our own device ID. + #[wasm_bindgen(getter, js_name = "deviceId")] + pub fn device_id(&self) -> DeviceId { + self.inner.device_id().to_owned().into() + } + + /// Get the user id of the other side. + #[wasm_bindgen(getter, js_name = "otherUserId")] + pub fn other_user_id(&self) -> UserId { + self.inner.other_user_id().to_owned().into() + } + + /// Get the device ID of the other side. + #[wasm_bindgen(getter, js_name = "otherDeviceId")] + pub fn other_device_id(&self) -> DeviceId { + self.inner.other_device_id().to_owned().into() + } + + /* + /// Get the device of the other user. + #[wasm_bindgen(js_name = "otherDevice")] + pub fn other_device(&self) { + todo!() + } + */ + + /// Get the unique ID that identifies this SAS verification flow, + /// be either a to-device request ID or a room event ID. + #[wasm_bindgen(getter, js_name = "flowId")] + pub fn flow_id(&self) -> String { + self.inner.flow_id().as_str().to_owned() + } + + /// Get the room ID if the verification is happening inside a + /// room. + #[wasm_bindgen(getter, js_name = "roomId")] + pub fn room_id(&self) -> Option { + self.inner.room_id().map(ToOwned::to_owned).map(Into::into) + } + + /// Does this verification flow support displaying emoji for the + /// short authentication string. + #[wasm_bindgen(js_name = "supportsEmoji")] + pub fn supports_emoji(&self) -> bool { + self.inner.supports_emoji() + } + + /// Did this verification flow start from a verification request. + #[wasm_bindgen(js_name = "startedFromRequest")] + pub fn started_from_request(&self) -> bool { + self.inner.started_from_request() + } + + /// Is this a verification that is veryfying one of our own + /// devices. + #[wasm_bindgen(js_name = "isSelfVerification")] + pub fn is_self_verification(&self) -> bool { + self.inner.is_self_verification() + } + + /// Have we confirmed that the short auth string matches. + #[wasm_bindgen(js_name = "haveWeConfirmed")] + pub fn have_we_confirmed(&self) -> bool { + self.inner.have_we_confirmed() + } + + /// Has the verification been accepted by both parties. + #[wasm_bindgen(js_name = "hasBeenAccepted")] + pub fn has_been_accepted(&self) -> bool { + self.inner.has_been_accepted() + } + + /// Get info about the cancellation if the verification flow has + /// been cancelled. + #[wasm_bindgen(js_name = "cancelInfo")] + pub fn cancel_info(&self) -> Option { + self.inner.cancel_info().map(Into::into) + } + + /// Did we initiate the verification flow. + #[wasm_bindgen(js_name = "weStarted")] + pub fn we_started(&self) -> bool { + self.inner.we_started() + } + + /* + pub fn accept(&self) { + todo!() + } + */ + + /* + #[wasm_bindgen(js_name = "acceptWithSettings")] + pub fn accept_with_settings(&self) { + todo!() + } + */ + + /* + pub fn confirm(&self) { + todo!() + } + */ + + /* + pub fn cancel(&self) { + todo!() + } + */ + + /* + #[wasm_bindgen(js_name = "cancelWithCode")] + pub fn cancel_with_code(&self) { + todo!() + } + */ + + /// Has the SAS verification flow timed out. + #[wasm_bindgen(js_name = "timedOut")] + pub fn timed_out(&self) -> bool { + self.inner.timed_out() + } + + /// Are we in a state where we can show the short auth string. + #[wasm_bindgen(js_name = "canBePresented")] + pub fn can_be_presented(&self) -> bool { + self.inner.can_be_presented() + } + + /// Is the SAS flow done. + #[wasm_bindgen(js_name = "isDone")] + pub fn is_done(&self) -> bool { + self.inner.is_done() + } + + /// Is the SAS flow canceled. + #[wasm_bindgen(js_name = "isCancelled")] + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } + + /// Get the emoji version of the short auth string. + /// + /// Returns `undefined` if we can't yet present the short auth string, + /// otherwise seven tuples containing the emoji and description. + pub fn emoji(&self) -> Option { + Some( + self.inner + .emoji()? + .iter() + .map(|emoji| Emoji::from(emoji.to_owned())) + .map(JsValue::from) + .collect(), + ) + } + + /// Get the index of the emoji representing the short auth string + /// + /// Returns `undefined` if we can’t yet present the short auth + /// string, otherwise seven `u8` numbers in the range from 0 to 63 + /// inclusive which can be converted to an emoji using [the + /// relevant specification + /// entry](https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji). + #[wasm_bindgen(js_name = "emoji_index")] + pub fn emoji_index(&self) -> Option { + Some(self.inner.emoji_index()?.into_iter().map(JsValue::from).collect()) + } + + /// Get the decimal version of the short auth string. + /// + /// Returns None if we can’t yet present the short auth string, + /// otherwise a tuple containing three 4-digit integers that + /// represent the short auth string. + pub fn decimals(&self) -> Option { + let decimals = self.inner.decimals()?; + + let out = Array::new_with_length(3); + out.set(0, JsValue::from(decimals.0)); + out.set(1, JsValue::from(decimals.1)); + out.set(2, JsValue::from(decimals.2)); + + Some(out) + } +} + +/// QR code based verification. +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +#[derive(Debug)] +pub struct Qr { + inner: matrix_sdk_crypto::QrVerification, +} + +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +impl Qr { + /// Has the QR verification been scanned by the other side. + /// + /// When the verification object is in this state it’s required + /// that the user confirms that the other side has scanned the QR + /// code. + #[wasm_bindgen(js_name = "hasBeenScanned")] + pub fn has_been_scanned(&self) -> bool { + self.inner.has_been_scanned() + } + + /// Has the scanning of the QR code been confirmed by us. + #[wasm_bindgen(js_name = "hasBeenConfirmed")] + pub fn has_been_confirmed(&self) -> bool { + self.inner.has_been_confirmed() + } + + /// Get our own user ID. + #[wasm_bindgen(getter, js_name = "userId")] + pub fn user_id(&self) -> UserId { + self.inner.user_id().to_owned().into() + } + + /// Get the user id of the other user that is participating in + /// this verification flow. + #[wasm_bindgen(getter, js_name = "otherUserId")] + pub fn other_user_id(&self) -> UserId { + self.inner.other_user_id().to_owned().into() + } + + /// Get the device ID of the other side. + #[wasm_bindgen(getter, js_name = "otherDeviceId")] + pub fn other_device_id(&self) -> DeviceId { + self.inner.other_device_id().to_owned().into() + } + + /* + #[wasm_bindgen(getter, js_name = "otherDevice")] + pub fn other_device(&self) -> ReadOnlyDevice {} + */ + + /// Did we initiate the verification request. + #[wasm_bindgen(js_name = "weStarted")] + pub fn we_started(&self) -> bool { + self.inner.we_started() + } + + /// Get info about the cancellation if the verification flow has + /// been cancelled. + #[wasm_bindgen(js_name = "cancelInfo")] + pub fn cancel_info(&self) -> Option { + self.inner.cancel_info().map(Into::into) + } + + /// Has the verification flow completed. + #[wasm_bindgen(js_name = "isDone")] + pub fn is_done(&self) -> bool { + self.inner.is_done() + } + + /// Has the verification flow been cancelled. + #[wasm_bindgen(js_name = "isCancelled")] + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } + + /// Is this a verification that is veryfying one of our own devices. + #[wasm_bindgen(js_name = "isSelfVerification")] + pub fn is_self_verification(&self) -> bool { + self.inner.is_self_verification() + } + + /// Have we successfully scanned the QR code and are able to send + /// a reciprocation event. + pub fn reciprocated(&self) -> bool { + self.inner.reciprocated() + } + + /// Get the unique ID that identifies this QR verification flow, + /// be either a to-device request ID or a room event ID. + #[wasm_bindgen(getter, js_name = "flowId")] + pub fn flow_id(&self) -> String { + self.inner.flow_id().as_str().to_owned() + } + + /// Get the room id if the verification is happening inside a + /// room. + #[wasm_bindgen(getter, js_name = "roomId")] + pub fn room_id(&self) -> Option { + self.inner.room_id().map(ToOwned::to_owned).map(Into::into) + } + + /// Generate a QR code object that is representing this + /// verification flow. + /// + /// The QrCode can then be rendered as an image or as an unicode + /// string. + /// + /// The `to_bytes` method can be used to instead output the raw + /// bytes that should be encoded as a QR code. + #[wasm_bindgen(js_name = "toQrCode")] + pub fn to_qr_code(&self) -> Result { + Ok(self.inner.to_qr_code().map(Into::into)?) + } + + /// Generate a the raw bytes that should be encoded as a QR code + /// is representing this verification flow. + /// + /// The `to_qr_code` method can be used to instead output a QrCode + /// object that can be rendered. + #[wasm_bindgen(js_name = "toBytes")] + pub fn to_bytes(&self) -> Result { + Ok(self.inner.to_bytes()?.into_iter().map(JsValue::from).collect()) + } + + /* + /// Cancel the verification flow. + pub fn cancel(&self) -> … {} + pub fn cancel_with_code(&self, code: …) -> … {} + pub fn reciprocate(&self) -> … {} + pub fn confirm_scanning(&self) -> … {} + */ +} + +/// Information about the cancellation of a verification request or +/// verification flow. +#[wasm_bindgen] +#[derive(Debug)] +pub struct CancelInfo { + inner: matrix_sdk_crypto::CancelInfo, +} + +impl From for CancelInfo { + fn from(inner: matrix_sdk_crypto::CancelInfo) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl CancelInfo { + /// Get the human readable reason of the cancellation. + pub fn reason(&self) -> JsString { + self.inner.reason().into() + } + + /// Get the `CancelCode` that cancelled this verification. + #[wasm_bindgen(js_name = "cancelCode")] + pub fn cancel_code(&self) -> CancelCode { + self.inner.cancel_code().into() + } + + /// Was the verification cancelled by us? + #[wasm_bindgen(js_name = "cancelledbyUs")] + pub fn cancelled_by_us(&self) -> bool { + self.inner.cancelled_by_us() + } +} + +/// An error code for why the process/request was cancelled by the +/// user. +#[wasm_bindgen] +#[derive(Debug)] +pub enum CancelCode { + /// Unknown cancel code. + Other, + + /// The user cancelled the verification. + User, + + /// The verification process timed out. + /// + /// Verification processes can define their own timeout + /// parameters. + Timeout, + + /// The device does not know about the given transaction ID. + UnknownTransaction, + + /// The device does not know how to handle the requested method. + /// + /// Should be sent for `m.key.verification.start` messages and + /// messages defined by individual verification processes. + UnknownMethod, + + /// The device received an unexpected message. + /// + /// Typically raised when one of the parties is handling the + /// verification out of order. + UnexpectedMessage, + + /// The key was not verified. + KeyMismatch, + + /// The expected user did not match the user verified. + UserMismatch, + + /// The message received was invalid. + InvalidMessage, + + /// An `m.key.verification.request` was accepted by a different + /// device. + /// + /// The device receiving this error can ignore the verification + /// request. + Accepted, + + /// The device receiving this error can ignore the verification + /// request. + MismatchedCommitment, + + /// The SAS did not match. + MismatchedSas, +} + +impl From<&RumaCancelCode> for CancelCode { + fn from(code: &RumaCancelCode) -> Self { + use RumaCancelCode::*; + + match code { + User => Self::User, + Timeout => Self::Timeout, + UnknownTransaction => Self::UnknownTransaction, + UnknownMethod => Self::UnknownMethod, + UnexpectedMessage => Self::UnexpectedMessage, + KeyMismatch => Self::KeyMismatch, + UserMismatch => Self::UserMismatch, + InvalidMessage => Self::InvalidMessage, + Accepted => Self::Accepted, + MismatchedCommitment => Self::MismatchedCommitment, + MismatchedSas => Self::MismatchedSas, + _ => Self::Other, + } + } +} + +/// An emoji that is used for interactive verification using a short +/// auth string. +/// +/// This will contain a single emoji and description from the list of +/// emojis from [the specification]. +/// +/// [the specification]: https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji +#[wasm_bindgen] +#[derive(Debug)] +pub struct Emoji { + inner: matrix_sdk_crypto::Emoji, +} + +impl From for Emoji { + fn from(inner: matrix_sdk_crypto::Emoji) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl Emoji { + /// The emoji symbol that represents a part of the short auth + /// string, for example: 🐶 + #[wasm_bindgen(getter)] + pub fn symbol(&self) -> JsString { + self.inner.symbol.into() + } + + /// The description of the emoji, for example ‘Dog’. + #[wasm_bindgen(getter)] + pub fn description(&self) -> JsString { + self.inner.description.into() + } +} + +/// A QR code. +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +pub struct QrCode { + inner: matrix_sdk_qrcode::qrcode::QrCode, +} + +#[cfg(feature = "qrcode")] +impl fmt::Debug for QrCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(stringify!(QrCode)).finish() + } +} + +#[cfg(feature = "qrcode")] +impl From for QrCode { + fn from(inner: matrix_sdk_qrcode::qrcode::QrCode) -> Self { + Self { inner } + } +} + +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +impl QrCode { + /// Render the QR code into a `Uint8Array` where 1 represents a + /// dark pixel and 0 a white pixel. + pub fn render_into_buffer(&self) -> Result { + let colors: Vec = + self.inner.to_colors().into_iter().map(|color| color.select(1u8, 0u8)).collect(); + let buffer = Uint8Array::new_with_length(colors.len().try_into()?); + buffer.copy_from(colors.as_slice()); + + Ok(buffer) + } +} + +/// An object controlling key verification requests. +/// +/// Interactive verification flows usually start with a verification +/// request, this object lets you send and reply to such a +/// verification request. +/// +/// After the initial handshake the verification flow transitions into +/// one of the verification methods. +#[wasm_bindgen] +#[derive(Debug)] +pub struct VerificationRequest { + inner: matrix_sdk_crypto::VerificationRequest, +} + +impl From for VerificationRequest { + fn from(inner: matrix_sdk_crypto::VerificationRequest) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl VerificationRequest { + #[wasm_bindgen] + pub fn request( + own_user_id: &UserId, + own_device_id: &DeviceId, + other_user_id: &UserId, + methods: Option, + ) -> Result { + let methods: Option> = methods + .map(|array| { + array + .iter() + .map(|method| VerificationMethod::try_from(method).map(Into::into)) + .collect::>() + }) + .transpose()?; + + Ok(serde_json::to_string(&matrix_sdk_crypto::VerificationRequest::request( + &own_user_id.inner, + &own_device_id.inner, + &other_user_id.inner, + methods, + ))?) + } + + /// Our own user id. + #[wasm_bindgen(js_name = "ownUserId")] + pub fn own_user_id(&self) -> UserId { + self.inner.own_user_id().to_owned().into() + } + + /// The ID of the other user that is participating in this + /// verification request. + #[wasm_bindgen(js_name = "otherUserId")] + pub fn other_user_id(&self) -> UserId { + self.inner.other_user().to_owned().into() + } + + /// The ID of the other device that is participating in this + /// verification. + #[wasm_bindgen(js_name = "otherDeviceId")] + pub fn other_device_id(&self) -> Option { + self.inner.other_device_id().map(Into::into) + } + + /// Get the room ID if the verification is happening inside a + /// room. + #[wasm_bindgen(js_name = "roomId")] + pub fn room_id(&self) -> Option { + self.inner.room_id().map(ToOwned::to_owned).map(Into::into) + } + + /// Get info about the cancellation if the verification request + /// has been cancelled. + #[wasm_bindgen(js_name = "cancelInfo")] + pub fn cancel_info(&self) -> Option { + self.inner.cancel_info().map(Into::into) + } + + /// Has the verification request been answered by another device. + #[wasm_bindgen(js_name = "isPassive")] + pub fn is_passive(&self) -> bool { + self.inner.is_passive() + } + + /// Is the verification request ready to start a verification flow. + #[wasm_bindgen(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + + /// Has the verification flow timed out. + #[wasm_bindgen(js_name = "timedOut")] + pub fn timed_out(&self) -> bool { + self.inner.timed_out() + } + + /// Get the supported verification methods of the other side. + /// + /// Will be present only if the other side requested the + /// verification or if we’re in the ready state. + /// + /// It return a `Option>`. + #[wasm_bindgen(js_name = "theirSupportedMethods")] + pub fn their_supported_methods(&self) -> Result, JsError> { + self.inner + .their_supported_methods() + .map(|methods| { + methods + .into_iter() + .map(|method| VerificationMethod::try_from(method).map(JsValue::from)) + .collect::>() + }) + .transpose() + } + + /// Get our own supported verification methods that we advertised. + /// + /// Will be present only we requested the verification or if we’re + /// in the ready state. + #[wasm_bindgen(js_name = "ourSupportedMethods")] + pub fn our_supported_methods(&self) -> Result, JsError> { + self.inner + .our_supported_methods() + .map(|methods| { + methods + .into_iter() + .map(|method| VerificationMethod::try_from(method).map(JsValue::from)) + .collect::>() + }) + .transpose() + } + + /// Get the unique ID of this verification request + #[wasm_bindgen(js_name = "flowId")] + pub fn flow_id(&self) -> String { + self.inner.flow_id().as_str().to_owned() + } + + /// Is this a verification that is veryfying one of our own + /// devices. + #[wasm_bindgen(js_name = "isSelfVerification")] + pub fn is_self_verification(&self) -> bool { + self.inner.is_self_verification() + } + + /// Did we initiate the verification request. + #[wasm_bindgen(js_name = "weStarted")] + pub fn we_started(&self) -> bool { + self.inner.we_started() + } + + /// Has the verification flow that was started with this request + /// finished. + #[wasm_bindgen(js_name = "isDone")] + pub fn is_done(&self) -> bool { + self.inner.is_done() + } + + /// Has the verification flow that was started with this request + /// been cancelled. + #[wasm_bindgen(js_name = "isCancelled")] + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } + + /// Accept the verification request signaling that our client + /// supports the given verification methods. + /// + /// `methods` represents the methods that we should advertise as + /// supported by us. + /// + /// It returns either a `ToDeviceRequest`, a `RoomMessageRequest` + /// or `undefined`. + #[wasm_bindgen(js_name = "acceptWithMethods")] + pub fn accept_with_methods(&self, methods: Array) -> Result { + let methods: Vec = methods + .iter() + .map(|method| VerificationMethod::try_from(method).map(Into::into)) + .collect::>()?; + + self.inner + .accept_with_methods(methods) + .map(OutgoingVerificationRequest) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + /// Accept the verification request. + /// + /// This method will accept the request and signal that it + /// supports the `m.sas.v1`, the `m.qr_code.show.v1`, and + /// `m.reciprocate.v1` method. + /// + /// `m.qr_code.show.v1` will only be signaled if the `qrcode` + /// feature is enabled. This feature is disabled by default. If + /// it's enabled and QR code scanning should be supported or QR + /// code showing shouldn't be supported the `accept_with_methods` + /// method should be used instead. + /// + /// It returns either a `ToDeviceRequest`, a `RoomMessageRequest` + /// or `undefined`. + pub fn accept(&self) -> Result { + self.inner + .accept() + .map(OutgoingVerificationRequest) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + /// Cancel the verification request. + /// + /// It returns either a `ToDeviceRequest`, a `RoomMessageRequest` + /// or `undefined`. + pub fn cancel(&self) -> Result { + self.inner + .cancel() + .map(OutgoingVerificationRequest) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + // start_sas + // generate_qr_code if `qrcode` + // scan_qr_code if `qrcode` +} + +// JavaScript has no complex enums like Rust. To return structs of +// different types, we have no choice that hiding everything behind a +// `JsValue`. +struct OutgoingVerificationRequest(pub(crate) matrix_sdk_crypto::OutgoingVerificationRequest); + +impl TryFrom for JsValue { + type Error = serde_json::Error; + + fn try_from(outgoing_request: OutgoingVerificationRequest) -> Result { + use matrix_sdk_crypto::OutgoingVerificationRequest::*; + + let request_id = outgoing_request.0.request_id().to_string(); + + Ok(match outgoing_request.0 { + ToDevice(request) => { + JsValue::from(requests::ToDeviceRequest::try_from((request_id, &request))?) + } + + InRoom(request) => { + JsValue::from(requests::RoomMessageRequest::try_from((request_id, &request))?) + } + }) + } +} From b6f01b3cecb4013820cc87a750fd1a9f234de6f9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 31 Aug 2022 15:32:09 +0200 Subject: [PATCH 03/37] feat(crypto-js): Implement the `Device` and `UserDevice` API. --- bindings/matrix-sdk-crypto-js/src/device.rs | 229 ++++++++++++++++++ .../matrix-sdk-crypto-js/src/identifiers.rs | 21 +- bindings/matrix-sdk-crypto-js/src/lib.rs | 3 +- bindings/matrix-sdk-crypto-js/src/machine.rs | 40 ++- bindings/matrix-sdk-crypto-js/src/types.rs | 6 +- .../matrix-sdk-crypto-js/src/vodozemac.rs | 107 ++++++++ .../matrix-sdk-crypto-js/tests/device.test.js | 79 ++++++ .../tests/machine.test.js | 6 +- 8 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 bindings/matrix-sdk-crypto-js/src/device.rs create mode 100644 bindings/matrix-sdk-crypto-js/tests/device.test.js diff --git a/bindings/matrix-sdk-crypto-js/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs new file mode 100644 index 000000000..3194bad0f --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -0,0 +1,229 @@ +//! Types for a `Device`. + +use js_sys::{Array, Map, Promise}; +use wasm_bindgen::prelude::*; + +use crate::{ + future::future_to_promise, + identifiers::{self, DeviceId, UserId}, + types, vodozemac, +}; + +#[wasm_bindgen] +#[derive(Debug)] +pub struct Device { + pub(crate) inner: matrix_sdk_crypto::Device, +} + +impl From for Device { + fn from(inner: matrix_sdk_crypto::Device) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl Device { + /// Is this device considered to be verified. + /// + /// This method returns true if either the `is_locally_trusted` + /// method returns `true` or if the `is_cross_signing_trusted` + /// method returns `true`. + #[wasm_bindgen(js_name = "isVerified")] + pub fn is_verified(&self) -> bool { + self.inner.verified() + } + + /// Is this device considered to be verified using cross signing. + #[wasm_bindgen(js_name = "isCrossSigningTrusted")] + pub fn is_cross_signing_trusted(&self) -> bool { + self.inner.is_cross_signing_trusted() + } + + /// Set the local trust state of the device to the given state. + /// + /// This won’t affect any cross signing trust state, this only + /// sets a flag marking to have the given trust state. + /// + /// `trust_state` represents the new trust state that should be + /// set for the device. + #[wasm_bindgen(js_name = "setLocalTrust")] + pub fn set_local_trust(&self, local_state: LocalTrust) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + me.set_local_trust(local_state.into()).await?; + + Ok(JsValue::NULL) + }) + } + + /// The user ID of the device owner. + #[wasm_bindgen(getter, js_name = "userId")] + pub fn user_id(&self) -> UserId { + self.inner.user_id().to_owned().into() + } + + /// The unique ID of the device. + #[wasm_bindgen(getter, js_name = "deviceId")] + pub fn device_id(&self) -> DeviceId { + self.inner.device_id().to_owned().into() + } + + /// Get the human readable name of the device. + #[wasm_bindgen(getter, js_name = "displayName")] + pub fn display_name(&self) -> Option { + self.inner.display_name().map(ToOwned::to_owned) + } + + /// Get the key of the given key algorithm belonging to this device. + #[wasm_bindgen(js_name = "getKey")] + pub fn get_key( + &self, + algorithm: identifiers::DeviceKeyAlgorithmName, + ) -> Result, JsError> { + Ok(self.inner.get_key(algorithm.try_into()?).cloned().map(Into::into)) + } + + /// Get the Curve25519 key of the given device. + #[wasm_bindgen(getter, js_name = "curve25519Key")] + pub fn curve25519_key(&self) -> Option { + self.inner.curve25519_key().map(Into::into) + } + + /// Get the Ed25519 key of the given device. + #[wasm_bindgen(getter, js_name = "ed25519Key")] + pub fn ed25519_key(&self) -> Option { + self.inner.ed25519_key().map(Into::into) + } + + /// Get a map containing all the device keys. + #[wasm_bindgen(getter)] + pub fn keys(&self) -> Map { + let map = Map::new(); + + for (device_key_id, device_key) in self.inner.keys() { + map.set( + &identifiers::DeviceKeyId::from(device_key_id.clone()).into(), + &vodozemac::DeviceKey::from(device_key.clone()).into(), + ); + } + + map + } + + /// Get a map containing all the device signatures. + #[wasm_bindgen(getter)] + pub fn signatures(&self) -> types::Signatures { + self.inner.signatures().clone().into() + } + + /// Get the trust state of the device. + #[wasm_bindgen(getter, js_name = "localTrustState")] + pub fn local_trust_state(&self) -> LocalTrust { + self.inner.local_trust_state().into() + } + + /// Is the device locally marked as trusted? + #[wasm_bindgen(js_name = "isLocallyTrusted")] + pub fn is_locally_trusted(&self) -> bool { + self.inner.is_locally_trusted() + } + + /// Is the device locally marked as blacklisted? + /// + /// Blacklisted devices won’t receive any group sessions. + #[wasm_bindgen(js_name = "isBlacklisted")] + pub fn is_blacklisted(&self) -> bool { + self.inner.is_blacklisted() + } + + /// Is the device deleted? + #[wasm_bindgen(js_name = "isDeleted")] + pub fn is_deleted(&self) -> bool { + self.inner.deleted() + } +} + +/// The local trust state of a device. +#[wasm_bindgen] +#[derive(Debug)] +pub enum LocalTrust { + /// The device has been verified and is trusted. + Verified, + + /// The device been blacklisted from communicating. + BlackListed, + + /// The trust state of the device is being ignored. + Ignored, + + /// The trust stte is unset. + Unset, +} + +impl From for LocalTrust { + fn from(value: matrix_sdk_crypto::LocalTrust) -> Self { + use matrix_sdk_crypto::LocalTrust::*; + + match value { + Verified => Self::Verified, + BlackListed => Self::BlackListed, + Ignored => Self::Ignored, + Unset => Self::Unset, + } + } +} + +impl From for matrix_sdk_crypto::LocalTrust { + fn from(value: LocalTrust) -> Self { + use LocalTrust::*; + + match value { + Verified => Self::Verified, + BlackListed => Self::BlackListed, + Ignored => Self::Ignored, + Unset => Self::Unset, + } + } +} + +/// A read only view over all devices belonging to a user. +#[wasm_bindgen] +#[derive(Debug)] +pub struct UserDevices { + pub(crate) inner: matrix_sdk_crypto::UserDevices, +} + +impl From for UserDevices { + fn from(inner: matrix_sdk_crypto::UserDevices) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl UserDevices { + /// Get the specific device with the given device ID. + pub fn get(&self, device_id: &DeviceId) -> Option { + self.inner.get(&device_id.inner).map(Into::into) + } + + /// Returns true if there is at least one devices of this user + /// that is considered to be verified, false otherwise. + /// + /// This won't consider your own device as verified, as your own + /// device is always implicitly verified. + #[wasm_bindgen(js_name = "isAnyVerified")] + pub fn is_any_verified(&self) -> bool { + self.inner.is_any_verified() + } + + /// Array over all the device IDs of the user devices. + pub fn keys(&self) -> Array { + self.inner.keys().map(ToOwned::to_owned).map(DeviceId::from).map(JsValue::from).collect() + } + + /// Iterator over all the devices of the user devices. + pub fn devices(&self) -> Array { + self.inner.devices().map(Device::from).map(JsValue::from).collect() + } +} diff --git a/bindings/matrix-sdk-crypto-js/src/identifiers.rs b/bindings/matrix-sdk-crypto-js/src/identifiers.rs index 4e18782b5..c75a75d56 100644 --- a/bindings/matrix-sdk-crypto-js/src/identifiers.rs +++ b/bindings/matrix-sdk-crypto-js/src/identifiers.rs @@ -135,7 +135,7 @@ impl DeviceKeyId { #[wasm_bindgen] #[derive(Debug)] pub struct DeviceKeyAlgorithm { - inner: ruma::DeviceKeyAlgorithm, + pub(crate) inner: ruma::DeviceKeyAlgorithm, } impl From for DeviceKeyAlgorithm { @@ -180,6 +180,25 @@ pub enum DeviceKeyAlgorithmName { Unknown, } +impl TryFrom for ruma::DeviceKeyAlgorithm { + type Error = JsError; + + fn try_from(value: DeviceKeyAlgorithmName) -> Result { + use DeviceKeyAlgorithmName::*; + + Ok(match value { + Ed25519 => Self::Ed25519, + Curve25519 => Self::Curve25519, + SignedCurve25519 => Self::SignedCurve25519, + Unknown => { + return Err(JsError::new( + "The `DeviceKeyAlgorithmName.Unknown` variant cannot be converted", + )) + } + }) + } +} + impl From for DeviceKeyAlgorithmName { fn from(value: ruma::DeviceKeyAlgorithm) -> Self { use ruma::DeviceKeyAlgorithm::*; diff --git a/bindings/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs index 3d826e979..61a613630 100644 --- a/bindings/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -18,6 +18,7 @@ #![allow(clippy::drop_non_drop)] // triggered by wasm_bindgen code pub mod attachment; +pub mod device; pub mod encryption; pub mod events; mod future; @@ -29,8 +30,8 @@ pub mod responses; pub mod sync_events; mod tracing; pub mod types; -pub mod vodozemac; pub mod verification; +pub mod vodozemac; use js_sys::{Object, Reflect}; use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*}; diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index fdb6abd47..bfed6064c 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -8,7 +8,7 @@ use serde_json::Value as JsonValue; use wasm_bindgen::prelude::*; use crate::{ - downcast, encryption, + device, downcast, encryption, future::future_to_promise, identifiers, olm, requests, requests::OutgoingRequest, @@ -453,6 +453,44 @@ impl OlmMachine { } })) } + + /// Get a map holding all the devices of a user. + /// + /// `user_id` represents the unique ID of the user that the + /// devices belong to. + #[wasm_bindgen(js_name = "getUserDevices")] + pub fn get_user_devices(&self, user_id: &identifiers::UserId) -> Promise { + let user_id = user_id.inner.clone(); + + let me = self.inner.clone(); + + future_to_promise::<_, device::UserDevices>(async move { + Ok(me.get_user_devices(&user_id, None).await.map(Into::into)?) + }) + } + + /// Get a specific device of a user if one is found and the crypto store + /// didn't throw an error. + /// + /// `user_id` represents the unique ID of the user that the + /// identity belongs to. `device_id` represents the unique ID of + /// the device. + #[wasm_bindgen(js_name = "getDevice")] + pub fn get_device( + &self, + user_id: &identifiers::UserId, + device_id: &identifiers::DeviceId, + ) -> Promise { + let user_id = user_id.inner.clone(); + let device_id = device_id.inner.clone(); + + let me = self.inner.clone(); + + future_to_promise::<_, Option>(async move { + Ok(me.get_device(&user_id, &device_id, None).await?.map(Into::into)) + }) + } + /// Get a verification object for the given user ID with the given /// flow ID (a to-device request ID if the verification has been /// requested by a to-device request, or a room event ID if the diff --git a/bindings/matrix-sdk-crypto-js/src/types.rs b/bindings/matrix-sdk-crypto-js/src/types.rs index 890d54cb3..22bf62a9b 100644 --- a/bindings/matrix-sdk-crypto-js/src/types.rs +++ b/bindings/matrix-sdk-crypto-js/src/types.rs @@ -77,7 +77,7 @@ impl Signatures { /// Do we hold any signatures or is our collection completely /// empty. - #[wasm_bindgen(getter, js_name = "isEmpty")] + #[wasm_bindgen(js_name = "isEmpty")] pub fn is_empty(&self) -> bool { self.inner.is_empty() } @@ -138,13 +138,13 @@ impl From for MaybeSignature { #[wasm_bindgen] impl MaybeSignature { /// Check whether the signature has been successfully decoded. - #[wasm_bindgen(getter, js_name = "isValid")] + #[wasm_bindgen(js_name = "isValid")] pub fn is_valid(&self) -> bool { self.inner.is_ok() } /// Check whether the signature could not be successfully decoded. - #[wasm_bindgen(getter, js_name = "isInvalid")] + #[wasm_bindgen(js_name = "isInvalid")] pub fn is_invalid(&self) -> bool { self.inner.is_err() } diff --git a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs index e80889227..851e528e1 100644 --- a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs +++ b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs @@ -25,6 +25,12 @@ impl Ed25519PublicKey { } } +impl From for Ed25519PublicKey { + fn from(inner: vodozemac::Ed25519PublicKey) -> Self { + Self { inner } + } +} + /// An Ed25519 digital signature, can be used to verify the /// authenticity of a message. #[wasm_bindgen] @@ -79,6 +85,12 @@ impl Curve25519PublicKey { } } +impl From for Curve25519PublicKey { + fn from(inner: vodozemac::Curve25519PublicKey) -> Self { + Self { inner } + } +} + /// Struct holding the two public identity keys of an account. #[wasm_bindgen(getter_with_clone)] #[derive(Debug)] @@ -98,3 +110,98 @@ impl From for IdentityKeys { } } } + +/// An enum over the different key types a device can have. +/// +/// Currently devices have a curve25519 and ed25519 keypair. The keys +/// transport format is a base64 encoded string, any unknown key type +/// will be left as such a string. +#[wasm_bindgen] +#[derive(Debug)] +pub struct DeviceKey { + inner: matrix_sdk_crypto::types::DeviceKey, +} + +impl From for DeviceKey { + fn from(inner: matrix_sdk_crypto::types::DeviceKey) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl DeviceKey { + /// Get the name of the device key. + #[wasm_bindgen(getter)] + pub fn name(&self) -> DeviceKeyName { + (&self.inner).into() + } + + /// Get the value associated to the `Curve25519` device key name. + #[wasm_bindgen(getter)] + pub fn curve25519(&self) -> Option { + use matrix_sdk_crypto::types::DeviceKey::*; + + match &self.inner { + Curve25519(key) => Some(key.clone().into()), + _ => None, + } + } + + /// Get the value associated to the `Ed25519` device key name. + #[wasm_bindgen(getter)] + pub fn ed25519(&self) -> Option { + use matrix_sdk_crypto::types::DeviceKey::*; + + match &self.inner { + Ed25519(key) => Some(key.clone().into()), + _ => None, + } + } + + /// Get the value associated to the `Unknown` device key name. + #[wasm_bindgen(getter)] + pub fn unknown(&self) -> Option { + use matrix_sdk_crypto::types::DeviceKey::*; + + match &self.inner { + Unknown(key) => Some(key.clone()), + _ => None, + } + } + + /// Convert the `DeviceKey` into a base64 encoded string. + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +impl From<&matrix_sdk_crypto::types::DeviceKey> for DeviceKeyName { + fn from(device_key: &matrix_sdk_crypto::types::DeviceKey) -> Self { + use matrix_sdk_crypto::types::DeviceKey::*; + + match device_key { + Curve25519(_) => Self::Curve25519, + Ed25519(_) => Self::Ed25519, + Unknown(_) => Self::Unknown, + } + } +} + +/// An enum over the different key types a device can have. +/// +/// Currently devices have a curve25519 and ed25519 keypair. The keys +/// transport format is a base64 encoded string, any unknown key type +/// will be left as such a string. +#[wasm_bindgen] +#[derive(Debug)] +pub enum DeviceKeyName { + /// The curve25519 device key. + Curve25519, + + /// The ed25519 device key. + Ed25519, + + /// An unknown device key. + Unknown, +} diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js new file mode 100644 index 000000000..0811de218 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -0,0 +1,79 @@ +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures } = require('../pkg/matrix_sdk_crypto_js'); + +describe('LocalTrust', () => { + test('has the correct variant values', () => { + expect(LocalTrust.Verified).toStrictEqual(0); + expect(LocalTrust.BlackListed).toStrictEqual(1); + expect(LocalTrust.Ignored).toStrictEqual(2); + expect(LocalTrust.Unset).toStrictEqual(3); + }); +}); + +describe('DeviceKeyName', () => { + test('has the correct variant values', () => { + expect(DeviceKeyName.Curve25519).toStrictEqual(0); + expect(DeviceKeyName.Ed25519).toStrictEqual(1); + expect(DeviceKeyName.Unknown).toStrictEqual(2); + }); +}); + +describe(OlmMachine.name, () => { + const user = new UserId('@alice:example.org'); + const device = new DeviceId('foobar'); + const room = new RoomId('!baz:matrix.org'); + + function machine(new_user, new_device) { + return new OlmMachine(new_user || user, new_device || device); + } + + test('can read user devices', async () => { + const m = await machine(); + const userDevices = await m.getUserDevices(user); + + expect(userDevices).toBeInstanceOf(UserDevices); + expect(userDevices.get(device)).toBeInstanceOf(Device); + expect(userDevices.isAnyVerified()).toStrictEqual(false); + expect(userDevices.keys().map(device_id => device_id.toString())).toStrictEqual([device.toString()]); + expect(userDevices.devices().map(device => device.deviceId.toString())).toStrictEqual([device.toString()]); + }); + + test('can read a user device', async () => { + const m = await machine(); + const dev = await m.getDevice(user, device); + + expect(dev).toBeInstanceOf(Device); + expect(dev.isVerified()).toStrictEqual(false); + expect(dev.isCrossSigningTrusted()).toStrictEqual(false); + + expect(dev.localTrustState).toStrictEqual(LocalTrust.Unset); + expect(dev.isLocallyTrusted()).toStrictEqual(false); + expect(await dev.setLocalTrust(LocalTrust.Verified)).toBeNull(); + expect(dev.localTrustState).toStrictEqual(LocalTrust.Verified); + expect(dev.isLocallyTrusted()).toStrictEqual(true); + + expect(dev.userId.toString()).toStrictEqual(user.toString()); + expect(dev.deviceId.toString()).toStrictEqual(device.toString()); + expect(dev.deviceName).toBeUndefined(); + + const deviceKey = dev.getKey(DeviceKeyAlgorithmName.Ed25519); + + expect(deviceKey).toBeInstanceOf(DeviceKey); + expect(deviceKey.name).toStrictEqual(DeviceKeyName.Ed25519); + expect(deviceKey.curve25519).toBeUndefined(); + expect(deviceKey.ed25519).toBeInstanceOf(Ed25519PublicKey); + expect(deviceKey.unknown).toBeUndefined(); + expect(deviceKey.toBase64()).toMatch(/^[A-Za-z0-9\+/]+$/); + + expect(dev.curve25519Key).toBeInstanceOf(Curve25519PublicKey); + expect(dev.ed25519Key).toBeInstanceOf(Ed25519PublicKey); + + for (const [deviceKeyId, deviceKey] of dev.keys) { + expect(deviceKeyId).toBeInstanceOf(DeviceKeyId); + expect(deviceKey).toBeInstanceOf(DeviceKey); + } + + expect(dev.signatures).toBeInstanceOf(Signatures); + expect(dev.isBlacklisted()).toStrictEqual(false); + expect(dev.isDeleted()).toStrictEqual(false); + }); +}); diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index d5c918622..573462de4 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -414,7 +414,7 @@ describe(OlmMachine.name, () => { const m = await machine(); const signatures = await m.sign('foo'); - expect(signatures.isEmpty).toStrictEqual(false); + expect(signatures.isEmpty()).toStrictEqual(false); expect(signatures.count).toStrictEqual(1); let base64; @@ -429,8 +429,8 @@ describe(OlmMachine.name, () => { expect(s).toBeInstanceOf(MaybeSignature); - expect(s.isValid).toStrictEqual(true); - expect(s.isInvalid).toStrictEqual(false); + expect(s.isValid()).toStrictEqual(true); + expect(s.isInvalid()).toStrictEqual(false); expect(s.invalidSignatureSource).toBeUndefined(); base64 = s.signature.toBase64(); From e00b9221b92b23723bd62af5dbd871c98e94fa31 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 31 Aug 2022 17:38:14 +0200 Subject: [PATCH 04/37] feat(crypto-js): Implement `Device.request_verification`. --- bindings/matrix-sdk-crypto-js/src/device.rs | 35 ++++++++++++++++++- .../matrix-sdk-crypto-js/src/verification.rs | 10 +++++- .../matrix-sdk-crypto-js/tests/device.test.js | 22 +++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs index 3194bad0f..1e97be637 100644 --- a/bindings/matrix-sdk-crypto-js/src/device.rs +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*; use crate::{ future::future_to_promise, identifiers::{self, DeviceId, UserId}, - types, vodozemac, + types, verification, vodozemac, }; #[wasm_bindgen] @@ -23,6 +23,39 @@ impl From for Device { #[wasm_bindgen] impl Device { + /// Request an interactive verification with this device. + #[wasm_bindgen(js_name = "requestVerification")] + pub fn request_verification(&self, methods: Option) -> Result { + let methods = methods + .map(|array| { + array + .iter() + .map(|method| { + verification::VerificationMethod::try_from(method).map(Into::into) + }) + .collect::>() + }) + .transpose()?; + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + let tuple = Array::new(); + let (verification_request, outgoing_verification_request) = match methods { + Some(methods) => me.request_verification_with_methods(methods).await, + None => me.request_verification().await, + }; + + tuple.set(0, verification::VerificationRequest::from(verification_request).into()); + tuple.set( + 1, + verification::OutgoingVerificationRequest::from(outgoing_verification_request) + .try_into()?, + ); + + Ok(tuple) + })) + } + /// Is this device considered to be verified. /// /// This method returns true if either the `is_locally_trusted` diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index f261fa98c..9752d9e92 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -876,7 +876,15 @@ impl VerificationRequest { // JavaScript has no complex enums like Rust. To return structs of // different types, we have no choice that hiding everything behind a // `JsValue`. -struct OutgoingVerificationRequest(pub(crate) matrix_sdk_crypto::OutgoingVerificationRequest); +pub(crate) struct OutgoingVerificationRequest( + pub(crate) matrix_sdk_crypto::OutgoingVerificationRequest, +); + +impl From for OutgoingVerificationRequest { + fn from(inner: matrix_sdk_crypto::OutgoingVerificationRequest) -> Self { + Self(inner) + } +} impl TryFrom for JsValue { type Error = serde_json::Error; diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 0811de218..5a492417d 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -1,4 +1,4 @@ -const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures } = require('../pkg/matrix_sdk_crypto_js'); +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationRequest, ToDeviceRequest } = require('../pkg/matrix_sdk_crypto_js'); describe('LocalTrust', () => { test('has the correct variant values', () => { @@ -77,3 +77,23 @@ describe(OlmMachine.name, () => { expect(dev.isDeleted()).toStrictEqual(false); }); }); + +describe(Device.name, () => { + const user = new UserId('@alice:example.org'); + const device = new DeviceId('foobar'); + const room = new RoomId('!baz:matrix.org'); + + function machine(new_user, new_device) { + return new OlmMachine(new_user || user, new_device || device); + } + + test('can request verification', async () => { + const m = await machine(); + const dev = await m.getDevice(user, device); + + const [verificationRequest, outgoingVerificationRequest] = await dev.requestVerification(); + + expect(verificationRequest).toBeInstanceOf(VerificationRequest); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + }); +}); From e659c724cdc485bf4d9b1aa31ab9da1844d524a1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 1 Sep 2022 10:59:39 +0200 Subject: [PATCH 05/37] chore(crypto-js): Some methods have been renamed. --- bindings/matrix-sdk-crypto-js/src/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs index 1e97be637..d2518f0cf 100644 --- a/bindings/matrix-sdk-crypto-js/src/device.rs +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -63,7 +63,7 @@ impl Device { /// method returns `true`. #[wasm_bindgen(js_name = "isVerified")] pub fn is_verified(&self) -> bool { - self.inner.verified() + self.inner.is_verified() } /// Is this device considered to be verified using cross signing. From 155b187d451f154cf8065412b48f6503715b41cc Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 1 Sep 2022 15:29:52 +0200 Subject: [PATCH 06/37] test(crypto-js): Write first tests for key verification. --- bindings/matrix-sdk-crypto-js/src/device.rs | 2 +- .../matrix-sdk-crypto-js/src/verification.rs | 20 +++--- .../matrix-sdk-crypto-js/tests/device.test.js | 71 ++++++++++++++++--- bindings/matrix-sdk-crypto-js/tests/helper.js | 48 +++++++++++++ 4 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 bindings/matrix-sdk-crypto-js/tests/helper.js diff --git a/bindings/matrix-sdk-crypto-js/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs index d2518f0cf..af9598bf7 100644 --- a/bindings/matrix-sdk-crypto-js/src/device.rs +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -173,7 +173,7 @@ impl Device { /// Is the device deleted? #[wasm_bindgen(js_name = "isDeleted")] pub fn is_deleted(&self) -> bool { - self.inner.deleted() + self.inner.is_deleted() } } diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 9752d9e92..408e7ee3d 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -661,6 +661,10 @@ impl From for VerificationRequest { #[wasm_bindgen] impl VerificationRequest { + /// Create an event content that can be sent as a room event to + /// request verification from the other side. This should be used + /// only for verifications of other users and it should be sent to + /// a room we consider to be a DM with the other user. #[wasm_bindgen] pub fn request( own_user_id: &UserId, @@ -686,35 +690,35 @@ impl VerificationRequest { } /// Our own user id. - #[wasm_bindgen(js_name = "ownUserId")] + #[wasm_bindgen(getter, js_name = "ownUserId")] pub fn own_user_id(&self) -> UserId { self.inner.own_user_id().to_owned().into() } /// The ID of the other user that is participating in this /// verification request. - #[wasm_bindgen(js_name = "otherUserId")] + #[wasm_bindgen(getter, js_name = "otherUserId")] pub fn other_user_id(&self) -> UserId { self.inner.other_user().to_owned().into() } /// The ID of the other device that is participating in this /// verification. - #[wasm_bindgen(js_name = "otherDeviceId")] + #[wasm_bindgen(getter, js_name = "otherDeviceId")] pub fn other_device_id(&self) -> Option { self.inner.other_device_id().map(Into::into) } /// Get the room ID if the verification is happening inside a /// room. - #[wasm_bindgen(js_name = "roomId")] + #[wasm_bindgen(getter, js_name = "roomId")] pub fn room_id(&self) -> Option { self.inner.room_id().map(ToOwned::to_owned).map(Into::into) } /// Get info about the cancellation if the verification request /// has been cancelled. - #[wasm_bindgen(js_name = "cancelInfo")] + #[wasm_bindgen(getter, js_name = "cancelInfo")] pub fn cancel_info(&self) -> Option { self.inner.cancel_info().map(Into::into) } @@ -743,7 +747,7 @@ impl VerificationRequest { /// verification or if we’re in the ready state. /// /// It return a `Option>`. - #[wasm_bindgen(js_name = "theirSupportedMethods")] + #[wasm_bindgen(getter, js_name = "theirSupportedMethods")] pub fn their_supported_methods(&self) -> Result, JsError> { self.inner .their_supported_methods() @@ -760,7 +764,7 @@ impl VerificationRequest { /// /// Will be present only we requested the verification or if we’re /// in the ready state. - #[wasm_bindgen(js_name = "ourSupportedMethods")] + #[wasm_bindgen(getter, js_name = "ourSupportedMethods")] pub fn our_supported_methods(&self) -> Result, JsError> { self.inner .our_supported_methods() @@ -774,7 +778,7 @@ impl VerificationRequest { } /// Get the unique ID of this verification request - #[wasm_bindgen(js_name = "flowId")] + #[wasm_bindgen(getter, js_name = "flowId")] pub fn flow_id(&self) -> String { self.inner.flow_id().as_str().to_owned() } diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 5a492417d..7042c7d82 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -1,4 +1,5 @@ -const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationRequest, ToDeviceRequest } = require('../pkg/matrix_sdk_crypto_js'); +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest } = require('../pkg/matrix_sdk_crypto_js'); +const { addMachineToMachine } = require('./helper'); describe('LocalTrust', () => { test('has the correct variant values', () => { @@ -79,21 +80,71 @@ describe(OlmMachine.name, () => { }); describe(Device.name, () => { - const user = new UserId('@alice:example.org'); - const device = new DeviceId('foobar'); - const room = new RoomId('!baz:matrix.org'); + const userId1 = new UserId('@alice:example.org'); + const deviceId1 = new DeviceId('foobar'); + + const userId2 = new UserId('@bob:example.org'); + const deviceId2 = new DeviceId('bazqux'); function machine(new_user, new_device) { - return new OlmMachine(new_user || user, new_device || device); + return new OlmMachine(new_user || userId1, new_device || deviceId1); } test('can request verification', async () => { - const m = await machine(); - const dev = await m.getDevice(user, device); + // First Olm machine. + const m1 = await machine(userId1, deviceId1); + // Second Olm machine. + const m2 = await machine(userId2, deviceId2); - const [verificationRequest, outgoingVerificationRequest] = await dev.requestVerification(); + // Make `m1` and `m2` be aware of each other. + { + await addMachineToMachine(m2, m1); + await addMachineToMachine(m1, m2); + } - expect(verificationRequest).toBeInstanceOf(VerificationRequest); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + // Pick the device we want to start the verification with. + const device2 = await m1.getDevice(userId2, deviceId2); + + expect(device2).toBeInstanceOf(Device); + + // Request a verification with `dev1`. + const [verificationRequest1, outgoingVerificationRequest1] = await device2.requestVerification(); + + { + expect(verificationRequest1).toBeInstanceOf(VerificationRequest); + expect(outgoingVerificationRequest1).toBeInstanceOf(ToDeviceRequest); + + expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest1.otherDeviceId).toBeUndefined(); + expect(verificationRequest1.roomId).toBeUndefined(); + expect(verificationRequest1.cancelInfo).toBeUndefined(); + expect(verificationRequest1.isPassive()).toStrictEqual(false); + expect(verificationRequest1.isReady()).toStrictEqual(false); + expect(verificationRequest1.timedOut()).toStrictEqual(false); + expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); + expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest1.weStarted()).toStrictEqual(true); + expect(verificationRequest1.isDone()).toStrictEqual(false); + expect(verificationRequest1.isCancelled()).toStrictEqual(false); + } + + + // Send the outgoing verification request from `m1` to `m2`. + { + //const receiveSyncChanges = await JSON.parse(await m2.receiveSyncChanges(outgoingVerificationRequest1.body, new DeviceLists(), new Map(), new Set())); + //console.log(receiveSyncChanges); + } + }); +}); + +describe('VerificationMethod', () => { + test('has the correct variant values', () => { + expect(VerificationMethod.SasV1).toStrictEqual(0); + expect(VerificationMethod.QrCodeScanV1).toStrictEqual(1); + expect(VerificationMethod.QrCodeShowV1).toStrictEqual(2); + expect(VerificationMethod.ReciprocateV1).toStrictEqual(3); }); }); diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js new file mode 100644 index 000000000..853845e5a --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -0,0 +1,48 @@ +const { DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest } = require('../pkg/matrix_sdk_crypto_js'); + +// Add a machine to another machine, i.e. be sure a machine knows +// another exists. +async function addMachineToMachine(machineToAdd, machine) { + const toDeviceEvents = JSON.stringify({}); + const changedDevices = new DeviceLists(); + const oneTimeKeyCounts = new Map(); + const unusedFallbackKeys = new Set(); + + const receiveSyncChanges = JSON.parse(await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); + + expect(receiveSyncChanges).toEqual({}); + + const outgoingRequests = await machineToAdd.outgoingRequests(); + + expect(outgoingRequests).toHaveLength(2); + + let keysUploadRequest; + // Read the `KeysUploadRequest`. + { + expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest); + expect(outgoingRequests[0].id).toBeDefined(); + expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload); + + const body = JSON.parse(outgoingRequests[0].body); + expect(body.device_keys).toBeDefined(); + expect(body.one_time_keys).toBeDefined(); + + keysUploadRequest = body; + } + + { + // Just to be sure… not important. + expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest); + } + + // Let's forge a `KeysQuery`'s response. + let keyQueryResponse = {'device_keys': {}}; + const userId = machineToAdd.userId.toString(); + const deviceId = machineToAdd.deviceId.toString(); + keyQueryResponse['device_keys'][userId] = {}; + keyQueryResponse['device_keys'][userId][deviceId] = keysUploadRequest.device_keys; + + await machine.markRequestAsSent('anID', RequestType.KeysQuery, JSON.stringify(keyQueryResponse)); +} + +module.exports = { addMachineToMachine }; From e6141d8efc1e7292fd6a3f2ba29583da54bfbbfb Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 5 Sep 2022 14:39:33 +0200 Subject: [PATCH 07/37] test(crypto-js): Continue to test `m.key.verification.request` and `.ready`. --- .../matrix-sdk-crypto-js/tests/device.test.js | 75 +++++++++++++++++-- bindings/matrix-sdk-crypto-js/tests/helper.js | 30 +++++--- 2 files changed, 88 insertions(+), 17 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 7042c7d82..650f68c94 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -81,10 +81,10 @@ describe(OlmMachine.name, () => { describe(Device.name, () => { const userId1 = new UserId('@alice:example.org'); - const deviceId1 = new DeviceId('foobar'); + const deviceId1 = new DeviceId('alice_device'); const userId2 = new UserId('@bob:example.org'); - const deviceId2 = new DeviceId('bazqux'); + const deviceId2 = new DeviceId('bob_device'); function machine(new_user, new_device) { return new OlmMachine(new_user || userId1, new_device || deviceId1); @@ -107,8 +107,8 @@ describe(Device.name, () => { expect(device2).toBeInstanceOf(Device); - // Request a verification with `dev1`. - const [verificationRequest1, outgoingVerificationRequest1] = await device2.requestVerification(); + // Request a verification from `m1` to `device2`. + let [verificationRequest1, outgoingVerificationRequest1] = await device2.requestVerification(); { expect(verificationRequest1).toBeInstanceOf(VerificationRequest); @@ -131,11 +131,72 @@ describe(Device.name, () => { expect(verificationRequest1.isCancelled()).toStrictEqual(false); } + let flowId; + + // Fetch the verification from `m2`. + let verificationRequest2; - // Send the outgoing verification request from `m1` to `m2`. { - //const receiveSyncChanges = await JSON.parse(await m2.receiveSyncChanges(outgoingVerificationRequest1.body, new DeviceLists(), new Map(), new Set())); - //console.log(receiveSyncChanges); + const outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest1.body); + const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()] + + // Let's pretend the message is coming from a server. + const toDeviceEvents = { + "events": [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingContent, + }] + }; + + // Let's send the verification request to `m2`. + const receiveSyncChanges = await JSON.parse(await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set())); + + // Oh, a new verification request. + flowId = outgoingContent.transaction_id; + verificationRequest2 = m2.getVerificationRequest(userId1, flowId); + + expect(verificationRequest2).toBeInstanceOf(VerificationRequest); + + expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(verificationRequest2.roomId).toBeUndefined(); + expect(verificationRequest2.cancelInfo).toBeUndefined(); + expect(verificationRequest2.isPassive()).toStrictEqual(false); + expect(verificationRequest2.isReady()).toStrictEqual(false); + expect(verificationRequest2.timedOut()).toStrictEqual(false); + expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); + expect(verificationRequest2.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest2.weStarted()).toStrictEqual(false); + expect(verificationRequest2.isDone()).toStrictEqual(false); + expect(verificationRequest2.isCancelled()).toStrictEqual(false); + + const verificationRequests = m2.getVerificationRequests(userId1); + expect(verificationRequests).toHaveLength(1); + expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same + } + + // The request verification is ready. + { + let outgoingVerificationRequest = verificationRequest2.accept(); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + + // Let's pretend the message is coming from a server. + const toDeviceEvents = { + "events": [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }] + }; + + // Let's send the verification ready to `m1`. + const receiveSyncChanges = await JSON.parse(await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set())); } }); }); diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js index 853845e5a..8f84308ca 100644 --- a/bindings/matrix-sdk-crypto-js/tests/helper.js +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -27,22 +27,32 @@ async function addMachineToMachine(machineToAdd, machine) { expect(body.device_keys).toBeDefined(); expect(body.one_time_keys).toBeDefined(); + // https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload + const hypothetical_response = JSON.stringify({ + "one_time_key_counts": { + "curve25519": 10, + "signed_curve25519": 20 + } + }); + const marked = await machineToAdd.markRequestAsSent(outgoingRequests[0].id, outgoingRequests[0].type, hypothetical_response); + expect(marked).toStrictEqual(true); + keysUploadRequest = body; } { - // Just to be sure… not important. expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest); + + // Let's forge a `KeysQuery`'s response. + let keyQueryResponse = {'device_keys': {}}; + const userId = machineToAdd.userId.toString(); + const deviceId = machineToAdd.deviceId.toString(); + keyQueryResponse['device_keys'][userId] = {}; + keyQueryResponse['device_keys'][userId][deviceId] = keysUploadRequest.device_keys; + + const marked = await machine.markRequestAsSent(outgoingRequests[1].id, outgoingRequests[1].type, JSON.stringify(keyQueryResponse)); + expect(marked).toStrictEqual(true); } - - // Let's forge a `KeysQuery`'s response. - let keyQueryResponse = {'device_keys': {}}; - const userId = machineToAdd.userId.toString(); - const deviceId = machineToAdd.deviceId.toString(); - keyQueryResponse['device_keys'][userId] = {}; - keyQueryResponse['device_keys'][userId][deviceId] = keysUploadRequest.device_keys; - - await machine.markRequestAsSent('anID', RequestType.KeysQuery, JSON.stringify(keyQueryResponse)); } module.exports = { addMachineToMachine }; From 14f22979c09baa6d80e9cf91da3ffbfb5cdcc572 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 5 Sep 2022 15:07:10 +0200 Subject: [PATCH 08/37] feat(crypto-js): Implement `VerificationRequest.start_sas`. --- .../matrix-sdk-crypto-js/src/verification.rs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 408e7ee3d..eeda883c6 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -1,12 +1,13 @@ //! Different verification types. -use js_sys::{Array, JsString}; +use js_sys::{Array, JsString, Promise}; use ruma::events::key::verification::{ cancel::CancelCode as RumaCancelCode, VerificationMethod as RumaVerificationMethod, }; use wasm_bindgen::prelude::*; use crate::{ + future::future_to_promise, identifiers::{DeviceId, RoomId, UserId}, requests, }; @@ -130,6 +131,12 @@ pub struct Sas { inner: matrix_sdk_crypto::Sas, } +impl From for Sas { + fn from(inner: matrix_sdk_crypto::Sas) -> Self { + Self { inner } + } +} + #[wasm_bindgen] impl Sas { /// Get our own user ID. @@ -872,7 +879,36 @@ impl VerificationRequest { .map_err(Into::into) } - // start_sas + /// Transition from this verification request into a SAS verification flow. + #[wasm_bindgen(js_name = "startSas")] + pub fn start_sas(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + match me + .start_sas() + .await? + .map(|(sas, outgoing_verification_request)| -> Result { + let tuple = Array::new(); + + tuple.set(0, Sas::from(sas).into()); + tuple.set( + 1, + OutgoingVerificationRequest::from(outgoing_verification_request) + .try_into()?, + ); + + Ok(tuple) + }) + .transpose() + { + Ok(a) => Ok(a), + Err(_) => { + Err(anyhow::Error::msg("Failed to build the outgoing verification request")) + } + } + }) + } // generate_qr_code if `qrcode` // scan_qr_code if `qrcode` } From 95709bb4b301999159074d69b334aa60bf9f5ac3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 5 Sep 2022 15:20:28 +0200 Subject: [PATCH 09/37] test(crypto-js): Test the `Sas` implementation. --- .../matrix-sdk-crypto-js/tests/device.test.js | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 650f68c94..5e196f7b2 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -1,4 +1,4 @@ -const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest } = require('../pkg/matrix_sdk_crypto_js'); +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest, Sas } = require('../pkg/matrix_sdk_crypto_js'); const { addMachineToMachine } = require('./helper'); describe('LocalTrust', () => { @@ -108,11 +108,11 @@ describe(Device.name, () => { expect(device2).toBeInstanceOf(Device); // Request a verification from `m1` to `device2`. - let [verificationRequest1, outgoingVerificationRequest1] = await device2.requestVerification(); + let [verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification(); { expect(verificationRequest1).toBeInstanceOf(VerificationRequest); - expect(outgoingVerificationRequest1).toBeInstanceOf(ToDeviceRequest); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); @@ -137,8 +137,8 @@ describe(Device.name, () => { let verificationRequest2; { - const outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest1.body); - const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()] + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; // Let's pretend the message is coming from a server. const toDeviceEvents = { @@ -198,6 +198,36 @@ describe(Device.name, () => { // Let's send the verification ready to `m1`. const receiveSyncChanges = await JSON.parse(await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set())); } + + // Let's start a SAS verification. + { + const [sas, outgoingVerificationRequest] = await verificationRequest2.startSas(); + expect(sas).toBeInstanceOf(Sas); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + { + expect(sas.userId.toString()).toStrictEqual(userId2.toString()); + expect(sas.deviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(sas.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas.flowId).toStrictEqual(flowId); + expect(sas.roomId).toBeUndefined(); + expect(sas.supportsEmoji()).toStrictEqual(false); + expect(sas.startedFromRequest()).toStrictEqual(true); + expect(sas.isSelfVerification()).toStrictEqual(false); + expect(sas.haveWeConfirmed()).toStrictEqual(false); + expect(sas.hasBeenAccepted()).toStrictEqual(false); + expect(sas.cancelInfo()).toBeUndefined(); + expect(sas.weStarted()).toStrictEqual(false); + expect(sas.timedOut()).toStrictEqual(false); + expect(sas.canBePresented()).toStrictEqual(false); + expect(sas.isDone()).toStrictEqual(false); + expect(sas.isCancelled()).toStrictEqual(false); + expect(sas.emoji()).toBeUndefined(); + expect(sas.emojiIndex()).toBeUndefined(); + expect(sas.decimals()).toBeUndefined(); + } + } }); }); From b5a81030235b839e23666badbcf34c28e4bb6e42 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 12:14:58 +0200 Subject: [PATCH 10/37] feat(crypto-js): Implement `Sas.accept`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 3 +- .../matrix-sdk-crypto-js/src/verification.rs | 22 ++- .../matrix-sdk-crypto-js/tests/device.test.js | 156 ++++++++++++++---- 3 files changed, 141 insertions(+), 40 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index bfed6064c..b49a8a571 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -496,7 +496,8 @@ impl OlmMachine { /// requested by a to-device request, or a room event ID if the /// verification has been requested by a room event). /// - /// It returns a `Verification` object. + /// It returns a “`Verification` object”, which is either a `Sas` + /// or `Qr` object. #[wasm_bindgen(js_name = "getVerification")] pub fn get_verification( &self, diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index eeda883c6..20ffb00fe 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -1,5 +1,4 @@ -//! Different verification types. - +///! Different verification types. use js_sys::{Array, JsString, Promise}; use ruma::events::key::verification::{ cancel::CancelCode as RumaCancelCode, VerificationMethod as RumaVerificationMethod, @@ -230,11 +229,20 @@ impl Sas { self.inner.we_started() } - /* - pub fn accept(&self) { - todo!() + /// Accept the SAS verification. + /// + /// This does nothing if the verification was already accepted, + /// otherwise it returns an `AcceptEventContent` that needs to be + /// sent out. + pub fn accept(&self) -> Result { + self.inner + .accept() + .map(OutgoingVerificationRequest) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) } - */ /* #[wasm_bindgen(js_name = "acceptWithSettings")] @@ -308,7 +316,7 @@ impl Sas { /// inclusive which can be converted to an emoji using [the /// relevant specification /// entry](https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji). - #[wasm_bindgen(js_name = "emoji_index")] + #[wasm_bindgen(js_name = "emojiIndex")] pub fn emoji_index(&self) -> Option { Some(self.inner.emoji_index()?.into_iter().map(JsValue::from).collect()) } diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 5e196f7b2..414c3773e 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -112,7 +112,6 @@ describe(Device.name, () => { { expect(verificationRequest1).toBeInstanceOf(VerificationRequest); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); @@ -129,6 +128,11 @@ describe(Device.name, () => { expect(verificationRequest1.weStarted()).toStrictEqual(true); expect(verificationRequest1.isDone()).toStrictEqual(false); expect(verificationRequest1.isCancelled()).toStrictEqual(false); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); } let flowId; @@ -137,12 +141,11 @@ describe(Device.name, () => { let verificationRequest2; { - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; // Let's pretend the message is coming from a server. const toDeviceEvents = { - "events": [{ + events: [{ sender: userId1.toString(), type: outgoingVerificationRequest.event_type, content: outgoingContent, @@ -150,7 +153,7 @@ describe(Device.name, () => { }; // Let's send the verification request to `m2`. - const receiveSyncChanges = await JSON.parse(await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set())); + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); // Oh, a new verification request. flowId = outgoingContent.transaction_id; @@ -184,49 +187,138 @@ describe(Device.name, () => { let outgoingVerificationRequest = verificationRequest2.accept(); expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); // Let's pretend the message is coming from a server. const toDeviceEvents = { - "events": [{ - sender: userId1.toString(), + events: [{ + sender: userId2.toString(), type: outgoingVerificationRequest.event_type, content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], }] }; // Let's send the verification ready to `m1`. - const receiveSyncChanges = await JSON.parse(await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set())); + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); } - // Let's start a SAS verification. + // Verification is ready on both side. { - const [sas, outgoingVerificationRequest] = await verificationRequest2.startSas(); - expect(sas).toBeInstanceOf(Sas); + expect(verificationRequest1.isReady()).toStrictEqual(true); + expect(verificationRequest2.isReady()).toStrictEqual(true); + + expect(verificationRequest1.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + + expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + } + + // Let's start a SAS verification, from `m2` for example. + let sas2; + + { + let [sas, outgoingVerificationRequest] = await verificationRequest2.startSas(); + sas2 = sas; + expect(sas2).toBeInstanceOf(Sas); + + expect(sas2.userId.toString()).toStrictEqual(userId2.toString()); + expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas2.flowId).toStrictEqual(flowId); + expect(sas2.roomId).toBeUndefined(); + expect(sas2.supportsEmoji()).toStrictEqual(false); + expect(sas2.startedFromRequest()).toStrictEqual(true); + expect(sas2.isSelfVerification()).toStrictEqual(false); + expect(sas2.haveWeConfirmed()).toStrictEqual(false); + expect(sas2.hasBeenAccepted()).toStrictEqual(false); + expect(sas2.cancelInfo()).toBeUndefined(); + expect(sas2.weStarted()).toStrictEqual(false); + expect(sas2.timedOut()).toStrictEqual(false); + expect(sas2.canBePresented()).toStrictEqual(false); + expect(sas2.isDone()).toStrictEqual(false); + expect(sas2.isCancelled()).toStrictEqual(false); + expect(sas2.emoji()).toBeUndefined(); + expect(sas2.emojiIndex()).toBeUndefined(); + expect(sas2.decimals()).toBeUndefined(); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - { - expect(sas.userId.toString()).toStrictEqual(userId2.toString()); - expect(sas.deviceId.toString()).toStrictEqual(deviceId2.toString()); - expect(sas.otherUserId.toString()).toStrictEqual(userId1.toString()); - expect(sas.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(sas.flowId).toStrictEqual(flowId); - expect(sas.roomId).toBeUndefined(); - expect(sas.supportsEmoji()).toStrictEqual(false); - expect(sas.startedFromRequest()).toStrictEqual(true); - expect(sas.isSelfVerification()).toStrictEqual(false); - expect(sas.haveWeConfirmed()).toStrictEqual(false); - expect(sas.hasBeenAccepted()).toStrictEqual(false); - expect(sas.cancelInfo()).toBeUndefined(); - expect(sas.weStarted()).toStrictEqual(false); - expect(sas.timedOut()).toStrictEqual(false); - expect(sas.canBePresented()).toStrictEqual(false); - expect(sas.isDone()).toStrictEqual(false); - expect(sas.isCancelled()).toStrictEqual(false); - expect(sas.emoji()).toBeUndefined(); - expect(sas.emojiIndex()).toBeUndefined(); - expect(sas.decimals()).toBeUndefined(); - } + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }] + }; + + // Let's send the SAS start to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + } + + // Let's accept the SAS start request. + let sas1; + + { + const sas = await m1.getVerification(userId2, flowId); + sas1 = sas; + + expect(sas1).toBeInstanceOf(Sas); + + expect(sas1.userId.toString()).toStrictEqual(userId1.toString()); + expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas1.flowId).toStrictEqual(flowId); + expect(sas1.roomId).toBeUndefined(); + expect(sas1.startedFromRequest()).toStrictEqual(true); + expect(sas1.isSelfVerification()).toStrictEqual(false); + expect(sas1.haveWeConfirmed()).toStrictEqual(false); + expect(sas1.hasBeenAccepted()).toStrictEqual(false); + expect(sas1.cancelInfo()).toBeUndefined(); + expect(sas1.weStarted()).toStrictEqual(true); + expect(sas1.timedOut()).toStrictEqual(false); + expect(sas1.canBePresented()).toStrictEqual(false); + expect(sas1.isDone()).toStrictEqual(false); + expect(sas1.isCancelled()).toStrictEqual(false); + expect(sas1.emoji()).toBeUndefined(); + expect(sas1.emojiIndex()).toBeUndefined(); + expect(sas1.decimals()).toBeUndefined(); + + + let outgoingVerificationRequest = sas1.accept(); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }] + }; + + // Let's send the SAS accept to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + } + + // Let's see if SAS's state on both side. + { + expect(sas1.supportsEmoji()).toStrictEqual(true); + expect(sas2.supportsEmoji()).toStrictEqual(true); + } + + // Let's send the verification key from `m2` to `m1`. + { + } }); }); From 6239d31bcfdf25780ab0eb665ab50f1822384e40 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 14:06:03 +0200 Subject: [PATCH 11/37] test(crypto-js): Test the `Emoji` and decimals implementations. --- .../matrix-sdk-crypto-js/tests/device.test.js | 116 ++++++++++++++++-- bindings/matrix-sdk-crypto-js/tests/helper.js | 13 +- 2 files changed, 120 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 414c3773e..634f3061d 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -1,5 +1,5 @@ -const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest, Sas } = require('../pkg/matrix_sdk_crypto_js'); -const { addMachineToMachine } = require('./helper'); +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest, Sas, Emoji } = require('../pkg/matrix_sdk_crypto_js'); +const { zip, addMachineToMachine } = require('./helper'); describe('LocalTrust', () => { test('has the correct variant values', () => { @@ -143,7 +143,6 @@ describe(Device.name, () => { { const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; - // Let's pretend the message is coming from a server. const toDeviceEvents = { events: [{ sender: userId1.toString(), @@ -191,13 +190,12 @@ describe(Device.name, () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - // Let's pretend the message is coming from a server. const toDeviceEvents = { events: [{ sender: userId2.toString(), type: outgoingVerificationRequest.event_type, content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }] + }], }; // Let's send the verification ready to `m1`. @@ -255,7 +253,7 @@ describe(Device.name, () => { sender: userId2.toString(), type: outgoingVerificationRequest.event_type, content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }] + }], }; // Let's send the SAS start to `m1`. @@ -303,7 +301,7 @@ describe(Device.name, () => { sender: userId1.toString(), type: outgoingVerificationRequest.event_type, content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] + }], }; // Let's send the SAS accept to `m2`. @@ -316,7 +314,109 @@ describe(Device.name, () => { expect(sas2.supportsEmoji()).toStrictEqual(true); } - // Let's send the verification key from `m2` to `m1`. + // Let's send the verification keys. + { + // From `m2` to `m1`. + { + const outgoingRequests = await m2.outgoingRequests(); + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS key to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m2.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + } + + // From `m1` to `m2`. + { + const outgoingRequests = await m1.outgoingRequests(); + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send te SAS key to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + } + } + + // Let's see the emojis :-]. + { + const emojis1 = sas1.emoji(); + const emojiIndexes1 = sas1.emojiIndex(); + const emojis2 = sas2.emoji(); + const emojiIndexes2 = sas2.emojiIndex(); + + expect(emojis1).toHaveLength(7); + expect(emojiIndexes1).toHaveLength(emojis1.length); + expect(emojis2).toHaveLength(emojis1.length); + expect(emojiIndexes2).toHaveLength(emojis1.length); + + const isEmoji = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; + + for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(emojis1, emojiIndexes1, emojis2, emojiIndexes2)) { + expect(emoji1).toBeInstanceOf(Emoji); + expect(emoji1.symbol).toMatch(isEmoji); + expect(emoji1.description).toBeTruthy(); + + expect(emojiIndex1).toBeGreaterThanOrEqual(0); + expect(emojiIndex1).toBeLessThanOrEqual(63); + + expect(emoji2).toBeInstanceOf(Emoji); + expect(emoji2.symbol).toStrictEqual(emoji1.symbol); + expect(emoji2.description).toStrictEqual(emoji1.description); + + expect(emojiIndex2).toStrictEqual(emojiIndex1); + } + } + + // Let's see the decimals. + { + const decimals1 = sas1.decimals(); + const decimals2 = sas2.decimals(); + + expect(decimals1).toHaveLength(3); + expect(decimals2).toHaveLength(decimals1.length); + + const isDecimal = /^[0-9]{4}$/; + + for (const [decimal1, decimal2] of zip(decimals1, decimals2)) { + expect(decimal1.toString()).toMatch(isDecimal); + + expect(decimal2).toStrictEqual(decimal1); + } + } + + // Let's confirm the verification: We have a match! { } diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js index 8f84308ca..2e7f54902 100644 --- a/bindings/matrix-sdk-crypto-js/tests/helper.js +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -1,5 +1,13 @@ const { DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest } = require('../pkg/matrix_sdk_crypto_js'); +function* zip(...arrays) { + const len = Math.min(...arrays.map((array) => array.length)); + + for (let nth = 0; nth < len; ++nth) { + yield [...arrays.map((array) => array.at(nth))] + } +} + // Add a machine to another machine, i.e. be sure a machine knows // another exists. async function addMachineToMachine(machineToAdd, machine) { @@ -55,4 +63,7 @@ async function addMachineToMachine(machineToAdd, machine) { } } -module.exports = { addMachineToMachine }; +module.exports = { + zip, + addMachineToMachine, +}; From c471a6fb4d79e2a7487d8b2809e54cb9c87e9cbf Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 14:23:16 +0200 Subject: [PATCH 12/37] test(crypto-js): Split the Key Verification test case into a test suite. --- .../matrix-sdk-crypto-js/tests/device.test.js | 567 +++++++++--------- 1 file changed, 286 insertions(+), 281 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 634f3061d..57c5aee01 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -79,7 +79,7 @@ describe(OlmMachine.name, () => { }); }); -describe(Device.name, () => { +describe('Key Verification', () => { const userId1 = new UserId('@alice:example.org'); const deviceId1 = new DeviceId('alice_device'); @@ -90,12 +90,24 @@ describe(Device.name, () => { return new OlmMachine(new_user || userId1, new_device || deviceId1); } - test('can request verification', async () => { - // First Olm machine. - const m1 = await machine(userId1, deviceId1); - // Second Olm machine. - const m2 = await machine(userId2, deviceId2); + // First Olm machine. + let m1; + // Second Olm machine. + let m2; + + beforeAll(async () => { + m1 = await machine(userId1, deviceId1); + m2 = await machine(userId2, deviceId2); + }); + + // Verification request for `m1`. + let verificationRequest1; + + // Temporary variable. + let outgoingVerificationRequest; + + test('can request verification (`m.key.verification.request`)', async () => { // Make `m1` and `m2` be aware of each other. { await addMachineToMachine(m2, m1); @@ -108,318 +120,311 @@ describe(Device.name, () => { expect(device2).toBeInstanceOf(Device); // Request a verification from `m1` to `device2`. - let [verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification(); + [verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification(); - { - expect(verificationRequest1).toBeInstanceOf(VerificationRequest); + expect(verificationRequest1).toBeInstanceOf(VerificationRequest); - expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); - expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); - expect(verificationRequest1.otherDeviceId).toBeUndefined(); - expect(verificationRequest1.roomId).toBeUndefined(); - expect(verificationRequest1.cancelInfo).toBeUndefined(); - expect(verificationRequest1.isPassive()).toStrictEqual(false); - expect(verificationRequest1.isReady()).toStrictEqual(false); - expect(verificationRequest1.timedOut()).toStrictEqual(false); - expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); - expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); - expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); - expect(verificationRequest1.weStarted()).toStrictEqual(true); - expect(verificationRequest1.isDone()).toStrictEqual(false); - expect(verificationRequest1.isCancelled()).toStrictEqual(false); + expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest1.otherDeviceId).toBeUndefined(); + expect(verificationRequest1.roomId).toBeUndefined(); + expect(verificationRequest1.cancelInfo).toBeUndefined(); + expect(verificationRequest1.isPassive()).toStrictEqual(false); + expect(verificationRequest1.isReady()).toStrictEqual(false); + expect(verificationRequest1.timedOut()).toStrictEqual(false); + expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); + expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest1.weStarted()).toStrictEqual(true); + expect(verificationRequest1.isDone()).toStrictEqual(false); + expect(verificationRequest1.isCancelled()).toStrictEqual(false); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - } + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); + }); - let flowId; + // Verification request for `m2`. + let verificationRequest2; - // Fetch the verification from `m2`. - let verificationRequest2; + // The flow ID. + let flowId; - { - const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; + test('can fetch received request verification', async () => { + const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingContent, - }] - }; + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingContent, + }] + }; - // Let's send the verification request to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + // Let's send the verification request to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - // Oh, a new verification request. - flowId = outgoingContent.transaction_id; - verificationRequest2 = m2.getVerificationRequest(userId1, flowId); + // Oh, a new verification request. + verificationRequest2 = m2.getVerificationRequest(userId1, outgoingContent.transaction_id); - expect(verificationRequest2).toBeInstanceOf(VerificationRequest); + expect(verificationRequest2).toBeInstanceOf(VerificationRequest); - expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString()); - expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString()); - expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(verificationRequest2.roomId).toBeUndefined(); - expect(verificationRequest2.cancelInfo).toBeUndefined(); - expect(verificationRequest2.isPassive()).toStrictEqual(false); - expect(verificationRequest2.isReady()).toStrictEqual(false); - expect(verificationRequest2.timedOut()).toStrictEqual(false); - expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); - expect(verificationRequest2.flowId).toMatch(/^[a-f0-9]+$/); - expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); - expect(verificationRequest2.weStarted()).toStrictEqual(false); - expect(verificationRequest2.isDone()).toStrictEqual(false); - expect(verificationRequest2.isCancelled()).toStrictEqual(false); + expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(verificationRequest2.roomId).toBeUndefined(); + expect(verificationRequest2.cancelInfo).toBeUndefined(); + expect(verificationRequest2.isPassive()).toStrictEqual(false); + expect(verificationRequest2.isReady()).toStrictEqual(false); + expect(verificationRequest2.timedOut()).toStrictEqual(false); + expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); + expect(verificationRequest2.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest2.weStarted()).toStrictEqual(false); + expect(verificationRequest2.isDone()).toStrictEqual(false); + expect(verificationRequest2.isCancelled()).toStrictEqual(false); - const verificationRequests = m2.getVerificationRequests(userId1); - expect(verificationRequests).toHaveLength(1); - expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same - } + const verificationRequests = m2.getVerificationRequests(userId1); + expect(verificationRequests).toHaveLength(1); + expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same + + flowId = verificationRequest2.flowId; + }); + + test('can accept a verification request (`m.key.verification.ready`)', async () => { + // Accept the verification request. + let outgoingVerificationRequest = verificationRequest2.accept(); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); // The request verification is ready. - { - let outgoingVerificationRequest = verificationRequest2.accept(); + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); + // Let's send the verification ready to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + test('verification requests are synchronized and automatically updated', async () => { + expect(verificationRequest1.isReady()).toStrictEqual(true); + expect(verificationRequest2.isReady()).toStrictEqual(true); - // Let's send the verification ready to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - } + expect(verificationRequest1.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - // Verification is ready on both side. - { - expect(verificationRequest1.isReady()).toStrictEqual(true); - expect(verificationRequest2.isReady()).toStrictEqual(true); + expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + }); - expect(verificationRequest1.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - - expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest2.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - } + // SAS verification for the second machine. + let sas2; + test('can start a SAS verification (`m.key.verification.start`)', async () => { // Let's start a SAS verification, from `m2` for example. - let sas2; + [sas2, outgoingVerificationRequest] = await verificationRequest2.startSas(); + expect(sas2).toBeInstanceOf(Sas); - { - let [sas, outgoingVerificationRequest] = await verificationRequest2.startSas(); - sas2 = sas; - expect(sas2).toBeInstanceOf(Sas); + expect(sas2.userId.toString()).toStrictEqual(userId2.toString()); + expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas2.flowId).toStrictEqual(flowId); + expect(sas2.roomId).toBeUndefined(); + expect(sas2.supportsEmoji()).toStrictEqual(false); + expect(sas2.startedFromRequest()).toStrictEqual(true); + expect(sas2.isSelfVerification()).toStrictEqual(false); + expect(sas2.haveWeConfirmed()).toStrictEqual(false); + expect(sas2.hasBeenAccepted()).toStrictEqual(false); + expect(sas2.cancelInfo()).toBeUndefined(); + expect(sas2.weStarted()).toStrictEqual(false); + expect(sas2.timedOut()).toStrictEqual(false); + expect(sas2.canBePresented()).toStrictEqual(false); + expect(sas2.isDone()).toStrictEqual(false); + expect(sas2.isCancelled()).toStrictEqual(false); + expect(sas2.emoji()).toBeUndefined(); + expect(sas2.emojiIndex()).toBeUndefined(); + expect(sas2.decimals()).toBeUndefined(); - expect(sas2.userId.toString()).toStrictEqual(userId2.toString()); - expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString()); - expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString()); - expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(sas2.flowId).toStrictEqual(flowId); - expect(sas2.roomId).toBeUndefined(); - expect(sas2.supportsEmoji()).toStrictEqual(false); - expect(sas2.startedFromRequest()).toStrictEqual(true); - expect(sas2.isSelfVerification()).toStrictEqual(false); - expect(sas2.haveWeConfirmed()).toStrictEqual(false); - expect(sas2.hasBeenAccepted()).toStrictEqual(false); - expect(sas2.cancelInfo()).toBeUndefined(); - expect(sas2.weStarted()).toStrictEqual(false); - expect(sas2.timedOut()).toStrictEqual(false); - expect(sas2.canBePresented()).toStrictEqual(false); - expect(sas2.isDone()).toStrictEqual(false); - expect(sas2.isCancelled()).toStrictEqual(false); - expect(sas2.emoji()).toBeUndefined(); - expect(sas2.emojiIndex()).toBeUndefined(); - expect(sas2.decimals()).toBeUndefined(); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + // Let's send the SAS start to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); - // Let's send the SAS start to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + // SAS verification for the second machine. + let sas1; + + test('can fetch and accept an ongoing SAS verification (`m.key.verification.accept`)', async () => { + // Let's fetch the ongoing SAS verification. + sas1 = await m1.getVerification(userId2, flowId); + + expect(sas1).toBeInstanceOf(Sas); + + expect(sas1.userId.toString()).toStrictEqual(userId1.toString()); + expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas1.flowId).toStrictEqual(flowId); + expect(sas1.roomId).toBeUndefined(); + expect(sas1.startedFromRequest()).toStrictEqual(true); + expect(sas1.isSelfVerification()).toStrictEqual(false); + expect(sas1.haveWeConfirmed()).toStrictEqual(false); + expect(sas1.hasBeenAccepted()).toStrictEqual(false); + expect(sas1.cancelInfo()).toBeUndefined(); + expect(sas1.weStarted()).toStrictEqual(true); + expect(sas1.timedOut()).toStrictEqual(false); + expect(sas1.canBePresented()).toStrictEqual(false); + expect(sas1.isDone()).toStrictEqual(false); + expect(sas1.isCancelled()).toStrictEqual(false); + expect(sas1.emoji()).toBeUndefined(); + expect(sas1.emojiIndex()).toBeUndefined(); + expect(sas1.decimals()).toBeUndefined(); + + // Let's accept thet SAS start request. + let outgoingVerificationRequest = sas1.accept(); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send the SAS accept to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('emojis are supported by both sides', async () => { + expect(sas1.supportsEmoji()).toStrictEqual(true); + expect(sas2.supportsEmoji()).toStrictEqual(true); + }); + + test('one side sends verification key (`m.key.verification.key`)', async () => { + // Let's send the verification keys from `m2` to `m1`. + const outgoingRequests = await m2.outgoingRequests(); + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS key to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m2.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + }); + + test('other side sends back verification key (`m.key.verification.key`)', async () => { + // Let's send the verification keys from `m1` to `m2`. + const outgoingRequests = await m1.outgoingRequests(); + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send te SAS key to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + }); + + test('emojis match from both sides', async () => { + const emojis1 = sas1.emoji(); + const emojiIndexes1 = sas1.emojiIndex(); + const emojis2 = sas2.emoji(); + const emojiIndexes2 = sas2.emojiIndex(); + + expect(emojis1).toHaveLength(7); + expect(emojiIndexes1).toHaveLength(emojis1.length); + expect(emojis2).toHaveLength(emojis1.length); + expect(emojiIndexes2).toHaveLength(emojis1.length); + + const isEmoji = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; + + for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(emojis1, emojiIndexes1, emojis2, emojiIndexes2)) { + expect(emoji1).toBeInstanceOf(Emoji); + expect(emoji1.symbol).toMatch(isEmoji); + expect(emoji1.description).toBeTruthy(); + + expect(emojiIndex1).toBeGreaterThanOrEqual(0); + expect(emojiIndex1).toBeLessThanOrEqual(63); + + expect(emoji2).toBeInstanceOf(Emoji); + expect(emoji2.symbol).toStrictEqual(emoji1.symbol); + expect(emoji2.description).toStrictEqual(emoji1.description); + + expect(emojiIndex2).toStrictEqual(emojiIndex1); } + }); - // Let's accept the SAS start request. - let sas1; + test('decimals match from both sides', async () => { + const decimals1 = sas1.decimals(); + const decimals2 = sas2.decimals(); - { - const sas = await m1.getVerification(userId2, flowId); - sas1 = sas; + expect(decimals1).toHaveLength(3); + expect(decimals2).toHaveLength(decimals1.length); - expect(sas1).toBeInstanceOf(Sas); + const isDecimal = /^[0-9]{4}$/; - expect(sas1.userId.toString()).toStrictEqual(userId1.toString()); - expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString()); - expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString()); - expect(sas1.flowId).toStrictEqual(flowId); - expect(sas1.roomId).toBeUndefined(); - expect(sas1.startedFromRequest()).toStrictEqual(true); - expect(sas1.isSelfVerification()).toStrictEqual(false); - expect(sas1.haveWeConfirmed()).toStrictEqual(false); - expect(sas1.hasBeenAccepted()).toStrictEqual(false); - expect(sas1.cancelInfo()).toBeUndefined(); - expect(sas1.weStarted()).toStrictEqual(true); - expect(sas1.timedOut()).toStrictEqual(false); - expect(sas1.canBePresented()).toStrictEqual(false); - expect(sas1.isDone()).toStrictEqual(false); - expect(sas1.isCancelled()).toStrictEqual(false); - expect(sas1.emoji()).toBeUndefined(); - expect(sas1.emojiIndex()).toBeUndefined(); - expect(sas1.decimals()).toBeUndefined(); + for (const [decimal1, decimal2] of zip(decimals1, decimals2)) { + expect(decimal1.toString()).toMatch(isDecimal); - - let outgoingVerificationRequest = sas1.accept(); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; - - // Let's send the SAS accept to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + expect(decimal2).toStrictEqual(decimal1); } + }); - // Let's see if SAS's state on both side. - { - expect(sas1.supportsEmoji()).toStrictEqual(true); - expect(sas2.supportsEmoji()).toStrictEqual(true); - } - - // Let's send the verification keys. - { - // From `m2` to `m1`. - { - const outgoingRequests = await m2.outgoingRequests(); - let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); - - expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); - const toDeviceRequestId = toDeviceRequest.id; - const toDeviceRequestType = toDeviceRequest.type; - - toDeviceRequest = JSON.parse(toDeviceRequest.body); - expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; - - // Let's send te SAS key to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - - m2.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); - } - - // From `m1` to `m2`. - { - const outgoingRequests = await m1.outgoingRequests(); - let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); - - expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); - const toDeviceRequestId = toDeviceRequest.id; - const toDeviceRequestType = toDeviceRequest.type; - - toDeviceRequest = JSON.parse(toDeviceRequest.body); - expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; - - // Let's send te SAS key to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - - m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); - } - } - - // Let's see the emojis :-]. - { - const emojis1 = sas1.emoji(); - const emojiIndexes1 = sas1.emojiIndex(); - const emojis2 = sas2.emoji(); - const emojiIndexes2 = sas2.emojiIndex(); - - expect(emojis1).toHaveLength(7); - expect(emojiIndexes1).toHaveLength(emojis1.length); - expect(emojis2).toHaveLength(emojis1.length); - expect(emojiIndexes2).toHaveLength(emojis1.length); - - const isEmoji = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; - - for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(emojis1, emojiIndexes1, emojis2, emojiIndexes2)) { - expect(emoji1).toBeInstanceOf(Emoji); - expect(emoji1.symbol).toMatch(isEmoji); - expect(emoji1.description).toBeTruthy(); - - expect(emojiIndex1).toBeGreaterThanOrEqual(0); - expect(emojiIndex1).toBeLessThanOrEqual(63); - - expect(emoji2).toBeInstanceOf(Emoji); - expect(emoji2.symbol).toStrictEqual(emoji1.symbol); - expect(emoji2.description).toStrictEqual(emoji1.description); - - expect(emojiIndex2).toStrictEqual(emojiIndex1); - } - } - - // Let's see the decimals. - { - const decimals1 = sas1.decimals(); - const decimals2 = sas2.decimals(); - - expect(decimals1).toHaveLength(3); - expect(decimals2).toHaveLength(decimals1.length); - - const isDecimal = /^[0-9]{4}$/; - - for (const [decimal1, decimal2] of zip(decimals1, decimals2)) { - expect(decimal1.toString()).toMatch(isDecimal); - - expect(decimal2).toStrictEqual(decimal1); - } - } - - // Let's confirm the verification: We have a match! - { - - } + test('can confirm keys match', async () => { + // We have a match! }); }); From 8948333e1ea513ba26609cd814558ce107e59940 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 15:19:14 +0200 Subject: [PATCH 13/37] test(crypto-js): Test until `m.key.verification.done` \o/. --- bindings/matrix-sdk-crypto-js/src/requests.rs | 20 +-- .../matrix-sdk-crypto-js/src/verification.rs | 52 ++++---- .../matrix-sdk-crypto-js/tests/device.test.js | 119 +++++++++++++++++- 3 files changed, 155 insertions(+), 36 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/requests.rs b/bindings/matrix-sdk-crypto-js/src/requests.rs index 99d02a40c..5f1447a92 100644 --- a/bindings/matrix-sdk-crypto-js/src/requests.rs +++ b/bindings/matrix-sdk-crypto-js/src/requests.rs @@ -273,21 +273,27 @@ impl KeysBackupRequest { macro_rules! request { ($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => { + impl $request { + pub(crate) fn to_json(request: &$ruma_request) -> Result { + let mut map = serde_json::Map::new(); + $( + map.insert(stringify!($field).to_owned(), serde_json::to_value(&request.$field).unwrap()); + )+ + let object = serde_json::Value::Object(map); + + serde_json::to_string(&object) + } + } + impl TryFrom<(String, &$ruma_request)> for $request { type Error = serde_json::Error; fn try_from( (request_id, request): (String, &$ruma_request), ) -> Result { - let mut map = serde_json::Map::new(); - $( - map.insert(stringify!($field).to_owned(), serde_json::to_value(&request.$field).unwrap()); - )+ - let value = serde_json::Value::Object(map); - Ok($request { id: request_id.into(), - body: serde_json::to_string(&value)?.into(), + body: Self::to_json(request)?.into(), }) } } diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 20ffb00fe..9e5cde134 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -162,14 +162,6 @@ impl Sas { self.inner.other_device_id().to_owned().into() } - /* - /// Get the device of the other user. - #[wasm_bindgen(js_name = "otherDevice")] - pub fn other_device(&self) { - todo!() - } - */ - /// Get the unique ID that identifies this SAS verification flow, /// be either a to-device request ID or a room event ID. #[wasm_bindgen(getter, js_name = "flowId")] @@ -244,18 +236,37 @@ impl Sas { .map_err(Into::into) } - /* - #[wasm_bindgen(js_name = "acceptWithSettings")] - pub fn accept_with_settings(&self) { - todo!() - } - */ + /// Confirm the SAS verification. + /// + /// This confirms that the short auth strings match on both sides. + /// + /// Does nothing if we’re not in a state where we can confirm the + /// short auth string, otherwise returns a `MacEventContent` that + /// needs to be sent to the server. + pub fn confirm(&self) -> Promise { + let me = self.inner.clone(); - /* - pub fn confirm(&self) { - todo!() + future_to_promise(async move { + let (outgoing_verification_requests, signature_upload_request) = me.confirm().await?; + let outgoing_verification_requests = outgoing_verification_requests + .into_iter() + .map(OutgoingVerificationRequest) + .map(JsValue::try_from) + .collect::>()?; + + let tuple = Array::new(); + tuple.set(0, outgoing_verification_requests.into()); + tuple.set( + 1, + signature_upload_request + .map(|request| requests::SignatureUploadRequest::to_json(&request)) + .transpose()? + .into(), + ); + + Ok(tuple) + }) } - */ /* pub fn cancel(&self) { @@ -384,11 +395,6 @@ impl Qr { self.inner.other_device_id().to_owned().into() } - /* - #[wasm_bindgen(getter, js_name = "otherDevice")] - pub fn other_device(&self) -> ReadOnlyDevice {} - */ - /// Did we initiate the verification request. #[wasm_bindgen(js_name = "weStarted")] pub fn we_started(&self) -> bool { diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 57c5aee01..314c3a5da 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -216,7 +216,7 @@ describe('Key Verification', () => { await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); }); - test('verification requests are synchronized and automatically updated', async () => { + test('verification requests are synchronized and automatically updated', () => { expect(verificationRequest1.isReady()).toStrictEqual(true); expect(verificationRequest2.isReady()).toStrictEqual(true); @@ -321,7 +321,7 @@ describe('Key Verification', () => { await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); }); - test('emojis are supported by both sides', async () => { + test('emojis are supported by both sides', () => { expect(sas1.supportsEmoji()).toStrictEqual(true); expect(sas2.supportsEmoji()).toStrictEqual(true); }); @@ -378,7 +378,7 @@ describe('Key Verification', () => { m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); }); - test('emojis match from both sides', async () => { + test('emojis match from both sides', () => { const emojis1 = sas1.emoji(); const emojiIndexes1 = sas1.emojiIndex(); const emojis2 = sas2.emoji(); @@ -407,7 +407,7 @@ describe('Key Verification', () => { } }); - test('decimals match from both sides', async () => { + test('decimals match from both sides', () => { const decimals1 = sas1.decimals(); const decimals2 = sas2.decimals(); @@ -423,8 +423,115 @@ describe('Key Verification', () => { } }); - test('can confirm keys match', async () => { - // We have a match! + test('can confirm keys match (`m.key.verification.mac`)', async () => { + // `m1` confirms. + const [outgoingVerificationRequests, signatureUploadRequest] = await sas1.confirm(); + + expect(signatureUploadRequest).toBeUndefined(); + expect(outgoingVerificationRequests).toHaveLength(1); + + let outgoingVerificationRequest = outgoingVerificationRequests[0]; + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send te SAS confirmation to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('can confirm back keys match (`m.key.verification.done`)', async () => { + // `m2` confirms. + const [outgoingVerificationRequests, signatureUploadRequest] = await sas2.confirm(); + + expect(signatureUploadRequest).toBeUndefined(); + expect(outgoingVerificationRequests).toHaveLength(2); + + // `.mac` + { + let outgoingVerificationRequest = outgoingVerificationRequests[0]; + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS confirmation to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + } + + // `.done` + { + let outgoingVerificationRequest = outgoingVerificationRequests[1]; + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS done to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + } + }); + + test('can send final done (`m.key.verification.done`)', async () => { + const outgoingRequests = await m1.outgoingRequests(); + expect(outgoingRequests).toHaveLength(3); + + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.done'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send te SAS key to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + }); + + test('can see if verification is done', () => { + expect(verificationRequest1.isDone()).toStrictEqual(true); + expect(verificationRequest2.isDone()).toStrictEqual(true); + + expect(sas1.isDone()).toStrictEqual(true); + expect(sas2.isDone()).toStrictEqual(true); }); }); From 71a2fac46feb08845a747dd6c2823070d0304dbb Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 15:47:16 +0200 Subject: [PATCH 14/37] test(crypto-js): Reorganize the tests a little bit. --- .../matrix-sdk-crypto-js/tests/device.test.js | 827 +++++++++--------- 1 file changed, 415 insertions(+), 412 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 314c3a5da..037bab109 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -90,374 +90,345 @@ describe('Key Verification', () => { return new OlmMachine(new_user || userId1, new_device || deviceId1); } - // First Olm machine. - let m1; - - // Second Olm machine. - let m2; - - beforeAll(async () => { - m1 = await machine(userId1, deviceId1); - m2 = await machine(userId2, deviceId2); - }); - - // Verification request for `m1`. - let verificationRequest1; - - // Temporary variable. - let outgoingVerificationRequest; - - test('can request verification (`m.key.verification.request`)', async () => { - // Make `m1` and `m2` be aware of each other. - { - await addMachineToMachine(m2, m1); - await addMachineToMachine(m1, m2); - } - - // Pick the device we want to start the verification with. - const device2 = await m1.getDevice(userId2, deviceId2); - - expect(device2).toBeInstanceOf(Device); - - // Request a verification from `m1` to `device2`. - [verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification(); - - expect(verificationRequest1).toBeInstanceOf(VerificationRequest); - - expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); - expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); - expect(verificationRequest1.otherDeviceId).toBeUndefined(); - expect(verificationRequest1.roomId).toBeUndefined(); - expect(verificationRequest1.cancelInfo).toBeUndefined(); - expect(verificationRequest1.isPassive()).toStrictEqual(false); - expect(verificationRequest1.isReady()).toStrictEqual(false); - expect(verificationRequest1.timedOut()).toStrictEqual(false); - expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); - expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); - expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); - expect(verificationRequest1.weStarted()).toStrictEqual(true); - expect(verificationRequest1.isDone()).toStrictEqual(false); - expect(verificationRequest1.isCancelled()).toStrictEqual(false); - - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - }); - - // Verification request for `m2`. - let verificationRequest2; - - // The flow ID. - let flowId; - - test('can fetch received request verification', async () => { - const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingContent, - }] - }; - - // Let's send the verification request to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - - // Oh, a new verification request. - verificationRequest2 = m2.getVerificationRequest(userId1, outgoingContent.transaction_id); - - expect(verificationRequest2).toBeInstanceOf(VerificationRequest); - - expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString()); - expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString()); - expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(verificationRequest2.roomId).toBeUndefined(); - expect(verificationRequest2.cancelInfo).toBeUndefined(); - expect(verificationRequest2.isPassive()).toStrictEqual(false); - expect(verificationRequest2.isReady()).toStrictEqual(false); - expect(verificationRequest2.timedOut()).toStrictEqual(false); - expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); - expect(verificationRequest2.flowId).toMatch(/^[a-f0-9]+$/); - expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); - expect(verificationRequest2.weStarted()).toStrictEqual(false); - expect(verificationRequest2.isDone()).toStrictEqual(false); - expect(verificationRequest2.isCancelled()).toStrictEqual(false); - - const verificationRequests = m2.getVerificationRequests(userId1); - expect(verificationRequests).toHaveLength(1); - expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same - - flowId = verificationRequest2.flowId; - }); - - test('can accept a verification request (`m.key.verification.ready`)', async () => { - // Accept the verification request. - let outgoingVerificationRequest = verificationRequest2.accept(); - - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - - // The request verification is ready. - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; - - // Let's send the verification ready to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - }); - - test('verification requests are synchronized and automatically updated', () => { - expect(verificationRequest1.isReady()).toStrictEqual(true); - expect(verificationRequest2.isReady()).toStrictEqual(true); - - expect(verificationRequest1.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - - expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest2.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - }); - - // SAS verification for the second machine. - let sas2; - - test('can start a SAS verification (`m.key.verification.start`)', async () => { - // Let's start a SAS verification, from `m2` for example. - [sas2, outgoingVerificationRequest] = await verificationRequest2.startSas(); - expect(sas2).toBeInstanceOf(Sas); - - expect(sas2.userId.toString()).toStrictEqual(userId2.toString()); - expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString()); - expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString()); - expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(sas2.flowId).toStrictEqual(flowId); - expect(sas2.roomId).toBeUndefined(); - expect(sas2.supportsEmoji()).toStrictEqual(false); - expect(sas2.startedFromRequest()).toStrictEqual(true); - expect(sas2.isSelfVerification()).toStrictEqual(false); - expect(sas2.haveWeConfirmed()).toStrictEqual(false); - expect(sas2.hasBeenAccepted()).toStrictEqual(false); - expect(sas2.cancelInfo()).toBeUndefined(); - expect(sas2.weStarted()).toStrictEqual(false); - expect(sas2.timedOut()).toStrictEqual(false); - expect(sas2.canBePresented()).toStrictEqual(false); - expect(sas2.isDone()).toStrictEqual(false); - expect(sas2.isCancelled()).toStrictEqual(false); - expect(sas2.emoji()).toBeUndefined(); - expect(sas2.emojiIndex()).toBeUndefined(); - expect(sas2.decimals()).toBeUndefined(); - - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); - - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; - - // Let's send the SAS start to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - }); - - // SAS verification for the second machine. - let sas1; - - test('can fetch and accept an ongoing SAS verification (`m.key.verification.accept`)', async () => { - // Let's fetch the ongoing SAS verification. - sas1 = await m1.getVerification(userId2, flowId); - - expect(sas1).toBeInstanceOf(Sas); - - expect(sas1.userId.toString()).toStrictEqual(userId1.toString()); - expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString()); - expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString()); - expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString()); - expect(sas1.flowId).toStrictEqual(flowId); - expect(sas1.roomId).toBeUndefined(); - expect(sas1.startedFromRequest()).toStrictEqual(true); - expect(sas1.isSelfVerification()).toStrictEqual(false); - expect(sas1.haveWeConfirmed()).toStrictEqual(false); - expect(sas1.hasBeenAccepted()).toStrictEqual(false); - expect(sas1.cancelInfo()).toBeUndefined(); - expect(sas1.weStarted()).toStrictEqual(true); - expect(sas1.timedOut()).toStrictEqual(false); - expect(sas1.canBePresented()).toStrictEqual(false); - expect(sas1.isDone()).toStrictEqual(false); - expect(sas1.isCancelled()).toStrictEqual(false); - expect(sas1.emoji()).toBeUndefined(); - expect(sas1.emojiIndex()).toBeUndefined(); - expect(sas1.decimals()).toBeUndefined(); - - // Let's accept thet SAS start request. - let outgoingVerificationRequest = sas1.accept(); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; - - // Let's send the SAS accept to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - }); - - test('emojis are supported by both sides', () => { - expect(sas1.supportsEmoji()).toStrictEqual(true); - expect(sas2.supportsEmoji()).toStrictEqual(true); - }); - - test('one side sends verification key (`m.key.verification.key`)', async () => { - // Let's send the verification keys from `m2` to `m1`. - const outgoingRequests = await m2.outgoingRequests(); - let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); - - expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); - const toDeviceRequestId = toDeviceRequest.id; - const toDeviceRequestType = toDeviceRequest.type; - - toDeviceRequest = JSON.parse(toDeviceRequest.body); - expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; - - // Let's send te SAS key to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - - m2.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); - }); - - test('other side sends back verification key (`m.key.verification.key`)', async () => { - // Let's send the verification keys from `m1` to `m2`. - const outgoingRequests = await m1.outgoingRequests(); - let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); - - expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); - const toDeviceRequestId = toDeviceRequest.id; - const toDeviceRequestType = toDeviceRequest.type; - - toDeviceRequest = JSON.parse(toDeviceRequest.body); - expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; - - // Let's send te SAS key to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - - m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); - }); - - test('emojis match from both sides', () => { - const emojis1 = sas1.emoji(); - const emojiIndexes1 = sas1.emojiIndex(); - const emojis2 = sas2.emoji(); - const emojiIndexes2 = sas2.emojiIndex(); - - expect(emojis1).toHaveLength(7); - expect(emojiIndexes1).toHaveLength(emojis1.length); - expect(emojis2).toHaveLength(emojis1.length); - expect(emojiIndexes2).toHaveLength(emojis1.length); - - const isEmoji = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; - - for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(emojis1, emojiIndexes1, emojis2, emojiIndexes2)) { - expect(emoji1).toBeInstanceOf(Emoji); - expect(emoji1.symbol).toMatch(isEmoji); - expect(emoji1.description).toBeTruthy(); - - expect(emojiIndex1).toBeGreaterThanOrEqual(0); - expect(emojiIndex1).toBeLessThanOrEqual(63); - - expect(emoji2).toBeInstanceOf(Emoji); - expect(emoji2.symbol).toStrictEqual(emoji1.symbol); - expect(emoji2.description).toStrictEqual(emoji1.description); - - expect(emojiIndex2).toStrictEqual(emojiIndex1); - } - }); - - test('decimals match from both sides', () => { - const decimals1 = sas1.decimals(); - const decimals2 = sas2.decimals(); - - expect(decimals1).toHaveLength(3); - expect(decimals2).toHaveLength(decimals1.length); + describe('SAS', () => { + // First Olm machine. + let m1; + + // Second Olm machine. + let m2; + + beforeAll(async () => { + m1 = await machine(userId1, deviceId1); + m2 = await machine(userId2, deviceId2); + }); + + // Verification request for `m1`. + let verificationRequest1; + + // The flow ID. + let flowId; + + test('can request verification (`m.key.verification.request`)', async () => { + // Make `m1` and `m2` be aware of each other. + { + await addMachineToMachine(m2, m1); + await addMachineToMachine(m1, m2); + } + + // Pick the device we want to start the verification with. + const device2 = await m1.getDevice(userId2, deviceId2); + + expect(device2).toBeInstanceOf(Device); + + let outgoingVerificationRequest; + // Request a verification from `m1` to `device2`. + [verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification(); + + expect(verificationRequest1).toBeInstanceOf(VerificationRequest); + + expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest1.otherDeviceId).toBeUndefined(); + expect(verificationRequest1.roomId).toBeUndefined(); + expect(verificationRequest1.cancelInfo).toBeUndefined(); + expect(verificationRequest1.isPassive()).toStrictEqual(false); + expect(verificationRequest1.isReady()).toStrictEqual(false); + expect(verificationRequest1.timedOut()).toStrictEqual(false); + expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); + expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest1.weStarted()).toStrictEqual(true); + expect(verificationRequest1.isDone()).toStrictEqual(false); + expect(verificationRequest1.isCancelled()).toStrictEqual(false); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); + + const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingContent, + }] + }; + + // Let's send the verification request to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + flowId = outgoingContent.transaction_id; + }); + + // Verification request for `m2`. + let verificationRequest2; + + test('can fetch received request verification', async () => { + // Oh, a new verification request. + verificationRequest2 = m2.getVerificationRequest(userId1, flowId); + + expect(verificationRequest2).toBeInstanceOf(VerificationRequest); + + expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(verificationRequest2.roomId).toBeUndefined(); + expect(verificationRequest2.cancelInfo).toBeUndefined(); + expect(verificationRequest2.isPassive()).toStrictEqual(false); + expect(verificationRequest2.isReady()).toStrictEqual(false); + expect(verificationRequest2.timedOut()).toStrictEqual(false); + expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); + expect(verificationRequest2.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest2.weStarted()).toStrictEqual(false); + expect(verificationRequest2.isDone()).toStrictEqual(false); + expect(verificationRequest2.isCancelled()).toStrictEqual(false); + + const verificationRequests = m2.getVerificationRequests(userId1); + expect(verificationRequests).toHaveLength(1); + expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same + }); + + test('can accept a verification request (`m.key.verification.ready`)', async () => { + // Accept the verification request. + let outgoingVerificationRequest = verificationRequest2.accept(); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + // The request verification is ready. + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send the verification ready to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('verification requests are synchronized and automatically updated', () => { + expect(verificationRequest1.isReady()).toStrictEqual(true); + expect(verificationRequest2.isReady()).toStrictEqual(true); + + expect(verificationRequest1.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + + expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + }); + + // SAS verification for the second machine. + let sas2; + + test('can start a SAS verification (`m.key.verification.start`)', async () => { + // Let's start a SAS verification, from `m2` for example. + [sas2, outgoingVerificationRequest] = await verificationRequest2.startSas(); + expect(sas2).toBeInstanceOf(Sas); + + expect(sas2.userId.toString()).toStrictEqual(userId2.toString()); + expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas2.flowId).toStrictEqual(flowId); + expect(sas2.roomId).toBeUndefined(); + expect(sas2.supportsEmoji()).toStrictEqual(false); + expect(sas2.startedFromRequest()).toStrictEqual(true); + expect(sas2.isSelfVerification()).toStrictEqual(false); + expect(sas2.haveWeConfirmed()).toStrictEqual(false); + expect(sas2.hasBeenAccepted()).toStrictEqual(false); + expect(sas2.cancelInfo()).toBeUndefined(); + expect(sas2.weStarted()).toStrictEqual(false); + expect(sas2.timedOut()).toStrictEqual(false); + expect(sas2.canBePresented()).toStrictEqual(false); + expect(sas2.isDone()).toStrictEqual(false); + expect(sas2.isCancelled()).toStrictEqual(false); + expect(sas2.emoji()).toBeUndefined(); + expect(sas2.emojiIndex()).toBeUndefined(); + expect(sas2.decimals()).toBeUndefined(); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send the SAS start to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + // SAS verification for the second machine. + let sas1; + + test('can fetch and accept an ongoing SAS verification (`m.key.verification.accept`)', async () => { + // Let's fetch the ongoing SAS verification. + sas1 = await m1.getVerification(userId2, flowId); + + expect(sas1).toBeInstanceOf(Sas); + + expect(sas1.userId.toString()).toStrictEqual(userId1.toString()); + expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(sas1.flowId).toStrictEqual(flowId); + expect(sas1.roomId).toBeUndefined(); + expect(sas1.startedFromRequest()).toStrictEqual(true); + expect(sas1.isSelfVerification()).toStrictEqual(false); + expect(sas1.haveWeConfirmed()).toStrictEqual(false); + expect(sas1.hasBeenAccepted()).toStrictEqual(false); + expect(sas1.cancelInfo()).toBeUndefined(); + expect(sas1.weStarted()).toStrictEqual(true); + expect(sas1.timedOut()).toStrictEqual(false); + expect(sas1.canBePresented()).toStrictEqual(false); + expect(sas1.isDone()).toStrictEqual(false); + expect(sas1.isCancelled()).toStrictEqual(false); + expect(sas1.emoji()).toBeUndefined(); + expect(sas1.emojiIndex()).toBeUndefined(); + expect(sas1.decimals()).toBeUndefined(); + + // Let's accept thet SAS start request. + let outgoingVerificationRequest = sas1.accept(); + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send the SAS accept to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('emojis are supported by both sides', () => { + expect(sas1.supportsEmoji()).toStrictEqual(true); + expect(sas2.supportsEmoji()).toStrictEqual(true); + }); + + test('one side sends verification key (`m.key.verification.key`)', async () => { + // Let's send the verification keys from `m2` to `m1`. + const outgoingRequests = await m2.outgoingRequests(); + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS key to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m2.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + }); + + test('other side sends back verification key (`m.key.verification.key`)', async () => { + // Let's send the verification keys from `m1` to `m2`. + const outgoingRequests = await m1.outgoingRequests(); + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }], + }; + + // Let's send te SAS key to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + }); + + test('emojis match from both sides', () => { + const emojis1 = sas1.emoji(); + const emojiIndexes1 = sas1.emojiIndex(); + const emojis2 = sas2.emoji(); + const emojiIndexes2 = sas2.emojiIndex(); + + expect(emojis1).toHaveLength(7); + expect(emojiIndexes1).toHaveLength(emojis1.length); + expect(emojis2).toHaveLength(emojis1.length); + expect(emojiIndexes2).toHaveLength(emojis1.length); + + const isEmoji = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; + + for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(emojis1, emojiIndexes1, emojis2, emojiIndexes2)) { + expect(emoji1).toBeInstanceOf(Emoji); + expect(emoji1.symbol).toMatch(isEmoji); + expect(emoji1.description).toBeTruthy(); + + expect(emojiIndex1).toBeGreaterThanOrEqual(0); + expect(emojiIndex1).toBeLessThanOrEqual(63); + + expect(emoji2).toBeInstanceOf(Emoji); + expect(emoji2.symbol).toStrictEqual(emoji1.symbol); + expect(emoji2.description).toStrictEqual(emoji1.description); + + expect(emojiIndex2).toStrictEqual(emojiIndex1); + } + }); + + test('decimals match from both sides', () => { + const decimals1 = sas1.decimals(); + const decimals2 = sas2.decimals(); + + expect(decimals1).toHaveLength(3); + expect(decimals2).toHaveLength(decimals1.length); - const isDecimal = /^[0-9]{4}$/; + const isDecimal = /^[0-9]{4}$/; - for (const [decimal1, decimal2] of zip(decimals1, decimals2)) { - expect(decimal1.toString()).toMatch(isDecimal); + for (const [decimal1, decimal2] of zip(decimals1, decimals2)) { + expect(decimal1.toString()).toMatch(isDecimal); - expect(decimal2).toStrictEqual(decimal1); - } - }); + expect(decimal2).toStrictEqual(decimal1); + } + }); - test('can confirm keys match (`m.key.verification.mac`)', async () => { - // `m1` confirms. - const [outgoingVerificationRequests, signatureUploadRequest] = await sas1.confirm(); + test('can confirm keys match (`m.key.verification.mac`)', async () => { + // `m1` confirms. + const [outgoingVerificationRequests, signatureUploadRequest] = await sas1.confirm(); - expect(signatureUploadRequest).toBeUndefined(); - expect(outgoingVerificationRequests).toHaveLength(1); + expect(signatureUploadRequest).toBeUndefined(); + expect(outgoingVerificationRequests).toHaveLength(1); - let outgoingVerificationRequest = outgoingVerificationRequests[0]; - - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); - - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; - - // Let's send te SAS confirmation to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - }); - - test('can confirm back keys match (`m.key.verification.done`)', async () => { - // `m2` confirms. - const [outgoingVerificationRequests, signatureUploadRequest] = await sas2.confirm(); - - expect(signatureUploadRequest).toBeUndefined(); - expect(outgoingVerificationRequests).toHaveLength(2); - - // `.mac` - { let outgoingVerificationRequest = outgoingVerificationRequests[0]; expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); @@ -467,71 +438,103 @@ describe('Key Verification', () => { const toDeviceEvents = { events: [{ - sender: userId2.toString(), + sender: userId1.toString(), type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], }], }; - // Let's send te SAS confirmation to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - } + // Let's send te SAS confirmation to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); - // `.done` - { - let outgoingVerificationRequest = outgoingVerificationRequests[1]; + test('can confirm back keys match (`m.key.verification.done`)', async () => { + // `m2` confirms. + const [outgoingVerificationRequests, signatureUploadRequest] = await sas2.confirm(); - expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + expect(signatureUploadRequest).toBeUndefined(); + expect(outgoingVerificationRequests).toHaveLength(2); - outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); - expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); + // `.mac` + { + let outgoingVerificationRequest = outgoingVerificationRequests[0]; + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS confirmation to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + } + + // `.done` + { + let outgoingVerificationRequest = outgoingVerificationRequests[1]; + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send te SAS done to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + } + }); + + test('can send final done (`m.key.verification.done`)', async () => { + const outgoingRequests = await m1.outgoingRequests(); + expect(outgoingRequests).toHaveLength(3); + + let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); + + expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); + const toDeviceRequestId = toDeviceRequest.id; + const toDeviceRequestType = toDeviceRequest.type; + + toDeviceRequest = JSON.parse(toDeviceRequest.body); + expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.done'); const toDeviceEvents = { events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], }], }; - // Let's send te SAS done to `m1`. - await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - } + // Let's send te SAS key to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); + }); + + test('can see if verification is done', () => { + expect(verificationRequest1.isDone()).toStrictEqual(true); + expect(verificationRequest2.isDone()).toStrictEqual(true); + + expect(sas1.isDone()).toStrictEqual(true); + expect(sas2.isDone()).toStrictEqual(true); + }); }); - test('can send final done (`m.key.verification.done`)', async () => { - const outgoingRequests = await m1.outgoingRequests(); - expect(outgoingRequests).toHaveLength(3); - - let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); - - expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest); - const toDeviceRequestId = toDeviceRequest.id; - const toDeviceRequestType = toDeviceRequest.type; - - toDeviceRequest = JSON.parse(toDeviceRequest.body); - expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.done'); - - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; - - // Let's send te SAS key to `m2`. - await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - - m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}'); - }); - - test('can see if verification is done', () => { - expect(verificationRequest1.isDone()).toStrictEqual(true); - expect(verificationRequest2.isDone()).toStrictEqual(true); - - expect(sas1.isDone()).toStrictEqual(true); - expect(sas2.isDone()).toStrictEqual(true); + describe('QR Code', () => { }); }); From e8331cc40c13b4b1b7caa48a64ab6e841407d4c5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 17:56:34 +0200 Subject: [PATCH 15/37] feat(crypto-js): Enable the `qrcode` feature by default. --- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index c0f32df9a..9d41f49be 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -21,7 +21,7 @@ wasm-opt = ['-Oz'] crate-type = ["cdylib"] [features] -default = ["tracing"] +default = ["tracing", "qrcode"] qrcode = ["matrix-sdk-crypto/qrcode", "matrix-sdk-qrcode"] tracing = [] From 1be17d354d129ce92668c6bbecaf95996581e973 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Sep 2022 17:57:17 +0200 Subject: [PATCH 16/37] feat(crypto-js): Implement `OlmMachine.(export|import)_cross_signing_keys`. --- bindings/matrix-sdk-crypto-js/src/lib.rs | 1 + bindings/matrix-sdk-crypto-js/src/machine.rs | 32 ++++++++++++++++- bindings/matrix-sdk-crypto-js/src/store.rs | 38 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 bindings/matrix-sdk-crypto-js/src/store.rs diff --git a/bindings/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs index 61a613630..e444a661d 100644 --- a/bindings/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -27,6 +27,7 @@ pub mod machine; pub mod olm; pub mod requests; pub mod responses; +pub mod store; pub mod sync_events; mod tracing; pub mod types; diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index b49a8a571..97df77199 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -13,7 +13,7 @@ use crate::{ identifiers, olm, requests, requests::OutgoingRequest, responses::{self, response_from_string}, - sync_events, types, verification, vodozemac, + store, sync_events, types, verification, vodozemac, }; /// State machine implementation of the Olm/Megolm encryption protocol @@ -353,6 +353,36 @@ impl OlmMachine { }) } + /// Export all the private cross signing keys we have. + /// + /// The export will contain the seed for the ed25519 keys as a + /// unpadded base64 encoded string. + /// + /// This method returns None if we don’t have any private cross + /// signing keys. + #[wasm_bindgen(js_name = "exportCrossSigningKeys")] + pub fn export_cross_signing_keys(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + Ok(me.export_cross_signing_keys().await.map(store::CrossSigningKeyExport::from)) + }) + } + + /// Import our private cross signing keys. + /// + /// The export needs to contain the seed for the ed25519 keys as + /// an unpadded base64 encoded string. + #[wasm_bindgen(js_name = "importCrossSigningKeys")] + pub fn import_cross_signing_keys(&self, export: store::CrossSigningKeyExport) -> Promise { + let me = self.inner.clone(); + let export = export.inner; + + future_to_promise(async move { + Ok(me.import_cross_signing_keys(export).await.map(olm::CrossSigningStatus::from)?) + }) + } + /// Sign the given message using our device key and if available /// cross-signing master key. pub fn sign(&self, message: String) -> Promise { diff --git a/bindings/matrix-sdk-crypto-js/src/store.rs b/bindings/matrix-sdk-crypto-js/src/store.rs new file mode 100644 index 000000000..eabae3f55 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/store.rs @@ -0,0 +1,38 @@ +//! Store types. + +use wasm_bindgen::prelude::*; + +/// A struct containing private cross signing keys that can be backed +/// up or uploaded to the secret store. +#[wasm_bindgen] +#[derive(Debug)] +pub struct CrossSigningKeyExport { + pub(crate) inner: matrix_sdk_crypto::store::CrossSigningKeyExport, +} + +impl From for CrossSigningKeyExport { + fn from(inner: matrix_sdk_crypto::store::CrossSigningKeyExport) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl CrossSigningKeyExport { + /// The seed of the master key encoded as unpadded base64. + #[wasm_bindgen(getter, js_name = "masterKey")] + pub fn master_key(&self) -> Option { + self.inner.master_key.clone() + } + + /// The seed of the self signing key encoded as unpadded base64. + #[wasm_bindgen(getter, js_name = "self_signing_key")] + pub fn self_signing_key(&self) -> Option { + self.inner.self_signing_key.clone() + } + + /// The seed of the user signing key encoded as unpadded base64. + #[wasm_bindgen(getter, js_name = "userSigningKey")] + pub fn user_signing_key(&self) -> Option { + self.inner.user_signing_key.clone() + } +} From e367d8574d6b7b06231c069664d5b0ab92b78fe6 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 14:00:54 +0200 Subject: [PATCH 17/37] feat(crypto-js): Implement `OlmMachine.bootstrap_cross_signing`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 30 +++++ bindings/matrix-sdk-crypto-js/src/requests.rs | 106 ++++++++++++------ .../matrix-sdk-crypto-js/src/verification.rs | 2 +- 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 97df77199..fa4aeefd1 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -383,6 +383,36 @@ impl OlmMachine { }) } + /// Create a new cross signing identity and get the upload request + /// to push the new public keys to the server. + /// + /// Warning: This will delete any existing cross signing keys that + /// might exist on the server and thus will reset the trust + /// between all the devices. + /// + /// Uploading these keys will require user interactive auth. + #[wasm_bindgen(js_name = "bootstrapCrossSigning")] + pub fn bootstrap_cross_signing(&self, reset: bool) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + let (upload_signing_keys_request, upload_signatures_request) = + me.bootstrap_cross_signing(reset).await?; + + let tuple = Array::new(); + tuple.set( + 0, + requests::SigningKeysUploadRequest::try_from(&upload_signing_keys_request)?.into(), + ); + tuple.set( + 1, + requests::SignatureUploadRequest::try_from(&upload_signatures_request)?.into(), + ); + + Ok(tuple) + }) + } + /// Sign the given message using our device key and if available /// cross-signing master key. pub fn sign(&self, message: String) -> Promise { diff --git a/bindings/matrix-sdk-crypto-js/src/requests.rs b/bindings/matrix-sdk-crypto-js/src/requests.rs index 5f1447a92..75b989cf3 100644 --- a/bindings/matrix-sdk-crypto-js/src/requests.rs +++ b/bindings/matrix-sdk-crypto-js/src/requests.rs @@ -3,18 +3,23 @@ use js_sys::JsString; use matrix_sdk_crypto::{ requests::{ - KeysBackupRequest as RumaKeysBackupRequest, KeysQueryRequest as RumaKeysQueryRequest, - RoomMessageRequest as RumaRoomMessageRequest, ToDeviceRequest as RumaToDeviceRequest, + KeysBackupRequest as OriginalKeysBackupRequest, + KeysQueryRequest as OriginalKeysQueryRequest, + RoomMessageRequest as OriginalRoomMessageRequest, + ToDeviceRequest as OriginalToDeviceRequest, + UploadSigningKeysRequest as OriginalUploadSigningKeysRequest, }, OutgoingRequests, }; use ruma::api::client::keys::{ - claim_keys::v3::Request as RumaKeysClaimRequest, - upload_keys::v3::Request as RumaKeysUploadRequest, - upload_signatures::v3::Request as RumaSignatureUploadRequest, + claim_keys::v3::Request as OriginalKeysClaimRequest, + upload_keys::v3::Request as OriginalKeysUploadRequest, + upload_signatures::v3::Request as OriginalSignatureUploadRequest, }; use wasm_bindgen::prelude::*; +/** Outgoing Requests * */ + /// Data for a request to the `/keys/upload` API endpoint /// ([specification]). /// @@ -26,7 +31,7 @@ use wasm_bindgen::prelude::*; pub struct KeysUploadRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -42,7 +47,7 @@ impl KeysUploadRequest { /// Create a new `KeysUploadRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> KeysUploadRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -63,7 +68,7 @@ impl KeysUploadRequest { pub struct KeysQueryRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -79,7 +84,7 @@ impl KeysQueryRequest { /// Create a new `KeysQueryRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> KeysQueryRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -101,7 +106,7 @@ impl KeysQueryRequest { pub struct KeysClaimRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -117,7 +122,7 @@ impl KeysClaimRequest { /// Create a new `KeysClaimRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> KeysClaimRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -138,7 +143,7 @@ impl KeysClaimRequest { pub struct ToDeviceRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -154,7 +159,7 @@ impl ToDeviceRequest { /// Create a new `ToDeviceRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> ToDeviceRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -175,7 +180,7 @@ impl ToDeviceRequest { pub struct SignatureUploadRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -191,7 +196,7 @@ impl SignatureUploadRequest { /// Create a new `SignatureUploadRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> SignatureUploadRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -210,7 +215,7 @@ impl SignatureUploadRequest { pub struct RoomMessageRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -226,7 +231,7 @@ impl RoomMessageRequest { /// Create a new `RoomMessageRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> RoomMessageRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -245,7 +250,7 @@ impl RoomMessageRequest { pub struct KeysBackupRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub id: JsString, + pub id: Option, /// A JSON-encoded object of form: /// @@ -256,12 +261,32 @@ pub struct KeysBackupRequest { pub body: JsString, } +/** Other Requests * */ + +/// Request that will publish a cross signing identity. +/// +/// This uploads the public cross signing key triplet. +#[wasm_bindgen(getter_with_clone)] +pub struct SigningKeysUploadRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub id: Option, + + /// A JSON-encoded object of form: + /// + /// ```json + /// {"master_key", …, "self_signing_key": …, "user_signing_key": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + #[wasm_bindgen] impl KeysBackupRequest { /// Create a new `KeysBackupRequest`. #[wasm_bindgen(constructor)] pub fn new(id: JsString, body: JsString) -> KeysBackupRequest { - Self { id, body } + Self { id: Some(id), body } } /// Get its request type. @@ -272,9 +297,9 @@ impl KeysBackupRequest { } macro_rules! request { - ($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => { - impl $request { - pub(crate) fn to_json(request: &$ruma_request) -> Result { + ($destination_request:ident from $source_request:ident maps fields $( $field:ident ),+ $(,)? ) => { + impl $destination_request { + pub(crate) fn to_json(request: &$source_request) -> Result { let mut map = serde_json::Map::new(); $( map.insert(stringify!($field).to_owned(), serde_json::to_value(&request.$field).unwrap()); @@ -285,14 +310,25 @@ macro_rules! request { } } - impl TryFrom<(String, &$ruma_request)> for $request { + impl TryFrom<&$source_request> for $destination_request { + type Error = serde_json::Error; + + fn try_from(request: &$source_request) -> Result { + Ok($destination_request { + id: None, + body: Self::to_json(request)?.into(), + }) + } + } + + impl TryFrom<(String, &$source_request)> for $destination_request { type Error = serde_json::Error; fn try_from( - (request_id, request): (String, &$ruma_request), + (request_id, request): (String, &$source_request), ) -> Result { - Ok($request { - id: request_id.into(), + Ok($destination_request { + id: Some(request_id.into()), body: Self::to_json(request)?.into(), }) } @@ -300,13 +336,17 @@ macro_rules! request { }; } -request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys); -request!(KeysQueryRequest from RumaKeysQueryRequest maps fields timeout, device_keys, token); -request!(KeysClaimRequest from RumaKeysClaimRequest maps fields timeout, one_time_keys); -request!(ToDeviceRequest from RumaToDeviceRequest maps fields event_type, txn_id, messages); -request!(SignatureUploadRequest from RumaSignatureUploadRequest maps fields signed_keys); -request!(RoomMessageRequest from RumaRoomMessageRequest maps fields room_id, txn_id, content); -request!(KeysBackupRequest from RumaKeysBackupRequest maps fields rooms); +// Outgoing Requests +request!(KeysUploadRequest from OriginalKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys); +request!(KeysQueryRequest from OriginalKeysQueryRequest maps fields timeout, device_keys, token); +request!(KeysClaimRequest from OriginalKeysClaimRequest maps fields timeout, one_time_keys); +request!(ToDeviceRequest from OriginalToDeviceRequest maps fields event_type, txn_id, messages); +request!(SignatureUploadRequest from OriginalSignatureUploadRequest maps fields signed_keys); +request!(RoomMessageRequest from OriginalRoomMessageRequest maps fields room_id, txn_id, content); +request!(KeysBackupRequest from OriginalKeysBackupRequest maps fields rooms); + +// Other Requests +request!(SigningKeysUploadRequest from OriginalUploadSigningKeysRequest maps fields master_key, self_signing_key, user_signing_key); // JavaScript has no complex enums like Rust. To return structs of // different types, we have no choice that hiding everything behind a diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 9e5cde134..ddfa52a9f 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -259,7 +259,7 @@ impl Sas { tuple.set( 1, signature_upload_request - .map(|request| requests::SignatureUploadRequest::to_json(&request)) + .map(|request| requests::SignatureUploadRequest::try_from(&request)) .transpose()? .into(), ); From bbfc076c7f6d75ad6fa7d9bfd77213fe14be0901 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 14:01:25 +0200 Subject: [PATCH 18/37] test(crypto-js): Inject bootstrap cross signing keys when setting up machines. --- bindings/matrix-sdk-crypto-js/tests/helper.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js index 2e7f54902..1ced03d1a 100644 --- a/bindings/matrix-sdk-crypto-js/tests/helper.js +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -51,12 +51,23 @@ async function addMachineToMachine(machineToAdd, machine) { { expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest); + let [signingKeysUploadRequest, _] = await machineToAdd.bootstrapCrossSigning(true); + signingKeysUploadRequest = JSON.parse(signingKeysUploadRequest.body); + // Let's forge a `KeysQuery`'s response. - let keyQueryResponse = {'device_keys': {}}; + let keyQueryResponse = { + device_keys: {}, + master_keys: {}, + self_signing_keys: {}, + user_signing_keys: {}, + }; const userId = machineToAdd.userId.toString(); const deviceId = machineToAdd.deviceId.toString(); - keyQueryResponse['device_keys'][userId] = {}; - keyQueryResponse['device_keys'][userId][deviceId] = keysUploadRequest.device_keys; + keyQueryResponse.device_keys[userId] = {}; + keyQueryResponse.device_keys[userId][deviceId] = keysUploadRequest.device_keys; + keyQueryResponse.master_keys[userId] = signingKeysUploadRequest.master_key; + keyQueryResponse.self_signing_keys[userId] = signingKeysUploadRequest.self_signing_key; + keyQueryResponse.user_signing_keys[userId] = signingKeysUploadRequest.user_signing_key; const marked = await machine.markRequestAsSent(outgoingRequests[1].id, outgoingRequests[1].type, JSON.stringify(keyQueryResponse)); expect(marked).toStrictEqual(true); From 0d57983e1b391684c02ec4f08bb14402dc7d6583 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 16:30:01 +0200 Subject: [PATCH 19/37] feat(crypto-js): Implement `VerificationRequest.generate_qr_code`. --- .../matrix-sdk-crypto-js/src/verification.rs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index ddfa52a9f..6f121fa7f 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -1,4 +1,10 @@ ///! Different verification types. + +#[cfg(feature = "qrcode")] +use std::fmt; + +#[cfg(feature = "qrcode")] +use js_sys::Uint8Array; use js_sys::{Array, JsString, Promise}; use ruma::events::key::verification::{ cancel::CancelCode as RumaCancelCode, VerificationMethod as RumaVerificationMethod, @@ -357,6 +363,13 @@ pub struct Qr { inner: matrix_sdk_crypto::QrVerification, } +#[cfg(feature = "qrcode")] +impl From for Qr { + fn from(inner: matrix_sdk_crypto::QrVerification) -> Self { + Self { inner } + } +} + #[cfg(feature = "qrcode")] #[wasm_bindgen] impl Qr { @@ -923,7 +936,21 @@ impl VerificationRequest { } }) } - // generate_qr_code if `qrcode` + + /// Generate a QR code that can be used by another client to start + /// a QR code based verification. + #[cfg(feature = "qrcode")] + #[wasm_bindgen(js_name = "generateQrCode")] + pub fn generate_qr_code(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + let qrcode_verification = me.generate_qr_code().await?; + + Ok(qrcode_verification.map(Qr::from)) + }) + } + // scan_qr_code if `qrcode` } From c7e0b3ee318fb9f67a1945e1b4e5014c44b463da Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 16:30:29 +0200 Subject: [PATCH 20/37] test(crypto-js): Testing key verification workflow until QR code generation. --- .../matrix-sdk-crypto-js/tests/device.test.js | 202 ++++++++++++++++-- 1 file changed, 189 insertions(+), 13 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 037bab109..bdf6227d1 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -1,4 +1,5 @@ -const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest, Sas, Emoji } = require('../pkg/matrix_sdk_crypto_js'); +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest, Sas, Emoji, SigningKeysUploadRequest, SignatureUploadRequest, Qr, QrCode } = require('../pkg/matrix_sdk_crypto_js'); +const { LoggerLevel, Tracing } = require('../pkg/matrix_sdk_crypto_js'); const { zip, addMachineToMachine } = require('./helper'); describe('LocalTrust', () => { @@ -135,7 +136,7 @@ describe('Key Verification', () => { expect(verificationRequest1.isReady()).toStrictEqual(false); expect(verificationRequest1.timedOut()).toStrictEqual(false); expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); - expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); expect(verificationRequest1.weStarted()).toStrictEqual(true); @@ -147,20 +148,18 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - const outgoingContent = outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()]; - const toDeviceEvents = { events: [{ sender: userId1.toString(), type: outgoingVerificationRequest.event_type, - content: outgoingContent, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], }] }; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); - flowId = outgoingContent.transaction_id; + flowId = verificationRequest1.flowId; }); // Verification request for `m2`. @@ -180,9 +179,9 @@ describe('Key Verification', () => { expect(verificationRequest2.isPassive()).toStrictEqual(false); expect(verificationRequest2.isReady()).toStrictEqual(false); expect(verificationRequest2.timedOut()).toStrictEqual(false); - expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); - expect(verificationRequest2.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest2.flowId).toStrictEqual(flowId); expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); expect(verificationRequest2.weStarted()).toStrictEqual(false); expect(verificationRequest2.isDone()).toStrictEqual(false); @@ -219,11 +218,11 @@ describe('Key Verification', () => { expect(verificationRequest1.isReady()).toStrictEqual(true); expect(verificationRequest2.isReady()).toStrictEqual(true); - expect(verificationRequest1.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest1.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest1.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); + expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); - expect(verificationRequest2.theirSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); - expect(verificationRequest2.ourSupportedMethods).toStrictEqual([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]); + expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); + expect(verificationRequest2.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); }); // SAS verification for the second machine. @@ -500,7 +499,7 @@ describe('Key Verification', () => { test('can send final done (`m.key.verification.done`)', async () => { const outgoingRequests = await m1.outgoingRequests(); - expect(outgoingRequests).toHaveLength(3); + expect(outgoingRequests).toHaveLength(4); let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice); @@ -535,6 +534,183 @@ describe('Key Verification', () => { }); describe('QR Code', () => { + if (undefined === Qr) { + // qrcode supports is not enabled + console.info('qrcode support is disabled, skip the associated test suite'); + + return; + } + + // First Olm machine. + let m1; + + // Second Olm machine. + let m2; + + beforeAll(async () => { + m1 = await machine(userId1, deviceId1); + m2 = await machine(userId2, deviceId2); + }); + + // Verification request for `m1`. + let verificationRequest1; + + // The flow ID. + let flowId; + + test('can request verification (`m.key.verification.request`)', async () => { + // Make `m1` and `m2` be aware of each other. + { + await addMachineToMachine(m2, m1); + await addMachineToMachine(m1, m2); + } + + // Pick the device we want to start the verification with. + const device2 = await m1.getDevice(userId2, deviceId2); + + expect(device2).toBeInstanceOf(Device); + + let outgoingVerificationRequest; + // Request a verification from `m1` to `device2`. + [verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification([ + VerificationMethod.QrCodeScanV1, // by default + VerificationMethod.QrCodeShowV1, // the one we add + ]); + + expect(verificationRequest1).toBeInstanceOf(VerificationRequest); + + expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest1.otherDeviceId).toBeUndefined(); + expect(verificationRequest1.roomId).toBeUndefined(); + expect(verificationRequest1.cancelInfo).toBeUndefined(); + expect(verificationRequest1.isPassive()).toStrictEqual(false); + expect(verificationRequest1.isReady()).toStrictEqual(false); + expect(verificationRequest1.timedOut()).toStrictEqual(false); + expect(verificationRequest1.theirSupportedMethods).toBeUndefined(); + expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeShowV1])); + expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/); + expect(verificationRequest1.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest1.weStarted()).toStrictEqual(true); + expect(verificationRequest1.isDone()).toStrictEqual(false); + expect(verificationRequest1.isCancelled()).toStrictEqual(false); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }] + }; + + // Let's send the verification request to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + + flowId = verificationRequest1.flowId; + }); + + // Verification request for `m2`. + let verificationRequest2; + + test('can fetch received request verification', async () => { + // Oh, a new verification request. + verificationRequest2 = m2.getVerificationRequest(userId1, flowId); + + expect(verificationRequest2).toBeInstanceOf(VerificationRequest); + + expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString()); + expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(verificationRequest2.roomId).toBeUndefined(); + expect(verificationRequest2.cancelInfo).toBeUndefined(); + expect(verificationRequest2.isPassive()).toStrictEqual(false); + expect(verificationRequest2.isReady()).toStrictEqual(false); + expect(verificationRequest2.timedOut()).toStrictEqual(false); + expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1])); + expect(verificationRequest2.ourSupportedMethods).toBeUndefined(); + expect(verificationRequest2.flowId).toStrictEqual(flowId); + expect(verificationRequest2.isSelfVerification()).toStrictEqual(false); + expect(verificationRequest2.weStarted()).toStrictEqual(false); + expect(verificationRequest2.isDone()).toStrictEqual(false); + expect(verificationRequest2.isCancelled()).toStrictEqual(false); + + const verificationRequests = m2.getVerificationRequests(userId1); + expect(verificationRequests).toHaveLength(1); + expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same + }); + + test('can accept a verification request with methods (`m.key.verification.ready`)', async () => { + // Accept the verification request. + let outgoingVerificationRequest = verificationRequest2.acceptWithMethods([ + VerificationMethod.QrCodeScanV1, // by default + VerificationMethod.QrCodeShowV1, // the one we add + ]); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + // The request verification is ready. + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }], + }; + + // Let's send the verification ready to `m1`. + await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('verification requests are synchronized and automatically updated', () => { + expect(verificationRequest1.isReady()).toStrictEqual(true); + expect(verificationRequest2.isReady()).toStrictEqual(true); + + expect(verificationRequest1.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1])); + expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1])); + + expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1])); + expect(verificationRequest2.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1])); + }); + + // QR verification for the second machine. + let qr2; + + test('can generate a QR code', async () => { + qr2 = await verificationRequest2.generateQrCode(); + + expect(qr2).toBeInstanceOf(Qr); + + expect(qr2.hasBeenScanned()).toStrictEqual(false); + expect(qr2.hasBeenConfirmed()).toStrictEqual(false); + expect(qr2.userId.toString()).toStrictEqual(userId2.toString()); + expect(qr2.otherUserId.toString()).toStrictEqual(userId1.toString()); + expect(qr2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString()); + expect(qr2.weStarted()).toStrictEqual(false); + expect(qr2.cancelInfo()).toBeUndefined(); + expect(qr2.isDone()).toStrictEqual(false); + expect(qr2.isCancelled()).toStrictEqual(false); + expect(qr2.isSelfVerification()).toStrictEqual(false); + expect(qr2.reciprocated()).toStrictEqual(false); + expect(qr2.reciprocated()).toStrictEqual(false); + expect(qr2.flowId).toMatch(/^[a-f0-9]+$/); + expect(qr2.roomId).toBeUndefined(); + expect(qr2.toQrCode()).toBeInstanceOf(QrCode); + expect(qr2.toBytes()).toBeInstanceOf(Array); + }); + + test('can read QR code\'s bytes', async () => { + const bytes = qr2.toBytes(); + + expect(bytes).toHaveLength(122); + }); }); }); From 6a05f834a9dd3ae8cabbf2768113a898208715f5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 16:40:03 +0200 Subject: [PATCH 21/37] test(crypto-js): Test `Qr.toBytes`. --- bindings/matrix-sdk-crypto-js/tests/device.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index bdf6227d1..8711f681f 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -710,6 +710,7 @@ describe('Key Verification', () => { const bytes = qr2.toBytes(); expect(bytes).toHaveLength(122); + expect(bytes.slice(0, 10)).toStrictEqual([77, 65, 84, 82, 73, 88, 2, 0, 0, 32]); }); }); }); From cbb508083740357c66c9490ca6041334f1e49d8d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 17:48:52 +0200 Subject: [PATCH 22/37] test(crypto-js): Properly test `Qr.toBytes`. --- bindings/matrix-sdk-crypto-js/tests/device.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 8711f681f..392cfa73c 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -707,10 +707,13 @@ describe('Key Verification', () => { }); test('can read QR code\'s bytes', async () => { + const qrCodeHeader = 'MATRIX'; + const qrCodeVersion = '\x02'; + const bytes = qr2.toBytes(); expect(bytes).toHaveLength(122); - expect(bytes.slice(0, 10)).toStrictEqual([77, 65, 84, 82, 73, 88, 2, 0, 0, 32]); + expect(bytes.slice(0, 7)).toStrictEqual([...qrCodeHeader, ...qrCodeVersion].map(char => char.charCodeAt(0))); }); }); }); From 6f89d025998af257e3f35f83f44f48a5a22ae5ec Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 17:55:57 +0200 Subject: [PATCH 23/37] test(crypto-js): Test `QrCode.render_into_buffer`. --- .../matrix-sdk-crypto-js/src/verification.rs | 1 + .../matrix-sdk-crypto-js/tests/device.test.js | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 6f121fa7f..3613f10e5 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -663,6 +663,7 @@ impl From for QrCode { impl QrCode { /// Render the QR code into a `Uint8Array` where 1 represents a /// dark pixel and 0 a white pixel. + #[wasm_bindgen(js_name = "renderIntoBuffer")] pub fn render_into_buffer(&self) -> Result { let colors: Vec = self.inner.to_colors().into_iter().map(|color| color.select(1u8, 0u8)).collect(); diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 392cfa73c..28dfd1fa4 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -702,8 +702,6 @@ describe('Key Verification', () => { expect(qr2.reciprocated()).toStrictEqual(false); expect(qr2.flowId).toMatch(/^[a-f0-9]+$/); expect(qr2.roomId).toBeUndefined(); - expect(qr2.toQrCode()).toBeInstanceOf(QrCode); - expect(qr2.toBytes()).toBeInstanceOf(Array); }); test('can read QR code\'s bytes', async () => { @@ -715,6 +713,22 @@ describe('Key Verification', () => { expect(bytes).toHaveLength(122); expect(bytes.slice(0, 7)).toStrictEqual([...qrCodeHeader, ...qrCodeVersion].map(char => char.charCodeAt(0))); }); + + test('can render QR code', async () => { + const qrCode = qr2.toQrCode(); + + expect(qrCode).toBeInstanceOf(QrCode); + + const buffer = qrCode.renderIntoBuffer(); + + expect(buffer).toBeInstanceOf(Uint8Array); + + // 45px ⨉ 45px + expect(buffer).toHaveLength(45 * 45); + + // 0 for a white pixel, 1 for a black pixel. + expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true); + }); }); }); From 9834b67bd527fa3f36c4483a3bd4b47510f9524d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 8 Sep 2022 18:00:53 +0200 Subject: [PATCH 24/37] feat(crypto-js): `QrCode.renderIntoBuffer` returns an `Uint8ClampedArray`. --- bindings/matrix-sdk-crypto-js/src/verification.rs | 8 ++++---- bindings/matrix-sdk-crypto-js/tests/device.test.js | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 3613f10e5..4ec3cd8a0 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -4,7 +4,7 @@ use std::fmt; #[cfg(feature = "qrcode")] -use js_sys::Uint8Array; +use js_sys::Uint8ClampedArray; use js_sys::{Array, JsString, Promise}; use ruma::events::key::verification::{ cancel::CancelCode as RumaCancelCode, VerificationMethod as RumaVerificationMethod, @@ -661,13 +661,13 @@ impl From for QrCode { #[cfg(feature = "qrcode")] #[wasm_bindgen] impl QrCode { - /// Render the QR code into a `Uint8Array` where 1 represents a + /// Render the QR code into a `Uint8ClampedArray` where 1 represents a /// dark pixel and 0 a white pixel. #[wasm_bindgen(js_name = "renderIntoBuffer")] - pub fn render_into_buffer(&self) -> Result { + pub fn render_into_buffer(&self) -> Result { let colors: Vec = self.inner.to_colors().into_iter().map(|color| color.select(1u8, 0u8)).collect(); - let buffer = Uint8Array::new_with_length(colors.len().try_into()?); + let buffer = Uint8ClampedArray::new_with_length(colors.len().try_into()?); buffer.copy_from(colors.as_slice()); Ok(buffer) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 28dfd1fa4..441134936 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -721,11 +721,9 @@ describe('Key Verification', () => { const buffer = qrCode.renderIntoBuffer(); - expect(buffer).toBeInstanceOf(Uint8Array); - + expect(buffer).toBeInstanceOf(Uint8ClampedArray); // 45px ⨉ 45px expect(buffer).toHaveLength(45 * 45); - // 0 for a white pixel, 1 for a black pixel. expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true); }); From 581c53739693f54150fcef25fc0fdd4119c2a7b0 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 09:36:19 +0200 Subject: [PATCH 25/37] test(crypto-js): Properly test `Qr.toQrCode. --- bindings/matrix-sdk-crypto-js/package.json | 9 +-- .../matrix-sdk-crypto-js/tests/device.test.js | 59 +++++++++++++++++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/package.json b/bindings/matrix-sdk-crypto-js/package.json index ed439f237..02f1108c7 100644 --- a/bindings/matrix-sdk-crypto-js/package.json +++ b/bindings/matrix-sdk-crypto-js/package.json @@ -26,12 +26,13 @@ "pkg/matrix_sdk_crypto.d.ts" ], "devDependencies": { - "wasm-pack": "^0.10.2", + "canvas": "^2.10.1", + "cross-env": "^7.0.3", + "fake-indexeddb": "^4.0", "jest": "^28.1.0", "typedoc": "^0.22.17", - "cross-env": "^7.0.3", - "yargs-parser": "~21.0.1", - "fake-indexeddb": "^4.0" + "wasm-pack": "^0.10.2", + "yargs-parser": "~21.0.1" }, "engines": { "node": ">= 10" diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 441134936..ace7e8715 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -719,13 +719,60 @@ describe('Key Verification', () => { expect(qrCode).toBeInstanceOf(QrCode); - const buffer = qrCode.renderIntoBuffer(); + let canvasBuffer; - expect(buffer).toBeInstanceOf(Uint8ClampedArray); - // 45px ⨉ 45px - expect(buffer).toHaveLength(45 * 45); - // 0 for a white pixel, 1 for a black pixel. - expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true); + { + const buffer = qrCode.renderIntoBuffer(); + + expect(buffer).toBeInstanceOf(Uint8ClampedArray); + // 45px ⨉ 45px + expect(buffer).toHaveLength(45 * 45); + // 0 for a white pixel, 1 for a black pixel. + expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true); + + const { Canvas } = require('canvas'); + const canvas = new Canvas(45, 45); + + const context = canvas.getContext('2d'); + // New image data, filled with black, transparent pixels. + const imageData = context.createImageData(45, 45); + const data = imageData.data; + + for ( + let dataNth = 0, + bufferNth = 0; + dataNth < data.length && bufferNth < buffer.length; + dataNth += 4, + bufferNth += 1 + ) { + // White pixel + if (buffer[bufferNth] == 0) { + data[dataNth] = 255; + data[dataNth + 1] = 255; + data[dataNth + 2] = 255; + data[dataNth + 3] = 255; + } + } + + context.putImageData(imageData, 0, 0); + canvasBuffer = canvas.toBuffer('image/png'); + } + + // Want to see the QR code? Uncomment the following block. + /* + { + const fs = require('fs/promises'); + const path = require('path'); + const os = require('os'); + + const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--')); + const qrCodeFile = path.join(tempDirectory, 'qrcode.png'); + + console.log(`View the QR code at \`${qrCodeFile}\`.`); + + expect(await fs.writeFile(qrCodeFile, canvasBuffer)).toBeUndefined(); + } + */ }); }); }); From 58ea598c68f5f3b9173a80f8e86c91f961c619ff Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 11:52:09 +0200 Subject: [PATCH 26/37] feat(crypto-js): Implement `VerificationRequest.scan_qr_code`. --- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-js/src/requests.rs | 1 + .../matrix-sdk-crypto-js/src/verification.rs | 42 ++++++++- .../matrix-sdk-crypto-js/tests/device.test.js | 85 ++++++++++++++++--- 4 files changed, 117 insertions(+), 13 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 9d41f49be..d098a563e 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -22,7 +22,7 @@ crate-type = ["cdylib"] [features] default = ["tracing", "qrcode"] -qrcode = ["matrix-sdk-crypto/qrcode", "matrix-sdk-qrcode"] +qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"] tracing = [] [dependencies] diff --git a/bindings/matrix-sdk-crypto-js/src/requests.rs b/bindings/matrix-sdk-crypto-js/src/requests.rs index 75b989cf3..d7f425dc2 100644 --- a/bindings/matrix-sdk-crypto-js/src/requests.rs +++ b/bindings/matrix-sdk-crypto-js/src/requests.rs @@ -267,6 +267,7 @@ pub struct KeysBackupRequest { /// /// This uploads the public cross signing key triplet. #[wasm_bindgen(getter_with_clone)] +#[derive(Debug)] pub struct SigningKeysUploadRequest { /// The request ID. #[wasm_bindgen(readonly)] diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 4ec3cd8a0..1950ed061 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -674,6 +674,35 @@ impl QrCode { } } +/// A scanned QR code. +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +pub struct QrCodeScan { + inner: matrix_sdk_qrcode::QrVerificationData, +} + +#[cfg(feature = "qrcode")] +impl From for QrCodeScan { + fn from(inner: matrix_sdk_qrcode::QrVerificationData) -> Self { + Self { inner } + } +} + +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +impl QrCodeScan { + /// Parse the decoded payload of a QR code in byte slice form. + /// + /// This method is useful if you would like to do your own custom QR code + /// decoding. + #[wasm_bindgen(js_name = "fromBytes")] + pub fn from_bytes(buffer: Uint8ClampedArray) -> Result { + let bytes = buffer.to_vec(); + + Ok(Self { inner: matrix_sdk_qrcode::QrVerificationData::from_bytes(&bytes)? }) + } +} + /// An object controlling key verification requests. /// /// Interactive verification flows usually start with a verification @@ -952,7 +981,18 @@ impl VerificationRequest { }) } - // scan_qr_code if `qrcode` + /// Start a QR code verification by providing a scanned QR code + /// for this verification flow. + #[cfg(feature = "qrcode")] + #[wasm_bindgen(js_name = "scanQrCode")] + pub fn scan_qr_code(&self, data: QrCodeScan) -> Promise { + let me = self.inner.clone(); + let qr_verification_data = data.inner; + + future_to_promise( + async move { Ok(me.scan_qr_code(qr_verification_data).await?.map(Qr::from)) }, + ) + } } // JavaScript has no complex enums like Rust. To return structs of diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index ace7e8715..eff820ea7 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -1,4 +1,34 @@ -const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, DeviceKey, DeviceKeyName, DeviceKeyAlgorithmName, Ed25519PublicKey, Curve25519PublicKey, Signatures, VerificationMethod, VerificationRequest, ToDeviceRequest, DeviceLists, KeysUploadRequest, RequestType, KeysQueryRequest, Sas, Emoji, SigningKeysUploadRequest, SignatureUploadRequest, Qr, QrCode } = require('../pkg/matrix_sdk_crypto_js'); +const { + OlmMachine, + UserId, + DeviceId, + DeviceKeyId, + RoomId, + DeviceKeyAlgorithName, + Device, + LocalTrust, + UserDevices, + DeviceKey, + DeviceKeyName, + DeviceKeyAlgorithmName, + Ed25519PublicKey, + Curve25519PublicKey, + Signatures, + VerificationMethod, + VerificationRequest, + ToDeviceRequest, + DeviceLists, + KeysUploadRequest, + RequestType, + KeysQueryRequest, + Sas, + Emoji, + SigningKeysUploadRequest, + SignatureUploadRequest, + Qr, + QrCode, + QrCodeScan, +} = require('../pkg/matrix_sdk_crypto_js'); const { LoggerLevel, Tracing } = require('../pkg/matrix_sdk_crypto_js'); const { zip, addMachineToMachine } = require('./helper'); @@ -699,19 +729,20 @@ describe('Key Verification', () => { expect(qr2.isCancelled()).toStrictEqual(false); expect(qr2.isSelfVerification()).toStrictEqual(false); expect(qr2.reciprocated()).toStrictEqual(false); - expect(qr2.reciprocated()).toStrictEqual(false); expect(qr2.flowId).toMatch(/^[a-f0-9]+$/); expect(qr2.roomId).toBeUndefined(); }); + let qrCodeBytes; + test('can read QR code\'s bytes', async () => { const qrCodeHeader = 'MATRIX'; const qrCodeVersion = '\x02'; - const bytes = qr2.toBytes(); + qrCodeBytes = qr2.toBytes(); - expect(bytes).toHaveLength(122); - expect(bytes.slice(0, 7)).toStrictEqual([...qrCodeHeader, ...qrCodeVersion].map(char => char.charCodeAt(0))); + expect(qrCodeBytes).toHaveLength(122); + expect(qrCodeBytes.slice(0, 7)).toStrictEqual([...qrCodeHeader, ...qrCodeVersion].map(char => char.charCodeAt(0))); }); test('can render QR code', async () => { @@ -731,13 +762,18 @@ describe('Key Verification', () => { expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true); const { Canvas } = require('canvas'); - const canvas = new Canvas(45, 45); + const canvas = new Canvas(55, 55); const context = canvas.getContext('2d'); + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + // New image data, filled with black, transparent pixels. const imageData = context.createImageData(45, 45); const data = imageData.data; + const [r, g, b, a] = [0, 1, 2, 3]; + for ( let dataNth = 0, bufferNth = 0; @@ -745,16 +781,17 @@ describe('Key Verification', () => { dataNth += 4, bufferNth += 1 ) { + data[dataNth + a] = 255; + // White pixel if (buffer[bufferNth] == 0) { - data[dataNth] = 255; - data[dataNth + 1] = 255; - data[dataNth + 2] = 255; - data[dataNth + 3] = 255; + data[dataNth + r] = 255; + data[dataNth + g] = 255; + data[dataNth + b] = 255; } } - context.putImageData(imageData, 0, 0); + context.putImageData(imageData, 5, 5); canvasBuffer = canvas.toBuffer('image/png'); } @@ -774,6 +811,32 @@ describe('Key Verification', () => { } */ }); + + let qr1; + + test('can scan a QR code from bytes', async () => { + const scan = QrCodeScan.fromBytes(qrCodeBytes); + + expect(scan).toBeInstanceOf(QrCodeScan); + + qr1 = await verificationRequest1.scanQrCode(scan); + + expect(qr1).toBeInstanceOf(Qr); + + expect(qr1.hasBeenScanned()).toStrictEqual(false); + expect(qr1.hasBeenConfirmed()).toStrictEqual(false); + expect(qr1.userId.toString()).toStrictEqual(userId1.toString()); + expect(qr1.otherUserId.toString()).toStrictEqual(userId2.toString()); + expect(qr1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString()); + expect(qr1.weStarted()).toStrictEqual(true); + expect(qr1.cancelInfo()).toBeUndefined(); + expect(qr1.isDone()).toStrictEqual(false); + expect(qr1.isCancelled()).toStrictEqual(false); + expect(qr1.isSelfVerification()).toStrictEqual(false); + expect(qr1.reciprocated()).toStrictEqual(true); + expect(qr1.flowId).toMatch(/^[a-f0-9]+$/); + expect(qr1.roomId).toBeUndefined(); + }); }); }); From 792b4581abb76e76e93a07cbb5ee38cb4fff21d7 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 14:26:54 +0200 Subject: [PATCH 27/37] feat(crypto-js): Implement `Qr.reciprocate` and `.confirm_scanning`. --- .../matrix-sdk-crypto-js/src/verification.rs | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 1950ed061..0ee9dca9d 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -235,7 +235,7 @@ impl Sas { pub fn accept(&self) -> Result { self.inner .accept() - .map(OutgoingVerificationRequest) + .map(OutgoingVerificationRequest::from) .map(JsValue::try_from) .transpose() .map(JsValue::from) @@ -256,7 +256,7 @@ impl Sas { let (outgoing_verification_requests, signature_upload_request) = me.confirm().await?; let outgoing_verification_requests = outgoing_verification_requests .into_iter() - .map(OutgoingVerificationRequest) + .map(OutgoingVerificationRequest::from) .map(JsValue::try_from) .collect::>()?; @@ -482,12 +482,37 @@ impl Qr { Ok(self.inner.to_bytes()?.into_iter().map(JsValue::from).collect()) } + /// Notify the other side that we have successfully scanned the QR + /// code and that the QR verification flow can start. + /// + /// This will return some OutgoingContent if the object is in the + /// correct state to start the verification flow, otherwise None. + pub fn reciprocate(&self) -> Result { + self.inner + .reciprocate() + .map(OutgoingVerificationRequest::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + /// Confirm that the other side has scanned our QR code. + #[wasm_bindgen(js_name = "confirmScanning")] + pub fn confirm_scanning(&self) -> Result { + self.inner + .confirm_scanning() + .map(OutgoingVerificationRequest::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + /* /// Cancel the verification flow. pub fn cancel(&self) -> … {} pub fn cancel_with_code(&self, code: …) -> … {} - pub fn reciprocate(&self) -> … {} - pub fn confirm_scanning(&self) -> … {} */ } @@ -891,7 +916,7 @@ impl VerificationRequest { self.inner .accept_with_methods(methods) - .map(OutgoingVerificationRequest) + .map(OutgoingVerificationRequest::from) .map(JsValue::try_from) .transpose() .map(JsValue::from) @@ -915,7 +940,7 @@ impl VerificationRequest { pub fn accept(&self) -> Result { self.inner .accept() - .map(OutgoingVerificationRequest) + .map(OutgoingVerificationRequest::from) .map(JsValue::try_from) .transpose() .map(JsValue::from) @@ -929,7 +954,7 @@ impl VerificationRequest { pub fn cancel(&self) -> Result { self.inner .cancel() - .map(OutgoingVerificationRequest) + .map(OutgoingVerificationRequest::from) .map(JsValue::try_from) .transpose() .map(JsValue::from) From cb95c59194aa692674e5aa8d98c882ef33db311e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 14:27:17 +0200 Subject: [PATCH 28/37] test(crypto-js): Test `m.key.verification.start` and `.done` for QR code. --- .../matrix-sdk-crypto-js/tests/device.test.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index eff820ea7..6e581524b 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -837,6 +837,54 @@ describe('Key Verification', () => { expect(qr1.flowId).toMatch(/^[a-f0-9]+$/); expect(qr1.roomId).toBeUndefined(); }); + + test('can start a QR verification/reciprocate (`m.key.verification.start`)', async () => { + let outgoingVerificationRequest = qr1.reciprocate(); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); + + const toDeviceEvents = { + events: [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }] + }; + + // Let's send the verification request to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('can confirm QR code has been scanned', () => { + expect(qr2.hasBeenScanned()).toStrictEqual(true); + }); + + test('can confirm scanning (`m.key.verification.done`)', async () => { + let outgoingVerificationRequest = qr2.confirmScanning(); + + expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest); + + outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); + expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); + + const toDeviceEvents = { + events: [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }] + }; + + // Let's send the verification request to `m2`. + await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); + }); + + test('can confirm QR code has beeen confirmed', () => { + expect(qr2.hasBeenConfirmed()).toStrictEqual(true); + }); }); }); From 1c50cee5d762d9a5f8018c440465ee5dd289a1bc Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 14:37:06 +0200 Subject: [PATCH 29/37] feat(crypto-js): Implement `Sas` and `Qr` `cancel*` methods. --- .../matrix-sdk-crypto-js/src/verification.rs | 78 ++++++++++++++++--- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 0ee9dca9d..b417c6b51 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -274,18 +274,30 @@ impl Sas { }) } - /* - pub fn cancel(&self) { - todo!() + /// Cancel the verification. + pub fn cancel(&self) -> Result { + self.inner + .cancel() + .map(OutgoingVerificationRequest::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) } - */ - /* + /// Cancel the verification. + /// + /// This cancels the verification with given code. #[wasm_bindgen(js_name = "cancelWithCode")] - pub fn cancel_with_code(&self) { - todo!() + pub fn cancel_with_code(&self, code: CancelCode) -> Result { + self.inner + .cancel_with_code(code.try_into()?) + .map(OutgoingVerificationRequest::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) } - */ /// Has the SAS verification flow timed out. #[wasm_bindgen(js_name = "timedOut")] @@ -509,11 +521,30 @@ impl Qr { .map_err(Into::into) } - /* /// Cancel the verification flow. - pub fn cancel(&self) -> … {} - pub fn cancel_with_code(&self, code: …) -> … {} - */ + pub fn cancel(&self) -> Result { + self.inner + .cancel() + .map(OutgoingVerificationRequest::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + /// Cancel the verification. + /// + /// This cancels the verification with given code. + #[wasm_bindgen(js_name = "cancelWithCode")] + pub fn cancel_with_code(&self, code: CancelCode) -> Result { + self.inner + .cancel_with_code(code.try_into()?) + .map(OutgoingVerificationRequest::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } } /// Information about the cancellation of a verification request or @@ -627,6 +658,29 @@ impl From<&RumaCancelCode> for CancelCode { } } +impl TryFrom for RumaCancelCode { + type Error = JsError; + + fn try_from(code: CancelCode) -> Result { + use CancelCode::*; + + Ok(match code { + User => Self::User, + Timeout => Self::Timeout, + UnknownTransaction => Self::UnknownTransaction, + UnknownMethod => Self::UnknownMethod, + UnexpectedMessage => Self::UnexpectedMessage, + KeyMismatch => Self::KeyMismatch, + UserMismatch => Self::UserMismatch, + InvalidMessage => Self::InvalidMessage, + Accepted => Self::Accepted, + MismatchedCommitment => Self::MismatchedCommitment, + MismatchedSas => Self::MismatchedSas, + Other => return Err(JsError::new("`Other` variant is invalid at this place")), + }) + } +} + /// An emoji that is used for interactive verification using a short /// auth string. /// From 3b526ea41248e7cf45b1f06f4eeb9184567e720b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 14:40:49 +0200 Subject: [PATCH 30/37] doc(crypto-js): Add missing documentation. --- bindings/matrix-sdk-crypto-js/src/device.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/matrix-sdk-crypto-js/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs index af9598bf7..2727ef449 100644 --- a/bindings/matrix-sdk-crypto-js/src/device.rs +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -9,6 +9,7 @@ use crate::{ types, verification, vodozemac, }; +/// A device represents a E2EE capable client of an user. #[wasm_bindgen] #[derive(Debug)] pub struct Device { From 7edd6a148cee73e7656c676abc4dd970dbeec382 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 14:41:33 +0200 Subject: [PATCH 31/37] doc(crypto-js): Fix module documentation. --- bindings/matrix-sdk-crypto-js/src/verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index b417c6b51..356e61439 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -1,4 +1,4 @@ -///! Different verification types. +//! Different verification types. #[cfg(feature = "qrcode")] use std::fmt; From 2f35b2cfc61d8b2fd4024d6aac7a35ab4ba2e0d9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 14:42:18 +0200 Subject: [PATCH 32/37] feat(crypto-js): `QrCodeScan` implements `Debug`. --- bindings/matrix-sdk-crypto-js/src/verification.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/matrix-sdk-crypto-js/src/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs index 356e61439..dd7134890 100644 --- a/bindings/matrix-sdk-crypto-js/src/verification.rs +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -756,6 +756,7 @@ impl QrCode { /// A scanned QR code. #[cfg(feature = "qrcode")] #[wasm_bindgen] +#[derive(Debug)] pub struct QrCodeScan { inner: matrix_sdk_qrcode::QrVerificationData, } From 1d9ac6e60cf80f71bc2814122d3ba4ef3ad14f30 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 15:11:09 +0200 Subject: [PATCH 33/37] chore(crypto-js): Fix typos. --- bindings/matrix-sdk-crypto-js/tests/device.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 6e581524b..9a60f1497 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -4,7 +4,6 @@ const { DeviceId, DeviceKeyId, RoomId, - DeviceKeyAlgorithName, Device, LocalTrust, UserDevices, @@ -882,7 +881,7 @@ describe('Key Verification', () => { await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); }); - test('can confirm QR code has beeen confirmed', () => { + test('can confirm QR code has been confirmed', () => { expect(qr2.hasBeenConfirmed()).toStrictEqual(true); }); }); From 8027a3036cd0cef553098cf511d99012aa3750f8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 12 Sep 2022 15:24:49 +0200 Subject: [PATCH 34/37] test(crypto-js): Remove dependency to `canvas`. --- bindings/matrix-sdk-crypto-js/package.json | 1 - bindings/matrix-sdk-crypto-js/tests/device.test.js | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/package.json b/bindings/matrix-sdk-crypto-js/package.json index 02f1108c7..726466786 100644 --- a/bindings/matrix-sdk-crypto-js/package.json +++ b/bindings/matrix-sdk-crypto-js/package.json @@ -26,7 +26,6 @@ "pkg/matrix_sdk_crypto.d.ts" ], "devDependencies": { - "canvas": "^2.10.1", "cross-env": "^7.0.3", "fake-indexeddb": "^4.0", "jest": "^28.1.0", diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 9a60f1497..d9d4fd609 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -749,7 +749,9 @@ describe('Key Verification', () => { expect(qrCode).toBeInstanceOf(QrCode); - let canvasBuffer; + // Want to get `canvasBuffer` to render the QR code? Install `npm install canvas` and uncomment the following blocks. + + //let canvasBuffer; { const buffer = qrCode.renderIntoBuffer(); @@ -760,6 +762,7 @@ describe('Key Verification', () => { // 0 for a white pixel, 1 for a black pixel. expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true); + /* const { Canvas } = require('canvas'); const canvas = new Canvas(55, 55); @@ -792,6 +795,7 @@ describe('Key Verification', () => { context.putImageData(imageData, 5, 5); canvasBuffer = canvas.toBuffer('image/png'); + */ } // Want to see the QR code? Uncomment the following block. From 6842fb97fd19c34f3de82325d1c33a6c70cca9a6 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 14 Sep 2022 09:52:53 +0200 Subject: [PATCH 35/37] chore(crypto-js): Make Clippy happy. --- bindings/matrix-sdk-crypto-js/src/vodozemac.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs index 851e528e1..e8b8cf2a1 100644 --- a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs +++ b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs @@ -142,7 +142,7 @@ impl DeviceKey { use matrix_sdk_crypto::types::DeviceKey::*; match &self.inner { - Curve25519(key) => Some(key.clone().into()), + Curve25519(key) => Some((*key).into()), _ => None, } } @@ -153,7 +153,7 @@ impl DeviceKey { use matrix_sdk_crypto::types::DeviceKey::*; match &self.inner { - Ed25519(key) => Some(key.clone().into()), + Ed25519(key) => Some((*key).into()), _ => None, } } From 4c4fcf91c1a55a6118f60a817ae18978bcc69aa1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 14 Sep 2022 10:01:03 +0200 Subject: [PATCH 36/37] chore(crypto-js): Inline `vodozemac` dependency. --- bindings/matrix-sdk-crypto-js/Cargo.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index d098a563e..c2f05c41a 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -31,6 +31,7 @@ matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" matrix-sdk-indexeddb = { version = "0.1.0", path = "../../crates/matrix-sdk-indexeddb" } matrix-sdk-qrcode = { version = "0.3.0", path = "../../crates/matrix-sdk-qrcode", optional = true } ruma = { version = "0.7.0", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { version = "0.3.0", features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" js-sys = "0.3.49" @@ -40,8 +41,4 @@ http = "0.2.6" anyhow = "1.0.58" tracing = { version = "0.1.35", default-features = false, features = ["attributes"] } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } -zeroize = "1.3.0" - -[dependencies.vodozemac] -version = "0.3.0" -features = ["js"] +zeroize = "1.3.0" \ No newline at end of file From 9d2e0fe8adaf388cc2402a96de06dccedcdf40db Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 15 Sep 2022 09:59:27 +0200 Subject: [PATCH 37/37] doc(crypto-js): Fix typos. --- bindings/matrix-sdk-crypto-js/src/device.rs | 2 +- bindings/matrix-sdk-crypto-js/src/requests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs index 2727ef449..84d79a5c9 100644 --- a/bindings/matrix-sdk-crypto-js/src/device.rs +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -191,7 +191,7 @@ pub enum LocalTrust { /// The trust state of the device is being ignored. Ignored, - /// The trust stte is unset. + /// The trust state is unset. Unset, } diff --git a/bindings/matrix-sdk-crypto-js/src/requests.rs b/bindings/matrix-sdk-crypto-js/src/requests.rs index d7f425dc2..aec419ae0 100644 --- a/bindings/matrix-sdk-crypto-js/src/requests.rs +++ b/bindings/matrix-sdk-crypto-js/src/requests.rs @@ -276,7 +276,7 @@ pub struct SigningKeysUploadRequest { /// A JSON-encoded object of form: /// /// ```json - /// {"master_key", …, "self_signing_key": …, "user_signing_key": …} + /// {"master_key": …, "self_signing_key": …, "user_signing_key": …} /// ``` #[wasm_bindgen(readonly)] pub body: JsString,