diff --git a/Cargo.lock b/Cargo.lock index e63e2f0db..a75ebd147 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..c2f05c41a 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -21,15 +21,17 @@ wasm-opt = ['-Oz'] crate-type = ["cdylib"] [features] -default = ["tracing"] -qrcode = ["matrix-sdk-crypto/qrcode"] +default = ["tracing", "qrcode"] +qrcode = ["matrix-sdk-crypto/qrcode", "dep: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"] } +vodozemac = { version = "0.3.0", features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" js-sys = "0.3.49" @@ -39,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 diff --git a/bindings/matrix-sdk-crypto-js/package.json b/bindings/matrix-sdk-crypto-js/package.json index ed439f237..726466786 100644 --- a/bindings/matrix-sdk-crypto-js/package.json +++ b/bindings/matrix-sdk-crypto-js/package.json @@ -26,12 +26,12 @@ "pkg/matrix_sdk_crypto.d.ts" ], "devDependencies": { - "wasm-pack": "^0.10.2", + "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/src/device.rs b/bindings/matrix-sdk-crypto-js/src/device.rs new file mode 100644 index 000000000..84d79a5c9 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/device.rs @@ -0,0 +1,263 @@ +//! 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, verification, vodozemac, +}; + +/// A device represents a E2EE capable client of an user. +#[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 { + /// 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` + /// 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.is_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.is_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 state 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 f9c43183e..e444a661d 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; @@ -26,9 +27,11 @@ pub mod machine; pub mod olm; pub mod requests; pub mod responses; +pub mod store; pub mod sync_events; mod tracing; pub mod types; +pub mod verification; pub mod vodozemac; use js_sys::{Object, Reflect}; diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index cd7d29e7a..fa4aeefd1 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -8,12 +8,12 @@ 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, responses::{self, response_from_string}, - sync_events, types, vodozemac, + store, sync_events, types, verification, vodozemac, }; /// State machine implementation of the Olm/Megolm encryption protocol @@ -353,6 +353,66 @@ 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)?) + }) + } + + /// 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 { @@ -453,4 +513,104 @@ 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 + /// verification has been requested by a room event). + /// + /// It returns a “`Verification` object”, which is either a `Sas` + /// or `Qr` 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/requests.rs b/bindings/matrix-sdk-crypto-js/src/requests.rs index 99d02a40c..aec419ae0 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,33 @@ 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)] +#[derive(Debug)] +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,35 +298,56 @@ impl KeysBackupRequest { } macro_rules! request { - ($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => { - impl TryFrom<(String, &$ruma_request)> for $request { - type Error = serde_json::Error; - - fn try_from( - (request_id, request): (String, &$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()); )+ - let value = serde_json::Value::Object(map); + let object = serde_json::Value::Object(map); - Ok($request { - id: request_id.into(), - body: serde_json::to_string(&value)?.into(), + serde_json::to_string(&object) + } + } + + 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, &$source_request), + ) -> Result { + Ok($destination_request { + id: Some(request_id.into()), + body: Self::to_json(request)?.into(), }) } } }; } -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/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() + } +} 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/verification.rs b/bindings/matrix-sdk-crypto-js/src/verification.rs new file mode 100644 index 000000000..dd7134890 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/verification.rs @@ -0,0 +1,1109 @@ +//! Different verification types. + +#[cfg(feature = "qrcode")] +use std::fmt; + +#[cfg(feature = "qrcode")] +use js_sys::Uint8ClampedArray; +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, +}; + +/// 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, +} + +impl From for Sas { + fn from(inner: matrix_sdk_crypto::Sas) -> Self { + Self { inner } + } +} + +#[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 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() + } + + /// 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::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + /// 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(); + + 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::from) + .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::try_from(&request)) + .transpose()? + .into(), + ); + + Ok(tuple) + }) + } + + /// 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, 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")] + 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 = "emojiIndex")] + 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")] +impl From for Qr { + fn from(inner: matrix_sdk_crypto::QrVerification) -> Self { + Self { inner } + } +} + +#[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() + } + + /// 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()) + } + + /// 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) -> 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 +/// 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, + } + } +} + +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. +/// +/// 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 `Uint8ClampedArray` 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(); + let buffer = Uint8ClampedArray::new_with_length(colors.len().try_into()?); + buffer.copy_from(colors.as_slice()); + + Ok(buffer) + } +} + +/// A scanned QR code. +#[cfg(feature = "qrcode")] +#[wasm_bindgen] +#[derive(Debug)] +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 +/// 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 { + /// 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, + 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(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(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(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(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(getter, 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(getter, 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(getter, 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(getter, 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::from) + .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::from) + .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::from) + .map(JsValue::try_from) + .transpose() + .map(JsValue::from) + .map_err(Into::into) + } + + /// 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 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)) + }) + } + + /// 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 +// different types, we have no choice that hiding everything behind a +// `JsValue`. +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; + + 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))?) + } + }) + } +} diff --git a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs index e80889227..e8b8cf2a1 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).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).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..d9d4fd609 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -0,0 +1,901 @@ +const { + OlmMachine, + UserId, + DeviceId, + DeviceKeyId, + RoomId, + 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'); + +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); + }); +}); + +describe('Key Verification', () => { + const userId1 = new UserId('@alice:example.org'); + const deviceId1 = new DeviceId('alice_device'); + + const userId2 = new UserId('@bob:example.org'); + const deviceId2 = new DeviceId('bob_device'); + + function machine(new_user, new_device) { + return new OlmMachine(new_user || userId1, new_device || deviceId1); + } + + 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).toEqual(expect.arrayContaining([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 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.SasV1, VerificationMethod.ReciprocateV1])); + 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 (`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).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1])); + expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([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. + 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}$/; + + for (const [decimal1, decimal2] of zip(decimals1, decimals2)) { + expect(decimal1.toString()).toMatch(isDecimal); + + expect(decimal2).toStrictEqual(decimal1); + } + }); + + 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(4); + + 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', () => { + 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.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'; + + qrCodeBytes = qr2.toBytes(); + + expect(qrCodeBytes).toHaveLength(122); + expect(qrCodeBytes.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); + + // Want to get `canvasBuffer` to render the QR code? Install `npm install canvas` and uncomment the following blocks. + + //let canvasBuffer; + + { + 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(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; + dataNth < data.length && bufferNth < buffer.length; + dataNth += 4, + bufferNth += 1 + ) { + data[dataNth + a] = 255; + + // White pixel + if (buffer[bufferNth] == 0) { + data[dataNth + r] = 255; + data[dataNth + g] = 255; + data[dataNth + b] = 255; + } + } + + context.putImageData(imageData, 5, 5); + 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(); + } + */ + }); + + 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(); + }); + + 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 been confirmed', () => { + expect(qr2.hasBeenConfirmed()).toStrictEqual(true); + }); + }); +}); + +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..1ced03d1a --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -0,0 +1,80 @@ +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) { + 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(); + + // 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; + } + + { + 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: {}, + 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.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); + } +} + +module.exports = { + zip, + addMachineToMachine, +}; 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(); 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()) } }