diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b3b5a2330..1371700e4 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -38,7 +38,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: tarpaulin - args: --ignore-config --exclude-files "crates/matrix-sdk/examples/*,crates/matrix-sdk-common,crates/matrix-sdk-test" --out Xml + args: --workspace --ignore-config --exclude-files "crates/matrix-sdk/examples/*,crates/matrix-sdk-common,crates/matrix-sdk-test" --exclude matrix-sdk-crypto-js --exclude matrix-sdk-crypto-nodejs --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1f0a6da2a..645892910 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings" with: command: doc - args: --no-deps --workspace --features docsrs -Zrustdoc-map + args: --no-deps --workspace --exclude matrix-sdk-crypto-js --exclude matrix-sdk-crypto-nodejs --features docsrs -Zrustdoc-map - name: Deploy docs if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 53b9d7fac..11e293676 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -27,7 +27,7 @@ jobs: - matrix-sdk-qrcode - matrix-sdk-base - matrix-sdk-common - - matrix-sdk-crypto + - matrix-sdk-crypto-js - indexeddb-no-crypto - indexeddb-with-crypto @@ -54,7 +54,7 @@ jobs: profile: minimal override: true - - name: Install WasmPack + - name: Install wasm-pack uses: jetli/wasm-pack-action@v0.3.0 with: version: 'latest' diff --git a/Cargo.toml b/Cargo.toml index 6597b0d88..97281a7aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,7 @@ members = ["benchmarks", "crates/*", "labs/*", "xtask"] # xtask and labs should only be compiled when invoked explicitly default-members = ["benchmarks", "crates/*"] - resolver = "2" + +[profile.release] +lto = true \ No newline at end of file diff --git a/crates/matrix-sdk-crypto-js/.cargo/config b/crates/matrix-sdk-crypto-js/.cargo/config new file mode 100644 index 000000000..f4e8c002f --- /dev/null +++ b/crates/matrix-sdk-crypto-js/.cargo/config @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/crates/matrix-sdk-crypto-js/Cargo.toml b/crates/matrix-sdk-crypto-js/Cargo.toml new file mode 100644 index 000000000..8a918b105 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/Cargo.toml @@ -0,0 +1,38 @@ +[package] +authors = ["Ivan Enderlin "] +description = "Matrix encryption library, for JavaScript" +edition = "2021" +homepage = "https://github.com/matrix-org/matrix-rust-sdk" +keywords = ["matrix", "chat", "messaging", "ruma", "nio"] +license = "Apache-2.0" +name = "matrix-sdk-crypto-js" +readme = "README.md" +repository = "https://github.com/matrix-org/matrix-rust-sdk" +rust-version = "1.60" +version = "0.5.0" + +[package.metadata.docs.rs] +features = ["docsrs"] +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-Oz'] + +[lib] +crate-type = ["cdylib"] + +[features] +default = [] +qrcode = ["matrix-sdk-crypto/qrcode"] +docsrs = [] + +[dependencies] +matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto" } +ruma = { version = "0.6.2", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36", features = ["js"] } +wasm-bindgen = "0.2.80" +wasm-bindgen-futures = "0.4.30" +js-sys = "0.3.49" +serde_json = "1.0.79" +http = "0.2.6" +anyhow = "1.0" diff --git a/crates/matrix-sdk-crypto-js/README.md b/crates/matrix-sdk-crypto-js/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/crates/matrix-sdk-crypto-js/js/Makefile b/crates/matrix-sdk-crypto-js/js/Makefile new file mode 100644 index 000000000..71e9e1c82 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/js/Makefile @@ -0,0 +1,5 @@ +build: + RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./js/pkg ../ + +test: + node --test ../tests/js/**.js diff --git a/crates/matrix-sdk-crypto-js/src/events.rs b/crates/matrix-sdk-crypto-js/src/events.rs new file mode 100644 index 000000000..6372ad552 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/events.rs @@ -0,0 +1,61 @@ +//! Types related to events. + +use ruma::events::room::history_visibility::HistoryVisibility as RumaHistoryVisibility; +use wasm_bindgen::prelude::*; + +/// Who can see a room's history. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub enum HistoryVisibility { + /// Previous events are accessible to newly joined members from + /// the point they were invited onwards. + /// + /// Events stop being accessible when the member's state changes + /// to something other than *invite* or *join*. + Invited, + + /// Previous events are accessible to newly joined members from + /// the point they joined the room onwards. + /// + /// Events stop being accessible when the member's state changes + /// to something other than *join*. + Joined, + + /// Previous events are always accessible to newly joined members. + /// + /// All events in the room are accessible, even those sent when + /// the member was not a part of the room. + Shared, + + /// All events while this is the `HistoryVisibility` value may be + /// shared by any participating homeserver with anyone, regardless + /// of whether they have ever joined the room. + WorldReadable, +} + +impl From for RumaHistoryVisibility { + fn from(value: HistoryVisibility) -> Self { + use HistoryVisibility::*; + + match value { + Invited => Self::Invited, + Joined => Self::Joined, + Shared => Self::Shared, + WorldReadable => Self::WorldReadable, + } + } +} + +impl From for HistoryVisibility { + fn from(value: RumaHistoryVisibility) -> Self { + use RumaHistoryVisibility::*; + + match value { + Invited => Self::Invited, + Joined => Self::Joined, + Shared => Self::Shared, + WorldReadable => Self::WorldReadable, + _ => unreachable!("Unknown variant"), + } + } +} diff --git a/crates/matrix-sdk-crypto-js/src/future.rs b/crates/matrix-sdk-crypto-js/src/future.rs new file mode 100644 index 000000000..04b2ef4cb --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/future.rs @@ -0,0 +1,26 @@ +use std::future::Future; + +use js_sys::Promise; +use wasm_bindgen::{JsValue, UnwrapThrowExt}; +use wasm_bindgen_futures::spawn_local; + +pub(crate) fn future_to_promise(future: F) -> Promise +where + F: Future> + 'static, + T: Into, +{ + let mut future = Some(future); + + Promise::new(&mut |resolve, reject| { + let future = future.take().unwrap_throw(); + + spawn_local(async move { + match future.await { + Ok(value) => resolve.call1(&JsValue::UNDEFINED, &value.into()).unwrap_throw(), + Err(value) => { + reject.call1(&JsValue::UNDEFINED, &value.to_string().into()).unwrap_throw() + } + }; + }); + }) +} diff --git a/crates/matrix-sdk-crypto-js/src/identifiers.rs b/crates/matrix-sdk-crypto-js/src/identifiers.rs new file mode 100644 index 000000000..3ff2f9068 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/identifiers.rs @@ -0,0 +1,171 @@ +//! Types for [Matrix](https://matrix.org/) identifiers for devices, +//! events, keys, rooms, servers, users and URIs. + +use wasm_bindgen::prelude::*; + +/// A Matrix [user ID]. +/// +/// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct UserId { + pub(crate) inner: ruma::OwnedUserId, +} + +impl UserId { + pub(crate) fn new_with(inner: ruma::OwnedUserId) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl UserId { + /// Parse/validate and create a new `UserId`. + #[wasm_bindgen(constructor)] + pub fn new(id: &str) -> Result { + Ok(Self::new_with(ruma::UserId::parse(id)?)) + } + + /// Returns the user's localpart. + pub fn localpart(&self) -> String { + self.inner.localpart().to_owned() + } + + /// Returns the server name of the user ID. + #[wasm_bindgen(js_name = "serverName")] + pub fn server_name(&self) -> ServerName { + ServerName { inner: self.inner.server_name().to_owned() } + } + + /// Whether this user ID is a historical one. + /// + /// A historical user ID is one that doesn't conform to the latest + /// specification of the user ID grammar but is still accepted + /// because it was previously allowed. + #[wasm_bindgen(getter, js_name = "isHistorical")] + pub fn is_historical(&self) -> bool { + self.inner.is_historical() + } + + /// Return the user ID as a string. + #[wasm_bindgen(js_name = "toString")] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.inner.as_str().to_owned() + } +} + +/// A Matrix key ID. +/// +/// Device identifiers in Matrix are completely opaque character +/// sequences. This type is provided simply for its semantic value. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct DeviceId { + pub(crate) inner: ruma::OwnedDeviceId, +} + +impl DeviceId { + pub(crate) fn new_with(inner: ruma::OwnedDeviceId) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl DeviceId { + /// Create a new `DeviceId`. + #[wasm_bindgen(constructor)] + pub fn new(id: &str) -> DeviceId { + Self::new_with(id.into()) + } + + /// Return the device ID as a string. + #[wasm_bindgen(js_name = "toString")] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.inner.as_str().to_owned() + } +} + +/// A Matrix [room ID]. +/// +/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct RoomId { + pub(crate) inner: ruma::OwnedRoomId, +} + +impl RoomId { + pub(crate) fn new_with(inner: ruma::OwnedRoomId) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl RoomId { + /// Parse/validate and create a new `RoomId`. + #[wasm_bindgen(constructor)] + pub fn new(id: &str) -> Result { + Ok(Self::new_with(ruma::RoomId::parse(id)?)) + } + + /// Returns the user's localpart. + pub fn localpart(&self) -> String { + self.inner.localpart().to_owned() + } + + /// Returns the server name of the room ID. + #[wasm_bindgen(js_name = "serverName")] + pub fn server_name(&self) -> ServerName { + ServerName { inner: self.inner.server_name().to_owned() } + } + + /// Return the room ID as a string. + #[wasm_bindgen(js_name = "toString")] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.inner.as_str().to_owned() + } +} + +/// A Matrix-spec compliant [server name]. +/// +/// It consists of a host and an optional port (separated by a colon if +/// present). +/// +/// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name +#[wasm_bindgen] +#[derive(Debug)] +pub struct ServerName { + inner: ruma::OwnedServerName, +} + +#[wasm_bindgen] +impl ServerName { + /// Parse/validate and create a new `ServerName`. + #[wasm_bindgen(constructor)] + pub fn new(name: &str) -> Result { + Ok(Self { inner: ruma::ServerName::parse(name)? }) + } + + /// Returns the host of the server name. + /// + /// That is: Return the part of the server before `:` or the + /// full server name if there is no port. + pub fn host(&self) -> String { + self.inner.host().to_owned() + } + + /// Returns the port of the server name if any. + pub fn port(&self) -> Option { + self.inner.port() + } + + /// Returns true if and only if the server name is an IPv4 or IPv6 + /// address. + #[wasm_bindgen(js_name = "isIpLiteral")] + pub fn is_ip_literal(&self) -> bool { + self.inner.is_ip_literal() + } +} diff --git a/crates/matrix-sdk-crypto-js/src/lib.rs b/crates/matrix-sdk-crypto-js/src/lib.rs new file mode 100644 index 000000000..03a1fc8fe --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/lib.rs @@ -0,0 +1,56 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(missing_docs, missing_debug_implementations)] + +pub mod events; +mod future; +pub mod identifiers; +pub mod machine; +pub mod requests; +pub mod responses; +pub mod sync_events; + +use js_sys::{Object, Reflect}; +use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*}; + +/// A really hacky and dirty code to downcast a `JsValue` to `T: +/// RefFromWasmAbi`, inspired by +/// https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288. +/// +/// The returned value is likely to be a `wasm_bindgen::__ref::Ref`. +fn downcast(value: &JsValue, classname: &str) -> Result +where + T: RefFromWasmAbi, +{ + let constructor_name = Object::get_prototype_of(value).constructor().name(); + + if constructor_name == classname { + let pointer = Reflect::get(value, &JsValue::from_str("ptr")) + .map_err(|_| JsError::new("Failed to read the `JsValue` pointer"))?; + let pointer = pointer + .as_f64() + .ok_or_else(|| JsError::new("Failed to read the `JsValue` pointer as a `f64`"))? + as u32; + + Ok(unsafe { T::ref_from_abi(pointer) }) + } else { + Err(JsError::new(&format!( + "Expect an `{}` instance, received `{}` instead", + classname, constructor_name, + ))) + } +} diff --git a/crates/matrix-sdk-crypto-js/src/machine.rs b/crates/matrix-sdk-crypto-js/src/machine.rs new file mode 100644 index 000000000..88202337b --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/machine.rs @@ -0,0 +1,519 @@ +//! The crypto specific Olm objects. + +use std::{collections::BTreeMap, time::Duration}; + +use js_sys::{Array, Map, Promise, Set}; +use ruma::{DeviceKeyAlgorithm, OwnedTransactionId, UInt}; +use serde_json::Value as JsonValue; +use wasm_bindgen::prelude::*; + +use crate::{ + downcast, events, + future::future_to_promise, + identifiers, requests, + requests::OutgoingRequest, + responses::{self, response_from_string}, + sync_events, +}; + +/// State machine implementation of the Olm/Megolm encryption protocol +/// used for Matrix end to end encryption. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct OlmMachine { + inner: matrix_sdk_crypto::OlmMachine, +} + +#[wasm_bindgen] +impl OlmMachine { + /// Create a new memory based `OlmMachine`. + /// + /// The created machine will keep the encryption keys only in + /// memory and once the objects is dropped, the keys will be lost. + /// + /// `user_id` represents the unique ID of the user that owns this + /// machine. `device_id` represents the unique ID of the device + /// that owns this machine. + #[wasm_bindgen(constructor)] + #[allow(clippy::new_ret_no_self)] + pub fn new(user_id: &identifiers::UserId, device_id: &identifiers::DeviceId) -> Promise { + let user_id = user_id.inner.clone(); + let device_id = device_id.inner.clone(); + + future_to_promise(async move { + Ok(OlmMachine { + inner: matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref()) + .await, + }) + }) + } + + /// The unique user ID that owns this `OlmMachine` instance. + #[wasm_bindgen(js_name = "userId")] + pub fn user_id(&self) -> identifiers::UserId { + identifiers::UserId::new_with(self.inner.user_id().to_owned()) + } + + /// The unique device ID that identifies this `OlmMachine`. + #[wasm_bindgen(js_name = "deviceId")] + pub fn device_id(&self) -> identifiers::DeviceId { + identifiers::DeviceId::new_with(self.inner.device_id().to_owned()) + } + + /// Get the public parts of our Olm identity keys. + #[wasm_bindgen(js_name = "identityKeys")] + pub fn identity_keys(&self) -> IdentityKeys { + self.inner.identity_keys().into() + } + + /// Get the display name of our own device. + #[wasm_bindgen(js_name = "displayName")] + pub fn display_name(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { Ok(me.display_name().await?) }) + } + + /// Get all the tracked users of our own device. + /// + /// Returns a `Set`. + #[wasm_bindgen(js_name = "trackedUsers")] + pub fn tracked_users(&self) -> Set { + let set = Set::new(&JsValue::UNDEFINED); + + self.inner.tracked_users().into_iter().map(identifiers::UserId::new_with).for_each( + |user| { + set.add(&user.into()); + }, + ); + + set + } + + /// Update the tracked users. + /// + /// `users` is an iterator over user IDs that should be marked for + /// tracking. + /// + /// This will mark users that weren't seen before for a key query + /// and tracking. + /// + /// If the user is already known to the Olm machine, it will not + /// be considered for a key query. + #[wasm_bindgen(js_name = "updateTrackedUsers")] + pub fn update_tracked_users(&self, users: &Array) -> Result { + let users = users + .iter() + .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) + .collect::, JsError>>()?; + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + me.update_tracked_users(users.iter().map(AsRef::as_ref)).await; + Ok(JsValue::UNDEFINED) + })) + } + + /// Handle to-device events and one-time key counts from a sync + /// response. + /// + /// This will decrypt and handle to-device events returning the + /// decrypted versions of them. + /// + /// To decrypt an event from the room timeline call + /// `decrypt_room_event`. + #[wasm_bindgen(js_name = "receiveSyncChanges")] + pub fn receive_sync_changes( + &self, + to_device_events: &str, + changed_devices: &sync_events::DeviceLists, + one_time_key_counts: &Map, + unused_fallback_keys: &Set, + ) -> Result { + let to_device_events = serde_json::from_str(to_device_events)?; + let changed_devices = changed_devices.inner.clone(); + let one_time_key_counts: BTreeMap = one_time_key_counts + .entries() + .into_iter() + .filter_map(|js_value| { + let pair = Array::from(&js_value.ok()?); + let (key, value) = ( + DeviceKeyAlgorithm::from(pair.at(0).as_string()?), + UInt::new(pair.at(1).as_f64()? as u64)?, + ); + + Some((key, value)) + }) + .collect(); + let unused_fallback_keys: Option> = Some( + unused_fallback_keys + .values() + .into_iter() + .filter_map(|js_value| Some(DeviceKeyAlgorithm::from(js_value.ok()?.as_string()?))) + .collect(), + ); + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + Ok(serde_json::to_string( + &me.receive_sync_changes( + to_device_events, + &changed_devices, + &one_time_key_counts, + unused_fallback_keys.as_deref(), + ) + .await?, + )?) + })) + } + + /// Get the outgoing requests that need to be sent out. + /// + /// This returns a list of `JsValue` to represent either: + /// * `KeysUploadRequest`, + /// * `KeysQueryRequest`, + /// * `KeysClaimRequest`, + /// * `ToDeviceRequest`, + /// * `SignatureUploadRequest`, + /// * `RoomMessageRequest` or + /// * `KeysBackupRequest`. + /// + /// Those requests need to be sent out to the server and the + /// responses need to be passed back to the state machine using + /// `mark_request_as_sent`. + #[wasm_bindgen(js_name = "outgoingRequests")] + pub fn outgoing_requests(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + Ok(me + .outgoing_requests() + .await? + .into_iter() + .map(OutgoingRequest) + .map(TryFrom::try_from) + .collect::, _>>()? + .into_iter() + .collect::()) + }) + } + + /// Mark the request with the given request ID as sent (see + /// `outgoing_requests`). + /// + /// Arguments are: + /// + /// * `request_id` represents the unique ID of the request that was sent + /// out. This is needed to couple the response with the now sent out + /// request. + /// * `response_type` represents the type of the request that was sent out. + /// * `response` represents the response that was received from the server + /// after the outgoing request was sent out. + #[wasm_bindgen(js_name = "markRequestAsSent")] + pub fn mark_request_as_sent( + &self, + request_id: &str, + request_type: requests::RequestType, + response: &str, + ) -> Result { + let transaction_id = OwnedTransactionId::from(request_id); + let response = response_from_string(response)?; + let incoming_response = responses::OwnedResponse::try_from((request_type, response))?; + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + Ok(me.mark_request_as_sent(&transaction_id, &incoming_response).await.map(|_| true)?) + })) + } + + /// Encrypt a room message for the given room. + /// + /// Beware that a group session needs to be shared before this + /// method can be called using the `share_group_session` method. + /// + /// `room_id` is the ID of the room for which the message should + /// be encrypted. `event_type` is the type of the event. `content` + /// is the plaintext content of the message that should be + /// encrypted. + /// + /// # Panics + /// + /// Panics if a group session for the given room wasn't shared + /// beforehand. + #[wasm_bindgen(js_name = "encryptRoomEvent")] + pub fn encrypt_room_event( + &self, + room_id: &identifiers::RoomId, + event_type: String, + content: &str, + ) -> Result { + let room_id = room_id.inner.clone(); + let content: JsonValue = serde_json::from_str(content)?; + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + Ok(serde_json::to_string( + &me.encrypt_room_event_raw(&room_id, content, event_type.as_ref()).await?, + )?) + })) + } + + /// Invalidate the currently active outbound group session for the + /// given room. + /// + /// Returns true if a session was invalidated, false if there was + /// no session to invalidate. + #[wasm_bindgen(js_name = "invalidateGroupSession")] + pub fn invalidate_group_session(&self, room_id: &identifiers::RoomId) -> Promise { + let room_id = room_id.inner.clone(); + let me = self.inner.clone(); + + future_to_promise(async move { Ok(me.invalidate_group_session(&room_id).await?) }) + } + + /// Get to-device requests to share a group session with users in a room. + /// + /// `room_id` is the room ID. `users` is an array of `UserId` + /// objects. `encryption_settings` are an `EncryptionSettings` + /// object. + #[wasm_bindgen(js_name = "shareGroupSession")] + pub fn share_group_session( + &self, + room_id: &identifiers::RoomId, + users: &Array, + encryption_settings: &EncryptionSettings, + ) -> Result { + let room_id = room_id.inner.clone(); + let users = users + .iter() + .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) + .collect::, JsError>>()?; + let encryption_settings = + matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings); + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + Ok(serde_json::to_string( + &me.share_group_session( + &room_id, + users.iter().map(AsRef::as_ref), + encryption_settings, + ) + .await?, + )?) + })) + } + + /// Get the a key claiming request for the user/device pairs that + /// we are missing Olm sessions for. + /// + /// Returns `NULL` if no key claiming request needs to be sent + /// out, otherwise it returns an `Array` where the first key is + /// the transaction ID as a string, and the second key is the keys + /// claim request serialized to JSON. + /// + /// Sessions need to be established between devices so group + /// sessions for a room can be shared with them. + /// + /// This should be called every time a group session needs to be + /// shared as well as between sync calls. After a sync some + /// devices may request room keys without us having a valid Olm + /// session with them, making it impossible to server the room key + /// request, thus it’s necessary to check for missing sessions + /// between sync as well. + /// + /// Note: Care should be taken that only one such request at a + /// time is in flight, e.g. using a lock. + /// + /// The response of a successful key claiming requests needs to be + /// passed to the `OlmMachine` with the `mark_request_as_sent`. + /// + /// `users` represents the list of users that we should check if + /// we lack a session with one of their devices. This can be an + /// empty iterator when calling this method between sync requests. + #[wasm_bindgen(js_name = "getMissingSessions")] + pub fn get_missing_sessions(&self, users: &Array) -> Result { + let users = users + .iter() + .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) + .collect::, JsError>>()?; + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + match me.get_missing_sessions(users.iter().map(AsRef::as_ref)).await? { + Some((transaction_id, keys_claim_request)) => { + Ok(JsValue::from(requests::KeysClaimRequest::try_from(( + transaction_id.to_string(), + &keys_claim_request, + ))?)) + } + + None => Ok(JsValue::NULL), + } + })) + } +} + +/// An Ed25519 public key, used to verify digital signatures. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct Ed25519PublicKey { + inner: vodozemac::Ed25519PublicKey, +} + +#[wasm_bindgen] +impl Ed25519PublicKey { + /// The number of bytes an Ed25519 public key has. + #[wasm_bindgen(getter)] + pub fn length(&self) -> usize { + vodozemac::Ed25519PublicKey::LENGTH + } + + /// Serialize an Ed25519 public key to an unpadded base64 + /// representation. + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +/// A Curve25519 public key. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub struct Curve25519PublicKey { + inner: vodozemac::Curve25519PublicKey, +} + +#[wasm_bindgen] +impl Curve25519PublicKey { + /// The number of bytes a Curve25519 public key has. + #[wasm_bindgen(getter)] + pub fn length(&self) -> usize { + vodozemac::Curve25519PublicKey::LENGTH + } + + /// Serialize an Curve25519 public key to an unpadded base64 + /// representation. + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +/// Struct holding the two public identity keys of an account. +#[wasm_bindgen(getter_with_clone)] +#[derive(Debug)] +pub struct IdentityKeys { + /// The Ed25519 public key, used for signing. + pub ed25519: Ed25519PublicKey, + + /// The Curve25519 public key, used for establish shared secrets. + pub curve25519: Curve25519PublicKey, +} + +impl From for IdentityKeys { + fn from(value: matrix_sdk_crypto::olm::IdentityKeys) -> Self { + Self { + ed25519: Ed25519PublicKey { inner: value.ed25519 }, + curve25519: Curve25519PublicKey { inner: value.curve25519 }, + } + } +} + +/// An encryption algorithm to be used to encrypt messages sent to a +/// room. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub enum EncryptionAlgorithm { + /// Olm version 1 using Curve25519, AES-256, and SHA-256. + OlmV1Curve25519AesSha2, + + /// Megolm version 1 using AES-256 and SHA-256. + MegolmV1AesSha2, +} + +impl From for ruma::EventEncryptionAlgorithm { + fn from(value: EncryptionAlgorithm) -> Self { + use EncryptionAlgorithm::*; + + match value { + OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, + MegolmV1AesSha2 => Self::MegolmV1AesSha2, + } + } +} + +impl From for EncryptionAlgorithm { + fn from(value: ruma::EventEncryptionAlgorithm) -> Self { + use ruma::EventEncryptionAlgorithm::*; + + match value { + OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, + MegolmV1AesSha2 => Self::MegolmV1AesSha2, + _ => unreachable!("Unknown variant"), + } + } +} + +/// Settings for an encrypted room. +/// +/// This determines the algorithm and rotation periods of a group +/// session. +#[wasm_bindgen(getter_with_clone)] +#[derive(Debug, Clone)] +pub struct EncryptionSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EncryptionAlgorithm, + + /// How long the session should be used before changing it, + /// expressed in microseconds. + #[wasm_bindgen(js_name = "rotationPeriod")] + pub rotation_period: u64, + + /// How many messages should be sent before changing the session. + #[wasm_bindgen(js_name = "rotationPeriodMessages")] + pub rotation_period_messages: u64, + + /// The history visibility of the room when the session was + /// created. + #[wasm_bindgen(js_name = "historyVisibility")] + pub history_visibility: events::HistoryVisibility, +} + +impl Default for EncryptionSettings { + fn default() -> Self { + let default = matrix_sdk_crypto::olm::EncryptionSettings::default(); + + Self { + algorithm: default.algorithm.into(), + rotation_period: default.rotation_period.as_micros().try_into().unwrap(), + rotation_period_messages: default.rotation_period_msgs, + history_visibility: default.history_visibility.into(), + } + } +} + +#[wasm_bindgen] +impl EncryptionSettings { + /// Create a new `EncryptionSettings` with default values. + #[wasm_bindgen(constructor)] + pub fn new() -> EncryptionSettings { + Self::default() + } +} + +impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings { + fn from(value: &EncryptionSettings) -> Self { + Self { + algorithm: value.algorithm.clone().into(), + rotation_period: Duration::from_micros(value.rotation_period), + rotation_period_msgs: value.rotation_period_messages, + history_visibility: value.history_visibility.clone().into(), + } + } +} diff --git a/crates/matrix-sdk-crypto-js/src/requests.rs b/crates/matrix-sdk-crypto-js/src/requests.rs new file mode 100644 index 000000000..f6b0a96a4 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/requests.rs @@ -0,0 +1,267 @@ +//! Types to handle requests. + +use js_sys::JsString; +use matrix_sdk_crypto::{ + requests::{ + KeysBackupRequest as RumaKeysBackupRequest, KeysQueryRequest as RumaKeysQueryRequest, + RoomMessageRequest as RumaRoomMessageRequest, ToDeviceRequest as RumaToDeviceRequest, + }, + OutgoingRequests, +}; +use ruma::api::client::keys::{ + claim_keys::v3::Request as RumaKeysClaimRequest, + upload_keys::v3::Request as RumaKeysUploadRequest, + upload_signatures::v3::Request as RumaSignatureUploadRequest, +}; +use wasm_bindgen::prelude::*; + +/// Data for a request to the `/keys/upload` API endpoint +/// ([specification]). +/// +/// Publishes end-to-end encryption keys for the device. +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysupload +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct KeysUploadRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ```json + /// {"device_keys": …, "one_time_keys": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +/// Data for a request to the `/keys/query` API endpoint +/// ([specification]). +/// +/// Returns the current devices and identity keys for the given users. +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysquery +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct KeysQueryRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ``` + /// {"timeout": …, "device_keys": …, "token": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +/// Data for a request to the `/keys/claim` API endpoint +/// ([specification]). +/// +/// Claims one-time keys that can be used to establish 1-to-1 E2EE +/// sessions. +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysclaim +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct KeysClaimRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ``` + /// {"timeout": …, "one_time_keys": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +/// Data for a request to the `/sendToDevice` API endpoint +/// ([specification]). +/// +/// Send an event to a single device or to a group of devices. +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3sendtodeviceeventtypetxnid +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct ToDeviceRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ``` + /// {"event_type": …, "txn_id": …, "messages": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +/// Data for a request to the `/keys/signatures/upload` API endpoint +/// ([specification]). +/// +/// Publishes cross-signing signatures for the user. +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keyssignaturesupload +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct SignatureUploadRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ``` + /// {"signed_keys": …, "txn_id": …, "messages": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +/// A customized owned request type for sending out room messages +/// ([specification]). +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct RoomMessageRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ``` + /// {"room_id": …, "txn_id": …, "content": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +/// A request that will back up a batch of room keys to the server +/// ([specification]). +/// +/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3room_keyskeys +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct KeysBackupRequest { + /// The request ID. + #[wasm_bindgen(readonly)] + pub request_id: JsString, + + /// A JSON-encoded object of form: + /// + /// ``` + /// {"rooms": …} + /// ``` + #[wasm_bindgen(readonly)] + pub body: JsString, +} + +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 { + let mut map = serde_json::Map::new(); + $( + map.insert(stringify!($field).to_owned(), serde_json::to_value(&request.$field).unwrap()); + )+ + let value = serde_json::Value::Object(map); + + Ok($request { + request_id: request_id.into(), + body: serde_json::to_string(&value)?.into(), + }) + } + } + }; +} + +request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_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); + +// 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 OutgoingRequest(pub(crate) matrix_sdk_crypto::OutgoingRequest); + +impl TryFrom for JsValue { + type Error = serde_json::Error; + + fn try_from(outgoing_request: OutgoingRequest) -> Result { + let request_id = outgoing_request.0.request_id().to_string(); + + Ok(match outgoing_request.0.request() { + OutgoingRequests::KeysUpload(request) => { + JsValue::from(KeysUploadRequest::try_from((request_id, request))?) + } + + OutgoingRequests::KeysQuery(request) => { + JsValue::from(KeysQueryRequest::try_from((request_id, request))?) + } + + OutgoingRequests::KeysClaim(request) => { + JsValue::from(KeysClaimRequest::try_from((request_id, request))?) + } + + OutgoingRequests::ToDeviceRequest(request) => { + JsValue::from(ToDeviceRequest::try_from((request_id, request))?) + } + + OutgoingRequests::SignatureUpload(request) => { + JsValue::from(SignatureUploadRequest::try_from((request_id, request))?) + } + + OutgoingRequests::RoomMessage(request) => { + JsValue::from(RoomMessageRequest::try_from((request_id, request))?) + } + + OutgoingRequests::KeysBackup(request) => { + JsValue::from(KeysBackupRequest::try_from((request_id, request))?) + } + }) + } +} + +/// Represent the type of a request. +#[wasm_bindgen] +#[derive(Debug)] +pub enum RequestType { + /// Represents a `KeysUploadRequest`. + KeysUpload, + + /// Represents a `KeysQueryRequest`. + KeysQuery, + + /// Represents a `KeysClaimRequest`. + KeysClaim, + + /// Represents a `ToDeviceRequest`. + ToDevice, + + /// Represents a `SignatureUploadRequest`. + SignatureUpload, + + /// Represents a `RoomMessageRequest`. + RoomMessage, + + /// Represents a `KeysBackupRequest`. + KeysBackup, +} diff --git a/crates/matrix-sdk-crypto-js/src/responses.rs b/crates/matrix-sdk-crypto-js/src/responses.rs new file mode 100644 index 000000000..080347dbb --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/responses.rs @@ -0,0 +1,128 @@ +//! Types related to responses. + +use matrix_sdk_crypto::IncomingResponse; +pub(crate) use ruma::api::client::{ + backup::add_backup_keys::v3::Response as KeysBackupResponse, + keys::{ + claim_keys::v3::Response as KeysClaimResponse, get_keys::v3::Response as KeysQueryResponse, + upload_keys::v3::Response as KeysUploadResponse, + upload_signatures::v3::Response as SignatureUploadResponse, + }, + message::send_message_event::v3::Response as RoomMessageResponse, + to_device::send_event_to_device::v3::Response as ToDeviceResponse, +}; +use ruma::api::IncomingResponse as RumaIncomingResponse; +use wasm_bindgen::prelude::*; + +use crate::requests::RequestType; + +pub(crate) fn response_from_string(body: &str) -> http::Result>> { + http::Response::builder().status(200).body(body.as_bytes().to_vec()) +} + +/// Intermediate private type to store an incoming owned response, +/// without the need to manage lifetime. +pub(crate) enum OwnedResponse { + KeysUpload(KeysUploadResponse), + KeysQuery(KeysQueryResponse), + KeysClaim(KeysClaimResponse), + ToDevice(ToDeviceResponse), + SignatureUpload(SignatureUploadResponse), + RoomMessage(RoomMessageResponse), + KeysBackup(KeysBackupResponse), +} + +impl From for OwnedResponse { + fn from(response: KeysUploadResponse) -> Self { + OwnedResponse::KeysUpload(response) + } +} + +impl From for OwnedResponse { + fn from(response: KeysQueryResponse) -> Self { + OwnedResponse::KeysQuery(response) + } +} + +impl From for OwnedResponse { + fn from(response: KeysClaimResponse) -> Self { + OwnedResponse::KeysClaim(response) + } +} + +impl From for OwnedResponse { + fn from(response: ToDeviceResponse) -> Self { + OwnedResponse::ToDevice(response) + } +} + +impl From for OwnedResponse { + fn from(response: SignatureUploadResponse) -> Self { + Self::SignatureUpload(response) + } +} + +impl From for OwnedResponse { + fn from(response: RoomMessageResponse) -> Self { + OwnedResponse::RoomMessage(response) + } +} + +impl From for OwnedResponse { + fn from(r: KeysBackupResponse) -> Self { + Self::KeysBackup(r) + } +} + +impl TryFrom<(RequestType, http::Response>)> for OwnedResponse { + type Error = JsError; + + fn try_from( + (request_type, response): (RequestType, http::Response>), + ) -> Result { + match request_type { + RequestType::KeysUpload => { + KeysUploadResponse::try_from_http_response(response).map(Into::into) + } + + RequestType::KeysQuery => { + KeysQueryResponse::try_from_http_response(response).map(Into::into) + } + + RequestType::KeysClaim => { + KeysClaimResponse::try_from_http_response(response).map(Into::into) + } + + RequestType::ToDevice => { + ToDeviceResponse::try_from_http_response(response).map(Into::into) + } + + RequestType::SignatureUpload => { + SignatureUploadResponse::try_from_http_response(response).map(Into::into) + } + + RequestType::RoomMessage => { + RoomMessageResponse::try_from_http_response(response).map(Into::into) + } + + RequestType::KeysBackup => { + KeysBackupResponse::try_from_http_response(response).map(Into::into) + } + } + .map_err(JsError::from) + } +} + +impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> { + fn from(response: &'a OwnedResponse) -> Self { + match response { + OwnedResponse::KeysUpload(response) => IncomingResponse::KeysUpload(response), + OwnedResponse::KeysQuery(response) => IncomingResponse::KeysQuery(response), + OwnedResponse::KeysClaim(response) => IncomingResponse::KeysClaim(response), + OwnedResponse::ToDevice(response) => IncomingResponse::ToDevice(response), + OwnedResponse::SignatureUpload(response) => IncomingResponse::SignatureUpload(response), + OwnedResponse::RoomMessage(response) => IncomingResponse::RoomMessage(response), + OwnedResponse::KeysBackup(response) => IncomingResponse::KeysBackup(response), + } + } +} diff --git a/crates/matrix-sdk-crypto-js/src/sync_events.rs b/crates/matrix-sdk-crypto-js/src/sync_events.rs new file mode 100644 index 000000000..a94528197 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/src/sync_events.rs @@ -0,0 +1,67 @@ +//! `GET /_matrix/client/*/sync` + +use js_sys::Array; +use wasm_bindgen::prelude::*; + +use crate::{downcast, identifiers}; + +/// Information on E2E device updates. +#[wasm_bindgen] +#[derive(Debug)] +pub struct DeviceLists { + pub(crate) inner: ruma::api::client::sync::sync_events::v3::DeviceLists, +} + +#[wasm_bindgen] +impl DeviceLists { + /// Create an empty `DeviceLists`. + /// + /// `changed` and `left` must be an array of strings representing + /// a user ID. Ideally, we should pass a `UserId` object instance, + /// but it's a limitation of `wasm-bindgen` (a workaround is + /// possible but it will slow down performance). + #[wasm_bindgen(constructor)] + pub fn new(changed: Array, left: Array) -> Result { + let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default(); + + inner.changed = changed + .iter() + .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) + .collect::, JsError>>()?; + + inner.left = left + .iter() + .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) + .collect::, JsError>>()?; + + Ok(Self { inner }) + } + + /// Returns true if there are no device list updates. + #[wasm_bindgen(js_name = "isEmpty")] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// List of users who have updated their device identity keys or who now + /// share an encrypted room with the client since the previous sync + pub fn changed(&self) -> Array { + self.inner + .changed + .iter() + .map(|user| identifiers::UserId::new_with(user.clone())) + .map(JsValue::from) + .collect() + } + + /// List of users who no longer share encrypted rooms since the previous + /// sync response. + pub fn left(&self) -> Array { + self.inner + .left + .iter() + .map(|user| identifiers::UserId::new_with(user.clone())) + .map(JsValue::from) + .collect() + } +} diff --git a/crates/matrix-sdk-crypto-js/tests/js/events.js b/crates/matrix-sdk-crypto-js/tests/js/events.js new file mode 100644 index 000000000..881d86e06 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/tests/js/events.js @@ -0,0 +1,10 @@ +const { HistoryVisibility } = require('../../js/pkg/matrix_sdk_crypto'); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +test('HistoryVisibility', (t) => { + assert.equal(HistoryVisibility.Invited, 0); + assert.equal(HistoryVisibility.Joined, 1); + assert.equal(HistoryVisibility.Shared, 2); + assert.equal(HistoryVisibility.WorldReadable, 3); +}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/identifiers.js b/crates/matrix-sdk-crypto-js/tests/js/identifiers.js new file mode 100644 index 000000000..38442111a --- /dev/null +++ b/crates/matrix-sdk-crypto-js/tests/js/identifiers.js @@ -0,0 +1,37 @@ +const { UserId, DeviceId, RoomId, ServerName } = require('../../js/pkg/matrix_sdk_crypto'); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +test('UserId', (t) => { + assert.throws(() => { new UserId('@foobar') }, Error, 'An invalid user ID must throw an error'); + + const user = new UserId('@foo:bar.org'); + + assert.equal(user.localpart(), 'foo', 'Localpart is present'); + assert.ok(user.serverName() instanceof ServerName, 'Server name is present'); + assert.equal(user.isHistorical, false, 'User ID is not historical'); + assert.equal(user.toString(), '@foo:bar.org', 'Can read the user ID as a string'); +}); + +test('DeviceId', (t) => { + assert.equal(new DeviceId('foo').toString(), 'foo', 'Can read the device ID as a string'); +}); + +test('RoomId', (t) => { + assert.throws(() => { new UserId('!foo') }, Error, 'An invalid room ID must throw an error'); + + const room = new RoomId('!foo:bar.org'); + + assert.equal(room.localpart(), 'foo', 'Localpart is present'); + assert.ok(room.serverName() instanceof ServerName, 'Server name is present'); + assert.equal(room.toString(), '!foo:bar.org', 'Can read the room ID as a string'); +}); + +test('ServerName', (t) => { + assert.throws(() => { new ServerName('@foobar') }, Error, 'An invalid server name must throw an error'); + + assert.equal(new ServerName('foo.org').host(), 'foo.org', 'Host is present'); + assert.equal(new ServerName('foo.org').port(), undefined, 'Port is absent'); + assert.equal(new ServerName('foo.org:1234').port(), 1234, 'Port is present'); + assert.equal(new ServerName('foo.org').isIpLiteral(), false, 'Server name is not an IP literal'); +}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/machine.js b/crates/matrix-sdk-crypto-js/tests/js/machine.js new file mode 100644 index 000000000..d887a3ba4 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/tests/js/machine.js @@ -0,0 +1,119 @@ +const { EncryptionAlgorithm, EncryptionSettings, HistoryVisibility, UserId, DeviceId, OlmMachine, DeviceLists, KeysUploadRequest, KeysQueryRequest } = require('../../js/pkg/matrix_sdk_crypto'); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +test('EncryptionAlgorithm', (t) => { + assert.equal(EncryptionAlgorithm.OlmV1Curve25519AesSha2, 0); + assert.equal(EncryptionAlgorithm.MegolmV1AesSha2, 1); +}); + +test('EncryptionSettings', (t) => { + let es = new EncryptionSettings(); + + assert.equal(es.algorithm, EncryptionAlgorithm.MegolmV1AesSha2, 'It has a default algorithm'); + assert.equal(es.rotationPeriod, 604800000000n, 'It has a default rotation period'); + assert.equal(es.rotationPeriodMessages, 100n, 'It has a default message rotation period'); + assert.equal(es.historyVisibility, HistoryVisibility.Shared, 'It has a default history visibility'); + + es.algorithm = EncryptionSettings.OlmV1Curve25519AesSha2; + assert.equal(es.algorithm, EncryptionAlgorithm.OlmV1Curve25519AesSha2, 'It has a new algorithm'); + assert.throws(() => { es.algorithm = 42 }, Error, 'Enum values are validated'); + + es.rotationPeriod = 42n; + assert.equal(es.rotationPeriod, 42n, 'It has a new rotation period'); + + es.rotationPeriodMessages = 153n; + assert.equal(es.rotationPeriodMessages, 153n, 'It has a new message rotation period'); + + es.historyVisibility = HistoryVisibility.WorldReadable; + assert.equal(es.historyVisibility, HistoryVisibility.WorldReadable, 'It has a new history visibility'); + assert.throws(() => { es.historyVisibility = 42 }, Error, 'Enum values are validated'); +}); + +test('OlmMachine', async (t) => { + const user_id = new UserId('@foo:bar.org'); + const device_id = new DeviceId('baz'); + + await t.test('Construct', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + + assert.ok(machine instanceof OlmMachine); + assert.equal(machine.userId().toString(), '@foo:bar.org', 'User ID is present'); + assert.equal(machine.deviceId().toString(), 'baz', 'Device ID is present'); + }); + + await t.test('Identity keys', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + const identity_keys = machine.identityKeys(); + + assert.match(identity_keys.ed25519.toBase64(), /^[A-Za-z0-9+/]+$/, 'Ed25519 can be base64-encoded'); + assert.match(identity_keys.curve25519.toBase64(), /^[A-Za-z0-9+/]+$/, 'Curve25519 can be base64-encoded'); + assert.ok(identity_keys.curve25519.length > 0, 'Curve25519\'s length is greater than zero'); + }); + + await t.test('Display name', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + + assert.equal(await machine.displayName(), undefined, 'Display name is absent by default'); + }); + + await t.test('Tracked users', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + const tracked_users = machine.trackedUsers(); + + assert.ok(tracked_users instanceof Set, 'Tracket users are stored in a `Set`'); + assert.equal(tracked_users.size, 0, 'No tracked users by default'); + }); + + await t.test('Update tracked users', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + const update_tracked_users = await machine.updateTrackedUsers([new UserId('@foo:matrix.org'), new UserId('@bar:matrix.org')]); + + assert.equal(update_tracked_users, undefined, 'Updating tracked users returns nothing'); + }); + + await t.test('Receive sync changes', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + const to_device_events = JSON.stringify({}); + const changed_devices = new DeviceLists( + [new UserId('@foo:matrix.org'), new UserId('@bar:matrix.org')], + [new UserId('@baz:matrix.org'), new UserId('@qux:matrix.org')], + ); + const one_time_key_counts = new Map(); + one_time_key_counts.set('foo', 42); + one_time_key_counts.set('bar', 153); + const unused_fallback_keys = new Set(); + unused_fallback_keys.add('baz'); + unused_fallback_keys.add('qux'); + + const decrypted_to_device = JSON.parse( + await machine.receiveSyncChanges( + to_device_events, + changed_devices, + one_time_key_counts, + unused_fallback_keys, + ) + ); + + assert.deepEqual(decrypted_to_device, {}, 'Nothing to do by default'); + }); + + await t.test('Outgoing requests', async (t) => { + const machine = await new OlmMachine(user_id, device_id); + const outgoing_requests = await machine.outgoingRequests(); + + assert.ok(outgoing_requests instanceof Array, 'Outgoing requests are stored in an `Array`'); + assert.equal(outgoing_requests.length, 2, 'There is 2 outgoing requests'); + + const request1 = outgoing_requests[0]; + const request2 = outgoing_requests[1]; + + assert.ok(request1 instanceof KeysUploadRequest, 'First request is `KeysUploadRequest'); + assert.ok(request1.request_id.length > 0, 'First request has an ID'); + assert.ok(JSON.parse(request1.body) instanceof Object, 'First request has a valid body'); + + assert.ok(request2 instanceof KeysQueryRequest, 'Second request is `KeysQueryRequest`'); + assert.ok(request2.request_id.length > 0, 'Second request has an ID'); + assert.ok(JSON.parse(request2.body) instanceof Object, 'Second request has a valid body'); + }); +}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/requests.js b/crates/matrix-sdk-crypto-js/tests/js/requests.js new file mode 100644 index 000000000..00c82511a --- /dev/null +++ b/crates/matrix-sdk-crypto-js/tests/js/requests.js @@ -0,0 +1,41 @@ +const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../../js/pkg/matrix_sdk_crypto'); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +test('RequestType', (t) => { + assert.equal(RequestType.KeysUpload, 0); + assert.equal(RequestType.KeysQuery, 1); + assert.equal(RequestType.KeysClaim, 2); + assert.equal(RequestType.ToDevice, 3); + assert.equal(RequestType.SignatureUpload, 4); + assert.equal(RequestType.RoomMessage, 5); + assert.equal(RequestType.KeysBackup, 6); +}); + +test('KeysUploadRequest', (t) => { + assert.ok(new KeysUploadRequest()); +}); + +test('KeysQueryRequest', (t) => { + assert.ok(new KeysQueryRequest()); +}); + +test('KeysClaimRequest', (t) => { + assert.ok(new KeysClaimRequest()); +}); + +test('ToDeviceRequest', (t) => { + assert.ok(new ToDeviceRequest()); +}); + +test('SignatureUploadRequest', (t) => { + assert.ok(new SignatureUploadRequest()); +}); + +test('RoomMessageRequest', (t) => { + assert.ok(new RoomMessageRequest()); +}); + +test('KeysBackupRequest', (t) => { + assert.ok(new KeysBackupRequest()); +}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/sync_events.js b/crates/matrix-sdk-crypto-js/tests/js/sync_events.js new file mode 100644 index 000000000..46cf63e38 --- /dev/null +++ b/crates/matrix-sdk-crypto-js/tests/js/sync_events.js @@ -0,0 +1,23 @@ +const { DeviceLists, UserId } = require('../../js/pkg/matrix_sdk_crypto'); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +test('DeviceLists', (t) => { + const empty = new DeviceLists([], []); + + assert.equal(empty.isEmpty(), true, 'List is empty'); + assert.equal(empty.changed().length, 0, 'No user ID changed'); + assert.equal(empty.left().length, 0, 'No user ID left'); + + const list = new DeviceLists([new UserId('@foo:bar.org')], [new UserId('@baz:qux.org')]); + + assert.equal(list.isEmpty(), false, 'List is not empty'); + + const changed = list.changed(); + assert.equal(changed.length, 1, 'There is one user ID changed'); + assert.equal(changed[0].toString(), '@foo:bar.org', 'The user ID changed is correct'); + + const left = list.left(); + assert.equal(left.length, 1, 'There is one user ID left'); + assert.equal(left[0].toString(), '@baz:qux.org', 'The user ID left is correct'); +}); diff --git a/crates/matrix-sdk-crypto-nodejs/Cargo.toml b/crates/matrix-sdk-crypto-nodejs/Cargo.toml new file mode 100644 index 000000000..a21878de1 --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/Cargo.toml @@ -0,0 +1,34 @@ +[package] +authors = ["Ivan Enderlin "] +description = "Matrix encryption library, for NodeJS" +edition = "2021" +homepage = "https://github.com/matrix-org/matrix-rust-sdk" +keywords = ["matrix", "chat", "messaging", "ruma", "nio"] +license = "Apache-2.0" +name = "matrix-sdk-crypto-nodejs" +readme = "README.md" +repository = "https://github.com/matrix-org/matrix-rust-sdk" +rust-version = "1.60" +version = "0.5.0" + +[package.metadata.docs.rs] +features = ["docsrs"] +rustdoc-args = ["--cfg", "docsrs"] + +[lib] +crate-type = ["cdylib"] + +[features] +default = [] +qrcode = ["matrix-sdk-crypto/qrcode"] +docsrs = [] + +[dependencies] +matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto" } +ruma = { version = "0.6.2", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = "0.2.0" +napi = { git = "https://github.com/Hywan/napi-rs", branch = "feat-tonapivalue-u16", default-features = false, features = ["napi4"] } +napi-derive = "2.4.1" + +[build-dependencies] +napi-build = "2.0.0" \ No newline at end of file diff --git a/crates/matrix-sdk-crypto-nodejs/README.md b/crates/matrix-sdk-crypto-nodejs/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/crates/matrix-sdk-crypto-nodejs/build.rs b/crates/matrix-sdk-crypto-nodejs/build.rs new file mode 100644 index 000000000..0f1b01002 --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/crates/matrix-sdk-crypto-nodejs/nodejs/Makefile b/crates/matrix-sdk-crypto-nodejs/nodejs/Makefile new file mode 100644 index 000000000..f2091d5bd --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/nodejs/Makefile @@ -0,0 +1,9 @@ +build: + cd .. && napi build --platform --release + test -f ../index.js && mv ../index.js pkg/ || true + test -f ../index.d.ts && mv ../index.d.ts pkg/ || true + test -f ../matrix-sdk-crypto.*.node && mv ../matrix-sdk-crypto.*.node pkg/ || true + echo '*' > pkg/.gitignore + +test: + echo 'nop' diff --git a/crates/matrix-sdk-crypto-nodejs/package.json b/crates/matrix-sdk-crypto-nodejs/package.json new file mode 100644 index 000000000..be38b651c --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/package.json @@ -0,0 +1,27 @@ +{ + "name": "matrix-sdk-crypto", + "version": "0.5.0", + "main": "index.js", + "types": "index.d.ts", + "napi": { + "name": "matrix-sdk-crypto", + "triples": { + "additional": [ + "aarch64-apple-darwin" + ] + } + }, + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^2.9.0", + "ava": "^4.2.0" + }, + "engines": { + "node": ">= 10" + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release", + "test": "ava" + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/src/errors.rs b/crates/matrix-sdk-crypto-nodejs/src/errors.rs new file mode 100644 index 000000000..3b35a55ce --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/src/errors.rs @@ -0,0 +1,18 @@ +/// Generic error wrapping `napi::Error`. +#[derive(Debug)] +pub struct Error(napi::Error); + +impl From for Error +where + E: std::error::Error, +{ + fn from(error: E) -> Self { + Self(napi::Error::from_reason(error.to_string())) + } +} + +impl From for napi::Error { + fn from(value: Error) -> Self { + value.0 + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/src/events.rs b/crates/matrix-sdk-crypto-nodejs/src/events.rs new file mode 100644 index 000000000..ce6916d55 --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/src/events.rs @@ -0,0 +1,62 @@ +//! Types related to events. + +use napi::bindgen_prelude::ToNapiValue; +use napi_derive::*; +use ruma::events::room::history_visibility::HistoryVisibility as RumaHistoryVisibility; + +/// Who can see a room's history. +#[napi] +#[derive(Debug)] +pub enum HistoryVisibility { + /// Previous events are accessible to newly joined members from + /// the point they were invited onwards. + /// + /// Events stop being accessible when the member's state changes + /// to something other than *invite* or *join*. + Invited, + + /// Previous events are accessible to newly joined members from + /// the point they joined the room onwards. + /// + /// Events stop being accessible when the member's state changes + /// to something other than *join*. + Joined, + + /// Previous events are always accessible to newly joined members. + /// + /// All events in the room are accessible, even those sent when + /// the member was not a part of the room. + Shared, + + /// All events while this is the `HistoryVisibility` value may be + /// shared by any participating homeserver with anyone, regardless + /// of whether they have ever joined the room. + WorldReadable, +} + +impl From for RumaHistoryVisibility { + fn from(value: HistoryVisibility) -> Self { + use HistoryVisibility::*; + + match value { + Invited => Self::Invited, + Joined => Self::Joined, + Shared => Self::Shared, + WorldReadable => Self::WorldReadable, + } + } +} + +impl From for HistoryVisibility { + fn from(value: RumaHistoryVisibility) -> Self { + use RumaHistoryVisibility::*; + + match value { + Invited => Self::Invited, + Joined => Self::Joined, + Shared => Self::Shared, + WorldReadable => Self::WorldReadable, + _ => unreachable!("Unknown variant"), + } + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/src/identifiers.rs b/crates/matrix-sdk-crypto-nodejs/src/identifiers.rs new file mode 100644 index 000000000..2a1f9bf8e --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/src/identifiers.rs @@ -0,0 +1,171 @@ +//! Types for [Matrix](https://matrix.org/) identifiers for devices, +//! events, keys, rooms, servers, users and URIs. + +use napi_derive::*; + +use crate::errors::*; + +/// A Matrix [user ID]. +/// +/// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers +#[napi] +#[derive(Debug, Clone)] +pub struct UserId { + pub(crate) inner: ruma::OwnedUserId, +} + +#[napi] +impl UserId { + /// Parse/validate and create a new `UserId`. + #[napi(constructor)] + pub fn new(id: String) -> Result { + Ok(Self { + inner: ruma::UserId::parse(id.as_str()) + .map_err(Error::from) + .map_err(Into::::into)?, + }) + } + + /// Returns the user's localpart. + #[napi] + pub fn localpart(&self) -> String { + self.inner.localpart().to_owned() + } + + /// Returns the server name of the user ID. + #[napi(js_name = "serverName")] + pub fn server_name(&self) -> ServerName { + ServerName { inner: self.inner.server_name().to_owned() } + } + + /// Whether this user ID is a historical one. + /// + /// A historical user ID is one that doesn't conform to the latest + /// specification of the user ID grammar but is still accepted + /// because it was previously allowed. + #[napi(getter, js_name = "isHistorical")] + pub fn is_historical(&self) -> bool { + self.inner.is_historical() + } + + /// Return the user ID as a string. + #[napi(js_name = "toString")] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.inner.as_str().to_owned() + } +} + +/// A Matrix key ID. +/// +/// Device identifiers in Matrix are completely opaque character +/// sequences. This type is provided simply for its semantic value. +#[napi] +#[derive(Debug, Clone)] +pub struct DeviceId { + pub(crate) inner: ruma::OwnedDeviceId, +} + +#[napi] +impl DeviceId { + /// Create a new `DeviceId`. + #[napi(constructor)] + pub fn new(id: String) -> DeviceId { + Self { inner: id.into() } + } + + /// Return the device ID as a string. + #[napi(js_name = "toString")] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.inner.as_str().to_owned() + } +} + +/// A Matrix [room ID]. +/// +/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids +#[napi] +#[derive(Debug, Clone)] +pub struct RoomId { + pub(crate) inner: ruma::OwnedRoomId, +} + +#[napi] +impl RoomId { + /// Parse/validate and create a new `RoomId`. + #[napi(constructor)] + pub fn new(id: String) -> Result { + Ok(Self { + inner: ruma::RoomId::parse(id) + .map_err(Error::from) + .map_err(Into::::into)?, + }) + } + + /// Returns the user's localpart. + #[napi] + pub fn localpart(&self) -> String { + self.inner.localpart().to_owned() + } + + /// Returns the server name of the room ID. + #[napi(js_name = "serverName")] + pub fn server_name(&self) -> ServerName { + ServerName { inner: self.inner.server_name().to_owned() } + } + + /// Return the room ID as a string. + #[napi(js_name = "toString")] + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + self.inner.as_str().to_owned() + } +} + +/// A Matrix-spec compliant [server name]. +/// +/// It consists of a host and an optional port (separated by a colon if +/// present). +/// +/// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name +#[napi] +#[derive(Debug)] +pub struct ServerName { + inner: ruma::OwnedServerName, +} + +#[napi] +impl ServerName { + /// Parse/validate and create a new `ServerName`. + #[napi(constructor)] + pub fn new(name: String) -> Result { + Ok(Self { + inner: ruma::ServerName::parse(name) + .map_err(Error::from) + .map_err(Into::::into)?, + }) + } + + /// Returns the host of the server name. + /// + /// That is: Return the part of the server before `:` or the + /// full server name if there is no port. + #[napi] + pub fn host(&self) -> String { + self.inner.host().to_owned() + } + + /// Returns the port of the server name if any. + #[napi] + pub fn port(&self) -> Option { + self.inner.port() + } + + /// Returns true if and only if the server name is an IPv4 or IPv6 + /// address. + #[napi(js_name = "isIpLiteral")] + pub fn is_ip_literal(&self) -> bool { + self.inner.is_ip_literal() + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/src/lib.rs b/crates/matrix-sdk-crypto-nodejs/src/lib.rs new file mode 100644 index 000000000..527863a64 --- /dev/null +++ b/crates/matrix-sdk-crypto-nodejs/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +//#![warn(missing_docs, missing_debug_implementations)] + +mod errors; +pub mod events; +//mod future; +pub mod identifiers; +//pub mod machine; +//pub mod requests; +//pub mod responses; +//pub mod sync_events; + +pub use crate::errors::Error; diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index df7b71915..e3c75bfac 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -48,22 +48,13 @@ thiserror = "1.0.30" tracing = "0.1.34" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } -[target.'cfg(target_arch = "wasm32")'.dependencies.ruma] -version = "0.6.1" -features = ["client-api-c", "js", "rand", "signatures", "unstable-msc2676", "unstable-msc2677"] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +ruma = { version = "0.6.2", features = ["client-api-c", "rand", "signatures", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36" } -[target.'cfg(target_arch = "wasm32")'.dependencies.vodozemac] -git = "https://github.com/matrix-org/vodozemac/" -rev = "d0e744287a14319c2a9148fef3747548c740fc36" -features = ["js"] - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.ruma] -version = "0.6.1" -features = ["client-api-c", "rand", "signatures", "unstable-msc2676", "unstable-msc2677"] - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.vodozemac] -git = "https://github.com/matrix-org/vodozemac/" -rev = "d0e744287a14319c2a9148fef3747548c740fc36" +[target.'cfg(target_arch = "wasm32")'.dependencies] +ruma = { version = "0.6.2", features = ["client-api-c", "js", "rand", "signatures", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36", features = ["js"] } [dev-dependencies] futures = { version = "0.3.21", default-features = false, features = ["executor"] } @@ -73,8 +64,4 @@ matches = "0.1.9" matrix-sdk-test = { version = "0.5.0", path = "../matrix-sdk-test" } proptest = { version = "1.0.0", default-features = false, features = ["std"] } # required for async_test macro -tokio = { version = "1.7.0", default-features = false, features = ["macros", "rt-multi-thread"] } - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -getrandom = { version = "0.2.6", features = ["js"] } -wasm-bindgen-test = "0.3.24" +tokio = { version = "1.7.0", default-features = false, features = ["macros", "rt-multi-thread"] } \ No newline at end of file diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 3d57bcc6c..6954fc64a 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -24,7 +24,7 @@ mod gossiping; mod identities; mod machine; pub mod olm; -mod requests; +pub mod requests; mod session_manager; pub mod store; pub mod types; diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 1dca0e289..d581267ab 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -1505,6 +1505,7 @@ impl OlmMachine { &self.backup_machine } } + #[cfg(any(feature = "testing", test))] pub(crate) mod testing { #![allow(dead_code)] diff --git a/crates/matrix-sdk-crypto/src/requests.rs b/crates/matrix-sdk-crypto/src/requests.rs index 8084f15d7..67ca3f9b3 100644 --- a/crates/matrix-sdk-crypto/src/requests.rs +++ b/crates/matrix-sdk-crypto/src/requests.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Modules containing customized request types. + use std::{collections::BTreeMap, iter, sync::Arc, time::Duration}; use ruma::{ diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 08c340cbb..fec35730c 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -66,7 +66,7 @@ enum WasmFeatureSet { MatrixSdkNoDefault, MatrixSdkBase, MatrixSdkCommon, - MatrixSdkCrypto, + MatrixSdkCryptoJs, MatrixSdkIndexeddbStoresNoCrypto, MatrixSdkIndexeddbStores, IndexeddbNoCrypto, @@ -208,7 +208,7 @@ fn run_wasm_checks(cmd: Option) -> Result<()> { ), (WasmFeatureSet::MatrixSdkBase, "-p matrix-sdk-base"), (WasmFeatureSet::MatrixSdkCommon, "-p matrix-sdk-common"), - (WasmFeatureSet::MatrixSdkCrypto, "-p matrix-sdk-crypto"), + (WasmFeatureSet::MatrixSdkCryptoJs, "-p matrix-sdk-crypto-js"), ( WasmFeatureSet::MatrixSdkIndexeddbStoresNoCrypto, "-p matrix-sdk --no-default-features --features indexeddb,rustls-tls", @@ -269,7 +269,7 @@ fn run_wasm_pack_tests(cmd: Option) -> Result<()> { ), (WasmFeatureSet::MatrixSdkBase, ("matrix-sdk-base", "")), (WasmFeatureSet::MatrixSdkCommon, ("matrix-sdk-common", "")), - (WasmFeatureSet::MatrixSdkCrypto, ("matrix-sdk-crypto", "")), + (WasmFeatureSet::MatrixSdkCryptoJs, ("matrix-sdk-crypto-js", "")), ( WasmFeatureSet::MatrixSdkIndexeddbStoresNoCrypto, ("matrix-sdk", "--no-default-features --features indexeddb,rustls-tls --lib"),