Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6497e8269 | |||
| 7f529b83eb |
@@ -1,3 +1,13 @@
|
||||
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
|
||||
# when a `--target` is passed to Cargo. Historically this was not the case, and
|
||||
# because of that, cross-compilation would not set the rustflags configured
|
||||
# below in `target.'cfg(all())'` for them, resulting in cache invalidation.
|
||||
#
|
||||
# Since this is an unstable feature (enabled at the bottom of the file), this
|
||||
# setting is unfortunately ignored on stable toolchains, but it's still better
|
||||
# to have it apply on nightly than using the old behavior for all toolchains.
|
||||
target-applies-to-host = false
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
@@ -5,5 +15,28 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
||||
|
||||
[target.'cfg(all())']
|
||||
rustflags = [
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wsemicolon_in_expressions_from_macros",
|
||||
"-Wunused_extern_crates",
|
||||
"-Wunused_import_braces",
|
||||
"-Wunused_qualifications",
|
||||
"-Wtrivial_casts",
|
||||
"-Wtrivial_numeric_casts",
|
||||
"-Wclippy::cloned_instead_of_copied",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Wclippy::needless_borrow",
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
]
|
||||
|
||||
# activate the target-applies-to-host feature.
|
||||
# Required for `target-applies-to-host` at the top to take effect.
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
target-applies-to-host = true
|
||||
|
||||
+2
-10
@@ -1,13 +1,5 @@
|
||||
|
||||
[profile.default]
|
||||
retries = { backoff = "exponential", count = 3, delay = "1s", jitter = true }
|
||||
retries = 2
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci]
|
||||
retries = { backoff = "exponential", count = 4, delay = "1s", jitter = true }
|
||||
# kill the slow tests if they still aren't up after 180s
|
||||
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
store-success-output = true
|
||||
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
|
||||
[graph]
|
||||
all-features = true
|
||||
exclude = [
|
||||
# dev only dependency
|
||||
"criterion"
|
||||
]
|
||||
|
||||
[advisories]
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
exceptions = [
|
||||
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
|
||||
{ allow = ["CDDL-1.0"], crate = "inferno" },
|
||||
{ allow = ["LicenseRef-ring"], crate = "ring" },
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "LicenseRef-ring"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[bans]
|
||||
# We should disallow this, but it's currently a PITA.
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
|
||||
allow-git = [
|
||||
# A patch override for the bindings fixing a bug for Android before upstream
|
||||
# releases a new version.
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Same as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/element-hq/async-compat",
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
]
|
||||
@@ -1,2 +0,0 @@
|
||||
* @matrix-org/rust
|
||||
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Security audit
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -8,16 +8,15 @@ jobs:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
environment: matrix-rust-bot
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2025-06-27
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
+131
-120
@@ -18,6 +18,8 @@ concurrency:
|
||||
|
||||
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:
|
||||
xtask:
|
||||
@@ -31,28 +33,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -61,88 +56,157 @@ jobs:
|
||||
- name: Build library & generate bindings
|
||||
run: target/debug/xtask ci bindings
|
||||
|
||||
test-android:
|
||||
name: matrix-rust-components-kotlin
|
||||
needs: xtask
|
||||
lint-js-bindings:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- name: "[m]-crypto-nodejs"
|
||||
path: "bindings/matrix-sdk-crypto-nodejs"
|
||||
- name: "[m]-crypto-js"
|
||||
path: "bindings/matrix-sdk-crypto-js"
|
||||
|
||||
name: lint ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
ref: main
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ${{ matrix.path }}
|
||||
run: npm install
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.7
|
||||
- name: run lint
|
||||
working-directory: ${{ matrix.path }}
|
||||
run: npm run lint
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
id: install-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
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-20.04]
|
||||
node-version: [14.0, 16.0, 18.0, 19.0]
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
os-name: 🐧
|
||||
|
||||
- os: macos-latest
|
||||
os-name: 🍏
|
||||
node-version: 18.0
|
||||
|
||||
- node-version: 18.0
|
||||
build-doc: true
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- 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@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
node-version: 18.0
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
|
||||
run: npm install
|
||||
|
||||
- name: Install Rust dependencies
|
||||
run: |
|
||||
rustup target add x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
- name: Build the WebAssembly + JavaScript binding
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
|
||||
run: npm run build
|
||||
|
||||
- name: Build SDK bindings for Android
|
||||
# Building for x86_64-linux-android as it's the most prone to breaking and building for every arch is too much
|
||||
run: |
|
||||
echo "Building SDK for x86_64-linux-android and creating bindings"
|
||||
target/debug/xtask kotlin build-android-library --package full-sdk --only-target x86_64-linux-android --src-dir rust-components-kotlin/sdk/sdk-android/src/main
|
||||
echo "Copying the result binary to the Android project"
|
||||
cd rust-components-kotlin
|
||||
echo "Building the Kotlin bindings"
|
||||
./gradlew :sdk:sdk-android:assembleDebug
|
||||
- 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
|
||||
needs: xtask
|
||||
runs-on: macos-15
|
||||
runs-on: macos-12
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -150,18 +214,11 @@ jobs:
|
||||
- name: Install aarch64-apple-ios target
|
||||
run: rustup target install aarch64-apple-ios
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
@@ -175,50 +232,4 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev --ios-deployment-target=18.0
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
with:
|
||||
use_rust_sdk: "." # use local checkout
|
||||
use_complement_crypto: "MATCHING_BRANCH"
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add rust targets
|
||||
run: |
|
||||
rustup target add aarch64-apple-ios
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Run the Build Framework script
|
||||
run: ./bindings/apple/build_crypto_xcframework.sh -i
|
||||
|
||||
- name: Is XCFramework generated?
|
||||
if: ${{ hashFiles('generated/MatrixSDKCryptoFFI.zip') != '' }}
|
||||
run: echo "XCFramework exists"
|
||||
run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios
|
||||
|
||||
+190
-107
@@ -6,6 +6,11 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -13,9 +18,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files.
|
||||
# Just want it to run the tests (option `no` instead of `auto`).
|
||||
INSTA_UPDATE: no
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
@@ -24,6 +26,7 @@ jobs:
|
||||
test-matrix-sdk-features:
|
||||
name: 🐧 [m], ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -31,9 +34,9 @@ jobs:
|
||||
matrix:
|
||||
name:
|
||||
- no-encryption
|
||||
- no-sqlite
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- no-sled
|
||||
- no-encryption-and-sled
|
||||
- sled-cryptostore
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
@@ -41,16 +44,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -60,13 +58,13 @@ jobs:
|
||||
|
||||
# ... but only save the cache on the main branch
|
||||
# cf https://github.com/Swatinem/rust-cache/issues/95
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/head/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -80,24 +78,23 @@ jobs:
|
||||
name: 🐧 [m]-examples
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -111,29 +108,23 @@ jobs:
|
||||
name: 🐧 [m]-crypto
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -145,6 +136,7 @@ jobs:
|
||||
|
||||
test-all-crates:
|
||||
name: ${{ matrix.name }}
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -165,28 +157,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install libsqlite
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
- name: Install Rust stable
|
||||
if: matrix.rust == 'stable'
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
@@ -194,7 +179,7 @@ jobs:
|
||||
- name: Test
|
||||
run: |
|
||||
cargo nextest run --workspace \
|
||||
--exclude matrix-sdk-integration-testing --features testing
|
||||
--exclude matrix-sdk-integration-testing --exclude sliding-sync-integration-test
|
||||
|
||||
- name: Test documentation
|
||||
run: |
|
||||
@@ -203,6 +188,7 @@ jobs:
|
||||
test-wasm:
|
||||
name: 🕸️ ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -219,25 +205,27 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m], no-default'
|
||||
- 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]-ui'
|
||||
cmd: matrix-sdk-ui
|
||||
check_only: true
|
||||
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- 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
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -246,8 +234,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.1
|
||||
if: '!matrix.check_only'
|
||||
uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.10.3
|
||||
|
||||
@@ -260,13 +247,13 @@ jobs:
|
||||
|
||||
# ... but only save the cache on the main branch
|
||||
# cf https://github.com/Swatinem/rust-cache/issues/95
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/head/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -277,123 +264,219 @@ jobs:
|
||||
target/debug/xtask ci wasm ${{ matrix.cmd }}
|
||||
|
||||
- name: Wasm-Pack test
|
||||
if: '!matrix.check_only'
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
test-appservice:
|
||||
name: ${{ matrix.os-name }} [m]-appservice
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
os-name: 🐧
|
||||
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
|
||||
- os: macos-latest
|
||||
os-name: 🍏
|
||||
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ matrix.xtask-cachekey }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
target/debug/xtask ci test-appservice
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
run: |
|
||||
cargo fmt -- --check
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
uses: crate-ci/typos@v1.13.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
clippy:
|
||||
name: Run clippy
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2025-06-27
|
||||
components: clippy, rustfmt
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Check Formatting
|
||||
run: |
|
||||
target/debug/xtask ci style
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
target/debug/xtask ci clippy
|
||||
|
||||
integration-tests:
|
||||
name: Integration test
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- uses: michaelkaye/setup-matrix-synapse@main
|
||||
with:
|
||||
uploadLogs: true
|
||||
httpPort: 8228
|
||||
disableRateLimiting: true
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
|
||||
sliding-sync-integration-tests:
|
||||
name: Sliding Sync Integration test
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
services:
|
||||
# sliding sync needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# run sliding sync and point it at the postgres container and synapse container.
|
||||
# the postgres container needs to be above this to make sure it has started prior to this service.
|
||||
slidingsync:
|
||||
image: "ghcr.io/matrix-org/sliding-sync:v0.99.0"
|
||||
env:
|
||||
SYNCV3_SERVER: "http://synapse:8008"
|
||||
SYNCV3_SECRET: "SUPER_CI_SECRET"
|
||||
SYNCV3_BINDADDR: ":8118"
|
||||
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
|
||||
ports:
|
||||
- 8118:8118
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml
|
||||
# Custom image built from https://github.com/matrix-org/synapse/tree/v1.72.0/docker/complement
|
||||
# with a dummy /complement/ca set
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.72.0
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
ports:
|
||||
- 8008:8008
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
RUST_LOG: "info,matrix_sdk=trace"
|
||||
RUST_LOG: "hyper=trace"
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
compile-bench:
|
||||
name: 🚄 Compile benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Compile benchmarks (no run)
|
||||
run: |
|
||||
cargo bench --profile dev --no-run
|
||||
cargo nextest run -p sliding-sync-integration-test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Code Coverage
|
||||
name: Code coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -12,113 +12,52 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# without matrix_sdk=trace, expressions in `trace!` fields are not evaluated
|
||||
# when the `trace!` statement is hit, and thus not covered
|
||||
RUST_LOG: info,matrix_sdk=trace
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
uses: ./.github/workflows/xtask.yml
|
||||
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
needs: xtask
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
services:
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
ports:
|
||||
- 8008:8008
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install tarpaulin
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-tarpaulin
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
# set up backend for integration tests
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: gnunicorn/setup-matrix-synapse@main
|
||||
with:
|
||||
uploadLogs: true
|
||||
httpPort: 8228
|
||||
disableRateLimiting: true
|
||||
serverName: "matrix-sdk.rs"
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Run tarpaulin
|
||||
run: |
|
||||
cargo tarpaulin --out Xml -e sliding-sync-integration-test
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Create the coverage report
|
||||
run: |
|
||||
target/debug/xtask ci coverage -o codecov
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
|
||||
- name: Get PR number and commit SHA
|
||||
run: |
|
||||
echo "Storing PR number ${{ github.event.number }}"
|
||||
echo "${{ github.event.number }}" > pr_number.txt
|
||||
|
||||
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
|
||||
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
|
||||
|
||||
- name: Move the JUnit file into the root directory
|
||||
shell: bash
|
||||
run: |
|
||||
mv target/nextest/ci/junit.xml ./junit.xml
|
||||
|
||||
# This stores the coverage report and metadata in artifacts.
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
coverage.xml
|
||||
junit.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- run: |
|
||||
echo 'The coverage report was stored in Github artifacts.'
|
||||
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
# Work around frequent upload errors, for runs inside the main repo (not PRs from forks).
|
||||
# Otherwise not required for public repos.
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
# The upload sometimes fails due to https://github.com/codecov/codecov-action/issues/837.
|
||||
# To make sure that the failure gets flagged clearly in the UI, fail the action.
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities)
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -1,35 +0,0 @@
|
||||
# Check if the path of changed file is longer than 260 characters
|
||||
# that windows filesystem allows
|
||||
|
||||
name: Detect long path among changed files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
long-path:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
MAX_LENGTH: 120 # set max length to 120, considering the base path of app project that uses matrix-sdk
|
||||
run: |
|
||||
for file in ${ALL_CHANGED_FILES}; do
|
||||
if [ ${#file} -gt $MAX_LENGTH ]; then
|
||||
echo "File path is too long. Length: ${#file}, Path: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -1,12 +0,0 @@
|
||||
name: Detects unused dependencies
|
||||
on:
|
||||
pull_request: { branches: "*" }
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@v0.8.0
|
||||
@@ -9,54 +9,61 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: All crates
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-06-27
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
# Keep in sync with xtask docs
|
||||
- name: Build documentation
|
||||
- name: Build rust documentation
|
||||
env:
|
||||
# Work around https://github.com/rust-lang/cargo/issues/10744
|
||||
CARGO_TARGET_APPLIES_TO_HOST: "true"
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
|
||||
cargo doc --no-deps --features docsrs
|
||||
|
||||
- name: Upload artifact
|
||||
- name: Build `matrix-sdk-crypto-nodejs` doc
|
||||
run: |
|
||||
cd bindings/matrix-sdk-crypto-nodejs
|
||||
npm install
|
||||
npm run build && npm run doc
|
||||
|
||||
- name: Build `matrix-sdk-crypto-js` doc
|
||||
run: |
|
||||
cd bindings/matrix-sdk-crypto-js
|
||||
npm install
|
||||
npm run build && npm run doc
|
||||
|
||||
- name: Prepare the doc hierarchy
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p doc/bindings/matrix-sdk-crypto-nodejs/
|
||||
mkdir -p doc/bindings/matrix-sdk-crypto-js/
|
||||
mv target/doc/* doc/
|
||||
mv bindings/matrix-sdk-crypto-nodejs/docs/* doc/bindings/matrix-sdk-crypto-nodejs/
|
||||
mv bindings/matrix-sdk-crypto-js/docs/* doc/bindings/matrix-sdk-crypto-js/
|
||||
|
||||
- name: Deploy documentation
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./doc/
|
||||
force_orphan: true
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
name: Git Checks
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
block-fixup:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@v2.0.0
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Rust version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
@@ -0,0 +1,119 @@
|
||||
name: Prepare Crypto-Node.js Release
|
||||
#
|
||||
# This is a helper workflow to craft a new Node.js release, trigger this via
|
||||
# the Github Workflow UI by dispatching it manually. Provide the version, the
|
||||
# matrix-sdk-crypto-nodejs npm package should be set to, and a optionally the
|
||||
# old version (as used in the git tag) this release should be compared to.
|
||||
#
|
||||
# This will then:
|
||||
# 1. bump the npm version to the one you specified
|
||||
# 2. commit that change together with the changelog (if it changed, see below)
|
||||
# 3. create the appropriate tag on that commit
|
||||
# 4. create the Github draft release, including the changes (if given, see below)
|
||||
# 5. push these to a new branch, including tag, triggering the `release-crypto-nodejs` workflow
|
||||
# 6. create a PR to merge these back into `main`
|
||||
#
|
||||
# Additionally, if you provide a tag to comapare this tag to, this will:
|
||||
# 1. create a changelog between the two releases, used for the github release
|
||||
# 2. update the Changelog.md and include it in the commit
|
||||
#
|
||||
# The remaining tasks are done by the release-crypto-nodejs workflow.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'New Node.js SemVer version to create'
|
||||
required: true
|
||||
type: string
|
||||
previous_version:
|
||||
description: 'Create the changelog by comparing to this old SemVer Version (as used in the tag) '
|
||||
type: string
|
||||
|
||||
env:
|
||||
PKG_PATH: "bindings/matrix-sdk-crypto-nodejs"
|
||||
TAG_PREFIX: "matrix-sdk-crypto-nodejs-v"
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: "Preparing crypto-nodejs release tag"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: "${{ env.TAG_PREFIX }}${{ inputs.version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
# Generate changelog since last tag, if given
|
||||
- name: Generate a changelog for upload
|
||||
if: inputs.previous_version
|
||||
uses: orhun/git-cliff-action@v1
|
||||
with:
|
||||
config: "${{ env.PKG_PATH }}/cliff.toml"
|
||||
args: --strip header "${{env.TAG_PREFIX}}${{ inputs.previous_version }}..HEAD"
|
||||
env:
|
||||
GIT_CLIFF_TAG: "Changes ${{ inputs.previous_version }} -> ${{ inputs.version }}"
|
||||
GIT_CLIFF_OUTPUT: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md"
|
||||
|
||||
# Update changelog since last tag, if given
|
||||
- name: Update existing Changelog
|
||||
if: inputs.previous_version
|
||||
uses: orhun/git-cliff-action@v1
|
||||
with:
|
||||
config: "${{ env.PKG_PATH }}/cliff.toml"
|
||||
args: "${{env.TAG_PREFIX}}${{ inputs.previous_version }}..HEAD"
|
||||
env:
|
||||
GIT_CLIFF_TAG: "${{ inputs.version }}"
|
||||
GIT_CLIFF_PREPEND: "${{ env.PKG_PATH }}/CHANGELOG.md"
|
||||
|
||||
- name: Set version
|
||||
id: package_version
|
||||
working-directory: ${{ env.PKG_PATH }}
|
||||
run: npm version ${{ inputs.version }}
|
||||
|
||||
- uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
default_author: github_actions
|
||||
message: "Tagging Crypto-Node.js for release"
|
||||
tag: "${{env.TAG_PREFIX}}${{ inputs.version }}"
|
||||
new_branch: "gh-action/release-${{ env.TAG_PREFIX }}${{ inputs.version }}"
|
||||
push: true
|
||||
add: |
|
||||
${{ env.PKG_PATH }}/package.json
|
||||
${{ env.PKG_PATH }}/CHANGELOG.md
|
||||
|
||||
# if we have generated changes
|
||||
- name: Update Github Release notes
|
||||
if: inputs.previous_version
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }}
|
||||
body_path: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md"
|
||||
|
||||
# no changes, use the default changelog for the body
|
||||
- name: Update Github Release notes
|
||||
if: ${{!inputs.previous_version}}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }}
|
||||
body_path: "${{ env.PKG_PATH }}/CHANGELOG.md"
|
||||
|
||||
# let's create a PR for all this, too
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
title: "Preparing Release ${{ env.TAG_PREFIX }}${{ inputs.version }}"
|
||||
body: |
|
||||
Automatic Pull-Request to merge release ${{ env.TAG_PREFIX }}${{ inputs.version }}
|
||||
|
||||
trigger-release:
|
||||
# and trigger the tagging release workflow
|
||||
uses: ./.github/workflows/release-crypto-nodejs.yml
|
||||
needs: ['prepare-release']
|
||||
name: "Trigger release Workflow"
|
||||
with:
|
||||
tag: ${{needs.prepare-release.outputs.tag}}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,137 @@
|
||||
name: Release Crypto-Node.js
|
||||
#
|
||||
# This workflow releases the crypto-bindings for nodejs
|
||||
#
|
||||
# It is triggered when seeing a tag prefixed matching `matrix-sdk-crypto-nodejs-v[0-9]+.*`,
|
||||
# which then build the native bindings for linux, mac and windows via the CI and uploads
|
||||
# them to the corresponding Github Release tag. Once they are finished, this workflow will
|
||||
# package the npm tar.gz and uploads that to the Github Release tag as well, before publishing
|
||||
# it to npmjs.com automatically.
|
||||
#
|
||||
# The usual way to trigger this is by manually triggering the `prep-crypto-nodejs-release`
|
||||
# workflow. See its documentation for instructions how to use it.
|
||||
|
||||
env:
|
||||
PKG_PATH: "bindings/matrix-sdk-crypto-nodejs"
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: 'aarch64-linux-gnu-gcc'
|
||||
CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER: 'i686-linux-gnu-gcc'
|
||||
CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: 'arm-linux-gnueabihf-gcc'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- matrix-sdk-crypto-nodejs-v[0-9]+.*
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "The tag to build with"
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
upload-assets:
|
||||
name: "Upload prebuilt libraries"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# ----------------------------------- Linux
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
- target: i686-unknown-linux-gnu
|
||||
apt_install: gcc-i686-linux-gnu g++-i686-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
apt_install: gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
- target: arm-unknown-linux-gnueabihf
|
||||
os: ubuntu-20.04
|
||||
apt_install: gcc-arm-linux-gnueabihf
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
apt_install: musl-tools
|
||||
# ----------------------------------- macOS
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
# ----------------------------------- Windows
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
- target: i686-pc-windows-msvc
|
||||
os: windows-latest
|
||||
- target: aarch64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# use the given tag
|
||||
- uses: actions/checkout@v3
|
||||
name: "Checking out ${{ inputs.tag }}"
|
||||
if: "${{ inputs.tag }}"
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
# use the default
|
||||
- uses: actions/checkout@v3
|
||||
if: "${{ !inputs.tag }}"
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- if: ${{ matrix.apt_install }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ${{ matrix.apt_install }}
|
||||
- name: Build lib
|
||||
working-directory: ${{env.PKG_PATH}}
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
npx napi build --platform --release --strip --target ${{ matrix.target }}
|
||||
- name: Upload artifacts to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{env.PKG_PATH}}/*.node
|
||||
|
||||
publish-nodejs-package:
|
||||
name: "Package nodejs package"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- upload-assets
|
||||
steps:
|
||||
# use the given tag
|
||||
- uses: actions/checkout@v3
|
||||
name: "Checking out ${{ inputs.tag }}"
|
||||
if: "${{ inputs.tag }}"
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
# use the default
|
||||
- uses: actions/checkout@v3
|
||||
if: "${{ !inputs.tag }}"
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
- name: Build lib
|
||||
working-directory: ${{env.PKG_PATH}}
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
npm run build
|
||||
npm pack
|
||||
- name: Upload npm package to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{env.PKG_PATH}}/*tgz
|
||||
- name: Publish to npmjs.com
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
package: ${{env.PKG_PATH}}/package.json
|
||||
access: public
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,59 @@
|
||||
# This workflow releases the `matrix-sdk-crypto-js` project.
|
||||
#
|
||||
# It is triggered when a new tag appears that matches
|
||||
# `matrix-sdk-crypto-js-[0-9]+.*`. This workflow builds the package
|
||||
# for the bindings, runs its tests to ensure everything is still
|
||||
# correct, and publishes the package on NPM and on a newly created
|
||||
# Github release.
|
||||
|
||||
|
||||
name: Release `crypto-js`
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
PKG_PATH: "bindings/matrix-sdk-crypto-js"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- matrix-sdk-crypto-js-[0-9]+.*
|
||||
|
||||
jobs:
|
||||
publish-matrix-sdk-crypto-js:
|
||||
name: Publish 🕸 [m]-crypto-js
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.0
|
||||
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ${{ env.PKG_PATH }}
|
||||
run: npm install
|
||||
|
||||
- name: Publish the WebAssembly + JavaScript binding (imply building + testing)
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
package: ${{env.PKG_PATH}}/package.json
|
||||
access: public
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create the Github release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{ env.PKG_PATH }}/pkg/matrix-org-matrix-sdk-crypto-js-*.tgz
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
|
||||
name: Upload code coverage
|
||||
|
||||
on:
|
||||
# This workflow is triggered after every successful execution
|
||||
# of `coverage` workflow.
|
||||
workflow_run:
|
||||
workflows: ["Code Coverage"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Upload coverage report
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: 'Fetch coverage report from artifacts'
|
||||
id: prepare_report
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
// List artifacts of the workflow run that triggered this workflow
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "codecov_report";
|
||||
});
|
||||
|
||||
if (codecovReport.length != 1) {
|
||||
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
|
||||
}
|
||||
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: codecovReport[0].id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
|
||||
|
||||
- id: parse_previous_artifacts
|
||||
run: |
|
||||
unzip codecov_report.zip
|
||||
|
||||
echo "Detected PR is: $(<pr_number.txt)"
|
||||
echo "Detected commit_sha is: $(<commit_sha.txt)"
|
||||
|
||||
# Make the params available as step output
|
||||
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
os-name: 🐧
|
||||
cachekey-id: linux
|
||||
|
||||
- os: macos-15
|
||||
- os: macos-12
|
||||
os-name: 🍏
|
||||
cachekey-id: macos
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
@@ -53,16 +53,16 @@ jobs:
|
||||
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check xtask cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
# use the cache key calculated in the step above. Bit of an awkward
|
||||
# use the cache key calculated in the step above. Bit of an awkard
|
||||
# syntax
|
||||
key: |
|
||||
${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }}
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
- name: Install rust stable toolchain
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ emsdk-*
|
||||
.build
|
||||
.swiftpm
|
||||
/Package.swift
|
||||
# code coverage report
|
||||
cobertura.xml
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
+16
-36
@@ -1,39 +1,19 @@
|
||||
[default]
|
||||
extend-ignore-re = [
|
||||
# base 58 strings with spaces every four chars.
|
||||
# this would also match regular sentence parts with eight or more words of
|
||||
# exactly four characters in row, but that doesn't really happen.
|
||||
"[1-9A-Za-z]{4}( [1-9A-Za-z]{4}){7,}",
|
||||
# some heuristics for base64 strings with no false matches found at the
|
||||
# time of writing.
|
||||
"[A-Za-z0-9+=]{72,}",
|
||||
"([A-Za-z0-9+=]|\\\\\\s\\*){72,}",
|
||||
"[0-9+][A-Za-z0-9+]{30,}[a-z0-9+]",
|
||||
"\\$[A-Z0-9+][A-Za-z0-9+]{6,}[a-z0-9+]",
|
||||
"\\b[a-z0-9+/=][A-Za-z0-9+/=]{7,}[a-z0-9+/=][A-Z]\\b",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
WeeChat = "WeeChat"
|
||||
|
||||
# all of these are valid words, but should never appear in this repo
|
||||
[default.extend-words]
|
||||
sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
ratatui = "ratatui"
|
||||
# base64 false positives
|
||||
Nd = "Nd"
|
||||
Abl = "Abl"
|
||||
Som = "Som"
|
||||
Ba = "Ba"
|
||||
Yur = "Yur" # as found in crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs
|
||||
TYE = "TYE" # as found in testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
|
||||
# Remove this once base64 gets correctly ignored by typos
|
||||
# Or if we're able to ignore certain lines.
|
||||
Fo = "Fo"
|
||||
BA = "BA"
|
||||
UE = "UE"
|
||||
Ure = "Ure"
|
||||
OFO = "OFO"
|
||||
Ot = "Ot"
|
||||
ket = "ket"
|
||||
# This is the thead html tag, remove this once typos is updated in the github
|
||||
# action. 1.3.1 seems to work correctly, while 1.11.0 on the CI seems to get
|
||||
# this wrong
|
||||
thead = "thead"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys.
|
||||
"*.json",
|
||||
# Fuzzy match patterns that can be understood as typos confusingly.
|
||||
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
|
||||
]
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
|
||||
# automatically ignored, we ignore them here.
|
||||
extend-exclude = ["*.json"]
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
The SDK is split into multiple layers:
|
||||
|
||||
```
|
||||
WASM (external crate matrix-rust-sdk-crypto-wasm)
|
||||
/
|
||||
/ uniffi
|
||||
/ /
|
||||
/ bindings (matrix-sdk-ffi)
|
||||
crypto |
|
||||
bindings |
|
||||
| |
|
||||
| UI (matrix-sdk-ui)
|
||||
| \
|
||||
| \
|
||||
| main (matrix-sdk)
|
||||
| / /
|
||||
crypto /
|
||||
\ /
|
||||
store (matrix-sdk-base, + all the store impls)
|
||||
|
|
||||
common (matrix-sdk-common)
|
||||
```
|
||||
|
||||
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
|
||||
`MemoryStore` which is defined in `matrix-sdk-base`.
|
||||
|
||||
## `crates/matrix-sdk`
|
||||
|
||||
This is the main crate, and one that is expected to be used by most consumers. Notable data types
|
||||
include:
|
||||
|
||||
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
|
||||
sync, etc.
|
||||
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
|
||||
allows running queries that are room-specific, notably sending events.
|
||||
|
||||
## `crates/matrix-sdk-base`
|
||||
|
||||
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
|
||||
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
|
||||
storage backends, as well as dummy in-memory implementations of these traits.
|
||||
|
||||
## `crates/matrix-sdk-common`
|
||||
|
||||
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
|
||||
crates (the only crate it's using is test helpers).
|
||||
|
||||
## `crates/matrix-sdk-crypto`
|
||||
|
||||
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
|
||||
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
|
||||
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
|
||||
|
||||
## `crates/matrix-sdk-indexeddb`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
indexeddb backend (for use in Web browsers, via WebAssembly).
|
||||
|
||||
## `crates/matrix-sdk-qrcode`
|
||||
|
||||
Implementation of QR codes for interactive verifications, used in the crypto crate.
|
||||
|
||||
## `crates/matrix-sdk-sqlite`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
SQLite backend.
|
||||
|
||||
## `crates/matrix-sdk-store-encryption`
|
||||
|
||||
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
|
||||
implement encryption at rest can use those primitives.
|
||||
|
||||
## `crates/matrix-sdk-ui`
|
||||
|
||||
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
|
||||
|
||||
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
everything related to crypto and E2EE for the current `Client`.
|
||||
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
retrieving the list of current rooms, and exposing its entries.
|
||||
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
|
||||
down.
|
||||
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
|
||||
(aggregations) into single timeline items.
|
||||
|
||||
## `bindings/matrix-sdk-crypto-ffi/`
|
||||
|
||||
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
|
||||
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
|
||||
Element apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi/`
|
||||
|
||||
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
|
||||
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
|
||||
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi-macros/`
|
||||
|
||||
Macros used in `bindings/matrix-sdk-ffi`.
|
||||
|
||||
## `testing/matrix-sdk-test/`
|
||||
|
||||
Common test helpers, used by all the other crates.
|
||||
|
||||
## `testing/matrix-sdk-test-macros/`
|
||||
|
||||
Implementation of the `#[async_test]` test macro.
|
||||
|
||||
## `testing/matrix-sdk-integration-testing/`
|
||||
|
||||
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
|
||||
setup is provided to ease running the tests, and it is compatible for running with Podman too.
|
||||
|
||||
# Inspiration
|
||||
|
||||
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
|
||||
+11
-240
@@ -29,240 +29,16 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
|
||||
synapse for testing purposes.
|
||||
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
You can add/review snapshot tests using [insta.rs](https://insta.rs)
|
||||
|
||||
Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it.
|
||||
Any code change that breaks serialisation will then break a test, the author will then have to decide
|
||||
how to handle migration and test it if needed.
|
||||
|
||||
|
||||
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
|
||||
|
||||
Unix:
|
||||
```
|
||||
curl -LsSf https://insta.rs/install.sh | sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
```
|
||||
powershell -c "irm https://insta.rs/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Usual flow is to first run the test, then review them.
|
||||
```
|
||||
cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
### Intermittent failure policy
|
||||
|
||||
While we strive to add test coverage for as many features as we can, it sometimes happens that the
|
||||
tests will be intermittently failing in CI (such tests are sometimes called "flaky"). This can be
|
||||
caused by race conditions of all sorts, either in the test code itself, but sometimes in the
|
||||
underlying feature being tested too, and as such, it requires some investigation, usually from the
|
||||
original author of the test.
|
||||
|
||||
Whenever such an intermittent failure happens, we try to open an issue to track the failures,
|
||||
adding the
|
||||
[`intermittent-failure`](https://github.com/matrix-org/matrix-rust-sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Aintermittent-failure)
|
||||
label to it, and commenting with links to CI runs where the failure happened.
|
||||
|
||||
If a test has been intermittently failing for **two weeks** or more, and no one is actively working
|
||||
on fixing it, then we might decide to mark the test as `ignored` until it is fixed, to not cause
|
||||
unrelated failures in other contributors' pull requests and pushes.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
each commit should have a *good commit message*.
|
||||
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR,
|
||||
following the same guidelines of a good commit message, including the
|
||||
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
|
||||
be a good PR title.
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`,
|
||||
that is, just the branch name.)
|
||||
|
||||
# Writing changelog entries
|
||||
|
||||
Our goal is to maintain clear, concise, and informative changelogs that
|
||||
accurately document changes in the project. Changelog entries should be written
|
||||
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
|
||||
|
||||
Be sure to include a link to the pull request for additional context. A
|
||||
well-written changelog entry should be understandable even to those who may not
|
||||
be deeply familiar with the project. Provide enough context to ensure clarity
|
||||
and ease of understanding.
|
||||
|
||||
A couple of examples of bad changelog entry would look like:
|
||||
|
||||
```markdown
|
||||
- Fixed a panic.
|
||||
```
|
||||
|
||||
```markdown
|
||||
- Added the Bar function to Foo.
|
||||
```
|
||||
|
||||
A good example of a changelog entry could look like the following:
|
||||
|
||||
```markdown
|
||||
- Use the inviter's server name and the server name from the room alias as
|
||||
fallback values for the via parameter when requesting the room summary from
|
||||
the homeserver. This ensures requests succeed even when the room being
|
||||
previewed is hosted on a federated server.
|
||||
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
|
||||
```
|
||||
|
||||
For security-related changelog entries, please include the following additional
|
||||
details alongside the pull request number:
|
||||
|
||||
* Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
|
||||
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
|
||||
|
||||
```markdown
|
||||
- Use a constant-time Base64 encoder for secret key material to mitigate
|
||||
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
|
||||
```
|
||||
|
||||
## Commit message format
|
||||
|
||||
Commit messages should be formatted as Conventional Commits. In addition, some
|
||||
git trailers are supported and have special meaning (see below).
|
||||
|
||||
### Conventional commits
|
||||
|
||||
Conventional Commits are structured as follows:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
```
|
||||
|
||||
The type of changes which will be included in changelogs is one of the following:
|
||||
|
||||
* `feat`: A new feature
|
||||
* `fix`: A bug fix
|
||||
* `doc`: Documentation changes
|
||||
* `refactor`: Code refactoring
|
||||
* `perf`: Performance improvements
|
||||
* `ci`: Changes to CI configuration files and scripts
|
||||
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata, which should also be reflected in the corresponding
|
||||
changelog entry.
|
||||
|
||||
The metadata must be included in the following git-trailers:
|
||||
|
||||
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
|
||||
* `CVE`: The CVE that was assigned to this issue.
|
||||
* `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Please include all of the fields that are available.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
fix(crypto): Use a constant-time Base64 encoder for secret key material
|
||||
|
||||
This patch fixes a security issue around a side-channel vulnerability[1]
|
||||
when decoding secret key material using Base64.
|
||||
|
||||
In some circumstances an attacker can obtain information about secret
|
||||
secret key material via a controlled-channel and side-channel attack.
|
||||
|
||||
This patch avoids the side-channel by switching to the base64ct crate
|
||||
for the encoding, and more importantly, the decoding of secret key
|
||||
material.
|
||||
|
||||
Security-Impact: Low
|
||||
CVE: CVE-2024-40640
|
||||
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
To streamline the review process and make it easier for maintainers to review
|
||||
your contributions, follow these basic rules:
|
||||
|
||||
1. Do not force push after a review has started. This helps maintainers track
|
||||
incremental changes without confusion and makes it easier to follow the
|
||||
evolution of the code.
|
||||
|
||||
2. Do not mix moves and refactoring with functional changes. Keep these in
|
||||
separate commits for clarity. This ensures that the purpose of each commit is
|
||||
clear and easy to review.
|
||||
|
||||
3. Each commit must compile. If commits don’t compile, git bisect becomes
|
||||
unusable, which hampers the debugging process and makes it harder to identify
|
||||
the source of issues.
|
||||
|
||||
4. Commits should only introduce test failures if they are proving that a bug
|
||||
exists. New features should never introduce test failures. Test failures
|
||||
should only be used to demonstrate existing bugs, not as part of adding new
|
||||
functionality.
|
||||
|
||||
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
|
||||
delays. Create small, focused commits that address a single topic. Use a
|
||||
combination of [git add] -p or git checkout -p to split changes into logical
|
||||
units. This makes your work easier to review and reduces the chance of
|
||||
introducing unrelated changes.
|
||||
|
||||
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
|
||||
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
|
||||
|
||||
### Addressing review comments using fixup commits
|
||||
|
||||
So you posted a PR and the maintainers aren't quite happy with it. Here are some
|
||||
guidelines to make the maintainers life easier and increase the chances that
|
||||
your PR will be reviewed swiftly.
|
||||
|
||||
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
|
||||
commits. These commits mark your changes as corrections of specific previous
|
||||
commits in the PR.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
git commit --fixup=<commit-hash>
|
||||
```
|
||||
|
||||
This command creates a new commit that refers to an existing one, making it
|
||||
easier to rebase and squash later while showing reviewers the history of fixes.
|
||||
For extra points, link to the fixup commit in the thread where the change was
|
||||
requested.
|
||||
|
||||
2. After all requested changes were addressed, feel free to re-request a review.
|
||||
People might not notice that all changes were addressed.
|
||||
|
||||
3. Once the PR has been approved, rebase your PR to squash all the fixup
|
||||
commits, the [autosquash] option can help with this.
|
||||
|
||||
```bash
|
||||
git rebase main --interactive --autosquash
|
||||
```
|
||||
|
||||
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
|
||||
[autosquash]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the [Linux Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
|
||||
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO ([Developer Certificate of Origin](http://developercertificate.org/)).
|
||||
This is a simple declaration that you wrote the contribution or otherwise have the right
|
||||
to contribute it to Matrix:
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
@@ -309,6 +85,11 @@ include the line in your commit or pull request comment:
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
@@ -319,13 +100,3 @@ on Git 2.17+ you can mass signoff using rebase:
|
||||
```
|
||||
git rebase --signoff origin/main
|
||||
```
|
||||
|
||||
## Tips for working on the `matrix-rust-sdk` with specific IDEs
|
||||
|
||||
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
|
||||
with all features enabled, causing an error in `matrix-sdk` ("only one of the
|
||||
features 'native-tls' or 'rustls-tls' can be enabled"). To work around this,
|
||||
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
|
||||
`native-tls` or `rustls-tls` feature definitions:
|
||||
|
||||

|
||||
|
||||
Generated
+2527
-3088
File diff suppressed because it is too large
Load Diff
+39
-153
@@ -2,152 +2,54 @@
|
||||
members = [
|
||||
"benchmarks",
|
||||
"bindings/matrix-sdk-crypto-ffi",
|
||||
"bindings/matrix-sdk-crypto-js",
|
||||
"bindings/matrix-sdk-crypto-nodejs",
|
||||
"bindings/matrix-sdk-ffi",
|
||||
"crates/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"testing/*",
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
exclude = ["testing/data"]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
# xtask, labs, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.65"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
as_variant = "1.3.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
anyhow = "1.0.68"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-compat = "0.2.4"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.8.0"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
getrandom = { version = "0.2.15", default-features = false }
|
||||
gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.2.0"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.5", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
] }
|
||||
ruma-common = "0.15.4"
|
||||
sentry = "0.36.0"
|
||||
sentry-tracing = "0.36.0"
|
||||
serde = { version = "1.0.217", features = ["rc"] }
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.18"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.4"
|
||||
uuid = "1.12.1"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
base64 = "0.21.0"
|
||||
byteorder = "1.4.3"
|
||||
ctor = "0.2.0"
|
||||
dashmap = "5.2.0"
|
||||
eyeball = "0.6.0"
|
||||
eyeball-im = "0.2.0"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.6"
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "0143bd9b9f5dcfcaa835afb76f342c12f014f945", features = ["client-api-c", "compat-user-id"] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "0143bd9b9f5dcfcaa835afb76f342c12f014f945" }
|
||||
once_cell = "1.16.0"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.24", default-features = false, features = ["sync"] }
|
||||
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.30"
|
||||
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "aa91307b6ac27aae6d5c7ad971b762df952d2745" }
|
||||
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "aa91307b6ac27aae6d5c7ad971b762df952d2745" }
|
||||
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" }
|
||||
zeroize = "1.3.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.13.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.13.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.13.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.13.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.13.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.13.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.13.0", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_let_else = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
redundant_clone = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unused_async = "warn"
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
@@ -160,9 +62,6 @@ debug = 0
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
@@ -172,22 +71,9 @@ debug = 2
|
||||
# Custom profile for use in (debug) builds of the binding crates, use
|
||||
# `--profile reldbg` to select
|
||||
[profile.reldbg]
|
||||
inherits = "dbg"
|
||||
inherits = "dev"
|
||||
incremental = false
|
||||
|
||||
# Compile all non-workspace crate in the dependency tree with optimizations
|
||||
[profile.reldbg.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
# LTO is too slow to compile.
|
||||
lto = false
|
||||
# Get symbol names for profiling purposes.
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
|
||||
@@ -1,90 +1,47 @@
|
||||
<h1 align="center">Matrix Rust SDK</h1>
|
||||

|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
[](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
|
||||
[](https://docs.rs/matrix-sdk)
|
||||
|
||||
<div align="center">
|
||||
<em>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</em>
|
||||
<br />
|
||||
<img src="contrib/logo.svg">
|
||||
<hr />
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
|
||||
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
|
||||
<a href="https://crates.io/crates/matrix-sdk/">
|
||||
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
|
||||
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
|
||||
<br />
|
||||
<a href="https://docs.rs/matrix-sdk/">
|
||||
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
|
||||
</div>
|
||||
# matrix-rust-sdk
|
||||
|
||||
<div align="center">
|
||||
**matrix-rust-sdk** is an implementation of a [Matrix][] client-server library in [Rust][].
|
||||
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build [Matrix] clients in [Rust].
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Development of the SDK is proudly sponsored and maintained by [Element](https://element.io). Element uses the SDK in their next-generation mobile apps Element X on [iOS](https://github.com/element-hq/element-x-ios) and [Android](https://github.com/element-hq/element-x-android) and has plans to introduce it to the web and desktop clients as well.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
</div>
|
||||
|
||||
## Purpose
|
||||
|
||||
The SDK takes care of the low-level details like encryption,
|
||||
syncing, and room state, so you can focus on your app's logic and UI. Whether
|
||||
you're writing a small bot, a desktop client, or something in between, the SDK
|
||||
is designed to be flexible, async-friendly, and ready to use out of the box.
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
## Project structure
|
||||
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each
|
||||
other. The following crates are expected to be usable as direct dependencies:
|
||||
The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
|
||||
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) – A high-level client library that makes it easy to build
|
||||
full-featured UI clients with minimal setup. Check out our reference client,
|
||||
[multiverse](https://github.com/matrix-org/matrix-rust-sdk/tree/main/labs/multiverse), for an example.
|
||||
- [matrix-sdk](https://docs.rs/matrix-sdk/latest/matrix_sdk/) – A mid-level client library, ideal for building bots, custom
|
||||
clients, or higher-level abstractions. You can find example usage in the
|
||||
[examples directory](https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples).
|
||||
- [matrix-sdk-crypto](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/) – A standalone encryption state machine with no network I/O,
|
||||
providing end-to-end encryption support for Matrix clients and libraries.
|
||||
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
|
||||
for a step-by-step introduction.
|
||||
- **matrix-sdk** - High level client library, with batteries included, you're most likely
|
||||
interested in this.
|
||||
- **matrix-sdk-base** - No (network) IO client state machine that can be used to embed a
|
||||
Matrix client in your project or build a full fledged network enabled client
|
||||
lib on top of it.
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
|
||||
All other crates are effectively internal-only and only structured as crates
|
||||
for organizational purposes and to improve compilation times. Direct usage of them is discouraged.
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.65`.
|
||||
|
||||
## Status
|
||||
|
||||
The library is considered production ready and backs multiple client
|
||||
implementations such as Element X
|
||||
[[1]](https://github.com/element-hq/element-x-ios)
|
||||
[[2]](https://github.com/element-hq/element-x-android),
|
||||
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
|
||||
confident to build upon it.
|
||||
The library is in an alpha state, things that are implemented generally work but
|
||||
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
|
||||
|
||||
The higher-level crates of the Matrix Rust SDK can be embedded in other
|
||||
environments such as Swift, Kotlin, JavaScript, and Node.js. Check out the
|
||||
[bindings/](./bindings/) directory to learn more about how to integrate the SDK
|
||||
into your language of choice.
|
||||
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)
|
||||
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Releasing and publishing the SDK
|
||||
|
||||
While the release process can be handled manually, `cargo-release` has been
|
||||
configured to make it more convenient.
|
||||
|
||||
By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes
|
||||
that no pull request is required to cut a release. However, since the SDK
|
||||
repo is set up so that each push requires a pull request, we need to slightly
|
||||
deviate from the default workflow. A `cargo-xtask` has been created to make the
|
||||
process as smooth as possible.
|
||||
|
||||
The procedure is as follows:
|
||||
|
||||
1. Switch to a release branch:
|
||||
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
|
||||
2. Prepare the release. This will update the `README.md`, set the versions in
|
||||
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
|
||||
satisfied, push the branch and open a PR.
|
||||
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
|
||||
4. Pass the review and merge the branch as you would with any other branch.
|
||||
|
||||
5. Create tags for your new release, publish the release on crates.io and push
|
||||
the tags:
|
||||
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
@@ -0,0 +1,121 @@
|
||||
# Upgrades 0.5 ➜ 0.6
|
||||
|
||||
This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel].
|
||||
|
||||
## Minimum Supported Rust Version Update: `1.60`
|
||||
|
||||
We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it:
|
||||
|
||||
> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60
|
||||
|
||||
## Dependencies
|
||||
|
||||
Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now.
|
||||
|
||||
## Repo Structure Updates
|
||||
|
||||
If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects.
|
||||
|
||||
|
||||
## Architecture Changes / API overall
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto:
|
||||
- the [login configuration][login builder] and [login with sso][ssologin builder],
|
||||
- [`SledStore` configuratiion][sled-store builder]
|
||||
- [`Indexeddb` configuration][indexeddb builder]
|
||||
|
||||
Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases.
|
||||
|
||||
### Splitting of concerns: Media
|
||||
|
||||
In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now.
|
||||
|
||||
The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now.
|
||||
|
||||
### Event Handling & sync updaes
|
||||
|
||||
If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release.
|
||||
|
||||
Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward.
|
||||
|
||||
### Refresh Tokens
|
||||
|
||||
This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default.
|
||||
|
||||
You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`.
|
||||
|
||||
### Further changes
|
||||
|
||||
- [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received.
|
||||
- `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it.
|
||||
- `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix.
|
||||
- `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is onw called `is_verified()`.
|
||||
- The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types>
|
||||
- `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config]
|
||||
- Indexeddb's `SerializationError` is now `IndexedDBStoreError`
|
||||
- Javascript specific features are now behind the `js` feature-gate
|
||||
- The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate
|
||||
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
You find yourself focussed with any of these, here are the steps to follow to upgrade your code accordingly:
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead
|
||||
|
||||
As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release.
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username)
|
||||
|
||||
We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release.
|
||||
|
||||
### expected slice `[u8]`, found struct ...
|
||||
|
||||
We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?`
|
||||
|
||||
### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope
|
||||
|
||||
Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion:
|
||||
```
|
||||
|
|
||||
69 | device.verified()
|
||||
| ^^^^^^^^ help: there is an associated function with a similar name: `is_verified`
|
||||
```
|
||||
|
||||
### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent`
|
||||
|
||||
Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it.
|
||||
|
||||
### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
||||
You are seeing something along the lines of:
|
||||
```
|
||||
19 | if room_member.state_key != client.user_id().await.unwrap() {
|
||||
| ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
|
||||
= help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
= note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited
|
||||
= note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
help: remove the `.await`
|
||||
|
|
||||
19 - if room_member.state_key != client.user_id().await.unwrap() {
|
||||
19 + if room_member.state_key != client.user_id().unwrap() {
|
||||
```
|
||||
|
||||
You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer.
|
||||
|
||||
|
||||
[matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
|
||||
[builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
|
||||
[login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html
|
||||
[ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html
|
||||
[sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html
|
||||
[indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html
|
||||
[media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html
|
||||
[sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback
|
||||
[session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html
|
||||
[refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892
|
||||
[store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html
|
||||
[message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html
|
||||
+9
-33
@@ -3,47 +3,23 @@ name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version.workspace = true
|
||||
rust-version = { workspace = true }
|
||||
version = "1.0.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-ui.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
criterion = { version = "0.4.0", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.6.0"}
|
||||
matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.2.0", default-features = false, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { path = "../testing/matrix-sdk-test", version = "0.6.0"}
|
||||
ruma = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
wiremock.workspace = true
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
pprof = { version = "0.11.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "linked_chunk"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
+3
-10
@@ -8,7 +8,7 @@ can be found [here](https://bheisler.github.io/criterion.rs/book/criterion_rs.ht
|
||||
|
||||
## Running the benchmarks
|
||||
|
||||
The benchmark can be run by using the `bench` command of `cargo`:
|
||||
The benchmark can be simply run by using the `bench` command of `cargo`:
|
||||
|
||||
```bash
|
||||
$ cargo bench
|
||||
@@ -16,13 +16,6 @@ $ cargo bench
|
||||
|
||||
This will work from the workspace directory of the rust-sdk.
|
||||
|
||||
To lower compile times, you might be interested in using the `profiling` profile, that's optimized
|
||||
for a fair tradeoff between compile times and runtime performance:
|
||||
|
||||
```bash
|
||||
$ cargo bench --profile profiling
|
||||
```
|
||||
|
||||
If you want to pass options to the benchmark [you'll need to specify the name of
|
||||
the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options):
|
||||
|
||||
@@ -30,7 +23,7 @@ the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench
|
||||
$ cargo bench --bench crypto_bench -- # Your options go here
|
||||
```
|
||||
|
||||
If you want to run only a specific benchmark, pass the name of the
|
||||
If you want to run only a specific benchmark, simply pass the name of the
|
||||
benchmark as an argument:
|
||||
|
||||
```bash
|
||||
@@ -72,7 +65,7 @@ permisive value is `-1`:
|
||||
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
```
|
||||
|
||||
To generate flame graphs feature, enable the profiling mode using the
|
||||
To generate flame graphs feature simply enable the profiling mode using the
|
||||
`--profile-time` command line flag:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::*;
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
use matrix_sdk_test::ruma_response_from_json;
|
||||
use matrix_sdk_sled::SledCryptoStore;
|
||||
use matrix_sdk_test::response_from_file;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
api::{
|
||||
client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
@@ -25,19 +28,25 @@ fn alice_device_id() -> &'static DeviceId {
|
||||
fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
ruma_response_from_json(&data)
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_claim.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
ruma_response_from_json(&data)
|
||||
let data = response_from_file(&data);
|
||||
claim_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
fn huge_keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query_2000_members.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
ruma_response_from_json(&data)
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys query response")
|
||||
}
|
||||
|
||||
pub fn keys_query(c: &mut Criterion) {
|
||||
@@ -56,31 +65,21 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
let name = format!("{count} device and cross signing keys");
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
// Benchmark sqlite store.
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
let store = Arc::new(runtime.block_on(SledCryptoStore::open(dir.path(), None)).unwrap());
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(machine);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
@@ -109,24 +108,21 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine);
|
||||
})
|
||||
runtime.block_on(machine.mark_request_as_sent(txn_id, response)).unwrap()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store =
|
||||
Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
Arc::new(runtime.block_on(SledCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
@@ -134,10 +130,7 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine)
|
||||
})
|
||||
runtime.block_on(machine.mark_request_as_sent(txn_id, response)).unwrap()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
@@ -167,8 +160,6 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
let name = format!("{count} devices");
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
@@ -186,22 +177,18 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// Benchmark sqlite store.
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
let store = Arc::new(runtime.block_on(SledCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("sled store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -218,15 +205,10 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(machine);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
@@ -247,36 +229,26 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
b.to_async(&runtime).iter_with_large_drop(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
// Benchmark sqlite store.
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
let store = Arc::new(runtime.block_on(SledCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("sled store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(machine);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
|
||||
SqliteEventCacheStore,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
|
||||
Event, Gap,
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
|
||||
use ruma::{room_id, EventId};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Operation {
|
||||
PushItemsBack(Vec<Event>),
|
||||
PushGapBack(Gap),
|
||||
}
|
||||
|
||||
fn writing(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("writing");
|
||||
group.sample_size(10).measurement_time(Duration::from_secs(30));
|
||||
|
||||
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Option<Arc<DynEventCacheStore>>); 3] = [
|
||||
("none", None),
|
||||
("memory store", Some(MemoryStore::default().into_event_cache_store())),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
Some(
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Create the operations we want to bench.
|
||||
let mut operations = Vec::new();
|
||||
|
||||
{
|
||||
let mut events = (0..number_of_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let mut gap_nth = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
{
|
||||
let events_to_push_back = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_to_push_back.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
operations.push(Operation::PushItemsBack(events_to_push_back));
|
||||
}
|
||||
|
||||
{
|
||||
operations.push(Operation::PushGapBack(Gap {
|
||||
prev_token: format!("gap{gap_nth}"),
|
||||
}));
|
||||
gap_nth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(number_of_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(store_name, number_of_events),
|
||||
&operations,
|
||||
|bencher, operations| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
|| operations.clone(),
|
||||
|operations| async {
|
||||
// The routine to bench!
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::PushItemsBack(events) => linked_chunk.push_items_back(events),
|
||||
Operation::PushGapBack(gap) => linked_chunk.push_gap_back(gap),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(store) = &store {
|
||||
let updates = linked_chunk.updates().unwrap().take();
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, updates).await.unwrap();
|
||||
// Empty the store.
|
||||
store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await.unwrap();
|
||||
}
|
||||
|
||||
},
|
||||
BatchSize::SmallInput
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn reading(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("reading");
|
||||
group.sample_size(10);
|
||||
|
||||
for num_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Arc<DynEventCacheStore>); 2] = [
|
||||
("memory store", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Store some events and gap chunks in the store.
|
||||
{
|
||||
let mut events = (0..num_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let mut lc =
|
||||
LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
let mut num_gaps = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
if events_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
// Now persist the updates to recreate this full linked chunk.
|
||||
let updates = lc.updates().unwrap().take();
|
||||
runtime
|
||||
.block_on(store.handle_linked_chunk_updates(linked_chunk_id, updates))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = criterion();
|
||||
targets = writing, reading,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,230 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{i}:matrix.org")).unwrap();
|
||||
let event = f
|
||||
.member(&user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.avatar_url(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
|
||||
.display_name("Alice Margatroid")
|
||||
.reason("Looking for support")
|
||||
.into_raw();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
for member_event in member_events.iter() {
|
||||
let event = member_event.clone().cast();
|
||||
changes.add_state_event(&room_id, event.deserialize().unwrap(), event);
|
||||
}
|
||||
|
||||
// Sqlite
|
||||
let sqlite_dir = tempfile::tempdir().unwrap();
|
||||
let sqlite_store = runtime.block_on(SqliteStateStore::open(sqlite_dir.path(), None)).unwrap();
|
||||
runtime
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
))
|
||||
.expect("Could not set session meta");
|
||||
|
||||
base_client.get_or_create_room(&room_id, RoomState::Joined);
|
||||
|
||||
let request = get_member_events::v3::Request::new(room_id.clone());
|
||||
let response = get_member_events::v3::Response::new(member_events);
|
||||
|
||||
let count = MEMBERS_IN_ROOM;
|
||||
let name = format!("{count} members");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(50);
|
||||
|
||||
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(base_client);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
const PINNED_EVENTS_COUNT: usize = 100;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id).sender(&sender_id);
|
||||
|
||||
let mut joined_room_builder =
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
|
||||
|
||||
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
|
||||
.map(|i| EventId::parse(format!("${i}")).expect("Invalid event id"))
|
||||
.collect();
|
||||
joined_room_builder = joined_room_builder.add_state_event(StateTestEvent::Custom(json!(
|
||||
{
|
||||
"content": {
|
||||
"pinned": pinned_event_ids
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.pinned_events",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
}
|
||||
)));
|
||||
|
||||
let (server, client, room) = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
let room = server.sync_room(&client, joined_room_builder).await;
|
||||
|
||||
server
|
||||
.mock_room_event()
|
||||
.respond_with(move |r: &Request| {
|
||||
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
|
||||
let event_id_str = segments[6];
|
||||
let event_id = EventId::parse(event_id_str).expect("Invalid event id in response");
|
||||
let event = f
|
||||
.text_msg(format!("Message {event_id_str}"))
|
||||
.event_id(&event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
ResponseTemplate::new(200)
|
||||
.set_delay(Duration::from_millis(50))
|
||||
.set_body_json(event.json())
|
||||
})
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client, room)
|
||||
});
|
||||
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
let count = PINNED_EVENTS_COUNT;
|
||||
let name = format!("{count} pinned events");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
// Reset cache so it always loads the events from the mocked endpoint
|
||||
client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents {
|
||||
max_events_to_load: 100,
|
||||
max_concurrent_requests: 10,
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), PINNED_EVENTS_COUNT + 1);
|
||||
timeline.clear().await;
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(server);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
@@ -1,130 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
|
||||
SessionTokens, StateChanges,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use ruma::{device_id, user_id, RoomId};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
/// Number of joined rooms in the benchmark.
|
||||
const NUM_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
/// Number of stripped rooms in the benchmark.
|
||||
const NUM_STRIPPED_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
pub fn restore_session(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
|
||||
for i in 0..NUM_JOINED_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap().to_owned();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
}
|
||||
|
||||
for i in 0..NUM_STRIPPED_JOINED_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!strippedroom{i}:example.com")).unwrap().to_owned();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Invited));
|
||||
}
|
||||
|
||||
let session = MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
};
|
||||
|
||||
// Start the benchmark.
|
||||
|
||||
let mut group = c.benchmark_group("Client reload");
|
||||
group.throughput(Throughput::Elements(100));
|
||||
|
||||
const NAME: &str = "restore a session";
|
||||
|
||||
// Memory
|
||||
let mem_store = Arc::new(MemoryStore::new());
|
||||
runtime.block_on(mem_store.save_changes(&changes)).expect("initial filling of mem failed");
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", NAME), &mem_store, |b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
client.restore_session(session.clone()).await.expect("couldn't restore session");
|
||||
})
|
||||
});
|
||||
|
||||
for encryption_password in [None, Some("hunter2")] {
|
||||
let encrypted_suffix = if encryption_password.is_some() { "encrypted" } else { "clear" };
|
||||
|
||||
// Sqlite
|
||||
let sqlite_dir = tempfile::tempdir().unwrap();
|
||||
let sqlite_store = runtime
|
||||
.block_on(SqliteStateStore::open(sqlite_dir.path(), encryption_password))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(format!("sqlite store {encrypted_suffix}"), NAME),
|
||||
&sqlite_store,
|
||||
|b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
client
|
||||
.restore_session(session.clone())
|
||||
.await
|
||||
.expect("couldn't restore session");
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(sqlite_store);
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion();
|
||||
targets = restore_session
|
||||
}
|
||||
criterion_main!(benches);
|
||||
@@ -1,140 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a timeline (with read receipt
|
||||
/// support), when there are many initial events at rest in the event cache.
|
||||
///
|
||||
/// `NUM_EVENTS` is the number of events that will be stored initially in the
|
||||
/// event cache. It will be a mix of messages, reactions, edits and redactions,
|
||||
/// so there are some aggregations to take into account by the timeline as well.
|
||||
pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
const NUM_EVENTS: usize = 10000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
let other_sender_id = owned_user_id!("@other_sender:example.com");
|
||||
let another_sender_id = owned_user_id!("@another_sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
|
||||
let mut events = Vec::new();
|
||||
for i in 0..NUM_EVENTS {
|
||||
let sender = match i % 3 {
|
||||
0 => &sender_id,
|
||||
1 => &other_sender_id,
|
||||
2 => &another_sender_id,
|
||||
_ => unreachable!("math genius over here"),
|
||||
};
|
||||
|
||||
let event_id = EventId::parse(format!("$event{i}")).unwrap();
|
||||
|
||||
let j = i % 10;
|
||||
if j < 6 {
|
||||
// Messages.
|
||||
events.push(
|
||||
f.text_msg(format!("Message {i}"))
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j < 8 {
|
||||
// Reactions.
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
|
||||
events.push(
|
||||
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
|
||||
);
|
||||
} else if j == 8 {
|
||||
// Edit.
|
||||
// Note: (i-3)%3 is the same as i%3 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
|
||||
events.push(
|
||||
f.text_msg(format!("* Message {}v2", i - 3))
|
||||
.edit(
|
||||
&prev_event,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!(
|
||||
"Message {}v2",
|
||||
i - 3
|
||||
)),
|
||||
)
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j == 9 {
|
||||
// Redaction.
|
||||
// Note: (i-6)%3 is the same as i%6 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
|
||||
events
|
||||
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
|
||||
}
|
||||
}
|
||||
|
||||
let builder = JoinedRoomBuilder::new(&room_id)
|
||||
.add_state_event(StateTestEvent::Encryption)
|
||||
.add_timeline_bulk(events);
|
||||
|
||||
let room = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
let room = server.sync_room(&client, builder).await;
|
||||
drop(server);
|
||||
|
||||
room
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(NUM_EVENTS as _));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), 20);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
targets = create_timeline_with_initial_events
|
||||
}
|
||||
criterion_main!(room);
|
||||
+7
-12
@@ -7,24 +7,19 @@ maintained by the owners of the Matrix Rust SDK project.
|
||||
[`matrix-sdk`] crate via [`matrix-sdk-ffi`],
|
||||
* [`matrix-sdk-crypto-ffi`], UniFFI (Kotlin, Swift, Python, Ruby) bindings of the [`matrix-sdk-crypto`]
|
||||
crate,
|
||||
* [`matrix-sdk-ffi`], UniFFI bindings of the [`matrix-sdk`] crate.
|
||||
|
||||
There are also external bindings in other repositories:
|
||||
|
||||
* [`matrix-sdk-crypto-wasm`], JavaScript / WebAssembly bindings of the
|
||||
* [`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-crypto`] crate,
|
||||
* [`matrix-sdk-ffi`], UniFFI bindings of the [`matrix-sdk`] crate.
|
||||
|
||||
[`apple`]: ./apple
|
||||
[`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi
|
||||
[`matrix-sdk-crypto-js`]: ./matrix-sdk-crypto-js
|
||||
[`matrix-sdk-crypto-nodejs`]: ./matrix-sdk-crypto-nodejs
|
||||
[`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto
|
||||
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
|
||||
[`matrix-sdk`]: ../crates/matrix-sdk
|
||||
|
||||
[`matrix-sdk-crypto-wasm`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm
|
||||
[`matrix-sdk-crypto-nodejs`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-nodejs
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute read this [guide](./CONTRIBUTING.md).
|
||||
# Contributing
|
||||
To contribute read this [guide](./CONTRIBUTING.md).
|
||||
@@ -13,7 +13,6 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.library(name: "MatrixRustSDK",
|
||||
type: .dynamic,
|
||||
targets: ["MatrixRustSDK"]),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.4.1"
|
||||
s.version = "0.0.1"
|
||||
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 = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.4.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.version = "0.0.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
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 = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
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"
|
||||
|
||||
@@ -2,9 +2,9 @@ import XCTest
|
||||
@testable import MatrixRustSDK
|
||||
|
||||
final class ClientTests: XCTestCase {
|
||||
func testBuildingWithHomeserverURL() async {
|
||||
func testBuildingWithHomeserverURL() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.build()
|
||||
} catch {
|
||||
@@ -12,9 +12,9 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithHomeserverURLAndUserAgent() async {
|
||||
func testBuildingWithHomeserverURLAndUserAgent() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.userAgent(userAgent: "golden-eye/007")
|
||||
.build()
|
||||
@@ -23,14 +23,24 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() async {
|
||||
func testBuildingWithUsername() {
|
||||
do {
|
||||
_ = try await ClientBuilder()
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:matrix.org")
|
||||
.build()
|
||||
} catch {
|
||||
XCTFail("The client should build successfully when given a username.")
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:invalid")
|
||||
.build()
|
||||
|
||||
XCTFail("The client should not build when given an invalid username.")
|
||||
} catch ClientBuildError.ServerUnreachable(let message) {
|
||||
} catch ClientError.Generic(let message) {
|
||||
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
|
||||
} catch {
|
||||
XCTFail("Not expecting any other kind of exception")
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eEu
|
||||
|
||||
helpFunction() {
|
||||
echo ""
|
||||
echo "Usage: $0 -only_ios"
|
||||
echo -e "\t-i Option to build only for iOS. Default will build for all targets."
|
||||
exit 1
|
||||
}
|
||||
|
||||
only_ios='false'
|
||||
|
||||
while getopts ':i' 'opt'; do
|
||||
case ${opt} in
|
||||
'i') only_ios='true' ;;
|
||||
?) helpFunction ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Path to the repo root
|
||||
@@ -38,63 +22,52 @@ TARGET_CRATE=matrix-sdk-crypto-ffi
|
||||
# Required by olm-sys crate
|
||||
export IOS_SDK_PATH=`xcrun --show-sdk-path --sdk iphoneos`
|
||||
|
||||
if ${only_ios}; then
|
||||
# iOS
|
||||
echo -e "Building only for iOS"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
else
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
fi
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
|
||||
echo -e "\nCreating XCFramework"
|
||||
# Lipo together the libraries for the same platform
|
||||
|
||||
if ! ${only_ios}; then
|
||||
echo "Lipo together the libraries for the same platform"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
fi
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# Generate uniffi files
|
||||
cd ../matrix-sdk-crypto-ffi && cargo run --bin matrix_sdk_crypto_ffi generate \
|
||||
cargo uniffi-bindgen generate \
|
||||
--language swift \
|
||||
--library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--out-dir ${GENERATED_DIR}
|
||||
--lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \
|
||||
--out-dir ${GENERATED_DIR} \
|
||||
"${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl"
|
||||
|
||||
# Move headers to the right place
|
||||
HEADERS_DIR=${GENERATED_DIR}/headers
|
||||
mkdir -p ${HEADERS_DIR}
|
||||
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
|
||||
|
||||
# Rename and merge the modulemap files into a single file to the right place
|
||||
for f in ${GENERATED_DIR}/*.modulemap
|
||||
do
|
||||
cat $f; echo;
|
||||
done > ${HEADERS_DIR}/module.modulemap
|
||||
rm ${GENERATED_DIR}/*.modulemap
|
||||
# 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"
|
||||
@@ -104,21 +77,15 @@ mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
|
||||
# Build the xcframework
|
||||
|
||||
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
|
||||
if ${only_ios}; then
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
else
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
fi
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
|
||||
# Cleanup
|
||||
if [ -d "${GENERATED_DIR}/macos" ]; then rm -rf "${GENERATED_DIR}/macos"; fi
|
||||
|
||||
@@ -3,70 +3,66 @@ name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
rust-version = { workspace = true }
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "matrix_sdk_crypto_ffi"
|
||||
path = "uniffi-bindgen.rs"
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures-util.workspace = true
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
futures-util = "0.3.25"
|
||||
hmac = "0.12.1"
|
||||
http.workspace = true
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
pbkdf2 = "0.12.2"
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
http = { workspace = true }
|
||||
pbkdf2 = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = "0.10.2"
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
vodozemac.workspace = true
|
||||
uniffi = { workspace = true }
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dependencies.js_int]
|
||||
version = "0.2.2"
|
||||
features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-common]
|
||||
path = "../../crates/matrix-sdk-common"
|
||||
version = "0.6.0"
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
workspace = true
|
||||
features = ["qrcode", "automatic-room-key-forwarding", "uniffi"]
|
||||
path = "../../crates/matrix-sdk-crypto"
|
||||
version = "0.6.0"
|
||||
features = ["qrcode", "backups_v1"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
workspace = true
|
||||
path = "../../crates/matrix-sdk-sqlite"
|
||||
version = "0.1.0"
|
||||
default_features = false
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.43.1"
|
||||
default-features = false
|
||||
version = "1.24.2"
|
||||
default_features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
tempfile = "3.8.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
tempfile = "3.3.0"
|
||||
assert_matches = { workspace = true }
|
||||
|
||||
@@ -71,6 +71,10 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
@@ -1,65 +1,3 @@
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
|
||||
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
|
||||
|
||||
Ok(())
|
||||
fn main() {
|
||||
uniffi::generate_scaffolding("./src/olm.udl").unwrap();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
|
||||
use std::{collections::HashMap, iter, ops::DerefMut};
|
||||
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::DecryptionError,
|
||||
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
backups::OlmPkDecryptionError,
|
||||
store::{CryptoStoreError as InnerStoreError, RecoveryKey},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
@@ -12,9 +12,8 @@ use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// The private part of the backup key, the one used for recovery.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct BackupRecoveryKey {
|
||||
pub(crate) inner: BackupDecryptionKey,
|
||||
pub(crate) inner: RecoveryKey,
|
||||
pub(crate) passphrase_info: Option<PassphraseInfo>,
|
||||
}
|
||||
|
||||
@@ -24,7 +23,7 @@ pub struct BackupRecoveryKey {
|
||||
pub enum PkDecryptionError {
|
||||
/// An internal libolm error happened during decryption.
|
||||
#[error("Error decryption a PkMessage {0}")]
|
||||
Olm(#[from] DecryptionError),
|
||||
Olm(#[from] OlmPkDecryptionError),
|
||||
}
|
||||
|
||||
/// Error type for the decoding and storing of the backup key.
|
||||
@@ -63,40 +62,46 @@ pub struct MegolmV1BackupKey {
|
||||
pub backup_algorithm: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Convert the recovery key to a base 58 encoded string.
|
||||
pub fn to_base58(&self) -> String {
|
||||
self.inner.to_base58()
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupRecoveryKey {
|
||||
const KEY_SIZE: usize = 32;
|
||||
const SALT_SIZE: usize = 32;
|
||||
const PBKDF_ROUNDS: i32 = 500_000;
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Create a new random [`BackupRecoveryKey`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
inner: BackupDecryptionKey::new()
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: RecoveryKey::new()
|
||||
.expect("Can't gather enough randomness to create a recovery key"),
|
||||
passphrase_info: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_base64(key: String) -> Result<Arc<Self>, DecodeError> {
|
||||
Ok(Arc::new(Self { inner: BackupDecryptionKey::from_base64(&key)?, passphrase_info: None }))
|
||||
pub fn from_base64(key: String) -> Result<Self, DecodeError> {
|
||||
Ok(Self { inner: RecoveryKey::from_base64(&key)?, passphrase_info: None })
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_base58(key: String) -> Result<Arc<Self>, DecodeError> {
|
||||
Ok(Arc::new(Self { inner: BackupDecryptionKey::from_base58(&key)?, passphrase_info: None }))
|
||||
pub fn from_base58(key: String) -> Result<Self, DecodeError> {
|
||||
Ok(Self { inner: RecoveryKey::from_base58(&key)?, passphrase_info: None })
|
||||
}
|
||||
|
||||
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
|
||||
#[uniffi::constructor]
|
||||
pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
|
||||
pub fn new_from_passphrase(passphrase: String) -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let salt: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
@@ -108,40 +113,28 @@ impl BackupRecoveryKey {
|
||||
}
|
||||
|
||||
/// Restore a [`BackupRecoveryKey`] from the given passphrase.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Arc<Self> {
|
||||
pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Self {
|
||||
let mut key = Box::new([0u8; Self::KEY_SIZE]);
|
||||
let rounds = rounds as u32;
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut())
|
||||
.expect(
|
||||
"We should be able to expand a passphrase of any length due to \
|
||||
HMAC being able to be initialized with any input size",
|
||||
);
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut());
|
||||
|
||||
let backup_decryption_key = BackupDecryptionKey::from_bytes(&key);
|
||||
let recovery_key = RecoveryKey::from_bytes(&key);
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Arc::new(Self {
|
||||
inner: backup_decryption_key,
|
||||
Self {
|
||||
inner: recovery_key,
|
||||
passphrase_info: Some(PassphraseInfo {
|
||||
private_key_salt: salt,
|
||||
private_key_iterations: rounds as i32,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 58 encoded string.
|
||||
pub fn to_base58(&self) -> String {
|
||||
self.inner.to_base58()
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Get the public part of the backup key.
|
||||
pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
|
||||
let public_key = self.inner.megolm_v1_public_key();
|
||||
@@ -149,22 +142,7 @@ impl BackupRecoveryKey {
|
||||
let signatures: HashMap<String, HashMap<String, String>> = public_key
|
||||
.signatures()
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
match v {
|
||||
Ok(s) => s.to_base64(),
|
||||
Err(s) => s.source,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect()))
|
||||
.collect();
|
||||
|
||||
MegolmV1BackupKey {
|
||||
@@ -183,51 +161,6 @@ impl BackupRecoveryKey {
|
||||
mac: String,
|
||||
ciphertext: String,
|
||||
) -> Result<String, PkDecryptionError> {
|
||||
self.inner.decrypt_v1(&ephemeral_key, &mac, &ciphertext).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::api::client::backup::KeyBackupData;
|
||||
use serde_json::json;
|
||||
|
||||
use super::BackupRecoveryKey;
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_key() {
|
||||
let recovery_key = BackupRecoveryKey::from_base64(
|
||||
"Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let data = json!({
|
||||
"first_message_index": 0,
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ephemeral": "HlLi76oV6wxHz3PCqE/bxJi6yF1HnYz5Dq3T+d/KpRw",
|
||||
"ciphertext": "MuM8E3Yc6TSAvhVGb77rQ++jE6p9dRepx63/3YPD2wACKAppkZHeFrnTH6wJ/HSyrmzo\
|
||||
7HfwqVl6tKNpfooSTHqUf6x1LHz+h4B/Id5ITO1WYt16AaI40LOnZqTkJZCfSPuE2oxa\
|
||||
lwEHnCS3biWybutcnrBFPR3LMtaeHvvkb+k3ny9l5ZpsU9G7vCm3XoeYkWfLekWXvDhb\
|
||||
qWrylXD0+CNUuaQJ/S527TzLd4XKctqVjjO/cCH7q+9utt9WJAfK8LGaWT/mZ3AeWjf5\
|
||||
kiqOpKKf5Cn4n5SSil5p/pvGYmjnURvZSEeQIzHgvunIBEPtzK/MYEPOXe/P5achNGlC\
|
||||
x+5N19Ftyp9TFaTFlTWCTi0mpD7ePfCNISrwpozAz9HZc0OhA8+1aSc7rhYFIeAYXFU3\
|
||||
26NuFIFHI5pvpSxjzPQlOA+mavIKmiRAtjlLw11IVKTxgrdT4N8lXeMr4ndCSmvIkAzF\
|
||||
Mo1uZA4fzjiAdQJE4/2WeXFNNpvdfoYmX8Zl9CAYjpSO5HvpwkAbk4/iLEH3hDfCVUwD\
|
||||
fMh05PdGLnxeRpiEFWSMSsJNp+OWAA+5JsF41BoRGrxoXXT+VKqlUDONd+O296Psu8Q+\
|
||||
d8/S618",
|
||||
"mac": "GtMrurhDTwo"
|
||||
}
|
||||
});
|
||||
|
||||
let key_backup_data: KeyBackupData = serde_json::from_value(data).unwrap();
|
||||
let ephemeral = key_backup_data.session_data.ephemeral.encode();
|
||||
let ciphertext = key_backup_data.session_data.ciphertext.encode();
|
||||
let mac = key_backup_data.session_data.mac.encode();
|
||||
|
||||
let _ = recovery_key
|
||||
.decrypt_v1(ephemeral, mac, ciphertext)
|
||||
.expect("The backed up key should be decrypted successfully");
|
||||
self.inner.decrypt_v1(ephemeral_key, mac, ciphertext).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum DehydrationError {
|
||||
#[error(transparent)]
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::DehydratedDeviceError),
|
||||
#[error(transparent)]
|
||||
LegacyPickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
#[error(transparent)]
|
||||
MissingSigningKey(#[from] matrix_sdk_crypto::SignatureError),
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
#[error(transparent)]
|
||||
Rand(#[from] rand::Error),
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
|
||||
fn from(value: matrix_sdk_crypto::dehydrated_devices::DehydrationError) -> Self {
|
||||
match value {
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Json(e) => Self::Json(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Pickle(e) => Self::Pickle(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::LegacyPickle(e) => {
|
||||
Self::LegacyPickle(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::MissingSigningKey(e) => {
|
||||
Self::MissingSigningKey(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
|
||||
Self::PickleKeyLength(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct DehydratedDevices {
|
||||
pub(crate) runtime: Handle,
|
||||
pub(crate) inner: ManuallyDrop<InnerDehydratedDevices>,
|
||||
}
|
||||
|
||||
impl Drop for DehydratedDevices {
|
||||
fn drop(&mut self) {
|
||||
// See the drop implementation for the `crate::OlmMachine` for an explanation.
|
||||
let _guard = self.runtime.enter();
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevices {
|
||||
pub fn create(&self) -> Result<Arc<DehydratedDevice>, DehydrationError> {
|
||||
let inner = self.runtime.block_on(self.inner.create())?;
|
||||
|
||||
Ok(Arc::new(DehydratedDevice {
|
||||
inner: ManuallyDrop::new(inner),
|
||||
runtime: self.runtime.to_owned(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn rehydrate(
|
||||
&self,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
device_id: String,
|
||||
device_data: String,
|
||||
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
|
||||
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
|
||||
let device_id: OwnedDeviceId = device_id.into();
|
||||
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let ret = RehydratedDevice {
|
||||
runtime: self.runtime.to_owned(),
|
||||
inner: ManuallyDrop::new(self.runtime.block_on(self.inner.rehydrate(
|
||||
&key,
|
||||
&device_id,
|
||||
device_data,
|
||||
))?),
|
||||
}
|
||||
.into();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Get the cached dehydrated device pickle key if any.
|
||||
///
|
||||
/// None if the key was not previously cached (via
|
||||
/// [`Self::save_dehydrated_device_pickle_key`]).
|
||||
///
|
||||
/// Should be used to periodically rotate the dehydrated device to avoid
|
||||
/// OTK exhaustion and accumulation of to_device messages.
|
||||
pub fn get_dehydrated_device_key(
|
||||
&self,
|
||||
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.get_dehydrated_device_pickle_key())?
|
||||
.map(crate::DehydratedDeviceKey::from))
|
||||
}
|
||||
|
||||
/// Store the dehydrated device pickle key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to periodically rotate dehydrated
|
||||
/// devices to avoid OTK exhaustion and accumulated to_device problems.
|
||||
pub fn save_dehydrated_device_key(
|
||||
&self,
|
||||
pickle_key: &crate::DehydratedDeviceKey,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
|
||||
}
|
||||
|
||||
/// Deletes the previously stored dehydrated device pickle key.
|
||||
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RehydratedDevice {
|
||||
inner: ManuallyDrop<InnerRehydratedDevice>,
|
||||
runtime: Handle,
|
||||
}
|
||||
|
||||
impl Drop for RehydratedDevice {
|
||||
fn drop(&mut self) {
|
||||
// See the drop implementation for the `crate::OlmMachine` for an explanation.
|
||||
let _guard = self.runtime.enter();
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RehydratedDevice {
|
||||
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
|
||||
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
|
||||
self.runtime.block_on(self.inner.receive_events(events))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct DehydratedDevice {
|
||||
pub(crate) runtime: Handle,
|
||||
pub(crate) inner: ManuallyDrop<InnerDehydratedDevice>,
|
||||
}
|
||||
|
||||
impl Drop for DehydratedDevice {
|
||||
fn drop(&mut self) {
|
||||
// See the drop implementation for the `crate::OlmMachine` for an explanation.
|
||||
let _guard = self.runtime.enter();
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
device_display_name: String,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let request =
|
||||
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
|
||||
|
||||
Ok(request.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
pub struct UploadDehydratedDeviceRequest {
|
||||
/// The serialized JSON body of the request.
|
||||
body: String,
|
||||
}
|
||||
|
||||
impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
for UploadDehydratedDeviceRequest
|
||||
{
|
||||
fn from(value: dehydrated_device::put_dehydrated_device::unstable::Request) -> Self {
|
||||
let body = json!({
|
||||
"device_id": value.device_id,
|
||||
"device_data": value.device_data,
|
||||
"initial_device_display_name": value.initial_device_display_name,
|
||||
"device_keys": value.device_keys,
|
||||
"one_time_keys": value.one_time_keys,
|
||||
"fallback_keys": value.fallback_keys,
|
||||
});
|
||||
|
||||
let body = serde_json::to_string(&body)
|
||||
.expect("We should be able to serialize the PUT dehydrated devices request body");
|
||||
|
||||
Self { body }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let result = DehydratedDeviceKey::new();
|
||||
assert!(result.is_ok());
|
||||
let dehydrated_device_key = result.unwrap();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
|
||||
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
|
||||
|
||||
assert_eq!(base_64, copy.to_base64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key_failure() {
|
||||
let bytes = [0u8; 24];
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
|
||||
|
||||
assert!(pickle_key.is_err());
|
||||
|
||||
match pickle_key {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
|
||||
assert_eq!(bytes.len(), pickle_key_length);
|
||||
}
|
||||
_ => panic!("Should have failed!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,6 @@ pub struct Device {
|
||||
/// Is our cross signing identity trusted and does the identity trust the
|
||||
/// device.
|
||||
pub cross_signing_trusted: bool,
|
||||
/// The first time this device was seen in local timestamp, milliseconds
|
||||
/// since epoch.
|
||||
pub first_time_seen_ts: u64,
|
||||
/// Whether or not the device is a dehydrated device.
|
||||
pub dehydrated: bool,
|
||||
}
|
||||
|
||||
impl From<InnerDevice> for Device {
|
||||
@@ -43,8 +38,6 @@ impl From<InnerDevice> for Device {
|
||||
is_blocked: d.is_blacklisted(),
|
||||
locally_trusted: d.is_locally_trusted(),
|
||||
cross_signing_trusted: d.is_cross_signing_trusted(),
|
||||
first_time_seen_ts: d.first_time_seen_ts().0.into(),
|
||||
dehydrated: d.is_dehydrated(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
|
||||
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
@@ -43,8 +42,7 @@ pub enum SignatureError {
|
||||
UnknownUserIdentity(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CryptoStoreError {
|
||||
#[error("Failed to open the store")]
|
||||
OpenStore(#[from] OpenStoreError),
|
||||
@@ -58,30 +56,23 @@ pub enum CryptoStoreError {
|
||||
InvalidUserId(String, IdParseError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
DehydrationError(#[from] InnerDehydrationError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[derive(Debug, uniffi::Error)]
|
||||
pub enum DecryptionError {
|
||||
#[error("serialization error: {error}")]
|
||||
Serialization { error: String },
|
||||
#[error("identifier parsing error: {error}")]
|
||||
Identifier { error: String },
|
||||
#[error("megolm error: {error}")]
|
||||
Megolm { error: String },
|
||||
#[error("missing room key: {error}")]
|
||||
MissingRoomKey { error: String, withheld_code: Option<String> },
|
||||
#[error("store error: {error}")]
|
||||
Store { error: String },
|
||||
}
|
||||
|
||||
impl From<MegolmError> for DecryptionError {
|
||||
fn from(value: MegolmError) -> Self {
|
||||
match &value {
|
||||
match value {
|
||||
MegolmError::MissingRoomKey(withheld_code) => Self::MissingRoomKey {
|
||||
error: value.to_string(),
|
||||
withheld_code: withheld_code.as_ref().map(|w| w.as_str().to_owned()),
|
||||
error: "Withheld Inbound group session".to_owned(),
|
||||
withheld_code: withheld_code.map(|w| w.as_str().to_owned()),
|
||||
},
|
||||
_ => Self::Megolm { error: value.to_string() },
|
||||
}
|
||||
@@ -108,21 +99,22 @@ impl From<InnerStoreError> for DecryptionError {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_crypto::MegolmError;
|
||||
|
||||
use super::DecryptionError;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_withheld_error_mapping() {
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
|
||||
|
||||
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
|
||||
|
||||
let binding_error: DecryptionError = inner_error.into();
|
||||
|
||||
assert_let!(
|
||||
DecryptionError::MissingRoomKey { error: _, withheld_code: Some(code) } = binding_error
|
||||
let code = assert_matches!(
|
||||
binding_error,
|
||||
DecryptionError::MissingRoomKey { error: _, withheld_code: Some(withheld_code) } => withheld_code
|
||||
);
|
||||
assert_eq!("m.unverified", code)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#![allow(unused_qualifications)]
|
||||
|
||||
mod backup_recovery_key;
|
||||
mod dehydrated_devices;
|
||||
mod device;
|
||||
mod error;
|
||||
mod logger;
|
||||
@@ -16,13 +15,9 @@ mod responses;
|
||||
mod users;
|
||||
mod verification;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Context;
|
||||
pub use backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
};
|
||||
@@ -33,20 +28,13 @@ pub use error::{
|
||||
use js_int::UInt;
|
||||
pub use logger::{set_logger, Logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{
|
||||
types::{
|
||||
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
CryptoStore,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
backups::SignatureState,
|
||||
olm::{IdentityKeys, InboundGroupSession, Session},
|
||||
store::{Changes, CryptoStore, RoomSettings as RustRoomSettings},
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
EncryptionSettings as RustEncryptionSettings, LocalTrust,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -54,9 +42,8 @@ pub use responses::{
|
||||
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
|
||||
RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility, DeviceId,
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
@@ -68,8 +55,6 @@ pub use verification::{
|
||||
};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::dehydrated_devices::DehydrationError;
|
||||
|
||||
/// Struct collecting data that is important to migrate to the rust-sdk
|
||||
#[derive(Deserialize, Serialize, uniffi::Record)]
|
||||
pub struct MigrationData {
|
||||
@@ -142,10 +127,10 @@ pub struct PickledSession {
|
||||
pub sender_key: String,
|
||||
/// Was the session created using a fallback key.
|
||||
pub created_using_fallback_key: bool,
|
||||
/// Unix timestamp (in seconds) when the session was created.
|
||||
pub creation_time: u64,
|
||||
/// Unix timestamp (in seconds) when the session was last used.
|
||||
pub last_use_time: u64,
|
||||
/// The Unix timestamp when the session was created.
|
||||
pub creation_time: String,
|
||||
/// The Unix timestamp when the session was last used.
|
||||
pub last_use_time: String,
|
||||
}
|
||||
|
||||
/// A pickled version of an `InboundGroupSession`.
|
||||
@@ -199,12 +184,12 @@ impl From<anyhow::Error> for MigrationError {
|
||||
/// * `path` - The path where the SQLite store should be created.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data at
|
||||
/// rest in the SQLite store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
/// rest in the SQLite store. **Warning**, if no passphrase is given, the store
|
||||
/// and all its data will remain unencrypted.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
pub fn migrate(
|
||||
data: MigrationData,
|
||||
path: String,
|
||||
@@ -224,7 +209,7 @@ async fn migrate_data(
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> anyhow::Result<()> {
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::types::BackupDecryptionKey};
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::RecoveryKey};
|
||||
use vodozemac::olm::Account;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -247,23 +232,26 @@ async fn migrate_data(
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
let user_id = parse_user_id(&data.account.user_id)?;
|
||||
let device_id: OwnedDeviceId = data.account.device_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();
|
||||
|
||||
let account = Account::from_libolm_pickle(&data.account.pickle, &data.pickle_key)?;
|
||||
let pickle = account.pickle();
|
||||
let identity_keys = Arc::new(account.identity_keys());
|
||||
let pickled_account = matrix_sdk_crypto::olm::PickledAccount {
|
||||
user_id: parse_user_id(&data.account.user_id)?,
|
||||
device_id: device_id.clone(),
|
||||
device_id: device_id.as_ref().to_owned(),
|
||||
pickle,
|
||||
dehydrated: false, // dehydrated devices are never involved in migration
|
||||
shared: data.account.shared,
|
||||
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
|
||||
fallback_key_creation_timestamp: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
};
|
||||
let account = matrix_sdk_crypto::olm::Account::from_pickle(pickled_account)?;
|
||||
let account = matrix_sdk_crypto::olm::ReadOnlyAccount::from_pickle(pickled_account)?;
|
||||
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
@@ -280,10 +268,8 @@ async fn migrate_data(
|
||||
data.inbound_group_sessions,
|
||||
)?;
|
||||
|
||||
let backup_decryption_key = data
|
||||
.backup_recovery_key
|
||||
.map(|k| BackupDecryptionKey::from_base58(k.as_str()))
|
||||
.transpose()?;
|
||||
let recovery_key =
|
||||
data.backup_recovery_key.map(|k| RecoveryKey::from_base58(k.as_str())).transpose()?;
|
||||
|
||||
let cross_signing = PrivateCrossSigningIdentity::empty((*user_id).into());
|
||||
cross_signing
|
||||
@@ -304,8 +290,8 @@ async fn migrate_data(
|
||||
let tracked_users: Vec<_> = data
|
||||
.tracked_users
|
||||
.into_iter()
|
||||
.filter_map(|s| parse_user_id(&s).ok().map(|u| (u, true)))
|
||||
.collect();
|
||||
.map(|u| Ok(((parse_user_id(&u)?), true)))
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
|
||||
let tracked_users: Vec<_> = tracked_users.iter().map(|(u, d)| (&**u, *d)).collect();
|
||||
store.save_tracked_users(tracked_users.as_slice()).await?;
|
||||
@@ -319,13 +305,12 @@ async fn migrate_data(
|
||||
room_settings.insert(room_id, settings.into());
|
||||
}
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account) }).await?;
|
||||
|
||||
let changes = Changes {
|
||||
account: Some(account),
|
||||
private_identity: Some(cross_signing),
|
||||
sessions,
|
||||
inbound_group_sessions,
|
||||
backup_decryption_key,
|
||||
recovery_key,
|
||||
backup_version: data.backup_version,
|
||||
room_settings,
|
||||
..Default::default()
|
||||
@@ -362,12 +347,12 @@ async fn save_changes(
|
||||
/// * `path` - The path where the SQLite store should be created.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data at
|
||||
/// rest in the SQLite store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
/// rest in the SQLite store. **Warning**, if no passphrase is given, the store
|
||||
/// and all its data will remain unencrypted.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
pub fn migrate_sessions(
|
||||
data: SessionMigrationData,
|
||||
path: String,
|
||||
@@ -394,7 +379,7 @@ async fn migrate_session_data(
|
||||
let total_steps = 1 + data.sessions.len() + data.inbound_group_sessions.len();
|
||||
let processed_steps = 0;
|
||||
|
||||
let user_id = UserId::parse(data.user_id)?;
|
||||
let user_id = UserId::parse(data.user_id)?.into();
|
||||
let device_id: OwnedDeviceId = data.device_id.into();
|
||||
|
||||
let identity_keys = IdentityKeys {
|
||||
@@ -409,7 +394,7 @@ async fn migrate_session_data(
|
||||
&listener,
|
||||
&data.pickle_key,
|
||||
user_id,
|
||||
device_id,
|
||||
device_id.into(),
|
||||
identity_keys,
|
||||
data.sessions,
|
||||
data.inbound_group_sessions,
|
||||
@@ -425,46 +410,21 @@ fn collect_sessions(
|
||||
total_steps: usize,
|
||||
listener: &dyn Fn(usize, usize),
|
||||
pickle_key: &[u8],
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<DeviceId>,
|
||||
identity_keys: Arc<IdentityKeys>,
|
||||
session_pickles: Vec<PickledSession>,
|
||||
group_session_pickles: Vec<PickledInboundGroupSession>,
|
||||
) -> anyhow::Result<(Vec<Session>, Vec<InboundGroupSession>)> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
// Create a DeviceKeys struct with enough information to get a working
|
||||
// Session, but we will won't actually use the Sessions (and we'll clear
|
||||
// the session cache after migration) so we don't need to worry about
|
||||
// signatures.
|
||||
let device_keys = DeviceKeys::new(
|
||||
user_id,
|
||||
device_id.clone(),
|
||||
Default::default(),
|
||||
BTreeMap::from([
|
||||
(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &device_id),
|
||||
DeviceKey::Ed25519(identity_keys.ed25519),
|
||||
),
|
||||
(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Curve25519, &device_id),
|
||||
DeviceKey::Curve25519(identity_keys.curve25519),
|
||||
),
|
||||
]),
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
for session_pickle in session_pickles {
|
||||
let pickle =
|
||||
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
|
||||
.pickle();
|
||||
|
||||
let creation_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.creation_time).context("invalid creation timestamp")?,
|
||||
);
|
||||
let last_use_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.last_use_time).context("invalid last use timestamp")?,
|
||||
);
|
||||
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
|
||||
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
|
||||
|
||||
let pickle = matrix_sdk_crypto::olm::PickledSession {
|
||||
pickle,
|
||||
@@ -474,7 +434,8 @@ fn collect_sessions(
|
||||
last_use_time,
|
||||
};
|
||||
|
||||
let session = Session::from_pickle(device_keys.clone(), pickle)?;
|
||||
let session =
|
||||
Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle);
|
||||
|
||||
sessions.push(session);
|
||||
processed_steps += 1;
|
||||
@@ -499,18 +460,16 @@ fn collect_sessions(
|
||||
.signing_key
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
let algorithm = DeviceKeyAlgorithm::from(k);
|
||||
let algorithm = DeviceKeyAlgorithm::try_from(k)?;
|
||||
let key = SigningKey::from_parts(&algorithm, v)?;
|
||||
|
||||
Ok((algorithm, key))
|
||||
})
|
||||
.collect::<anyhow::Result<_>>()?,
|
||||
sender_data: SenderData::legacy(),
|
||||
room_id: RoomId::parse(session.room_id)?,
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
shared_history: false,
|
||||
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
};
|
||||
|
||||
@@ -539,9 +498,9 @@ fn collect_sessions(
|
||||
/// * `path` - The path where the Sqlite store should be created.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data at
|
||||
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the store
|
||||
/// and all its data will remain unencrypted.
|
||||
#[uniffi::export]
|
||||
pub fn migrate_room_settings(
|
||||
room_settings: HashMap<String, RoomSettings>,
|
||||
path: String,
|
||||
@@ -567,7 +526,6 @@ pub fn migrate_room_settings(
|
||||
}
|
||||
|
||||
/// Callback that will be passed over the FFI to report progress
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ProgressListener {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -677,27 +635,16 @@ pub struct EncryptionSettings {
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
/// Should fail to send when a verified user has unverified devices, or when
|
||||
/// a previously verified user replaces their identity.
|
||||
pub error_on_verified_user_problem: bool,
|
||||
}
|
||||
|
||||
impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
fn from(v: EncryptionSettings) -> Self {
|
||||
let sharing_strategy = if v.only_allow_trusted_devices {
|
||||
CollectStrategy::OnlyTrustedDevices
|
||||
} else if v.error_on_verified_user_problem {
|
||||
CollectStrategy::ErrorOnVerifiedUserProblem
|
||||
} else {
|
||||
CollectStrategy::AllDevices
|
||||
};
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy,
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -742,24 +689,19 @@ pub enum ShieldColor {
|
||||
#[allow(missing_docs)]
|
||||
pub struct ShieldState {
|
||||
color: ShieldColor,
|
||||
code: Option<ShieldStateCode>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
impl From<RustShieldState> for ShieldState {
|
||||
fn from(value: RustShieldState) -> Self {
|
||||
match value {
|
||||
RustShieldState::Red { code, message } => Self {
|
||||
color: ShieldColor::Red,
|
||||
code: Some(code),
|
||||
message: Some(message.to_owned()),
|
||||
},
|
||||
RustShieldState::Grey { code, message } => Self {
|
||||
color: ShieldColor::Grey,
|
||||
code: Some(code),
|
||||
message: Some(message.to_owned()),
|
||||
},
|
||||
RustShieldState::None => Self { color: ShieldColor::None, code: None, message: None },
|
||||
RustShieldState::Red { message } => {
|
||||
Self { color: ShieldColor::Red, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::Grey { message } => {
|
||||
Self { color: ShieldColor::Grey, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::None => Self { color: ShieldColor::None, message: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -808,7 +750,7 @@ pub struct BackupKeys {
|
||||
backup_version: String,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl BackupKeys {
|
||||
/// Get the recovery key that we're holding on to.
|
||||
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
|
||||
@@ -821,13 +763,13 @@ impl BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
|
||||
impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(keys: matrix_sdk_crypto::store::types::BackupKeys) -> Result<Self, Self::Error> {
|
||||
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
recovery_key: BackupRecoveryKey {
|
||||
inner: keys.decryption_key.ok_or(())?,
|
||||
inner: keys.recovery_key.ok_or(())?,
|
||||
passphrase_info: None,
|
||||
}
|
||||
.into(),
|
||||
@@ -836,41 +778,8 @@ impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dehydrated device key
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct DehydratedDeviceKey {
|
||||
pub(crate) inner: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
pub fn new() -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::new()?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
///
|
||||
/// Fail if the slice length is not 32.
|
||||
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
|
||||
inner.to_base64()
|
||||
}
|
||||
}
|
||||
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
|
||||
DehydratedDeviceKey { inner: pickle_key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::types::RoomKeyCounts) -> Self {
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
}
|
||||
}
|
||||
@@ -929,7 +838,6 @@ impl From<RoomSettings> for RustRoomSettings {
|
||||
Self {
|
||||
algorithm: value.algorithm.into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
..RustRoomSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -938,101 +846,20 @@ fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
|
||||
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn version_info() -> VersionInfo {
|
||||
VersionInfo {
|
||||
version: matrix_sdk_crypto::VERSION.to_owned(),
|
||||
vodozemac_version: matrix_sdk_crypto::vodozemac::VERSION.to_owned(),
|
||||
git_description: env!("VERGEN_GIT_DESCRIBE").to_owned(),
|
||||
git_sha: env!("VERGEN_GIT_SHA").to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build-time information about important crates that are used.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct VersionInfo {
|
||||
/// The version of the matrix-sdk-crypto crate.
|
||||
pub version: String,
|
||||
/// The version of the vodozemac crate.
|
||||
pub vodozemac_version: String,
|
||||
/// The Git commit hash of the crate's source tree at build time.
|
||||
pub git_sha: String,
|
||||
/// The build-time output of the `git describe` command of the source tree
|
||||
/// of crate.
|
||||
pub git_description: String,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn version() -> String {
|
||||
matrix_sdk_crypto::VERSION.to_owned()
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
fn vodozemac_version() -> String {
|
||||
vodozemac::VERSION.to_owned()
|
||||
}
|
||||
|
||||
/// The encryption component of PkEncryption support.
|
||||
///
|
||||
/// This struct can be created using a [`Curve25519PublicKey`] corresponding to
|
||||
/// a `PkDecryption` object, allowing messages to be encrypted for the
|
||||
/// associated decryption object.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct PkEncryption {
|
||||
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl PkEncryption {
|
||||
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
|
||||
/// encoded as Base64.
|
||||
///
|
||||
/// The public key should come from an existing `PkDecryption` object.
|
||||
/// Returns a `DecodeError` if the Curve25519 key could not be decoded
|
||||
/// correctly.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_base64(key: &str) -> Result<Arc<Self>, DecodeError> {
|
||||
let key = vodozemac::Curve25519PublicKey::from_base64(key)
|
||||
.map_err(matrix_sdk_crypto::backups::DecodeError::PublicKey)?;
|
||||
let inner = vodozemac::pk_encryption::PkEncryption::from_key(key);
|
||||
|
||||
Ok(Self { inner }.into())
|
||||
}
|
||||
|
||||
/// Encrypt a message using this [`PkEncryption`] object.
|
||||
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
|
||||
use vodozemac::base64_encode;
|
||||
|
||||
let message = self.inner.encrypt(plaintext.as_ref());
|
||||
|
||||
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
|
||||
|
||||
PkMessage {
|
||||
ciphertext: base64_encode(ciphertext),
|
||||
mac: base64_encode(mac),
|
||||
ephemeral_key: ephemeral_key.to_base64(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A message that was encrypted using a [`PkEncryption`] object.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct PkMessage {
|
||||
/// The ciphertext of the message.
|
||||
pub ciphertext: String,
|
||||
/// The message authentication code of the message.
|
||||
///
|
||||
/// *Warning*: This does not authenticate the ciphertext.
|
||||
pub mac: String,
|
||||
/// The ephemeral Curve25519 key of the message which was used to derive the
|
||||
/// individual message key.
|
||||
pub ephemeral_key: String,
|
||||
}
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
uniffi::include_scaffolding!("olm");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod test {
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tempfile::tempdir;
|
||||
@@ -1049,35 +876,36 @@ mod tests {
|
||||
"pickle":"FFGTGho89T3Xgd56l+EedOPV37s09RR8aYnS9305qPKF66LG+ly29YpCibjJOvkwm0dZwN9A2bOH/z7WscriqwZn/p0GE6YSNwLzffCy5iROzYzpYzFe0HtiyJmCQWCezvLc5lHV8YsfD00C1pKGX2R9M1wwp3/n4/3VjtTyPsdnmtwAPu4WdcPSkisCaQ3a6JaSKqv8zYzUjnpzgcpXHvPUR5d5+TzXgrVz3BeCOe8NEOWIW6xYUxFtGteYP0BczOkkJ22t7Css0tSMSrYgCll4zZUGNrd6D9b/z7KwcDnb978epsZ16DcZ/aaTxPdM5uDIkHgF/qHWerfxcaqsqs4EQfJdSgOTeqhjHBw1k0uWF2bByJLK+n7sGkYXEAuTzc4+0XvSFvu3Qp+1bHZuT7QejngRZzyxznORyBxd8la3/JjeJlehSK80OL7zSmohoYZD59S6i3tFWfopjQThJ0/eIyVOhEN/c3tfIcVr3lFEQeokgpCRNOVldhPcQWq994NHaL7jtb6yhUqT1gShY4zYayFL/VRz6nBSXXYwzrC9jho67knqXSri3lIKYevP9aOi384IvzbkinQdumc804dYwiCbs5hZppfEnfhfgiDDm+kVrJ9WaPRF4SySCTlS8jdGmBeL2CfCQ5IcZ5nK6X7tZM3tmtYwva0RuQiTNltp3XTfbMa0EoaEBximv25165hFTpzrWgoszBTpZPfgsMuWENWBcIX4AcLSk0CJ0qzPDeUwvmRcFStstGYV4drs5u5HEqovFSI48CoHPSEZfwwERCI4c/0efZ0CVEfnm8VcMv3AbnAfedD7v3QNdVwWOEhz/fGR76BQi2WjZP4MWvYRJ/vsLO5hcVWUvaJGQs5kANUFZMWuJQeJv3DmkV9kKKXnyfFUerlQ4Uk/5tp2mXiG+adHjuRp/Eeh5V/biCcIaX3rNuIY6MJaPz6SOwlFe79MMBaNwaS3j4Kh/Aq9BRw0QXdjO4CqMI4p2xCE1N5QTPdeaRTHTZ3r7mLkHX3FpZMxitc8vDl9L2FRoSOMMh/sRD1boBCkjrsty9rvTUGYY3li05jBuTXnYMjA4zj79dC9TGo4g+/wi+h537EhtP5+170LwqnIzfHt8yfjbsMMC7iwLpC1C57sTwxpMkNo3nQEvZOfqCxjq+ihiGuL9iN5lSstu9/C4qP2tQll86ASXf1axxRZQlUB0hlLHbEW6/7O7xOU6FTs4yXAZC04souRkggmfhDzZ9kQmN/zRTbqlATFI7l9/0VGxwLOVnCMUhgiDX5yL8CYK9I4ENMLf5zOuO6P3GbYISjEoHC7fUOzQ6OwGgLyI0wCEVdSJzQcdKh+W15VV+eDjhE/qEJHQWx024hTQFTKYHlDn95+lMmRI9BJLP1HU2JW6onVWsTsE5zSYu9jLj739EKfV4gS/pWzoQDRa7a9ZG6+m+RrwyJhCso3gkUekDNobhFlDX6YeH+Btj91N0uS3F9qr8lbo491s/z2fNV42zT4NYObzgrAYDQAV/2WYF8tXtxLV/Jzk8AMmyr/cfNaT2dXxVJKWq+nN2BYHBmg9CCWPJ2aB/1WWIcHfcDOlngtH991gP6246f/DEaVC/Ayxz7bPtSH5tlZ4Xbpc2P4BYxaRp/yxhhQ2C9H2I/PTt3mnNNgky/t8PZrN3W5+eiSVE9sONF8G3mYsa4XFqM+KxfbPUqsrEnrRBmvmJ250hpTPkFcIF775RvvRRKALXdlTKs+S4HKDW7KoP0Dm9+r4RlO0UHpWND9w0WSMItvWQyo0VViXJgZfBjYtWDoO0Ud+Kc7PLWNX6RUKY7RlDjXadJTC4adH6CN3UBC/ouqqfTrYvPOkyd2oKf4RLjEVcFAUIftFbLy+WBcWv8072nnAFJIlm3CxGq++80TyjqFR45P+qfIJavxQNIt5zhHPfMgHjX27OA3+l7rHDxqfMLBPxhtARwlyF+qx1IJiSWbmlHkdz2ylD9unoLSpf+DmmFvvgTj+3EEP4bY2jA/t91XFeG3uaTQSy3ryDvhbX21U7G2HGOEl9rCkmz+hG0YRB/6KxZZ0eMIDr7OWfpPEuHV8oYwDNYbsT9zCGsR1hHxBJtdo60b36mjMemtf761DhJ/oQZ4eU738yzx1hvVS3aCJsfyp70H5u+pUjgrA565uG2lEMNLu4T4NFVw0UdrVudyrhmT8P7vF4v+mR4pp+OzRbLf8AtZrKmHlMqRst+/wOHUHug/Tpz6EwZPDWGiQyFyPUkjHWW7ACouegBFOWFabsk+zCDhyxoSNrSMCtdB1L+qK72jRPGOvXk8p/1kBOIJfAjaK1ZWz8hTc30hOSWYxkRP296zPHiQF0ibNYSPNZ9tNxgq9nV/cEQ68TsNr3SULfDr0TSjCPf4AfmJ0k1k5xphSYv/TtGIbjg/9yGVFqclg4Y/6rrfkApbx36PQEBNxLiRsZ4hGpCfVU6h0jOekk8TV6CAguXVX/G31UqsAEa4sOD2g10Ir+5JD7bdd3JE/999kHGdiCqc0DNcgSqWYbq2QYwrN/mb+mMUbiQSNMcc34kK1n+7dGxppnt7YN7UsJqBWJdH0Lw1Epxi11ViTeVma9bqioJYXi6N5exdpZTT7KmcGYFsoTqO958EX6AppgcML7N9oP3TO8qSgCpV3Bbbemq4bvjV43aM6Rdx17pC4GZo0jjU97p4K8jE4PvgoHlYkuPwSJDOSAdnYPh+Inq/vCk48UfIlup0ATJFVUXD7uf84v9roZSwZPXZ5j/88+MkHBIJwPv8cugmz5uN2EuBW5IScMuEqG7Cmk72SU3/QA39G79S0Gpw7iPhTos5LXxhfvohGcnSaNEvfNeecQf7fpVciTdHwuvcgqJizUKpSFg2P+LDBiO44mJD15RNAaT37Rrj5P06YITO4PDj+FMdc6gx+JQUFbcSRhScE/0gfsVm0P1BYIH5q0k/QDgEVoerf/n19lITTzPib1F2OHP4hyF3BEq1pd9NwuPhhsVVqTVTK5MzFwFIOH7cwJyY7aBykmsWBavdb2J7UA5wjKqMHl1auUGPlNL+lZjqG4tw05bchtFAF+PGWQXJhJCtRSkkzTOCrLRyYyyI9mWYEjoc23cGLanlIs7WA1Nd0Jz+5RSNlf9Gtnd65yQp/W1eqY6yzURPHUUa7FrynyORmjaR9adT9utSQkXy8++IeDNzhMtFr+SqQ/gKECLe0GeuyTs6E5bImUtqpN+xopBXnEeq8wp+bvLf76d98qPE5ibTRwlsSyCE4c1Y7vrJrlc15Yc2R9ciIuKUS8rUKLSdGBFe/TD4R3cPhCKAnnRLGWnJiPPgxoTVwHVZMISdsAjNaWblBmiAOzFcu7443d3PCLyXVcfR9xgvW51HTumo91t5Qyx4HIXGoZxayZYFm2hrhSlieUqLnDL2j2gYgGU5NGoQl4OnEY2QqobpRUF4xJ4HhLzYbLrBeXmTDPvj0MasC3kKsRlm/HrsRRWZ2iPSMw9601tLvDfyjG53ddPISiVNnkdXcaAN5np7dwipdBOC1s4a0sEmKakNbkkDb8LsGBNte/g6UYs5yYaKr0bnXlDjMCznHQa7pypBjE7S55T3UeRpwo3IvZ1tfIGdb+z9RIA/PDvUksxJ3Xq3lqtZzkZJF5aeedfIOekGS/G0LiCSYsELgRceH5veknHqoGoL6xi4Q6/VjmfpZVXT19bDcTNtaR9Dlaq4LDjpQl9rl5C3O/X1hgADvJUuINCiLrD114sLY1DG/TDXE0sp+TK7utnjLAoHuAuj+6anY5vN66CSbwyUNmvo+m8li/AMkRYdtSDoPWkV7Y1ixMBPcua0Llwn2HSKKwnCjvhDIDIIVwbWwb1s6b9cztH81WF5RWUgFujewPvTElM1Sy10y7BcZohKw28uLRFVsKunc9yX2PiQoTSB4PHBHRA4U5dEQV3GHQJ93nee7VT3oeQPMVebWhuhOhi34Z33LQajzpCF3OjIbJb0tOPP6L6N/ODqkNsYViI3kgCnkNhexadOuGFWIqen2Q8iv2uOZWbPirt0YEeKZIk2dpND07L8Q3OsoQCk2rjpnw9LuFrjgu7gN9gFyPq25HJRBn7PM/lS60DF+xVkJq94PwN+CiZWC43SVcBGx65DFZIs/N78MZCUzZbFlsS7FsIrDJt878cp9eZdq/Ai4LZhL8QYHpVUrQxRxZGSqooA755N6nOxw66JkA1VPnjECCMgoNNtWox0JzhMe8PBdh2ZliXf8yQ6/eTvsG6FD84F+49pc7m0L99pfWHb9ClyO3KRHscp/MOIC1MJmqoB4dNxV20U+z8/lSTIvcmM8DiaAZj/yxlst90drlGydlyPjQzYd/XtIYcO5gHoeD1KUCZRapE5dkyk5vh97WZJn/JkR8hsslU3D6x3rNGwJbQVRu0IiA3PpeAQNZBNAJHHfv8IzIYxPhMJdYq0YqLIGSUYu87D04cDOxJY7hgawYs+ExOWb7XkbpuRoITQd8zpwVDFlSCS+wFO+qah3Vn8RBTc6cXHO5xRWfUNj+NrEtPdVmax+9EXqXtHQyFpxaauvL96RH+mGwpKHOk3aisXbZ6gLE2mF4egGjjJOIJdHyb2ZR+kj+4GIvkoBwipDgUfr4UBXY8pvFxQOxRgtI4LgOY9Z1Aco7Mwp6qi1KoMFJW8d+gJwsgM3cMsyEeYH1n/mdpJW6VDbIWzOHkP5n+OKKNm2vJTkQFFwF9eOtGy9fNBtS4qo4jvOUJnnAPsrPbGMbBYd1dMC3daHLEwvIKCAVBn7q1Z2c4zAD5eEoY0EwZj/j8x8lGQ8TswFT81ZotW7ZBDai/YtV8mkGfuaWJRI5yHc/bV7GWLF+yrMji/jicBF5jy2UoqwxseqjgTut49FRgBH3h1qwnfYbXD3FvQljyAAgBCiZV726pFRG+sZv0FjDbq0iCKILVSEUDZgmQ",
|
||||
"shared":true,
|
||||
"uploaded_signed_key_count":50
|
||||
|
||||
},
|
||||
"sessions":[
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4s+SdrKUYAMUdGcYD7QukrPklEOy7fJho9YGK/jV04QdA8JABiOfD+ngJTR4V8eZdmDuG08+Q5EL79V81hQwU2fKndP0y/9nAXPUIADYq0Zrg4EsOnXz7aE+hAeBAm0IBog1s8RYUvynZ15uwjbd/OTLP+gpqpX33DwVg2leiBkQetiUSpOpZCuQ8CcZwIA0MoGCqvaT7h76VHX9JxJx+2fCMhsJMx1nhd99WJH1W9ge5CtdbC4KUP92OSxIrPOnMrNcOPJPp/paZP+HFNQ3PDL+z8pGKXmCnrXGSbd7iPHurPYESrVkBzr",
|
||||
"sender_key":"WJ6Ce7U67a6jqkHYHd8o0+5H4bqdi9hInZdk0+swuXs",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649425011424u64,
|
||||
"last_use_time": 1649425011424u64
|
||||
"creation_time":"1649425011424",
|
||||
"last_use_time":"1649425011424"
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4t2W/lowyrV6SXVZp+uG59im0AAfNSKjhjZuiOpQlX7MS+AOJkCNvyujJ2g3KSjLZ94IkoHxkBDHLWSjwaLPu40rfOzJPDpm0XZsR6bQrsxKOmXLGEw2qw5jOTouzMVL2gvuuTix97nSYSU8j3XvTMRUoh0AF/tUpRLcvEFZeGrdUYmTMlyTv4na+FVUalUZ+jrk8t1/sM99JNq3SY1IBSjrBq/0rCOHieiippz0sw2fe2b87id4rqj1g3R9w2MWTWEdOz3ugjMGYF1YDBQZA1tJZ/hmgppk2AU2xKQXE2X3DgSC6fC66D4",
|
||||
"sender_key":"RzRROfmHNlBfzxnNCUYBfn/5oZNQ11XYjDg59hS+mV0",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649425011503u64,
|
||||
"last_use_time": 1649425011503u64
|
||||
"creation_time":"1649425011503",
|
||||
"last_use_time":"1649425011503"
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4titbL3SS12PYHpcBPJc6hXnOnZXqrjtjYOD545fck+3utEo8cqqwWubc9tsvxGW3tOWPttLBdAW30Vn8V1M8ebqVCNVWEAb1GKjV4ni8xG7G9SlEcCjLjnF4lJpddSZkqVMFoN0ITr9aSz/eJwXpc3HLreUFXwc8LuQp7krQ4Vt1e5EE/klduqsdurZf5V14RHsmWz2lKjt7nVgtIz/dhtF5F/sGJdg8kCGaHIMSbGAPuPPpa4/Laicb/5otrYt4pg4W4KdFpSGJIcvUQNjXaOZMx3cu/RPJIOyNhx7whG1QiYAUBqAJvr",
|
||||
"sender_key":"IXSZugAHig1v8MowE1jxi2wDDDfuZBeJynHlegJVwUc",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649425011566u64,
|
||||
"last_use_time": 1649425011566u64
|
||||
"creation_time":"1649425011566",
|
||||
"last_use_time":"1649425011566"
|
||||
},
|
||||
{
|
||||
"pickle":"SmkDiFZjNukiarQ7XHQo25FILHsuhNOnxy56cMSQU/Y71jaGbJes4YrvN4Dfy4RSONfejEDXDkbW2JudlHHRP/rWEmnfJiGbK6ArbrG2puqIZgOecPnOUgPfCisr49p1Gmf36dPaO5lm/ZSrngfSoxahoeJJE/CcJN98sYM15XytRk2LBwc+CyYDqr4V1qxfsBt6tzJ4+tsAZeRdD0UtipQgysgH56o8N7nKTCkaZz5lfpYCl3FEgwXpLJ0MGQvtQmbORFvOLqR1jZ/EbmNGKiqDDIYsqG0sf78ii1jqfpLDBXLuYDccsg",
|
||||
"sender_key":"EB9SC4jVAydKhM6/GcwMc9biKwVNywqW3TerNTrtb1M",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time": 1649542063182u64,
|
||||
"last_use_time": 1649542063182u64
|
||||
"creation_time":"1649542063182",
|
||||
"last_use_time":"1649542063182"
|
||||
}
|
||||
],
|
||||
"inbound_group_sessions":[
|
||||
@@ -1119,8 +947,7 @@ mod tests {
|
||||
"@ganfra146:matrix.org",
|
||||
"@this-is-me:matrix.org",
|
||||
"@Amandine:matrix.org",
|
||||
"@ganfra:matrix.org",
|
||||
"NotAUser%ID"
|
||||
"@ganfra:matrix.org"
|
||||
],
|
||||
"room_settings": {
|
||||
"!AZkqtjvtwPAuyNOXEt:matrix.org": {
|
||||
@@ -1145,20 +972,14 @@ mod tests {
|
||||
|
||||
migrate(migration_data, path.clone(), None, Box::new(|_, _| {}))?;
|
||||
|
||||
let machine = OlmMachine::new(
|
||||
"@ganfra146:matrix.org".to_owned(),
|
||||
"DEWRCMENGS".to_owned(),
|
||||
path,
|
||||
None,
|
||||
)?;
|
||||
let machine = OlmMachine::new("@ganfra146:matrix.org", "DEWRCMENGS", &path, None)?;
|
||||
|
||||
assert_eq!(
|
||||
machine.identity_keys()["ed25519"],
|
||||
"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
|
||||
);
|
||||
|
||||
let room_keys =
|
||||
machine.runtime.block_on(machine.inner.store().export_room_keys(|_| true))?;
|
||||
let room_keys = machine.runtime.block_on(machine.inner.export_room_keys(|_| true))?;
|
||||
assert_eq!(room_keys.len(), 2);
|
||||
|
||||
let cross_signing_status = machine.cross_signing_status();
|
||||
@@ -1190,11 +1011,6 @@ mod tests {
|
||||
let settings3 = machine.get_room_settings("!XYZ:matrix.org".into())?;
|
||||
assert!(settings3.is_none());
|
||||
|
||||
assert!(machine.is_user_tracked("@ganfra146:matrix.org".into()).unwrap());
|
||||
assert!(machine.is_user_tracked("@Amandine:matrix.org".into()).unwrap());
|
||||
assert!(machine.is_user_tracked("@this-is-me:matrix.org".into()).unwrap());
|
||||
assert!(machine.is_user_tracked("@ganfra:matrix.org".into()).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait Logger: Send {
|
||||
/// Called every time the Rust side wants to post a log line.
|
||||
fn log(&self, log_line: String);
|
||||
@@ -42,7 +41,7 @@ pub struct LoggerWrapper {
|
||||
}
|
||||
|
||||
/// Set the logger that should be used to forward Rust logs over FFI.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
pub fn set_logger(logger: Box<dyn Logger>) {
|
||||
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@ use matrix_sdk_crypto::{
|
||||
},
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
store::{Changes, RecoveryKey},
|
||||
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -38,12 +37,10 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, MessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
@@ -51,7 +48,6 @@ use tokio::runtime::Runtime;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError},
|
||||
parse_user_id,
|
||||
responses::{response_from_string, OwnedResponse},
|
||||
@@ -62,53 +58,7 @@ use crate::{
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
};
|
||||
|
||||
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
|
||||
///
|
||||
/// Will contain various information about the `/sync` changes the
|
||||
/// [`OlmMachine`] processed.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SyncChangesResult {
|
||||
/// The, now possibly decrypted, to-device events the [`OlmMachine`]
|
||||
/// received, decrypted, and processed.
|
||||
to_device_events: Vec<String>,
|
||||
|
||||
/// Information about the room keys that were extracted out of the to-device
|
||||
/// events.
|
||||
room_key_infos: Vec<RoomKeyInfo>,
|
||||
}
|
||||
|
||||
/// Information on a room key that has been received or imported.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomKeyInfo {
|
||||
/// The [messaging algorithm] that this key is used for. Will be one of the
|
||||
/// `m.megolm.*` algorithms.
|
||||
///
|
||||
/// [messaging algorithm]: https://spec.matrix.org/v1.6/client-server-api/#messaging-algorithms
|
||||
pub algorithm: String,
|
||||
|
||||
/// The room where the key is used.
|
||||
pub room_id: String,
|
||||
|
||||
/// The Curve25519 key of the device which initiated the session originally.
|
||||
pub sender_key: String,
|
||||
|
||||
/// The ID of the session that the key is for.
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::types::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::types::RoomKeyInfo) -> Self {
|
||||
Self {
|
||||
algorithm: value.algorithm.to_string(),
|
||||
room_id: value.room_id.to_string(),
|
||||
sender_key: value.sender_key.to_base64(),
|
||||
session_id: value.session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A high level state machine that handles E2EE for Matrix.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct OlmMachine {
|
||||
pub(crate) inner: ManuallyDrop<InnerMachine>,
|
||||
pub(crate) runtime: Runtime,
|
||||
@@ -116,15 +66,14 @@ pub struct OlmMachine {
|
||||
|
||||
impl Drop for OlmMachine {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::take to be used safely.
|
||||
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
|
||||
let _guard = self.runtime.enter();
|
||||
// Dropping the inner OlmMachine must happen within a tokio context
|
||||
// because deadpool drops sqlite connections in the DB pool on tokio's
|
||||
// blocking threadpool to avoid blocking async worker threads.
|
||||
let _guard = self.runtime.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
drop(inner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +128,6 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
@@ -192,17 +140,16 @@ impl OlmMachine {
|
||||
/// * `path` - The path where the state of the machine should be persisted.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data
|
||||
/// at rest in the crypto store. **Warning**, if no passphrase is given,
|
||||
/// the store and all its data will remain unencrypted.
|
||||
#[uniffi::constructor]
|
||||
/// at rest in the Sled store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
pub fn new(
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
path: String,
|
||||
user_id: &str,
|
||||
device_id: &str,
|
||||
path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
) -> Result<Arc<Self>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
) -> Result<Self, CryptoStoreError> {
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
let device_id = device_id.into();
|
||||
let runtime = Runtime::new().expect("Couldn't create a tokio runtime");
|
||||
|
||||
let store = runtime
|
||||
@@ -210,16 +157,44 @@ impl OlmMachine {
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
let inner = runtime.block_on(InnerMachine::with_store(
|
||||
&user_id,
|
||||
device_id,
|
||||
Arc::new(store),
|
||||
None,
|
||||
))?;
|
||||
let inner =
|
||||
runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?;
|
||||
|
||||
Ok(Arc::new(OlmMachine { inner: ManuallyDrop::new(inner), runtime }))
|
||||
Ok(OlmMachine { inner: ManuallyDrop::new(inner), runtime })
|
||||
}
|
||||
|
||||
fn import_room_keys_helper(
|
||||
&self,
|
||||
keys: Vec<ExportedRoomKey>,
|
||||
from_backup: bool,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
|
||||
Ok(KeysImportResult {
|
||||
imported: result.imported_count as i64,
|
||||
total: result.total_count as i64,
|
||||
keys: result
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(|(r, m)| {
|
||||
(
|
||||
r.to_string(),
|
||||
m.into_iter().map(|(s, k)| (s, k.into_iter().collect())).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl OlmMachine {
|
||||
/// Get the user ID of the owner of this `OlmMachine`.
|
||||
pub fn user_id(&self) -> String {
|
||||
self.inner.user_id().to_string()
|
||||
@@ -253,12 +228,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `user_id` - The unique id of the user that the identity belongs to
|
||||
///
|
||||
/// * `timeout` - The time in seconds we should wait before returning if the
|
||||
/// user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being
|
||||
/// processed and sent out. Namely, this waits for a `/keys/query`
|
||||
/// response to be received.
|
||||
/// * `timeout` - The time in seconds we should wait before returning if
|
||||
/// the user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
|
||||
/// and sent out. Namely, this waits for a `/keys/query` response to be
|
||||
/// received.
|
||||
pub fn get_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -287,7 +262,10 @@ impl OlmMachine {
|
||||
if let Some(identity) =
|
||||
self.runtime.block_on(self.inner.get_identity(&user_id, None))?
|
||||
{
|
||||
identity.is_verified()
|
||||
match identity {
|
||||
UserIdentities::Own(i) => i.is_verified(),
|
||||
UserIdentities::Other(i) => i.is_verified(),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
},
|
||||
@@ -315,8 +293,8 @@ impl OlmMachine {
|
||||
|
||||
if let Some(user_identity) = user_identity {
|
||||
Ok(match user_identity {
|
||||
SdkUserIdentity::Own(i) => self.runtime.block_on(i.verify())?,
|
||||
SdkUserIdentity::Other(i) => self.runtime.block_on(i.verify())?,
|
||||
UserIdentities::Own(i) => self.runtime.block_on(i.verify())?,
|
||||
UserIdentities::Other(i) => self.runtime.block_on(i.verify())?,
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
@@ -332,12 +310,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `device_id` - The id of the device itself.
|
||||
///
|
||||
/// * `timeout` - The time in seconds we should wait before returning if the
|
||||
/// user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being
|
||||
/// processed and sent out. Namely, this waits for a `/keys/query`
|
||||
/// response to be received.
|
||||
/// * `timeout` - The time in seconds we should wait before returning if
|
||||
/// the user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
|
||||
/// and sent out. Namely, this waits for a `/keys/query` response to be
|
||||
/// received.
|
||||
pub fn get_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -354,7 +332,7 @@ impl OlmMachine {
|
||||
.map(|d| d.into()))
|
||||
}
|
||||
|
||||
/// Manually verify the device of the given user with the given device ID.
|
||||
/// Manually the device of the given user with the given device ID.
|
||||
///
|
||||
/// This method will attempt to sign the device using our private cross
|
||||
/// signing key.
|
||||
@@ -415,12 +393,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `user_id` - The id of the device owner.
|
||||
///
|
||||
/// * `timeout` - The time in seconds we should wait before returning if the
|
||||
/// user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being
|
||||
/// processed and sent out. Namely, this waits for a `/keys/query`
|
||||
/// response to be received.
|
||||
/// * `timeout` - The time in seconds we should wait before returning if
|
||||
/// the user's device list has been marked as stale. Passing a 0 as the
|
||||
/// timeout means that we won't wait at all. **Note**, this assumes that
|
||||
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
|
||||
/// and sent out. Namely, this waits for a `/keys/query` response to be
|
||||
/// received.
|
||||
pub fn get_user_devices(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -442,7 +420,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// After the request was sent out and a successful response was received
|
||||
/// the response body should be passed back to the state machine using the
|
||||
/// [mark_request_as_sent()](Self::mark_request_as_sent) method.
|
||||
/// [mark_request_as_sent()](#method.mark_request_as_sent) method.
|
||||
///
|
||||
/// **Note**: This method call should be locked per call.
|
||||
pub fn outgoing_requests(&self) -> Result<Vec<Request>, CryptoStoreError> {
|
||||
@@ -516,7 +494,7 @@ impl OlmMachine {
|
||||
/// current sync response.
|
||||
///
|
||||
/// * `device_changes` - The list of devices that have changed in some way
|
||||
/// since the previous sync.
|
||||
/// since the previous sync.
|
||||
///
|
||||
/// * `key_counts` - The map of uploaded one-time key types and counts.
|
||||
pub fn receive_sync_changes(
|
||||
@@ -525,15 +503,14 @@ impl OlmMachine {
|
||||
device_changes: DeviceLists,
|
||||
key_counts: HashMap<String, i32>,
|
||||
unused_fallback_keys: Option<Vec<String>>,
|
||||
next_batch_token: String,
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
let key_counts: BTreeMap<OneTimeKeyAlgorithm, UInt> = key_counts
|
||||
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
OneTimeKeyAlgorithm::from(k),
|
||||
DeviceKeyAlgorithm::from(k),
|
||||
v.clamp(0, i32::MAX)
|
||||
.try_into()
|
||||
.expect("Couldn't convert key counts into an UInt"),
|
||||
@@ -541,26 +518,17 @@ impl OlmMachine {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
}),
|
||||
)?;
|
||||
let events = self.runtime.block_on(self.inner.receive_sync_changes(
|
||||
to_device.events,
|
||||
&device_changes,
|
||||
&key_counts,
|
||||
unused_fallback_keys.as_deref(),
|
||||
))?;
|
||||
|
||||
let to_device_events = to_device_events
|
||||
.into_iter()
|
||||
.map(|event| event.to_raw().json().get().to_owned())
|
||||
.collect();
|
||||
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
|
||||
|
||||
Ok(SyncChangesResult { to_device_events, room_key_infos })
|
||||
Ok(serde_json::to_string(&events)?)
|
||||
}
|
||||
|
||||
/// Add the given list of users to be tracked, triggering a key query
|
||||
@@ -573,7 +541,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// *Note*: Only users that aren't already tracked will be considered for an
|
||||
/// update. It's safe to call this with already tracked users, it won't
|
||||
/// result in excessive `/keys/query` requests.
|
||||
/// result in excessive keys query requests.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -601,15 +569,15 @@ impl OlmMachine {
|
||||
///
|
||||
/// After the request was sent out and a successful response was received
|
||||
/// the response body should be passed back to the state machine using the
|
||||
/// [mark_request_as_sent()](Self::mark_request_as_sent) method.
|
||||
/// [mark_request_as_sent()](#method.mark_request_as_sent) method.
|
||||
///
|
||||
/// This method should be called every time before a call to
|
||||
/// [`share_room_key()`](Self::share_room_key) is made.
|
||||
/// [`share_room_key()`](#method.share_room_key) is made.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users` - The list of users for which we would like to establish 1:1
|
||||
/// Olm sessions for.
|
||||
/// Olm sessions for.
|
||||
pub fn get_missing_sessions(
|
||||
&self,
|
||||
users: Vec<String>,
|
||||
@@ -627,9 +595,9 @@ impl OlmMachine {
|
||||
/// whether to encrypt only for trusted devices.
|
||||
///
|
||||
/// These settings can be modified via
|
||||
/// [set_room_algorithm()](Self::set_room_algorithm) and
|
||||
/// [set_room_only_allow_trusted_devices()](Self::set_room_only_allow_trusted_devices)
|
||||
/// methods.
|
||||
/// [set_room_algorithm()](#method.set_room_algorithm) and
|
||||
/// [set_room_only_allow_trusted_devices()](#method.
|
||||
/// set_room_only_allow_trusted_devices) methods.
|
||||
pub fn get_room_settings(
|
||||
&self,
|
||||
room_id: String,
|
||||
@@ -670,8 +638,8 @@ impl OlmMachine {
|
||||
/// devices, or whether they should be excluded from the conversation.
|
||||
///
|
||||
/// Note that per-room setting may be overridden by a global
|
||||
/// [set_only_allow_trusted_devices()](Self::set_only_allow_trusted_devices)
|
||||
/// method.
|
||||
/// [set_only_allow_trusted_devices()](#method.
|
||||
/// set_only_allow_trusted_devices) method.
|
||||
pub fn set_room_only_allow_trusted_devices(
|
||||
&self,
|
||||
room_id: String,
|
||||
@@ -722,19 +690,19 @@ impl OlmMachine {
|
||||
///
|
||||
/// After the request was sent out and a successful response was received
|
||||
/// the response body should be passed back to the state machine using the
|
||||
/// [mark_request_as_sent()](Self::mark_request_as_sent) method.
|
||||
/// [mark_request_as_sent()](#method.mark_request_as_sent) method.
|
||||
///
|
||||
/// This method should be called every time before a call to
|
||||
/// [`encrypt()`](Self::encrypt) with the given `room_id` is made.
|
||||
/// [`encrypt()`](#method.encrypt) with the given `room_id` is made.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The unique id of the room, note that this doesn't strictly
|
||||
/// need to be a Matrix room, it just needs to be an unique identifier for
|
||||
/// the group that will participate in the conversation.
|
||||
/// need to be a Matrix room, it just needs to be an unique identifier for
|
||||
/// the group that will participate in the conversation.
|
||||
///
|
||||
/// * `users` - The list of users which are considered to be members of the
|
||||
/// room and should receive the room key.
|
||||
/// room and should receive the room key.
|
||||
///
|
||||
/// * `settings` - The settings that should be used for the room key.
|
||||
pub fn share_room_key(
|
||||
@@ -767,12 +735,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// 1. Get the one-time key claim request to establish 1:1 Olm sessions for
|
||||
/// the room members of the room we wish to participate in. This is done
|
||||
/// using the [`get_missing_sessions()`](Self::get_missing_sessions)
|
||||
/// using the [`get_missing_sessions()`](#method.get_missing_sessions)
|
||||
/// method. This method call should be locked per call.
|
||||
///
|
||||
/// 2. Share a room key with all the room members using the
|
||||
/// [`share_room_key()`](Self::share_room_key). This method call should
|
||||
/// be locked per room.
|
||||
/// [`share_room_key()`](#method.share_room_key). This method
|
||||
/// call should be locked per room.
|
||||
///
|
||||
/// 3. Encrypt the event using this method.
|
||||
///
|
||||
@@ -796,63 +764,16 @@ impl OlmMachine {
|
||||
content: String,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let content = serde_json::from_str(&content)?;
|
||||
let content: Value = serde_json::from_str(&content)?;
|
||||
|
||||
let encrypted_content = self
|
||||
.runtime
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type))
|
||||
.expect("Encrypting an event produced an error");
|
||||
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
}
|
||||
|
||||
/// Encrypt the given event with the given type and content for the given
|
||||
/// device. This method is used to send an event to a specific device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user who owns the target device.
|
||||
/// * `device_id` - The ID of the device to which the message will be sent.
|
||||
/// * `event_type` - The event type.
|
||||
/// * `content` - The serialized content of the event.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result` containing the request to be sent out if the encryption was
|
||||
/// successful. If the device is not found, the result will be `Ok(None)`.
|
||||
///
|
||||
/// The caller should ensure that there is an olm session (see
|
||||
/// `get_missing_sessions`) with the target device before calling this
|
||||
/// method.
|
||||
pub fn create_encrypted_to_device_request(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
event_type: String,
|
||||
content: String,
|
||||
) -> Result<Option<Request>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
let encrypted_content =
|
||||
self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id.as_ref(),
|
||||
DeviceIdOrAllDevices::DeviceId(device_id.to_owned()),
|
||||
"m.room.encrypted",
|
||||
encrypted_content.cast(),
|
||||
);
|
||||
|
||||
Ok(Some(request.into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt the given event that was sent in the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -864,14 +785,12 @@ impl OlmMachine {
|
||||
/// * `strict_shields` - If `true`, messages will be decorated with strict
|
||||
/// warnings (use `false` to match legacy behaviour where unsafe keys have
|
||||
/// lower severity warnings and unverified identities are not decorated).
|
||||
/// * `decryption_settings` - The setting for decrypting messages.
|
||||
pub fn decrypt_room_event(
|
||||
&self,
|
||||
event: String,
|
||||
room_id: String,
|
||||
handle_verification_events: bool,
|
||||
strict_shields: bool,
|
||||
decryption_settings: DecryptionSettings,
|
||||
) -> Result<DecryptedEvent, DecryptionError> {
|
||||
// Element Android wants only the content and the type and will create a
|
||||
// decrypted event with those two itself, this struct makes sure we
|
||||
@@ -887,14 +806,10 @@ impl OlmMachine {
|
||||
let event: Raw<_> = serde_json::from_str(&event)?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(
|
||||
&event,
|
||||
&room_id,
|
||||
&decryption_settings,
|
||||
))?;
|
||||
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?;
|
||||
|
||||
if handle_verification_events {
|
||||
if let Ok(e) = decrypted.event.deserialize() {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
@@ -912,30 +827,26 @@ impl OlmMachine {
|
||||
}
|
||||
}
|
||||
|
||||
let encryption_info = decrypted.encryption_info;
|
||||
let encryption_info =
|
||||
decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info");
|
||||
|
||||
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
|
||||
|
||||
Ok(match &encryption_info.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key,
|
||||
sender_claimed_keys,
|
||||
session_id: _,
|
||||
} => DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519).cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
},
|
||||
AlgorithmInfo::OlmV1Curve25519AesSha2 { .. } => {
|
||||
// cannot happen because `decrypt_room_event` would have fail to decrypt olm for
|
||||
// a room (EventError::UnsupportedAlgorithm)
|
||||
panic!("Unsupported olm algorithm in room")
|
||||
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys } => {
|
||||
DecryptedEvent {
|
||||
clear_event: serde_json::to_string(&event_json)?,
|
||||
sender_curve25519_key: curve25519_key.to_owned(),
|
||||
claimed_ed25519_key: sender_claimed_keys
|
||||
.get(&DeviceKeyAlgorithm::Ed25519)
|
||||
.cloned(),
|
||||
forwarding_curve25519_chain: vec![],
|
||||
shield_state: if strict_shields {
|
||||
encryption_info.verification_state.to_shield_state_strict().into()
|
||||
} else {
|
||||
encryption_info.verification_state.to_shield_state_lax().into()
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -946,7 +857,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The undecryptable event that we would wish to request a room
|
||||
/// key for.
|
||||
/// key for.
|
||||
///
|
||||
/// * `room_id` - The id of the room the event was sent to.
|
||||
pub fn request_room_key(
|
||||
@@ -971,16 +882,16 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the key
|
||||
/// export.
|
||||
/// export.
|
||||
///
|
||||
/// * `rounds` - The number of rounds that should be used when expanding the
|
||||
/// passphrase into an key.
|
||||
/// passphrase into an key.
|
||||
pub fn export_room_keys(
|
||||
&self,
|
||||
passphrase: String,
|
||||
rounds: i32,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let keys = self.runtime.block_on(self.inner.store().export_room_keys(|_| true))?;
|
||||
let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?;
|
||||
|
||||
let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32)
|
||||
.map_err(CryptoStoreError::Serialization)?;
|
||||
@@ -997,7 +908,7 @@ impl OlmMachine {
|
||||
/// * `passphrase` - The passphrase that was used to encrypt the key export.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
/// progress of the key import.
|
||||
pub fn import_room_keys(
|
||||
&self,
|
||||
keys: String,
|
||||
@@ -1006,7 +917,7 @@ impl OlmMachine {
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys = Cursor::new(keys);
|
||||
let keys = decrypt_room_key_export(keys, &passphrase)?;
|
||||
self.import_room_keys_helper(keys, None, progress_listener)
|
||||
self.import_room_keys_helper(keys, false, progress_listener)
|
||||
}
|
||||
|
||||
/// Import room keys from the given serialized unencrypted key export.
|
||||
@@ -1016,52 +927,22 @@ impl OlmMachine {
|
||||
/// should be used if the room keys are coming from the server-side backup,
|
||||
/// the method will mark all imported room keys as backed up.
|
||||
///
|
||||
/// **Note**: This has been deprecated. Use
|
||||
/// [`OlmMachine::import_room_keys_from_backup`] instead.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - The serialized version of the unencrypted key export.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
/// progress of the key import.
|
||||
pub fn import_decrypted_room_keys(
|
||||
&self,
|
||||
keys: String,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
// Assume that the keys came from the current backup version.
|
||||
let backup_version = self.runtime.block_on(self.inner.backup_machine().backup_version());
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
self.import_room_keys_helper(keys, backup_version.as_deref(), progress_listener)
|
||||
}
|
||||
|
||||
/// Import room keys from the given serialized unencrypted key export.
|
||||
///
|
||||
/// This method is the same as [`OlmMachine::import_room_keys`] but the
|
||||
/// decryption step is skipped and should be performed by the caller. This
|
||||
/// should be used if the room keys are coming from the server-side backup.
|
||||
/// The method will mark all imported room keys as backed up.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - The serialized version of the unencrypted key export.
|
||||
///
|
||||
/// * `backup_version` - The version of the backup that these keys came
|
||||
/// from.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
pub fn import_room_keys_from_backup(
|
||||
&self,
|
||||
keys: String,
|
||||
backup_version: String,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
self.import_room_keys_helper(keys, Some(&backup_version), progress_listener)
|
||||
|
||||
self.import_room_keys_helper(keys, true, progress_listener)
|
||||
}
|
||||
|
||||
/// Discard the currently active room key for the given room if there is
|
||||
@@ -1069,7 +950,7 @@ impl OlmMachine {
|
||||
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
self.runtime.block_on(self.inner.discard_room_key(&room_id))?;
|
||||
self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1112,7 +993,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to fetch the
|
||||
/// verification requests.
|
||||
/// verification requests.
|
||||
pub fn get_verification_requests(&self, user_id: String) -> Vec<Arc<VerificationRequest>> {
|
||||
let Ok(user_id) = UserId::parse(user_id) else {
|
||||
return vec![];
|
||||
@@ -1133,7 +1014,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to fetch the
|
||||
/// verification requests.
|
||||
/// verification requests.
|
||||
///
|
||||
/// * `flow_id` - The ID that uniquely identifies the verification flow.
|
||||
pub fn get_verification_request(
|
||||
@@ -1153,10 +1034,10 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user which we would like to request to
|
||||
/// verify.
|
||||
/// verify.
|
||||
///
|
||||
/// * `methods` - The list of verification methods we want to advertise to
|
||||
/// support.
|
||||
/// support.
|
||||
pub fn verification_request_content(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1169,7 +1050,8 @@ impl OlmMachine {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
|
||||
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
|
||||
let content = identity.verification_request_content(Some(methods));
|
||||
let content =
|
||||
self.runtime.block_on(identity.verification_request_content(Some(methods)));
|
||||
Some(serde_json::to_string(&content)?)
|
||||
} else {
|
||||
None
|
||||
@@ -1182,20 +1064,20 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user which we would like to request to
|
||||
/// verify.
|
||||
/// verify.
|
||||
///
|
||||
/// * `room_id` - The ID of the room that represents a DM with the given
|
||||
/// user.
|
||||
/// user.
|
||||
///
|
||||
/// * `event_id` - The event ID of the `m.key.verification.request` event
|
||||
/// that we sent out to request the verification to begin. The content for
|
||||
/// this request can be created using the [verification_request_content()]
|
||||
/// method.
|
||||
/// that we sent out to request the verification to begin. The content for
|
||||
/// this request can be created using the [verification_request_content()]
|
||||
/// method.
|
||||
///
|
||||
/// * `methods` - The list of verification methods we advertised as
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
///
|
||||
/// [verification_request_content()]: Self::verification_request_content
|
||||
/// [verification_request_content()]: #method.verification_request_content
|
||||
pub fn request_verification(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1212,7 +1094,11 @@ impl OlmMachine {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
|
||||
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
|
||||
let request = identity.request_verification(&room_id, &event_id, Some(methods));
|
||||
let request = self.runtime.block_on(identity.request_verification(
|
||||
&room_id,
|
||||
&event_id,
|
||||
Some(methods),
|
||||
));
|
||||
|
||||
Some(
|
||||
VerificationRequest { inner: request, runtime: self.runtime.handle().to_owned() }
|
||||
@@ -1228,12 +1114,12 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user which we would like to request to
|
||||
/// verify.
|
||||
/// verify.
|
||||
///
|
||||
/// * `device_id` - The ID of the device that we wish to verify.
|
||||
///
|
||||
/// * `methods` - The list of verification methods we advertised as
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
pub fn request_verification_with_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1249,7 +1135,8 @@ impl OlmMachine {
|
||||
if let Some(device) =
|
||||
self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?
|
||||
{
|
||||
let (verification, request) = device.request_verification_with_methods(methods);
|
||||
let (verification, request) =
|
||||
self.runtime.block_on(device.request_verification_with_methods(methods));
|
||||
|
||||
Some(RequestVerificationResult {
|
||||
verification: VerificationRequest {
|
||||
@@ -1302,7 +1189,7 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to fetch the
|
||||
/// verification.
|
||||
/// verification.
|
||||
///
|
||||
/// * `flow_id` - The ID that uniquely identifies the verification flow.
|
||||
pub fn get_verification(&self, user_id: String, flow_id: String) -> Option<Arc<Verification>> {
|
||||
@@ -1322,11 +1209,11 @@ impl OlmMachine {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// SAS verification.
|
||||
/// SAS verification.
|
||||
///
|
||||
/// * `device_id` - The ID of device we would like to verify.
|
||||
///
|
||||
/// [request_verification_with_device()]: Self::request_verification_with_device
|
||||
/// [request_verification_with_device()]: #method.request_verification_with_device
|
||||
pub fn start_sas_with_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1342,8 +1229,7 @@ impl OlmMachine {
|
||||
let (sas, request) = self.runtime.block_on(device.start_verification())?;
|
||||
|
||||
Some(StartSasResult {
|
||||
sas: Sas { inner: Box::new(sas), runtime: self.runtime.handle().to_owned() }
|
||||
.into(),
|
||||
sas: Sas { inner: sas, runtime: self.runtime.handle().to_owned() }.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
@@ -1365,10 +1251,8 @@ impl OlmMachine {
|
||||
///
|
||||
/// This method returns `None` if we don't have any private cross signing
|
||||
/// keys.
|
||||
pub fn export_cross_signing_keys(
|
||||
&self,
|
||||
) -> Result<Option<CrossSigningKeyExport>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.export_cross_signing_keys())?.map(|e| e.into()))
|
||||
pub fn export_cross_signing_keys(&self) -> Option<CrossSigningKeyExport> {
|
||||
self.runtime.block_on(self.inner.export_cross_signing_keys()).map(|e| e.into())
|
||||
}
|
||||
|
||||
/// Import our private cross signing keys.
|
||||
@@ -1384,16 +1268,6 @@ impl OlmMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request missing local secrets from our devices (cross signing private
|
||||
/// keys, megolm backup). This will ask the sdk to create outgoing
|
||||
/// request to get the missing secrets.
|
||||
///
|
||||
/// The requests will be processed as soon as `outgoing_requests()` is
|
||||
/// called to process them.
|
||||
pub fn query_missing_secrets_from_other_sessions(&self) -> Result<bool, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.query_missing_secrets_from_other_sessions())?)
|
||||
}
|
||||
|
||||
/// Activate the given backup key to be used with the given backup version.
|
||||
///
|
||||
/// **Warning**: The caller needs to make sure that the given `BackupKey` is
|
||||
@@ -1458,12 +1332,12 @@ impl OlmMachine {
|
||||
// We need to clone here due to FFI limitations but RecoveryKey does
|
||||
// not want to expose clone since it's private key material.
|
||||
let mut encoded = k.to_base64();
|
||||
let key = BackupDecryptionKey::from_base64(&encoded)
|
||||
let key = RecoveryKey::from_base64(&encoded)
|
||||
.expect("Encoding and decoding from base64 should always work");
|
||||
encoded.zeroize();
|
||||
key
|
||||
});
|
||||
Ok(self.runtime.block_on(self.inner.backup_machine().save_decryption_key(key, version))?)
|
||||
Ok(self.runtime.block_on(self.inner.backup_machine().save_recovery_key(key, version))?)
|
||||
}
|
||||
|
||||
/// Get the backup keys we have saved in our crypto store.
|
||||
@@ -1478,13 +1352,9 @@ impl OlmMachine {
|
||||
|
||||
/// Sign the given message using our device key and if available cross
|
||||
/// signing master key.
|
||||
pub fn sign(
|
||||
&self,
|
||||
message: String,
|
||||
) -> Result<HashMap<String, HashMap<String, String>>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.sign(&message))?
|
||||
pub fn sign(&self, message: String) -> HashMap<String, HashMap<String, String>> {
|
||||
self.runtime
|
||||
.block_on(self.inner.sign(&message))
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
@@ -1502,7 +1372,7 @@ impl OlmMachine {
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if the given backup has been verified by us or by another of our
|
||||
@@ -1531,47 +1401,4 @@ impl OlmMachine {
|
||||
.block_on(self.inner.backup_machine().verify_backup(backup_info, false))?
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Manage dehydrated devices.
|
||||
pub fn dehydrated_devices(&self) -> Arc<DehydratedDevices> {
|
||||
DehydratedDevices {
|
||||
inner: ManuallyDrop::new(self.inner.dehydrated_devices()),
|
||||
runtime: self.runtime.handle().to_owned(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
fn import_room_keys_helper(
|
||||
&self,
|
||||
keys: Vec<ExportedRoomKey>,
|
||||
from_backup_version: Option<&str>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
let result = self.runtime.block_on(self.inner.store().import_room_keys(
|
||||
keys,
|
||||
from_backup_version,
|
||||
listener,
|
||||
))?;
|
||||
|
||||
Ok(KeysImportResult {
|
||||
imported: result.imported_count as i64,
|
||||
total: result.total_count as i64,
|
||||
keys: result
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(|(r, m)| {
|
||||
(
|
||||
r.to_string(),
|
||||
m.into_iter().map(|(s, k)| (s, k.into_iter().collect())).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
namespace matrix_sdk_crypto_ffi {};
|
||||
|
||||
callback interface Logger {
|
||||
void log(string log_line);
|
||||
};
|
||||
|
||||
callback interface ProgressListener {
|
||||
void on_progress(i32 progress, i32 total);
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum CryptoStoreError {
|
||||
"OpenStore",
|
||||
"CryptoStore",
|
||||
"OlmError",
|
||||
"Serialization",
|
||||
"InvalidUserId",
|
||||
"Identifier",
|
||||
};
|
||||
|
||||
dictionary CancelInfo {
|
||||
string cancel_code;
|
||||
string reason;
|
||||
boolean cancelled_by_us;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface SasState {
|
||||
Started();
|
||||
Accepted();
|
||||
KeysExchanged(sequence<i32>? emojis, sequence<i32> decimals);
|
||||
Confirmed();
|
||||
Done();
|
||||
Cancelled(CancelInfo cancel_info);
|
||||
};
|
||||
|
||||
callback interface SasListener {
|
||||
void on_change(SasState state);
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface QrCodeState {
|
||||
Started();
|
||||
Scanned();
|
||||
Confirmed();
|
||||
Reciprocated();
|
||||
Done();
|
||||
Cancelled(CancelInfo cancel_info);
|
||||
};
|
||||
|
||||
callback interface QrCodeListener {
|
||||
void on_change(QrCodeState state);
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface VerificationRequestState {
|
||||
Requested();
|
||||
Ready(sequence<string> their_methods, sequence<string> our_methods);
|
||||
Done();
|
||||
Cancelled(CancelInfo cancel_info);
|
||||
};
|
||||
|
||||
callback interface VerificationRequestListener {
|
||||
void on_change(VerificationRequestState state);
|
||||
};
|
||||
|
||||
enum LocalTrust {
|
||||
"Verified",
|
||||
"BlackListed",
|
||||
"Ignored",
|
||||
"Unset",
|
||||
};
|
||||
|
||||
interface OlmMachine {
|
||||
[Throws=CryptoStoreError]
|
||||
constructor(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string device_id,
|
||||
[ByRef] string path,
|
||||
string? passphrase
|
||||
);
|
||||
};
|
||||
|
||||
enum SignatureState {
|
||||
"Missing",
|
||||
"Invalid",
|
||||
"ValidButNotTrusted",
|
||||
"ValidAndTrusted",
|
||||
};
|
||||
|
||||
interface BackupRecoveryKey {
|
||||
constructor();
|
||||
[Name=from_passphrase]
|
||||
constructor(string passphrase, string salt, i32 rounds);
|
||||
[Name=new_from_passphrase]
|
||||
constructor(string passphrase);
|
||||
[Name=from_base64, Throws=DecodeError]
|
||||
constructor(string key);
|
||||
[Name=from_base58, Throws=DecodeError]
|
||||
constructor(string key);
|
||||
};
|
||||
@@ -4,12 +4,8 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests,
|
||||
IncomingResponse, OutgoingRequest, OutgoingVerificationRequest as SdkVerificationRequest,
|
||||
RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
@@ -74,23 +70,17 @@ impl From<RustUploadSigningKeysRequest> for UploadSigningKeysRequest {
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct BootstrapCrossSigningResult {
|
||||
/// Optional request to upload some device keys alongside.
|
||||
///
|
||||
/// Must be sent first if present, and marked with `mark_request_as_sent`.
|
||||
pub upload_keys_request: Option<Request>,
|
||||
/// Request to upload the signing keys. Must be sent second.
|
||||
pub upload_signing_keys_request: UploadSigningKeysRequest,
|
||||
/// Request to upload the keys signatures, including for the signing keys
|
||||
/// and optionally for the device keys. Must be sent last.
|
||||
pub upload_signature_request: SignatureUploadRequest,
|
||||
pub signature_request: SignatureUploadRequest,
|
||||
}
|
||||
|
||||
impl From<CrossSigningBootstrapRequests> for BootstrapCrossSigningResult {
|
||||
fn from(requests: CrossSigningBootstrapRequests) -> Self {
|
||||
impl From<(RustUploadSigningKeysRequest, RustSignatureUploadRequest)>
|
||||
for BootstrapCrossSigningResult
|
||||
{
|
||||
fn from(requests: (RustUploadSigningKeysRequest, RustSignatureUploadRequest)) -> Self {
|
||||
Self {
|
||||
upload_signing_keys_request: requests.upload_signing_keys_req.into(),
|
||||
upload_keys_request: requests.upload_keys_req.map(Request::from),
|
||||
upload_signature_request: requests.upload_signatures_req.into(),
|
||||
upload_signing_keys_request: requests.0.into(),
|
||||
signature_request: requests.1.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,7 +129,7 @@ pub enum Request {
|
||||
|
||||
impl From<OutgoingRequest> for Request {
|
||||
fn from(r: OutgoingRequest) -> Self {
|
||||
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
|
||||
use matrix_sdk_crypto::OutgoingRequests::*;
|
||||
|
||||
match r.request() {
|
||||
KeysUpload(u) => {
|
||||
@@ -152,7 +142,7 @@ impl From<OutgoingRequest> for Request {
|
||||
Request::KeysUpload {
|
||||
request_id: r.request_id().to_string(),
|
||||
body: serde_json::to_string(&body)
|
||||
.expect("Can't serialize `/keys/upload` request"),
|
||||
.expect("Can't serialize keys upload request"),
|
||||
}
|
||||
}
|
||||
KeysQuery(k) => {
|
||||
@@ -167,6 +157,12 @@ impl From<OutgoingRequest> for Request {
|
||||
},
|
||||
RoomMessage(r) => Request::from(r),
|
||||
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
|
||||
KeysBackup(b) => Request::KeysBackup {
|
||||
request_id: r.request_id().to_string(),
|
||||
version: b.version.to_owned(),
|
||||
rooms: serde_json::to_string(&b.rooms)
|
||||
.expect("Can't serialize keys backup request"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,19 +197,6 @@ impl From<(OwnedTransactionId, KeysClaimRequest)> for Request {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(OwnedTransactionId, KeysBackupRequest)> for Request {
|
||||
fn from(request_tuple: (OwnedTransactionId, KeysBackupRequest)) -> Self {
|
||||
let (request_id, request) = request_tuple;
|
||||
|
||||
Request::KeysBackup {
|
||||
request_id: request_id.to_string(),
|
||||
version: request.version.to_owned(),
|
||||
rooms: serde_json::to_string(&request.rooms)
|
||||
.expect("Can't serialize keys backup request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ToDeviceRequest> for Request {
|
||||
fn from(r: &ToDeviceRequest) -> Self {
|
||||
Request::ToDevice {
|
||||
@@ -224,8 +207,8 @@ impl From<&ToDeviceRequest> for Request {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Box<RoomMessageRequest>> for Request {
|
||||
fn from(r: &Box<RoomMessageRequest>) -> Self {
|
||||
impl From<&RoomMessageRequest> for Request {
|
||||
fn from(r: &RoomMessageRequest) -> Self {
|
||||
Self::RoomMessage {
|
||||
request_id: r.txn_id.to_string(),
|
||||
room_id: r.room_id.to_string(),
|
||||
@@ -341,16 +324,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
fn from(r: &'a OwnedResponse) -> Self {
|
||||
match r {
|
||||
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
|
||||
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities};
|
||||
|
||||
use crate::CryptoStoreError;
|
||||
|
||||
/// Enum representing cross signing identity of our own user or some other
|
||||
/// Enum representing cross signing identities of our own user or some other
|
||||
/// user.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum UserIdentity {
|
||||
@@ -18,8 +18,6 @@ pub enum UserIdentity {
|
||||
user_signing_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
/// The user identity of other users.
|
||||
Other {
|
||||
@@ -29,15 +27,13 @@ pub enum UserIdentity {
|
||||
master_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl UserIdentity {
|
||||
pub(crate) async fn from_rust(i: SdkUserIdentity) -> Result<Self, CryptoStoreError> {
|
||||
pub(crate) async fn from_rust(i: UserIdentities) -> Result<Self, CryptoStoreError> {
|
||||
Ok(match i {
|
||||
SdkUserIdentity::Own(i) => {
|
||||
UserIdentities::Own(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
@@ -48,10 +44,9 @@ impl UserIdentity {
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
user_signing_key: serde_json::to_string(&user_signing)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
SdkUserIdentity::Other(i) => {
|
||||
UserIdentities::Other(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
|
||||
@@ -59,7 +54,6 @@ impl UserIdentity {
|
||||
user_id: i.user_id().to_string(),
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64::{
|
||||
alphabet,
|
||||
engine::{general_purpose, GeneralPurpose},
|
||||
Engine,
|
||||
};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
|
||||
@@ -9,13 +13,15 @@ use matrix_sdk_crypto::{
|
||||
VerificationRequestState as RustVerificationRequestState,
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
|
||||
|
||||
const STANDARD_NO_PAD: GeneralPurpose =
|
||||
GeneralPurpose::new(&alphabet::STANDARD, general_purpose::NO_PAD);
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a SAS
|
||||
/// verification.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SasListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -26,13 +32,9 @@ pub trait SasListener: Send {
|
||||
}
|
||||
|
||||
/// An Enum describing the state the SAS verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SasState {
|
||||
/// The verification has been created, the protocols that should be used
|
||||
/// have been proposed to the other party.
|
||||
Created,
|
||||
/// The verification has been started, the other party proposed the
|
||||
/// protocols that should be used and that can be accepted.
|
||||
/// The verification has been started, the protocols that should be used
|
||||
/// have been proposed and can be accepted.
|
||||
Started,
|
||||
/// The verification has been accepted and both sides agreed to a set of
|
||||
/// protocols that will be used for the verification process.
|
||||
@@ -61,7 +63,6 @@ pub enum SasState {
|
||||
impl From<RustSasState> for SasState {
|
||||
fn from(s: RustSasState) -> Self {
|
||||
match s {
|
||||
RustSasState::Created { .. } => Self::Created,
|
||||
RustSasState::Started { .. } => Self::Started,
|
||||
RustSasState::Accepted { .. } => Self::Accepted,
|
||||
RustSasState::KeysExchanged { emojis, decimals } => Self::KeysExchanged {
|
||||
@@ -82,13 +83,13 @@ pub struct Verification {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl Verification {
|
||||
/// Try to represent the `Verification` as an `Sas` verification object,
|
||||
/// returns `None` if the verification is not a `Sas` verification.
|
||||
pub fn as_sas(&self) -> Option<Arc<Sas>> {
|
||||
if let InnerVerification::SasV1(sas) = &self.inner {
|
||||
Some(Sas { inner: sas.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(Sas { inner: sas.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -98,7 +99,7 @@ impl Verification {
|
||||
/// returns `None` if the verification is not a `QrCode` verification.
|
||||
pub fn as_qr(&self) -> Option<Arc<QrCode>> {
|
||||
if let InnerVerification::QrV1(qr) = &self.inner {
|
||||
Some(QrCode { inner: qr.clone(), runtime: self.runtime.to_owned() }.into())
|
||||
Some(QrCode { inner: qr.to_owned(), runtime: self.runtime.to_owned() }.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -108,11 +109,11 @@ impl Verification {
|
||||
/// The `m.sas.v1` verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Sas {
|
||||
pub(crate) inner: Box<InnerSas>,
|
||||
pub(crate) inner: InnerSas,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl Sas {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -169,8 +170,8 @@ impl Sas {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cancel_code` - The error code for why the verification was cancelled,
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
|
||||
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
|
||||
@@ -276,7 +277,6 @@ impl Sas {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a QrCode
|
||||
/// verification.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrCodeListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -287,7 +287,6 @@ pub trait QrCodeListener: Send {
|
||||
}
|
||||
|
||||
/// An Enum describing the state the QrCode verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum QrCodeState {
|
||||
/// The QR verification has been started.
|
||||
Started,
|
||||
@@ -324,11 +323,11 @@ impl From<QrVerificationState> for QrCodeState {
|
||||
/// verification flow.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct QrCode {
|
||||
pub(crate) inner: Box<InnerQr>,
|
||||
pub(crate) inner: InnerQr,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl QrCode {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -391,8 +390,8 @@ impl QrCode {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cancel_code` - The error code for why the verification was cancelled,
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
/// manual cancellatio usually happens with `m.user` cancel code. The full
|
||||
/// list of cancel codes can be found in the [spec]
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
|
||||
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
|
||||
@@ -417,7 +416,7 @@ impl QrCode {
|
||||
/// decoded on the other side before it can be put through a QR code
|
||||
/// generator.
|
||||
pub fn generate_qr_code(&self) -> Option<String> {
|
||||
self.inner.to_bytes().map(base64_encode).ok()
|
||||
self.inner.to_bytes().map(|data| STANDARD_NO_PAD.encode(data)).ok()
|
||||
}
|
||||
|
||||
/// Set a listener for changes in the QrCode verification process.
|
||||
@@ -459,7 +458,6 @@ impl QrCode {
|
||||
}
|
||||
|
||||
/// Information on why a verification flow has been cancelled and by whom.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct CancelInfo {
|
||||
/// The textual representation of the cancel reason
|
||||
pub reason: String,
|
||||
@@ -522,7 +520,6 @@ pub struct ConfirmVerificationResult {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a
|
||||
/// verification request.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationRequestListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -533,7 +530,6 @@ pub trait VerificationRequestListener: Send {
|
||||
}
|
||||
|
||||
/// An Enum describing the state the QrCode verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VerificationRequestState {
|
||||
/// The verification request was sent
|
||||
Requested,
|
||||
@@ -554,6 +550,26 @@ pub enum VerificationRequestState {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<RustVerificationRequestState> for VerificationRequestState {
|
||||
fn from(value: RustVerificationRequestState) -> Self {
|
||||
match value {
|
||||
// The clients do not need to distinguish `Created` and `Requested` state
|
||||
RustVerificationRequestState::Created { .. } => Self::Requested,
|
||||
RustVerificationRequestState::Requested { .. } => Self::Requested,
|
||||
RustVerificationRequestState::Ready {
|
||||
their_methods,
|
||||
our_methods,
|
||||
other_device_id: _,
|
||||
} => Self::Ready {
|
||||
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
|
||||
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
|
||||
},
|
||||
RustVerificationRequestState::Done => Self::Done,
|
||||
RustVerificationRequestState::Cancelled(c) => Self::Cancelled { cancel_info: c.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The verificatoin request object which then can transition into some concrete
|
||||
/// verification method
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -562,7 +578,7 @@ pub struct VerificationRequest {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
#[uniffi::export]
|
||||
impl VerificationRequest {
|
||||
/// The id of the other user that is participating in this verification
|
||||
/// request.
|
||||
@@ -640,12 +656,12 @@ impl VerificationRequest {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to accept the
|
||||
/// verification requests.
|
||||
/// verification requests.
|
||||
///
|
||||
/// * `flow_id` - The ID that uniquely identifies the verification flow.
|
||||
///
|
||||
/// * `methods` - A list of verification methods that we want to advertise
|
||||
/// as supported.
|
||||
/// as supported.
|
||||
pub fn accept(&self, methods: Vec<String>) -> Option<OutgoingVerificationRequest> {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
self.inner.accept_with_methods(methods).map(|r| r.into())
|
||||
@@ -663,13 +679,13 @@ impl VerificationRequest {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// SAS verification.
|
||||
/// SAS verification.
|
||||
///
|
||||
/// * `flow_id` - The ID of the verification request that initiated the
|
||||
/// verification flow.
|
||||
/// verification flow.
|
||||
pub fn start_sas_verification(&self) -> Result<Option<StartSasResult>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.start_sas())?.map(|(sas, r)| StartSasResult {
|
||||
sas: Arc::new(Sas { inner: Box::new(sas), runtime: self.runtime.clone() }),
|
||||
sas: Arc::new(Sas { inner: sas, runtime: self.runtime.clone() }),
|
||||
request: r.into(),
|
||||
}))
|
||||
}
|
||||
@@ -681,16 +697,16 @@ impl VerificationRequest {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the QR
|
||||
/// code verification.
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// QR code verification.
|
||||
///
|
||||
/// * `flow_id` - The ID of the verification request that initiated the
|
||||
/// verification flow.
|
||||
/// verification flow.
|
||||
pub fn start_qr_verification(&self) -> Result<Option<Arc<QrCode>>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.generate_qr_code())?
|
||||
.map(|qr| QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into()))
|
||||
.map(|qr| QrCode { inner: qr, runtime: self.runtime.clone() }.into()))
|
||||
}
|
||||
|
||||
/// Pass data from a scanned QR code to an active verification request and
|
||||
@@ -701,23 +717,23 @@ impl VerificationRequest {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user for which we would like to start the QR
|
||||
/// code verification.
|
||||
/// * `user_id` - The ID of the user for which we would like to start the
|
||||
/// QR code verification.
|
||||
///
|
||||
/// * `flow_id` - The ID of the verification request that initiated the
|
||||
/// verification flow.
|
||||
/// verification flow.
|
||||
///
|
||||
/// * `data` - The data that was extracted from the scanned QR code as an
|
||||
/// base64 encoded string, without padding.
|
||||
/// base64 encoded string, without padding.
|
||||
pub fn scan_qr_code(&self, data: String) -> Option<ScanResult> {
|
||||
let data = base64_decode(data).ok()?;
|
||||
let data = STANDARD_NO_PAD.decode(data).ok()?;
|
||||
let data = QrVerificationData::from_bytes(data).ok()?;
|
||||
|
||||
if let Some(qr) = self.runtime.block_on(self.inner.scan_qr_code(data)).ok()? {
|
||||
let request = qr.reciprocate()?;
|
||||
|
||||
Some(ScanResult {
|
||||
qr: QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into(),
|
||||
qr: QrCode { inner: qr, runtime: self.runtime.clone() }.into(),
|
||||
request: request.into(),
|
||||
})
|
||||
} else {
|
||||
@@ -731,57 +747,17 @@ impl VerificationRequest {
|
||||
pub fn set_changes_listener(&self, listener: Box<dyn VerificationRequestListener>) {
|
||||
let stream = self.inner.changes();
|
||||
|
||||
self.runtime.spawn(Self::changes_listener(self.inner.to_owned(), stream, listener));
|
||||
self.runtime.spawn(Self::changes_listener(stream, listener));
|
||||
}
|
||||
|
||||
/// Get the current state of the verification request.
|
||||
pub fn state(&self) -> VerificationRequestState {
|
||||
Self::convert_verification_request(&self.inner, self.inner.state())
|
||||
self.inner.state().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl VerificationRequest {
|
||||
fn convert_verification_request(
|
||||
request: &InnerVerificationRequest,
|
||||
value: RustVerificationRequestState,
|
||||
) -> VerificationRequestState {
|
||||
match value {
|
||||
// The clients do not need to distinguish `Created` and `Requested` state
|
||||
RustVerificationRequestState::Created { .. } => VerificationRequestState::Requested,
|
||||
RustVerificationRequestState::Requested { .. } => VerificationRequestState::Requested,
|
||||
RustVerificationRequestState::Ready {
|
||||
their_methods,
|
||||
our_methods,
|
||||
other_device_data: _,
|
||||
} => VerificationRequestState::Ready {
|
||||
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
|
||||
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
|
||||
},
|
||||
RustVerificationRequestState::Done => VerificationRequestState::Done,
|
||||
RustVerificationRequestState::Transitioned { .. } => {
|
||||
let their_methods = request
|
||||
.their_supported_methods()
|
||||
.expect("The transitioned state should know the other side's methods")
|
||||
.into_iter()
|
||||
.map(|m| m.to_string())
|
||||
.collect();
|
||||
let our_methods = request
|
||||
.our_supported_methods()
|
||||
.expect("The transitioned state should know our own supported methods")
|
||||
.iter()
|
||||
.map(|m| m.to_string())
|
||||
.collect();
|
||||
VerificationRequestState::Ready { their_methods, our_methods }
|
||||
}
|
||||
|
||||
RustVerificationRequestState::Cancelled(c) => {
|
||||
VerificationRequestState::Cancelled { cancel_info: c.into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn changes_listener(
|
||||
request: InnerVerificationRequest,
|
||||
mut stream: impl Stream<Item = RustVerificationRequestState> + std::marker::Unpin,
|
||||
listener: Box<dyn VerificationRequestListener>,
|
||||
) {
|
||||
@@ -791,12 +767,11 @@ impl VerificationRequest {
|
||||
// task.
|
||||
let should_break = matches!(
|
||||
state,
|
||||
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
|
||||
RustVerificationRequestState::Done { .. }
|
||||
| RustVerificationRequestState::Cancelled { .. }
|
||||
);
|
||||
|
||||
let state = Self::convert_verification_request(&request, state);
|
||||
|
||||
listener.on_change(state);
|
||||
listener.on_change(state.into());
|
||||
|
||||
if should_break {
|
||||
break;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
@@ -0,0 +1,3 @@
|
||||
/docs
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
@@ -0,0 +1 @@
|
||||
/pkg
|
||||
@@ -0,0 +1,9 @@
|
||||
// prettier configuration: the same as the conventions used throughout Matrix.org
|
||||
// see: https://github.com/matrix-org/eslint-plugin-matrix-org/blob/main/.prettierrc.js
|
||||
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 4,
|
||||
quoteProps: "consistent",
|
||||
trailingComma: "all",
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix "matrix-sdk-crypto-js-"
|
||||
version-git-message "matrix-sdk-crypto-js v%s"
|
||||
@@ -0,0 +1,11 @@
|
||||
# v0.1.0-alpha.7
|
||||
|
||||
- Add new accessors `Device.algorithms` and `Device.isSignedByOwner`
|
||||
- In `OlmMachine.getUserDevices`, wait a limited time for any in-flight
|
||||
device-list updates to complete.
|
||||
|
||||
# v0.1.0-alpha.6
|
||||
|
||||
- Add new accessor `InboundGroupSession.senderKey`.
|
||||
- Add a new API, `OlmMachine.registerRoomKeyUpdatedCallback`, which
|
||||
applications can use to listen for received room keys.
|
||||
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "matrix-sdk-crypto-js"
|
||||
description = "Matrix encryption library, for JavaScript"
|
||||
authors = ["Ivan Enderlin <ivane@element.io>"]
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.1.0-alpha.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.profiling]
|
||||
wasm-opt = false
|
||||
|
||||
[package.metadata.wasm-pack.profile.profiling.wasm-bindgen]
|
||||
debug-js-glue = false
|
||||
demangle-name-section = true
|
||||
dwarf-debug-info = true
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ['-Oz']
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["tracing", "qrcode"]
|
||||
qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"]
|
||||
tracing = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures-util = "0.3.27"
|
||||
http = { workspace = true }
|
||||
js-sys = "0.3.49"
|
||||
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
|
||||
matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] }
|
||||
matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-indexeddb" }
|
||||
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
|
||||
ruma = { workspace = true, features = ["js", "rand", "unstable-msc2677"] }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
|
||||
vodozemac = { workspace = true, features = ["js"] }
|
||||
wasm-bindgen = "0.2.83"
|
||||
wasm-bindgen-futures = "0.4.33"
|
||||
zeroize = { workspace = true }
|
||||
@@ -0,0 +1,57 @@
|
||||
# `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
|
||||
|
||||
[The documentation can be found
|
||||
online](https://matrix-org.github.io/matrix-rust-sdk/bindings/matrix-sdk-crypto-js/).
|
||||
|
||||
To generate the documentation locally, 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,13 @@
|
||||
# Steps for releasing `matrix-sdk-crypto-js`
|
||||
|
||||
1. Create a new branch, named `release-matrix-sdk-crypto-js-<version>`.
|
||||
2. Update `CHANGELOG.md`, if necessary.
|
||||
3. Run `yarn version` to bump the version number and create a tag.
|
||||
4. Push the branch, but not yet the tag.
|
||||
5. Create a PR to approve the changes.
|
||||
6. Once approved:
|
||||
1. Update the git tag to the new head of the branch, if necessary.
|
||||
2. Push the git tag; doing so triggers the github actions workflow which
|
||||
builds and publishes to npm, and creates a draft GH release.
|
||||
3. Merge the PR.
|
||||
7. Update the release on github and publish.
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@matrix-org/matrix-sdk-crypto-js",
|
||||
"version": "0.1.0-alpha.7",
|
||||
"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": "pkg/matrix_sdk_crypto_js.js",
|
||||
"types": "pkg/matrix_sdk_crypto_js.d.ts",
|
||||
"files": [
|
||||
"pkg/matrix_sdk_crypto_js_bg.wasm.js",
|
||||
"pkg/matrix_sdk_crypto_js_bg.wasm.d.ts",
|
||||
"pkg/matrix_sdk_crypto_js.js",
|
||||
"pkg/matrix_sdk_crypto_js.d.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fake-indexeddb": "^4.0",
|
||||
"jest": "^28.1.0",
|
||||
"prettier": "^2.8.3",
|
||||
"typedoc": "^0.22.17",
|
||||
"wasm-pack": "^0.10.2",
|
||||
"yargs-parser": "~21.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "prettier --check .",
|
||||
"build": "WASM_PACK_ARGS=--release ./scripts/build.sh",
|
||||
"build:dev": "WASM_PACK_ARGS=--dev ./scripts/build.sh",
|
||||
"test": "jest --verbose",
|
||||
"doc": "typedoc --tsconfig .",
|
||||
"prepack": "npm run build && npm run test"
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build the JavaScript modules
|
||||
#
|
||||
# This script is really a workaround for https://github.com/rustwasm/wasm-pack/issues/1074.
|
||||
#
|
||||
# Currently, the only reliable way to load WebAssembly in all the JS
|
||||
# environments we want to target (web-via-webpack, web-via-browserify, jest)
|
||||
# seems to be to pack the WASM into base64, and then unpack it and instantiate
|
||||
# it at runtime.
|
||||
#
|
||||
# Hopefully one day, https://github.com/rustwasm/wasm-pack/issues/1074 will be
|
||||
# fixed and this will be unnecessary.
|
||||
|
||||
set -e
|
||||
|
||||
cd $(dirname "$0")/..
|
||||
|
||||
RUSTFLAGS='-C opt-level=z' WASM_BINDGEN_WEAKREF=1 wasm-pack build --target nodejs --scope matrix-org --out-dir pkg "${WASM_PACK_ARGS[@]}"
|
||||
|
||||
# Convert the Wasm into a JS file that exports the base64'ed Wasm.
|
||||
echo "module.exports = \`$(base64 pkg/matrix_sdk_crypto_js_bg.wasm)\`;" > pkg/matrix_sdk_crypto_js_bg.wasm.js
|
||||
|
||||
# In the JavaScript:
|
||||
# 1. Strip out the lines that load the WASM, and our new epilogue.
|
||||
# 2. Remove the imports of `TextDecoder` and `TextEncoder`. We rely on the global defaults.
|
||||
{
|
||||
sed -e '/Text..coder.*= require(.util.)/d' \
|
||||
-e '/^const path = /,$d' pkg/matrix_sdk_crypto_js.js
|
||||
cat scripts/epilogue.js
|
||||
} > pkg/matrix_sdk_crypto_js.js.new
|
||||
mv pkg/matrix_sdk_crypto_js.js.new pkg/matrix_sdk_crypto_js.js
|
||||
|
||||
# also extend the typescript
|
||||
cat scripts/epilogue.d.ts >> pkg/matrix_sdk_crypto_js.d.ts
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Load the WebAssembly module in the background, if it has not already been loaded.
|
||||
*
|
||||
* Returns a promise which will resolve once the other methods are ready.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function initAsync(): Promise<void>;
|
||||
@@ -0,0 +1,88 @@
|
||||
// Initially, 'wasm' is set to a Proxy object which will synchronously load the WebAssembly module and
|
||||
// replace 'wasm' with a reference to the exports from the wasm module.
|
||||
//
|
||||
// Ideally this will never get used because the application will call initAsync instead.
|
||||
wasm = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop, receiver) => __initSync()[prop],
|
||||
},
|
||||
);
|
||||
|
||||
let inited = false;
|
||||
__initSync = function () {
|
||||
if (inited) {
|
||||
return;
|
||||
}
|
||||
if (initPromise) {
|
||||
throw new Error("Asynchronous initialisation already in progress: cannot initialise synchronously");
|
||||
}
|
||||
const bytes = unbase64(require("./matrix_sdk_crypto_js_bg.wasm.js"));
|
||||
const mod = new WebAssembly.Module(bytes);
|
||||
const instance = new WebAssembly.Instance(mod, imports);
|
||||
wasm = instance.exports;
|
||||
wasm.__wbindgen_start();
|
||||
inited = true;
|
||||
return wasm;
|
||||
};
|
||||
|
||||
let initPromise = null;
|
||||
|
||||
/**
|
||||
* Load the WebAssembly module in the background, if it has not already been loaded.
|
||||
*
|
||||
* Returns a promise which will resolve once the other methods are ready.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
module.exports.initAsync = function () {
|
||||
if (inited) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!initPromise) {
|
||||
initPromise = Promise.resolve()
|
||||
.then(() => require("./matrix_sdk_crypto_js_bg.wasm.js"))
|
||||
.then((b64) => WebAssembly.instantiate(unbase64(b64), imports))
|
||||
.then((result) => {
|
||||
wasm = result.instance.exports;
|
||||
wasm.__wbindgen_start();
|
||||
inited = true;
|
||||
});
|
||||
}
|
||||
return initPromise;
|
||||
};
|
||||
|
||||
const b64lookup = new Uint8Array([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 62, 0, 62, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7,
|
||||
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32,
|
||||
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
|
||||
]);
|
||||
|
||||
// base64 decoder, based on the code at https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_2_%E2%80%93_rewriting_atob_and_btoa_using_typedarrays_and_utf-8
|
||||
function unbase64(sBase64) {
|
||||
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, "");
|
||||
const nInLen = sB64Enc.length;
|
||||
const nOutLen = (nInLen * 3 + 1) >> 2;
|
||||
const taBytes = new Uint8Array(nOutLen);
|
||||
|
||||
let nMod3;
|
||||
let nMod4;
|
||||
let nUint24 = 0;
|
||||
let nOutIdx = 0;
|
||||
for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
|
||||
nMod4 = nInIdx & 3;
|
||||
nUint24 |= b64lookup[sB64Enc.charCodeAt(nInIdx)] << (6 * (3 - nMod4));
|
||||
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
||||
nMod3 = 0;
|
||||
while (nMod3 < 3 && nOutIdx < nOutLen) {
|
||||
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
|
||||
nMod3++;
|
||||
nOutIdx++;
|
||||
}
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return taBytes;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Attachment API.
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// A type to encrypt and to decrypt anything that can fit in an
|
||||
/// `Uint8Array`, usually big buffer.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Attachment;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Attachment {
|
||||
/// Encrypt the content of the `Uint8Array`.
|
||||
///
|
||||
/// It produces an `EncryptedAttachment`, which can be used to
|
||||
/// retrieve the media encryption information, or the encrypted
|
||||
/// data.
|
||||
#[wasm_bindgen]
|
||||
pub fn encrypt(array: &[u8]) -> Result<EncryptedAttachment, JsError> {
|
||||
let mut cursor = Cursor::new(array);
|
||||
let mut encryptor = matrix_sdk_crypto::AttachmentEncryptor::new(&mut cursor);
|
||||
|
||||
let mut encrypted_data = Vec::new();
|
||||
encryptor.read_to_end(&mut encrypted_data)?;
|
||||
|
||||
let media_encryption_info = Some(encryptor.finish());
|
||||
|
||||
Ok(EncryptedAttachment { 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`.
|
||||
pub fn decrypt(attachment: &mut EncryptedAttachment) -> Result<Vec<u8>, JsError> {
|
||||
let Some(media_encryption_info) = attachment.media_encryption_info.take() else {
|
||||
return Err(JsError::new(
|
||||
"The media encryption info are absent from the given encrypted attachment",
|
||||
));
|
||||
};
|
||||
|
||||
let encrypted_data: &[u8] = attachment.encrypted_data.as_slice();
|
||||
|
||||
let mut cursor = Cursor::new(encrypted_data);
|
||||
let mut decryptor =
|
||||
matrix_sdk_crypto::AttachmentDecryptor::new(&mut cursor, media_encryption_info)?;
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
decryptor.read_to_end(&mut decrypted_data)?;
|
||||
|
||||
Ok(decrypted_data)
|
||||
}
|
||||
}
|
||||
|
||||
/// An encrypted attachment, usually created from `Attachment.encrypt`.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptedAttachment {
|
||||
media_encryption_info: Option<matrix_sdk_crypto::MediaEncryptionInfo>,
|
||||
encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
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).
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
encrypted_data: Vec<u8>,
|
||||
media_encryption_info: &str,
|
||||
) -> Result<EncryptedAttachment, JsError> {
|
||||
Ok(Self {
|
||||
encrypted_data,
|
||||
media_encryption_info: Some(serde_json::from_str(media_encryption_info)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// The actual encrypted data.
|
||||
///
|
||||
/// **Warning**: It returns a **copy** of the entire encrypted
|
||||
/// data; be nice with your memory.
|
||||
#[wasm_bindgen(getter, js_name = "encryptedData")]
|
||||
pub fn encrypted_data(&self) -> Vec<u8> {
|
||||
self.encrypted_data.clone()
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
#[wasm_bindgen(getter, js_name = "mediaEncryptionInfo")]
|
||||
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.
|
||||
#[wasm_bindgen(getter, js_name = "hasMediaEncryptionInfoBeenConsumed")]
|
||||
pub fn has_media_encryption_info_been_consumed(&self) -> bool {
|
||||
self.media_encryption_info.is_none()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//! Types for a `Device`.
|
||||
|
||||
use js_sys::{Array, Map, Promise};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
encryption::EncryptionAlgorithm,
|
||||
future::future_to_promise,
|
||||
identifiers::{self, DeviceId, UserId},
|
||||
impl_from_to_inner,
|
||||
js::try_array_to_vec,
|
||||
types, verification, vodozemac,
|
||||
};
|
||||
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
pub(crate) inner: matrix_sdk_crypto::Device,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::Device => Device);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Device {
|
||||
/// Request an interactive verification with this device.
|
||||
#[wasm_bindgen(js_name = "requestVerification")]
|
||||
pub fn request_verification(&self, methods: Option<Array>) -> Result<Promise, JsError> {
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let tuple = Array::new();
|
||||
let (verification_request, outgoing_verification_request) = match methods {
|
||||
Some(methods) => me.request_verification_with_methods(methods).await,
|
||||
None => me.request_verification().await,
|
||||
};
|
||||
|
||||
tuple.set(0, verification::VerificationRequest::from(verification_request).into());
|
||||
tuple.set(
|
||||
1,
|
||||
verification::OutgoingVerificationRequest::from(outgoing_verification_request)
|
||||
.try_into()?,
|
||||
);
|
||||
|
||||
Ok(tuple)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Is this device considered to be verified.
|
||||
///
|
||||
/// This method returns true if either the `is_locally_trusted`
|
||||
/// method returns `true` or if the `is_cross_signing_trusted`
|
||||
/// method returns `true`.
|
||||
#[wasm_bindgen(js_name = "isVerified")]
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// Is this device considered to be verified using cross signing.
|
||||
#[wasm_bindgen(js_name = "isCrossSigningTrusted")]
|
||||
pub fn is_cross_signing_trusted(&self) -> bool {
|
||||
self.inner.is_cross_signing_trusted()
|
||||
}
|
||||
|
||||
/// Is this device cross-signed by its owner?
|
||||
#[wasm_bindgen(js_name = "isCrossSignedByOwner")]
|
||||
pub fn is_cross_signed_by_owner(&self) -> bool {
|
||||
self.inner.is_cross_signed_by_owner()
|
||||
}
|
||||
|
||||
/// Set the local trust state of the device to the given state.
|
||||
///
|
||||
/// This won’t affect any cross signing trust state, this only
|
||||
/// sets a flag marking to have the given trust state.
|
||||
///
|
||||
/// `trust_state` represents the new trust state that should be
|
||||
/// set for the device.
|
||||
#[wasm_bindgen(js_name = "setLocalTrust")]
|
||||
pub fn set_local_trust(&self, local_state: LocalTrust) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
me.set_local_trust(local_state.into()).await?;
|
||||
|
||||
Ok(JsValue::NULL)
|
||||
})
|
||||
}
|
||||
|
||||
/// The user ID of the device owner.
|
||||
#[wasm_bindgen(getter, js_name = "userId")]
|
||||
pub fn user_id(&self) -> UserId {
|
||||
self.inner.user_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// The unique ID of the device.
|
||||
#[wasm_bindgen(getter, js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> DeviceId {
|
||||
self.inner.device_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// Get the human readable name of the device.
|
||||
#[wasm_bindgen(getter, js_name = "displayName")]
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
self.inner.display_name().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the key of the given key algorithm belonging to this device.
|
||||
#[wasm_bindgen(js_name = "getKey")]
|
||||
pub fn get_key(
|
||||
&self,
|
||||
algorithm: identifiers::DeviceKeyAlgorithmName,
|
||||
) -> Result<Option<vodozemac::DeviceKey>, JsError> {
|
||||
Ok(self.inner.get_key(algorithm.try_into()?).cloned().map(Into::into))
|
||||
}
|
||||
|
||||
/// Get the Curve25519 key of the given device.
|
||||
#[wasm_bindgen(getter, js_name = "curve25519Key")]
|
||||
pub fn curve25519_key(&self) -> Option<vodozemac::Curve25519PublicKey> {
|
||||
self.inner.curve25519_key().map(Into::into)
|
||||
}
|
||||
|
||||
/// Get the Ed25519 key of the given device.
|
||||
#[wasm_bindgen(getter, js_name = "ed25519Key")]
|
||||
pub fn ed25519_key(&self) -> Option<vodozemac::Ed25519PublicKey> {
|
||||
self.inner.ed25519_key().map(Into::into)
|
||||
}
|
||||
|
||||
/// Get a map containing all the device keys.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn keys(&self) -> Map {
|
||||
let map = Map::new();
|
||||
|
||||
for (device_key_id, device_key) in self.inner.keys() {
|
||||
map.set(
|
||||
&identifiers::DeviceKeyId::from(device_key_id.clone()).into(),
|
||||
&vodozemac::DeviceKey::from(device_key.clone()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
/// Get the list of algorithms this device supports.
|
||||
///
|
||||
/// Returns `Array<EncryptionAlgorithm>`.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn algorithms(&self) -> Array {
|
||||
self.inner
|
||||
.algorithms()
|
||||
.iter()
|
||||
.map(|alg| EncryptionAlgorithm::from(alg.clone()))
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a map containing all the device signatures.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn signatures(&self) -> types::Signatures {
|
||||
self.inner.signatures().clone().into()
|
||||
}
|
||||
|
||||
/// Get the trust state of the device.
|
||||
#[wasm_bindgen(getter, js_name = "localTrustState")]
|
||||
pub fn local_trust_state(&self) -> LocalTrust {
|
||||
self.inner.local_trust_state().into()
|
||||
}
|
||||
|
||||
/// Is the device locally marked as trusted?
|
||||
#[wasm_bindgen(js_name = "isLocallyTrusted")]
|
||||
pub fn is_locally_trusted(&self) -> bool {
|
||||
self.inner.is_locally_trusted()
|
||||
}
|
||||
|
||||
/// Is the device locally marked as blacklisted?
|
||||
///
|
||||
/// Blacklisted devices won’t receive any group sessions.
|
||||
#[wasm_bindgen(js_name = "isBlacklisted")]
|
||||
pub fn is_blacklisted(&self) -> bool {
|
||||
self.inner.is_blacklisted()
|
||||
}
|
||||
|
||||
/// Is the device deleted?
|
||||
#[wasm_bindgen(js_name = "isDeleted")]
|
||||
pub fn is_deleted(&self) -> bool {
|
||||
self.inner.is_deleted()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local trust state of a device.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum LocalTrust {
|
||||
/// The device has been verified and is trusted.
|
||||
Verified,
|
||||
|
||||
/// The device been blacklisted from communicating.
|
||||
BlackListed,
|
||||
|
||||
/// The trust state of the device is being ignored.
|
||||
Ignored,
|
||||
|
||||
/// The trust state is unset.
|
||||
Unset,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::LocalTrust> for LocalTrust {
|
||||
fn from(value: matrix_sdk_crypto::LocalTrust) -> Self {
|
||||
use matrix_sdk_crypto::LocalTrust::*;
|
||||
|
||||
match value {
|
||||
Verified => Self::Verified,
|
||||
BlackListed => Self::BlackListed,
|
||||
Ignored => Self::Ignored,
|
||||
Unset => Self::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalTrust> for matrix_sdk_crypto::LocalTrust {
|
||||
fn from(value: LocalTrust) -> Self {
|
||||
use LocalTrust::*;
|
||||
|
||||
match value {
|
||||
Verified => Self::Verified,
|
||||
BlackListed => Self::BlackListed,
|
||||
Ignored => Self::Ignored,
|
||||
Unset => Self::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct UserDevices {
|
||||
pub(crate) inner: matrix_sdk_crypto::UserDevices,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::UserDevices => UserDevices);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserDevices {
|
||||
/// Get the specific device with the given device ID.
|
||||
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
|
||||
self.inner.get(&device_id.inner).map(Into::into)
|
||||
}
|
||||
|
||||
/// Returns true if there is at least one devices of this user
|
||||
/// that is considered to be verified, false otherwise.
|
||||
///
|
||||
/// This won't consider your own device as verified, as your own
|
||||
/// device is always implicitly verified.
|
||||
#[wasm_bindgen(js_name = "isAnyVerified")]
|
||||
pub fn is_any_verified(&self) -> bool {
|
||||
self.inner.is_any_verified()
|
||||
}
|
||||
|
||||
/// Array over all the device IDs of the user devices.
|
||||
pub fn keys(&self) -> Array {
|
||||
self.inner.keys().map(ToOwned::to_owned).map(DeviceId::from).map(JsValue::from).collect()
|
||||
}
|
||||
|
||||
/// Iterator over all the devices of the user devices.
|
||||
pub fn devices(&self) -> Array {
|
||||
self.inner.devices().map(Device::from).map(JsValue::from).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Encryption types & siblings.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
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,
|
||||
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
#[wasm_bindgen(js_name = "onlyAllowTrustedDevices")]
|
||||
pub only_allow_trusted_devices: bool,
|
||||
}
|
||||
|
||||
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(),
|
||||
only_allow_trusted_devices: default.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let algorithm = value.algorithm.clone().into();
|
||||
|
||||
Self {
|
||||
algorithm,
|
||||
rotation_period: Duration::from_micros(value.rotation_period),
|
||||
rotation_period_msgs: value.rotation_period_messages,
|
||||
history_visibility: value.history_visibility.clone().into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 JsValue {
|
||||
fn from(value: EncryptionAlgorithm) -> Self {
|
||||
use EncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => JsValue::from(0),
|
||||
MegolmV1AesSha2 => JsValue::from(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptionAlgorithm> for matrix_sdk_crypto::types::EventEncryptionAlgorithm {
|
||||
fn from(value: EncryptionAlgorithm) -> Self {
|
||||
use EncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
|
||||
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::types::EventEncryptionAlgorithm> for EncryptionAlgorithm {
|
||||
fn from(value: matrix_sdk_crypto::types::EventEncryptionAlgorithm) -> Self {
|
||||
use matrix_sdk_crypto::types::EventEncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
|
||||
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
|
||||
/// for more info.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ShieldColor {
|
||||
/// Important warning
|
||||
Red,
|
||||
/// Low warning
|
||||
Grey,
|
||||
/// No warning
|
||||
None,
|
||||
}
|
||||
|
||||
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
|
||||
/// for more info.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShieldState {
|
||||
/// The shield color
|
||||
pub color: ShieldColor,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ShieldState {
|
||||
/// Error message that can be displayed as a tooltip
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn message(&self) -> Option<String> {
|
||||
self.message.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RustShieldState> for ShieldState {
|
||||
fn from(value: RustShieldState) -> Self {
|
||||
match value {
|
||||
RustShieldState::Red { message } => {
|
||||
Self { color: ShieldColor::Red, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::Grey { message } => {
|
||||
Self { color: ShieldColor::Grey, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::None => Self { color: ShieldColor::None, message: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Types related to events.
|
||||
|
||||
use ruma::events::room::history_visibility::HistoryVisibility as RumaHistoryVisibility;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Who can see a room's history.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they joined the room onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when
|
||||
/// the member was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be
|
||||
/// shared by any participating homeserver with anyone, regardless
|
||||
/// of whether they have ever joined the room.
|
||||
WorldReadable,
|
||||
}
|
||||
|
||||
impl From<HistoryVisibility> for RumaHistoryVisibility {
|
||||
fn from(value: HistoryVisibility) -> Self {
|
||||
use HistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
use RumaHistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::future::Future;
|
||||
|
||||
use js_sys::Promise;
|
||||
use wasm_bindgen::{JsError, JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
pub(crate) fn future_to_promise<F, T>(future: F) -> Promise
|
||||
where
|
||||
F: Future<Output = Result<T, anyhow::Error>> + 'static,
|
||||
T: Into<JsValue>,
|
||||
{
|
||||
let mut future = Some(future);
|
||||
|
||||
Promise::new(&mut |resolve, reject| {
|
||||
let future = future.take().unwrap_throw();
|
||||
|
||||
spawn_local(async move {
|
||||
match future.await {
|
||||
Ok(value) => resolve.call1(&JsValue::UNDEFINED, &value.into()).unwrap_throw(),
|
||||
Err(value) => reject
|
||||
.call1(&JsValue::UNDEFINED, &JsError::new(&value.to_string()).into())
|
||||
.unwrap_throw(),
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Types for [Matrix](https://matrix.org/) identifiers for devices,
|
||||
//! events, keys, rooms, servers, users and URIs.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::impl_from_to_inner;
|
||||
|
||||
/// A Matrix [user ID].
|
||||
///
|
||||
/// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserId {
|
||||
pub(crate) inner: ruma::OwnedUserId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedUserId => UserId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserId {
|
||||
/// Parse/validate and create a new `UserId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<UserId, JsError> {
|
||||
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(getter, js_name = "serverName")]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Whether this user ID is a historical one.
|
||||
///
|
||||
/// A historical user ID is one that doesn't conform to the latest
|
||||
/// specification of the user ID grammar but is still accepted
|
||||
/// because it was previously allowed.
|
||||
#[wasm_bindgen(js_name = "isHistorical")]
|
||||
pub fn is_historical(&self) -> bool {
|
||||
self.inner.is_historical()
|
||||
}
|
||||
|
||||
/// Return the user ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix key ID.
|
||||
///
|
||||
/// Device identifiers in Matrix are completely opaque character
|
||||
/// sequences. This type is provided simply for its semantic value.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceId {
|
||||
pub(crate) inner: ruma::OwnedDeviceId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedDeviceId => DeviceId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceId {
|
||||
/// Create a new `DeviceId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> DeviceId {
|
||||
Self::from(ruma::OwnedDeviceId::from(id))
|
||||
}
|
||||
|
||||
/// Return the device ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix device key ID.
|
||||
///
|
||||
/// A key algorithm and a device ID, combined with a ‘:’.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceKeyId {
|
||||
pub(crate) inner: ruma::OwnedDeviceKeyId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedDeviceKeyId => DeviceKeyId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceKeyId {
|
||||
/// Parse/validate and create a new `DeviceKeyId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: String) -> Result<DeviceKeyId, JsError> {
|
||||
Ok(Self::from(ruma::DeviceKeyId::parse(id.as_str())?))
|
||||
}
|
||||
|
||||
/// Returns key algorithm of the device key ID.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn algorithm(&self) -> DeviceKeyAlgorithm {
|
||||
self.inner.algorithm().into()
|
||||
}
|
||||
|
||||
/// Returns device ID of the device key ID.
|
||||
#[wasm_bindgen(getter, js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> DeviceId {
|
||||
self.inner.device_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// Return the device key ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The basic key algorithms in the specification.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceKeyAlgorithm {
|
||||
pub(crate) inner: ruma::DeviceKeyAlgorithm,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::DeviceKeyAlgorithm => DeviceKeyAlgorithm);
|
||||
|
||||
#[wasm_bindgen]
|
||||
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.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn name(&self) -> DeviceKeyAlgorithmName {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
/// Return the device key algorithm as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The basic key algorithm names in the specification.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
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 TryFrom<DeviceKeyAlgorithmName> for ruma::DeviceKeyAlgorithm {
|
||||
type Error = JsError;
|
||||
|
||||
fn try_from(value: DeviceKeyAlgorithmName) -> Result<Self, Self::Error> {
|
||||
use DeviceKeyAlgorithmName::*;
|
||||
|
||||
Ok(match value {
|
||||
Ed25519 => Self::Ed25519,
|
||||
Curve25519 => Self::Curve25519,
|
||||
SignedCurve25519 => Self::SignedCurve25519,
|
||||
Unknown => {
|
||||
return Err(JsError::new(
|
||||
"The `DeviceKeyAlgorithmName.Unknown` variant cannot be converted",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoomId {
|
||||
pub(crate) inner: ruma::OwnedRoomId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedRoomId => RoomId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomId {
|
||||
/// Parse/validate and create a new `RoomId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<RoomId, JsError> {
|
||||
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(getter, js_name = "serverName")]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Return the room ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix-spec compliant [server name].
|
||||
///
|
||||
/// It consists of a host and an optional port (separated by a colon if
|
||||
/// present).
|
||||
///
|
||||
/// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct ServerName {
|
||||
inner: ruma::OwnedServerName,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ServerName {
|
||||
/// Parse/validate and create a new `ServerName`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(name: &str) -> Result<ServerName, JsError> {
|
||||
Ok(Self { inner: ruma::ServerName::parse(name)? })
|
||||
}
|
||||
|
||||
/// Returns the host of the server name.
|
||||
///
|
||||
/// That is: Return the part of the server before `:<port>` or the
|
||||
/// full server name if there is no port.
|
||||
#[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()
|
||||
}
|
||||
|
||||
/// Returns true if and only if the server name is an IPv4 or IPv6
|
||||
/// address.
|
||||
#[wasm_bindgen(js_name = "isIpLiteral")]
|
||||
pub fn is_ip_literal(&self) -> bool {
|
||||
self.inner.is_ip_literal()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix [event ID].
|
||||
///
|
||||
/// An `EventId` is generated randomly or converted from a string
|
||||
/// slice, and can be converted back into a string as needed.
|
||||
///
|
||||
/// [event ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct EventId {
|
||||
pub(crate) inner: ruma::OwnedEventId,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EventId {
|
||||
/// Parse/validate and create a new `EventId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<EventId, JsError> {
|
||||
Ok(Self { inner: <&ruma::EventId>::try_from(id)?.to_owned() })
|
||||
}
|
||||
|
||||
/// Returns the event's localpart.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the event ID.
|
||||
#[wasm_bindgen(getter, js_name = "serverName")]
|
||||
pub fn server_name(&self) -> Option<ServerName> {
|
||||
Some(ServerName { inner: self.inner.server_name()?.to_owned() })
|
||||
}
|
||||
|
||||
/// Return the event ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! User identities.
|
||||
|
||||
use js_sys::{Array, Promise};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
future::future_to_promise, identifiers, impl_from_to_inner, js::try_array_to_vec, requests,
|
||||
verification,
|
||||
};
|
||||
|
||||
pub(crate) struct UserIdentities {
|
||||
inner: matrix_sdk_crypto::UserIdentities,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::UserIdentities => UserIdentities);
|
||||
|
||||
impl From<UserIdentities> for JsValue {
|
||||
fn from(user_identities: UserIdentities) -> Self {
|
||||
use matrix_sdk_crypto::UserIdentities::*;
|
||||
|
||||
match user_identities.inner {
|
||||
Own(own) => JsValue::from(OwnUserIdentity::from(own)),
|
||||
Other(other) => JsValue::from(UserIdentity::from(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a cross signing identity of a user.
|
||||
///
|
||||
/// This is the user identity of a user that is our own.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct OwnUserIdentity {
|
||||
inner: matrix_sdk_crypto::OwnUserIdentity,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::OwnUserIdentity => OwnUserIdentity);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OwnUserIdentity {
|
||||
/// Mark our user identity as verified.
|
||||
///
|
||||
/// This will mark the identity locally as verified and sign it with our own
|
||||
/// device.
|
||||
///
|
||||
/// Returns a signature upload request that needs to be sent out.
|
||||
pub fn verify(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(requests::SignatureUploadRequest::try_from(&me.verify().await?)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a verification request to our other devices.
|
||||
#[wasm_bindgen(js_name = "requestVerification")]
|
||||
pub fn request_verification(&self, methods: Option<Array>) -> Result<Promise, JsError> {
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let tuple = Array::new();
|
||||
let (verification_request, outgoing_verification_request) = match methods {
|
||||
Some(methods) => me.request_verification_with_methods(methods).await?,
|
||||
None => me.request_verification().await?,
|
||||
};
|
||||
|
||||
tuple.set(0, verification::VerificationRequest::from(verification_request).into());
|
||||
tuple.set(
|
||||
1,
|
||||
verification::OutgoingVerificationRequest::from(outgoing_verification_request)
|
||||
.try_into()?,
|
||||
);
|
||||
|
||||
Ok(tuple)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Does our user identity trust our own device, i.e. have we signed our own
|
||||
/// device keys with our self-signing key?
|
||||
#[wasm_bindgen(js_name = "trustsOurOwnDevice")]
|
||||
pub fn trusts_our_own_device(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.trusts_our_own_device().await?) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a cross signing identity of a user.
|
||||
///
|
||||
/// This is the user identity of a user that isn't our own. Other users will
|
||||
/// only contain a master key and a self signing key, meaning that only device
|
||||
/// signatures can be checked with this identity.
|
||||
///
|
||||
/// This struct wraps a read-only version of the struct and allows verifications
|
||||
/// to be requested to verify our own device with the user identity.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct UserIdentity {
|
||||
inner: matrix_sdk_crypto::UserIdentity,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::UserIdentity => UserIdentity);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserIdentity {
|
||||
/// Is this user identity verified?
|
||||
#[wasm_bindgen(js_name = "isVerified")]
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// Manually verify this user.
|
||||
///
|
||||
/// This method will attempt to sign the user identity using our private
|
||||
/// cross signing key.
|
||||
///
|
||||
/// This method fails if we don't have the private part of our user-signing
|
||||
/// key.
|
||||
///
|
||||
/// Returns a request that needs to be sent out for the user to be marked as
|
||||
/// verified.
|
||||
pub fn verify(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(requests::SignatureUploadRequest::try_from(&me.verify().await?)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `VerificationRequest` object after the verification
|
||||
/// request content has been sent out.
|
||||
#[wasm_bindgen(js_name = "requestVerification")]
|
||||
pub fn request_verification(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
request_event_id: &identifiers::EventId,
|
||||
methods: Option<Array>,
|
||||
) -> Result<Promise, JsError> {
|
||||
let me = self.inner.clone();
|
||||
let room_id = room_id.inner.clone();
|
||||
let request_event_id = request_event_id.inner.clone();
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
|
||||
Ok(future_to_promise::<_, verification::VerificationRequest>(async move {
|
||||
Ok(me
|
||||
.request_verification(room_id.as_ref(), request_event_id.as_ref(), methods)
|
||||
.await
|
||||
.into())
|
||||
}))
|
||||
}
|
||||
|
||||
/// Send a verification request to the given user.
|
||||
///
|
||||
/// The returned content needs to be sent out into a DM room with the given
|
||||
/// user.
|
||||
///
|
||||
/// After the content has been sent out a VerificationRequest can be started
|
||||
/// with the `request_verification` method.
|
||||
#[wasm_bindgen(js_name = "verificationRequestContent")]
|
||||
pub fn verification_request_content(&self, methods: Option<Array>) -> Result<Promise, JsError> {
|
||||
let me = self.inner.clone();
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(&me.verification_request_content(methods).await)?)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use js_sys::{Array, Object, Reflect};
|
||||
use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};
|
||||
|
||||
/// A really hacky and dirty code to downcast a `JsValue` to `T:
|
||||
/// RefFromWasmAbi`, inspired by
|
||||
/// https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288.
|
||||
///
|
||||
/// The returned value is likely to be a `wasm_bindgen::__ref::Ref<T>`.
|
||||
pub(crate) fn downcast<T>(value: &JsValue, classname: &str) -> Result<T::Anchor, JsError>
|
||||
where
|
||||
T: RefFromWasmAbi<Abi = u32>,
|
||||
{
|
||||
let constructor_name = Object::get_prototype_of(value).constructor().name();
|
||||
|
||||
if constructor_name == classname {
|
||||
let pointer = Reflect::get(value, &JsValue::from_str("ptr"))
|
||||
.map_err(|_| JsError::new("Failed to read the `JsValue` pointer"))?;
|
||||
let pointer = pointer
|
||||
.as_f64()
|
||||
.ok_or_else(|| JsError::new("Failed to read the `JsValue` pointer as a `f64`"))?
|
||||
as u32;
|
||||
|
||||
Ok(unsafe { T::ref_from_abi(pointer) })
|
||||
} else {
|
||||
Err(JsError::new(&format!(
|
||||
"Expect an `{classname}` instance, received `{constructor_name}` instead",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform a value `JS` from JavaScript to a Rust wrapper, to a
|
||||
/// Rust wrapped type.
|
||||
pub(crate) fn try_array_to_vec<JS, Rust>(
|
||||
array: Array,
|
||||
) -> Result<Vec<Rust>, <JS as TryFrom<JsValue>>::Error>
|
||||
where
|
||||
JS: TryFrom<JsValue> + Into<Rust>,
|
||||
{
|
||||
array.iter().map(|item| JS::try_from(item).map(Into::into)).collect()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
// triggered by wasm_bindgen code
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
|
||||
pub mod attachment;
|
||||
pub mod device;
|
||||
pub mod encryption;
|
||||
pub mod events;
|
||||
mod future;
|
||||
pub mod identifiers;
|
||||
pub mod identities;
|
||||
mod js;
|
||||
pub mod machine;
|
||||
mod macros;
|
||||
pub mod olm;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
pub mod store;
|
||||
pub mod sync_events;
|
||||
mod tracing;
|
||||
pub mod types;
|
||||
pub mod verification;
|
||||
pub mod vodozemac;
|
||||
|
||||
use js_sys::JsString;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Object containing the versions of the Rust libraries we are using.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Versions {
|
||||
/// The version of the vodozemac crate.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub vodozemac: JsString,
|
||||
/// The version of the matrix-sdk-crypto crate.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub matrix_sdk_crypto: JsString,
|
||||
}
|
||||
|
||||
/// Get the versions of the Rust libraries we are using.
|
||||
#[wasm_bindgen(js_name = "getVersions")]
|
||||
pub fn get_versions() -> Versions {
|
||||
Versions {
|
||||
vodozemac: matrix_sdk_crypto::vodozemac::VERSION.into(),
|
||||
matrix_sdk_crypto: matrix_sdk_crypto::VERSION.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run some stuff when the Wasm module is instantiated.
|
||||
///
|
||||
/// Right now, it does the following:
|
||||
///
|
||||
/// * Redirect Rust panics to JavaScript console.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
@@ -0,0 +1,843 @@
|
||||
//! The crypto specific Olm objects.
|
||||
|
||||
use std::{collections::BTreeMap, ops::Deref, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use js_sys::{Array, Function, Map, Promise, Set};
|
||||
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
use tracing::warn;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
|
||||
use crate::{
|
||||
device, encryption,
|
||||
future::future_to_promise,
|
||||
identifiers, identities,
|
||||
js::downcast,
|
||||
olm, requests,
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
responses::{self, response_from_string},
|
||||
store,
|
||||
store::RoomKeyInfo,
|
||||
sync_events, types, verification, vodozemac,
|
||||
};
|
||||
|
||||
/// State machine implementation of the Olm/Megolm encryption protocol
|
||||
/// used for Matrix end to end encryption.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OlmMachine {
|
||||
inner: matrix_sdk_crypto::OlmMachine,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OlmMachine {
|
||||
/// Constructor will always fail. To create a new `OlmMachine`, please use
|
||||
/// the `initialize` method.
|
||||
///
|
||||
/// Why this pattern? `initialize` returns a `Promise`. Returning a
|
||||
// `Promise` from a constructor is not idiomatic in JavaScript.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Result<OlmMachine, JsError> {
|
||||
Err(JsError::new("To build an `OlmMachine`, please use the `initialize` method"))
|
||||
}
|
||||
|
||||
/// Create a new `OlmMachine`.
|
||||
///
|
||||
/// The created machine will keep the encryption keys either in a IndexedDB
|
||||
/// based store, or in a memory store and once the objects is dropped,
|
||||
/// the keys will be lost.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - represents the unique ID of the user that owns this
|
||||
/// machine.
|
||||
///
|
||||
/// * `device_id` - represents the unique ID of the device
|
||||
/// that owns this machine.
|
||||
///
|
||||
/// * `store_name` - The name that should be used to open the IndexedDB
|
||||
/// based database. If this isn't provided, a memory-only store will be
|
||||
/// used. *Note* the memory-only store will lose your E2EE keys when the
|
||||
/// `OlmMachine` gets dropped.
|
||||
///
|
||||
/// * `store_passphrase` - The passphrase that should be used to encrypt the
|
||||
/// IndexedDB based
|
||||
pub fn initialize(
|
||||
user_id: &identifiers::UserId,
|
||||
device_id: &identifiers::DeviceId,
|
||||
store_name: Option<String>,
|
||||
store_passphrase: Option<String>,
|
||||
) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
let device_id = device_id.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match (store_name, store_passphrase) {
|
||||
(Some(store_name), Some(mut store_passphrase)) => {
|
||||
use zeroize::Zeroize;
|
||||
|
||||
let store = Some(
|
||||
matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase(
|
||||
&store_name,
|
||||
&store_passphrase,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
store_passphrase.zeroize();
|
||||
|
||||
store
|
||||
}
|
||||
|
||||
(Some(store_name), None) => Some(
|
||||
matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_name(&store_name).await?,
|
||||
),
|
||||
|
||||
(None, Some(_)) => {
|
||||
return Err(anyhow::Error::msg(
|
||||
"The `store_passphrase` has been set, but it has an effect only if \
|
||||
`store_name` is set, which is not; please provide one",
|
||||
))
|
||||
}
|
||||
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
Ok(OlmMachine {
|
||||
inner: match store {
|
||||
// We need this `#[cfg]` because `IndexeddbCryptoStore`
|
||||
// implements `CryptoStore` only on `target_arch =
|
||||
// "wasm32"`. Without that, we could have a compilation
|
||||
// error when checking the entire workspace. In practice,
|
||||
// it doesn't impact this crate because it's always
|
||||
// compiled for `wasm32`.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Some(store) => {
|
||||
matrix_sdk_crypto::OlmMachine::with_store(
|
||||
user_id.as_ref(),
|
||||
device_id.as_ref(),
|
||||
store,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
_ => {
|
||||
matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref())
|
||||
.await
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The unique user ID that owns this `OlmMachine` instance.
|
||||
#[wasm_bindgen(getter, js_name = "userId")]
|
||||
pub fn user_id(&self) -> identifiers::UserId {
|
||||
identifiers::UserId::from(self.inner.user_id().to_owned())
|
||||
}
|
||||
|
||||
/// The unique device ID that identifies this `OlmMachine`.
|
||||
#[wasm_bindgen(getter, js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> identifiers::DeviceId {
|
||||
identifiers::DeviceId::from(self.inner.device_id().to_owned())
|
||||
}
|
||||
|
||||
/// Get the public parts of our Olm identity keys.
|
||||
#[wasm_bindgen(getter, js_name = "identityKeys")]
|
||||
pub fn identity_keys(&self) -> vodozemac::IdentityKeys {
|
||||
self.inner.identity_keys().into()
|
||||
}
|
||||
|
||||
/// Get the display name of our own device.
|
||||
#[wasm_bindgen(getter, js_name = "displayName")]
|
||||
pub fn display_name(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.display_name().await?) })
|
||||
}
|
||||
|
||||
/// Get the list of users whose devices we are currently tracking.
|
||||
///
|
||||
/// A user can be marked for tracking using the
|
||||
/// [`update_tracked_users`](#method.update_tracked_users) method.
|
||||
///
|
||||
/// Returns a `Set<UserId>`.
|
||||
#[wasm_bindgen(js_name = "trackedUsers")]
|
||||
pub fn tracked_users(&self) -> Result<Promise, JsError> {
|
||||
let set = Set::new(&JsValue::UNDEFINED);
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
for user in me.tracked_users().await? {
|
||||
set.add(&identifiers::UserId::from(user).into());
|
||||
}
|
||||
Ok(set)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Update the list of tracked users.
|
||||
///
|
||||
/// The OlmMachine maintains a list of users whose devices we are keeping
|
||||
/// track of: these are known as "tracked users". These must be users
|
||||
/// that we share a room with, so that the server sends us updates for
|
||||
/// their device lists.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users` - An array of user ids that should be added to the list of
|
||||
/// tracked users
|
||||
///
|
||||
/// Any users that hadn't been seen before will be flagged for a key query
|
||||
/// immediately, and whenever `receive_sync_changes` receives a
|
||||
/// "changed" notification for that user in the future.
|
||||
///
|
||||
/// Users that were already in the list are unaffected.
|
||||
#[wasm_bindgen(js_name = "updateTrackedUsers")]
|
||||
pub fn update_tracked_users(&self, users: &Array) -> Result<Promise, JsError> {
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
me.update_tracked_users(users.iter().map(AsRef::as_ref)).await?;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Handle to-device events and one-time key counts from a sync
|
||||
/// response.
|
||||
///
|
||||
/// This will decrypt and handle to-device events returning the
|
||||
/// decrypted versions of them.
|
||||
///
|
||||
/// To decrypt an event from the room timeline call
|
||||
/// `decrypt_room_event`.
|
||||
#[wasm_bindgen(js_name = "receiveSyncChanges")]
|
||||
pub fn receive_sync_changes(
|
||||
&self,
|
||||
to_device_events: &str,
|
||||
changed_devices: &sync_events::DeviceLists,
|
||||
one_time_key_counts: &Map,
|
||||
unused_fallback_keys: &Set,
|
||||
) -> Result<Promise, JsError> {
|
||||
let to_device_events = serde_json::from_str(to_device_events)?;
|
||||
let changed_devices = changed_devices.inner.clone();
|
||||
let one_time_key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = one_time_key_counts
|
||||
.entries()
|
||||
.into_iter()
|
||||
.filter_map(|js_value| {
|
||||
let pair = Array::from(&js_value.ok()?);
|
||||
let (key, value) = (
|
||||
DeviceKeyAlgorithm::from(pair.at(0).as_string()?),
|
||||
UInt::new(pair.at(1).as_f64()? as u64)?,
|
||||
);
|
||||
|
||||
Some((key, value))
|
||||
})
|
||||
.collect();
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> = Some(
|
||||
unused_fallback_keys
|
||||
.values()
|
||||
.into_iter()
|
||||
.filter_map(|js_value| Some(DeviceKeyAlgorithm::from(js_value.ok()?.as_string()?)))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.receive_sync_changes(
|
||||
to_device_events,
|
||||
&changed_devices,
|
||||
&one_time_key_counts,
|
||||
unused_fallback_keys.as_deref(),
|
||||
)
|
||||
.await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the outgoing requests that need to be sent out.
|
||||
///
|
||||
/// This returns a list of `JsValue` to represent either:
|
||||
/// * `KeysUploadRequest`,
|
||||
/// * `KeysQueryRequest`,
|
||||
/// * `KeysClaimRequest`,
|
||||
/// * `ToDeviceRequest`,
|
||||
/// * `SignatureUploadRequest`,
|
||||
/// * `RoomMessageRequest` or
|
||||
/// * `KeysBackupRequest`.
|
||||
///
|
||||
/// Those requests need to be sent out to the server and the
|
||||
/// responses need to be passed back to the state machine using
|
||||
/// `mark_request_as_sent`.
|
||||
#[wasm_bindgen(js_name = "outgoingRequests")]
|
||||
pub fn outgoing_requests(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me
|
||||
.outgoing_requests()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(OutgoingRequest)
|
||||
.map(TryFrom::try_from)
|
||||
.collect::<Result<Vec<JsValue>, _>>()?
|
||||
.into_iter()
|
||||
.collect::<Array>())
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark the request with the given request ID as sent (see
|
||||
/// `outgoing_requests`).
|
||||
///
|
||||
/// Arguments are:
|
||||
///
|
||||
/// * `request_id` represents the unique ID of the request that was sent
|
||||
/// out. This is needed to couple the response with the now sent out
|
||||
/// request.
|
||||
/// * `response_type` represents the type of the request that was sent out.
|
||||
/// * `response` represents the response that was received from the server
|
||||
/// after the outgoing request was sent out.
|
||||
#[wasm_bindgen(js_name = "markRequestAsSent")]
|
||||
pub fn mark_request_as_sent(
|
||||
&self,
|
||||
request_id: &str,
|
||||
request_type: requests::RequestType,
|
||||
response: &str,
|
||||
) -> Result<Promise, JsError> {
|
||||
let transaction_id = OwnedTransactionId::from(request_id);
|
||||
let response = response_from_string(response)?;
|
||||
let incoming_response = responses::OwnedResponse::try_from((request_type, response))?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(me.mark_request_as_sent(&transaction_id, &incoming_response).await.map(|_| true)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encrypt a room message for the given room.
|
||||
///
|
||||
/// Beware that a room key needs to be shared before this
|
||||
/// method can be called using the `share_room_key` method.
|
||||
///
|
||||
/// `room_id` is the ID of the room for which the message should
|
||||
/// be encrypted. `event_type` is the type of the event. `content`
|
||||
/// is the plaintext content of the message that should be
|
||||
/// encrypted.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if a group session for the given room wasn't shared
|
||||
/// beforehand.
|
||||
#[wasm_bindgen(js_name = "encryptRoomEvent")]
|
||||
pub fn encrypt_room_event(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
event_type: String,
|
||||
content: &str,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let content: JsonValue = serde_json::from_str(content)?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.encrypt_room_event_raw(&room_id, content, event_type.as_ref()).await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// 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: Raw<_> = 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))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the status of the private cross signing keys.
|
||||
///
|
||||
/// This can be used to check which private cross signing keys we
|
||||
/// have stored locally.
|
||||
#[wasm_bindgen(js_name = "crossSigningStatus")]
|
||||
pub fn cross_signing_status(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, olm::CrossSigningStatus>(async move {
|
||||
Ok(me.cross_signing_status().await.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Export all the private cross signing keys we have.
|
||||
///
|
||||
/// The export will contain the seed for the ed25519 keys as a
|
||||
/// unpadded base64 encoded string.
|
||||
///
|
||||
/// This method returns None if we don’t have any private cross
|
||||
/// signing keys.
|
||||
#[wasm_bindgen(js_name = "exportCrossSigningKeys")]
|
||||
pub fn export_cross_signing_keys(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me.export_cross_signing_keys().await.map(store::CrossSigningKeyExport::from))
|
||||
})
|
||||
}
|
||||
|
||||
/// Import our private cross signing keys.
|
||||
///
|
||||
/// The export needs to contain the seed for the ed25519 keys as
|
||||
/// an unpadded base64 encoded string.
|
||||
#[wasm_bindgen(js_name = "importCrossSigningKeys")]
|
||||
pub fn import_cross_signing_keys(&self, export: store::CrossSigningKeyExport) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
let export = export.inner;
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me.import_cross_signing_keys(export).await.map(olm::CrossSigningStatus::from)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new cross signing identity and get the upload request
|
||||
/// to push the new public keys to the server.
|
||||
///
|
||||
/// Warning: This will delete any existing cross signing keys that
|
||||
/// might exist on the server and thus will reset the trust
|
||||
/// between all the devices.
|
||||
///
|
||||
/// Uploading these keys will require user interactive auth.
|
||||
#[wasm_bindgen(js_name = "bootstrapCrossSigning")]
|
||||
pub fn bootstrap_cross_signing(&self, reset: bool) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
let (upload_signing_keys_request, upload_signatures_request) =
|
||||
me.bootstrap_cross_signing(reset).await?;
|
||||
|
||||
let tuple = Array::new();
|
||||
tuple.set(
|
||||
0,
|
||||
requests::SigningKeysUploadRequest::try_from(&upload_signing_keys_request)?.into(),
|
||||
);
|
||||
tuple.set(
|
||||
1,
|
||||
requests::SignatureUploadRequest::try_from(&upload_signatures_request)?.into(),
|
||||
);
|
||||
|
||||
Ok(tuple)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the cross signing user identity of a user.
|
||||
#[wasm_bindgen(js_name = "getIdentity")]
|
||||
pub fn get_identity(&self, user_id: &identifiers::UserId) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
let user_id = user_id.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me.get_identity(user_id.as_ref(), None).await?.map(identities::UserIdentities::from))
|
||||
})
|
||||
}
|
||||
|
||||
/// Sign the given message using our device key and if available
|
||||
/// cross-signing master key.
|
||||
pub fn sign(&self, message: String) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, types::Signatures>(async move { Ok(me.sign(&message).await.into()) })
|
||||
}
|
||||
|
||||
/// Invalidate the currently active outbound group session for the
|
||||
/// given room.
|
||||
///
|
||||
/// Returns true if a session was invalidated, false if there was
|
||||
/// no session to invalidate.
|
||||
#[wasm_bindgen(js_name = "invalidateGroupSession")]
|
||||
pub fn invalidate_group_session(&self, room_id: &identifiers::RoomId) -> Promise {
|
||||
let room_id = room_id.inner.clone();
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.invalidate_group_session(&room_id).await?) })
|
||||
}
|
||||
|
||||
/// Get to-device requests to share a room key with users in a room.
|
||||
///
|
||||
/// `room_id` is the room ID. `users` is an array of `UserId`
|
||||
/// objects. `encryption_settings` are an `EncryptionSettings`
|
||||
/// object.
|
||||
///
|
||||
/// Returns an array of `ToDeviceRequest`s.
|
||||
#[wasm_bindgen(js_name = "shareRoomKey")]
|
||||
pub fn share_room_key(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
users: &Array,
|
||||
encryption_settings: &encryption::EncryptionSettings,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
let encryption_settings =
|
||||
matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let to_device_requests = me
|
||||
.share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings)
|
||||
.await?;
|
||||
|
||||
// convert each request to our own ToDeviceRequest struct, and then wrap it in a
|
||||
// JsValue.
|
||||
//
|
||||
// Then collect the results into a javascript Array, throwing any errors into
|
||||
// the promise.
|
||||
Ok(to_device_requests
|
||||
.into_iter()
|
||||
.map(|td| ToDeviceRequest::try_from(td.deref()).map(JsValue::from))
|
||||
.collect::<Result<Array, _>>()?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the a key claiming request for the user/device pairs that
|
||||
/// we are missing Olm sessions for.
|
||||
///
|
||||
/// Returns `null` if no key claiming request needs to be sent
|
||||
/// out, otherwise it returns a `KeysClaimRequest` object.
|
||||
///
|
||||
/// Sessions need to be established between devices so group
|
||||
/// sessions for a room can be shared with them.
|
||||
///
|
||||
/// This should be called every time a group session needs to be
|
||||
/// shared as well as between sync calls. After a sync some
|
||||
/// devices may request room keys without us having a valid Olm
|
||||
/// session with them, making it impossible to server the room key
|
||||
/// request, thus it’s necessary to check for missing sessions
|
||||
/// between sync as well.
|
||||
///
|
||||
/// Note: Care should be taken that only one such request at a
|
||||
/// time is in flight, e.g. using a lock.
|
||||
///
|
||||
/// The response of a successful key claiming requests needs to be
|
||||
/// passed to the `OlmMachine` with the `mark_request_as_sent`.
|
||||
///
|
||||
/// `users` represents the list of users that we should check if
|
||||
/// we lack a session with one of their devices. This can be an
|
||||
/// empty iterator when calling this method between sync requests.
|
||||
#[wasm_bindgen(js_name = "getMissingSessions")]
|
||||
pub fn get_missing_sessions(&self, users: &Array) -> Result<Promise, JsError> {
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
match me.get_missing_sessions(users.iter().map(AsRef::as_ref)).await? {
|
||||
Some((transaction_id, keys_claim_request)) => {
|
||||
Ok(JsValue::from(requests::KeysClaimRequest::try_from((
|
||||
transaction_id.to_string(),
|
||||
&keys_claim_request,
|
||||
))?))
|
||||
}
|
||||
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a map holding all the devices of a user.
|
||||
///
|
||||
/// `user_id` represents the unique ID of the user that the
|
||||
/// devices belong to.
|
||||
#[wasm_bindgen(js_name = "getUserDevices")]
|
||||
pub fn get_user_devices(&self, user_id: &identifiers::UserId) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, device::UserDevices>(async move {
|
||||
// wait for up to a second for any in-flight device list requests to complete.
|
||||
// The reason for this isn't so much to avoid races (some level of raciness is
|
||||
// inevitable for this method) but to make testing easier.
|
||||
Ok(me.get_user_devices(&user_id, Some(Duration::from_secs(1))).await.map(Into::into)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a specific device of a user if one is found and the crypto store
|
||||
/// didn't throw an error.
|
||||
///
|
||||
/// `user_id` represents the unique ID of the user that the
|
||||
/// identity belongs to. `device_id` represents the unique ID of
|
||||
/// the device.
|
||||
#[wasm_bindgen(js_name = "getDevice")]
|
||||
pub fn get_device(
|
||||
&self,
|
||||
user_id: &identifiers::UserId,
|
||||
device_id: &identifiers::DeviceId,
|
||||
) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
let device_id = device_id.inner.clone();
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, Option<device::Device>>(async move {
|
||||
Ok(me.get_device(&user_id, &device_id, None).await?.map(Into::into))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a verification object for the given user ID with the given
|
||||
/// flow ID (a to-device request ID if the verification has been
|
||||
/// requested by a to-device request, or a room event ID if the
|
||||
/// verification has been requested by a room event).
|
||||
///
|
||||
/// It returns a “`Verification` object”, which is either a `Sas`
|
||||
/// or `Qr` object.
|
||||
#[wasm_bindgen(js_name = "getVerification")]
|
||||
pub fn get_verification(
|
||||
&self,
|
||||
user_id: &identifiers::UserId,
|
||||
flow_id: &str,
|
||||
) -> Result<JsValue, JsError> {
|
||||
self.inner
|
||||
.get_verification(&user_id.inner, flow_id)
|
||||
.map(verification::Verification)
|
||||
.map(JsValue::try_from)
|
||||
.transpose()
|
||||
.map(JsValue::from)
|
||||
}
|
||||
|
||||
/// Get a verification request object with the given flow ID.
|
||||
#[wasm_bindgen(js_name = "getVerificationRequest")]
|
||||
pub fn get_verification_request(
|
||||
&self,
|
||||
user_id: &identifiers::UserId,
|
||||
flow_id: &str,
|
||||
) -> Option<verification::VerificationRequest> {
|
||||
self.inner.get_verification_request(&user_id.inner, flow_id).map(Into::into)
|
||||
}
|
||||
|
||||
/// Get all the verification requests of a given user.
|
||||
#[wasm_bindgen(js_name = "getVerificationRequests")]
|
||||
pub fn get_verification_requests(&self, user_id: &identifiers::UserId) -> Array {
|
||||
self.inner
|
||||
.get_verification_requests(&user_id.inner)
|
||||
.into_iter()
|
||||
.map(verification::VerificationRequest::from)
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Receive a verification event.
|
||||
///
|
||||
/// This method can be used to pass verification events that are happening
|
||||
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
|
||||
#[wasm_bindgen(js_name = "receiveVerificationEvent")]
|
||||
pub fn receive_verification_event(
|
||||
&self,
|
||||
event: &str,
|
||||
room_id: &identifiers::RoomId,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let event: ruma::events::AnySyncMessageLikeEvent = serde_json::from_str(event)?;
|
||||
let event = event.into_full_event(room_id);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(me.receive_verification_event(&event).await.map(|_| JsValue::UNDEFINED)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Export the keys that match the given predicate.
|
||||
///
|
||||
/// `predicate` is a closure that will be called for every known
|
||||
/// `InboundGroupSession`, which represents a room key. If the closure
|
||||
/// returns `true`, the `InboundGroupSession` will be included in the
|
||||
/// export, otherwise it won't.
|
||||
#[wasm_bindgen(js_name = "exportRoomKeys")]
|
||||
pub fn export_room_keys(&self, predicate: Function) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.export_room_keys(|session| {
|
||||
let session = session.clone();
|
||||
|
||||
predicate
|
||||
.call1(&JsValue::NULL, &olm::InboundGroupSession::from(session).into())
|
||||
.expect("Predicate function passed to `export_room_keys` failed")
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.await?,
|
||||
)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
///
|
||||
/// `exported_keys` is a list of previously exported keys that should be
|
||||
/// imported into our store. If we already have a better version of a key,
|
||||
/// the key will _not_ be imported.
|
||||
///
|
||||
/// `progress_listener` is a closure that takes 2 arguments: `progress` and
|
||||
/// `total`, and returns nothing.
|
||||
#[wasm_bindgen(js_name = "importRoomKeys")]
|
||||
pub fn import_room_keys(
|
||||
&self,
|
||||
exported_room_keys: &str,
|
||||
progress_listener: Function,
|
||||
) -> Result<Promise, JsError> {
|
||||
let me = self.inner.clone();
|
||||
let exported_room_keys: Vec<matrix_sdk_crypto::olm::ExportedRoomKey> =
|
||||
serde_json::from_str(exported_room_keys)?;
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let matrix_sdk_crypto::RoomKeyImportResult { imported_count, total_count, keys } = me
|
||||
.import_room_keys(exported_room_keys, false, |progress, total| {
|
||||
let progress: u64 = progress.try_into().unwrap();
|
||||
let total: u64 = total.try_into().unwrap();
|
||||
|
||||
progress_listener
|
||||
.call2(&JsValue::NULL, &JsValue::from(progress), &JsValue::from(total))
|
||||
.expect("Progress listener passed to `import_room_keys` failed");
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(serde_json::to_string(&json!({
|
||||
"imported_count": imported_count,
|
||||
"total_count": total_count,
|
||||
"keys": keys,
|
||||
}))?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encrypt the list of exported room keys using the given passphrase.
|
||||
///
|
||||
/// `exported_room_keys` is a list of sessions that should be encrypted
|
||||
/// (it's generally returned by `export_room_keys`). `passphrase` is the
|
||||
/// passphrase that will be used to encrypt the exported room keys. And
|
||||
/// `rounds` is the number of rounds that should be used for the key
|
||||
/// derivation when the passphrase gets turned into an AES key. More rounds
|
||||
/// are increasingly computationnally intensive and as such help against
|
||||
/// brute-force attacks. Should be at least `10_000`, while values in the
|
||||
/// `100_000` ranges should be preferred.
|
||||
#[wasm_bindgen(js_name = "encryptExportedRoomKeys")]
|
||||
pub fn encrypt_exported_room_keys(
|
||||
exported_room_keys: &str,
|
||||
passphrase: &str,
|
||||
rounds: u32,
|
||||
) -> Result<String, JsError> {
|
||||
let exported_room_keys: Vec<matrix_sdk_crypto::olm::ExportedRoomKey> =
|
||||
serde_json::from_str(exported_room_keys)?;
|
||||
|
||||
Ok(matrix_sdk_crypto::encrypt_room_key_export(&exported_room_keys, passphrase, rounds)?)
|
||||
}
|
||||
|
||||
/// Try to decrypt a reader into a list of exported room keys.
|
||||
///
|
||||
/// `encrypted_exported_room_keys` is the result from
|
||||
/// `encrypt_exported_room_keys`. `passphrase` is the passphrase that was
|
||||
/// used when calling `encrypt_exported_room_keys`.
|
||||
#[wasm_bindgen(js_name = "decryptExportedRoomKeys")]
|
||||
pub fn decrypt_exported_room_keys(
|
||||
encrypted_exported_room_keys: &str,
|
||||
passphrase: &str,
|
||||
) -> Result<String, JsError> {
|
||||
Ok(serde_json::to_string(&matrix_sdk_crypto::decrypt_room_key_export(
|
||||
encrypted_exported_room_keys.as_bytes(),
|
||||
passphrase,
|
||||
)?)?)
|
||||
}
|
||||
|
||||
/// Register a callback which will be called whenever there is an update to
|
||||
/// a room key.
|
||||
///
|
||||
/// `callback` should be a function that takes a single argument (an array
|
||||
/// of {@link RoomKeyInfo}) and returns a Promise.
|
||||
#[wasm_bindgen(js_name = "registerRoomKeyUpdatedCallback")]
|
||||
pub async fn register_room_key_updated_callback(&self, callback: Function) {
|
||||
let stream = self.inner.store().room_keys_received_stream();
|
||||
|
||||
// fire up a promise chain which will call `cb` on each result from the stream
|
||||
spawn_local(async move {
|
||||
// take a reference to `callback` (which we then pass into the closure), to stop
|
||||
// the callback being moved into the closure (which would mean we could only
|
||||
// call the closure once)
|
||||
let callback_ref = &callback;
|
||||
stream.for_each(move |item| send_room_key_info_to_callback(callback_ref, item)).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Shut down the `OlmMachine`.
|
||||
///
|
||||
/// The `OlmMachine` cannot be used after this method has been called.
|
||||
///
|
||||
/// All associated resources will be closed too, like IndexedDB
|
||||
/// connections.
|
||||
pub fn close(self) {}
|
||||
}
|
||||
|
||||
// helper for register_room_key_received_callback: wraps the key info
|
||||
// into our own RoomKeyInfo struct, and passes it into the javascript
|
||||
// function
|
||||
async fn send_room_key_info_to_callback(
|
||||
callback: &Function,
|
||||
room_key_info: Vec<matrix_sdk_crypto::store::RoomKeyInfo>,
|
||||
) {
|
||||
let rki: Array = room_key_info.into_iter().map(RoomKeyInfo::from).map(JsValue::from).collect();
|
||||
match promise_result_to_future(callback.call1(&JsValue::NULL, &rki)).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
warn!("Error calling room-key-received callback: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a result from a javascript function which returns a Promise (or throws
|
||||
/// an exception before returning one), convert the result to a rust Future
|
||||
/// which completes with the result of the promise
|
||||
async fn promise_result_to_future(res: Result<JsValue, JsValue>) -> Result<JsValue, JsValue> {
|
||||
match res {
|
||||
Ok(retval) => {
|
||||
if !retval.has_type::<Promise>() {
|
||||
panic!("not a promise");
|
||||
}
|
||||
let prom: Promise = retval.dyn_into().map_err(|v| {
|
||||
JsError::new(&format!("function returned a non-Promise value {v:?}"))
|
||||
})?;
|
||||
JsFuture::from(prom).await
|
||||
}
|
||||
Err(e) => {
|
||||
// the function threw an exception before it returned the promise. We can just
|
||||
// return the error as an error result.
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// We have the following pattern quite often in our code:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct Foo {
|
||||
/// inner: Bar,
|
||||
/// }
|
||||
///
|
||||
/// impl From<Bar> for Foo {
|
||||
/// fn from(inner: Bar) -> Self {
|
||||
/// Self { inner }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Because I feel lazy, let's do a macro to write this:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct Foo {
|
||||
/// inner: Bar,
|
||||
/// }
|
||||
///
|
||||
/// impl_from_to_inner!(Bar => Foo);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! impl_from_to_inner {
|
||||
($from:ty => $to:ty) => {
|
||||
impl From<$from> for $to {
|
||||
fn from(inner: $from) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Olm types.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{identifiers, impl_from_to_inner, vodozemac::Curve25519PublicKey};
|
||||
|
||||
/// Struct representing the state of our private cross signing keys,
|
||||
/// it shows which private cross signing keys we have locally stored.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct CrossSigningStatus {
|
||||
inner: matrix_sdk_crypto::olm::CrossSigningStatus,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::olm::CrossSigningStatus => CrossSigningStatus);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl CrossSigningStatus {
|
||||
/// Do we have the master key?
|
||||
#[wasm_bindgen(getter, js_name = "hasMaster")]
|
||||
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.
|
||||
#[wasm_bindgen(getter, js_name = "hasSelfSigning")]
|
||||
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.
|
||||
#[wasm_bindgen(getter, js_name = "hasUserSigning")]
|
||||
pub fn has_user_signing(&self) -> bool {
|
||||
self.inner.has_user_signing
|
||||
}
|
||||
}
|
||||
|
||||
/// Inbound group session.
|
||||
///
|
||||
/// Inbound group sessions are used to exchange room messages between a group of
|
||||
/// participants. Inbound group sessions are used to decrypt the room messages.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct InboundGroupSession {
|
||||
inner: matrix_sdk_crypto::olm::InboundGroupSession,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::olm::InboundGroupSession => InboundGroupSession);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl InboundGroupSession {
|
||||
/// The room where this session is used in.
|
||||
#[wasm_bindgen(getter, js_name = "roomId")]
|
||||
pub fn room_id(&self) -> identifiers::RoomId {
|
||||
self.inner.room_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// The Curve25519 key of the sender of this session, as a
|
||||
/// [Curve25519PublicKey].
|
||||
#[wasm_bindgen(getter, js_name = "senderKey")]
|
||||
pub fn sender_key(&self) -> Curve25519PublicKey {
|
||||
self.inner.sender_key().into()
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
#[wasm_bindgen(getter, js_name = "sessionId")]
|
||||
pub fn session_id(&self) -> String {
|
||||
self.inner.session_id().to_owned()
|
||||
}
|
||||
|
||||
/// Has the session been imported from a file or server-side backup? As
|
||||
/// opposed to being directly received as an `m.room_key` event.
|
||||
#[wasm_bindgen(js_name = "hasBeenImported")]
|
||||
pub fn has_been_imported(&self) -> bool {
|
||||
self.inner.has_been_imported()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
//! Types to handle requests.
|
||||
|
||||
use js_sys::JsString;
|
||||
use matrix_sdk_crypto::{
|
||||
requests::{
|
||||
KeysBackupRequest as OriginalKeysBackupRequest,
|
||||
KeysQueryRequest as OriginalKeysQueryRequest,
|
||||
RoomMessageRequest as OriginalRoomMessageRequest,
|
||||
ToDeviceRequest as OriginalToDeviceRequest,
|
||||
UploadSigningKeysRequest as OriginalUploadSigningKeysRequest,
|
||||
},
|
||||
OutgoingRequests,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::keys::{
|
||||
claim_keys::v3::Request as OriginalKeysClaimRequest,
|
||||
upload_keys::v3::Request as OriginalKeysUploadRequest,
|
||||
upload_signatures::v3::Request as OriginalSignatureUploadRequest,
|
||||
},
|
||||
events::EventContent,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/** Outgoing Requests * */
|
||||
|
||||
/// Data for a request to the `/keys/upload` API endpoint
|
||||
/// ([specification]).
|
||||
///
|
||||
/// Publishes end-to-end encryption keys for the device.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysupload
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysUploadRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `device_keys`,
|
||||
/// `one_time_keys`, `fallback_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[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: Some(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]).
|
||||
///
|
||||
/// Returns the current devices and identity keys for the given users.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysquery
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysQueryRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `timeout`,
|
||||
/// `device_keys`, `token`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysQueryRequest {
|
||||
/// Create a new `KeysQueryRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysQueryRequest {
|
||||
Self { id: Some(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]).
|
||||
///
|
||||
/// Claims one-time keys that can be used to establish 1-to-1 E2EE
|
||||
/// sessions.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysclaim
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysClaimRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `timeout`,
|
||||
/// `one_time_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysClaimRequest {
|
||||
/// Create a new `KeysClaimRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysClaimRequest {
|
||||
Self { id: Some(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]).
|
||||
///
|
||||
/// Send an event to a single device or to a group of devices.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3sendtodeviceeventtypetxnid
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct ToDeviceRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A string representing the type of event being sent to each devices.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub event_type: JsString,
|
||||
|
||||
/// A string representing a request identifier unique to the access token
|
||||
/// used to send the request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub txn_id: JsString,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `messages`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ToDeviceRequest {
|
||||
/// Create a new `ToDeviceRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
id: JsString,
|
||||
event_type: JsString,
|
||||
txn_id: JsString,
|
||||
body: JsString,
|
||||
) -> ToDeviceRequest {
|
||||
Self { id: Some(id), event_type, txn_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]).
|
||||
///
|
||||
/// Publishes cross-signing signatures for the user.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keyssignaturesupload
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct SignatureUploadRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `signed_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SignatureUploadRequest {
|
||||
/// Create a new `SignatureUploadRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> SignatureUploadRequest {
|
||||
Self { id: Some(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]).
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct RoomMessageRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A string representing the room to send the event to.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub room_id: JsString,
|
||||
|
||||
/// A string representing the transaction ID for this event.
|
||||
///
|
||||
/// Clients should generate an ID unique across requests with the same
|
||||
/// access token; it will be used by the server to ensure idempotency of
|
||||
/// requests.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub txn_id: JsString,
|
||||
|
||||
/// A string representing the type of event to be sent.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub event_type: JsString,
|
||||
|
||||
/// A JSON-encoded string containing the message's content.
|
||||
#[wasm_bindgen(readonly, js_name = "body")]
|
||||
pub content: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomMessageRequest {
|
||||
/// Create a new `RoomMessageRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
id: JsString,
|
||||
room_id: JsString,
|
||||
txn_id: JsString,
|
||||
event_type: JsString,
|
||||
content: JsString,
|
||||
) -> RoomMessageRequest {
|
||||
Self { id: Some(id), room_id, txn_id, event_type, content }
|
||||
}
|
||||
|
||||
/// 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]).
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3room_keyskeys
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysBackupRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `rooms`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysBackupRequest {
|
||||
/// Create a new `KeysBackupRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysBackupRequest {
|
||||
Self { id: Some(id), body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::KeysBackup
|
||||
}
|
||||
}
|
||||
|
||||
/** Other Requests * */
|
||||
|
||||
/// Request that will publish a cross signing identity.
|
||||
///
|
||||
/// This uploads the public cross signing key triplet.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct SigningKeysUploadRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `master_key`,
|
||||
/// `self_signing_key`, `user_signing_key`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
macro_rules! request {
|
||||
(
|
||||
$destination_request:ident from $source_request:ident
|
||||
$( extracts $( $field_name:ident : $field_type:tt ),+ $(,)? )?
|
||||
$( $( and )? groups $( $grouped_field_name:ident ),+ $(,)? )?
|
||||
) => {
|
||||
impl TryFrom<&$source_request> for $destination_request {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(request: &$source_request) -> Result<Self, Self::Error> {
|
||||
request!(
|
||||
@__try_from $destination_request from $source_request
|
||||
(request_id = None, request = request)
|
||||
$( extracts [ $( $field_name : $field_type, )+ ] )?
|
||||
$( groups [ $( $grouped_field_name, )+ ] )?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(String, &$source_request)> for $destination_request {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(
|
||||
(request_id, request): (String, &$source_request),
|
||||
) -> Result<Self, Self::Error> {
|
||||
request!(
|
||||
@__try_from $destination_request from $source_request
|
||||
(request_id = Some(request_id.into()), request = request)
|
||||
$( extracts [ $( $field_name : $field_type, )+ ] )?
|
||||
$( groups [ $( $grouped_field_name, )+ ] )?
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@__try_from $destination_request:ident from $source_request:ident
|
||||
(request_id = $request_id:expr, request = $request:expr)
|
||||
$( extracts [ $( $field_name:ident : $field_type:tt ),* $(,)? ] )?
|
||||
$( groups [ $( $grouped_field_name:ident ),* $(,)? ] )?
|
||||
) => {
|
||||
{
|
||||
Ok($destination_request {
|
||||
id: $request_id,
|
||||
$(
|
||||
$(
|
||||
$field_name: request!(@__field $field_name : $field_type ; request = $request),
|
||||
)*
|
||||
)?
|
||||
$(
|
||||
body: {
|
||||
let mut map = serde_json::Map::new();
|
||||
$(
|
||||
map.insert(stringify!($grouped_field_name).to_owned(), serde_json::to_value(&$request.$grouped_field_name).unwrap());
|
||||
)*
|
||||
let object = serde_json::Value::Object(map);
|
||||
|
||||
serde_json::to_string(&object)?.into()
|
||||
}
|
||||
)?
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
( @__field $field_name:ident : $field_type:ident ; request = $request:expr ) => {
|
||||
request!(@__field_type as $field_type ; request = $request, field_name = $field_name)
|
||||
};
|
||||
|
||||
( @__field_type as string ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
$request.$field_name.to_string().into()
|
||||
};
|
||||
|
||||
( @__field_type as json ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
serde_json::to_string(&$request.$field_name)?.into()
|
||||
};
|
||||
|
||||
( @__field_type as event_type ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
$request.content.event_type().to_string().into()
|
||||
};
|
||||
}
|
||||
|
||||
// Outgoing Requests
|
||||
request!(KeysUploadRequest from OriginalKeysUploadRequest groups device_keys, one_time_keys, fallback_keys);
|
||||
request!(KeysQueryRequest from OriginalKeysQueryRequest groups timeout, device_keys, token);
|
||||
request!(KeysClaimRequest from OriginalKeysClaimRequest groups timeout, one_time_keys);
|
||||
request!(ToDeviceRequest from OriginalToDeviceRequest extracts event_type: string, txn_id: string and groups messages);
|
||||
request!(SignatureUploadRequest from OriginalSignatureUploadRequest groups signed_keys);
|
||||
request!(RoomMessageRequest from OriginalRoomMessageRequest extracts room_id: string, txn_id: string, event_type: event_type, content: json);
|
||||
request!(KeysBackupRequest from OriginalKeysBackupRequest groups rooms);
|
||||
|
||||
// Other Requests
|
||||
request!(SigningKeysUploadRequest from OriginalUploadSigningKeysRequest groups master_key, self_signing_key, user_signing_key);
|
||||
|
||||
// JavaScript has no complex enums like Rust. To return structs of
|
||||
// different types, we have no choice that hiding everything behind a
|
||||
// `JsValue`.
|
||||
pub(crate) struct OutgoingRequest(pub(crate) matrix_sdk_crypto::OutgoingRequest);
|
||||
|
||||
impl TryFrom<OutgoingRequest> for JsValue {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(outgoing_request: OutgoingRequest) -> Result<Self, Self::Error> {
|
||||
let request_id = outgoing_request.0.request_id().to_string();
|
||||
|
||||
Ok(match outgoing_request.0.request() {
|
||||
OutgoingRequests::KeysUpload(request) => {
|
||||
JsValue::from(KeysUploadRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::KeysQuery(request) => {
|
||||
JsValue::from(KeysQueryRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::KeysClaim(request) => {
|
||||
JsValue::from(KeysClaimRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
JsValue::from(ToDeviceRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::SignatureUpload(request) => {
|
||||
JsValue::from(SignatureUploadRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::RoomMessage(request) => {
|
||||
JsValue::from(RoomMessageRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::KeysBackup(request) => {
|
||||
JsValue::from(KeysBackupRequest::try_from((request_id, request))?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the type of a request.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum RequestType {
|
||||
/// Represents a `KeysUploadRequest`.
|
||||
KeysUpload,
|
||||
|
||||
/// Represents a `KeysQueryRequest`.
|
||||
KeysQuery,
|
||||
|
||||
/// Represents a `KeysClaimRequest`.
|
||||
KeysClaim,
|
||||
|
||||
/// Represents a `ToDeviceRequest`.
|
||||
ToDevice,
|
||||
|
||||
/// Represents a `SignatureUploadRequest`.
|
||||
SignatureUpload,
|
||||
|
||||
/// Represents a `RoomMessageRequest`.
|
||||
RoomMessage,
|
||||
|
||||
/// Represents a `KeysBackupRequest`.
|
||||
KeysBackup,
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Types related to responses.
|
||||
|
||||
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,
|
||||
keys::{
|
||||
claim_keys::v3::Response as KeysClaimResponse, get_keys::v3::Response as KeysQueryResponse,
|
||||
upload_keys::v3::Response as KeysUploadResponse,
|
||||
upload_signatures::v3::Response as SignatureUploadResponse,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
};
|
||||
use ruma::api::IncomingResponse as RumaIncomingResponse;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{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())
|
||||
}
|
||||
|
||||
/// Intermediate private type to store an incoming owned response,
|
||||
/// without the need to manage lifetime.
|
||||
pub(crate) enum OwnedResponse {
|
||||
KeysUpload(KeysUploadResponse),
|
||||
KeysQuery(KeysQueryResponse),
|
||||
KeysClaim(KeysClaimResponse),
|
||||
ToDevice(ToDeviceResponse),
|
||||
SignatureUpload(SignatureUploadResponse),
|
||||
RoomMessage(RoomMessageResponse),
|
||||
KeysBackup(KeysBackupResponse),
|
||||
}
|
||||
|
||||
impl From<KeysUploadResponse> for OwnedResponse {
|
||||
fn from(response: KeysUploadResponse) -> Self {
|
||||
OwnedResponse::KeysUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysQueryResponse> for OwnedResponse {
|
||||
fn from(response: KeysQueryResponse) -> Self {
|
||||
OwnedResponse::KeysQuery(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysClaimResponse> for OwnedResponse {
|
||||
fn from(response: KeysClaimResponse) -> Self {
|
||||
OwnedResponse::KeysClaim(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceResponse> for OwnedResponse {
|
||||
fn from(response: ToDeviceResponse) -> Self {
|
||||
OwnedResponse::ToDevice(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureUploadResponse> for OwnedResponse {
|
||||
fn from(response: SignatureUploadResponse) -> Self {
|
||||
Self::SignatureUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomMessageResponse> for OwnedResponse {
|
||||
fn from(response: RoomMessageResponse) -> Self {
|
||||
OwnedResponse::RoomMessage(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysBackupResponse> for OwnedResponse {
|
||||
fn from(r: KeysBackupResponse) -> Self {
|
||||
Self::KeysBackup(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(RequestType, http::Response<Vec<u8>>)> for OwnedResponse {
|
||||
type Error = JsError;
|
||||
|
||||
fn try_from(
|
||||
(request_type, response): (RequestType, http::Response<Vec<u8>>),
|
||||
) -> Result<Self, Self::Error> {
|
||||
match request_type {
|
||||
RequestType::KeysUpload => {
|
||||
KeysUploadResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::KeysQuery => {
|
||||
KeysQueryResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::KeysClaim => {
|
||||
KeysClaimResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::ToDevice => {
|
||||
ToDeviceResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::SignatureUpload => {
|
||||
SignatureUploadResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::RoomMessage => {
|
||||
RoomMessageResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::KeysBackup => {
|
||||
KeysBackupResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
}
|
||||
.map_err(JsError::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a OwnedResponse) -> Self {
|
||||
match response {
|
||||
OwnedResponse::KeysUpload(response) => IncomingResponse::KeysUpload(response),
|
||||
OwnedResponse::KeysQuery(response) => IncomingResponse::KeysQuery(response),
|
||||
OwnedResponse::KeysClaim(response) => IncomingResponse::KeysClaim(response),
|
||||
OwnedResponse::ToDevice(response) => IncomingResponse::ToDevice(response),
|
||||
OwnedResponse::SignatureUpload(response) => IncomingResponse::SignatureUpload(response),
|
||||
OwnedResponse::RoomMessage(response) => IncomingResponse::RoomMessage(response),
|
||||
OwnedResponse::KeysBackup(response) => IncomingResponse::KeysBackup(response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into())
|
||||
}
|
||||
|
||||
/// 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) -> Array {
|
||||
Array::new()
|
||||
}
|
||||
|
||||
/// 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(js_name = "shieldState")]
|
||||
pub fn shield_state(&self, strict: bool) -> Option<encryption::ShieldState> {
|
||||
let state = &self.encryption_info.as_ref()?.verification_state;
|
||||
|
||||
if strict {
|
||||
Some(state.to_shield_state_strict().into())
|
||||
} else {
|
||||
Some(state.to_shield_state_lax().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_common::deserialized_responses::TimelineEvent> for DecryptedRoomEvent {
|
||||
fn from(value: matrix_sdk_common::deserialized_responses::TimelineEvent) -> Self {
|
||||
Self {
|
||||
event: value.event.json().get().to_owned().into(),
|
||||
encryption_info: value.encryption_info,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Store types.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
encryption::EncryptionAlgorithm, identifiers::RoomId, impl_from_to_inner,
|
||||
vodozemac::Curve25519PublicKey,
|
||||
};
|
||||
|
||||
/// A struct containing private cross signing keys that can be backed
|
||||
/// up or uploaded to the secret store.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct CrossSigningKeyExport {
|
||||
pub(crate) inner: matrix_sdk_crypto::store::CrossSigningKeyExport,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::store::CrossSigningKeyExport => CrossSigningKeyExport);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl CrossSigningKeyExport {
|
||||
/// The seed of the master key encoded as unpadded base64.
|
||||
#[wasm_bindgen(getter, js_name = "masterKey")]
|
||||
pub fn master_key(&self) -> Option<String> {
|
||||
self.inner.master_key.clone()
|
||||
}
|
||||
|
||||
/// The seed of the self signing key encoded as unpadded base64.
|
||||
#[wasm_bindgen(getter, js_name = "self_signing_key")]
|
||||
pub fn self_signing_key(&self) -> Option<String> {
|
||||
self.inner.self_signing_key.clone()
|
||||
}
|
||||
|
||||
/// The seed of the user signing key encoded as unpadded base64.
|
||||
#[wasm_bindgen(getter, js_name = "userSigningKey")]
|
||||
pub fn user_signing_key(&self) -> Option<String> {
|
||||
self.inner.user_signing_key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on a room key that has been received or imported.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct RoomKeyInfo {
|
||||
pub(crate) inner: matrix_sdk_crypto::store::RoomKeyInfo,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::store::RoomKeyInfo => RoomKeyInfo);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomKeyInfo {
|
||||
/// The {@link EncryptionAlgorithm} that this key is used for. Will be one
|
||||
/// of the `m.megolm.*` algorithms.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn algorithm(&self) -> EncryptionAlgorithm {
|
||||
self.inner.algorithm.clone().into()
|
||||
}
|
||||
|
||||
/// The room where the key is used.
|
||||
#[wasm_bindgen(getter, js_name = "roomId")]
|
||||
pub fn room_id(&self) -> RoomId {
|
||||
self.inner.room_id.clone().into()
|
||||
}
|
||||
|
||||
/// The Curve25519 key of the device which initiated the session originally.
|
||||
#[wasm_bindgen(getter, js_name = "senderKey")]
|
||||
pub fn sender_key(&self) -> Curve25519PublicKey {
|
||||
self.inner.sender_key.into()
|
||||
}
|
||||
|
||||
/// The ID of the session that the key is for.
|
||||
#[wasm_bindgen(getter, js_name = "sessionId")]
|
||||
pub fn session_id(&self) -> String {
|
||||
self.inner.session_id.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//! `GET /_matrix/client/*/sync`
|
||||
|
||||
use js_sys::Array;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{identifiers, js::downcast};
|
||||
|
||||
/// Information on E2E device updates.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceLists {
|
||||
pub(crate) inner: ruma::api::client::sync::sync_events::DeviceLists,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceLists {
|
||||
/// Create an empty `DeviceLists`.
|
||||
///
|
||||
/// `changed` and `left` must be an array of `UserId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(changed: Option<Array>, left: Option<Array>) -> Result<DeviceLists, JsError> {
|
||||
let mut inner = ruma::api::client::sync::sync_events::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>>()?;
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/// Returns true if there are no device list updates.
|
||||
#[wasm_bindgen(js_name = "isEmpty")]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// List of users who have updated their device identity keys or
|
||||
/// who now share an encrypted room with the client since the
|
||||
/// previous sync
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn changed(&self) -> Array {
|
||||
self.inner
|
||||
.changed
|
||||
.iter()
|
||||
.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.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn left(&self) -> Array {
|
||||
self.inner
|
||||
.left
|
||||
.iter()
|
||||
.map(|user| identifiers::UserId::from(user.clone()))
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Logger level.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoggerLevel {
|
||||
/// `TRACE` level.
|
||||
///
|
||||
/// Designate very low priority, often extremely verbose,
|
||||
/// information.
|
||||
Trace,
|
||||
|
||||
/// `DEBUG` level.
|
||||
///
|
||||
/// Designate lower priority information.
|
||||
Debug,
|
||||
|
||||
/// `INFO` level.
|
||||
///
|
||||
/// Designate useful information.
|
||||
Info,
|
||||
|
||||
/// `WARN` level.
|
||||
///
|
||||
/// Designate hazardous situations.
|
||||
Warn,
|
||||
|
||||
/// `ERROR` level.
|
||||
///
|
||||
/// Designate very serious errors.
|
||||
Error,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
mod inner {
|
||||
use std::{
|
||||
fmt,
|
||||
fmt::Write as _,
|
||||
sync::{Arc, Once},
|
||||
};
|
||||
|
||||
use tracing::{
|
||||
field::{Field, Visit},
|
||||
metadata::LevelFilter,
|
||||
Event, Level, Metadata, Subscriber,
|
||||
};
|
||||
use tracing_subscriber::{
|
||||
layer::{Context, Layer as TracingLayer},
|
||||
prelude::*,
|
||||
reload, Registry,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
type TracingInner = Arc<reload::Handle<Layer, Registry>>;
|
||||
|
||||
/// Type to install and to manipulate the tracing layer.
|
||||
#[wasm_bindgen]
|
||||
pub struct Tracing {
|
||||
handle: TracingInner,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Tracing {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Tracing").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Tracing {
|
||||
/// Check whether the `tracing` feature has been enabled.
|
||||
#[wasm_bindgen(js_name = "isAvailable")]
|
||||
pub fn is_available() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Install the tracing layer.
|
||||
///
|
||||
/// `Tracing` is a singleton. Once it is installed,
|
||||
/// consecutive calls to the constructor will construct a new
|
||||
/// `Tracing` object but with the exact same inner
|
||||
/// state. Calling the constructor with a new `min_level` will
|
||||
/// just update the `min_level` parameter; in that regard, it
|
||||
/// is similar to calling the `min_level` method on an
|
||||
/// existing `Tracing` object.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(min_level: LoggerLevel) -> Result<Tracing, JsError> {
|
||||
static mut INSTALL: Option<TracingInner> = None;
|
||||
static INSTALLED: Once = Once::new();
|
||||
|
||||
INSTALLED.call_once(|| {
|
||||
let (filter, reload_handle) = reload::Layer::new(Layer::new(min_level.clone()));
|
||||
|
||||
tracing_subscriber::registry().with(filter).init();
|
||||
|
||||
unsafe { INSTALL = Some(Arc::new(reload_handle)) };
|
||||
});
|
||||
|
||||
let tracing = Tracing {
|
||||
handle: unsafe { INSTALL.as_ref() }
|
||||
.cloned()
|
||||
.expect("`Tracing` has not been installed correctly"),
|
||||
};
|
||||
|
||||
// If it's not the first call to `install`, the
|
||||
// `min_level` can be different. Let's update it.
|
||||
tracing.min_level(min_level);
|
||||
|
||||
Ok(tracing)
|
||||
}
|
||||
|
||||
/// Re-define the minimum logger level.
|
||||
#[wasm_bindgen(setter, js_name = "minLevel")]
|
||||
pub fn min_level(&self, min_level: LoggerLevel) {
|
||||
let _ = self.handle.modify(|layer| layer.min_level = min_level.into());
|
||||
}
|
||||
|
||||
/// Turn the logger on, i.e. it emits logs again if it was turned
|
||||
/// off.
|
||||
#[wasm_bindgen(js_name = "turnOn")]
|
||||
pub fn turn_on(&self) {
|
||||
let _ = self.handle.modify(|layer| layer.turn_on());
|
||||
}
|
||||
|
||||
/// Turn the logger off, i.e. it no long emits logs.
|
||||
#[wasm_bindgen(js_name = "turnOff")]
|
||||
pub fn turn_off(&self) {
|
||||
let _ = self.handle.modify(|layer| layer.turn_off());
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LoggerLevel> for Level {
|
||||
fn from(value: LoggerLevel) -> Self {
|
||||
use LoggerLevel::*;
|
||||
|
||||
match value {
|
||||
Trace => Self::TRACE,
|
||||
Debug => Self::DEBUG,
|
||||
Info => Self::INFO,
|
||||
Warn => Self::WARN,
|
||||
Error => Self::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "debug")]
|
||||
fn log_debug(message: String);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "info")]
|
||||
fn log_info(message: String);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "warn")]
|
||||
fn log_warn(message: String);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "error")]
|
||||
fn log_error(message: String);
|
||||
}
|
||||
|
||||
struct Layer {
|
||||
min_level: Level,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Layer {
|
||||
fn new<L>(min_level: L) -> Self
|
||||
where
|
||||
L: Into<Level>,
|
||||
{
|
||||
Self { min_level: min_level.into(), enabled: true }
|
||||
}
|
||||
|
||||
fn turn_on(&mut self) {
|
||||
self.enabled = true;
|
||||
}
|
||||
|
||||
fn turn_off(&mut self) {
|
||||
self.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> TracingLayer<S> for Layer
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
fn enabled(&self, metadata: &Metadata<'_>, _: Context<'_, S>) -> bool {
|
||||
self.enabled && metadata.level() <= &self.min_level
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<LevelFilter> {
|
||||
if !self.enabled {
|
||||
Some(LevelFilter::OFF)
|
||||
} else {
|
||||
Some(LevelFilter::from_level(self.min_level))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) {
|
||||
let mut recorder = StringVisitor::new();
|
||||
event.record(&mut recorder);
|
||||
let metadata = event.metadata();
|
||||
let level = metadata.level();
|
||||
|
||||
let origin = metadata
|
||||
.file()
|
||||
.and_then(|file| metadata.line().map(|ln| format!("{file}:{ln}")))
|
||||
.unwrap_or_default();
|
||||
|
||||
let message = format!("{level} {origin}{recorder}");
|
||||
|
||||
match *level {
|
||||
Level::TRACE => log_debug(message),
|
||||
Level::DEBUG => log_debug(message),
|
||||
Level::INFO => log_info(message),
|
||||
Level::WARN => log_warn(message),
|
||||
Level::ERROR => log_error(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StringVisitor {
|
||||
string: String,
|
||||
}
|
||||
|
||||
impl StringVisitor {
|
||||
fn new() -> Self {
|
||||
Self { string: String::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for StringVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
|
||||
match field.name() {
|
||||
"message" => {
|
||||
if !self.string.is_empty() {
|
||||
self.string.push('\n');
|
||||
}
|
||||
|
||||
let _ = write!(self.string, "{value:?}");
|
||||
}
|
||||
|
||||
field_name => {
|
||||
let _ = write!(self.string, "\n{field_name} = {value:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StringVisitor {
|
||||
fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if !self.string.is_empty() {
|
||||
write!(&mut f, " {}", self.string)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
mod inner {
|
||||
use super::*;
|
||||
|
||||
/// Type to install and to manipulate the tracing layer.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Tracing;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Tracing {
|
||||
/// Check whether the `tracing` feature has been enabled.
|
||||
#[wasm_bindgen(js_name = "isAvailable")]
|
||||
pub fn is_available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The `tracing` feature is not enabled, so this constructor
|
||||
/// will raise an error.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(_min_level: LoggerLevel) -> Result<Tracing, JsError> {
|
||||
Err(JsError::new("The `tracing` feature is disabled. Check `Tracing.isAvailable` before constructing `Tracing`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use inner::*;
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Extra types, like `Signatures`.
|
||||
|
||||
use js_sys::Map;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
identifiers::{DeviceKeyId, UserId},
|
||||
impl_from_to_inner,
|
||||
vodozemac::Ed25519Signature,
|
||||
};
|
||||
|
||||
/// A collection of `Signature`.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Signatures {
|
||||
inner: matrix_sdk_crypto::types::Signatures,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::types::Signatures => Signatures);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Signatures {
|
||||
/// Creates a new, empty, signatures collection.
|
||||
#[wasm_bindgen(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.
|
||||
#[wasm_bindgen(js_name = "addSignature")]
|
||||
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.
|
||||
#[wasm_bindgen(js_name = "getSignature")]
|
||||
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.
|
||||
pub fn get(&self, signer: &UserId) -> Option<Map> {
|
||||
let map = Map::new();
|
||||
|
||||
for (device_key_id, maybe_signature) in
|
||||
self.inner.get(signer.inner.as_ref()).map(|map| {
|
||||
map.iter().map(|(device_key_id, maybe_signature)| {
|
||||
(
|
||||
device_key_id.as_str().to_owned(),
|
||||
MaybeSignature::from(maybe_signature.clone()),
|
||||
)
|
||||
})
|
||||
})?
|
||||
{
|
||||
map.set(&device_key_id.into(), &maybe_signature.into());
|
||||
}
|
||||
|
||||
Some(map)
|
||||
}
|
||||
|
||||
/// Remove all the signatures we currently hold.
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.clear();
|
||||
}
|
||||
|
||||
/// Do we hold any signatures or is our collection completely
|
||||
/// empty.
|
||||
#[wasm_bindgen(js_name = "isEmpty")]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// How many signatures do we currently hold.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn count(&self) -> usize {
|
||||
self.inner.signature_count()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a potentially decoded signature (but not a validated
|
||||
/// one).
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Signature {
|
||||
inner: matrix_sdk_crypto::types::Signature,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::types::Signature => Signature);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Signature {
|
||||
/// Get the Ed25519 signature, if this is one.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn ed25519(&self) -> Option<Ed25519Signature> {
|
||||
self.inner.ed25519().map(Into::into)
|
||||
}
|
||||
|
||||
/// Convert the signature to a base64 encoded string.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
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.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct MaybeSignature {
|
||||
inner: MaybeSignatureInner,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(MaybeSignatureInner => MaybeSignature);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MaybeSignature {
|
||||
/// Check whether the signature has been successfully decoded.
|
||||
#[wasm_bindgen(js_name = "isValid")]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.inner.is_ok()
|
||||
}
|
||||
|
||||
/// Check whether the signature could not be successfully decoded.
|
||||
#[wasm_bindgen(js_name = "isInvalid")]
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
self.inner.is_err()
|
||||
}
|
||||
|
||||
/// The signature, if successfully decoded.
|
||||
#[wasm_bindgen(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.
|
||||
#[wasm_bindgen(getter, js_name = "invalidSignatureSource")]
|
||||
pub fn invalid_signature_source(&self) -> Option<String> {
|
||||
match &self.inner {
|
||||
Ok(_) => None,
|
||||
Err(signature) => Some(signature.source.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,193 @@
|
||||
//! Vodozemac types.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::impl_from_to_inner;
|
||||
|
||||
/// An Ed25519 public key, used to verify digital signatures.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ed25519PublicKey {
|
||||
inner: vodozemac::Ed25519PublicKey,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Ed25519PublicKey {
|
||||
/// The number of bytes an Ed25519 public key has.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
vodozemac::Ed25519PublicKey::LENGTH
|
||||
}
|
||||
|
||||
/// Serialize an Ed25519 public key to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_to_inner!(vodozemac::Ed25519PublicKey => Ed25519PublicKey);
|
||||
|
||||
/// An Ed25519 digital signature, can be used to verify the
|
||||
/// authenticity of a message.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Ed25519Signature {
|
||||
pub(crate) inner: vodozemac::Ed25519Signature,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(vodozemac::Ed25519Signature => Ed25519Signature);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Ed25519Signature {
|
||||
/// Try to create an Ed25519 signature from an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(signature: String) -> Result<Ed25519Signature, JsError> {
|
||||
Ok(Self { inner: vodozemac::Ed25519Signature::from_base64(signature.as_str())? })
|
||||
}
|
||||
|
||||
/// Serialize a Ed25519 signature to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Curve25519 public key.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Curve25519PublicKey {
|
||||
inner: vodozemac::Curve25519PublicKey,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Curve25519PublicKey {
|
||||
/// The number of bytes a Curve25519 public key has.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
vodozemac::Curve25519PublicKey::LENGTH
|
||||
}
|
||||
|
||||
/// Serialize an Curve25519 public key to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_to_inner!(vodozemac::Curve25519PublicKey => Curve25519PublicKey);
|
||||
|
||||
/// Struct holding the two public identity keys of an account.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct IdentityKeys {
|
||||
/// The Ed25519 public key, used for signing.
|
||||
pub ed25519: Ed25519PublicKey,
|
||||
|
||||
/// The Curve25519 public key, used for establish shared secrets.
|
||||
pub curve25519: Curve25519PublicKey,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::olm::IdentityKeys> for IdentityKeys {
|
||||
fn from(value: matrix_sdk_crypto::olm::IdentityKeys) -> Self {
|
||||
Self {
|
||||
ed25519: Ed25519PublicKey { inner: value.ed25519 },
|
||||
curve25519: Curve25519PublicKey { inner: value.curve25519 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum over the different key types a device can have.
|
||||
///
|
||||
/// Currently devices have a curve25519 and ed25519 keypair. The keys
|
||||
/// transport format is a base64 encoded string, any unknown key type
|
||||
/// will be left as such a string.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceKey {
|
||||
inner: matrix_sdk_crypto::types::DeviceKey,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::types::DeviceKey => DeviceKey);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceKey {
|
||||
/// Get the name of the device key.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn name(&self) -> DeviceKeyName {
|
||||
(&self.inner).into()
|
||||
}
|
||||
|
||||
/// Get the value associated to the `Curve25519` device key name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn curve25519(&self) -> Option<Curve25519PublicKey> {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match &self.inner {
|
||||
Curve25519(key) => Some((*key).into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value associated to the `Ed25519` device key name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn ed25519(&self) -> Option<Ed25519PublicKey> {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match &self.inner {
|
||||
Ed25519(key) => Some((*key).into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value associated to the `Unknown` device key name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn unknown(&self) -> Option<String> {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match &self.inner {
|
||||
Unknown(key) => Some(key.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the `DeviceKey` into a base64 encoded string.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_crypto::types::DeviceKey> for DeviceKeyName {
|
||||
fn from(device_key: &matrix_sdk_crypto::types::DeviceKey) -> Self {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match device_key {
|
||||
Curve25519(_) => Self::Curve25519,
|
||||
Ed25519(_) => Self::Ed25519,
|
||||
Unknown(_) => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum over the different key types a device can have.
|
||||
///
|
||||
/// Currently devices have a curve25519 and ed25519 keypair. The keys
|
||||
/// transport format is a base64 encoded string, any unknown key type
|
||||
/// will be left as such a string.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum DeviceKeyName {
|
||||
/// The curve25519 device key.
|
||||
Curve25519,
|
||||
|
||||
/// The ed25519 device key.
|
||||
Ed25519,
|
||||
|
||||
/// An unknown device key.
|
||||
Unknown,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const { UserId, initAsync } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
test("can instantiate rust objects with async initialiser", async () => {
|
||||
initUserId = () => new UserId("@foo:bar.org");
|
||||
|
||||
// stub out the synchronous WebAssembly loader with one that raises an error
|
||||
jest.spyOn(WebAssembly, "Module").mockImplementation(() => {
|
||||
throw new Error("synchronous WebAssembly.Module() not allowed");
|
||||
});
|
||||
|
||||
// this should fail
|
||||
expect(initUserId).toThrow(/synchronous/);
|
||||
|
||||
// but once we init with async, it should work
|
||||
await initAsync();
|
||||
initUserId();
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const { Attachment, EncryptedAttachment } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
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,957 @@
|
||||
const {
|
||||
OlmMachine,
|
||||
UserId,
|
||||
DeviceId,
|
||||
DeviceKeyId,
|
||||
RoomId,
|
||||
Device,
|
||||
LocalTrust,
|
||||
UserDevices,
|
||||
DeviceKey,
|
||||
DeviceKeyName,
|
||||
DeviceKeyAlgorithmName,
|
||||
Ed25519PublicKey,
|
||||
EncryptionAlgorithm,
|
||||
Curve25519PublicKey,
|
||||
Signatures,
|
||||
VerificationMethod,
|
||||
VerificationRequest,
|
||||
ToDeviceRequest,
|
||||
DeviceLists,
|
||||
KeysUploadRequest,
|
||||
RequestType,
|
||||
KeysQueryRequest,
|
||||
Sas,
|
||||
Emoji,
|
||||
SigningKeysUploadRequest,
|
||||
SignatureUploadRequest,
|
||||
Qr,
|
||||
QrCode,
|
||||
QrCodeScan,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
const { zip, addMachineToMachine } = require("./helper");
|
||||
|
||||
describe("LocalTrust", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(LocalTrust.Verified).toStrictEqual(0);
|
||||
expect(LocalTrust.BlackListed).toStrictEqual(1);
|
||||
expect(LocalTrust.Ignored).toStrictEqual(2);
|
||||
expect(LocalTrust.Unset).toStrictEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DeviceKeyName", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(DeviceKeyName.Curve25519).toStrictEqual(0);
|
||||
expect(DeviceKeyName.Ed25519).toStrictEqual(1);
|
||||
expect(DeviceKeyName.Unknown).toStrictEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe(OlmMachine.name, () => {
|
||||
const user = new UserId("@alice:example.org");
|
||||
const device = new DeviceId("foobar");
|
||||
const room = new RoomId("!baz:matrix.org");
|
||||
|
||||
function machine(new_user, new_device) {
|
||||
return OlmMachine.initialize(new_user || user, new_device || device);
|
||||
}
|
||||
|
||||
test("can read user devices", async () => {
|
||||
const m = await machine();
|
||||
const userDevices = await m.getUserDevices(user);
|
||||
|
||||
expect(userDevices).toBeInstanceOf(UserDevices);
|
||||
expect(userDevices.get(device)).toBeInstanceOf(Device);
|
||||
expect(userDevices.isAnyVerified()).toStrictEqual(false);
|
||||
expect(userDevices.keys().map((device_id) => device_id.toString())).toStrictEqual([device.toString()]);
|
||||
expect(userDevices.devices().map((device) => device.deviceId.toString())).toStrictEqual([device.toString()]);
|
||||
});
|
||||
|
||||
test("can read a user device", async () => {
|
||||
const m = await machine();
|
||||
|
||||
const 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: {},
|
||||
});
|
||||
// Insert another device into the store
|
||||
await m.markRequestAsSent("ID", RequestType.KeysQuery, hypothetical_response);
|
||||
|
||||
const secondDeviceId = new DeviceId("JLAFKJWSCS");
|
||||
const dev = await m.getDevice(user, secondDeviceId);
|
||||
|
||||
expect(dev).toBeInstanceOf(Device);
|
||||
expect(dev.isVerified()).toStrictEqual(false);
|
||||
expect(dev.isCrossSigningTrusted()).toStrictEqual(false);
|
||||
expect(dev.isCrossSignedByOwner()).toStrictEqual(false);
|
||||
|
||||
expect(dev.localTrustState).toStrictEqual(LocalTrust.Unset);
|
||||
expect(dev.isLocallyTrusted()).toStrictEqual(false);
|
||||
expect(await dev.setLocalTrust(LocalTrust.Verified)).toBeNull();
|
||||
expect(dev.localTrustState).toStrictEqual(LocalTrust.Verified);
|
||||
expect(dev.isLocallyTrusted()).toStrictEqual(true);
|
||||
|
||||
expect(dev.userId.toString()).toStrictEqual(user.toString());
|
||||
expect(dev.deviceId.toString()).toStrictEqual(secondDeviceId.toString());
|
||||
expect(dev.deviceName).toBeUndefined();
|
||||
expect(dev.algorithms).toEqual([
|
||||
EncryptionAlgorithm.OlmV1Curve25519AesSha2,
|
||||
EncryptionAlgorithm.MegolmV1AesSha2,
|
||||
]);
|
||||
|
||||
const deviceKey = dev.getKey(DeviceKeyAlgorithmName.Ed25519);
|
||||
|
||||
expect(deviceKey).toBeInstanceOf(DeviceKey);
|
||||
expect(deviceKey.name).toStrictEqual(DeviceKeyName.Ed25519);
|
||||
expect(deviceKey.curve25519).toBeUndefined();
|
||||
expect(deviceKey.ed25519).toBeInstanceOf(Ed25519PublicKey);
|
||||
expect(deviceKey.unknown).toBeUndefined();
|
||||
expect(deviceKey.toBase64()).toMatch(/^[A-Za-z0-9\+/]+$/);
|
||||
|
||||
expect(dev.curve25519Key).toBeInstanceOf(Curve25519PublicKey);
|
||||
expect(dev.ed25519Key).toBeInstanceOf(Ed25519PublicKey);
|
||||
|
||||
for (const [deviceKeyId, deviceKey] of dev.keys) {
|
||||
expect(deviceKeyId).toBeInstanceOf(DeviceKeyId);
|
||||
expect(deviceKey).toBeInstanceOf(DeviceKey);
|
||||
}
|
||||
|
||||
expect(dev.signatures).toBeInstanceOf(Signatures);
|
||||
expect(dev.isBlacklisted()).toStrictEqual(false);
|
||||
expect(dev.isDeleted()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Key Verification", () => {
|
||||
const userId1 = new UserId("@alice:example.org");
|
||||
const deviceId1 = new DeviceId("alice_device");
|
||||
|
||||
const userId2 = new UserId("@bob:example.org");
|
||||
const deviceId2 = new DeviceId("bob_device");
|
||||
|
||||
function machine(new_user, new_device) {
|
||||
return OlmMachine.initialize(new_user || userId1, new_device || deviceId1);
|
||||
}
|
||||
|
||||
describe("SAS", () => {
|
||||
// First Olm machine.
|
||||
let m1;
|
||||
|
||||
// Second Olm machine.
|
||||
let m2;
|
||||
|
||||
beforeAll(async () => {
|
||||
m1 = await machine(userId1, deviceId1);
|
||||
m2 = await machine(userId2, deviceId2);
|
||||
});
|
||||
|
||||
// Verification request for `m1`.
|
||||
let verificationRequest1;
|
||||
|
||||
// The flow ID.
|
||||
let flowId;
|
||||
|
||||
test("can request verification (`m.key.verification.request`)", async () => {
|
||||
// Make `m1` and `m2` be aware of each other.
|
||||
{
|
||||
await addMachineToMachine(m2, m1);
|
||||
await addMachineToMachine(m1, m2);
|
||||
}
|
||||
|
||||
// Pick the device we want to start the verification with.
|
||||
const device2 = await m1.getDevice(userId2, deviceId2);
|
||||
|
||||
expect(device2).toBeInstanceOf(Device);
|
||||
|
||||
let outgoingVerificationRequest;
|
||||
// Request a verification from `m1` to `device2`.
|
||||
[verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification();
|
||||
|
||||
expect(verificationRequest1).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest1.otherDeviceId).toBeUndefined();
|
||||
expect(verificationRequest1.roomId).toBeUndefined();
|
||||
expect(verificationRequest1.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest1.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest1.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest1.theirSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(verificationRequest1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest1.weStarted()).toStrictEqual(true);
|
||||
expect(verificationRequest1.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isCancelled()).toStrictEqual(false);
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.request");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
flowId = verificationRequest1.flowId;
|
||||
});
|
||||
|
||||
// Verification request for `m2`.
|
||||
let verificationRequest2;
|
||||
|
||||
test("can fetch received request verification", async () => {
|
||||
// Oh, a new verification request.
|
||||
verificationRequest2 = m2.getVerificationRequest(userId1, flowId);
|
||||
|
||||
expect(verificationRequest2).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(verificationRequest2.roomId).toBeUndefined();
|
||||
expect(verificationRequest2.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest2.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest2.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest2.flowId).toStrictEqual(flowId);
|
||||
expect(verificationRequest2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest2.weStarted()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isCancelled()).toStrictEqual(false);
|
||||
|
||||
const verificationRequests = m2.getVerificationRequests(userId1);
|
||||
expect(verificationRequests).toHaveLength(1);
|
||||
expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same
|
||||
});
|
||||
|
||||
test("can accept a verification request (`m.key.verification.ready`)", async () => {
|
||||
// Accept the verification request.
|
||||
let outgoingVerificationRequest = verificationRequest2.accept();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
|
||||
// The request verification is ready.
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.ready");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification ready to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("verification requests are synchronized and automatically updated", () => {
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(true);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(true);
|
||||
|
||||
expect(verificationRequest1.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
});
|
||||
|
||||
// SAS verification for the second machine.
|
||||
let sas2;
|
||||
|
||||
test("can start a SAS verification (`m.key.verification.start`)", async () => {
|
||||
// Let's start a SAS verification, from `m2` for example.
|
||||
[sas2, outgoingVerificationRequest] = await verificationRequest2.startSas();
|
||||
expect(sas2).toBeInstanceOf(Sas);
|
||||
|
||||
expect(sas2.userId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString());
|
||||
expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(sas2.flowId).toStrictEqual(flowId);
|
||||
expect(sas2.roomId).toBeUndefined();
|
||||
expect(sas2.supportsEmoji()).toStrictEqual(false);
|
||||
expect(sas2.startedFromRequest()).toStrictEqual(true);
|
||||
expect(sas2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(sas2.haveWeConfirmed()).toStrictEqual(false);
|
||||
expect(sas2.hasBeenAccepted()).toStrictEqual(false);
|
||||
expect(sas2.cancelInfo()).toBeUndefined();
|
||||
expect(sas2.weStarted()).toStrictEqual(false);
|
||||
expect(sas2.timedOut()).toStrictEqual(false);
|
||||
expect(sas2.canBePresented()).toStrictEqual(false);
|
||||
expect(sas2.isDone()).toStrictEqual(false);
|
||||
expect(sas2.isCancelled()).toStrictEqual(false);
|
||||
expect(sas2.emoji()).toBeUndefined();
|
||||
expect(sas2.emojiIndex()).toBeUndefined();
|
||||
expect(sas2.decimals()).toBeUndefined();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.start");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the SAS start to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
// SAS verification for the second machine.
|
||||
let sas1;
|
||||
|
||||
test("can fetch and accept an ongoing SAS verification (`m.key.verification.accept`)", async () => {
|
||||
// Let's fetch the ongoing SAS verification.
|
||||
sas1 = await m1.getVerification(userId2, flowId);
|
||||
|
||||
expect(sas1).toBeInstanceOf(Sas);
|
||||
|
||||
expect(sas1.userId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString());
|
||||
expect(sas1.flowId).toStrictEqual(flowId);
|
||||
expect(sas1.roomId).toBeUndefined();
|
||||
expect(sas1.startedFromRequest()).toStrictEqual(true);
|
||||
expect(sas1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(sas1.haveWeConfirmed()).toStrictEqual(false);
|
||||
expect(sas1.hasBeenAccepted()).toStrictEqual(false);
|
||||
expect(sas1.cancelInfo()).toBeUndefined();
|
||||
expect(sas1.weStarted()).toStrictEqual(true);
|
||||
expect(sas1.timedOut()).toStrictEqual(false);
|
||||
expect(sas1.canBePresented()).toStrictEqual(false);
|
||||
expect(sas1.isDone()).toStrictEqual(false);
|
||||
expect(sas1.isCancelled()).toStrictEqual(false);
|
||||
expect(sas1.emoji()).toBeUndefined();
|
||||
expect(sas1.emojiIndex()).toBeUndefined();
|
||||
expect(sas1.decimals()).toBeUndefined();
|
||||
|
||||
// Let's accept thet SAS start request.
|
||||
let outgoingVerificationRequest = sas1.accept();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.accept");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the SAS accept to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("emojis are supported by both sides", () => {
|
||||
expect(sas1.supportsEmoji()).toStrictEqual(true);
|
||||
expect(sas2.supportsEmoji()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
test("one side sends verification key (`m.key.verification.key`)", async () => {
|
||||
// Let's send the verification keys from `m2` to `m1`.
|
||||
const outgoingRequests = await m2.outgoingRequests();
|
||||
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
|
||||
|
||||
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(toDeviceRequest.event_type).toStrictEqual("m.key.verification.key");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: toDeviceRequest.event_type,
|
||||
content: JSON.parse(toDeviceRequest.body).messages[userId1.toString()][deviceId1.toString()],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS key to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
m2.markRequestAsSent(toDeviceRequest.id, toDeviceRequest.type, "{}");
|
||||
});
|
||||
|
||||
test("other side sends back verification key (`m.key.verification.key`)", async () => {
|
||||
// Let's send the verification keys from `m1` to `m2`.
|
||||
const outgoingRequests = await m1.outgoingRequests();
|
||||
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
|
||||
|
||||
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(toDeviceRequest.event_type).toStrictEqual("m.key.verification.key");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: toDeviceRequest.event_type,
|
||||
content: JSON.parse(toDeviceRequest.body).messages[userId2.toString()][deviceId2.toString()],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS key to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
m1.markRequestAsSent(toDeviceRequest.id, toDeviceRequest.type, "{}");
|
||||
});
|
||||
|
||||
test("emojis match from both sides", () => {
|
||||
const emojis1 = sas1.emoji();
|
||||
const emojiIndexes1 = sas1.emojiIndex();
|
||||
const emojis2 = sas2.emoji();
|
||||
const emojiIndexes2 = sas2.emojiIndex();
|
||||
|
||||
expect(emojis1).toHaveLength(7);
|
||||
expect(emojiIndexes1).toHaveLength(emojis1.length);
|
||||
expect(emojis2).toHaveLength(emojis1.length);
|
||||
expect(emojiIndexes2).toHaveLength(emojis1.length);
|
||||
|
||||
const isEmoji =
|
||||
/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/;
|
||||
|
||||
for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(
|
||||
emojis1,
|
||||
emojiIndexes1,
|
||||
emojis2,
|
||||
emojiIndexes2,
|
||||
)) {
|
||||
expect(emoji1).toBeInstanceOf(Emoji);
|
||||
expect(emoji1.symbol).toMatch(isEmoji);
|
||||
expect(emoji1.description).toBeTruthy();
|
||||
|
||||
expect(emojiIndex1).toBeGreaterThanOrEqual(0);
|
||||
expect(emojiIndex1).toBeLessThanOrEqual(63);
|
||||
|
||||
expect(emoji2).toBeInstanceOf(Emoji);
|
||||
expect(emoji2.symbol).toStrictEqual(emoji1.symbol);
|
||||
expect(emoji2.description).toStrictEqual(emoji1.description);
|
||||
|
||||
expect(emojiIndex2).toStrictEqual(emojiIndex1);
|
||||
}
|
||||
});
|
||||
|
||||
test("decimals match from both sides", () => {
|
||||
const decimals1 = sas1.decimals();
|
||||
const decimals2 = sas2.decimals();
|
||||
|
||||
expect(decimals1).toHaveLength(3);
|
||||
expect(decimals2).toHaveLength(decimals1.length);
|
||||
|
||||
const isDecimal = /^[0-9]{4}$/;
|
||||
|
||||
for (const [decimal1, decimal2] of zip(decimals1, decimals2)) {
|
||||
expect(decimal1.toString()).toMatch(isDecimal);
|
||||
|
||||
expect(decimal2).toStrictEqual(decimal1);
|
||||
}
|
||||
});
|
||||
|
||||
test("can confirm keys match (`m.key.verification.mac`)", async () => {
|
||||
// `m1` confirms.
|
||||
const [outgoingVerificationRequests, signatureUploadRequest] = await sas1.confirm();
|
||||
|
||||
expect(signatureUploadRequest).toBeUndefined();
|
||||
expect(outgoingVerificationRequests).toHaveLength(1);
|
||||
|
||||
let outgoingVerificationRequest = outgoingVerificationRequests[0];
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.mac");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS confirmation to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("can confirm back keys match (`m.key.verification.done`)", async () => {
|
||||
// `m2` confirms.
|
||||
const [outgoingVerificationRequests, signatureUploadRequest] = await sas2.confirm();
|
||||
|
||||
expect(signatureUploadRequest).toBeUndefined();
|
||||
expect(outgoingVerificationRequests).toHaveLength(2);
|
||||
|
||||
// `.mac`
|
||||
{
|
||||
let outgoingVerificationRequest = outgoingVerificationRequests[0];
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.mac");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS confirmation to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
}
|
||||
|
||||
// `.done`
|
||||
{
|
||||
let outgoingVerificationRequest = outgoingVerificationRequests[1];
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.done");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS done to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
}
|
||||
});
|
||||
|
||||
test("can send final done (`m.key.verification.done`)", async () => {
|
||||
const outgoingRequests = await m1.outgoingRequests();
|
||||
expect(outgoingRequests).toHaveLength(4);
|
||||
|
||||
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
|
||||
|
||||
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(toDeviceRequest.event_type).toStrictEqual("m.key.verification.done");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: toDeviceRequest.event_type,
|
||||
content: JSON.parse(toDeviceRequest.body).messages[userId2.toString()][deviceId2.toString()],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS key to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
m1.markRequestAsSent(toDeviceRequest.id, toDeviceRequest.type, "{}");
|
||||
});
|
||||
|
||||
test("can see if verification is done", () => {
|
||||
expect(verificationRequest1.isDone()).toStrictEqual(true);
|
||||
expect(verificationRequest2.isDone()).toStrictEqual(true);
|
||||
|
||||
expect(sas1.isDone()).toStrictEqual(true);
|
||||
expect(sas2.isDone()).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("QR Code", () => {
|
||||
if (undefined === Qr) {
|
||||
// qrcode supports is not enabled
|
||||
console.info("qrcode support is disabled, skip the associated test suite");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// First Olm machine.
|
||||
let m1;
|
||||
|
||||
// Second Olm machine.
|
||||
let m2;
|
||||
|
||||
beforeAll(async () => {
|
||||
m1 = await machine(userId1, deviceId1);
|
||||
m2 = await machine(userId2, deviceId2);
|
||||
});
|
||||
|
||||
// Verification request for `m1`.
|
||||
let verificationRequest1;
|
||||
|
||||
// The flow ID.
|
||||
let flowId;
|
||||
|
||||
test("can request verification (`m.key.verification.request`)", async () => {
|
||||
// Make `m1` and `m2` be aware of each other.
|
||||
{
|
||||
await addMachineToMachine(m2, m1);
|
||||
await addMachineToMachine(m1, m2);
|
||||
}
|
||||
|
||||
// Pick the device we want to start the verification with.
|
||||
const device2 = await m1.getDevice(userId2, deviceId2);
|
||||
|
||||
expect(device2).toBeInstanceOf(Device);
|
||||
|
||||
let outgoingVerificationRequest;
|
||||
// Request a verification from `m1` to `device2`.
|
||||
[verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification([
|
||||
VerificationMethod.QrCodeScanV1, // by default
|
||||
VerificationMethod.QrCodeShowV1, // the one we add
|
||||
]);
|
||||
|
||||
expect(verificationRequest1).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest1.otherDeviceId).toBeUndefined();
|
||||
expect(verificationRequest1.roomId).toBeUndefined();
|
||||
expect(verificationRequest1.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest1.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest1.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest1.theirSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(verificationRequest1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest1.weStarted()).toStrictEqual(true);
|
||||
expect(verificationRequest1.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isCancelled()).toStrictEqual(false);
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.request");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
flowId = verificationRequest1.flowId;
|
||||
});
|
||||
|
||||
// Verification request for `m2`.
|
||||
let verificationRequest2;
|
||||
|
||||
test("can fetch received request verification", async () => {
|
||||
// Oh, a new verification request.
|
||||
verificationRequest2 = m2.getVerificationRequest(userId1, flowId);
|
||||
|
||||
expect(verificationRequest2).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(verificationRequest2.roomId).toBeUndefined();
|
||||
expect(verificationRequest2.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest2.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest2.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest2.flowId).toStrictEqual(flowId);
|
||||
expect(verificationRequest2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest2.weStarted()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isCancelled()).toStrictEqual(false);
|
||||
|
||||
const verificationRequests = m2.getVerificationRequests(userId1);
|
||||
expect(verificationRequests).toHaveLength(1);
|
||||
expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same
|
||||
});
|
||||
|
||||
test("can accept a verification request with methods (`m.key.verification.ready`)", async () => {
|
||||
// Accept the verification request.
|
||||
let outgoingVerificationRequest = verificationRequest2.acceptWithMethods([
|
||||
VerificationMethod.QrCodeScanV1, // by default
|
||||
VerificationMethod.QrCodeShowV1, // the one we add
|
||||
]);
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
|
||||
// The request verification is ready.
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.ready");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification ready to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("verification requests are synchronized and automatically updated", () => {
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(true);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(true);
|
||||
|
||||
expect(verificationRequest1.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
});
|
||||
|
||||
// QR verification for the second machine.
|
||||
let qr2;
|
||||
|
||||
test("can generate a QR code", async () => {
|
||||
qr2 = await verificationRequest2.generateQrCode();
|
||||
|
||||
expect(qr2).toBeInstanceOf(Qr);
|
||||
|
||||
expect(qr2.hasBeenScanned()).toStrictEqual(false);
|
||||
expect(qr2.hasBeenConfirmed()).toStrictEqual(false);
|
||||
expect(qr2.userId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(qr2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(qr2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(qr2.weStarted()).toStrictEqual(false);
|
||||
expect(qr2.cancelInfo()).toBeUndefined();
|
||||
expect(qr2.isDone()).toStrictEqual(false);
|
||||
expect(qr2.isCancelled()).toStrictEqual(false);
|
||||
expect(qr2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(qr2.reciprocated()).toStrictEqual(false);
|
||||
expect(qr2.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(qr2.roomId).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read QR code's bytes", async () => {
|
||||
const qrCodeHeader = "MATRIX";
|
||||
const qrCodeVersion = "\x02";
|
||||
|
||||
const qrCodeBytes = qr2.toBytes();
|
||||
|
||||
expect(qrCodeBytes).toHaveLength(122);
|
||||
expect(Array.from(qrCodeBytes.slice(0, 7))).toEqual(
|
||||
[...qrCodeHeader, ...qrCodeVersion].map((char) => char.charCodeAt(0)),
|
||||
);
|
||||
});
|
||||
|
||||
test("can render QR code", async () => {
|
||||
const qrCode = qr2.toQrCode();
|
||||
|
||||
expect(qrCode).toBeInstanceOf(QrCode);
|
||||
|
||||
// Want to get `canvasBuffer` to render the QR code? Install `npm install canvas` and uncomment the following blocks.
|
||||
|
||||
//let canvasBuffer;
|
||||
|
||||
{
|
||||
const buffer = qrCode.renderIntoBuffer();
|
||||
|
||||
expect(buffer).toBeInstanceOf(Uint8ClampedArray);
|
||||
// 45px ⨉ 45px
|
||||
expect(buffer).toHaveLength(45 * 45);
|
||||
// 0 for a white pixel, 1 for a black pixel.
|
||||
expect(buffer.every((p) => p == 0 || p == 1)).toStrictEqual(true);
|
||||
|
||||
/*
|
||||
const { Canvas } = require('canvas');
|
||||
const canvas = new Canvas(55, 55);
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = 'white';
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// New image data, filled with black, transparent pixels.
|
||||
const imageData = context.createImageData(45, 45);
|
||||
const data = imageData.data;
|
||||
|
||||
const [r, g, b, a] = [0, 1, 2, 3];
|
||||
|
||||
for (
|
||||
let dataNth = 0,
|
||||
bufferNth = 0;
|
||||
dataNth < data.length && bufferNth < buffer.length;
|
||||
dataNth += 4,
|
||||
bufferNth += 1
|
||||
) {
|
||||
data[dataNth + a] = 255;
|
||||
|
||||
// White pixel
|
||||
if (buffer[bufferNth] == 0) {
|
||||
data[dataNth + r] = 255;
|
||||
data[dataNth + g] = 255;
|
||||
data[dataNth + b] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
context.putImageData(imageData, 5, 5);
|
||||
canvasBuffer = canvas.toBuffer('image/png');
|
||||
*/
|
||||
}
|
||||
|
||||
// Want to see the QR code? Uncomment the following block.
|
||||
/*
|
||||
{
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
|
||||
const qrCodeFile = path.join(tempDirectory, 'qrcode.png');
|
||||
|
||||
console.log(`View the QR code at \`${qrCodeFile}\`.`);
|
||||
|
||||
expect(await fs.writeFile(qrCodeFile, canvasBuffer)).toBeUndefined();
|
||||
}
|
||||
*/
|
||||
});
|
||||
|
||||
let qr1;
|
||||
|
||||
test("can scan a QR code from bytes", async () => {
|
||||
const scan = QrCodeScan.fromBytes(qr2.toBytes());
|
||||
|
||||
expect(scan).toBeInstanceOf(QrCodeScan);
|
||||
|
||||
qr1 = await verificationRequest1.scanQrCode(scan);
|
||||
|
||||
expect(qr1).toBeInstanceOf(Qr);
|
||||
|
||||
expect(qr1.hasBeenScanned()).toStrictEqual(false);
|
||||
expect(qr1.hasBeenConfirmed()).toStrictEqual(false);
|
||||
expect(qr1.userId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(qr1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(qr1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString());
|
||||
expect(qr1.weStarted()).toStrictEqual(true);
|
||||
expect(qr1.cancelInfo()).toBeUndefined();
|
||||
expect(qr1.isDone()).toStrictEqual(false);
|
||||
expect(qr1.isCancelled()).toStrictEqual(false);
|
||||
expect(qr1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(qr1.reciprocated()).toStrictEqual(true);
|
||||
expect(qr1.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(qr1.roomId).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can start a QR verification/reciprocate (`m.key.verification.start`)", async () => {
|
||||
let outgoingVerificationRequest = qr1.reciprocate();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.start");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("can confirm QR code has been scanned", () => {
|
||||
expect(qr2.hasBeenScanned()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
test("can confirm scanning (`m.key.verification.done`)", async () => {
|
||||
let outgoingVerificationRequest = qr2.confirmScanning();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.done");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("can confirm QR code has been confirmed", () => {
|
||||
expect(qr2.hasBeenConfirmed()).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("VerificationMethod", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(VerificationMethod.SasV1).toStrictEqual(0);
|
||||
expect(VerificationMethod.QrCodeScanV1).toStrictEqual(1);
|
||||
expect(VerificationMethod.QrCodeShowV1).toStrictEqual(2);
|
||||
expect(VerificationMethod.ReciprocateV1).toStrictEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const {
|
||||
EncryptionAlgorithm,
|
||||
EncryptionSettings,
|
||||
HistoryVisibility,
|
||||
VerificationState,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
const { HistoryVisibility } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
const { DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
function* zip(...arrays) {
|
||||
const len = Math.min(...arrays.map((array) => array.length));
|
||||
|
||||
for (let nth = 0; nth < len; ++nth) {
|
||||
yield [...arrays.map((array) => array.at(nth))];
|
||||
}
|
||||
}
|
||||
|
||||
// Add a machine to another machine, i.e. be sure a machine knows
|
||||
// another exists.
|
||||
async function addMachineToMachine(machineToAdd, machine) {
|
||||
const toDeviceEvents = JSON.stringify([]);
|
||||
const changedDevices = new DeviceLists();
|
||||
const oneTimeKeyCounts = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
const receiveSyncChanges = JSON.parse(
|
||||
await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys),
|
||||
);
|
||||
|
||||
expect(receiveSyncChanges).toEqual([]);
|
||||
|
||||
const outgoingRequests = await machineToAdd.outgoingRequests();
|
||||
|
||||
expect(outgoingRequests).toHaveLength(2);
|
||||
|
||||
let keysUploadRequest;
|
||||
// Read the `KeysUploadRequest`.
|
||||
{
|
||||
expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest);
|
||||
expect(outgoingRequests[0].id).toBeDefined();
|
||||
expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload);
|
||||
expect(outgoingRequests[0].body).toBeDefined();
|
||||
|
||||
const body = JSON.parse(outgoingRequests[0].body);
|
||||
expect(body.device_keys).toBeDefined();
|
||||
expect(body.one_time_keys).toBeDefined();
|
||||
|
||||
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload
|
||||
const hypothetical_response = JSON.stringify({
|
||||
one_time_key_counts: {
|
||||
curve25519: 10,
|
||||
signed_curve25519: 20,
|
||||
},
|
||||
});
|
||||
const marked = await machineToAdd.markRequestAsSent(
|
||||
outgoingRequests[0].id,
|
||||
outgoingRequests[0].type,
|
||||
hypothetical_response,
|
||||
);
|
||||
expect(marked).toStrictEqual(true);
|
||||
|
||||
keysUploadRequest = outgoingRequests[0];
|
||||
}
|
||||
|
||||
{
|
||||
expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest);
|
||||
|
||||
let [signingKeysUploadRequest, _] = await machineToAdd.bootstrapCrossSigning(true);
|
||||
|
||||
// Let's forge a `KeysQuery`'s response.
|
||||
let keyQueryResponse = {
|
||||
device_keys: {},
|
||||
master_keys: {},
|
||||
self_signing_keys: {},
|
||||
user_signing_keys: {},
|
||||
};
|
||||
const userId = machineToAdd.userId.toString();
|
||||
const deviceId = machineToAdd.deviceId.toString();
|
||||
keyQueryResponse.device_keys[userId] = {};
|
||||
keyQueryResponse.device_keys[userId][deviceId] = JSON.parse(keysUploadRequest.body).device_keys;
|
||||
|
||||
const keys = JSON.parse(signingKeysUploadRequest.body);
|
||||
keyQueryResponse.master_keys[userId] = keys.master_key;
|
||||
keyQueryResponse.self_signing_keys[userId] = keys.self_signing_key;
|
||||
keyQueryResponse.user_signing_keys[userId] = keys.user_signing_key;
|
||||
|
||||
const marked = await machine.markRequestAsSent(
|
||||
outgoingRequests[1].id,
|
||||
outgoingRequests[1].type,
|
||||
JSON.stringify(keyQueryResponse),
|
||||
);
|
||||
expect(marked).toStrictEqual(true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
zip,
|
||||
addMachineToMachine,
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
const {
|
||||
DeviceId,
|
||||
DeviceKeyAlgorithm,
|
||||
DeviceKeyAlgorithmName,
|
||||
DeviceKeyId,
|
||||
EventId,
|
||||
RoomId,
|
||||
ServerName,
|
||||
UserId,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
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(undefined);
|
||||
expect(new ServerName("foo.org:1234").port).toStrictEqual(1234);
|
||||
});
|
||||
|
||||
test("server is not an IP literal", () => {
|
||||
expect(new ServerName("foo.org").isIpLiteral()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(EventId.name, () => {
|
||||
test("cannot be invalid", () => {
|
||||
expect(() => {
|
||||
new EventId("%foo");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe("Versions 1 & 2", () => {
|
||||
const room = new EventId("$h29iv0s8:foo.org");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("h29iv0s8");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeInstanceOf(ServerName);
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("$h29iv0s8:foo.org");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Version 3", () => {
|
||||
const room = new EventId("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Version 4", () => {
|
||||
const room = new EventId("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,872 @@
|
||||
const {
|
||||
CrossSigningStatus,
|
||||
DecryptedRoomEvent,
|
||||
DeviceId,
|
||||
DeviceKeyId,
|
||||
DeviceLists,
|
||||
EncryptionSettings,
|
||||
EventId,
|
||||
InboundGroupSession,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
MaybeSignature,
|
||||
OlmMachine,
|
||||
OwnUserIdentity,
|
||||
RequestType,
|
||||
RoomId,
|
||||
RoomMessageRequest,
|
||||
ShieldColor,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
UserId,
|
||||
UserIdentity,
|
||||
VerificationRequest,
|
||||
VerificationState,
|
||||
Versions,
|
||||
getVersions,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
const { addMachineToMachine } = require("./helper");
|
||||
require("fake-indexeddb/auto");
|
||||
|
||||
describe("Versions", () => {
|
||||
test("can find out the crate versions", async () => {
|
||||
const versions = getVersions();
|
||||
|
||||
expect(versions).toBeInstanceOf(Versions);
|
||||
expect(versions.vodozemac).toBeDefined();
|
||||
expect(versions.matrix_sdk_crypto).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe(OlmMachine.name, () => {
|
||||
test("can be instantiated with the async initializer", async () => {
|
||||
expect(await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"))).toBeInstanceOf(OlmMachine);
|
||||
});
|
||||
|
||||
test("can be instantiated with a store", async () => {
|
||||
let store_name = "hello";
|
||||
let store_passphrase = "world";
|
||||
|
||||
const by_store_name = (db) => db.name.startsWith(store_name);
|
||||
|
||||
// No databases.
|
||||
expect((await indexedDB.databases()).filter(by_store_name)).toHaveLength(0);
|
||||
|
||||
// Creating a new Olm machine.
|
||||
expect(
|
||||
await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), store_name, store_passphrase),
|
||||
).toBeInstanceOf(OlmMachine);
|
||||
|
||||
// Oh, there is 2 databases now, prefixed by `store_name`.
|
||||
let databases = (await indexedDB.databases()).filter(by_store_name);
|
||||
|
||||
expect(databases).toHaveLength(2);
|
||||
expect(databases).toStrictEqual([
|
||||
{ name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 },
|
||||
{ name: `${store_name}::matrix-sdk-crypto`, version: 3 },
|
||||
]);
|
||||
|
||||
// Creating a new Olm machine, with the stored state.
|
||||
expect(
|
||||
await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), store_name, store_passphrase),
|
||||
).toBeInstanceOf(OlmMachine);
|
||||
|
||||
// Same number of databases.
|
||||
expect((await indexedDB.databases()).filter(by_store_name)).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe("cannot be instantiated with a store", () => {
|
||||
test("store name is missing", async () => {
|
||||
let store_name = null;
|
||||
let store_passphrase = "world";
|
||||
|
||||
let err = null;
|
||||
|
||||
try {
|
||||
await OlmMachine.initialize(
|
||||
new UserId("@foo:bar.org"),
|
||||
new DeviceId("baz"),
|
||||
store_name,
|
||||
store_passphrase,
|
||||
);
|
||||
} catch (error) {
|
||||
err = error;
|
||||
}
|
||||
|
||||
expect(err).toBeDefined();
|
||||
});
|
||||
|
||||
test("store passphrase is missing", async () => {
|
||||
let store_name = "hello";
|
||||
let store_passphrase = null;
|
||||
|
||||
let err = null;
|
||||
|
||||
try {
|
||||
await OlmMachine.initialize(
|
||||
new UserId("@foo:bar.org"),
|
||||
new DeviceId("baz"),
|
||||
store_name,
|
||||
store_passphrase,
|
||||
);
|
||||
} catch (error) {
|
||||
err = error;
|
||||
}
|
||||
|
||||
expect(err).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
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 drop/close", async () => {
|
||||
m = await machine();
|
||||
m.close();
|
||||
});
|
||||
|
||||
test("can drop/close with a store", async () => {
|
||||
let store_name = "temporary";
|
||||
let store_passphrase = "temporary";
|
||||
|
||||
const by_store_name = (db) => db.name.startsWith(store_name);
|
||||
|
||||
// No databases.
|
||||
expect((await indexedDB.databases()).filter(by_store_name)).toHaveLength(0);
|
||||
|
||||
// Creating a new Olm machine.
|
||||
const m = await OlmMachine.initialize(
|
||||
new UserId("@foo:bar.org"),
|
||||
new DeviceId("baz"),
|
||||
store_name,
|
||||
store_passphrase,
|
||||
);
|
||||
expect(m).toBeInstanceOf(OlmMachine);
|
||||
|
||||
// Oh, there is 2 databases now, prefixed by `store_name`.
|
||||
let databases = (await indexedDB.databases()).filter(by_store_name);
|
||||
|
||||
expect(databases).toHaveLength(2);
|
||||
expect(databases).toStrictEqual([
|
||||
{ name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 },
|
||||
{ name: `${store_name}::matrix-sdk-crypto`, version: 3 },
|
||||
]);
|
||||
|
||||
// Let's force to close the `OlmMachine`.
|
||||
m.close();
|
||||
|
||||
// Now we can delete the databases!
|
||||
for (const database_name of [`${store_name}::matrix-sdk-crypto`, `${store_name}::matrix-sdk-crypto-meta`]) {
|
||||
const deleting = indexedDB.deleteDatabase(database_name);
|
||||
deleting.onsuccess = () => {};
|
||||
deleting.onerror = () => {
|
||||
throw new Error("failed to remove the database (error)");
|
||||
};
|
||||
deleting.onblocked = () => {
|
||||
throw new Error("failed to remove the database (blocked)");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
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 read display name", async () => {
|
||||
expect(await machine().displayName).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read tracked users", async () => {
|
||||
const m = await machine();
|
||||
const trackedUsers = await m.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 = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
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 = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
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);
|
||||
expect(outgoingRequests[0].body).toBeDefined();
|
||||
|
||||
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);
|
||||
expect(outgoingRequests[1].body).toBeDefined();
|
||||
|
||||
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 = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
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 = await m.shareRoomKey(room, other_users, new EncryptionSettings());
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(requests[0].event_type).toEqual("m.room.encrypted");
|
||||
expect(requests[0].txn_id).toBeDefined();
|
||||
const content = JSON.parse(requests[0].body);
|
||||
expect(Object.keys(content.messages)).toEqual(["@example:localhost"]);
|
||||
});
|
||||
|
||||
let encrypted;
|
||||
|
||||
test("can encrypt an event", async () => {
|
||||
encrypted = JSON.parse(
|
||||
await m.encryptRoomEvent(
|
||||
room,
|
||||
"m.room.message",
|
||||
JSON.stringify({
|
||||
msgtype: "m.text",
|
||||
body: "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.msgtype).toStrictEqual("m.text");
|
||||
expect(event.content.body).toStrictEqual("Hello, 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.shieldState(true).color).toStrictEqual(ShieldColor.Red);
|
||||
expect(decrypted.shieldState(false).color).toStrictEqual(ShieldColor.Red);
|
||||
});
|
||||
});
|
||||
|
||||
test("failure to decrypt returns a valid error", async () => {
|
||||
const m = await machine();
|
||||
const evt = {
|
||||
type: "m.room.encrypted",
|
||||
event_id: "$xxxxx:example.org",
|
||||
origin_server_ts: Date.now(),
|
||||
sender: user.toString(),
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blah",
|
||||
},
|
||||
};
|
||||
await expect(() => m.decryptRoomEvent(JSON.stringify(evt), room)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
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(1);
|
||||
|
||||
let base64;
|
||||
|
||||
// `get`
|
||||
{
|
||||
const signature = signatures.get(user);
|
||||
|
||||
expect(signature.has("ed25519:foobar")).toStrictEqual(true);
|
||||
|
||||
const s = signature.get("ed25519:foobar");
|
||||
|
||||
expect(s).toBeInstanceOf(MaybeSignature);
|
||||
|
||||
expect(s.isValid()).toStrictEqual(true);
|
||||
expect(s.isInvalid()).toStrictEqual(false);
|
||||
expect(s.invalidSignatureSource).toBeUndefined();
|
||||
|
||||
base64 = s.signature.toBase64();
|
||||
|
||||
expect(base64).toMatch(/^[A-Za-z0-9\+/]+$/);
|
||||
expect(s.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"))).toBeUndefined();
|
||||
expect(signatures.getSignature(user, new DeviceKeyId("world:foobar"))).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("can get a user identities", async () => {
|
||||
const m = await machine();
|
||||
let _ = m.bootstrapCrossSigning(true);
|
||||
|
||||
const identity = await m.getIdentity(user);
|
||||
|
||||
expect(identity).toBeInstanceOf(OwnUserIdentity);
|
||||
|
||||
const signatureUploadRequest = await identity.verify();
|
||||
expect(signatureUploadRequest).toBeInstanceOf(SignatureUploadRequest);
|
||||
|
||||
const [verificationRequest, outgoingVerificationRequest] = await identity.requestVerification();
|
||||
expect(verificationRequest).toBeInstanceOf(VerificationRequest);
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
|
||||
const isTrusted = await identity.trustsOurOwnDevice();
|
||||
|
||||
expect(isTrusted).toStrictEqual(false);
|
||||
});
|
||||
|
||||
describe("can export/import room keys", () => {
|
||||
let exportedRoomKeys;
|
||||
|
||||
test("can export room keys", async () => {
|
||||
let m = await machine();
|
||||
await m.shareRoomKey(room, [new UserId("@bob:example.org")], new EncryptionSettings());
|
||||
|
||||
exportedRoomKeys = await m.exportRoomKeys((session) => {
|
||||
expect(session).toBeInstanceOf(InboundGroupSession);
|
||||
expect(session.senderKey.toBase64()).toEqual(m.identityKeys.curve25519.toBase64());
|
||||
expect(session.roomId.toString()).toStrictEqual(room.toString());
|
||||
expect(session.sessionId).toBeDefined();
|
||||
expect(session.hasBeenImported()).toStrictEqual(false);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const roomKeys = JSON.parse(exportedRoomKeys);
|
||||
expect(roomKeys).toHaveLength(1);
|
||||
expect(roomKeys[0]).toMatchObject({
|
||||
algorithm: expect.any(String),
|
||||
room_id: room.toString(),
|
||||
sender_key: expect.any(String),
|
||||
session_id: expect.any(String),
|
||||
session_key: expect.any(String),
|
||||
sender_claimed_keys: {
|
||||
ed25519: expect.any(String),
|
||||
},
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
});
|
||||
|
||||
let encryptedExportedRoomKeys;
|
||||
let encryptionPassphrase = "Hello, Matrix!";
|
||||
|
||||
test("can encrypt the exported room keys", () => {
|
||||
encryptedExportedRoomKeys = OlmMachine.encryptExportedRoomKeys(
|
||||
exportedRoomKeys,
|
||||
encryptionPassphrase,
|
||||
100_000,
|
||||
);
|
||||
|
||||
expect(encryptedExportedRoomKeys).toMatch(/^-----BEGIN MEGOLM SESSION DATA-----/);
|
||||
});
|
||||
|
||||
test("can decrypt the exported room keys", () => {
|
||||
const decryptedExportedRoomKeys = OlmMachine.decryptExportedRoomKeys(
|
||||
encryptedExportedRoomKeys,
|
||||
encryptionPassphrase,
|
||||
);
|
||||
|
||||
expect(decryptedExportedRoomKeys).toStrictEqual(exportedRoomKeys);
|
||||
});
|
||||
|
||||
test("can import room keys", async () => {
|
||||
const progressListener = (progress, total) => {
|
||||
expect(progress).toBeLessThan(total);
|
||||
|
||||
// Since it's called only once, let's be crazy.
|
||||
expect(progress).toStrictEqual(0n);
|
||||
expect(total).toStrictEqual(1n);
|
||||
};
|
||||
|
||||
let m = await machine();
|
||||
const result = JSON.parse(await m.importRoomKeys(exportedRoomKeys, progressListener));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
imported_count: expect.any(Number),
|
||||
total_count: expect.any(Number),
|
||||
keys: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test("importing room keys calls RoomKeyUpdatedCallback", async () => {
|
||||
const callback = jest.fn();
|
||||
callback.mockImplementation(() => Promise.resolve(undefined));
|
||||
let m = await machine();
|
||||
m.registerRoomKeyUpdatedCallback(callback);
|
||||
await m.importRoomKeys(exportedRoomKeys, (_, _1) => {});
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
let keyInfoList = callback.mock.calls[0][0];
|
||||
expect(keyInfoList.length).toEqual(1);
|
||||
expect(keyInfoList[0].roomId.toString()).toStrictEqual(room.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("can do in-room verification", () => {
|
||||
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 inject devices from someone else", async () => {
|
||||
{
|
||||
const hypothetical_response = JSON.stringify({
|
||||
device_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
ATRLDCRXAC: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "ATRLDCRXAC",
|
||||
keys: {
|
||||
"curve25519:ATRLDCRXAC": "cAVT5Es3Z3F5pFD+2w3HT7O9+R3PstzYVkzD51X/FWQ",
|
||||
"ed25519:ATRLDCRXAC": "V2w/T/x7i7AXiCCtS6JldrpbvRliRoef3CqTUNqMRHA",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:ATRLDCRXAC":
|
||||
"ro2BjO5J6089B/JOANHnFmGrogrC2TIdMlgJbJO00DjOOcGxXfvOezCFIORTwZNHvkHU617YIGl/4keTDIWvBQ",
|
||||
},
|
||||
},
|
||||
user_id: "@example:morpheus.localhost",
|
||||
unsigned: {
|
||||
device_display_name: "Element Desktop: Linux",
|
||||
},
|
||||
},
|
||||
EYYGYTCTNC: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "EYYGYTCTNC",
|
||||
keys: {
|
||||
"curve25519:EYYGYTCTNC": "Pqu50fo472wgb6NjKkaUxjuqoAIEAmhln2gw/zSQ7Ek",
|
||||
"ed25519:EYYGYTCTNC": "Pf/2QPvui8lDty6TCTglVPRVM+irNHYavNNkyv5yFpU",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:EYYGYTCTNC":
|
||||
"pnP5BYLEUUaxDgrvdzCznkjNDbvY1/MFBr1JejdnLiXlcmxRULQpIWZUCO7QTbULsCwMsYQNGn50nfmjBQX3CQ",
|
||||
},
|
||||
},
|
||||
user_id: "@example:morpheus.localhost",
|
||||
unsigned: {
|
||||
device_display_name: "WeeChat-Matrix-rs",
|
||||
},
|
||||
},
|
||||
SUMODVLSIU: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "SUMODVLSIU",
|
||||
keys: {
|
||||
"curve25519:SUMODVLSIU": "geQXWGWc++gcUHk0JcFmEVSjyzDOnk2mjVsUQwbNqQU",
|
||||
"ed25519:SUMODVLSIU": "ccktaQ3g+B18E6FwVhTBYie26OlHbvDUzDEtxOQ4Qcs",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:SUMODVLSIU":
|
||||
"Yn+AOxHRt1GQpY2xT2Jcqqn8jh5+Vw23ctA7NXyDiWPsLPLNTpjGWHMjZdpUqflQvpiKfhODPICoIa7Pu0iSAg",
|
||||
"ed25519:rUiMNDjIu6gqsrhJPbj3phyIzuEtuQGrLOEa9mCbtTM":
|
||||
"Cio6k/sq289XNTOvTCWre7Q6zg+A3euzMUe7Uy1T3gPqYFzX+kt7EAxrhbPqx1HyXAEz9zD0D/uw9VEXFCvWBQ",
|
||||
},
|
||||
},
|
||||
user_id: "@example:morpheus.localhost",
|
||||
unsigned: {
|
||||
device_display_name: "Element Desktop (Linux)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
user_id: "@example:morpheus.localhost",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A":
|
||||
"ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:SUMODVLSIU":
|
||||
"RL6WOuuzB/mZ+edfUFG/KeEcmKh+NaWpM6m2bUYmDnJrtTCYyoU+pgHJuL2/6nynemmONo18JEHBuqtNcMq2AQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
user_id: "@example:morpheus.localhost",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:rUiMNDjIu6gqsrhJPbj3phyIzuEtuQGrLOEa9mCbtTM":
|
||||
"rUiMNDjIu6gqsrhJPbj3phyIzuEtuQGrLOEa9mCbtTM",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A":
|
||||
"uCBn9rpeg6umY8H97ejN26UMp6QDwNL98869t1DoVGL50J8adLN05OZd8lYk9QzwTr2d56ZTGYSYX8kv28SDDA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
user_id: "@example:morpheus.localhost",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:GLhEKLQ50jnF6IMEPsO2ucpHUNIUEnbBXs5gYbHg4Aw":
|
||||
"GLhEKLQ50jnF6IMEPsO2ucpHUNIUEnbBXs5gYbHg4Aw",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A":
|
||||
"4fIyWlVzuz1pgoegNLZASycORXqKycVS0dNq5vmmwsVEudp1yrPhndnaIJ3fjF8LDHvwzXTvohOid7DiU1j0AA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const marked = await m.markRequestAsSent("foo", RequestType.KeysQuery, hypothetical_response);
|
||||
}
|
||||
});
|
||||
|
||||
test("can start an in-room SAS verification", async () => {
|
||||
let _ = m.bootstrapCrossSigning(true);
|
||||
const identity = await m.getIdentity(new UserId("@example:morpheus.localhost"));
|
||||
|
||||
expect(identity).toBeInstanceOf(UserIdentity);
|
||||
expect(identity.isVerified()).toStrictEqual(false);
|
||||
|
||||
const eventId = new EventId("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
const verificationRequest = await identity.requestVerification(room, eventId);
|
||||
expect(verificationRequest).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
await m.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
sender: "@example:morpheus.localhost",
|
||||
type: "m.key.verification.ready",
|
||||
event_id: "$QguWmaeMt6Hao7Ea6XHDInvr8ndknev79t9a2eBxlz0",
|
||||
origin_server_ts: 1674037263075,
|
||||
content: {
|
||||
"methods": ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"],
|
||||
"from_device": "SUMODVLSIU",
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: eventId.toString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
room,
|
||||
);
|
||||
|
||||
expect(verificationRequest.roomId.toString()).toStrictEqual(room.toString());
|
||||
|
||||
const [_sas, outgoingVerificationRequest] = await verificationRequest.startSas();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(RoomMessageRequest);
|
||||
expect(outgoingVerificationRequest.id).toBeDefined();
|
||||
expect(outgoingVerificationRequest.room_id).toStrictEqual(room.toString());
|
||||
expect(outgoingVerificationRequest.txn_id).toBeDefined();
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.start");
|
||||
expect(outgoingVerificationRequest.body).toBeDefined();
|
||||
|
||||
const body = JSON.parse(outgoingVerificationRequest.body);
|
||||
expect(body).toMatchObject({
|
||||
"from_device": expect.any(String),
|
||||
"method": "m.sas.v1",
|
||||
"key_agreement_protocols": [expect.any(String)],
|
||||
"hashes": [expect.any(String)],
|
||||
"message_authentication_codes": [expect.any(String), expect.any(String)],
|
||||
"short_authentication_string": ["decimal", "emoji"],
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: eventId.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user