Merge remote-tracking branch 'origin/main' into gnunicorn/issue756

This commit is contained in:
Benjamin Kampmann
2022-07-11 15:16:31 +02:00
190 changed files with 7220 additions and 3328 deletions
+12 -4
View File
@@ -1,4 +1,4 @@
name: Appservice
name: AppService
on:
push:
@@ -17,13 +17,18 @@ env:
jobs:
test-appservice:
if: github.event_name == 'push' || !github.event.pull_request.draft
name: ${{ matrix.os }} / appservice / stable
name: ${{ matrix.os-name }} [m]-appservice
runs-on: ${{ matrix.os }}-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu, macOS]
include:
- os: ubuntu-latest
os-name: 🐧
- os: macos-latest
os-name: 🍏
steps:
- name: Checkout
@@ -39,6 +44,9 @@ jobs:
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Run checks
uses: actions-rs/cargo@v1
with:
+172
View File
@@ -0,0 +1,172 @@
name: Bindings tests
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
CARGO_TERM_COLOR: always
MATRIX_SDK_CRYPTO_NODEJS_PATH: bindings/matrix-sdk-crypto-nodejs
MATRIX_SDK_CRYPTO_JS_PATH: bindings/matrix-sdk-crypto-js
jobs:
test-matrix-sdk-crypto-nodejs:
name: ${{ matrix.os-name }} [m]-crypto-nodejs, v${{ matrix.node-version }}
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [14.0, 16.0, 18.0]
include:
- os: ubuntu-latest
os-name: 🐧
- os: macos-latest
os-name: 🍏
- node-version: 18.0
build-doc: true
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install NPM dependencies
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm install
- name: Build the Node.js binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm run release-build
- name: Test the Node.js binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm run test
# Building in dev-mode and copy lib in failure case
- name: Build the Node.js binding in non-release
if: failure()
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: |
cp *.node release-mode-lib.node
npm run build
- uses: actions/upload-artifact@v3
if: failure()
with:
name: Failure Files
path: |
bindings/matrix-sdk-crypto-nodejs/*.node
/var/crash/*.crash
- if: ${{ matrix.build-doc }}
name: Build the documentation
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm run doc
test-matrix-sdk-crypto-js:
name: 🕸 [m]-crypto-js
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Node.js
uses: actions/setup-node@v3
- name: Install NPM dependencies
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm install
- name: Build the WebAssembly + JavaScript binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run build
- name: Test the JavaScript binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run test
- name: Build the documentation
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run doc
test-apple:
name: matrix-rust-components-swift
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- name: Install targets
run: |
rustup target add aarch64-apple-ios-sim --toolchain nightly
rustup target add x86_64-apple-ios --toolchain nightly
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Uniffi
uses: actions-rs/cargo@v1
with:
command: install
# keep in sync with uniffi dependency in Cargo.toml's
args: uniffi_bindgen --version ^0.18
- name: Generate .xcframework
run: sh bindings/apple/debug_build_xcframework.sh ci
- name: Run XCTests
run: |
xcodebuild test \
-project bindings/apple/MatrixRustSDK.xcodeproj \
-scheme MatrixRustSDK \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 13,OS=15.4'
+82 -39
View File
@@ -1,4 +1,4 @@
name: CI
name: Rust tests
on:
workflow_dispatch:
@@ -16,8 +16,8 @@ env:
CARGO_TERM_COLOR: always
jobs:
test-features:
name: linux / features-${{ matrix.name }}
test-matrix-sdk-features:
name: 🐧 [m], ${{ matrix.name }}
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
@@ -48,14 +48,17 @@ jobs:
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Test
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci test-features ${{ matrix.name }}
test-crypto-features:
name: linux / crypto-crate features
test-matrix-sdk-crypto:
name: 🐧 [m]-crypto
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
@@ -73,33 +76,35 @@ jobs:
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Clippy
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Test
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci test-crypto
test:
test-all-crates:
name: ${{ matrix.name }}
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
name:
- linux / stable
- linux / beta
- macOS / stable
include:
- name: linux / stable
- name: 🐧 all crates, 🦀 stable
rust: stable
os: ubuntu-latest
- name: linux / beta
- name: 🐧 all crates, 🦀 beta
rust: beta
os: ubuntu-latest
- name: macOS / stable
os: macOS-latest
- name: 🍏 all crates, 🦀 stable
rust: stable
os: macos-latest
steps:
- name: Checkout
@@ -108,30 +113,64 @@ jobs:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust || 'stable' }}
toolchain: ${{ matrix.rust }}
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
command: nextest
args: run --workspace
test-nodejs:
name: linux / node.js (${{ matrix.node-version }})
- name: Test documentation
uses: actions-rs/cargo@v1
with:
command: test
args: --doc
test-wasm:
name: 🕸️ ${{ matrix.name }}
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
node-version: [14.0, 16.0, 18.0]
include:
- node-version: 18.0
build-doc: true
- name: '[m]-qrcode'
cmd: matrix-sdk-qrcode
- name: '[m]-base'
cmd: matrix-sdk-base
- name: '[m]-common'
cmd: matrix-sdk-common
- name: '[m]-indexeddb, no crypto'
cmd: indexeddb-no-crypto
- name: '[m]-indexeddb, with crypto'
cmd: indexeddb-with-crypto
- name: '[m], no-default, wasm-flags'
cmd: matrix-sdk-no-default
- name: '[m], indexeddb stores'
cmd: matrix-sdk-indexeddb-stores
- name: '[m], indexeddb stores, no crypto'
cmd: matrix-sdk-indexeddb-stores-no-crypto
- name: '[m], wasm-example'
cmd: matrix-sdk-command-bot
steps:
- name: Checkout the repo
@@ -141,26 +180,30 @@ jobs:
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
components: clippy
profile: minimal
override: true
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: latest
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Node.js
uses: actions/setup-node@v3
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Rust Check
uses: actions-rs/cargo@v1
with:
node-version: ${{ matrix.node-version }}
command: run
args: -p xtask -- ci wasm ${{ matrix.cmd }}
- name: Install NPM dependencies
run: cd crates/matrix-sdk-crypto-nodejs && npm install
- name: Build the Node.js binding
run: cd crates/matrix-sdk-crypto-nodejs && npm run build
- name: Test the Node.js binding
run: cd crates/matrix-sdk-crypto-nodejs && npm run test
- if: ${{ matrix.build-doc }}
name: Build the documentation
run: cd crates/matrix-sdk-crypto-nodejs && npm run doc
- name: Wasm-Pack test
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci wasm-pack ${{ matrix.cmd }}
@@ -1,4 +1,4 @@
name: Docs
name: Documentation
on:
push:
@@ -7,7 +7,7 @@ on:
jobs:
docs:
name: Docs
name: All crates
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
@@ -26,7 +26,7 @@ jobs:
uses: Swatinem/rust-cache@v1
# Keep in sync with xtask docs
- name: Build docs
- name: Build documentation
uses: actions-rs/cargo@v1
env:
# Work around https://github.com/rust-lang/cargo/issues/10744
@@ -34,9 +34,9 @@ jobs:
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
with:
command: doc
args: --no-deps --workspace --exclude matrix-sdk-crypto-js --exclude matrix-sdk-crypto-nodejs --features docsrs
args: --no-deps --workspace --features docsrs
- name: Deploy docs
- name: Deploy documentation
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
-58
View File
@@ -1,58 +0,0 @@
name: FFI
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run Apple platform tests
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- name: Install targets
run: |
rustup target add aarch64-apple-ios-sim --toolchain nightly
rustup target add x86_64-apple-ios --toolchain nightly
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Uniffi
uses: actions-rs/cargo@v1
with:
command: install
args: uniffi_bindgen
- name: Generate .xcframework
run: sh bindings/apple/debug_build_xcframework.sh ci
- name: Run XCTests
run: |
xcodebuild test \
-project bindings/apple/MatrixRustSDK.xcodeproj \
-scheme MatrixRustSDK \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 13,OS=15.4'
-75
View File
@@ -1,75 +0,0 @@
name: WASM
on:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
CARGO_TERM_COLOR: always
jobs:
check-wasm:
name: Build test / ${{ matrix.name }}
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
strategy:
fail-fast: true
matrix:
name:
- matrix-sdk-qrcode
- matrix-sdk-base
- matrix-sdk-common
- matrix-sdk-crypto-js
- indexeddb-no-crypto
- indexeddb-with-crypto
include:
- name: matrix-sdk (no-default, wasm-flags)
cmd: matrix-sdk-no-default
- name: matrix-sdk / indexeddb_stores
cmd: matrix-sdk-indexeddb-stores
- name: matrix-sdk / indexeddb_stores / no crypto
cmd: matrix-sdk-indexeddb-stores-no-crypto
- name: matrix-sdk / wasm-example
cmd: matrix-sdk-command-bot
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
components: clippy
profile: minimal
override: true
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: 'latest'
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Rust Check
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci wasm ${{ matrix.cmd || matrix.name }}
- name: Wasm-Pack test
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci wasm-pack ${{ matrix.cmd || matrix.name }}
+3
View File
@@ -8,3 +8,6 @@ emsdk-*
## User settings
xcuserdata/
.vscode/
## OS garbage
.DS_Store
+12 -2
View File
@@ -1,6 +1,15 @@
[workspace]
members = ["benchmarks", "crates/*", "labs/*", "xtask"]
# xtask and labs should only be compiled when invoked explicitly
members = [
"benchmarks",
"bindings/matrix-sdk-crypto-ffi",
"bindings/matrix-sdk-crypto-js",
"bindings/matrix-sdk-crypto-nodejs",
"bindings/matrix-sdk-ffi",
"crates/*",
"labs/*",
"xtask",
]
# xtask, labs and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*"]
resolver = "2"
@@ -11,3 +20,4 @@ lto = true
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
quote = { opt-level = 2 }
sha2 = { opt-level = 2 }
+6
View File
@@ -36,6 +36,12 @@ the API will change in breaking ways.
If you are interested in using the matrix-sdk now is the time to try it out and
provide feedback.
## Bindings
Some crates of the **matrix-rust-sdk** can be embedded inside other
environments, like Swift, Kotlin, JavaScript, Node.js etc. Please,
explore the [`bindings/`](./bindings/) directory to learn more.
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
+1 -1
View File
@@ -12,7 +12,7 @@ criterion = { version = "0.3.5", features = ["async", "async_tokio", "html_repor
matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.5.0" }
matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.1.0", default-features = false, features = ["crypto-store"] }
matrix-sdk-test = { path = "../crates/matrix-sdk-test", version = "0.5.0" }
ruma = "0.6.1"
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" }
serde_json = "1.0.79"
tempfile = "3.3.0"
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] }
+22
View File
@@ -0,0 +1,22 @@
# Matrix Rust SDK bindings
In this directory, one can find bindings to the Rust SDK that are
maintained by the owners of the Matrix Rust SDK project.
* [`apple`] or `matrix-rust-components-swift`, Swift bindings of the
[`matrix-sdk`] crate via [`matrix-sdk-ffi`],
* [`matrix-sdk-crypto-ffi`], bindings of the [`matrix-sdk-crypto`]
crate,
* [`matrix-sdk-crypto-js`], JavaScript bindings of the
[`matrix-sdk-crypto`] crate,
* [`matrix-sdk-crypto-nodejs`], Node.js bindings of the
[`matrix-sdk-crypto`] crate,
* [`matrix-sdk-ffi`], bindings of the [`matrix-sdk`] crate,
[`apple`]: ./apple
[`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi
[`matrix-sdk-crypto-js`]: ../crates/matrix-sdk-crypto
[`matrix-sdk-crypto-nodejs`]: ../crates/matrix-sdk-crypto
[`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
[`matrix-sdk`]: ../crates/matrix-sdk
@@ -10,23 +10,14 @@ import XCTest
class MatrixRustSDKTests: XCTestCase {
static var client: Client!
override class func setUp() {
client = try! guestClient(basePath: basePath, homeserver: "https://matrix.org")
}
func testClientProperties() {
XCTAssertTrue(Self.client.isGuest())
XCTAssertNotNil(try? Self.client.restoreToken())
XCTAssertNotNil(try? Self.client.deviceId())
XCTAssertNotNil(try? Self.client.displayName())
}
func testReadOnlyFileSystemError() {
do {
let _ = try loginNewClient(basePath: "", username: "test", password: "test")
let client = try ClientBuilder()
.basePath(path: "")
.username(username: "@test:domain")
.build()
try client.login(username: "@test:domain", password: "test")
} catch ClientError.Generic(let message) {
XCTAssertNotNil(message.range(of: "Read-only file system"))
} catch {
+17
View File
@@ -0,0 +1,17 @@
Pod::Spec.new do |s|
s.name = "MatrixSDKCrypto"
s.version = "0.1.0"
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
s.author = { "matrix.org" => "support@matrix.org" }
s.ios.deployment_target = "11.0"
s.swift_versions = ['5.0']
s.source = { :http => "https://github.com/matrix-org/matrix-rust-sdk/releases/download/matrix-sdk-crypto-ffi-#{s.version}/MatrixSDKCryptoFFI.zip" }
s.vendored_frameworks = "MatrixSDKCryptoFFI.xcframework"
s.source_files = "Sources/**/*.{swift}"
end
+30 -16
View File
@@ -1,29 +1,42 @@
# Apple platforms support
This project and build script demonstrate how to create an XCFramework that can be imported into an Xcode project and run on Apple platforms.
This project and build script demonstrate how to create an XCFramework that can be imported into an Xcode project and run on Apple platforms. It can compile and bundle an [entire SDK](#Building-the-SDK), or only a smaller [Crypto module](#Building-only-the-Crypto-SDK) that provides end-to-end encryption for clients that already depend on an SDK (e.g. [Matrix iOS SDK](https://github.com/matrix-org/matrix-ios-sdk))
## Building the universal framework
## Prerequisites for building universal frameworks
* the Rust toolchain
* UniFFI - `cargo install uniffi_bindgen`
* Apple targets (e.g. `rustup target add aarch64-apple-ios`)
* `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html)
* `lipo` for creating the fat static libs
## Building the SDK
```
sh build_xcframework.sh
```
**Prerequisites**
* the Rust toolchain
* UniFFI - `cargo install uniffi_bindge`
* Apple targets (e.g. `rustup target add aarch64-apple-ios`)
* `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html)
* `lipo` for creating the fat static libs
The `build_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`:
1. compile `matrix-sdk-ffi` libraries for iOS, the iOS simulator, MacOS, and Mac Catalyst under `/target`. Some targets are not part of the standard library and they will be built using the nightly toolchain.
* `lipo` together the libraries for the same platform under `/generated`
* run `uniffi` and generate the C header, module map and swift files
* `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKFFI.xcframework`
* cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework)
2. `lipo` together the libraries for the same platform under `/generated`
3. run `uniffi` and generate the C header, module map and swift files
4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKFFI.xcframework`
5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework)
## Building only the Crypto SDK
```
sh build_crypto_xcframework.sh
```
The `build_crypto_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`:
1. compile `matrix-sdk-crypto-ffi` libraries for iOS and the iOS simulator under `/target`
2. `lipo` together the libraries for the same platform under `/generated`
3. run `uniffi` and generate the C header, module map and swift files
4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKCryptoFFI.xcframework`
5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework)
## Running the Xcode project
@@ -36,4 +49,5 @@ It makes the compiled code available to swift by importing the C header through
Once all the generated components are available running it should be as easy as choosing a platform and clicking run.
## Distribution
The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a Swift package available [TBD](here)
The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a Swift package available [TBD](here) in the case of SDK, and as CocoaPods podspec in the case of Crypto SDK.
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -eEu
cd "$(dirname "$0")"
# Path to the repo root
SRC_ROOT=../..
TARGET_DIR="${SRC_ROOT}/target"
GENERATED_DIR="${SRC_ROOT}/generated"
if [ -d "${GENERATED_DIR}" ]; then rm -rf "${GENERATED_DIR}"; fi
mkdir -p ${GENERATED_DIR}
REL_FLAG="--release"
REL_TYPE_DIR="release"
TARGET_CRATE=matrix-sdk-crypto-ffi
# Build static libs for all the different architectures
# iOS
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
# iOS Simulator
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
# Lipo together the libraries for the same platform
# iOS Simulator
lipo -create \
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_crypto_ffi.a"
# Generate uniffi files
uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" --language swift --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" --out-dir ${GENERATED_DIR}
# Move headers to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
mkdir -p ${HEADERS_DIR}
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
# Rename and move modulemap to the right place
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
# Move source files to the right place
SWIFT_DIR="${GENERATED_DIR}/Sources"
mkdir -p ${SWIFT_DIR}
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
# Build the xcframework
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/libmatrix_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
# Cleanup
if [ -f "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" ]; then rm -rf "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a"; fi
if [ -f "${GENERATED_DIR}/libmatrix_crypto_ffi.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_crypto_ffi.a"; fi
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
# Zip up framework, sources and LICENSE, ready to be uploaded to GitHub Releases and used by MatrixSDKCrypto.podspec
cp ${SRC_ROOT}/LICENSE $GENERATED_DIR
cd $GENERATED_DIR
zip -r MatrixSDKCryptoFFI.zip MatrixSDKCryptoFFI.xcframework Sources LICENSE
+1 -1
View File
@@ -53,7 +53,7 @@ lipo -create \
# Generate uniffi files
uniffi-bindgen generate "${SRC_ROOT}/crates/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR}
uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR}
# Move them to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
+2 -2
View File
@@ -33,7 +33,7 @@ lipo -create \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
# Generate uniffi files
uniffi-bindgen generate "${SRC_ROOT}/crates/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR}
uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR}
# Move them to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
@@ -64,7 +64,7 @@ if [ "$IS_CI" = false ] ; then
echo "Preparing matrix-rust-components-swift"
# Debug -> Copy generated files over to ../../../matrix-rust-components-swift
echo "$(echo "import MatrixSDKFFIWrapper\n"; cat "${SWIFT_DIR}/sdk.swift")" > "${SWIFT_DIR}/sdk.swift"
echo "$(printf "import MatrixSDKFFIWrapper\n\n"; cat "${SWIFT_DIR}/sdk.swift")" > "${SWIFT_DIR}/sdk.swift"
rsync -a --delete "${GENERATED_DIR}/MatrixSDKFFI.xcframework" "${SRC_ROOT}/../matrix-rust-components-swift/"
rsync -a --delete "${GENERATED_DIR}/swift/" "${SRC_ROOT}/../matrix-rust-components-swift/Sources/MatrixRustSDK"
@@ -20,14 +20,15 @@ hmac = "0.12.1"
http = "0.2.6"
pbkdf2 = "0.11.0"
rand = "0.8.5"
ruma = { version = "0.6.1", features = ["client-api-c"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] }
serde = "1.0.136"
serde_json = "1.0.79"
sha2 = "0.10.2"
thiserror = "1.0.30"
tracing = "0.1.34"
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
uniffi = "0.17.0"
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
uniffi = "0.18.0"
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
[dependencies.js_int]
@@ -35,16 +36,16 @@ version = "0.2.2"
features = ["lax_deserialize"]
[dependencies.matrix-sdk-common]
path = "../matrix-sdk-common"
path = "../../crates/matrix-sdk-common"
version = "0.5.0"
[dependencies.matrix-sdk-crypto]
path = "../matrix-sdk-crypto"
path = "../../crates/matrix-sdk-crypto"
version = "0.5.0"
features = ["qrcode", "backups_v1"]
[dependencies.matrix-sdk-sled]
path = "../matrix-sdk-sled"
path = "../../crates/matrix-sdk-sled"
version = "0.1.0"
default_features = false
features = ["crypto-store"]
@@ -56,10 +57,10 @@ features = ["rt-multi-thread"]
[dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "d0e744287a14319c2a9148fef3747548c740fc36"
rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd"
[build-dependencies]
uniffi_build = { version = "0.17.0", features = ["builtin-bindgen"] }
uniffi_build = { version = "0.18.0", features = ["builtin-bindgen"] }
[dev-dependencies]
tempfile = "3.3.0"
@@ -1,4 +1,4 @@
use std::{collections::HashMap, iter};
use std::{collections::HashMap, iter, ops::DerefMut};
use hmac::Hmac;
use matrix_sdk_crypto::{
@@ -101,7 +101,7 @@ impl BackupRecoveryKey {
let mut key = Box::new([0u8; Self::KEY_SIZE]);
let rounds = rounds as u32;
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, &mut *key);
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut());
let recovery_key = RecoveryKey::from_bytes(&key);
@@ -14,7 +14,7 @@ mod responses;
mod users;
mod verification;
use std::{collections::HashMap, convert::TryFrom, str::FromStr, sync::Arc};
use std::{borrow::Borrow, collections::HashMap, convert::TryFrom, str::FromStr, sync::Arc};
pub use backup_recovery_key::{
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
@@ -67,7 +67,7 @@ pub struct MigrationData {
pub struct PickledAccount {
/// The user id of the account owner.
pub user_id: String,
/// The device id of the account owner.
/// The device ID of the account owner.
pub device_id: String,
/// The pickled version of the Olm account.
pub pickle: String,
@@ -190,7 +190,12 @@ pub fn migrate(
processed_steps += 1;
listener(processed_steps, total_steps);
let user_id: Arc<UserId> = (&*parse_user_id(&data.account.user_id)?).into();
let user_id: Arc<UserId> = {
let user_id: OwnedUserId = parse_user_id(&data.account.user_id)?;
let user_id: &UserId = user_id.borrow();
user_id.into()
};
let device_id: Box<DeviceId> = data.account.device_id.into();
let device_id: Arc<DeviceId> = device_id.into();
@@ -270,7 +270,7 @@ impl OlmMachine {
}
}
/// Mark the device of the given user with the given device id as trusted.
/// Mark the device of the given user with the given device ID as trusted.
pub fn mark_device_as_trusted(
&self,
user_id: &str,
@@ -526,7 +526,7 @@ impl OlmMachine {
EncryptionSettings::default(),
))?;
Ok(requests.into_iter().map(|r| (&*r).into()).collect())
Ok(requests.into_iter().map(|r| r.as_ref().into()).collect())
}
/// Encrypt the given event with the given type and content for the given
@@ -15,7 +15,7 @@ interface MigrationError {
};
callback interface Logger {
void log(string log_line);
void log(string logLine);
};
callback interface ProgressListener {
@@ -132,6 +132,7 @@ impl From<OutgoingRequest> for Request {
let body = json!({
"device_keys": u.device_keys,
"one_time_keys": u.one_time_keys,
"fallback_keys": u.fallback_keys,
});
Request::KeysUpload {
@@ -0,0 +1,2 @@
[bindings.swift]
module_name = "MatrixSDKCrypto"
+3
View File
@@ -0,0 +1,3 @@
/docs
/node_modules
/package-lock.json
@@ -27,9 +27,10 @@ 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"] }
matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" }
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] }
wasm-bindgen = "0.2.80"
wasm-bindgen-futures = "0.4.30"
js-sys = "0.3.49"
+55
View File
@@ -0,0 +1,55 @@
# `matrix-sdk-crypto-js`
Welcome to the [WebAssembly] + JavaScript binding for the Rust
[`matrix-sdk-crypto`] library! WebAssembly can run anywhere, but these
bindings are designed to run on a JavaScript host. These bindings are
part of the [`matrix-rust-sdk`] project, which is a library
implementation of a [Matrix] client-server.
`matrix-sdk-crypto` is a no-network-IO implementation of a state
machine, named `OlmMachine`, that handles E2EE ([End-to-End
Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for
[Matrix] clients.
## Usage
These WebAssembly bindings are written in [Rust]. To build them, you
need to install the Rust compiler, see [the Install Rust
Page](https://www.rust-lang.org/tools/install). Then, the workflow is
pretty classical by using [npm], see [the Downloading and installing
Node.js and npm
Page](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
Once the Rust compiler, Node.js and npm are installed, you can run the
following commands:
```sh
$ npm install
$ npm run build
$ npm run test
```
A `matrix_sdk_crypto.js`, `matrix_sdk_crypto.d.ts` and a
`matrix_sdk_crypto_bg.wasm` files should be generated in the `pkg/`
directory.
TBD
## Documentation
To generate the documentation, please run the following command:
```sh
$ npm run doc
```
The documentation is generated in the `./docs` directory.
[WebAssembly]: https://webassembly.org/
[`matrix-sdk-crypto`]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto
[`matrix-rust-sdk`]: https://github.com/matrix-org/matrix-rust-sdk
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
[npm]: https://www.npmjs.com/
@@ -0,0 +1,41 @@
{
"name": "matrix-sdk-crypto-js",
"version": "0.5.0",
"homepage": "https://github.com/matrix-org/matrix-rust-sdk",
"description": "Matrix encryption library, for JavaScript",
"license": "Apache-2.0",
"collaborators": [
"Ivan Enderlin <ivane@element.io>"
],
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-rust-sdk"
},
"keywords": [
"matrix",
"chat",
"messaging",
"ruma",
"nio"
],
"main": "matrix_sdk_crypto.js",
"types": "pkg/matrix_sdk_crypto.d.ts",
"files": [
"pkg/matrix_sdk_crypto_bg.wasm",
"pkg/matrix_sdk_crypto.js",
"pkg/matrix_sdk_crypto.d.ts"
],
"devDependencies": {
"wasm-pack": "^0.10.2",
"jest": "^28.1.0",
"typedoc": "^0.22.17"
},
"engines": {
"node": ">= 10"
},
"scripts": {
"build": "RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./pkg",
"test": "jest --verbose",
"doc": "typedoc --tsconfig ."
}
}
@@ -0,0 +1,126 @@
//! Encryption types & siblings.
use std::time::Duration;
use wasm_bindgen::prelude::*;
use crate::events;
/// 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(),
}
}
}
/// 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"),
}
}
}
/// The verification state of the device that sent an event to us.
#[wasm_bindgen]
#[derive(Debug)]
pub enum VerificationState {
/// The device is trusted.
Trusted,
/// The device is not trusted.
Untrusted,
/// The device is not known to us.
UnknownDevice,
}
impl From<&matrix_sdk_common::deserialized_responses::VerificationState> for VerificationState {
fn from(value: &matrix_sdk_common::deserialized_responses::VerificationState) -> Self {
use matrix_sdk_common::deserialized_responses::VerificationState::*;
match value {
Trusted => Self::Trusted,
Untrusted => Self::Untrusted,
UnknownDevice => Self::UnknownDevice,
}
}
}
@@ -12,8 +12,8 @@ pub struct UserId {
pub(crate) inner: ruma::OwnedUserId,
}
impl UserId {
pub(crate) fn new_with(inner: ruma::OwnedUserId) -> Self {
impl From<ruma::OwnedUserId> for UserId {
fn from(inner: ruma::OwnedUserId) -> Self {
Self { inner }
}
}
@@ -23,16 +23,17 @@ 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)?))
Ok(Self::from(ruma::UserId::parse(id)?))
}
/// Returns the user's localpart.
#[wasm_bindgen(getter)]
pub fn localpart(&self) -> String {
self.inner.localpart().to_owned()
}
/// Returns the server name of the user ID.
#[wasm_bindgen(js_name = "serverName")]
#[wasm_bindgen(getter, js_name = "serverName")]
pub fn server_name(&self) -> ServerName {
ServerName { inner: self.inner.server_name().to_owned() }
}
@@ -42,7 +43,7 @@ impl UserId {
/// 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")]
#[wasm_bindgen(js_name = "isHistorical")]
pub fn is_historical(&self) -> bool {
self.inner.is_historical()
}
@@ -65,8 +66,8 @@ pub struct DeviceId {
pub(crate) inner: ruma::OwnedDeviceId,
}
impl DeviceId {
pub(crate) fn new_with(inner: ruma::OwnedDeviceId) -> Self {
impl From<ruma::OwnedDeviceId> for DeviceId {
fn from(inner: ruma::OwnedDeviceId) -> Self {
Self { inner }
}
}
@@ -76,7 +77,7 @@ impl DeviceId {
/// Create a new `DeviceId`.
#[wasm_bindgen(constructor)]
pub fn new(id: &str) -> DeviceId {
Self::new_with(id.into())
Self::from(ruma::OwnedDeviceId::from(id))
}
/// Return the device ID as a string.
@@ -96,8 +97,8 @@ pub struct RoomId {
pub(crate) inner: ruma::OwnedRoomId,
}
impl RoomId {
pub(crate) fn new_with(inner: ruma::OwnedRoomId) -> Self {
impl From<ruma::OwnedRoomId> for RoomId {
fn from(inner: ruma::OwnedRoomId) -> Self {
Self { inner }
}
}
@@ -107,16 +108,17 @@ 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)?))
Ok(Self::from(ruma::RoomId::parse(id)?))
}
/// Returns the user's localpart.
#[wasm_bindgen(getter)]
pub fn localpart(&self) -> String {
self.inner.localpart().to_owned()
}
/// Returns the server name of the room ID.
#[wasm_bindgen(js_name = "serverName")]
#[wasm_bindgen(getter, js_name = "serverName")]
pub fn server_name(&self) -> ServerName {
ServerName { inner: self.inner.server_name().to_owned() }
}
@@ -153,11 +155,13 @@ impl ServerName {
///
/// That is: Return the part of the server before `:<port>` or the
/// full server name if there is no port.
#[wasm_bindgen(getter)]
pub fn host(&self) -> String {
self.inner.host().to_owned()
}
/// Returns the port of the server name if any.
#[wasm_bindgen(getter)]
pub fn port(&self) -> Option<u16> {
self.inner.port()
}
@@ -15,7 +15,9 @@
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs, missing_debug_implementations)]
#![allow(clippy::drop_non_drop)] // triggered by wasm_bindgen code
pub mod encryption;
pub mod events;
mod future;
pub mod identifiers;
@@ -1,14 +1,17 @@
//! The crypto specific Olm objects.
use std::{collections::BTreeMap, time::Duration};
use std::collections::BTreeMap;
use js_sys::{Array, Map, Promise, Set};
use ruma::{DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use ruma::{
events::room::encrypted::OriginalSyncRoomEncryptedEvent, DeviceKeyAlgorithm,
OwnedTransactionId, UInt,
};
use serde_json::Value as JsonValue;
use wasm_bindgen::prelude::*;
use crate::{
downcast, events,
downcast, encryption,
future::future_to_promise,
identifiers, requests,
requests::OutgoingRequest,
@@ -49,25 +52,25 @@ impl OlmMachine {
}
/// The unique user ID that owns this `OlmMachine` instance.
#[wasm_bindgen(js_name = "userId")]
#[wasm_bindgen(getter, js_name = "userId")]
pub fn user_id(&self) -> identifiers::UserId {
identifiers::UserId::new_with(self.inner.user_id().to_owned())
identifiers::UserId::from(self.inner.user_id().to_owned())
}
/// The unique device ID that identifies this `OlmMachine`.
#[wasm_bindgen(js_name = "deviceId")]
#[wasm_bindgen(getter, js_name = "deviceId")]
pub fn device_id(&self) -> identifiers::DeviceId {
identifiers::DeviceId::new_with(self.inner.device_id().to_owned())
identifiers::DeviceId::from(self.inner.device_id().to_owned())
}
/// Get the public parts of our Olm identity keys.
#[wasm_bindgen(js_name = "identityKeys")]
#[wasm_bindgen(getter, 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")]
#[wasm_bindgen(getter, js_name = "displayName")]
pub fn display_name(&self) -> Promise {
let me = self.inner.clone();
@@ -81,11 +84,9 @@ impl OlmMachine {
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());
},
);
for user in self.inner.tracked_users() {
set.add(&identifiers::UserId::from(user).into());
}
set
}
@@ -261,6 +262,29 @@ impl OlmMachine {
}))
}
/// Decrypt an event from a room timeline.
///
/// # Arguments
///
/// * `event`, the event that should be decrypted.
/// * `room_id`, the ID of the room where the event was sent to.
#[wasm_bindgen(js_name = "decryptRoomEvent")]
pub fn decrypt_room_event(
&self,
event: &str,
room_id: &identifiers::RoomId,
) -> Result<Promise, JsError> {
let event: OriginalSyncRoomEncryptedEvent = serde_json::from_str(event)?;
let room_id = room_id.inner.clone();
let me = self.inner.clone();
Ok(future_to_promise(async move {
let room_event = me.decrypt_room_event(&event, room_id.as_ref()).await?;
Ok(responses::DecryptedRoomEvent::from(room_event))
}))
}
/// Invalidate the currently active outbound group session for the
/// given room.
///
@@ -284,7 +308,7 @@ impl OlmMachine {
&self,
room_id: &identifiers::RoomId,
users: &Array,
encryption_settings: &EncryptionSettings,
encryption_settings: &encryption::EncryptionSettings,
) -> Result<Promise, JsError> {
let room_id = room_id.inner.clone();
let users = users
@@ -420,96 +444,3 @@ impl From<matrix_sdk_crypto::olm::IdentityKeys> for IdentityKeys {
}
}
}
/// 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(),
}
}
}
@@ -26,17 +26,32 @@ use wasm_bindgen::prelude::*;
pub struct KeysUploadRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
/// ```json
/// {"device_keys": …, "one_time_keys": …}
/// {"device_keys": …, "one_time_keys": …, "fallback_keys": …}
/// ```
#[wasm_bindgen(readonly)]
pub body: JsString,
}
#[wasm_bindgen]
impl KeysUploadRequest {
/// Create a new `KeysUploadRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> KeysUploadRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::KeysUpload
}
}
/// Data for a request to the `/keys/query` API endpoint
/// ([specification]).
///
@@ -48,7 +63,7 @@ pub struct KeysUploadRequest {
pub struct KeysQueryRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
@@ -59,6 +74,21 @@ pub struct KeysQueryRequest {
pub body: JsString,
}
#[wasm_bindgen]
impl KeysQueryRequest {
/// Create a new `KeysQueryRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> KeysQueryRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::KeysQuery
}
}
/// Data for a request to the `/keys/claim` API endpoint
/// ([specification]).
///
@@ -71,7 +101,7 @@ pub struct KeysQueryRequest {
pub struct KeysClaimRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
@@ -82,6 +112,21 @@ pub struct KeysClaimRequest {
pub body: JsString,
}
#[wasm_bindgen]
impl KeysClaimRequest {
/// Create a new `KeysClaimRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> KeysClaimRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::KeysClaim
}
}
/// Data for a request to the `/sendToDevice` API endpoint
/// ([specification]).
///
@@ -93,7 +138,7 @@ pub struct KeysClaimRequest {
pub struct ToDeviceRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
@@ -104,6 +149,21 @@ pub struct ToDeviceRequest {
pub body: JsString,
}
#[wasm_bindgen]
impl ToDeviceRequest {
/// Create a new `ToDeviceRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> ToDeviceRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::ToDevice
}
}
/// Data for a request to the `/keys/signatures/upload` API endpoint
/// ([specification]).
///
@@ -115,7 +175,7 @@ pub struct ToDeviceRequest {
pub struct SignatureUploadRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
@@ -126,6 +186,21 @@ pub struct SignatureUploadRequest {
pub body: JsString,
}
#[wasm_bindgen]
impl SignatureUploadRequest {
/// Create a new `SignatureUploadRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> SignatureUploadRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::SignatureUpload
}
}
/// A customized owned request type for sending out room messages
/// ([specification]).
///
@@ -135,7 +210,7 @@ pub struct SignatureUploadRequest {
pub struct RoomMessageRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
@@ -146,6 +221,21 @@ pub struct RoomMessageRequest {
pub body: JsString,
}
#[wasm_bindgen]
impl RoomMessageRequest {
/// Create a new `RoomMessageRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> RoomMessageRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::RoomMessage
}
}
/// A request that will back up a batch of room keys to the server
/// ([specification]).
///
@@ -155,7 +245,7 @@ pub struct RoomMessageRequest {
pub struct KeysBackupRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub request_id: JsString,
pub id: JsString,
/// A JSON-encoded object of form:
///
@@ -166,6 +256,21 @@ pub struct KeysBackupRequest {
pub body: JsString,
}
#[wasm_bindgen]
impl KeysBackupRequest {
/// Create a new `KeysBackupRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> KeysBackupRequest {
Self { id, body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::KeysBackup
}
}
macro_rules! request {
($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => {
impl TryFrom<(String, &$ruma_request)> for $request {
@@ -181,7 +286,7 @@ macro_rules! request {
let value = serde_json::Value::Object(map);
Ok($request {
request_id: request_id.into(),
id: request_id.into(),
body: serde_json::to_string(&value)?.into(),
})
}
@@ -189,7 +294,7 @@ macro_rules! request {
};
}
request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys);
request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys);
request!(KeysQueryRequest from RumaKeysQueryRequest maps fields timeout, device_keys, token);
request!(KeysClaimRequest from RumaKeysClaimRequest maps fields timeout, one_time_keys);
request!(ToDeviceRequest from RumaToDeviceRequest maps fields event_type, txn_id, messages);
@@ -1,5 +1,9 @@
//! Types related to responses.
use std::borrow::Borrow;
use js_sys::{Array, JsString};
use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo};
use matrix_sdk_crypto::IncomingResponse;
pub(crate) use ruma::api::client::{
backup::add_backup_keys::v3::Response as KeysBackupResponse,
@@ -14,7 +18,7 @@ pub(crate) use ruma::api::client::{
use ruma::api::IncomingResponse as RumaIncomingResponse;
use wasm_bindgen::prelude::*;
use crate::requests::RequestType;
use crate::{encryption, identifiers, 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())
@@ -126,3 +130,81 @@ impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
}
}
}
/// A decrypted room event.
#[wasm_bindgen(getter_with_clone)]
#[derive(Debug)]
pub struct DecryptedRoomEvent {
/// The JSON-encoded decrypted event.
#[wasm_bindgen(readonly)]
pub event: JsString,
encryption_info: Option<EncryptionInfo>,
}
#[wasm_bindgen]
impl DecryptedRoomEvent {
/// The user ID of the event sender, note this is untrusted data
/// unless the `verification_state` is as well trusted.
#[wasm_bindgen(getter)]
pub fn sender(&self) -> Option<identifiers::UserId> {
Some(identifiers::UserId::from(self.encryption_info.as_ref()?.sender.clone()))
}
/// The device ID of the device that sent us the event, note this
/// is untrusted data unless `verification_state` is as well
/// trusted.
#[wasm_bindgen(getter, js_name = "senderDevice")]
pub fn sender_device(&self) -> Option<identifiers::DeviceId> {
Some(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone()))
}
/// The Curve25519 key of the device that created the megolm
/// decryption key originally.
#[wasm_bindgen(getter, js_name = "senderCurve25519Key")]
pub fn sender_curve25519_key(&self) -> Option<JsString> {
Some(match &self.encryption_info.as_ref()?.algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => curve25519_key.clone().into(),
})
}
/// The signing Ed25519 key that have created the megolm key that
/// was used to decrypt this session.
#[wasm_bindgen(getter, js_name = "senderClaimedEd25519Key")]
pub fn sender_claimed_ed25519_key(&self) -> Option<JsString> {
match &self.encryption_info.as_ref()?.algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { sender_claimed_keys, .. } => {
sender_claimed_keys.get(&ruma::DeviceKeyAlgorithm::Ed25519).cloned().map(Into::into)
}
}
}
/// Chain of Curve25519 keys through which this session was
/// forwarded, via `m.forwarded_room_key` events.
#[wasm_bindgen(getter, js_name = "forwardingCurve25519KeyChain")]
pub fn forwarding_curve25519_key_chain(&self) -> Option<Array> {
Some(match &self.encryption_info.as_ref()?.algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { forwarding_curve25519_key_chain, .. } => {
forwarding_curve25519_key_chain.iter().map(JsValue::from).collect()
}
})
}
/// The verification state of the device that sent us the event,
/// note this is the state of the device at the time of
/// decryption. It may change in the future if a device gets
/// verified or deleted.
#[wasm_bindgen(getter, js_name = "verificationState")]
pub fn verification_state(&self) -> Option<encryption::VerificationState> {
Some((self.encryption_info.as_ref()?.verification_state.borrow()).into())
}
}
impl From<matrix_sdk_common::deserialized_responses::RoomEvent> for DecryptedRoomEvent {
fn from(value: matrix_sdk_common::deserialized_responses::RoomEvent) -> Self {
Self {
event: value.event.json().get().to_owned().into(),
encryption_info: value.encryption_info,
}
}
}
@@ -18,15 +18,17 @@ impl DeviceLists {
///
/// `changed` and `left` must be an array of `UserId`.
#[wasm_bindgen(constructor)]
pub fn new(changed: Array, left: Array) -> Result<DeviceLists, JsError> {
pub fn new(changed: Option<Array>, left: Option<Array>) -> Result<DeviceLists, JsError> {
let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default();
inner.changed = changed
.unwrap_or_default()
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
inner.left = left
.unwrap_or_default()
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
@@ -40,24 +42,27 @@ impl DeviceLists {
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
/// List of users who have updated their device identity keys or
/// who now share an encrypted room with the client since the
/// previous sync
#[wasm_bindgen(getter)]
pub fn changed(&self) -> Array {
self.inner
.changed
.iter()
.map(|user| identifiers::UserId::new_with(user.clone()))
.map(|user| identifiers::UserId::from(user.clone()))
.map(JsValue::from)
.collect()
}
/// List of users who no longer share encrypted rooms since the previous
/// sync response.
/// List of users who no longer share encrypted rooms since the
/// previous sync response.
#[wasm_bindgen(getter)]
pub fn left(&self) -> Array {
self.inner
.left
.iter()
.map(|user| identifiers::UserId::new_with(user.clone()))
.map(|user| identifiers::UserId::from(user.clone()))
.map(JsValue::from)
.collect()
}
@@ -0,0 +1,36 @@
const { EncryptionAlgorithm, EncryptionSettings, HistoryVisibility, VerificationState } = require('../pkg/matrix_sdk_crypto');
describe('EncryptionAlgorithm', () => {
test('has the correct variant values', () => {
expect(EncryptionAlgorithm.OlmV1Curve25519AesSha2).toStrictEqual(0);
expect(EncryptionAlgorithm.MegolmV1AesSha2).toStrictEqual(1);
});
});
describe(EncryptionSettings.name, () => {
test('can be instantiated with default values', () => {
const es = new EncryptionSettings();
expect(es.algorithm).toStrictEqual(EncryptionAlgorithm.MegolmV1AesSha2);
expect(es.rotationPeriod).toStrictEqual(604800000000n);
expect(es.rotationPeriodMessages).toStrictEqual(100n);
expect(es.historyVisibility).toStrictEqual(HistoryVisibility.Shared);
});
test('checks the history visibility values', () => {
const es = new EncryptionSettings();
es.historyVisibility = HistoryVisibility.Invited;
expect(es.historyVisibility).toStrictEqual(HistoryVisibility.Invited);
expect(() => { es.historyVisibility = 42 }).toThrow();
});
});
describe('VerificationState', () => {
test('has the correct variant values', () => {
expect(VerificationState.Trusted).toStrictEqual(0);
expect(VerificationState.Untrusted).toStrictEqual(1);
expect(VerificationState.UnknownDevice).toStrictEqual(2);
});
});
@@ -0,0 +1,10 @@
const { HistoryVisibility } = require('../pkg/matrix_sdk_crypto');
describe('HistoryVisibility', () => {
test('has the correct variant values', () => {
expect(HistoryVisibility.Invited).toStrictEqual(0);
expect(HistoryVisibility.Joined).toStrictEqual(1);
expect(HistoryVisibility.Shared).toStrictEqual(2);
expect(HistoryVisibility.WorldReadable).toStrictEqual(3);
});
});
@@ -1,4 +1,4 @@
const { UserId, DeviceId, RoomId, ServerName } = require('../');
const { UserId, DeviceId, RoomId, ServerName } = require('../pkg/matrix_sdk_crypto');
describe(UserId.name, () => {
test('cannot be invalid', () => {
@@ -62,7 +62,7 @@ describe(ServerName.name, () => {
});
test('port can be optional', () => {
expect(new ServerName('foo.org').port).toStrictEqual(null);
expect(new ServerName('foo.org').port).toStrictEqual(undefined);
expect(new ServerName('foo.org:1234').port).toStrictEqual(1234);
});
@@ -1,37 +1,16 @@
const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../');
const path = require('path');
const os = require('os');
const fs = require('fs/promises');
const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../pkg/matrix_sdk_crypto');
describe(OlmMachine.name, () => {
test('cannot be instantiated with the constructor', () => {
expect(() => { new OlmMachine() }).toThrow();
});
test('can be instantiated with the async initializer', async () => {
expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine);
expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine);
});
describe('can be instantiated with a store', () => {
test('with no passphrase', async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory)).toBeInstanceOf(OlmMachine);
});
test('with a passphrase', async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory, 'hello')).toBeInstanceOf(OlmMachine);
});
});
const user = new UserId('@alice:example.org');
const device = new DeviceId('foobar');
const room = new RoomId('!baz:matrix.org');
function machine(new_user, new_device) {
return OlmMachine.initialize(new_user || user, new_device || device);
return new OlmMachine(new_user || user, new_device || device);
}
test('can read user ID', async () => {
@@ -49,12 +28,29 @@ describe(OlmMachine.name, () => {
expect(identityKeys.curve25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
});
test('can read display name', async () => {
expect(await machine().displayName).toBeUndefined();
});
test('can read tracked users', async () => {
const trackedUsers = (await machine()).trackedUsers();
expect(trackedUsers).toBeInstanceOf(Set);
expect(trackedUsers.size).toStrictEqual(0);
});
test('can update tracked users', async () => {
const m = await machine();
expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined);
});
test('can receive sync changes', async () => {
const m = await machine();
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
const oneTimeKeyCounts = new Map();
const unusedFallbackKeys = new Set();
const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys));
@@ -65,8 +61,8 @@ describe(OlmMachine.name, () => {
const m = await machine();
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
const oneTimeKeyCounts = new Map();
const unusedFallbackKeys = new Set();
const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys));
@@ -107,8 +103,8 @@ describe(OlmMachine.name, () => {
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
const oneTimeKeyCounts = new Map();
const unusedFallbackKeys = new Set();
const receiveSyncChanges = await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys);
outgoingRequests = await m.outgoingRequests();
@@ -342,10 +338,4 @@ describe(OlmMachine.name, () => {
expect(decrypted.verificationState).toStrictEqual(VerificationState.Trusted);
});
});
test('can update tracked users', async () => {
const m = await machine();
expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined);
});
});
@@ -0,0 +1,35 @@
const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../pkg/matrix_sdk_crypto');
describe('RequestType', () => {
test('has the correct variant values', () => {
expect(RequestType.KeysUpload).toStrictEqual(0);
expect(RequestType.KeysQuery).toStrictEqual(1);
expect(RequestType.KeysClaim).toStrictEqual(2);
expect(RequestType.ToDevice).toStrictEqual(3);
expect(RequestType.SignatureUpload).toStrictEqual(4);
expect(RequestType.RoomMessage).toStrictEqual(5);
expect(RequestType.KeysBackup).toStrictEqual(6);
});
});
for (const [request, request_type] of [
[KeysUploadRequest, RequestType.KeysUpload],
[KeysQueryRequest, RequestType.KeysQuery],
[KeysClaimRequest, RequestType.KeysClaim],
[ToDeviceRequest, RequestType.ToDevice],
[SignatureUploadRequest, RequestType.SignatureUpload],
[RoomMessageRequest, RequestType.RoomMessage],
[KeysBackupRequest, RequestType.KeysBackup],
]) {
describe(request.name, () => {
test('can be instantiated', () => {
const r = new (request)('foo', '{"bar": "baz"}');
expect(r).toBeInstanceOf(request);
expect(r.id).toStrictEqual('foo');
expect(r.body).toStrictEqual('{"bar": "baz"}');
expect(r.type).toStrictEqual(request_type);
});
})
}
@@ -0,0 +1,31 @@
const { DeviceLists, UserId } = require('../pkg/matrix_sdk_crypto');
describe(DeviceLists.name, () => {
test('can be empty', () => {
const empty = new DeviceLists();
expect(empty.isEmpty()).toStrictEqual(true);
expect(empty.changed).toHaveLength(0);
expect(empty.left).toHaveLength(0);
});
test('can be coerced empty', () => {
const empty = new DeviceLists([], []);
expect(empty.isEmpty()).toStrictEqual(true);
expect(empty.changed).toHaveLength(0);
expect(empty.left).toHaveLength(0);
});
test('returns the correct `changed` and `left`', () => {
const list = new DeviceLists([new UserId('@foo:bar.org')], [new UserId('@baz:qux.org')]);
expect(list.isEmpty()).toStrictEqual(false);
expect(list.changed).toHaveLength(1);
expect(list.changed[0].toString()).toStrictEqual('@foo:bar.org');
expect(list.left).toHaveLength(1);
expect(list.left[0].toString()).toStrictEqual('@baz:qux.org');
});
});
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"strict": true
},
"typedocOptions": {
"entryPoints": ["pkg/matrix_sdk_crypto.d.ts"],
"out": "docs",
"readme": "README.md",
}
}
@@ -25,13 +25,13 @@ docsrs = []
tracing = ["tracing-subscriber"]
[dependencies]
matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto" }
matrix-sdk-common = { version = "0.5.0", path = "../matrix-sdk-common" }
matrix-sdk-sled = { version = "0.1.0", path = "../matrix-sdk-sled", default-features = false, features = ["crypto-store"] }
ruma = { version = "0.6.2", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36" }
napi = { git = "https://github.com/Hywan/napi-rs", branch = "feat-either-n-up-to-26", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = { git = "https://github.com/Hywan/napi-rs", branch = "feat-either-n-up-to-26" }
matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" }
matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" }
matrix-sdk-sled = { version = "0.1.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" }
napi = { version = "2.6.1", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = "2.6.0"
serde_json = "1.0.79"
http = "0.2.6"
zeroize = "1.3.0"
@@ -1,5 +1,5 @@
{
"name": "matrix-sdk-crypto",
"name": "@matrix-org/matrix-sdk-crypto",
"version": "0.5.0",
"main": "index.js",
"types": "index.d.ts",
@@ -11,7 +11,7 @@
]
}
},
"license": "MIT",
"license": "Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.9.0",
"jest": "^28.1.0",
@@ -21,8 +21,9 @@
"node": ">= 14"
},
"scripts": {
"build": "napi build --platform --release --strip",
"test": "jest --verbose",
"release-build": "napi build --platform --release --strip",
"build": "napi build --platform",
"test": "jest --verbose --testTimeout 10000",
"doc": "typedoc --tsconfig ."
}
}
@@ -0,0 +1,126 @@
use std::{
io::{Cursor, Read},
ops::Deref,
};
use napi::bindgen_prelude::Uint8Array;
use napi_derive::*;
use crate::into_err;
/// A type to encrypt and to decrypt anything that can fit in an
/// `Uint8Array`, usually big buffer.
#[napi]
pub struct Attachment;
#[napi]
impl Attachment {
/// Encrypt the content of the `Uint8Array`.
///
/// It produces an `EncryptedAttachment`, we can be used to
/// retrieve the media encryption information, or the encrypted
/// data.
#[napi]
pub fn encrypt(array: Uint8Array) -> napi::Result<EncryptedAttachment> {
let buffer: &[u8] = array.deref();
let mut cursor = Cursor::new(buffer);
let mut encryptor = matrix_sdk_crypto::AttachmentEncryptor::new(&mut cursor);
let mut encrypted_data = Vec::new();
encryptor.read_to_end(&mut encrypted_data).map_err(into_err)?;
let media_encryption_info = Some(encryptor.finish());
Ok(EncryptedAttachment {
encrypted_data: Uint8Array::new(encrypted_data),
media_encryption_info,
})
}
/// Decrypt an `EncryptedAttachment`.
///
/// The encrypted attachment can be created manually, or from the
/// `encrypt` method.
///
/// **Warning**: The encrypted attachment can be used only
/// **once**! The encrypted data will still be present, but the
/// media encryption info (which contain secrets) will be
/// destroyed. It is still possible to get a JSON-encoded backup
/// by calling `EncryptedAttachment.mediaEncryptionInfo`.
#[napi]
pub fn decrypt(attachment: &mut EncryptedAttachment) -> napi::Result<Uint8Array> {
let media_encryption_info = match attachment.media_encryption_info.take() {
Some(media_encryption_info) => media_encryption_info,
None => {
return Err(napi::Error::from_reason(
"The media encryption info are absent from the given encrypted attachment"
.to_string(),
))
}
};
let encrypted_data: &[u8] = attachment.encrypted_data.deref();
let mut cursor = Cursor::new(encrypted_data);
let mut decryptor =
matrix_sdk_crypto::AttachmentDecryptor::new(&mut cursor, media_encryption_info)
.map_err(into_err)?;
let mut decrypted_data = Vec::new();
decryptor.read_to_end(&mut decrypted_data).map_err(into_err)?;
Ok(Uint8Array::new(decrypted_data))
}
}
/// An encrypted attachment, usually created from `Attachment.encrypt`.
#[napi]
pub struct EncryptedAttachment {
media_encryption_info: Option<matrix_sdk_crypto::MediaEncryptionInfo>,
/// The actual encrypted data.
pub encrypted_data: Uint8Array,
}
#[napi]
impl EncryptedAttachment {
/// Create a new encrypted attachment manually.
///
/// It needs encrypted data, stored in an `Uint8Array`, and a
/// [media encryption
/// information](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/struct.MediaEncryptionInfo.html),
/// as a JSON-encoded string.
///
/// The media encryption information aren't stored as a string:
/// they are parsed, validated and fully deserialized.
///
/// See [the specification to learn
/// more](https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes).
#[napi(constructor)]
pub fn new(encrypted_data: Uint8Array, media_encryption_info: String) -> napi::Result<Self> {
Ok(Self {
encrypted_data,
media_encryption_info: Some(
serde_json::from_str(media_encryption_info.as_str()).map_err(into_err)?,
),
})
}
/// Return the media encryption info as a JSON-encoded string. The
/// structure is fully valid.
///
/// If the media encryption info have been consumed already, it
/// will return `null`.
#[napi(getter)]
pub fn media_encryption_info(&self) -> Option<String> {
serde_json::to_string(self.media_encryption_info.as_ref()?).ok()
}
/// Check whether the media encryption info has been consumed by
/// `Attachment.decrypt` already.
#[napi(getter)]
pub fn has_media_encryption_info_been_consumed(&self) -> bool {
self.media_encryption_info.is_none()
}
}
@@ -1,6 +1,7 @@
//! Types for [Matrix](https://matrix.org/) identifiers for devices,
//! events, keys, rooms, servers, users and URIs.
use napi::bindgen_prelude::ToNapiValue;
use napi_derive::*;
use crate::into_err;
@@ -58,11 +59,7 @@ impl UserId {
}
}
pub(crate) fn lower_user_ids_to_ruma(users: Vec<&UserId>) -> impl Iterator<Item = &ruma::UserId> {
users.into_iter().map(|user| user.inner.as_ref())
}
/// A Matrix key ID.
/// A Matrix device ID.
///
/// Device identifiers in Matrix are completely opaque character
/// sequences. This type is provided simply for its semantic value.
@@ -94,6 +91,109 @@ impl DeviceId {
}
}
/// A Matrix device key ID.
///
/// A key algorithm and a device ID, combined with a :.
#[napi]
#[derive(Debug, Clone)]
pub struct DeviceKeyId {
pub(crate) inner: ruma::OwnedDeviceKeyId,
}
impl From<ruma::OwnedDeviceKeyId> for DeviceKeyId {
fn from(inner: ruma::OwnedDeviceKeyId) -> Self {
Self { inner }
}
}
#[napi]
impl DeviceKeyId {
/// Parse/validate and create a new `DeviceKeyId`.
#[napi(constructor)]
pub fn new(id: String) -> napi::Result<Self> {
Ok(Self::from(ruma::DeviceKeyId::parse(id.as_str()).map_err(into_err)?))
}
/// Returns key algorithm of the device key ID.
#[napi(getter)]
pub fn algorithm(&self) -> DeviceKeyAlgorithm {
self.inner.algorithm().into()
}
/// Returns device ID of the device key ID.
#[napi(getter)]
pub fn device_id(&self) -> DeviceId {
self.inner.device_id().to_owned().into()
}
/// Return the device key ID as a string.
#[napi]
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
self.inner.to_string()
}
}
/// The basic key algorithms in the specification.
#[napi]
pub struct DeviceKeyAlgorithm {
inner: ruma::DeviceKeyAlgorithm,
}
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithm {
fn from(inner: ruma::DeviceKeyAlgorithm) -> Self {
Self { inner }
}
}
#[napi]
impl DeviceKeyAlgorithm {
/// Read the device key algorithm's name. If the name is
/// `Unknown`, one may be interested by the `to_string` method to
/// read the original name.
#[napi(getter)]
pub fn name(&self) -> DeviceKeyAlgorithmName {
self.inner.clone().into()
}
/// Return the device key algorithm as a string.
#[napi]
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
self.inner.to_string()
}
}
/// The basic key algorithm names in the specification.
#[napi]
pub enum DeviceKeyAlgorithmName {
/// The Ed25519 signature algorithm.
Ed25519,
/// The Curve25519 ECDH algorithm.
Curve25519,
/// The Curve25519 ECDH algorithm, but the key also contains
/// signatures.
SignedCurve25519,
/// An unknown device key algorithm.
Unknown,
}
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithmName {
fn from(value: ruma::DeviceKeyAlgorithm) -> Self {
use ruma::DeviceKeyAlgorithm::*;
match value {
Ed25519 => Self::Ed25519,
Curve25519 => Self::Curve25519,
SignedCurve25519 => Self::SignedCurve25519,
_ => Self::Unknown,
}
}
}
/// A Matrix [room ID].
///
/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
@@ -16,15 +16,19 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
//#![warn(missing_docs, missing_debug_implementations)]
pub mod attachment;
pub mod encryption;
mod errors;
pub mod events;
pub mod identifiers;
pub mod machine;
pub mod olm;
pub mod requests;
pub mod responses;
pub mod sync_events;
#[cfg(feature = "tracing")]
pub mod tracing;
pub mod types;
pub mod vodozemac;
use crate::errors::into_err;
@@ -15,8 +15,8 @@ use serde_json::Value as JsonValue;
use zeroize::Zeroize;
use crate::{
encryption, identifiers, into_err, requests, responses, responses::response_from_string,
sync_events,
encryption, identifiers, into_err, olm, requests, responses, responses::response_from_string,
sync_events, types, vodozemac,
};
/// State machine implementation of the Olm/Megolm encryption protocol
@@ -63,6 +63,9 @@ impl OlmMachine {
store_path: Option<String>,
mut store_passphrase: Option<String>,
) -> napi::Result<OlmMachine> {
let user_id = user_id.clone();
let device_id = device_id.clone();
let store = store_path
.map(|store_path| {
matrix_sdk_sled::CryptoStore::open_with_passphrase(
@@ -121,7 +124,7 @@ impl OlmMachine {
/// Get the public parts of our Olm identity keys.
#[napi(getter)]
pub fn identity_keys(&self) -> IdentityKeys {
pub fn identity_keys(&self) -> vodozemac::IdentityKeys {
self.inner.identity_keys().into()
}
@@ -150,7 +153,7 @@ impl OlmMachine {
unused_fallback_keys: Vec<String>,
) -> napi::Result<String> {
let to_device_events = serde_json::from_str(to_device_events.as_ref()).map_err(into_err)?;
let changed_devices = &changed_devices.inner;
let changed_devices = changed_devices.inner.clone();
let one_time_key_counts = one_time_key_counts
.iter()
.map(|(key, value)| (DeviceKeyAlgorithm::from(key.as_str()), UInt::from(*value)))
@@ -167,7 +170,7 @@ impl OlmMachine {
.inner
.receive_sync_changes(
to_device_events,
changed_devices,
&changed_devices,
&one_time_key_counts,
unused_fallback_keys.as_deref(),
)
@@ -212,8 +215,7 @@ impl OlmMachine {
.into_iter()
.map(requests::OutgoingRequest)
.map(TryFrom::try_from)
.collect::<Result<Vec<_>, _>>()
.map_err(into_err)
.collect()
}
/// Mark the request with the given request ID as sent.
@@ -275,9 +277,15 @@ impl OlmMachine {
&self,
users: Option<Vec<&identifiers::UserId>>,
) -> napi::Result<Option<requests::KeysClaimRequest>> {
let users = users
.unwrap_or_default()
.into_iter()
.map(|user| user.inner.clone())
.collect::<Vec<_>>();
match self
.inner
.get_missing_sessions(identifiers::lower_user_ids_to_ruma(users.unwrap_or_default()))
.get_missing_sessions(users.iter().map(AsRef::as_ref))
.await
.map_err(into_err)?
{
@@ -306,7 +314,9 @@ impl OlmMachine {
/// * `users`, an array over user IDs that should be marked for tracking.
#[napi]
pub async fn update_tracked_users(&self, users: Vec<&identifiers::UserId>) {
self.inner.update_tracked_users(identifiers::lower_user_ids_to_ruma(users)).await;
let users = users.into_iter().map(|user| user.inner.clone()).collect::<Vec<_>>();
self.inner.update_tracked_users(users.iter().map(AsRef::as_ref)).await;
}
/// Get to-device requests to share a room key with users in a room.
@@ -323,15 +333,15 @@ impl OlmMachine {
users: Vec<&identifiers::UserId>,
encryption_settings: &encryption::EncryptionSettings,
) -> napi::Result<String> {
let room_id = room_id.inner.as_ref();
let users = identifiers::lower_user_ids_to_ruma(users);
let room_id = room_id.inner.clone();
let users = users.into_iter().map(|user| user.inner.clone()).collect::<Vec<_>>();
let encryption_settings =
matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings);
serde_json::to_string(
&self
.inner
.share_room_key(room_id, users, encryption_settings)
.share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings)
.await
.map_err(into_err)?,
)
@@ -354,13 +364,13 @@ impl OlmMachine {
event_type: String,
content: String,
) -> napi::Result<String> {
let room_id = room_id.inner.as_ref();
let room_id = room_id.inner.clone();
let content: JsonValue = serde_json::from_str(content.as_str()).map_err(into_err)?;
serde_json::to_string(
&self
.inner
.encrypt_room_event_raw(room_id, content, event_type.as_ref())
.encrypt_room_event_raw(&room_id, content, event_type.as_ref())
.await
.map_err(into_err)?,
)
@@ -381,86 +391,26 @@ impl OlmMachine {
) -> napi::Result<responses::DecryptedRoomEvent> {
let event: OriginalSyncRoomEncryptedEvent =
serde_json::from_str(event.as_str()).map_err(into_err)?;
let room_id = room_id.inner.as_ref();
let room_event = self.inner.decrypt_room_event(&event, room_id).await.map_err(into_err)?;
let room_id = room_id.inner.clone();
let room_event = self.inner.decrypt_room_event(&event, &room_id).await.map_err(into_err)?;
Ok(room_event.into())
}
}
/// An Ed25519 public key, used to verify digital signatures.
#[napi]
#[derive(Clone)]
pub struct Ed25519PublicKey {
inner: vodozemac::Ed25519PublicKey,
}
#[napi]
impl Ed25519PublicKey {
/// The number of bytes an Ed25519 public key has.
#[napi(getter)]
pub fn length(&self) -> u32 {
vodozemac::Ed25519PublicKey::LENGTH as u32
}
/// Serialize an Ed25519 public key to an unpadded base64
/// representation.
/// Get the status of the private cross signing keys.
///
/// This can be used to check which private cross signing keys we
/// have stored locally.
#[napi]
pub fn to_base64(&self) -> String {
self.inner.to_base64()
}
}
/// A Curve25519 public key.
#[napi]
#[derive(Clone)]
pub struct Curve25519PublicKey {
inner: vodozemac::Curve25519PublicKey,
}
#[napi]
impl Curve25519PublicKey {
/// The number of bytes a Curve25519 public key has.
#[napi(getter)]
pub fn length(&self) -> u32 {
vodozemac::Curve25519PublicKey::LENGTH as u32
pub async fn cross_signing_status(&self) -> olm::CrossSigningStatus {
self.inner.cross_signing_status().await.into()
}
/// Serialize an Curve25519 public key to an unpadded base64
/// representation.
/// Sign the given message using our device key and if available
/// cross-signing master key.
#[napi]
pub fn to_base64(&self) -> String {
self.inner.to_base64()
}
}
/// Struct holding the two public identity keys of an account.
#[napi]
pub struct IdentityKeys {
ed25519: Ed25519PublicKey,
curve25519: Curve25519PublicKey,
}
#[napi]
impl IdentityKeys {
/// The Ed25519 public key, used for signing.
#[napi(getter)]
pub fn ed25519(&self) -> Ed25519PublicKey {
self.ed25519.clone()
}
/// The Curve25519 public key, used for establish shared secrets.
#[napi(getter)]
pub fn curve25519(&self) -> Curve25519PublicKey {
self.curve25519.clone()
}
}
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 },
}
pub async fn sign(&self, message: String) -> types::Signatures {
self.inner.sign(message.as_str()).await.into()
}
}
@@ -0,0 +1,40 @@
//! Olm types.
use napi_derive::*;
/// Struct representing the state of our private cross signing keys,
/// it shows which private cross signing keys we have locally stored.
#[napi]
#[derive(Debug)]
pub struct CrossSigningStatus {
inner: matrix_sdk_crypto::olm::CrossSigningStatus,
}
impl From<matrix_sdk_crypto::olm::CrossSigningStatus> for CrossSigningStatus {
fn from(inner: matrix_sdk_crypto::olm::CrossSigningStatus) -> Self {
Self { inner }
}
}
#[napi]
impl CrossSigningStatus {
/// Do we have the master key.
#[napi(getter)]
pub fn has_master(&self) -> bool {
self.inner.has_master
}
/// Do we have the self signing key, this one is necessary to sign
/// our own devices.
#[napi(getter)]
pub fn has_self_signing(&self) -> bool {
self.inner.has_self_signing
}
/// Do we have the user signing key, this one is necessary to sign
/// other users.
#[napi(getter)]
pub fn has_user_signing(&self) -> bool {
self.inner.has_user_signing
}
}
@@ -1,5 +1,7 @@
//! Types to handle requests.
use std::time::Duration;
use matrix_sdk_crypto::requests::{
KeysBackupRequest as RumaKeysBackupRequest, KeysQueryRequest as RumaKeysQueryRequest,
RoomMessageRequest as RumaRoomMessageRequest, ToDeviceRequest as RumaToDeviceRequest,
@@ -12,6 +14,8 @@ use ruma::api::client::keys::{
upload_signatures::v3::Request as RumaSignatureUploadRequest,
};
use crate::into_err;
/// Data for a request to the `/keys/upload` API endpoint
/// ([specification]).
///
@@ -27,7 +31,7 @@ pub struct KeysUploadRequest {
/// A JSON-encoded object of form:
///
/// ```json
/// {"device_keys": …, "one_time_keys": …}
/// {"device_keys": …, "one_time_keys": …, "fallback_keys": …}
/// ```
#[napi(readonly)]
pub body: String,
@@ -56,7 +60,7 @@ pub struct KeysQueryRequest {
/// A JSON-encoded object of form:
///
/// ```
/// ```json
/// {"timeout": …, "device_keys": …, "token": …}
/// ```
#[napi(readonly)]
@@ -87,7 +91,7 @@ pub struct KeysClaimRequest {
/// A JSON-encoded object of form:
///
/// ```
/// ```json
/// {"timeout": …, "one_time_keys": …}
/// ```
#[napi(readonly)]
@@ -117,7 +121,7 @@ pub struct ToDeviceRequest {
/// A JSON-encoded object of form:
///
/// ```
/// ```json
/// {"event_type": …, "txn_id": …, "messages": …}
/// ```
#[napi(readonly)]
@@ -147,7 +151,7 @@ pub struct SignatureUploadRequest {
/// A JSON-encoded object of form:
///
/// ```
/// ```json
/// {"signed_keys": …, "txn_id": …, "messages": …}
/// ```
#[napi(readonly)]
@@ -175,7 +179,7 @@ pub struct RoomMessageRequest {
/// A JSON-encoded object of form:
///
/// ```
/// ```json
/// {"room_id": …, "txn_id": …, "content": …}
/// ```
#[napi(readonly)]
@@ -203,7 +207,7 @@ pub struct KeysBackupRequest {
/// A JSON-encoded object of form:
///
/// ```
/// ```json
/// {"rooms": …}
/// ```
#[napi(readonly)]
@@ -220,31 +224,39 @@ impl KeysBackupRequest {
}
macro_rules! request {
($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => {
($request:ident from $ruma_request:ident maps fields $( $field:ident $( { $transformation:expr } )? ),+ $(,)? ) => {
impl TryFrom<(String, &$ruma_request)> for $request {
type Error = serde_json::Error;
type Error = napi::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)?);
let field = &request.$field;
$(
let field = {
let $field = field;
$transformation
};
)?
map.insert(stringify!($field).to_owned(), serde_json::to_value(field).map_err(into_err)?);
)+
let value = serde_json::Value::Object(map);
Ok($request {
id: request_id,
body: serde_json::to_string(&value)?.into(),
body: serde_json::to_string(&value).map_err(into_err)?.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!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys);
request!(KeysQueryRequest from RumaKeysQueryRequest maps fields timeout { timeout.as_ref().map(Duration::as_millis).map(u64::try_from).transpose().map_err(into_err)? }, device_keys, token);
request!(KeysClaimRequest from RumaKeysClaimRequest maps fields timeout { timeout.as_ref().map(Duration::as_millis).map(u64::try_from).transpose().map_err(into_err)? }, 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);
@@ -263,7 +275,7 @@ pub type OutgoingRequests = Either7<
pub(crate) struct OutgoingRequest(pub(crate) matrix_sdk_crypto::OutgoingRequest);
impl TryFrom<OutgoingRequest> for OutgoingRequests {
type Error = serde_json::Error;
type Error = napi::Error;
fn try_from(outgoing_request: OutgoingRequest) -> Result<Self, Self::Error> {
let request_id = outgoing_request.0.request_id().to_string();
@@ -1,3 +1,5 @@
use std::borrow::Borrow;
use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo};
use matrix_sdk_crypto::IncomingResponse;
use napi_derive::*;
@@ -190,7 +192,7 @@ impl DecryptedRoomEvent {
/// verified or deleted.
#[napi(getter)]
pub fn verification_state(&self) -> Option<encryption::VerificationState> {
Some((&self.encryption_info.as_ref()?.verification_state).into())
Some(self.encryption_info.as_ref()?.verification_state.borrow().into())
}
}
@@ -0,0 +1,156 @@
use std::collections::HashMap;
use napi_derive::*;
use crate::{
identifiers::{DeviceKeyId, UserId},
vodozemac::Ed25519Signature,
};
#[napi]
#[derive(Default)]
pub struct Signatures {
inner: matrix_sdk_crypto::types::Signatures,
}
impl From<matrix_sdk_crypto::types::Signatures> for Signatures {
fn from(inner: matrix_sdk_crypto::types::Signatures) -> Self {
Self { inner }
}
}
#[napi]
impl Signatures {
/// Creates a new, empty, signatures collection.
#[napi(constructor)]
pub fn new() -> Self {
matrix_sdk_crypto::types::Signatures::new().into()
}
/// Add the given signature from the given signer and the given key ID to
/// the collection.
#[napi]
pub fn add_signature(
&mut self,
signer: &UserId,
key_id: &DeviceKeyId,
signature: &Ed25519Signature,
) -> Option<MaybeSignature> {
self.inner
.add_signature(signer.inner.clone(), key_id.inner.clone(), signature.inner)
.map(Into::into)
}
/// Try to find an Ed25519 signature from the given signer with
/// the given key ID.
#[napi]
pub fn get_signature(&self, signer: &UserId, key_id: &DeviceKeyId) -> Option<Ed25519Signature> {
self.inner.get_signature(signer.inner.as_ref(), key_id.inner.as_ref()).map(Into::into)
}
/// Get the map of signatures that belong to the given user.
#[napi]
pub fn get(&self, signer: &UserId) -> Option<HashMap<String, MaybeSignature>> {
self.inner.get(signer.inner.as_ref()).map(|map| {
map.iter()
.map(|(device_key_id, maybe_signature)| {
(device_key_id.as_str().to_owned(), maybe_signature.clone().into())
})
.collect()
})
}
/// Remove all the signatures we currently hold.
#[napi]
pub fn clear(&mut self) {
self.inner.clear();
}
/// Do we hold any signatures or is our collection completely
/// empty.
#[napi(getter)]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// How many signatures do we currently hold.
#[napi(getter)]
pub fn count(&self) -> usize {
self.inner.signature_count()
}
}
/// Represents a potentially decoded signature (but not a validated
/// one).
#[napi]
pub struct Signature {
inner: matrix_sdk_crypto::types::Signature,
}
impl From<matrix_sdk_crypto::types::Signature> for Signature {
fn from(inner: matrix_sdk_crypto::types::Signature) -> Self {
Self { inner }
}
}
#[napi]
impl Signature {
/// Get the Ed25519 signature, if this is one.
#[napi(getter)]
pub fn ed25519(&self) -> Option<Ed25519Signature> {
self.inner.ed25519().map(Into::into)
}
/// Convert the signature to a base64 encoded string.
#[napi]
pub fn to_base64(&self) -> String {
self.inner.to_base64()
}
}
type MaybeSignatureInner =
Result<matrix_sdk_crypto::types::Signature, matrix_sdk_crypto::types::InvalidSignature>;
/// Represents a signature that is either valid _or_ that could not be
/// decoded.
#[napi]
pub struct MaybeSignature {
inner: MaybeSignatureInner,
}
impl From<MaybeSignatureInner> for MaybeSignature {
fn from(inner: MaybeSignatureInner) -> Self {
Self { inner }
}
}
#[napi]
impl MaybeSignature {
/// Check whether the signature has been successfully decoded.
#[napi(getter)]
pub fn is_valid(&self) -> bool {
self.inner.is_ok()
}
/// Check whether the signature could not be successfully decoded.
#[napi(getter)]
pub fn is_invalid(&self) -> bool {
self.inner.is_err()
}
/// The signature, if successfully decoded.
#[napi(getter)]
pub fn signature(&self) -> Option<Signature> {
self.inner.as_ref().cloned().map(Into::into).ok()
}
/// The base64 encoded string that is claimed to contain a
/// signature but could not be decoded, if any.
#[napi(getter)]
pub fn invalid_signature_source(&self) -> Option<String> {
match &self.inner {
Ok(_) => None,
Err(signature) => Some(signature.source.clone()),
}
}
}
@@ -0,0 +1,113 @@
use napi_derive::*;
use crate::into_err;
/// An Ed25519 public key, used to verify digital signatures.
#[napi]
#[derive(Clone)]
pub struct Ed25519PublicKey {
inner: vodozemac::Ed25519PublicKey,
}
#[napi]
impl Ed25519PublicKey {
/// The number of bytes an Ed25519 public key has.
#[napi(getter)]
pub fn length(&self) -> u32 {
vodozemac::Ed25519PublicKey::LENGTH as u32
}
/// Serialize an Ed25519 public key to an unpadded base64
/// representation.
#[napi]
pub fn to_base64(&self) -> String {
self.inner.to_base64()
}
}
/// An Ed25519 digital signature, can be used to verify the
/// authenticity of a message.
#[napi]
pub struct Ed25519Signature {
pub(crate) inner: vodozemac::Ed25519Signature,
}
impl From<vodozemac::Ed25519Signature> for Ed25519Signature {
fn from(inner: vodozemac::Ed25519Signature) -> Self {
Self { inner }
}
}
#[napi]
impl Ed25519Signature {
/// Try to create an Ed25519 signature from an unpadded base64
/// representation.
#[napi(constructor)]
pub fn new(signature: String) -> napi::Result<Self> {
Ok(Self {
inner: vodozemac::Ed25519Signature::from_base64(signature.as_str())
.map_err(into_err)?,
})
}
/// Serialize a Ed25519 signature to an unpadded base64
/// representation.
#[napi]
pub fn to_base64(&self) -> String {
self.inner.to_base64()
}
}
/// A Curve25519 public key.
#[napi]
#[derive(Clone)]
pub struct Curve25519PublicKey {
inner: vodozemac::Curve25519PublicKey,
}
#[napi]
impl Curve25519PublicKey {
/// The number of bytes a Curve25519 public key has.
#[napi(getter)]
pub fn length(&self) -> u32 {
vodozemac::Curve25519PublicKey::LENGTH as u32
}
/// Serialize an Curve25519 public key to an unpadded base64
/// representation.
#[napi]
pub fn to_base64(&self) -> String {
self.inner.to_base64()
}
}
/// Struct holding the two public identity keys of an account.
#[napi]
pub struct IdentityKeys {
ed25519: Ed25519PublicKey,
curve25519: Curve25519PublicKey,
}
#[napi]
impl IdentityKeys {
/// The Ed25519 public key, used for signing.
#[napi(getter)]
pub fn ed25519(&self) -> Ed25519PublicKey {
self.ed25519.clone()
}
/// The Curve25519 public key, used for establish shared secrets.
#[napi(getter)]
pub fn curve25519(&self) -> Curve25519PublicKey {
self.curve25519.clone()
}
}
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 },
}
}
}
@@ -0,0 +1,77 @@
const { Attachment, EncryptedAttachment } = require('../');
describe(Attachment.name, () => {
const originalData = 'hello';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
let encryptedAttachment;
test('can encrypt data', () => {
encryptedAttachment = Attachment.encrypt(textEncoder.encode(originalData));
const mediaEncryptionInfo = JSON.parse(encryptedAttachment.mediaEncryptionInfo);
expect(mediaEncryptionInfo).toMatchObject({
v: 'v2',
key: {
kty: expect.any(String),
key_ops: expect.arrayContaining(['encrypt', 'decrypt']),
alg: expect.any(String),
k: expect.any(String),
ext: expect.any(Boolean),
},
iv: expect.stringMatching(/^[A-Za-z0-9\+/]+$/),
hashes: {
sha256: expect.stringMatching(/^[A-Za-z0-9\+/]+$/)
}
});
const encryptedData = encryptedAttachment.encryptedData;
expect(encryptedData.every((i) => { i != 0 })).toStrictEqual(false);
});
test('can decrypt data', () => {
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false);
const decryptedAttachment = Attachment.decrypt(encryptedAttachment);
expect(textDecoder.decode(decryptedAttachment)).toStrictEqual(originalData);
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true);
});
test('can only decrypt once', () => {
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true);
expect(() => { textDecoder.decode(decryptedAttachment) }).toThrow()
});
});
describe(EncryptedAttachment.name, () => {
const originalData = 'hello';
const textDecoder = new TextDecoder();
test('can be created manually', () => {
const encryptedAttachment = new EncryptedAttachment(
new Uint8Array([24, 150, 67, 37, 144]),
JSON.stringify({
v: 'v2',
key: {
kty: 'oct',
key_ops: [ 'encrypt', 'decrypt' ],
alg: 'A256CTR',
k: 'QbNXUjuukFyEJ8cQZjJuzN6mMokg0HJIjx0wVMLf5BM',
ext: true
},
iv: 'xk2AcWkomiYAAAAAAAAAAA',
hashes: {
sha256: 'JsRbDXgOja4xvDiF3DwBuLHdxUzIrVYIuj7W/t3aEok'
}
})
);
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false);
expect(textDecoder.decode(Attachment.decrypt(encryptedAttachment))).toStrictEqual(originalData);
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true);
});
});
@@ -0,0 +1,118 @@
const { UserId, DeviceId, DeviceKeyId, DeviceKeyAlgorithm, DeviceKeyAlgorithmName, RoomId, ServerName } = require('../');
describe(UserId.name, () => {
test('cannot be invalid', () => {
expect(() => { new UserId('@foobar') }).toThrow();
});
const user = new UserId('@foo:bar.org');
test('localpart is present', () => {
expect(user.localpart).toStrictEqual('foo');
});
test('server name is present', () => {
expect(user.serverName).toBeInstanceOf(ServerName);
});
test('user ID is not historical', () => {
expect(user.isHistorical()).toStrictEqual(false);
});
test('can read the user ID as a string', () => {
expect(user.toString()).toStrictEqual('@foo:bar.org');
})
});
describe(DeviceId.name, () => {
const device = new DeviceId('foo');
test('can read the device ID as a string', () => {
expect(device.toString()).toStrictEqual('foo');
})
});
describe(DeviceKeyId.name, () => {
for (const deviceKey of [
{ name: 'ed25519',
id: 'ed25519:foobar',
algorithmName: DeviceKeyAlgorithmName.Ed25519,
algorithm: 'ed25519',
deviceId: 'foobar' },
{ name: 'curve25519',
id: 'curve25519:foobar',
algorithmName: DeviceKeyAlgorithmName.Curve25519,
algorithm: 'curve25519',
deviceId: 'foobar' },
{ name: 'signed curve25519',
id: 'signed_curve25519:foobar',
algorithmName: DeviceKeyAlgorithmName.SignedCurve25519,
algorithm: 'signed_curve25519',
deviceId: 'foobar' },
{ name: 'unknown',
id: 'hello:foobar',
algorithmName: DeviceKeyAlgorithmName.Unknown,
algorithm: 'hello',
deviceId: 'foobar' },
]) {
test(`${deviceKey.name} algorithm`, () => {
const dk = new DeviceKeyId(deviceKey.id);
expect(dk.algorithm.name).toStrictEqual(deviceKey.algorithmName);
expect(dk.algorithm.toString()).toStrictEqual(deviceKey.algorithm);
expect(dk.deviceId.toString()).toStrictEqual(deviceKey.deviceId);
expect(dk.toString()).toStrictEqual(deviceKey.id);
});
}
});
describe('DeviceKeyAlgorithmName', () => {
test('has the correct variants', () => {
expect(DeviceKeyAlgorithmName.Ed25519).toStrictEqual(0);
expect(DeviceKeyAlgorithmName.Curve25519).toStrictEqual(1);
expect(DeviceKeyAlgorithmName.SignedCurve25519).toStrictEqual(2);
expect(DeviceKeyAlgorithmName.Unknown).toStrictEqual(3);
});
});
describe(RoomId.name, () => {
test('cannot be invalid', () => {
expect(() => { new RoomId('!foo') }).toThrow();
});
const room = new RoomId('!foo:bar.org');
test('localpart is present', () => {
expect(room.localpart).toStrictEqual('foo');
});
test('server name is present', () => {
expect(room.serverName).toBeInstanceOf(ServerName);
});
test('can read the room ID as string', () => {
expect(room.toString()).toStrictEqual('!foo:bar.org');
});
});
describe(ServerName.name, () => {
test('cannot be invalid', () => {
expect(() => { new ServerName('@foobar') }).toThrow()
});
test('host is present', () => {
expect(new ServerName('foo.org').host).toStrictEqual('foo.org');
});
test('port can be optional', () => {
expect(new ServerName('foo.org').port).toStrictEqual(null);
expect(new ServerName('foo.org:1234').port).toStrictEqual(1234);
});
test('server is not an IP literal', () => {
expect(new ServerName('foo.org').isIpLiteral()).toStrictEqual(false);
});
});
@@ -0,0 +1,400 @@
const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState, CrossSigningStatus, MaybeSignature } = require('../');
const path = require('path');
const os = require('os');
const fs = require('fs/promises');
describe(OlmMachine.name, () => {
test('cannot be instantiated with the constructor', () => {
expect(() => { new OlmMachine() }).toThrow();
});
test('can be instantiated with the async initializer', async () => {
expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine);
});
describe('can be instantiated with a store', () => {
test('with no passphrase', async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory)).toBeInstanceOf(OlmMachine);
});
test('with a passphrase', async () => {
const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory, 'hello')).toBeInstanceOf(OlmMachine);
});
});
const user = new UserId('@alice:example.org');
const device = new DeviceId('foobar');
const room = new RoomId('!baz:matrix.org');
function machine(new_user, new_device) {
return OlmMachine.initialize(new_user || user, new_device || device);
}
test('can read user ID', async () => {
expect((await machine()).userId.toString()).toStrictEqual(user.toString());
});
test('can read device ID', async () => {
expect((await machine()).deviceId.toString()).toStrictEqual(device.toString());
});
test('can read identity keys', async () => {
const identityKeys = (await machine()).identityKeys;
expect(identityKeys.ed25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
expect(identityKeys.curve25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
});
test('can receive sync changes', async () => {
const m = await machine();
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys));
expect(receiveSyncChanges).toEqual({});
});
test('can get the outgoing requests that need to be send out', async () => {
const m = await machine();
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys));
expect(receiveSyncChanges).toEqual({});
const outgoingRequests = await m.outgoingRequests();
expect(outgoingRequests).toHaveLength(2);
{
expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest);
expect(outgoingRequests[0].id).toBeDefined();
expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload);
const body = JSON.parse(outgoingRequests[0].body);
expect(body.device_keys).toBeDefined();
expect(body.one_time_keys).toBeDefined();
}
{
expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest);
expect(outgoingRequests[1].id).toBeDefined();
expect(outgoingRequests[1].type).toStrictEqual(RequestType.KeysQuery);
const body = JSON.parse(outgoingRequests[1].body);
expect(body.timeout).toBeDefined();
expect(body.device_keys).toBeDefined();
expect(body.token).toBeDefined();
}
});
describe('setup workflow to mark requests as sent', () => {
let m;
let ougoingRequests;
beforeAll(async () => {
m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID'));
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
const receiveSyncChanges = await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys);
outgoingRequests = await m.outgoingRequests();
expect(outgoingRequests).toHaveLength(2);
});
test('can mark requests as sent', async () => {
{
const request = outgoingRequests[0];
expect(request).toBeInstanceOf(KeysUploadRequest);
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload
const hypothetical_response = JSON.stringify({
"one_time_key_counts": {
"curve25519": 10,
"signed_curve25519": 20
}
});
const marked = await m.markRequestAsSent(request.id, request.type, hypothetical_response);
expect(marked).toStrictEqual(true);
}
{
const request = outgoingRequests[1];
expect(request).toBeInstanceOf(KeysQueryRequest);
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysquery
const hypothetical_response = JSON.stringify({
"device_keys": {
"@alice:example.org": {
"JLAFKJWSCS": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "JLAFKJWSCS",
"keys": {
"curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
"ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
},
"signatures": {
"@alice:example.org": {
"ed25519:JLAFKJWSCS": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA"
}
},
"unsigned": {
"device_display_name": "Alice's mobile phone"
},
"user_id": "@alice:example.org"
}
}
},
"failures": {}
});
const marked = await m.markRequestAsSent(request.id, request.type, hypothetical_response);
expect(marked).toStrictEqual(true);
}
});
});
describe('setup workflow to encrypt/decrypt events', () => {
let m;
const user = new UserId('@alice:example.org');
const device = new DeviceId('JLAFKJWSCS');
const room = new RoomId('!test:localhost');
beforeAll(async () => {
m = await machine(user, device);
});
test('can pass keysquery and keysclaim requests directly', async () => {
{
// derived from https://github.com/matrix-org/matrix-rust-sdk/blob/7f49618d350fab66b7e1dc4eaf64ec25ceafd658/benchmarks/benches/crypto_bench/keys_query.json
const hypothetical_response = JSON.stringify({
"device_keys": {
"@example:localhost": {
"AFGUOBTZWM": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "AFGUOBTZWM",
"keys": {
"curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo",
"ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec"
},
"signatures": {
"@example:localhost": {
"ed25519:AFGUOBTZWM": "RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA"
}
},
"user_id": "@example:localhost",
"unsigned": {
"device_display_name": "rust-sdk"
}
},
}
},
"failures": {},
"master_keys": {
"@example:localhost": {
"user_id": "@example:localhost",
"usage": [
"master"
],
"keys": {
"ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU": "n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU"
},
"signatures": {
"@example:localhost": {
"ed25519:TCSJXPWGVS": "+j9G3L41I1fe0++wwusTTQvbboYW0yDtRWUEujhwZz4MAltjLSfJvY0hxhnz+wHHmuEXvQDen39XOpr1p29sAg"
}
}
}
},
"self_signing_keys": {
"@example:localhost": {
"user_id": "@example:localhost",
"usage": [
"self_signing"
],
"keys": {
"ed25519:kQXOuy639Yt47mvNTdrIluoC6DMvfbZLYbxAmwiDyhI": "kQXOuy639Yt47mvNTdrIluoC6DMvfbZLYbxAmwiDyhI"
},
"signatures": {
"@example:localhost": {
"ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU": "q32ifix/qyRpvmegw2BEJklwoBCAJldDNkcX+fp+lBA4Rpyqtycxge6BA4hcJdxYsy3oV0IHRuugS8rJMMFyAA"
}
}
}
},
"user_signing_keys": {
"@example:localhost": {
"user_id": "@example:localhost",
"usage": [
"user_signing"
],
"keys": {
"ed25519:g4ED07Fnqf3GzVWNN1pZ0IFrPQVdqQf+PYoJNH4eE0s": "g4ED07Fnqf3GzVWNN1pZ0IFrPQVdqQf+PYoJNH4eE0s"
},
"signatures": {
"@example:localhost": {
"ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU": "nKQu8alQKDefNbZz9luYPcNj+Z+ouQSot4fU/A23ELl1xrI06QVBku/SmDx0sIW1ytso0Cqwy1a+3PzCa1XABg"
}
}
}
}
});
const marked = await m.markRequestAsSent('foo', RequestType.KeysQuery, hypothetical_response);
}
{
// derived from https://github.com/matrix-org/matrix-rust-sdk/blob/7f49618d350fab66b7e1dc4eaf64ec25ceafd658/benchmarks/benches/crypto_bench/keys_claim.json
const hypothetical_response = JSON.stringify({
"one_time_keys": {
"@example:localhost": {
"AFGUOBTZWM": {
"signed_curve25519:AAAABQ": {
"key": "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws",
"signatures": {
"@example:localhost": {
"ed25519:AFGUOBTZWM": "2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg"
}
}
}
},
}
},
"failures": {}
});
const marked = await m.markRequestAsSent('bar', RequestType.KeysClaim, hypothetical_response);
}
});
test('can share a room key', async () => {
const other_users = [new UserId('@example:localhost')];
const requests = JSON.parse(await m.shareRoomKey(room, other_users, new EncryptionSettings()));
expect(requests).toHaveLength(1);
expect(requests[0].event_type).toBeDefined();
expect(requests[0].txn_id).toBeDefined();
expect(requests[0].messages).toBeDefined();
expect(requests[0].messages['@example:localhost']).toBeDefined();
});
let encrypted;
test('can encrypt an event', async () => {
encrypted = JSON.parse(await m.encryptRoomEvent(
room,
'm.room.message',
JSON.stringify({
"hello": "world"
}),
));
expect(encrypted.algorithm).toBeDefined();
expect(encrypted.ciphertext).toBeDefined();
expect(encrypted.sender_key).toBeDefined();
expect(encrypted.device_id).toStrictEqual(device.toString());
expect(encrypted.session_id).toBeDefined();
});
test('can decrypt an event', async () => {
const decrypted = await m.decryptRoomEvent(
JSON.stringify({
"type": "m.room.encrypted",
"event_id": "$xxxxx:example.org",
"origin_server_ts": Date.now(),
"sender": user.toString(),
content: encrypted,
unsigned: {
"age": 1234
}
}),
room,
);
expect(decrypted).toBeInstanceOf(DecryptedRoomEvent);
const event = JSON.parse(decrypted.event);
expect(event.content.hello).toStrictEqual("world");
expect(decrypted.sender.toString()).toStrictEqual(user.toString());
expect(decrypted.senderDevice.toString()).toStrictEqual(device.toString());
expect(decrypted.senderCurve25519Key).toBeDefined();
expect(decrypted.senderClaimedEd25519Key).toBeDefined();
expect(decrypted.forwardingCurve25519KeyChain).toHaveLength(0);
expect(decrypted.verificationState).toStrictEqual(VerificationState.Trusted);
});
});
test('can update tracked users', async () => {
const m = await machine();
expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined);
});
test('can read cross-signing status', async () => {
const m = await machine();
const crossSigningStatus = await m.crossSigningStatus();
expect(crossSigningStatus).toBeInstanceOf(CrossSigningStatus);
expect(crossSigningStatus.hasMaster).toStrictEqual(false);
expect(crossSigningStatus.hasSelfSigning).toStrictEqual(false);
expect(crossSigningStatus.hasUserSigning).toStrictEqual(false);
});
test('can sign a message', async () => {
const m = await machine();
const signatures = await m.sign('foo');
expect(signatures.isEmpty).toStrictEqual(false);
expect(signatures.count).toStrictEqual(1n);
let base64;
// `get`
{
const signature = signatures.get(user);
expect(signature).toMatchObject({
"ed25519:foobar": expect.any(MaybeSignature),
});
expect(signature['ed25519:foobar'].isValid).toStrictEqual(true);
expect(signature['ed25519:foobar'].isInvalid).toStrictEqual(false);
expect(signature['ed25519:foobar'].invalidSignatureSource).toBeNull();
base64 = signature['ed25519:foobar'].signature.toBase64();
expect(base64).toMatch(/^[A-Za-z0-9\+/]+$/);
expect(signature['ed25519:foobar'].signature.ed25519.toBase64()).toStrictEqual(base64);
}
// `getSignature`
{
const signature = signatures.getSignature(user, new DeviceKeyId('ed25519:foobar'));
expect(signature.toBase64()).toStrictEqual(base64);
}
// Unknown signatures.
{
expect(signatures.get(new UserId('@hello:example.org'))).toBeNull();
expect(signatures.getSignature(user, new DeviceKeyId('world:foobar'))).toBeNull();
}
});
});
@@ -26,5 +26,4 @@ for (const request of [
expect(() => { new (request)() }).toThrow();
});
})
}
@@ -10,7 +10,7 @@ rust-version = "1.56"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
[lib]
crate-type = ["cdylib", "staticlib"]
crate-type = ["staticlib"]
[build-dependencies]
@@ -21,7 +21,7 @@ anyhow = "1.0.51"
extension-trait = "1.0.1"
futures-core = "0.3.17"
futures-util = { version = "0.3.17", default-features = false }
matrix-sdk = { path = "../matrix-sdk", features = ["experimental-timeline", "markdown"] }
matrix-sdk = { path = "../../crates/matrix-sdk", features = ["experimental-timeline", "markdown"] }
once_cell = "1.10.0"
parking_lot = "0.12.0"
sanitize-filename-reader-friendly = "2.2.1"
@@ -31,5 +31,6 @@ thiserror = "1.0.30"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.8"
tracing = "0.1.32"
# keep in sync with uniffi dependency in matrix-sdk-crypto-ffi, and uniffi_bindgen in ffi CI job
uniffi = "0.18.0"
uniffi_macros = "0.18.0"
@@ -1,13 +1,4 @@
namespace sdk {
[Throws=ClientError]
Client login_new_client(string base_path, string username, string password);
[Throws=ClientError]
Client guest_client(string base_path, string homeserver);
[Throws=ClientError]
Client login_with_token(string base_path, string restore_token);
MediaSource media_source_from_url(string url);
MessageEventContent message_event_content_from_markdown(string md);
string gen_transaction_id();
@@ -22,9 +13,33 @@ callback interface ClientDelegate {
void did_receive_sync_update();
};
interface ClientBuilder {
constructor();
[Self=ByArc]
ClientBuilder base_path(string path);
[Self=ByArc]
ClientBuilder username(string username);
[Self=ByArc]
ClientBuilder homeserver_url(string url);
[Throws=ClientError, Self=ByArc]
Client build();
};
interface Client {
void set_delegate(ClientDelegate? delegate);
[Throws=ClientError]
void login(string username, string password);
[Throws=ClientError]
void restore_login(string restore_token);
string homeserver();
void start_sync();
[Throws=ClientError]
@@ -52,6 +67,9 @@ interface Client {
[Throws=ClientError]
sequence<u8> get_media_content(MediaSource source);
[Throws=ClientError]
SessionVerificationController get_session_verification_controller();
};
callback interface RoomDelegate {
@@ -135,3 +153,58 @@ interface EmoteMessage {
interface MediaSource {
string url();
};
[Error]
enum AuthenticationError {
"ClientMissing",
"Generic",
};
interface AuthenticationService {
constructor(string base_path);
[Throws=AuthenticationError]
string homeserver();
[Throws=AuthenticationError]
string? authentication_issuer();
[Throws=AuthenticationError]
boolean supports_password_login();
[Throws=AuthenticationError]
void use_server(string server_name);
[Throws=AuthenticationError]
Client login(string username, string password);
};
interface SessionVerificationEmoji {
string symbol();
string description();
};
callback interface SessionVerificationControllerDelegate {
void did_receive_verification_data(sequence<SessionVerificationEmoji> data);
void did_fail();
void did_cancel();
void did_finish();
};
interface SessionVerificationController {
void set_delegate(SessionVerificationControllerDelegate? delegate);
boolean is_verified();
[Throws=ClientError]
void request_verification();
[Throws=ClientError]
void approve_verification();
[Throws=ClientError]
void decline_verification();
[Throws=ClientError]
void cancel_verification();
};
@@ -0,0 +1,100 @@
use std::sync::Arc;
use parking_lot::RwLock;
use super::{client::Client, client_builder::ClientBuilder};
pub struct AuthenticationService {
base_path: String,
client: RwLock<Option<Arc<Client>>>,
}
#[derive(Debug, thiserror::Error)]
pub enum AuthenticationError {
#[error("A successful call to use_server must be made first.")]
ClientMissing,
#[error("An error occurred: {message}")]
Generic { message: String },
}
impl From<anyhow::Error> for AuthenticationError {
fn from(e: anyhow::Error) -> AuthenticationError {
AuthenticationError::Generic { message: e.to_string() }
}
}
impl AuthenticationService {
/// Creates a new service to authenticate a user with.
pub fn new(base_path: String) -> Self {
AuthenticationService { base_path, client: RwLock::new(None) }
}
/// The currently configured homeserver.
pub fn homeserver(&self) -> Result<String, AuthenticationError> {
self.client
.read()
.as_ref()
.ok_or(AuthenticationError::ClientMissing)
.map(|client| client.homeserver())
}
/// The OIDC Provider that is trusted by the homeserver. `None` when
/// not configured.
pub fn authentication_issuer(&self) -> Result<Option<String>, AuthenticationError> {
self.client
.read()
.as_ref()
.ok_or(AuthenticationError::ClientMissing)
.map(|client| client.authentication_issuer())
}
/// Whether the current homeserver supports the password login flow.
pub fn supports_password_login(&self) -> Result<bool, AuthenticationError> {
self.client
.read()
.as_ref()
.ok_or(AuthenticationError::ClientMissing)
.and_then(|client| client.supports_password_login().map_err(AuthenticationError::from))
}
/// Updates the server to authenticate with the specified homeserver.
pub fn use_server(&self, server_name: String) -> Result<(), AuthenticationError> {
// Construct a username as the builder currently requires one.
let username = format!("@auth:{}", server_name);
let client = Arc::new(ClientBuilder::new())
.base_path(self.base_path.clone())
.username(username)
.build()
.map_err(AuthenticationError::from)?;
*self.client.write() = Some(client);
Ok(())
}
/// Performs a password login using the current homeserver.
pub fn login(
&self,
username: String,
password: String,
) -> Result<Arc<Client>, AuthenticationError> {
match self.client.read().as_ref() {
Some(client) => {
let homeserver_url = client.homeserver();
// Create a new client to setup the store path for the username
let client = Arc::new(ClientBuilder::new())
.base_path(self.base_path.clone())
.homeserver_url(homeserver_url)
.username(username.clone())
.build()
.map_err(AuthenticationError::from)?;
client
.login(username, password)
.map(|_| client.clone())
.map_err(AuthenticationError::from)
}
None => Err(AuthenticationError::ClientMissing),
}
}
}
@@ -6,6 +6,7 @@ use matrix_sdk::{
ruma::{
api::client::{
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
session::get_login_types,
sync::sync_events::v3::Filter,
},
events::room::MediaSource,
@@ -15,7 +16,10 @@ use matrix_sdk::{
};
use parking_lot::RwLock;
use super::{room::Room, ClientState, RestoreToken, RUNTIME};
use super::{
room::Room, session_verification::SessionVerificationController, ClientState, RestoreToken,
RUNTIME,
};
impl std::ops::Deref for Client {
type Target = MatrixClient;
@@ -33,6 +37,8 @@ pub struct Client {
client: MatrixClient,
state: Arc<RwLock<ClientState>>,
delegate: Arc<RwLock<Option<Box<dyn ClientDelegate>>>>,
session_verification_controller:
Arc<matrix_sdk::locks::RwLock<Option<SessionVerificationController>>>,
}
impl Client {
@@ -41,17 +47,60 @@ impl Client {
client,
state: Arc::new(RwLock::new(state)),
delegate: Arc::new(RwLock::new(None)),
session_verification_controller: Arc::new(matrix_sdk::locks::RwLock::new(None)),
}
}
pub fn login(&self, username: String, password: String) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
self.client.login_username(&username, &password).send().await?;
Ok(())
})
}
pub fn restore_login(&self, restore_token: String) -> anyhow::Result<()> {
let RestoreToken { session, homeurl: _, is_guest: _ } =
serde_json::from_str(&restore_token)?;
RUNTIME.block_on(async move {
self.client.restore_login(session).await?;
Ok(())
})
}
pub fn set_delegate(&self, delegate: Option<Box<dyn ClientDelegate>>) {
*self.delegate.write() = delegate;
}
/// The homeserver this client is configured to use.
pub fn homeserver(&self) -> String {
RUNTIME.block_on(async move { self.client.homeserver().await.to_string() })
}
/// The OIDC Provider that is trusted by the homeserver. `None` when
/// not configured.
pub fn authentication_issuer(&self) -> Option<String> {
RUNTIME.block_on(async move {
self.client.authentication_issuer().await.map(|server| server.to_string())
})
}
/// Whether or not the client's homeserver supports the password login flow.
pub fn supports_password_login(&self) -> anyhow::Result<bool> {
RUNTIME.block_on(async move {
let login_types = self.client.get_login_types().await?;
let supports_password = login_types.flows.iter().any(|login_type| {
matches!(login_type, get_login_types::v3::LoginType::Password(_))
});
Ok(supports_password)
})
}
pub fn start_sync(&self) {
let client = self.client.clone();
let state = self.state.clone();
let delegate = self.delegate.clone();
let session_verification_controller = self.session_verification_controller.clone();
RUNTIME.spawn(async move {
let mut filter = FilterDefinition::default();
let mut room_filter = RoomFilter::default();
@@ -67,7 +116,7 @@ impl Client {
let sync_settings = SyncSettings::new().filter(Filter::FilterId(&filter_id));
client
.sync_with_callback(sync_settings, |_| async {
.sync_with_callback(sync_settings, |sync_response| async {
if !state.read().has_first_synced {
state.write().has_first_synced = true
}
@@ -79,9 +128,18 @@ impl Client {
state.write().is_syncing = true;
}
if let Some(ref delegate) = *delegate.read() {
if let Some(delegate) = &*delegate.read() {
delegate.did_receive_sync_update()
}
if let Some(session_verification_controller) =
&*session_verification_controller.read().await
{
session_verification_controller
.process_to_device_messages(sync_response.to_device)
.await;
}
LoopCtrl::Continue
})
.await;
@@ -155,6 +213,33 @@ impl Client {
.await?)
})
}
pub fn get_session_verification_controller(
&self,
) -> anyhow::Result<Arc<SessionVerificationController>> {
RUNTIME.block_on(async move {
if let Some(session_verification_controller) =
&*self.session_verification_controller.read().await
{
return Ok(Arc::new(session_verification_controller.clone()));
}
let user_id = self.client.user_id().expect("Failed retrieving current user_id");
let user_identity = self
.client
.encryption()
.get_user_identity(user_id)
.await?
.expect("Failed retrieving user identity");
let session_verification_controller = SessionVerificationController::new(user_identity);
*self.session_verification_controller.write().await =
Some(session_verification_controller.clone());
Ok(Arc::new(session_verification_controller))
})
}
}
pub fn gen_transaction_id() -> String {
@@ -0,0 +1,87 @@
use std::{fs, path::PathBuf, sync::Arc};
use anyhow::Context;
use matrix_sdk::{
ruma::UserId, store::make_store_config, Client as MatrixClient,
ClientBuilder as MatrixClientBuilder,
};
use sanitize_filename_reader_friendly::sanitize;
use super::{client::Client, ClientState, RUNTIME};
#[derive(Clone)]
pub struct ClientBuilder {
base_path: Option<String>,
username: Option<String>,
homeserver_url: Option<String>,
inner: MatrixClientBuilder,
}
impl ClientBuilder {
pub fn new() -> Self {
Self {
base_path: None,
username: None,
homeserver_url: None,
inner: MatrixClient::builder().user_agent("rust-sdk-ios"),
}
}
pub fn base_path(self: Arc<Self>, path: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.base_path = Some(path);
Arc::new(builder)
}
pub fn username(self: Arc<Self>, username: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.username = Some(username);
Arc::new(builder)
}
pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.homeserver_url = Some(url);
Arc::new(builder)
}
pub fn build(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
let builder = unwrap_or_clone_arc(self);
let base_path = builder.base_path.context("Base path was not set")?;
let username = builder
.username
.context("Username to determine homeserver and home path was not set")?;
// Determine store path
let data_path = PathBuf::from(base_path).join(sanitize(&username));
fs::create_dir_all(&data_path)?;
let store_config = make_store_config(&data_path, None)?;
let mut inner_builder = builder.inner.store_config(store_config);
// Determine server either from explicitly set homeserver or from userId
if let Some(homeserver_url) = builder.homeserver_url {
inner_builder = inner_builder.homeserver_url(homeserver_url);
} else {
let user = UserId::parse(username)?;
inner_builder = inner_builder.server_name(user.server_name());
}
RUNTIME.block_on(async move {
let client = inner_builder.build().await?;
let c = Client::new(client, ClientState::default());
Ok(Arc::new(c))
})
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
fn unwrap_or_clone_arc<T: Clone>(arc: Arc<T>) -> T {
Arc::try_unwrap(arc).unwrap_or_else(|x| (*x).clone())
}
+57
View File
@@ -0,0 +1,57 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications)]
pub mod authentication_service;
pub mod backward_stream;
pub mod client;
pub mod client_builder;
pub mod messages;
pub mod room;
pub mod session_verification;
mod uniffi_api;
use client::Client;
use client_builder::ClientBuilder;
use matrix_sdk::Session;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
pub use uniffi_api::*;
pub static RUNTIME: Lazy<Runtime> =
Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime"));
pub use matrix_sdk::ruma::{api::client::account::register, UserId};
pub use self::{
authentication_service::*, backward_stream::*, client::*, messages::*, room::*,
session_verification::*,
};
#[derive(Default, Debug)]
pub struct ClientState {
is_guest: bool,
has_first_synced: bool,
is_syncing: bool,
should_stop_syncing: bool,
}
#[derive(Serialize, Deserialize)]
struct RestoreToken {
is_guest: bool,
homeurl: String,
session: Session,
}
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("client error: {msg}")]
Generic { msg: String },
}
impl From<anyhow::Error> for ClientError {
fn from(e: anyhow::Error) -> ClientError {
ClientError::Generic { msg: e.to_string() }
}
}
@@ -0,0 +1,193 @@
use std::sync::Arc;
use matrix_sdk::{
encryption::{
identities::UserIdentity,
verification::{SasVerification, VerificationRequest},
},
ruma::{
api::client::sync::sync_events::v3::ToDevice,
events::{key::verification::VerificationMethod, AnyToDeviceEvent},
},
};
use parking_lot::RwLock;
use super::RUNTIME;
pub struct SessionVerificationEmoji {
symbol: String,
description: String,
}
impl SessionVerificationEmoji {
pub fn symbol(&self) -> String {
self.symbol.clone()
}
pub fn description(&self) -> String {
self.description.clone()
}
}
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_receive_verification_data(&self, data: Vec<Arc<SessionVerificationEmoji>>);
fn did_fail(&self);
fn did_cancel(&self);
fn did_finish(&self);
}
#[derive(Clone)]
pub struct SessionVerificationController {
user_identity: UserIdentity,
delegate: Arc<RwLock<Option<Box<dyn SessionVerificationControllerDelegate>>>>,
verification_request: Arc<RwLock<Option<VerificationRequest>>>,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
}
impl SessionVerificationController {
pub fn new(user_identity: UserIdentity) -> Self {
SessionVerificationController {
user_identity,
delegate: Arc::new(RwLock::new(None)),
verification_request: Arc::new(RwLock::new(None)),
sas_verification: Arc::new(RwLock::new(None)),
}
}
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
*self.delegate.write() = delegate;
}
pub fn is_verified(&self) -> bool {
self.user_identity.verified()
}
pub fn request_verification(&self) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
let methods = vec![VerificationMethod::SasV1];
let verification_request =
self.user_identity.request_verification_with_methods(methods).await?;
*self.verification_request.write() = Some(verification_request);
Ok(())
})
}
pub fn approve_verification(&self) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
let sas_verification = self.sas_verification.read().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.confirm().await?;
}
Ok(())
})
}
pub fn decline_verification(&self) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
let sas_verification = self.sas_verification.read().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.mismatch().await?;
}
Ok(())
})
}
pub fn cancel_verification(&self) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
let verification_request = self.verification_request.read().clone();
if let Some(verification) = verification_request {
verification.cancel().await?;
}
Ok(())
})
}
pub async fn process_to_device_messages(&self, to_device: ToDevice) {
let sas_verification = self.sas_verification.clone();
for event in to_device.events.into_iter().filter_map(|e| e.deserialize().ok()) {
match event {
AnyToDeviceEvent::KeyVerificationReady(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
self.start_sas_verification().await;
}
AnyToDeviceEvent::KeyVerificationCancel(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(delegate) = &*self.delegate.read() {
delegate.did_cancel()
}
}
AnyToDeviceEvent::KeyVerificationKey(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(sas_verification) = &*sas_verification.read() {
if let Some(emojis) = sas_verification.emoji() {
if let Some(delegate) = &*self.delegate.read() {
let emojis = emojis
.iter()
.map(|e| {
Arc::new(SessionVerificationEmoji {
symbol: e.symbol.to_owned(),
description: e.description.to_owned(),
})
})
.collect::<Vec<_>>();
delegate.did_receive_verification_data(emojis);
}
} else if let Some(delegate) = &*self.delegate.read() {
delegate.did_fail()
}
} else if let Some(delegate) = &*self.delegate.read() {
delegate.did_fail()
}
}
AnyToDeviceEvent::KeyVerificationDone(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(delegate) = &*self.delegate.read() {
delegate.did_finish()
}
}
_ => (),
}
}
}
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
if let Some(verification) = &*self.verification_request.read() {
return verification.flow_id() == transaction_id;
}
false
}
async fn start_sas_verification(&self) {
let verification_request = self.verification_request.read().clone();
if let Some(verification) = verification_request {
match verification.start_sas().await {
Ok(verification) => {
*self.sas_verification.write() = verification;
}
Err(_) => {
if let Some(delegate) = &*self.delegate.read() {
delegate.did_fail()
}
}
}
}
}
}
+4 -6
View File
@@ -24,14 +24,12 @@ coverage:
informational: true
paths:
- "bindings/"
- "crates/matrix-sdk-crypto-ffi/"
- "crates/matrix-sdk-ffi/"
patch: off
ignore:
- "crates/matrix-sdk-crypto-ffi"
- "crates/matrix-sdk-crypto-js"
- "crates/matrix-sdk-crypto-nodejs"
- "crates/matrix-sdk-ffi"
- "bindings/matrix-sdk-crypto-ffi"
- "bindings/matrix-sdk-crypto-js"
- "bindings/matrix-sdk-crypto-nodejs"
- "bindings/matrix-sdk-ffi"
- "crates/matrix-sdk-indexeddb"
- "crates/matrix-sdk-test"
- "crates/matrix-sdk-test-macros"
+2 -2
View File
@@ -34,7 +34,7 @@ http = "0.2.6"
matrix-sdk = { version = "0.5.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] }
percent-encoding = "2.1.0"
regex = "1.5.5"
ruma = { version = "0.6.1", features = ["client-api-c", "appservice-api-s"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "appservice-api-s"] }
serde = "1.0.136"
serde_json = "1.0.79"
serde_yaml = "0.8.23"
@@ -46,6 +46,6 @@ warp = { version = "0.3.2", default-features = false }
[dev-dependencies]
matrix-sdk-test = { version = "0.5.0", path = "../matrix-sdk-test", features = ["appservice"] }
mockito = "0.31.0"
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] }
tracing-subscriber = "0.3.11"
wiremock = "0.5.13"
+1 -1
View File
@@ -161,7 +161,7 @@ impl<'a> VirtualUserBuilder<'a> {
}
}
/// Set the device id of the virtual user
/// Set the device ID of the virtual user
pub fn device_id(mut self, device_id: Option<OwnedDeviceId>) -> Self {
self.device_id = device_id;
self
+98 -26
View File
@@ -18,15 +18,22 @@ use ruma::{
};
use serde_json::json;
use warp::{Filter, Reply};
use wiremock::{
matchers::{body_json, header, method, path},
Mock, MockServer, ResponseTemplate,
};
fn registration_string() -> String {
include_str!("../tests/registration.yaml").to_owned()
}
async fn appservice(registration: Option<Registration>) -> Result<AppService> {
async fn appservice(
homeserver_url: Option<String>,
registration: Option<Registration>,
) -> Result<AppService> {
// env::set_var(
// "RUST_LOG",
// "mockito=debug,matrix_sdk=debug,ruma=debug,warp=debug",
// "wiremock=debug,matrix_sdk=debug,ruma=debug,warp=debug",
// );
let _ = tracing_subscriber::fmt::try_init();
@@ -35,7 +42,7 @@ async fn appservice(registration: Option<Registration>) -> Result<AppService> {
None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(),
};
let homeserver_url = mockito::server_url();
let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned());
let server_name = "localhost";
let client_builder = Client::builder()
@@ -53,28 +60,27 @@ async fn appservice(registration: Option<Registration>) -> Result<AppService> {
#[async_test]
async fn test_register_virtual_user() -> Result<()> {
let appservice = appservice(None).await?;
let server = MockServer::start().await;
let appservice = appservice(Some(server.uri()), None).await?;
let localpart = "someone";
let _mock = mockito::mock("POST", "/_matrix/client/r0/register")
.match_query(mockito::Matcher::Missing)
.match_header(
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/register"))
.and(header(
"authorization",
mockito::Matcher::Exact(format!("Bearer {}", appservice.registration().as_token)),
)
.match_body(mockito::Matcher::Json(json!({
format!("Bearer {}", appservice.registration().as_token).as_str(),
))
.and(body_json(json!({
"username": localpart.to_owned(),
"type": "m.login.application_service"
})))
.with_body(format!(
r#"{{
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"access_token": "abc123",
"device_id": "GHTYAJCE",
"user_id": "@{localpart}:localhost"
}}"#,
localpart = localpart
))
.create();
"user_id": format!("@{localpart}:localhost"),
})))
.mount(&server)
.await;
appservice.register_virtual_user(localpart).await?;
@@ -89,7 +95,7 @@ async fn test_put_transaction() -> Result<()> {
transaction_builder.add_room_event(EventsJson::Member);
let transaction = transaction_builder.build_json_transaction();
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
let status = warp::test::request()
.method("PUT")
@@ -106,9 +112,75 @@ async fn test_put_transaction() -> Result<()> {
Ok(())
}
#[async_test]
async fn test_put_transaction_with_repeating_txn_id() -> Result<()> {
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
let mut transaction_builder = TransactionBuilder::new();
transaction_builder.add_room_event(EventsJson::Member);
let transaction = transaction_builder.build_json_transaction();
let appservice = appservice(None, None).await?;
#[allow(clippy::mutex_atomic)]
let on_state_member = Arc::new(Mutex::new(false));
appservice
.register_event_handler({
let on_state_member = on_state_member.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
*on_state_member.lock().unwrap() = true;
future::ready(())
}
})
.await?;
let status = warp::test::request()
.method("PUT")
.path(uri)
.json(&transaction)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
assert_eq!(status, 200);
{
let on_room_member_called = *on_state_member.lock().unwrap();
assert!(on_room_member_called);
}
// Reset this to check that next time it doesnt get called
{
let mut on_room_member_called = on_state_member.lock().unwrap();
*on_room_member_called = false;
}
let status = warp::test::request()
.method("PUT")
.path(uri)
.json(&transaction)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
// According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events
// This should noop and return 200.
assert_eq!(status, 200);
{
let on_room_member_called = *on_state_member.lock().unwrap();
// This time we should not have called the event handler.
assert!(!on_room_member_called);
}
Ok(())
}
#[async_test]
async fn test_get_user() -> Result<()> {
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await;
let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token";
@@ -129,7 +201,7 @@ async fn test_get_user() -> Result<()> {
#[async_test]
async fn test_get_room() -> Result<()> {
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await;
let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token";
@@ -156,7 +228,7 @@ async fn test_invalid_access_token() -> Result<()> {
let transaction =
transaction_builder.add_room_event(EventsJson::Member).build_json_transaction();
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
let status = warp::test::request()
.method("PUT")
@@ -181,7 +253,7 @@ async fn test_no_access_token() -> Result<()> {
transaction_builder.add_room_event(EventsJson::Member);
let transaction = transaction_builder.build_json_transaction();
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
{
let status = warp::test::request()
@@ -202,7 +274,7 @@ async fn test_no_access_token() -> Result<()> {
#[async_test]
async fn test_event_handler() -> Result<()> {
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
#[allow(clippy::mutex_atomic)]
let on_state_member = Arc::new(Mutex::new(false));
@@ -238,7 +310,7 @@ async fn test_event_handler() -> Result<()> {
#[async_test]
async fn test_unrelated_path() -> Result<()> {
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
let status = {
let consumer_filter = warp::any()
@@ -274,7 +346,7 @@ async fn test_appservice_on_sub_path() -> Result<()> {
transaction_builder.add_room_event(EventsJson::MemberNameChange);
let transaction_2 = transaction_builder.build_json_transaction();
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
{
warp::test::request()
@@ -381,7 +453,7 @@ async fn test_receive_transaction() -> Result<()> {
}))?
.cast::<AnyRoomEvent>(),
];
let appservice = appservice(None).await?;
let appservice = appservice(None, None).await?;
let alice = appservice.virtual_user_client("_appservice_alice").await?;
let bob = appservice.virtual_user_client("_appservice_bob").await?;
+2 -2
View File
@@ -48,10 +48,10 @@ 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", "signatures"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "canonical-json"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ruma = { version = "0.6.2", features = ["client-api-c", "signatures"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "canonical-json"] }
[dev-dependencies]
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
+20 -24
View File
@@ -14,6 +14,7 @@
// limitations under the License.
use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet},
fmt,
};
@@ -111,7 +112,7 @@ impl BaseClient {
let store = config.state_store.map(Store::new).unwrap_or_else(Store::open_memory_store);
#[cfg(feature = "e2e-encryption")]
let crypto_store =
config.crypto_store.unwrap_or_else(|| Box::new(MemoryCryptoStore::default())).into();
config.crypto_store.unwrap_or_else(|| Arc::new(MemoryCryptoStore::default()));
BaseClient {
store,
@@ -149,7 +150,7 @@ impl BaseClient {
/// # Arguments
///
/// * `response` - A successful login response that contains our access
/// token and device id.
/// token and device ID.
pub async fn receive_login_response(
&self,
response: &api::session::login::v3::Response,
@@ -329,25 +330,19 @@ impl BaseClient {
}
if let Some(context) = &push_context {
if event
.event
.get_field::<OwnedUserId>("sender")?
.map_or(false, |id| id != user_id)
{
let actions = push_rules.get_actions(&event.event, context).to_vec();
let actions = push_rules.get_actions(&event.event, context);
if actions.iter().any(|a| matches!(a, Action::Notify)) {
changes.add_notification(
room_id,
Notification::new(
actions,
event.event.clone(),
false,
room_id.to_owned(),
MilliSecondsSinceUnixEpoch::now(),
),
);
}
if actions.iter().any(|a| matches!(a, Action::Notify)) {
changes.add_notification(
room_id,
Notification::new(
actions.to_owned(),
event.event.clone(),
false,
room_id.to_owned(),
MilliSecondsSinceUnixEpoch::now(),
),
);
}
// TODO if there is an
// Action::SetTweak(Tweak::Highlight) we need to store
@@ -447,7 +442,7 @@ impl BaseClient {
// having confusing profile changes when a member gets
// kicked/banned.
if member.state_key() == member.sender() {
profiles.insert(member.sender().to_owned(), (&member).into());
profiles.insert(member.sender().to_owned(), member.borrow().into());
}
members.insert(member.state_key().to_owned(), member);
@@ -573,7 +568,7 @@ impl BaseClient {
};
let mut changes = StateChanges::new(next_batch.clone());
let mut ambiguity_cache = AmbiguityCache::new(self.store.clone());
let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone());
self.handle_account_data(&account_data.events, &mut changes).await;
@@ -830,7 +825,7 @@ impl BaseClient {
})
.collect();
let mut ambiguity_cache = AmbiguityCache::new(self.store.clone());
let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone());
if let Some(room) = self.store.get_room(room_id) {
let mut room_info = room.clone_info();
@@ -860,7 +855,7 @@ impl BaseClient {
.profiles
.entry(room_id.to_owned())
.or_default()
.insert(member.sender().to_owned(), (&member).into());
.insert(member.sender().to_owned(), member.borrow().into());
}
changes
@@ -1053,6 +1048,7 @@ impl BaseClient {
};
Ok(Some(PushConditionRoomCtx {
user_id: user_id.to_owned(),
room_id: room_id.to_owned(),
member_count: UInt::new(member_count).unwrap_or(UInt::MAX),
user_display_name,
@@ -46,6 +46,11 @@ impl RoomMember {
self.event.user_id()
}
/// Get the original member event
pub fn event(&self) -> &Arc<MemberEvent> {
&self.event
}
/// Get the display name of the member if there is one.
pub fn display_name(&self) -> Option<&str> {
if let Some(p) = self.profile.as_ref() {

Some files were not shown because too many files have changed in this diff Show More