Merge pull request #675 from Hywan/feat-crypto-wasm

feat(crypto): Port to Wasm (in a JS host) and to NodeJS
This commit is contained in:
Ivan Enderlin
2022-05-31 10:01:57 +02:00
committed by GitHub
35 changed files with 1943 additions and 29 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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'
+2 -2
View File
@@ -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'
+3 -1
View File
@@ -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
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
+38
View File
@@ -0,0 +1,38 @@
[package]
authors = ["Ivan Enderlin <ivane@element.io>"]
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"
+5
View File
@@ -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
+61
View File
@@ -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<HistoryVisibility> 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<RumaHistoryVisibility> 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"),
}
}
}
+26
View File
@@ -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<F, T>(future: F) -> Promise
where
F: Future<Output = Result<T, anyhow::Error>> + 'static,
T: Into<JsValue>,
{
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()
}
};
});
})
}
@@ -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<UserId, JsError> {
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<RoomId, JsError> {
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<ServerName, JsError> {
Ok(Self { inner: ruma::ServerName::parse(name)? })
}
/// Returns the host of the server name.
///
/// That is: Return the part of the server before `:<port>` 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<u16> {
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()
}
}
+56
View File
@@ -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<T>`.
fn downcast<T>(value: &JsValue, classname: &str) -> Result<T::Anchor, JsError>
where
T: RefFromWasmAbi<Abi = u32>,
{
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,
)))
}
}
+519
View File
@@ -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<UserId>`.
#[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<Promise, JsError> {
let users = users
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, 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<Promise, JsError> {
let to_device_events = serde_json::from_str(to_device_events)?;
let changed_devices = changed_devices.inner.clone();
let one_time_key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = 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<Vec<DeviceKeyAlgorithm>> = 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::<Result<Vec<JsValue>, _>>()?
.into_iter()
.collect::<Array>())
})
}
/// 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<Promise, JsError> {
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<Promise, JsError> {
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<Promise, JsError> {
let room_id = room_id.inner.clone();
let users = users
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, 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 its 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<Promise, JsError> {
let users = users
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, 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<matrix_sdk_crypto::olm::IdentityKeys> 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<EncryptionAlgorithm> for ruma::EventEncryptionAlgorithm {
fn from(value: EncryptionAlgorithm) -> Self {
use EncryptionAlgorithm::*;
match value {
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
}
}
}
impl From<ruma::EventEncryptionAlgorithm> 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(),
}
}
}
+267
View File
@@ -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<Self, Self::Error> {
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<OutgoingRequest> for JsValue {
type Error = serde_json::Error;
fn try_from(outgoing_request: OutgoingRequest) -> Result<Self, Self::Error> {
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,
}
@@ -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<Vec<u8>>> {
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<KeysUploadResponse> for OwnedResponse {
fn from(response: KeysUploadResponse) -> Self {
OwnedResponse::KeysUpload(response)
}
}
impl From<KeysQueryResponse> for OwnedResponse {
fn from(response: KeysQueryResponse) -> Self {
OwnedResponse::KeysQuery(response)
}
}
impl From<KeysClaimResponse> for OwnedResponse {
fn from(response: KeysClaimResponse) -> Self {
OwnedResponse::KeysClaim(response)
}
}
impl From<ToDeviceResponse> for OwnedResponse {
fn from(response: ToDeviceResponse) -> Self {
OwnedResponse::ToDevice(response)
}
}
impl From<SignatureUploadResponse> for OwnedResponse {
fn from(response: SignatureUploadResponse) -> Self {
Self::SignatureUpload(response)
}
}
impl From<RoomMessageResponse> for OwnedResponse {
fn from(response: RoomMessageResponse) -> Self {
OwnedResponse::RoomMessage(response)
}
}
impl From<KeysBackupResponse> for OwnedResponse {
fn from(r: KeysBackupResponse) -> Self {
Self::KeysBackup(r)
}
}
impl TryFrom<(RequestType, http::Response<Vec<u8>>)> for OwnedResponse {
type Error = JsError;
fn try_from(
(request_type, response): (RequestType, http::Response<Vec<u8>>),
) -> Result<Self, Self::Error> {
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),
}
}
}
@@ -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<DeviceLists, JsError> {
let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default();
inner.changed = changed
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
inner.left = left
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, 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()
}
}
@@ -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);
});
@@ -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');
});
@@ -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');
});
});
@@ -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());
});
@@ -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');
});
@@ -0,0 +1,34 @@
[package]
authors = ["Ivan Enderlin <ivane@element.io>"]
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"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
napi_build::setup();
}
@@ -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'
@@ -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"
}
}
@@ -0,0 +1,18 @@
/// Generic error wrapping `napi::Error`.
#[derive(Debug)]
pub struct Error(napi::Error);
impl<E> From<E> for Error
where
E: std::error::Error,
{
fn from(error: E) -> Self {
Self(napi::Error::from_reason(error.to_string()))
}
}
impl From<Error> for napi::Error {
fn from(value: Error) -> Self {
value.0
}
}
@@ -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<HistoryVisibility> 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<RumaHistoryVisibility> 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"),
}
}
}
@@ -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<UserId, napi::Error> {
Ok(Self {
inner: ruma::UserId::parse(id.as_str())
.map_err(Error::from)
.map_err(Into::<napi::Error>::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<RoomId, napi::Error> {
Ok(Self {
inner: ruma::RoomId::parse(id)
.map_err(Error::from)
.map_err(Into::<napi::Error>::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<ServerName, napi::Error> {
Ok(Self {
inner: ruma::ServerName::parse(name)
.map_err(Error::from)
.map_err(Into::<napi::Error>::into)?,
})
}
/// Returns the host of the server name.
///
/// That is: Return the part of the server before `:<port>` 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<u16> {
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()
}
}
@@ -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;
+7 -20
View File
@@ -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"] }
+1 -1
View File
@@ -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;
+1
View File
@@ -1505,6 +1505,7 @@ impl OlmMachine {
&self.backup_machine
}
}
#[cfg(any(feature = "testing", test))]
pub(crate) mod testing {
#![allow(dead_code)]
+2
View File
@@ -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::{
+3 -3
View File
@@ -66,7 +66,7 @@ enum WasmFeatureSet {
MatrixSdkNoDefault,
MatrixSdkBase,
MatrixSdkCommon,
MatrixSdkCrypto,
MatrixSdkCryptoJs,
MatrixSdkIndexeddbStoresNoCrypto,
MatrixSdkIndexeddbStores,
IndexeddbNoCrypto,
@@ -208,7 +208,7 @@ fn run_wasm_checks(cmd: Option<WasmFeatureSet>) -> 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<WasmFeatureSet>) -> 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"),