Compare commits

..

5 Commits

Author SHA1 Message Date
Jonas Platte 266e59b567 Release matrix-sdk 0.6.2 2022-10-24 12:55:00 +02:00
Jonas Platte 51c7294fce fix(sdk): Remove token field from SyncSettings Debug output 2022-10-24 12:53:25 +02:00
Jonas Platte 2ee5f73d48 Release matrix-sdk-base 0.6.1 and matrix-sdk 0.6.1 2022-10-24 12:26:47 +02:00
Jonas Platte 85ec213db8 fix(base): Don't leak access and refresh tokens in Debug output 2022-10-24 12:25:33 +02:00
Jonas Platte 6fd906dd6f fix(sdk): Don't log the access token in HttpClient::send 2022-10-24 12:07:20 +02:00
609 changed files with 46681 additions and 125747 deletions
+2 -16
View File
@@ -1,7 +1,7 @@
# 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(...)'` for them, resulting in cache invalidation.
# 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
@@ -10,17 +10,11 @@ target-applies-to-host = false
[alias]
xtask = "run --package xtask --"
uniffi-bindgen = "run --package uniffi-bindgen --"
[doc.extern-map.registries]
crates-io = "https://docs.rs/"
# Exclude tarpaulin, android and ios from extra lints since on stable, without
# the nightly-only target-applies-to-host setting at the top, cross compilation
# and otherwise changing cfg's can be very bad for caching. These should never
# be the default either and don't have much target-specific code that would
# benefit from the extra lints.
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
[target.'cfg(all())']
rustflags = [
"-Wrust_2018_idioms",
"-Wsemicolon_in_expressions_from_macros",
@@ -40,14 +34,6 @@ rustflags = [
"-Wclippy::todo",
]
[target.'cfg(target_arch = "wasm32")']
rustflags = [
# We have some types that are !Send and/or !Sync only on wasm, it would be
# slightly more efficient, but also pretty annoying, to wrap them in Rc
# where we would use Arc on other platforms.
"-Aclippy::arc_with_non_send_sync",
]
# activate the target-applies-to-host feature.
# Required for `target-applies-to-host` at the top to take effect.
[unstable]
+2 -1
View File
@@ -1,4 +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 }
-1
View File
@@ -1 +0,0 @@
* @matrix-org/rust
-7
View File
@@ -1,7 +0,0 @@
<!-- description of the changes in this PR -->
- [ ] Public API changes documented in changelogs (optional)
<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v1
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+4 -2
View File
@@ -15,10 +15,12 @@ jobs:
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2023-11-08
toolchain: nightly
components: rustfmt
profile: minimal
override: true
- name: Run Benchmarks
run: cargo bench | tee benchmark-output.txt
+128 -62
View File
@@ -12,100 +12,166 @@ on:
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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:
uses: ./.github/workflows/xtask.yml
test-uniffi-codegen:
name: Test UniFFI bindings generation
needs: xtask
test-matrix-sdk-crypto-nodejs:
name: ${{ matrix.os-name }} [m]-crypto-nodejs, v${{ matrix.node-version }}
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
node-version: [14.0, 16.0, 18.0]
include:
- os: ubuntu-latest
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: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install NPM dependencies
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm install
- name: Build the Node.js binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm run release-build
- name: Test the Node.js binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm run test
# Building in dev-mode and copy lib in failure case
- name: Build the Node.js binding in non-release
if: failure()
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: |
cp *.node release-mode-lib.node
npm run build
- uses: actions/upload-artifact@v3
if: failure()
with:
name: Failure Files
path: |
bindings/matrix-sdk-crypto-nodejs/*.node
/var/crash/*.crash
- if: ${{ matrix.build-doc }}
name: Build the documentation
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
run: npm run doc
test-matrix-sdk-crypto-js:
name: 🕸 [m]-crypto-js
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout the repo
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- 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
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- name: Get xtask
uses: actions/cache/restore@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
node-version: 18.0
- name: Build library & generate bindings
run: target/debug/xtask ci bindings
- name: Install NPM dependencies
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm install
- name: Build the WebAssembly + JavaScript binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run build
- name: Test the JavaScript binding
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run test
- name: Build the documentation
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
run: npm run doc
test-apple:
name: matrix-rust-components-swift
needs: xtask
runs-on: macos-12
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout
uses: actions/checkout@v3
# 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
uses: actions/checkout@v1
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- 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: Install targets
run: |
rustup target add aarch64-apple-ios-sim --toolchain nightly
rustup target add x86_64-apple-ios --toolchain nightly
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- name: Get xtask
uses: actions/cache/restore@v3
- name: Install Uniffi
uses: actions-rs/cargo@v1
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
fail-on-cache-miss: true
command: install
# keep in sync with uniffi dependency in Cargo.toml's
args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev 091c3561656e72e1a4160412c83b36d98e556d06
- name: Build library & bindings
run: target/debug/xtask swift build-library
- name: Generate .xcframework
working-directory: bindings/apple
run: sh ./debug_build_xcframework.sh x86_64 ci
- name: Run XCTests
working-directory: bindings/apple
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios
run: |
xcodebuild test \
-scheme MatrixRustSDK \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 13'
+195 -151
View File
@@ -12,16 +12,46 @@ on:
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
cancel-others:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
xtask:
uses: ./.github/workflows/xtask.yml
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Check xtask cache
uses: actions/cache@v3
id: xtask-cache
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
- name: Install rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: actions-rs/cargo@v1
with:
command: build
args: -p xtask
test-matrix-sdk-features:
name: 🐧 [m], ${{ matrix.name }}
@@ -34,9 +64,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
@@ -44,35 +74,32 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v1
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
key: "${{ matrix.name }}"
# ... 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' }}
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
key: xtask-${{ hashFiles('xtask/**') }}
- name: Test
run: |
target/debug/xtask ci test-features ${{ matrix.name }}
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci test-features ${{ matrix.name }}
test-matrix-sdk-examples:
name: 🐧 [m]-examples
@@ -85,26 +112,29 @@ jobs:
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
key: xtask-${{ hashFiles('xtask/**') }}
- name: Test
run: |
target/debug/xtask ci examples
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci examples
test-matrix-sdk-crypto:
name: 🐧 [m]-crypto
@@ -117,26 +147,29 @@ jobs:
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
key: xtask-${{ hashFiles('xtask/**') }}
- name: Test
run: |
target/debug/xtask ci test-crypto
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci test-crypto
test-all-crates:
name: ${{ matrix.name }}
@@ -161,34 +194,32 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v1
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Test
run: |
cargo nextest run --workspace \
--exclude matrix-sdk-integration-testing --features testing
uses: actions-rs/cargo@v1
with:
command: nextest
args: run --workspace --exclude matrix-sdk-integration-testing
- name: Test documentation
run: |
cargo test --doc --features docsrs
uses: actions-rs/cargo@v1
with:
command: test
args: --doc
test-wasm:
name: 🕸️ ${{ matrix.name }}
@@ -225,49 +256,95 @@ jobs:
- 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@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
targets: wasm32-unknown-unknown
toolchain: stable
target: wasm32-unknown-unknown
components: clippy
profile: minimal
override: true
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.3
version: latest
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
key: "${{ matrix.cmd }}"
# ... 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' }}
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
key: xtask-${{ hashFiles('xtask/**') }}
- name: Rust Check
run: |
target/debug/xtask ci wasm ${{ matrix.cmd }}
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci wasm ${{ matrix.cmd }}
- name: Wasm-Pack test
run: |
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
uses: actions-rs/cargo@v1
with:
command: run
args: -p 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: 🐧
- os: macos-latest
os-name: 🍏
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache@v3
with:
path: target/debug/xtask
key: xtask-${{ hashFiles('xtask/**') }}
- name: Run checks
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci test-appservice
formatting:
name: Check Formatting
@@ -279,14 +356,18 @@ jobs:
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2023-11-08
toolchain: nightly
components: rustfmt
profile: minimal
override: true
- name: Cargo fmt
run: |
cargo fmt -- --check
uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check
typos:
name: Spell Check with Typos
@@ -298,7 +379,7 @@ jobs:
uses: actions/checkout@v3
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.17.0
uses: crate-ci/typos@master
clippy:
name: Run clippy
@@ -310,32 +391,28 @@ jobs:
- name: Checkout the repo
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2023-11-08
toolchain: nightly
components: clippy
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache@v3
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
key: xtask-${{ hashFiles('xtask/**') }}
- name: Clippy
run: |
target/debug/xtask ci clippy
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci clippy
integration-tests:
name: Integration test
@@ -343,68 +420,35 @@ jobs:
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.11" # keep in sync with ./coverage.yml
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.94.0 # keep in sync with ./coverage.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
ports:
- 8008:8008
steps:
- name: Checkout the repo
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
uses: Swatinem/rust-cache@v1
- 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
env:
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
uses: actions-rs/cargo@v1
with:
command: nextest
args: run -p matrix-sdk-integration-testing
+34 -96
View File
@@ -5,21 +5,9 @@ on:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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:
code_coverage:
@@ -27,95 +15,45 @@ jobs:
runs-on: "ubuntu-latest"
if: github.event_name == 'push' || !github.event.pull_request.draft
# 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.11" # keep in sync with ./ci.yml
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.94.0 # keep in sync with ./ci.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
ports:
- 8008:8008
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
# 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@v1
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install tarpaulin
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-tarpaulin
- name: Install tarpaulin
uses: taiki-e/install-action@v2
with:
tool: cargo-tarpaulin
# set up backend for integration tests
- uses: actions/setup-python@v4
with:
python-version: 3.8
# set up backend for integration tests
- uses: actions/setup-python@v4
with:
python-version: 3.8
- uses: gnunicorn/setup-matrix-synapse@main
with:
uploadLogs: true
httpPort: 8228
disableRateLimiting: true
serverName: "matrix-sdk.rs"
- name: Run tarpaulin
run: |
rustup run stable cargo tarpaulin \
--skip-clean --profile cov --out xml \
--features experimental-widgets,testing
env:
CARGO_PROFILE_COV_INHERITS: 'dev'
CARGO_PROFILE_COV_DEBUG: 'false'
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
- name: Run tarpaulin
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: --out Xml
- 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
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
+37 -36
View File
@@ -4,20 +4,6 @@ on:
push:
branches: [main]
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pages: write
id-token: write
jobs:
docs:
@@ -29,42 +15,57 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2023-11-08
profile: minimal
toolchain: nightly
override: true
- name: Install Node.js
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' }}
uses: Swatinem/rust-cache@v1
# Keep in sync with xtask docs
- name: Build documentation
- name: Build rust documentation
uses: actions-rs/cargo@v1
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
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v1
with:
path: './target/doc/'
command: doc
args: --no-deps --features docsrs
- name: Deploy to GitHub Pages
- 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'
id: deployment
uses: actions/deploy-pages@v2
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./doc/
force_orphan: true
@@ -0,0 +1,117 @@
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: "${{ 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: matrix-org/matrix-rust-sdk/.github/workflows/release-crypto-nodejs.yml@main
needs: ['prepare-release']
name: "Trigger release Workflow"
with:
tag: ${{needs.prepare-release.outputs.tag}}
+139
View File
@@ -0,0 +1,139 @@
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
jobs:
upload-assets:
name: "Upload prebuilt libraries"
strategy:
fail-fast: false
matrix:
include:
# ----------------------------------- Linux
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: i686-unknown-linux-gnu
apt_install: gcc-i686-linux-gnu g++-i686-linux-gnu
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
apt_install: gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
- target: arm-unknown-linux-gnueabihf
os: ubuntu-latest
apt_install: gcc-arm-linux-gnueabihf
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
# ----------------------------------- 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: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
target: ${{ matrix.target }}
override: true
- name: Install Node.js
uses: actions/setup-node@v3
- name: Load cache
uses: Swatinem/rust-cache@v1
- 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: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- 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 }}
+63
View File
@@ -0,0 +1,63 @@
# This workflow releases the `matrix-sdk-crypto-js` project.
#
# It is triggered when a new tag appears that matches
# `matrix-sdk-crypto-js-v[0-9]+.*`. This workflow builds the package
# for the binding, run its tests to ensure everything is still
# correct, and publish 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-v[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: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18.0
- name: Install NPM dependencies
working-directory: ${{ env.PKG_PATH }}
run: npm install
- name: Configure NPM auth token
working-directory: ${{ env.PKG_PATH }}
run: npm set "//registry.npmjs.org/:_authToken" "${{ secrets.NPM_TOKEN }}"
- name: Publish the WebAssembly + JavaScript binding (imply building + testing)
working-directory: ${{ env.PKG_PATH }}
run: npm run publish
- 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
-76
View File
@@ -1,76 +0,0 @@
# A reusable github actions workflow that will build xtask, if it is not
# already cached.
#
# It will create a pair of GHA cache entries, if they do not already exist.
# The cache keys take the form `xtask-{os}-{hash}`, where "{os}" is "linux"
# or "macos", and "{hash}" is the hash of the xtask# directory.
#
# The cache keys are written to output variables named "cachekey-{os}".
#
name: Build xtask if necessary
on:
workflow_call:
outputs:
cachekey-linux:
description: "The cache key for the linux build artifact"
value: "${{ jobs.xtask.outputs.cachekey-linux }}"
cachekey-macos:
description: "The cache key for the macos build artifact"
value: "${{ jobs.xtask.outputs.cachekey-macos }}"
env:
CARGO_TERM_COLOR: always
jobs:
xtask:
name: "xtask-${{ matrix.os-name }}"
strategy:
fail-fast: true
matrix:
include:
- os: ubuntu-latest
os-name: 🐧
cachekey-id: linux
- os: macos-12
os-name: 🍏
cachekey-id: macos
runs-on: "${{ matrix.os }}"
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Calculate cache key
id: cachekey
# set a step output variable "cachekey-{os}" that can be referenced in
# the job outputs below.
run: |
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
- name: Check xtask cache
uses: actions/cache@v3
id: xtask-cache
with:
path: target/debug/xtask
# 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
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
run: |
cargo build -p xtask
outputs:
"cachekey-linux": "${{ steps.cachekey.outputs.cachekey-linux }}"
"cachekey-macos": "${{ steps.cachekey.outputs.cachekey-macos }}"
-8
View File
@@ -4,9 +4,6 @@ master.zip
emsdk-*
.idea/
.env
.build
.swiftpm
/Package.swift
## User settings
xcuserdata/
@@ -14,8 +11,3 @@ xcuserdata/
## OS garbage
.DS_Store
## Kotlin bindings generated files
bindings/kotlin/.gradle/
bindings/kotlin/buildSrc/build/
bindings/kotlin/buildSrc/.gradle/
+12 -28
View File
@@ -1,33 +1,17 @@
[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"
Nd = "Nd"
# 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"
Ot = "Ot"
# 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]
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
# automatically ignored, we ignore them here.
extend-exclude = [
"*.json",
# We are using some fuzzy match patterns that can be understood as typos confusingly.
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
]
extend-exclude = ["*.json"]
-102
View File
@@ -1,102 +0,0 @@
# Contributing to matrix-rust-sdk
## Chat rooms
In addition to our [main Matrix room], we have a development room at
[#matrix-rust-sdk-dev:flipdot.org]. Please use it to discuss potential changes,
the overall direction of development and related topics.
[main Matrix room]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
[#matrix-rust-sdk-dev:flipdot.org]: https://matrix.to/#/#matrix-rust-sdk-dev:flipdot.org
## Testing
You can run most of the tests that also happen in CI also using
`cargo xtask ci`. This needs a few dependencies to be installed, as it also runs
automatic WASM tests:
```bash
rustup component add clippy
cargo install cargo-nextest typos-cli wasm-pack
```
If you want to execute only one part of CI, there are a few sub-commands (see
`cargo xtask ci --help`).
Some tests are not automatically run in `cargo xtask ci`, for example the
integration tests that need a running synapse instance. These tests reside in
`./testing/matrix-sdk-integration-testing`. See its
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
synapse for testing purposes.
## 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:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
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.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/main
```
+120
View File
@@ -0,0 +1,120 @@
# Conventional Commits
This project uses [Conventional
Commits](https://www.conventionalcommits.org/). Read the
[Summary](https://www.conventionalcommits.org/en/v1.0.0/#summary) or
the [Full
Specification](https://www.conventionalcommits.org/en/v1.0.0/#specification)
to learn more.
## Types
Conventional Commits defines _type_ (as in `type(scope):
message`). This section aims at listing the types used inside this
project:
| Type | Definition |
|-|-|
| `feat` | About a new feature. |
| `fix` | About a bug fix. |
| `test` | About a test (suite, case, runner…). |
| `doc` | About a documentation modification. |
| `refactor` | About a refactoring. |
| `ci` | About a Continuous Integration modification. |
| `chore` | About some cleanup, or regular tasks. |
## Scopes
Conventional Commits defines _scope_ (as in `type(scope): message`). This
section aims at listing all the scopes used inside this project:
<table>
<thead>
<tr>
<th>Group</th>
<th>Scope</th>
<th>Definition</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="10">Crates</td>
<td><code>sdk</code></td>
<td>About the <code>matrix-sdk</code> crate.</td>
</tr>
<tr>
<td><code>appservice</code></td>
<td>About the <code>matrix-sdk-appservice</code> crate.</td>
</tr>
<tr>
<td><code>base</code></td>
<td>About the <code>matrix-sdk-base</code> crate.</td>
</tr>
<tr>
<td><code>common</code></td>
<td>About the <code>matrix-sdk-common</code> crate.</td>
</tr>
<tr>
<td><code>crypto</code></td>
<td>About the <code>matrix-sdk-crypto</code> crate.</td>
</tr>
<tr>
<td><code>indexeddb</code></td>
<td>About the <code>matrix-sdk-indexeddb</code> crate.</td>
</tr>
<tr>
<td><code>qrcode</code></td>
<td>About the <code>matrix-sdk-qrcode</code> crate.</td>
</tr>
<tr>
<td><code>sled</code></td>
<td>About the <code>matrix-sdk-sled</code> crate.</td>
</tr>
<tr>
<td><code>store-encryption</code></td>
<td>About the <code>matrix-sdk-store-encryption</code> crate.</td>
</tr>
<tr>
<td><code>test</code></td>
<td>About the <code>matrix-sdk-test</code> and <code>matrix-sdk-test-macros</code> crate.</td>
</tr>
<tr>
<td rowspan="4">Bindings</td>
<td><code>apple</code></td>
<td>About the <code>matrix-rust-components-swift</code> binding.</td>
</tr>
<tr>
<td><code>crypto-nodejs</code></td>
<td>About the <code>matrix-sdk-crypto-nodejs</code> binding.</td>
</tr>
<tr>
<td><code>crypto-js</code></td>
<td>About the <code>matrix-sdk-crypto-js</code> binding.</td>
</tr>
<tr>
<td><code>crypto-ffi</code></td>
<td>About the <code>matrix-sdk-crypto-ffi</code> binding.</td>
</tr>
<tr>
<td>Labs</td>
<td><code>sled-state-inspector</code></td>
<td>About the <code>sled-state-inspector</code> project.</td>
</tr>
<tr>
<td>Continuous Integration</td>
<td><code>xtask</code></td>
<td>About the <code>xtask</code> project.</td>
</tr>
</tbody>
</table>
## Generating `CHANGELOG.md`
The [`git-cliff`](https://github.com/orhun/git-cliff) project is used
to generate `CHANGELOG.md` automatically. Hence the various
`cliff.toml` files that are present in this project, or the
`package.metadata.git-cliff` sections in various `Cargo.toml` files.
Its companion,
[`git-cliff-action`](https://github.com/orhun/git-cliff-action)
project, is used inside Github Action workflows.
Generated
+1938 -3379
View File
File diff suppressed because it is too large Load Diff
+7 -73
View File
@@ -2,77 +2,26 @@
members = [
"benchmarks",
"bindings/matrix-sdk-crypto-ffi",
"bindings/matrix-sdk-crypto-js",
"bindings/matrix-sdk-crypto-nodejs",
"bindings/matrix-sdk-ffi",
"crates/*",
"testing/*",
"examples/*",
"uniffi-bindgen",
"labs/*",
"xtask",
]
# xtask, testing and the bindings should only be built when invoked explicitly.
# xtask, labs, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*"]
resolver = "2"
[workspace.package]
rust-version = "1.70"
[workspace.dependencies]
anyhow = "1.0.68"
assert-json-diff = "2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.1"
async-rx = "0.1.3"
async-stream = "0.3.3"
async-trait = "0.1.60"
as_variant = "1.2.0"
base64 = "0.21.0"
byteorder = "1.4.3"
eyeball = { version = "0.8.7", features = ["tracing"] }
eyeball-im = { version = "0.4.1", features = ["tracing"] }
eyeball-im-util = "0.5.1"
futures-core = "0.3.28"
futures-executor = "0.3.21"
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
http = "0.2.6"
itertools = "0.12.0"
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
ruma-common = "0.12.0"
once_cell = "1.16.0"
rand = "0.8.5"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
sha2 = "0.10.8"
stream_assert = "0.1.1"
thiserror = "1.0.38"
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.14"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
vodozemac = { version = "0.7.0" }
zeroize = "1.6.0"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
lto = true
# Default development profile; default for most Cargo commands, otherwise
# selected with `--debug`
[profile.dev]
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
# Copied from rust-analyzer. Saves a lot of disk space and hopefully
# compilation time / mem usage too, at the expense of potentially having to
# change this setting here when you want to use a debugger.
debug = 0
[profile.dev.package]
@@ -80,18 +29,3 @@ 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 }
# Custom profile with full debugging info, use `--profile dbg` to select
[profile.dbg]
inherits = "dev"
debug = 2
# Custom profile for use in (debug) builds of the binding crates, use
# `--profile reldbg` to select
[profile.reldbg]
inherits = "dbg"
opt-level = 3
[patch.crates-io]
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
+2 -2
View File
@@ -1,4 +1,4 @@
![Build Status](https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat-square)
![Build Status](https://img.shields.io/github/workflow/status/matrix-org/matrix-rust-sdk/CI?style=flat-square)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
@@ -26,7 +26,7 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.60`
## Status
+2 -2
View File
@@ -51,7 +51,7 @@ You can stay informed about updates on the access token by listening to `client.
- [`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 now called `is_verified()`.
- `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`
@@ -61,7 +61,7 @@ You can stay informed about updates on the access token by listening to `client.
## Quick Troubleshooting
You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly:
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
+9 -15
View File
@@ -3,29 +3,23 @@ name = "benchmarks"
description = "Matrix SDK benchmarks"
edition = "2021"
license = "Apache-2.0"
rust-version = { workspace = true }
rust-version = "1.56"
version = "1.0.0"
publish = false
[dependencies]
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
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 = { workspace = true }
ruma = { workspace = true }
serde_json = { workspace = true }
criterion = { version = "0.3.5", 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 = "0.7.0"
serde_json = "1.0.79"
tempfile = "3.3.0"
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] }
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
pprof = { version = "0.10.0", features = ["flamegraph", "criterion"] }
[[bench]]
name = "crypto_bench"
harness = false
[[bench]]
name = "store_bench"
harness = false
+15 -49
View File
@@ -1,8 +1,8 @@
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_sled::SledCryptoStore;
use matrix_sdk_test::response_from_file;
use ruma::{
api::{
@@ -30,7 +30,7 @@ fn keys_query_response() -> get_keys::v3::Response {
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
.expect("Can't parse the keys upload response")
}
fn keys_claim_response() -> claim_keys::v3::Response {
@@ -38,7 +38,7 @@ fn keys_claim_response() -> claim_keys::v3::Response {
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
claim_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
.expect("Can't parse the keys upload response")
}
fn huge_keys_query_response() -> get_keys::v3::Response {
@@ -46,7 +46,7 @@ fn huge_keys_query_response() -> get_keys::v3::Response {
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the `/keys/query` response")
.expect("Can't parse the keys query response")
}
pub fn keys_query(c: &mut Criterion) {
@@ -65,30 +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 store = Arc::new(SledCryptoStore::open_with_passphrase(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()
}
@@ -117,21 +108,18 @@ 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(SledCryptoStore::open_with_passphrase(dir.path(), None).unwrap());
let machine = runtime
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store))
@@ -142,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,
)
@@ -175,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
@@ -197,18 +180,15 @@ pub fn room_key_sharing(c: &mut Criterion) {
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(SledCryptoStore::open_with_passphrase(dir.path(), 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(
@@ -229,11 +209,6 @@ pub fn room_key_sharing(c: &mut Criterion) {
})
});
{
let _guard = runtime.enter();
drop(machine);
}
group.finish()
}
@@ -254,35 +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(SledCryptoStore::open_with_passphrase(dir.path(), 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()
}
-125
View File
@@ -1,125 +0,0 @@
use std::sync::Arc;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
config::StoreConfig,
matrix_auth::{MatrixSession, MatrixSessionTokens},
Client, RoomInfo, RoomState, 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: MatrixSessionTokens { 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().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().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);
-41
View File
@@ -1,41 +0,0 @@
## Introduction
**matrix-rust-sdk** leverages [UniFFI](https://mozilla.github.io/uniffi-rs/) to generate bindings for host languages (eg. Swift and Kotlin).
Rust code related with bindings live in the [matrix-rust-sdk/bindings](https://github.com/matrix-org/matrix-rust-sdk/tree/main/bindings) folder.
Developers can expose Rust code to UniFFI using two different approaches:
- Using an `.udl` file. When a crate has one, you find it under the `src` folder (an example is [here](https://github.com/matrix-org/matrix-rust-sdk/blob/main/bindings/matrix-sdk-ffi/src/api.udl)).
- Add UniFFI directivies as Rust attributes. In this case Rust source files (`.rs`) contain attributes related to UniFFI (e.g. `#[uniffi::export]`). Attributes are preferred, where applicable.
## Expose Rust definitions to UniFFI
### Check if the API is already on UniFFI
First of all check if the Rust definition you are looking for exists on UniFFI already. Most of exposed matrix definitions are collected in the crate [matrix-sdk-ffi](https://github.com/matrix-org/matrix-rust-sdk/tree/main/bindings/matrix-sdk-ffi).
This crate contains mainly small Rust wrappers around the actual Rust SDK (e.g. the crate [matrix-sdk](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk))
If the Rust definition is on UniFFI already, you either:
- find it in a `.udl` file like [matrix-sdk-ffi/src/api.udl](https://github.com/matrix-org/matrix-rust-sdk/blob/main/bindings/matrix-sdk-ffi/src/api.udl)
- see it marked with a proper UniFFI Rust attribute like this `#[uniffi::export]`
### Adding a missing matrix API
1. Unless you want to contribute on the crypto side, you probably need to add some code in the [matrix-sdk-ffi](https://github.com/matrix-org/matrix-rust-sdk/tree/main/bindings/matrix-sdk-ffi) crate. After you find the crate you need to understand which file is best to contain the new Rust definition. When exposing new matrix API often (but not always) you need to touch the file [client.rs](https://github.com/matrix-org/matrix-rust-sdk/blob/main/bindings/matrix-sdk-ffi/src/client.rs)
2. Identify the API to expose in the target Rust crate (typically in [matrix-sdk](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk). If you cant find it, you probably need to touch the actual Rust SDK as well. In this case you typically just need to write some code around [ruma](https://github.com/ruma/ruma) (a Rust SDKs dependency) which already implements most of the matrix protocol
3. After you got (by finding or writing) the required Rust code, you need to expose to UniFFI. To do that just write a small Rust wrapper in the related UniFFI crate (most of the time is **matrix-sdk-ffi**) you found on step 1.
4. When your new (wrapping) Rust definition is ready, remember to expose it to UniFFI.
Its best to do it using UniFFI Rust attributes (e.g. `#[uniffi::export]`). Otherwise add the new definition in the crates `.udl` file. For the **matrix-sdk-ffi** crate the definition file is [api.udl](https://github.com/matrix-org/matrix-rust-sdk/blob/main/bindings/matrix-sdk-ffi/src/api.udl). **Remember**: the language inside a `.udl` file isnt Rust. To learn more about how map Rust into UDL read [here](https://mozilla.github.io/uniffi-rs/udl_file_spec.html)
## FAQ
**Q**: I wrote my Rust code and exposed it to UniFFI. How can I check if the compiler is happy?\
**A**: Run `cargo build` in the crate you touched (e.g. matrix-sdk-ffi). The compiler will complain if the Rust code and/or the `.udl` is wrong.
**Q**: The compiler is happy with my code but the CI is failing on GitHub. How can I fix it?\
**A**: The CI may fail for different reasons, you need to have a look on the failing GitHub workflow. One common reason though is that the linter ([Clippy](https://github.com/rust-lang/rust-clippy)) isnt happy with your code. If this is the case, you can run `cargo clippy` in the crate you touched to see whats wrong and fix it accordingly.
+5 -13
View File
@@ -5,26 +5,18 @@ maintained by the owners of the Matrix Rust SDK project.
* [`apple`] or `matrix-rust-components-swift`, Swift bindings of the
[`matrix-sdk`] crate via [`matrix-sdk-ffi`],
* [`matrix-sdk-crypto-ffi`], UniFFI (Kotlin, Swift, Python, Ruby) bindings of the [`matrix-sdk-crypto`]
* [`matrix-sdk-crypto-ffi`], 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-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`], bindings of the [`matrix-sdk`] crate,
[`apple`]: ./apple
[`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi
[`matrix-sdk-crypto-js`]: ../crates/matrix-sdk-crypto
[`matrix-sdk-crypto-nodejs`]: ../crates/matrix-sdk-crypto
[`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
[`matrix-sdk`]: ../crates/matrix-sdk
[`matrix-sdk-crypto-js`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-web
[`matrix-sdk-crypto-nodejs`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-nodejs
## Contributing
To contribute read this [guide](./CONTRIBUTING.md).
-8
View File
@@ -1,8 +0,0 @@
// swift-tools-version:5.6
// A package manifest for local development. This file will be copied
// into the root of the repo when generating an XCFramework.
import PackageDescription
let package = Package()
-24
View File
@@ -1,24 +0,0 @@
// swift-tools-version:5.6
// A package manifest for local development. This file will be copied
// into the root of the repo when generating an XCFramework.
import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.macOS(.v12)
],
products: [
.library(name: "MatrixRustSDK",
targets: ["MatrixRustSDK"]),
],
targets: [
.binaryTarget(name: "MatrixSDKFFI", path: "bindings/apple/generated/MatrixSDKFFI.xcframework"),
.target(name: "MatrixRustSDK",
dependencies: [.target(name: "MatrixSDKFFI")],
path: "bindings/apple/generated/swift")
]
)
@@ -1,21 +0,0 @@
# Podspec file for local-development only, which points to local version of generated framework instead of github release
# To use this, the file needs to be copied to the root of `matrix-rust-sdk` in order to have access to the source files.`
Pod::Spec.new do |s|
s.name = "MatrixSDKCrypto"
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 = "11.0"
s.osx.deployment_target = "10.10"
s.swift_versions = ['5.1', '5.2']
s.source = { :git => "Not Published", :tag => "Cocoapods/#{s.name}/#{s.version}" }
s.vendored_frameworks = "generated/MatrixSDKCryptoFFI.xcframework"
s.source_files = "generated/Sources/**/*.{swift}"
s.resources = ["bindings/matrix-sdk-crypto-ffi/src/**/*.rs", "crates/matrix-sdk-crypto/src/**/*.rs"]
end
+1 -3
View File
@@ -1,15 +1,13 @@
Pod::Spec.new do |s|
s.name = "MatrixSDKCrypto"
s.version = "0.0.1" # Version is only incremented manually and locally before pushing to CocoaPods
s.version = "0.1.0"
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
s.author = { "matrix.org" => "support@matrix.org" }
s.ios.deployment_target = "11.0"
s.osx.deployment_target = "10.10"
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" }
+5 -17
View File
@@ -5,29 +5,17 @@ import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.macOS(.v12)
],
platforms: [.iOS(.v15)],
products: [
.library(name: "MatrixRustSDK",
targets: ["MatrixRustSDK"]),
],
targets: [
.binaryTarget(name: "MatrixSDKFFI", path: "generated/MatrixSDKFFI.xcframework"),
.target(name: "MatrixRustSDK",
path: "generated/swift",
swiftSettings: [
.unsafeFlags(["-I", "./generated/matrix_sdk_ffi"])
]),
dependencies: [.target(name: "MatrixSDKFFI")],
path: "generated/swift"),
.testTarget(name: "MatrixRustSDKTests",
dependencies: ["MatrixRustSDK"],
swiftSettings: [
.unsafeFlags(["-I", "./generated/matrix_sdk_ffi"])
],
linkerSettings: [
.linkedLibrary("matrix_sdk_ffi", .when(platforms: [.macOS])),
.linkedLibrary("matrix_sdk_ffiFFI", .when(platforms: [.linux])),
.unsafeFlags(["-L./generated/matrix_sdk_ffi"])
])
dependencies: ["MatrixRustSDK"]),
]
)
+20 -40
View File
@@ -2,37 +2,32 @@
This project and build script demonstrate how to create an XCFramework that can be imported into an Xcode project and run on Apple platforms. It can compile and bundle an [entire SDK](#Building-the-SDK), or only a smaller [Crypto module](#Building-only-the-Crypto-SDK) that provides end-to-end encryption for clients that already depend on an SDK (e.g. [Matrix iOS SDK](https://github.com/matrix-org/matrix-ios-sdk))
## Building
## Prerequisites for building universal frameworks
### Prerequisites for building universal frameworks
* the Rust toolchain
* UniFFI - `cargo install uniffi_bindgen`
* Apple targets (e.g. `rustup target add aarch64-apple-ios`)
* `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html)
* `lipo` for creating the fat static libs
- the Rust toolchain
- Apple targets (e.g. `rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-darwin x86_64-apple-darwin`)
- `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html)
- `lipo` for creating the fat static libs
### Building the SDK
## Building the SDK
```
cargo xtask swift build-framework
sh build_xcframework.sh
```
The `build-framework` task will go through all the steps required to generate a fully usable `.xcframework`:
The `build_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`:
1. compile `matrix-sdk-ffi` libraries for iOS, the iOS simulator and macOS under `/target`. Some targets are not part of the standard library and they will be built using the nightly toolchain.
1. compile `matrix-sdk-ffi` libraries for iOS, the iOS simulator, MacOS, and Mac Catalyst under `/target`. Some targets are not part of the standard library and they will be built using the nightly toolchain.
2. `lipo` together the libraries for the same platform under `/generated`
3. run `uniffi` and generate the C header, module map and swift files
4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKFFI.xcframework`
5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework)
For development purposes, it will additionally generate a `Package.swift` file in the root of the repo that can be used to add the framework to your project and enable debugging through the use of [rust-xcode-plugin](https://github.com/BrainiumLLC/rust-xcode-plugin) (make sure to run the task with the argument `--profile=reldbg`).
When building the SDK for release you should pass the `--release` argument to the task, which will strip away any symbols and optimise the created binary.
### Building only the Crypto SDK
## Building only the Crypto SDK
```
build_crypto_xcframework.sh
sh build_crypto_xcframework.sh
```
The `build_crypto_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`:
@@ -43,31 +38,16 @@ The `build_crypto_xcframework.sh` script will go through all the steps required
4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKCryptoFFI.xcframework`
5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework)
### Building & testing the Swift package
## Running the Xcode project
The `Package.swift` file in this directory provides a simple example on how to integrate everything together but also a place to run unit and integration tests from.
The Xcode project is meant to provide a simple example on how to integrate everything together but also a place to run unit and integration tests from.
It's pre-configured to link to the generated static lib and .swift files so successfully running `cargo xtask swift build-library` first is necessary for it to compile. Afterwards you can execute the tests with `swift test`. Note that for the moment this only works on macOS but we're planning to add Linux support in the future.
It's pre-configured to link to the generated .xcframework and .swift files so successfully running the script first is necessary for it to compile.
It makes the compiled code available to swift by importing the C header through its bridging header.
Once all the generated components are available running it should be as easy as choosing a platform and clicking run.
## Distribution
The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a [Swift package](https://github.com/matrix-org/matrix-rust-components-swift/) in the case of SDK, and as a CocoaPods podspec in the case of Crypto SDK.
### Publishing MatrixSDKCrypto
1. Navigate into `bindings/apple` and run [`build_crypto_xcframework.sh`](#building-only-the-crypto-sdk) script which generates a .zip file with the framework in `./generated` folder
Note that whilst you can run this command from any git branch, if you want to make a public release, ensure you are building from latest `main`
2. Tag the commit you just used to build the release and push the tag to GitHub
Use `matrix-sdk-crypto-ffi-<major>.<minor>.<patch>` tag name
3. Create a new [GitHub release](https://github.com/matrix-org/matrix-rust-sdk/releases) using this tag (see [example](https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-crypto-ffi-0.3.4))
4. Upload the previously generated .zip file to this release
5. Increment the version in [`MatrixSDKCrypto.podspec`](./MatrixSDKCrypto.podspec)
Note that this is not automated and is a local-only change. To determine the most recent published version, run `pod repo update && pod search MatrixSDKCrypto`
or check git tags via `git tag | grep matrix-sdk-crypto-ffi`
6. Push new Podspec version to Cocoapods via `pod trunk push MatrixSDKCrypto.podspec --allow-warnings`
The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a Swift package available [TBD](here) in the case of SDK, and as CocoaPods podspec in the case of Crypto SDK.
@@ -11,17 +11,6 @@ final class ClientTests: XCTestCase {
XCTFail("The client should build successfully when given a homeserver.")
}
}
func testBuildingWithHomeserverURLAndUserAgent() {
do {
_ = try ClientBuilder()
.homeserverUrl(url: "https://localhost:8008")
.userAgent(userAgent: "golden-eye/007")
.build()
} catch {
XCTFail("The client should build successfully when given a homeserver and user agent.")
}
}
func testBuildingWithUsername() {
do {
+10 -38
View File
@@ -10,7 +10,7 @@ TARGET_DIR="${SRC_ROOT}/target"
GENERATED_DIR="${SRC_ROOT}/generated"
if [ -d "${GENERATED_DIR}" ]; then rm -rf "${GENERATED_DIR}"; fi
mkdir -p ${GENERATED_DIR}/{macos,simulator}
mkdir -p ${GENERATED_DIR}
REL_FLAG="--release"
REL_TYPE_DIR="release"
@@ -19,47 +19,23 @@ TARGET_CRATE=matrix-sdk-crypto-ffi
# Build static libs for all the different architectures
# Required by olm-sys crate
export IOS_SDK_PATH=`xcrun --show-sdk-path --sdk iphoneos`
# 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"
# 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
# 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"
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_crypto_ffi.a"
# Generate uniffi files
cargo uniffi-bindgen generate \
--language swift \
--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"
uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" --language swift --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" --out-dir ${GENERATED_DIR}
# Move headers to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
@@ -79,23 +55,19 @@ mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_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" \
-library "${GENERATED_DIR}/libmatrix_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
# Cleanup
if [ -d "${GENERATED_DIR}/macos" ]; then rm -rf "${GENERATED_DIR}/macos"; fi
if [ -d "${GENERATED_DIR}/simulator" ]; then rm -rf "${GENERATED_DIR}/simulator"; fi
if [ -f "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" ]; then rm -rf "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a"; fi
if [ -f "${GENERATED_DIR}/libmatrix_crypto_ffi.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_crypto_ffi.a"; fi
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
# Zip up framework, sources and LICENSE, ready to be uploaded to GitHub Releases and used by MatrixSDKCrypto.podspec
cp ${SRC_ROOT}/LICENSE $GENERATED_DIR
cd $GENERATED_DIR
zip -r MatrixSDKCryptoFFI.zip MatrixSDKCryptoFFI.xcframework Sources LICENSE
rm LICENSE
echo "XCFramework is ready 🚀"
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -eEu
cd "$(dirname "$0")"
# Path to the repo root
SRC_ROOT=../..
TARGET_DIR="${SRC_ROOT}/target"
GENERATED_DIR="${SRC_ROOT}/generated"
mkdir -p ${GENERATED_DIR}
REL_FLAG="--release"
REL_TYPE_DIR="release"
# Build static libs for all the different architectures
# iOS
echo -e "Building for iOS [1/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios"
# MacOS
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-darwin"
echo -e "\nBuilding for macOS (Intel) [3/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-darwin"
# iOS Simulator
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios-sim"
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-ios"
echo -e "\nCreating XCFramework"
# Lipo together the libraries for the same platform
# MacOS
lipo -create \
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a"
# iOS Simulator
lipo -create \
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
# Generate uniffi files
# Architecture for the .a file argument doesn't matter, since the API is the same on all
uniffi-bindgen generate \
--language swift \
--lib-file "${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
--out-dir ${GENERATED_DIR} \
"${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl"
# Move them to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
mkdir -p ${HEADERS_DIR}
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
# Rename and move modulemap to the right place
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
SWIFT_DIR="${GENERATED_DIR}/swift"
mkdir -p ${SWIFT_DIR}
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
# Build the xcframework
if [ -d "${GENERATED_DIR}/MatrixSDKFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" \
-headers ${HEADERS_DIR} \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKFFI.xcframework"
# Cleanup
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a"; fi
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"; fi
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -eEu
cd "$(dirname "$0")"
IS_CI=false
ACTIVE_ARCH="arm64"
if [ $# -eq 2 ]; then
echo "Running CI build"
IS_CI=true
ARCHS=( $1 )
ACTIVE_ARCH=${ARCHS[0]}
elif [ $# -eq 1 ]; then
echo "Running debug build"
ARCHS=( $1 )
ACTIVE_ARCH=${ARCHS[0]}
else
echo "Running debug build"
fi
echo "Active architecture ${ACTIVE_ARCH}"
# Path to the repo root
SRC_ROOT=../..
TARGET_DIR="${SRC_ROOT}/target"
GENERATED_DIR="${SRC_ROOT}/bindings/apple/generated"
mkdir -p ${GENERATED_DIR}
REL_FLAG=""
REL_TYPE_DIR="debug"
# iOS Simulator arm64
if [ "$ACTIVE_ARCH" = "arm64" ]; then
TARGET="aarch64-apple-ios-sim"
# iOS Simulator intel
else
TARGET="x86_64-apple-ios"
fi
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "$TARGET"
lipo -create \
"${TARGET_DIR}/$TARGET/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
# Generate uniffi files
uniffi-bindgen generate \
--language swift \
--lib-file "${TARGET_DIR}/$TARGET/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
--out-dir ${GENERATED_DIR} \
"${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl"
# Move them to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
mkdir -p ${HEADERS_DIR}
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
# Rename and move modulemap to the right place
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
SWIFT_DIR="${GENERATED_DIR}/swift"
mkdir -p ${SWIFT_DIR}
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
# Build the xcframework
if [ -d "${GENERATED_DIR}/MatrixSDKFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKFFI.xcframework"
# Cleanup
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"; fi
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
if [ "$IS_CI" = false ] ; then
echo "Preparing matrix-rust-components-swift"
# Debug -> Copy generated files over to ../../../matrix-rust-components-swift
rsync -a --delete "${GENERATED_DIR}/MatrixSDKFFI.xcframework" "${SRC_ROOT}/../matrix-rust-components-swift/"
rsync -a --delete "${GENERATED_DIR}/swift/" "${SRC_ROOT}/../matrix-rust-components-swift/Sources/MatrixRustSDK"
fi
+34 -30
View File
@@ -3,7 +3,7 @@ name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
rust-version = { workspace = true }
rust-version = "1.60"
description = "Uniffi based bindings for the Rust SDK crypto crate"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
license = "Apache-2.0"
@@ -11,51 +11,55 @@ publish = false
[lib]
crate-type = ["cdylib", "staticlib"]
[features]
default = ["bundled-sqlite"]
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
name = "matrix_crypto_ffi"
[dependencies]
anyhow = { workspace = true }
futures-util = "0.3.28"
anyhow = "1.0.57"
base64 = "0.13.0"
hmac = "0.12.1"
http = { workspace = true }
matrix-sdk-common = { 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 = { version = "0.3.17", features = ["env-filter"] }
http = "0.2.6"
pbkdf2 = "0.11.0"
rand = "0.8.5"
ruma = { version = "0.7.0", features = ["client-api-c"] }
serde = "1.0.136"
serde_json = "1.0.79"
sha2 = "0.10.2"
thiserror = "1.0.30"
tracing = "0.1.34"
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
uniffi = { workspace = true }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "091c3561656e72e1a4160412c83b36d98e556d06" }
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
[dependencies.js_int]
version = "0.2.2"
features = ["lax_deserialize"]
[dependencies.matrix-sdk-crypto]
workspace = true
features = ["qrcode", "automatic-room-key-forwarding"]
[dependencies.matrix-sdk-common]
path = "../../crates/matrix-sdk-common"
version = "0.6.0"
[dependencies.matrix-sdk-sqlite]
workspace = true
[dependencies.matrix-sdk-crypto]
path = "../../crates/matrix-sdk-crypto"
version = "0.6.0"
features = ["qrcode", "backups_v1"]
[dependencies.matrix-sdk-sled]
path = "../../crates/matrix-sdk-sled"
version = "0.2.0"
default_features = false
features = ["crypto-store"]
[dependencies.tokio]
version = "1.33.0"
version = "1.17.0"
default_features = false
features = ["rt-multi-thread"]
[dependencies.vodozemac]
version = "0.3.0"
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "091c3561656e72e1a4160412c83b36d98e556d06", features = ["builtin-bindgen"] }
[dev-dependencies]
tempfile = "3.8.0"
assert_matches2 = { workspace = true }
tempfile = "3.3.0"
+1 -3
View File
@@ -5,8 +5,6 @@ README mainly describes how to build and integrate the bindings into a Kotlin
based Android project, but the Android specific bits can be skipped if you are
targeting an x86 Linux project.
To build and distribute bindings for iOS projects, see a [dedicated page](../apple/README.md)
## Prerequisites
### Rust
@@ -73,7 +71,7 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.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`.
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.60`.
## License
+2 -38
View File
@@ -1,39 +1,3 @@
use std::{env, error::Error};
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 comes from: https://github.com/mozilla/application-services/pull/5442
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" {
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
let build_os = match env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"windows" => "windows",
_ => panic!(
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
}
}
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
uniffi::generate_scaffolding("./src/olm.udl")?;
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
Ok(())
fn main() {
uniffi_build::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::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
backups::OlmPkDecryptionError,
store::{CryptoStoreError as InnerStoreError, RecoveryKey},
};
use pbkdf2::pbkdf2;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
@@ -12,24 +12,21 @@ 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>,
}
/// Error type for the decryption of backed up room keys.
#[derive(Debug, Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, Error)]
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.
#[derive(Debug, Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, Error)]
pub enum DecodeError {
/// An error happened while decoding the recovery key.
#[error(transparent)]
@@ -42,7 +39,7 @@ pub enum DecodeError {
/// Struct containing info about the way the backup key got derived from a
/// passphrase.
#[derive(Debug, Clone, uniffi::Record)]
#[derive(Debug, Clone)]
pub struct PassphraseInfo {
/// The salt that was used during key derivation.
pub private_key_salt: String,
@@ -51,7 +48,6 @@ pub struct PassphraseInfo {
}
/// The public part of the backup key.
#[derive(uniffi::Record)]
pub struct MegolmV1BackupKey {
/// The actual base64 encoded public key.
pub public_key: String,
@@ -67,36 +63,29 @@ impl BackupRecoveryKey {
const KEY_SIZE: usize = 32;
const SALT_SIZE: usize = 32;
const PBKDF_ROUNDS: i32 = 500_000;
}
#[uniffi::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,28 +97,41 @@ 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,
}),
})
}
}
/// 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();
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(), v)).collect()))
.collect();
MegolmV1BackupKey {
public_key: public_key.to_base64(),
signatures,
passphrase_info: self.passphrase_info.clone(),
backup_algorithm: public_key.backup_algorithm().to_owned(),
}
}
/// Convert the recovery key to a base 58 encoded string.
@@ -142,39 +144,6 @@ impl BackupRecoveryKey {
self.inner.to_base64()
}
/// 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();
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(),
)
})
.collect();
MegolmV1BackupKey {
public_key: public_key.to_base64(),
signatures,
passphrase_info: self.passphrase_info.clone(),
backup_algorithm: public_key.backup_algorithm().to_owned(),
}
}
/// Try to decrypt a message that was encrypted using the public part of the
/// backup key.
pub fn decrypt_v1(
@@ -183,51 +152,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,191 +0,0 @@
use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_crypto::dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use serde_json::json;
use tokio::runtime::Handle;
use zeroize::Zeroize;
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum DehydrationError {
#[error(transparent)]
Pickle(#[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),
}
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::MissingSigningKey(e) => {
Self::MissingSigningKey(e)
}
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
}
}
}
#[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);
}
}
}
#[uniffi::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: Vec<u8>,
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 mut key = get_pickle_key(&pickle_key)?;
let ret = RehydratedDevice {
runtime: self.runtime.to_owned(),
inner: ManuallyDrop::new(self.runtime.block_on(self.inner.rehydrate(
&key,
&device_id,
device_data,
))?),
}
.into();
key.zeroize();
Ok(ret)
}
}
#[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);
}
}
}
#[uniffi::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);
}
}
}
#[uniffi::export]
impl DehydratedDevice {
pub fn keys_for_upload(
&self,
device_display_name: String,
pickle_key: Vec<u8>,
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
let mut key = get_pickle_key(&pickle_key)?;
let request =
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
key.zeroize();
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 }
}
}
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
let pickle_key_length = pickle_key.len();
if pickle_key_length == 32 {
let mut key = Box::new([0u8; 32]);
key.copy_from_slice(pickle_key);
Ok(key)
} else {
Err(DehydrationError::PickleKeyLength(pickle_key_length))
}
}
@@ -3,7 +3,6 @@ use std::collections::HashMap;
use matrix_sdk_crypto::Device as InnerDevice;
/// An E2EE capable Matrix device.
#[derive(uniffi::Record)]
pub struct Device {
/// The device owner.
pub user_id: String,
@@ -25,9 +24,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, seconds since
/// epoch.
pub first_time_seen_ts: u64,
}
impl From<InnerDevice> for Device {
@@ -41,7 +37,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(),
}
}
}
+11 -74
View File
@@ -4,11 +4,9 @@ use matrix_sdk_crypto::{
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
};
use matrix_sdk_sqlite::OpenStoreError;
use ruma::{IdParseError, OwnedUserId};
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, thiserror::Error)]
pub enum KeyImportError {
#[error(transparent)]
Export(#[from] KeyExportError),
@@ -18,8 +16,7 @@ pub enum KeyImportError {
Json(#[from] serde_json::Error),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, thiserror::Error)]
pub enum SecretImportError {
#[error(transparent)]
CryptoStore(#[from] InnerStoreError),
@@ -27,8 +24,7 @@ pub enum SecretImportError {
Import(#[from] RustSecretImportError),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
#[derive(Debug, thiserror::Error)]
pub enum SignatureError {
#[error(transparent)]
Signature(#[from] InnerSignatureError),
@@ -42,11 +38,8 @@ 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),
#[error(transparent)]
CryptoStore(#[from] InnerStoreError),
#[error(transparent)]
@@ -59,68 +52,12 @@ pub enum CryptoStoreError {
Identifier(#[from] IdParseError),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[derive(Debug, thiserror::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 {
MegolmError::MissingRoomKey(withheld_code) => Self::MissingRoomKey {
error: "Withheld Inbound group session".to_owned(),
withheld_code: withheld_code.map(|w| w.as_str().to_owned()),
},
_ => Self::Megolm { error: value.to_string() },
}
}
}
impl From<serde_json::Error> for DecryptionError {
fn from(err: serde_json::Error) -> Self {
Self::Serialization { error: err.to_string() }
}
}
impl From<IdParseError> for DecryptionError {
fn from(err: IdParseError) -> Self {
Self::Identifier { error: err.to_string() }
}
}
impl From<InnerStoreError> for DecryptionError {
fn from(err: InnerStoreError) -> Self {
Self::Store { error: err.to_string() }
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_let;
use matrix_sdk_crypto::MegolmError;
use super::DecryptionError;
#[test]
fn test_withheld_error_mapping() {
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
);
assert_eq!("m.unverified", code)
}
#[error(transparent)]
Serialization(#[from] serde_json::Error),
#[error(transparent)]
Identifier(#[from] IdParseError),
#[error(transparent)]
Megolm(#[from] MegolmError),
}
+108 -561
View File
@@ -4,10 +4,8 @@
//! client or client library in any of the language targets Uniffi supports.
#![warn(missing_docs)]
#![allow(unused_qualifications)]
mod backup_recovery_key;
mod dehydrated_devices;
mod device;
mod error;
mod logger;
@@ -16,9 +14,8 @@ mod responses;
mod users;
mod verification;
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc};
use anyhow::Context;
pub use backup_recovery_key::{
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
};
@@ -28,37 +25,22 @@ 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;
use matrix_sdk_crypto::{
backups::SignatureState,
olm::{IdentityKeys, InboundGroupSession, Session},
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
EncryptionSettings as RustEncryptionSettings, LocalTrust,
};
use matrix_sdk_sqlite::SqliteCryptoStore;
pub use machine::{KeyRequestPair, OlmMachine};
use matrix_sdk_crypto::types::{EventEncryptionAlgorithm, SigningKey};
pub use responses::{
BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest,
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
};
use ruma::{
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId,
SecondsSinceUnixEpoch, UserId,
};
use ruma::{DeviceId, DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId};
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
pub use users::UserIdentity;
pub use verification::{
CancelInfo, ConfirmVerificationResult, QrCode, QrCodeListener, QrCodeState,
RequestVerificationResult, Sas, SasListener, SasState, ScanResult, StartSasResult,
Verification, VerificationRequest, VerificationRequestListener, VerificationRequestState,
CancelInfo, ConfirmVerificationResult, QrCode, RequestVerificationResult, Sas, ScanResult,
StartSasResult, Verification, VerificationRequest,
};
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
/// Struct collecting data that is important to migrate to the rust-sdk
#[derive(Deserialize, Serialize, uniffi::Record)]
#[derive(Deserialize, Serialize)]
pub struct MigrationData {
/// The pickled version of the Olm Account
account: PickledAccount,
@@ -76,34 +58,13 @@ pub struct MigrationData {
cross_signing: CrossSigningKeyExport,
/// The list of users that the Rust SDK should track.
tracked_users: Vec<String>,
/// Map of room settings
room_settings: HashMap<String, RoomSettings>,
}
/// Struct collecting data that is important to migrate sessions to the rust-sdk
#[derive(uniffi::Record)]
pub struct SessionMigrationData {
/// The user id that the data belongs to.
user_id: String,
/// The device id that the data belongs to.
device_id: String,
/// The Curve25519 public key of the Account that owns this data.
curve25519_key: String,
/// The Ed25519 public key of the Account that owns this data.
ed25519_key: String,
/// The list of pickleds Olm Sessions.
sessions: Vec<PickledSession>,
/// The list of pickled Megolm inbound group sessions.
inbound_group_sessions: Vec<PickledInboundGroupSession>,
/// The Olm pickle key that was used to pickle all the Olm objects.
pickle_key: Vec<u8>,
}
/// A pickled version of an `Account`.
///
/// Holds all the information that needs to be stored in a database to restore
/// an account.
#[derive(Debug, Deserialize, Serialize, uniffi::Record)]
#[derive(Debug, Deserialize, Serialize)]
pub struct PickledAccount {
/// The user id of the account owner.
pub user_id: String,
@@ -121,7 +82,7 @@ pub struct PickledAccount {
///
/// Holds all the information that needs to be stored in a database to restore
/// a Session.
#[derive(Debug, Deserialize, Serialize, uniffi::Record)]
#[derive(Debug, Deserialize, Serialize)]
pub struct PickledSession {
/// The pickle string holding the Olm Session.
pub pickle: String,
@@ -139,7 +100,7 @@ pub struct PickledSession {
///
/// Holds all the information that needs to be stored in a database to restore
/// an InboundGroupSession.
#[derive(Debug, Deserialize, Serialize, uniffi::Record)]
#[derive(Debug, Deserialize, Serialize)]
pub struct PickledInboundGroupSession {
/// The pickle string holding the InboundGroupSession.
pub pickle: String,
@@ -160,7 +121,7 @@ pub struct PickledInboundGroupSession {
}
/// Error type for the migration process.
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[derive(thiserror::Error, Debug)]
pub enum MigrationError {
/// Generic catch all error variant.
#[error("error migrating database: {error_message}")]
@@ -176,43 +137,38 @@ impl From<anyhow::Error> for MigrationError {
}
}
/// Migrate a libolm based setup to a vodozemac based setup stored in a SQLite
/// Migrate a libolm based setup to a vodozemac based setup stored in a Sled
/// store.
///
/// # Arguments
///
/// * `data` - The data that should be migrated over to the SQLite store.
/// * `data` - The data that should be migrated over to the Sled store.
///
/// * `path` - The path where the SQLite store should be created.
/// * `path` - The path where the Sled 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
/// rest in the Sled 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.
#[uniffi::export]
pub fn migrate(
data: MigrationData,
path: String,
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> Result<(), MigrationError> {
let runtime = Runtime::new().context("initializing tokio runtime")?;
runtime.block_on(async move {
migrate_data(data, &path, passphrase, progress_listener).await?;
Ok(())
})
}
async fn migrate_data(
mut data: MigrationData,
path: &str,
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::BackupDecryptionKey};
use vodozemac::olm::Account;
use matrix_sdk_crypto::{
olm::PrivateCrossSigningIdentity,
store::{Changes as RustChanges, CryptoStore, RecoveryKey},
};
use matrix_sdk_sled::SledCryptoStore;
use tokio::runtime::Runtime;
use vodozemac::{
megolm::InboundGroupSession,
olm::{Account, Session},
Curve25519PublicKey,
};
use zeroize::Zeroize;
// The total steps here include all the sessions/inbound group sessions and
@@ -229,199 +185,41 @@ async fn migrate_data(
progress_listener.on_progress(progress as i32, total as i32)
};
let store = SqliteCryptoStore::open(path, passphrase.as_deref()).await?;
let store = SledCryptoStore::open_with_passphrase(path, passphrase.as_deref())?;
let runtime = Runtime::new()?;
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,
shared: data.account.shared,
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
creation_local_time: MilliSecondsSinceUnixEpoch(UInt::default()),
};
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);
let (sessions, inbound_group_sessions) = collect_sessions(
processed_steps,
total_steps,
&listener,
&data.pickle_key,
user_id.clone(),
device_id,
identity_keys,
data.sessions,
data.inbound_group_sessions,
)?;
let backup_decryption_key = data
.backup_recovery_key
.map(|k| BackupDecryptionKey::from_base58(k.as_str()))
.transpose()?;
let cross_signing = PrivateCrossSigningIdentity::empty((*user_id).into());
cross_signing
.import_secrets_unchecked(
data.cross_signing.master_key.as_deref(),
data.cross_signing.self_signing_key.as_deref(),
data.cross_signing.user_signing_key.as_deref(),
)
.await?;
data.cross_signing.master_key.zeroize();
data.cross_signing.self_signing_key.zeroize();
data.cross_signing.user_signing_key.zeroize();
processed_steps += 1;
listener(processed_steps, total_steps);
let tracked_users: Vec<_> = data
.tracked_users
.into_iter()
.filter_map(|s| parse_user_id(&s).ok().map(|u| (u, true)))
.collect();
let tracked_users: Vec<_> = tracked_users.iter().map(|(u, d)| (&**u, *d)).collect();
store.save_tracked_users(tracked_users.as_slice()).await?;
processed_steps += 1;
listener(processed_steps, total_steps);
let mut room_settings = HashMap::new();
for (room_id, settings) in data.room_settings {
let room_id = RoomId::parse(room_id)?;
room_settings.insert(room_id, settings.into());
}
store.save_pending_changes(PendingChanges { account: Some(account) }).await?;
let changes = Changes {
private_identity: Some(cross_signing),
sessions,
inbound_group_sessions,
backup_decryption_key,
backup_version: data.backup_version,
room_settings,
..Default::default()
};
save_changes(processed_steps, total_steps, &listener, changes, &store).await
}
async fn save_changes(
mut processed_steps: usize,
total_steps: usize,
listener: &dyn Fn(usize, usize),
changes: Changes,
store: &SqliteCryptoStore,
) -> anyhow::Result<()> {
store.save_changes(changes).await?;
processed_steps += 1;
listener(processed_steps, total_steps);
Ok(())
}
/// Migrate sessions and group sessions of a libolm based setup to a vodozemac
/// based setup stored in a SQLite store.
///
/// This method allows you to migrate a subset of the data, it should only be
/// used after the [`migrate()`] method has been already used.
///
/// # Arguments
///
/// * `data` - The data that should be migrated over to the SQLite store.
///
/// * `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.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
pub fn migrate_sessions(
data: SessionMigrationData,
path: String,
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> Result<(), MigrationError> {
let runtime = Runtime::new().context("initializing tokio runtime")?;
runtime.block_on(migrate_session_data(data, &path, passphrase, progress_listener))?;
Ok(())
}
async fn migrate_session_data(
data: SessionMigrationData,
path: &str,
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
let store = SqliteCryptoStore::open(path, passphrase.as_deref()).await?;
let listener = |progress: usize, total: usize| {
progress_listener.on_progress(progress as i32, total as i32)
};
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 device_id: OwnedDeviceId = data.device_id.into();
let identity_keys = IdentityKeys {
ed25519: Ed25519PublicKey::from_base64(&data.ed25519_key)?,
curve25519: Curve25519PublicKey::from_base64(&data.curve25519_key)?,
}
.into();
let (sessions, inbound_group_sessions) = collect_sessions(
processed_steps,
total_steps,
&listener,
&data.pickle_key,
user_id,
device_id,
identity_keys,
data.sessions,
data.inbound_group_sessions,
)?;
let changes = Changes { sessions, inbound_group_sessions, ..Default::default() };
save_changes(processed_steps, total_steps, &listener, changes, &store).await
}
#[allow(clippy::too_many_arguments)]
fn collect_sessions(
mut processed_steps: usize,
total_steps: usize,
listener: &dyn Fn(usize, usize),
pickle_key: &[u8],
user_id: OwnedUserId,
device_id: OwnedDeviceId,
identity_keys: Arc<IdentityKeys>,
session_pickles: Vec<PickledSession>,
group_session_pickles: Vec<PickledInboundGroupSession>,
) -> anyhow::Result<(Vec<Session>, Vec<InboundGroupSession>)> {
let mut sessions = Vec::new();
for session_pickle in session_pickles {
for session_pickle in data.sessions {
let pickle =
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
.pickle();
Session::from_libolm_pickle(&session_pickle.pickle, &data.pickle_key)?.pickle();
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
@@ -434,8 +232,12 @@ fn collect_sessions(
last_use_time,
};
let session =
Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle);
let session = matrix_sdk_crypto::olm::Session::from_pickle(
user_id.clone(),
device_id.clone(),
identity_keys.clone(),
pickle,
);
sessions.push(session);
processed_steps += 1;
@@ -444,12 +246,9 @@ fn collect_sessions(
let mut inbound_group_sessions = Vec::new();
for session in group_session_pickles {
let pickle = vodozemac::megolm::InboundGroupSession::from_libolm_pickle(
&session.pickle,
pickle_key,
)?
.pickle();
for session in data.inbound_group_sessions {
let pickle =
InboundGroupSession::from_libolm_pickle(&session.pickle, &data.pickle_key)?.pickle();
let sender_key = Curve25519PublicKey::from_base64(&session.sender_key)?;
@@ -460,7 +259,7 @@ 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))
@@ -470,7 +269,7 @@ fn collect_sessions(
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
};
let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?;
@@ -480,53 +279,54 @@ fn collect_sessions(
listener(processed_steps, total_steps);
}
Ok((sessions, inbound_group_sessions))
}
let recovery_key =
data.backup_recovery_key.map(|k| RecoveryKey::from_base58(k.as_str())).transpose()?;
/// Migrate room settings, including room algorithm and whether to block
/// untrusted devices from legacy store to Sqlite store.
///
/// Note that this method should only be used if a client has already migrated
/// account data via [migrate](#method.migrate) method, which did not include
/// room settings. For a brand new migration, the [migrate](#method.migrate)
/// method will take care of room settings automatically, if provided.
///
/// # Arguments
///
/// * `room_settings` - Map of room settings
///
/// * `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.
#[uniffi::export]
pub fn migrate_room_settings(
room_settings: HashMap<String, RoomSettings>,
path: String,
passphrase: Option<String>,
) -> Result<(), MigrationError> {
let runtime = Runtime::new().context("initializing tokio runtime")?;
runtime.block_on(async move {
let store = SqliteCryptoStore::open(path, passphrase.as_deref())
.await
.context("opening sqlite crypto store")?;
let cross_signing = PrivateCrossSigningIdentity::empty((*user_id).into());
runtime.block_on(cross_signing.import_secrets_unchecked(
data.cross_signing.master_key.as_deref(),
data.cross_signing.self_signing_key.as_deref(),
data.cross_signing.user_signing_key.as_deref(),
))?;
let mut rust_settings = HashMap::new();
for (room_id, settings) in room_settings {
let room_id = RoomId::parse(room_id).context("parsing room ID")?;
rust_settings.insert(room_id, settings.into());
}
data.cross_signing.master_key.zeroize();
data.cross_signing.self_signing_key.zeroize();
data.cross_signing.user_signing_key.zeroize();
let changes = Changes { room_settings: rust_settings, ..Default::default() };
store.save_changes(changes).await.context("saving changes")?;
processed_steps += 1;
listener(processed_steps, total_steps);
Ok(())
})
let tracked_users: Vec<_> = data
.tracked_users
.into_iter()
.map(|u| Ok(((parse_user_id(&u)?), true)))
.collect::<anyhow::Result<_>>()?;
let tracked_users: Vec<_> = tracked_users.iter().map(|(u, d)| (&**u, *d)).collect();
runtime.block_on(store.save_tracked_users(tracked_users.as_slice()))?;
processed_steps += 1;
listener(processed_steps, total_steps);
let changes = RustChanges {
account: Some(account),
private_identity: Some(cross_signing),
sessions,
inbound_group_sessions,
recovery_key,
backup_version: data.backup_version,
..Default::default()
};
runtime.block_on(store.save_changes(changes))?;
processed_steps += 1;
listener(processed_steps, total_steps);
Ok(())
}
/// Callback that will be passed over the FFI to report progress
#[uniffi::export(callback_interface)]
pub trait ProgressListener {
/// The callback that should be called on the Rust side
///
@@ -544,114 +344,7 @@ impl<T: Fn(i32, i32)> ProgressListener for T {
}
}
/// An encryption algorithm to be used to encrypt messages sent to a room.
#[derive(Debug, Deserialize, Serialize, PartialEq, uniffi::Enum)]
pub enum EventEncryptionAlgorithm {
/// Olm version 1 using Curve25519, AES-256, and SHA-256.
OlmV1Curve25519AesSha2,
/// Megolm version 1 using AES-256 and SHA-256.
MegolmV1AesSha2,
}
impl From<EventEncryptionAlgorithm> for RustEventEncryptionAlgorithm {
fn from(a: EventEncryptionAlgorithm) -> Self {
match a {
EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
EventEncryptionAlgorithm::MegolmV1AesSha2 => Self::MegolmV1AesSha2,
}
}
}
impl TryFrom<RustEventEncryptionAlgorithm> for EventEncryptionAlgorithm {
type Error = serde_json::Error;
fn try_from(value: RustEventEncryptionAlgorithm) -> Result<Self, Self::Error> {
match value {
RustEventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => {
Ok(Self::OlmV1Curve25519AesSha2)
}
RustEventEncryptionAlgorithm::MegolmV1AesSha2 => Ok(Self::MegolmV1AesSha2),
_ => Err(serde::de::Error::custom(format!("Unsupported algorithm {value}"))),
}
}
}
/// Who can see a room's history.
#[derive(uniffi::Enum)]
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 RustHistoryVisibility {
fn from(h: HistoryVisibility) -> Self {
match h {
HistoryVisibility::Invited => Self::Invited,
HistoryVisibility::Joined => Self::Joined,
HistoryVisibility::Shared => Self::Shared,
HistoryVisibility::WorldReadable => Self::Shared,
}
}
}
/// Settings that should be used when a room key is shared.
///
/// These settings control which algorithm the room key should use, how long a
/// room key should be used and some other important information that determines
/// the lifetime of a room key.
#[derive(uniffi::Record)]
pub struct EncryptionSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
/// How long can the room key be used before it should be rotated. Time in
/// seconds.
pub rotation_period: u64,
/// How many messages should be sent before the room key should be rotated.
pub rotation_period_msgs: u64,
/// The current history visibility of the room. The visibility will be
/// tracked by the room key and the key will be rotated if the visibility
/// changes.
pub history_visibility: HistoryVisibility,
/// Should untrusted devices receive the room key, or should they be
/// excluded from the conversation.
pub only_allow_trusted_devices: bool,
}
impl From<EncryptionSettings> for RustEncryptionSettings {
fn from(v: EncryptionSettings) -> Self {
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(),
only_allow_trusted_devices: v.only_allow_trusted_devices,
}
}
}
/// An event that was successfully decrypted.
#[derive(uniffi::Record)]
pub struct DecryptedEvent {
/// The decrypted version of the event.
pub clear_event: String,
@@ -663,53 +356,11 @@ pub struct DecryptedEvent {
/// key to us. Is empty if the key came directly from the sender of the
/// event.
pub forwarding_curve25519_chain: Vec<String>,
/// The shield state (color and message to display to user) for the event,
/// representing the event's authenticity. Computed from the properties of
/// the sender user identity and their Olm device.
///
/// Note that this is computed at time of decryption, so the value reflects
/// the computed event authenticity at that time. Authenticity-related
/// properties can change later on, such as when a user identity is
/// subsequently verified or a device is deleted.
pub shield_state: ShieldState,
}
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
/// for more info.
#[allow(missing_docs)]
#[derive(uniffi::Enum)]
pub enum ShieldColor {
Red,
Grey,
None,
}
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
/// for more info.
#[derive(uniffi::Record)]
#[allow(missing_docs)]
pub struct ShieldState {
color: ShieldColor,
message: Option<String>,
}
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 },
}
}
}
/// Struct representing the state of our private cross signing keys, it shows
/// which private cross signing keys we have locally stored.
#[derive(Debug, Clone, uniffi::Record)]
#[derive(Debug, Clone)]
pub struct CrossSigningStatus {
/// Do we have the master key.
pub has_master: bool,
@@ -723,7 +374,7 @@ pub struct CrossSigningStatus {
/// A struct containing private cross signing keys that can be backed up or
/// uploaded to the secret store.
#[derive(Deserialize, Serialize, uniffi::Record)]
#[derive(Deserialize, Serialize)]
pub struct CrossSigningKeyExport {
/// The seed of the master key encoded as unpadded base64.
pub master_key: Option<String>,
@@ -734,7 +385,6 @@ pub struct CrossSigningKeyExport {
}
/// Struct holding the number of room keys we have.
#[derive(uniffi::Record)]
pub struct RoomKeyCounts {
/// The total number of room keys.
pub total: i64,
@@ -743,7 +393,6 @@ pub struct RoomKeyCounts {
}
/// Backup keys and information we load from the store.
#[derive(uniffi::Object)]
pub struct BackupKeys {
/// The recovery key as a base64 encoded string.
recovery_key: Arc<BackupRecoveryKey>,
@@ -751,7 +400,6 @@ pub struct BackupKeys {
backup_version: String,
}
#[uniffi::export]
impl BackupKeys {
/// Get the recovery key that we're holding on to.
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
@@ -770,7 +418,7 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
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(),
@@ -815,82 +463,26 @@ impl From<matrix_sdk_crypto::CrossSigningStatus> for CrossSigningStatus {
}
}
/// Room encryption settings which are modified by state events or user options
#[derive(Debug, PartialEq, Deserialize, Serialize, uniffi::Record)]
pub struct RoomSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
/// Should untrusted devices receive the room key, or should they be
/// excluded from the conversation.
pub only_allow_trusted_devices: bool,
}
impl TryFrom<RustRoomSettings> for RoomSettings {
type Error = serde_json::Error;
fn try_from(value: RustRoomSettings) -> Result<Self, Self::Error> {
let algorithm = value.algorithm.try_into()?;
Ok(Self { algorithm, only_allow_trusted_devices: value.only_allow_trusted_devices })
}
}
impl From<RoomSettings> for RustRoomSettings {
fn from(value: RoomSettings) -> Self {
Self {
algorithm: value.algorithm.into(),
only_allow_trusted_devices: value.only_allow_trusted_devices,
}
}
}
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))
}
#[uniffi::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(),
}
#[allow(warnings)]
mod generated {
use super::*;
include!(concat!(env!("OUT_DIR"), "/olm.uniffi.rs"));
}
/// 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,
}
#[uniffi::export]
fn version() -> String {
matrix_sdk_crypto::VERSION.to_owned()
}
#[uniffi::export]
fn vodozemac_version() -> String {
vodozemac::VERSION.to_owned()
}
uniffi::include_scaffolding!("olm");
pub use generated::*;
#[cfg(test)]
mod tests {
mod test {
use anyhow::Result;
use serde_json::{json, Value};
use tempfile::tempdir;
use super::MigrationData;
use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings};
use crate::{migrate, OlmMachine};
#[test]
fn android_migration() -> Result<()> {
@@ -972,38 +564,19 @@ mod tests {
"@ganfra146:matrix.org",
"@this-is-me:matrix.org",
"@Amandine:matrix.org",
"@ganfra:matrix.org",
"NotAUser%ID"
],
"room_settings": {
"!AZkqtjvtwPAuyNOXEt:matrix.org": {
"algorithm": "OlmV1Curve25519AesSha2",
"only_allow_trusted_devices": true
},
"!CWLUCoEWXSFyTCOtfL:matrix.org": {
"algorithm": "MegolmV1AesSha2",
"only_allow_trusted_devices": false
},
}
"@ganfra:matrix.org"
]
});
let migration_data: MigrationData = serde_json::from_value(data)?;
let dir = tempdir()?;
let path = dir
.path()
.to_str()
.expect("Creating a string from the tempdir path should not fail")
.to_owned();
let path =
dir.path().to_str().expect("Creating a string from the tempdir path should not fail");
migrate(migration_data, path.clone(), None, Box::new(|_, _| {}))?;
migrate(migration_data, path, 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"],
@@ -1021,32 +594,6 @@ mod tests {
let backup_keys = machine.get_backup_keys()?;
assert!(backup_keys.is_some());
let settings1 = machine.get_room_settings("!AZkqtjvtwPAuyNOXEt:matrix.org".into())?;
assert_eq!(
Some(RoomSettings {
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
only_allow_trusted_devices: true
}),
settings1
);
let settings2 = machine.get_room_settings("!CWLUCoEWXSFyTCOtfL:matrix.org".into())?;
assert_eq!(
Some(RoomSettings {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
only_allow_trusted_devices: false
}),
settings2
);
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(())
}
}
+3 -10
View File
@@ -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.
#[uniffi::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,22 +41,16 @@ pub struct LoggerWrapper {
}
/// Set the logger that should be used to forward Rust logs over FFI.
#[uniffi::export]
pub fn set_logger(logger: Box<dyn Logger>) {
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
let filter = EnvFilter::from_default_env()
.add_directive(
"matrix_sdk_crypto=trace".parse().expect("Can't parse logging filter directive"),
)
.add_directive(
"matrix_sdk_sqlite=debug".parse().expect("Can't parse logging filter directive"),
);
let filter = EnvFilter::from_default_env().add_directive(
"matrix_sdk_crypto=trace".parse().expect("Can't parse logging filter directive"),
);
let _ = tracing_subscriber::fmt()
.with_writer(logger)
.with_env_filter(filter)
.with_ansi(false)
.without_time()
.try_init();
}
File diff suppressed because it is too large Load Diff
+480 -12
View File
@@ -1,15 +1,483 @@
namespace matrix_sdk_crypto_ffi {};
enum LocalTrust {
"Verified",
"BlackListed",
"Ignored",
"Unset",
namespace olm {
void set_logger(Logger logger);
[Throws=MigrationError]
void migrate(
MigrationData data,
[ByRef] string path,
string? passphrase,
ProgressListener progress_listener
);
};
enum SignatureState {
"Missing",
"Invalid",
"ValidButNotTrusted",
"ValidAndTrusted",
[Error]
interface MigrationError {
Generic(string error_message);
};
callback interface Logger {
void log(string logLine);
};
callback interface ProgressListener {
void on_progress(i32 progress, i32 total);
};
[Error]
enum PkDecryptionError {
"Olm",
};
[Error]
enum KeyImportError {
"Export",
"CryptoStore",
"Json",
};
[Error]
enum SignatureError {
"Signature",
"Identifier",
"CryptoStore",
"UnknownDevice",
"UnknownUserIdentity",
};
[Error]
enum SecretImportError {
"Import",
"CryptoStore",
};
[Error]
enum CryptoStoreError {
"CryptoStore",
"OlmError",
"Serialization",
"Identifier",
"InvalidUserId",
};
[Error]
enum DecryptionError {
"Identifier",
"Serialization",
"Megolm",
};
dictionary DeviceLists {
sequence<string> changed;
sequence<string> left;
};
dictionary KeysImportResult {
i64 imported;
i64 total;
record<DOMString, record<DOMString, sequence<string>>> keys;
};
dictionary DecryptedEvent {
string clear_event;
string sender_curve25519_key;
string? claimed_ed25519_key;
sequence<string> forwarding_curve25519_chain;
};
dictionary Device {
string user_id;
string device_id;
record<DOMString, string> keys;
sequence<string> algorithms;
string? display_name;
boolean is_blocked;
boolean locally_trusted;
boolean cross_signing_trusted;
};
[Enum]
interface UserIdentity {
Own(
string user_id,
boolean trusts_our_own_device,
string master_key,
string self_signing_key,
string user_signing_key
);
Other(
string user_id,
string master_key,
string self_signing_key
);
};
dictionary CrossSigningStatus {
boolean has_master;
boolean has_self_signing;
boolean has_user_signing;
};
dictionary CrossSigningKeyExport {
string? master_key;
string? self_signing_key;
string? user_signing_key;
};
dictionary UploadSigningKeysRequest {
string master_key;
string self_signing_key;
string user_signing_key;
};
dictionary BootstrapCrossSigningResult {
UploadSigningKeysRequest upload_signing_keys_request;
SignatureUploadRequest signature_request;
};
dictionary CancelInfo {
string cancel_code;
string reason;
boolean cancelled_by_us;
};
dictionary StartSasResult {
Sas sas;
OutgoingVerificationRequest request;
};
dictionary Sas {
string other_user_id;
string other_device_id;
string flow_id;
string? room_id;
boolean we_started;
boolean has_been_accepted;
boolean can_be_presented;
boolean supports_emoji;
boolean have_we_confirmed;
boolean is_done;
boolean is_cancelled;
CancelInfo? cancel_info;
};
dictionary ScanResult {
QrCode qr;
OutgoingVerificationRequest request;
};
dictionary QrCode {
string other_user_id;
string other_device_id;
string flow_id;
string? room_id;
boolean we_started;
boolean other_side_scanned;
boolean has_been_confirmed;
boolean reciprocated;
boolean is_done;
boolean is_cancelled;
CancelInfo? cancel_info;
};
dictionary VerificationRequest {
string other_user_id;
string? other_device_id;
string flow_id;
string? room_id;
boolean we_started;
boolean is_ready;
boolean is_passive;
boolean is_done;
boolean is_cancelled;
CancelInfo? cancel_info;
sequence<string>? their_methods;
sequence<string>? our_methods;
};
dictionary RequestVerificationResult {
VerificationRequest verification;
OutgoingVerificationRequest request;
};
dictionary ConfirmVerificationResult {
sequence<OutgoingVerificationRequest> requests;
SignatureUploadRequest? signature_request;
};
[Enum]
interface Verification {
SasV1(Sas sas);
QrCodeV1(QrCode qrcode);
};
dictionary KeyRequestPair {
Request? cancellation;
Request key_request;
};
[Enum]
interface OutgoingVerificationRequest {
ToDevice(string request_id, string event_type, string body);
InRoom(string request_id, string room_id, string event_type, string content);
};
[Enum]
interface Request {
ToDevice(string request_id, string event_type, string body);
KeysUpload(string request_id, string body);
KeysQuery(string request_id, sequence<string> users);
KeysClaim(string request_id, record<DOMString, record<DOMString, string>> one_time_keys);
KeysBackup(string request_id, string version, string rooms);
RoomMessage(string request_id, string room_id, string event_type, string content);
SignatureUpload(string request_id, string body);
};
dictionary SignatureUploadRequest {
string body;
};
enum RequestType {
"KeysQuery",
"KeysClaim",
"KeysUpload",
"ToDevice",
"SignatureUpload",
"KeysBackup",
"RoomMessage",
};
interface OlmMachine {
[Throws=CryptoStoreError]
constructor(
[ByRef] string user_id,
[ByRef] string device_id,
[ByRef] string path,
string? passphrase
);
record<DOMString, string> identity_keys();
string user_id();
string device_id();
[Throws=CryptoStoreError]
string receive_sync_changes([ByRef] string events,
DeviceLists device_changes,
record<DOMString, i32> key_counts,
sequence<string>? unused_fallback_keys);
[Throws=CryptoStoreError]
sequence<Request> outgoing_requests();
[Throws=CryptoStoreError]
void mark_request_as_sent(
[ByRef] string request_id,
RequestType request_type,
[ByRef] string response
);
[Throws=DecryptionError]
DecryptedEvent decrypt_room_event([ByRef] string event, [ByRef] string room_id);
[Throws=CryptoStoreError]
string encrypt([ByRef] string room_id, [ByRef] string event_type, [ByRef] string content);
[Throws=CryptoStoreError]
UserIdentity? get_identity([ByRef] string user_id, u32 timeout);
[Throws=SignatureError]
SignatureUploadRequest verify_identity([ByRef] string user_id);
[Throws=CryptoStoreError]
Device? get_device([ByRef] string user_id, [ByRef] string device_id, u32 timeout);
[Throws=CryptoStoreError]
void mark_device_as_trusted([ByRef] string user_id, [ByRef] string device_id);
[Throws=SignatureError]
SignatureUploadRequest verify_device([ByRef] string user_id, [ByRef] string device_id);
[Throws=CryptoStoreError]
sequence<Device> get_user_devices([ByRef] string user_id, u32 timeout);
[Throws=CryptoStoreError]
boolean is_user_tracked([ByRef] string user_id);
void update_tracked_users(sequence<string> users);
[Throws=CryptoStoreError]
Request? get_missing_sessions(sequence<string> users);
[Throws=CryptoStoreError]
sequence<Request> share_room_key([ByRef] string room_id, sequence<string> users);
[Throws=CryptoStoreError]
void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id);
sequence<VerificationRequest> get_verification_requests([ByRef] string user_id);
VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id);
Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id);
[Throws=CryptoStoreError]
VerificationRequest? request_verification(
[ByRef] string user_id,
[ByRef] string room_id,
[ByRef] string event_id,
sequence<string> methods
);
[Throws=CryptoStoreError]
string? verification_request_content(
[ByRef] string user_id,
sequence<string> methods
);
[Throws=CryptoStoreError]
RequestVerificationResult? request_self_verification(sequence<string> methods);
[Throws=CryptoStoreError]
RequestVerificationResult? request_verification_with_device(
[ByRef] string user_id,
[ByRef] string device_id,
sequence<string> methods
);
OutgoingVerificationRequest? accept_verification_request(
[ByRef] string user_id,
[ByRef] string flow_id,
sequence<string> methods
);
[Throws=CryptoStoreError]
ConfirmVerificationResult? confirm_verification([ByRef] string user_id, [ByRef] string flow_id);
OutgoingVerificationRequest? cancel_verification(
[ByRef] string user_id,
[ByRef] string flow_id,
[ByRef] string cancel_code
);
[Throws=CryptoStoreError]
StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id);
[Throws=CryptoStoreError]
StartSasResult? start_sas_verification([ByRef] string user_id, [ByRef] string flow_id);
OutgoingVerificationRequest? accept_sas_verification([ByRef] string user_id, [ByRef] string flow_id);
sequence<i32>? get_emoji_index([ByRef] string user_id, [ByRef] string flow_id);
sequence<i32>? get_decimals([ByRef] string user_id, [ByRef] string flow_id);
[Throws=CryptoStoreError]
QrCode? start_qr_verification([ByRef] string user_id, [ByRef] string flow_id);
ScanResult? scan_qr_code([ByRef] string user_id, [ByRef] string flow_id, [ByRef] string data);
string? generate_qr_code([ByRef] string user_id, [ByRef] string flow_id);
[Throws=DecryptionError]
KeyRequestPair request_room_key([ByRef] string event, [ByRef] string room_id);
[Throws=CryptoStoreError]
string export_room_keys([ByRef] string passphrase, i32 rounds);
[Throws=KeyImportError]
KeysImportResult import_room_keys(
[ByRef] string keys,
[ByRef] string passphrase,
ProgressListener progress_listener
);
[Throws=KeyImportError]
KeysImportResult import_decrypted_room_keys(
[ByRef] string keys,
ProgressListener progress_listener
);
[Throws=CryptoStoreError]
void discard_room_key([ByRef] string room_id);
CrossSigningStatus cross_signing_status();
[Throws=CryptoStoreError]
BootstrapCrossSigningResult bootstrap_cross_signing();
CrossSigningKeyExport? export_cross_signing_keys();
[Throws=SecretImportError]
void import_cross_signing_keys(CrossSigningKeyExport export);
[Throws=CryptoStoreError]
boolean is_identity_verified([ByRef] string user_id);
record<DOMString, record<DOMString, string>> sign([ByRef] string message);
[Throws=DecodeError]
void enable_backup_v1(MegolmV1BackupKey key, string version);
[Throws=CryptoStoreError]
void disable_backup();
[Throws=CryptoStoreError]
Request? backup_room_keys();
[Throws=CryptoStoreError]
void save_recovery_key(BackupRecoveryKey? key, string? version);
[Throws=CryptoStoreError]
RoomKeyCounts room_key_counts();
[Throws=CryptoStoreError]
BackupKeys? get_backup_keys();
boolean backup_enabled();
[Throws=CryptoStoreError]
boolean verify_backup([ByRef] string auth_data);
};
dictionary PassphraseInfo {
string private_key_salt;
i32 private_key_iterations;
};
dictionary MegolmV1BackupKey {
string public_key;
record<DOMString, record<DOMString, string>> signatures;
PassphraseInfo? passphrase_info;
string backup_algorithm;
};
interface BackupKeys {
BackupRecoveryKey recovery_key();
string backup_version();
};
dictionary RoomKeyCounts {
i64 total;
i64 backed_up;
};
[Error]
enum DecodeError {
"Decode",
"CryptoStore",
};
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);
string to_base58();
string to_base64();
MegolmV1BackupKey megolm_v1_public_key();
[Throws=PkDecryptionError]
string decrypt_v1(string ephemeral_key, string mac, string ciphertext);
};
dictionary MigrationData {
PickledAccount account;
sequence<PickledSession> sessions;
sequence<PickledInboundGroupSession> inbound_group_sessions;
string? backup_version;
string? backup_recovery_key;
sequence<u8> pickle_key;
CrossSigningKeyExport cross_signing;
sequence<string> tracked_users;
};
dictionary PickledAccount {
string user_id;
string device_id;
string pickle;
boolean shared;
i64 uploaded_signed_key_count;
};
dictionary PickledSession {
string pickle;
string sender_key;
boolean created_using_fallback_key;
string creation_time;
string last_use_time;
};
dictionary PickledInboundGroupSession {
string pickle;
string sender_key;
record<DOMString, string> signing_key;
string room_id;
sequence<string> forwarding_chains;
boolean imported;
boolean backed_up;
};
+19 -41
View File
@@ -4,9 +4,8 @@ use std::collections::HashMap;
use http::Response;
use matrix_sdk_crypto::{
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
IncomingResponse, OutgoingRequest, OutgoingVerificationRequest as SdkVerificationRequest,
RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest as RustUploadSigningKeysRequest,
};
use ruma::{
api::client::{
@@ -20,7 +19,7 @@ use ruma::{
},
},
message::send_message_event::v3::Response as RoomMessageResponse,
sync::sync_events::DeviceLists as RumaDeviceLists,
sync::sync_events::v3::DeviceLists as RumaDeviceLists,
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
assign,
@@ -29,7 +28,6 @@ use ruma::{
};
use serde_json::json;
#[derive(uniffi::Record)]
pub struct SignatureUploadRequest {
pub body: String,
}
@@ -43,7 +41,6 @@ impl From<RustSignatureUploadRequest> for SignatureUploadRequest {
}
}
#[derive(uniffi::Record)]
pub struct UploadSigningKeysRequest {
pub master_key: String,
pub self_signing_key: String,
@@ -69,30 +66,22 @@ 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(),
}
}
}
#[derive(uniffi::Enum)]
pub enum OutgoingVerificationRequest {
ToDevice { request_id: String, event_type: String, body: String },
InRoom { request_id: String, room_id: String, event_type: String, content: String },
@@ -123,15 +112,15 @@ impl From<ToDeviceRequest> for OutgoingVerificationRequest {
}
}
#[derive(Debug, uniffi::Enum)]
#[derive(Debug)]
pub enum Request {
ToDevice { request_id: String, event_type: String, body: String },
KeysUpload { request_id: String, body: String },
KeysQuery { request_id: String, users: Vec<String> },
KeysClaim { request_id: String, one_time_keys: HashMap<String, HashMap<String, String>> },
KeysBackup { request_id: String, version: String, rooms: String },
RoomMessage { request_id: String, room_id: String, event_type: String, content: String },
SignatureUpload { request_id: String, body: String },
KeysBackup { request_id: String, version: String, rooms: String },
}
impl From<OutgoingRequest> for Request {
@@ -149,7 +138,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) => {
@@ -164,7 +153,12 @@ impl From<OutgoingRequest> for Request {
},
RoomMessage(r) => Request::from(r),
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
KeysBackup(b) => (r.request_id().to_owned(), b.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"),
},
}
}
}
@@ -199,19 +193,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 {
@@ -240,7 +221,6 @@ pub(crate) fn response_from_string(body: &str) -> Response<Vec<u8>> {
.expect("Can't create HTTP response")
}
#[derive(uniffi::Enum)]
pub enum RequestType {
KeysQuery,
KeysClaim,
@@ -251,7 +231,6 @@ pub enum RequestType {
RoomMessage,
}
#[derive(uniffi::Record)]
pub struct DeviceLists {
pub changed: Vec<String>,
pub left: Vec<String>,
@@ -274,7 +253,6 @@ impl From<DeviceLists> for RumaDeviceLists {
}
}
#[derive(uniffi::Record)]
pub struct KeysImportResult {
/// The number of room keys that were imported.
pub imported: i64,
@@ -4,7 +4,6 @@ use crate::CryptoStoreError;
/// Enum representing cross signing identities of our own user or some other
/// user.
#[derive(uniffi::Enum)]
pub enum UserIdentity {
/// Our own user identity.
Own {
+150 -721
View File
@@ -1,461 +1,105 @@
use std::sync::Arc;
use futures_util::{Stream, StreamExt};
use matrix_sdk_crypto::{
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
Verification as InnerVerification, VerificationRequest as InnerVerificationRequest,
VerificationRequestState as RustVerificationRequestState,
CancelInfo as RustCancelInfo, QrVerification as InnerQr, Sas as InnerSas,
VerificationRequest as InnerVerificationRequest,
};
use ruma::events::key::verification::VerificationMethod;
use tokio::runtime::Handle;
use vodozemac::{base64_decode, base64_encode};
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
/// Listener that will be passed over the FFI to report changes to a SAS
/// verification.
#[uniffi::export(callback_interface)]
pub trait SasListener: Send {
/// The callback that should be called on the Rust side
///
/// # Arguments
///
/// * `state` - The current state of the SAS verification.
fn on_change(&self, state: SasState);
}
/// An Enum describing the state the SAS verification is in.
#[derive(uniffi::Enum)]
pub enum SasState {
/// 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.
Accepted,
/// The public keys have been exchanged and the short auth string can be
/// presented to the user.
KeysExchanged {
/// The emojis that represent the short auth string, will be `None` if
/// the emoji SAS method wasn't one of accepted protocols.
emojis: Option<Vec<i32>>,
/// The list of decimals that represent the short auth string.
decimals: Vec<i32>,
},
/// The verification process has been confirmed from our side, we're waiting
/// for the other side to confirm as well.
Confirmed,
/// The verification process has been successfully concluded.
Done,
/// The verification process has been cancelled.
Cancelled {
/// Information about the reason of the cancellation.
cancel_info: CancelInfo,
},
}
impl From<RustSasState> for SasState {
fn from(s: RustSasState) -> Self {
match s {
RustSasState::Started { .. } => Self::Started,
RustSasState::Accepted { .. } => Self::Accepted,
RustSasState::KeysExchanged { emojis, decimals } => Self::KeysExchanged {
emojis: emojis.map(|e| e.indices.map(|i| i as i32).to_vec()),
decimals: [decimals.0.into(), decimals.1.into(), decimals.2.into()].to_vec(),
},
RustSasState::Confirmed => Self::Confirmed,
RustSasState::Done { .. } => Self::Done,
RustSasState::Cancelled(c) => Self::Cancelled { cancel_info: c.into() },
}
}
}
use crate::{OutgoingVerificationRequest, SignatureUploadRequest};
/// Enum representing the different verification flows we support.
#[derive(uniffi::Object)]
pub struct Verification {
pub(crate) inner: InnerVerification,
pub(crate) runtime: Handle,
}
#[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.to_owned(), runtime: self.runtime.to_owned() }.into())
} else {
None
}
}
/// Try to represent the `Verification` as an `QrCode` verification object,
/// 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.to_owned(), runtime: self.runtime.to_owned() }.into())
} else {
None
}
}
pub enum Verification {
/// The `m.sas.v1` verification flow.
SasV1 {
#[allow(missing_docs)]
sas: Sas,
},
/// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1`
/// verification flow.
QrCodeV1 {
#[allow(missing_docs)]
qrcode: QrCode,
},
}
/// The `m.sas.v1` verification flow.
#[derive(uniffi::Object)]
pub struct Sas {
pub(crate) inner: InnerSas,
pub(crate) runtime: Handle,
}
#[uniffi::export]
impl Sas {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
self.inner.other_user_id().to_string()
}
/// Get the device ID of the other side.
pub fn other_device_id(&self) -> String {
self.inner.other_device_id().to_string()
}
/// Get the unique ID that identifies this SAS verification flow.
pub fn flow_id(&self) -> String {
self.inner.flow_id().as_str().to_owned()
}
/// Get the room id if the verification is happening inside a room.
pub fn room_id(&self) -> Option<String> {
self.inner.room_id().map(|r| r.to_string())
}
/// Is the SAS flow done.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Did we initiate the verification flow.
pub fn we_started(&self) -> bool {
self.inner.we_started()
}
/// Accept that we're going forward with the short auth string verification.
pub fn accept(&self) -> Option<OutgoingVerificationRequest> {
self.inner.accept().map(|r| r.into())
}
/// Confirm a verification was successful.
///
/// This method should be called if a short auth string should be confirmed
/// as matching.
pub fn confirm(&self) -> Result<Option<ConfirmVerificationResult>, CryptoStoreError> {
let (requests, signature_request) = self.runtime.block_on(self.inner.confirm())?;
let requests = requests.into_iter().map(|r| r.into()).collect();
Ok(Some(ConfirmVerificationResult {
requests,
signature_request: signature_request.map(|s| s.into()),
}))
}
/// Cancel the SAS verification using the given cancel code.
///
/// # 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]
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
self.inner.cancel_with_code(cancel_code.into()).map(|r| r.into())
}
/// Get a list of emoji indices of the emoji representation of the short
/// auth string.
///
/// *Note*: A SAS verification needs to be started and in the presentable
/// state for this to return the list of emoji indices, otherwise returns
/// `None`.
pub fn get_emoji_indices(&self) -> Option<Vec<i32>> {
self.inner.emoji_index().map(|v| v.iter().map(|i| (*i).into()).collect())
}
/// Get the decimal representation of the short auth string.
///
/// *Note*: A SAS verification needs to be started and in the presentable
/// state for this to return the list of decimals, otherwise returns
/// `None`.
pub fn get_decimals(&self) -> Option<Vec<i32>> {
self.inner.decimals().map(|v| [v.0.into(), v.1.into(), v.2.into()].to_vec())
}
/// Set a listener for changes in the SAS verification process.
///
/// The given callback will be called whenever the state changes.
///
/// This method can be used to react to changes in the state of the
/// verification process, or rather the method can be used to handle
/// each step of the verification process.
///
/// This method will spawn a tokio task on the Rust side, once we reach the
/// Done or Cancelled state, the task will stop listening for changes.
///
/// # Flowchart
///
/// The flow of the verification process is pictured bellow. Please note
/// that the process can be cancelled at each step of the process.
/// Either side can cancel the process.
///
/// ```text
/// ┌───────┐
/// │Started│
/// └───┬───┘
/// │
/// ┌────⌄───┐
/// │Accepted│
/// └────┬───┘
/// │
/// ┌───────⌄──────┐
/// │Keys Exchanged│
/// └───────┬──────┘
/// │
/// ________⌄________
/// ╲ ┌─────────┐
/// Does the short ╲______│Cancelled│
/// ╲ auth string match no └─────────┘
/// ╲_________________
/// │yes
/// │
/// ┌────⌄────┐
/// │Confirmed│
/// └────┬────┘
/// │
/// ┌───⌄───┐
/// │ Done │
/// └───────┘
/// ```
pub fn set_changes_listener(&self, listener: Box<dyn SasListener>) {
let stream = self.inner.changes();
self.runtime.spawn(Self::changes_listener(stream, listener));
}
/// Get the current state of the SAS verification process.
pub fn state(&self) -> SasState {
self.inner.state().into()
}
}
impl Sas {
async fn changes_listener(
mut stream: impl Stream<Item = RustSasState> + std::marker::Unpin,
listener: Box<dyn SasListener>,
) {
while let Some(state) = stream.next().await {
// If we receive a done or a cancelled state we're at the end of our road, we
// break out of the loop to deallocate the stream and finish the
// task.
let should_break =
matches!(state, RustSasState::Done { .. } | RustSasState::Cancelled { .. });
listener.on_change(state.into());
if should_break {
break;
}
}
}
}
/// Listener that will be passed over the FFI to report changes to a QrCode
/// verification.
#[uniffi::export(callback_interface)]
pub trait QrCodeListener: Send {
/// The callback that should be called on the Rust side
///
/// # Arguments
///
/// * `state` - The current state of the QrCode verification.
fn on_change(&self, state: QrCodeState);
}
/// An Enum describing the state the QrCode verification is in.
#[derive(uniffi::Enum)]
pub enum QrCodeState {
/// The QR verification has been started.
Started,
/// The QR verification has been scanned by the other side.
Scanned,
/// The scanning of the QR code has been confirmed by us.
Confirmed,
/// We have successfully scanned the QR code and are able to send a
/// reciprocation event.
Reciprocated,
/// The verification process has been successfully concluded.
Done,
/// The verification process has been cancelled.
Cancelled {
/// Information about the reason of the cancellation.
cancel_info: CancelInfo,
},
}
impl From<QrVerificationState> for QrCodeState {
fn from(value: QrVerificationState) -> Self {
match value {
QrVerificationState::Started => Self::Started,
QrVerificationState::Scanned => Self::Scanned,
QrVerificationState::Confirmed => Self::Confirmed,
QrVerificationState::Reciprocated => Self::Reciprocated,
QrVerificationState::Done { .. } => Self::Done,
QrVerificationState::Cancelled(c) => Self::Cancelled { cancel_info: c.into() },
}
}
/// The other user that is participating in the verification flow
pub other_user_id: String,
/// The other user's device that is participating in the verification flow
pub other_device_id: String,
/// The unique ID of this verification flow, will be a random string for
/// to-device events or a event ID for in-room events.
pub flow_id: String,
/// The room ID where this verification is happening, will be `None` if the
/// verification is going through to-device messages
pub room_id: Option<String>,
/// Did we initiate the verification flow
pub we_started: bool,
/// Has the non-initiating side accepted the verification flow
pub has_been_accepted: bool,
/// Can the short auth string be presented
pub can_be_presented: bool,
/// Does the flow support the emoji representation of the short auth string
pub supports_emoji: bool,
/// Have we confirmed that the short auth strings match
pub have_we_confirmed: bool,
/// Has the verification completed successfully
pub is_done: bool,
/// Has the flow been cancelled
pub is_cancelled: bool,
/// Information about the cancellation of the flow, will be `None` if the
/// flow hasn't been cancelled
pub cancel_info: Option<CancelInfo>,
}
/// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1`
/// verification flow.
#[derive(uniffi::Object)]
pub struct QrCode {
pub(crate) inner: InnerQr,
pub(crate) runtime: Handle,
/// The other user that is participating in the verification flow
pub other_user_id: String,
/// The other user's device that is participating in the verification flow
pub other_device_id: String,
/// The unique ID of this verification flow, will be a random string for
/// to-device events or a event ID for in-room events.
pub flow_id: String,
/// The room ID where this verification is happening, will be `None` if the
/// verification is going through to-device messages
pub room_id: Option<String>,
/// Did we initiate the verification flow
pub we_started: bool,
/// Has the QR code been scanned by the other side
pub other_side_scanned: bool,
/// Has the scanning of the QR code been confirmed by us
pub has_been_confirmed: bool,
/// Did we scan the QR code and sent out a reciprocation
pub reciprocated: bool,
/// Has the verification completed successfully
pub is_done: bool,
/// Has the flow been cancelled
pub is_cancelled: bool,
/// Information about the cancellation of the flow, will be `None` if the
/// flow hasn't been cancelled
pub cancel_info: Option<CancelInfo>,
}
#[uniffi::export]
impl QrCode {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
self.inner.other_user_id().to_string()
}
/// Get the device ID of the other side.
pub fn other_device_id(&self) -> String {
self.inner.other_device_id().to_string()
}
/// Get the unique ID that identifies this QR code verification flow.
pub fn flow_id(&self) -> String {
self.inner.flow_id().as_str().to_owned()
}
/// Get the room id if the verification is happening inside a room.
pub fn room_id(&self) -> Option<String> {
self.inner.room_id().map(|r| r.to_string())
}
/// Is the QR code verification done.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Has the verification flow been cancelled.
pub fn is_cancelled(&self) -> bool {
self.inner.is_cancelled()
}
/// Did we initiate the verification flow.
pub fn we_started(&self) -> bool {
self.inner.we_started()
}
/// Get the CancelInfo of this QR code verification object.
///
/// Will be `None` if the flow has not been cancelled.
pub fn cancel_info(&self) -> Option<CancelInfo> {
self.inner.cancel_info().map(|c| c.into())
}
/// Has the QR verification been scanned by the other side.
///
/// When the verification object is in this state it's required that the
/// user confirms that the other side has scanned the QR code.
pub fn has_been_scanned(&self) -> bool {
self.inner.has_been_scanned()
}
/// Have we successfully scanned the QR code and are able to send a
/// reciprocation event.
pub fn reciprocated(&self) -> bool {
self.inner.reciprocated()
}
/// Cancel the QR code verification using the given cancel code.
///
/// # 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]
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
self.inner.cancel_with_code(cancel_code.into()).map(|r| r.into())
}
/// Confirm a verification was successful.
///
/// This method should be called if we want to confirm that the other side
/// has scanned our QR code.
pub fn confirm(&self) -> Option<ConfirmVerificationResult> {
self.inner.confirm_scanning().map(|r| ConfirmVerificationResult {
requests: vec![r.into()],
signature_request: None,
})
}
/// Generate data that should be encoded as a QR code.
///
/// This method should be called right before a QR code should be displayed,
/// the returned data is base64 encoded (without padding) and needs to be
/// 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()
}
/// Set a listener for changes in the QrCode verification process.
///
/// The given callback will be called whenever the state changes.
pub fn set_changes_listener(&self, listener: Box<dyn QrCodeListener>) {
let stream = self.inner.changes();
self.runtime.spawn(Self::changes_listener(stream, listener));
}
/// Get the current state of the QrCode verification process.
pub fn state(&self) -> QrCodeState {
self.inner.state().into()
}
}
impl QrCode {
async fn changes_listener(
mut stream: impl Stream<Item = QrVerificationState> + std::marker::Unpin,
listener: Box<dyn QrCodeListener>,
) {
while let Some(state) = stream.next().await {
// If we receive a done or a cancelled state we're at the end of our road, we
// break out of the loop to deallocate the stream and finish the
// task.
let should_break = matches!(
state,
QrVerificationState::Done { .. } | QrVerificationState::Cancelled { .. }
);
listener.on_change(state.into());
if should_break {
break;
}
impl From<InnerQr> for QrCode {
fn from(qr: InnerQr) -> Self {
Self {
other_user_id: qr.other_user_id().to_string(),
flow_id: qr.flow_id().as_str().to_owned(),
is_cancelled: qr.is_cancelled(),
is_done: qr.is_done(),
cancel_info: qr.cancel_info().map(|c| c.into()),
reciprocated: qr.reciprocated(),
we_started: qr.we_started(),
other_side_scanned: qr.has_been_scanned(),
has_been_confirmed: qr.has_been_confirmed(),
other_device_id: qr.other_device_id().to_string(),
room_id: qr.room_id().map(|r| r.to_string()),
}
}
}
/// 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,
@@ -476,37 +120,52 @@ impl From<RustCancelInfo> for CancelInfo {
}
/// A result type for starting SAS verifications.
#[derive(uniffi::Record)]
pub struct StartSasResult {
/// The SAS verification object that got created.
pub sas: Arc<Sas>,
pub sas: Sas,
/// The request that needs to be sent out to notify the other side that a
/// SAS verification should start.
pub request: OutgoingVerificationRequest,
}
/// A result type for scanning QR codes.
#[derive(uniffi::Record)]
pub struct ScanResult {
/// The QR code verification object that got created.
pub qr: Arc<QrCode>,
pub qr: QrCode,
/// The request that needs to be sent out to notify the other side that a
/// QR code verification should start.
pub request: OutgoingVerificationRequest,
}
impl From<InnerSas> for Sas {
fn from(sas: InnerSas) -> Self {
Self {
other_user_id: sas.other_user_id().to_string(),
other_device_id: sas.other_device_id().to_string(),
flow_id: sas.flow_id().as_str().to_owned(),
is_cancelled: sas.is_cancelled(),
is_done: sas.is_done(),
can_be_presented: sas.can_be_presented(),
supports_emoji: sas.supports_emoji(),
have_we_confirmed: sas.have_we_confirmed(),
we_started: sas.we_started(),
room_id: sas.room_id().map(|r| r.to_string()),
has_been_accepted: sas.has_been_accepted(),
cancel_info: sas.cancel_info().map(|c| c.into()),
}
}
}
/// A result type for requesting verifications.
#[derive(uniffi::Record)]
pub struct RequestVerificationResult {
/// The verification request object that got created.
pub verification: Arc<VerificationRequest>,
pub verification: VerificationRequest,
/// The request that needs to be sent out to notify the other side that
/// we're requesting verification to begin.
pub request: OutgoingVerificationRequest,
}
/// A result type for confirming verifications.
#[derive(uniffi::Record)]
pub struct ConfirmVerificationResult {
/// The requests that needs to be sent out to notify the other side that we
/// confirmed the verification.
@@ -516,288 +175,58 @@ pub struct ConfirmVerificationResult {
pub signature_request: Option<SignatureUploadRequest>,
}
/// Listener that will be passed over the FFI to report changes to a
/// verification request.
#[uniffi::export(callback_interface)]
pub trait VerificationRequestListener: Send {
/// The callback that should be called on the Rust side
///
/// # Arguments
///
/// * `state` - The current state of the verification request.
fn on_change(&self, state: VerificationRequestState);
}
/// An Enum describing the state the QrCode verification is in.
#[derive(uniffi::Enum)]
pub enum VerificationRequestState {
/// The verification request was sent
Requested,
/// The verification request is ready to start a verification flow.
Ready {
/// The verification methods supported by the other side.
their_methods: Vec<String>,
/// The verification methods supported by the us.
our_methods: Vec<String>,
},
/// The verification flow that was started with this request has finished.
Done,
/// The verification process has been cancelled.
Cancelled {
/// Information about the reason of the cancellation.
cancel_info: CancelInfo,
},
}
/// The verificatoin request object which then can transition into some concrete
/// verification method
#[derive(uniffi::Object)]
pub struct VerificationRequest {
pub(crate) inner: InnerVerificationRequest,
pub(crate) runtime: Handle,
/// The other user that is participating in the verification flow
pub other_user_id: String,
/// The other user's device that is participating in the verification flow
pub other_device_id: Option<String>,
/// The unique ID of this verification flow, will be a random string for
/// to-device events or a event ID for in-room events.
pub flow_id: String,
/// The room ID where this verification is happening, will be `None` if the
/// verification is going through to-device messages
pub room_id: Option<String>,
/// Did we initiate the verification flow
pub we_started: bool,
/// Did both parties aggree to verification
pub is_ready: bool,
/// Did another device respond to the verification request
pub is_passive: bool,
/// Has the verification completed successfully
pub is_done: bool,
/// Has the flow been cancelled
pub is_cancelled: bool,
/// The list of verification methods that the other side advertised as
/// supported
pub their_methods: Option<Vec<String>>,
/// The list of verification methods that we advertised as supported
pub our_methods: Option<Vec<String>>,
/// Information about the cancellation of the flow, will be `None` if the
/// flow hasn't been cancelled
pub cancel_info: Option<CancelInfo>,
}
#[uniffi::export]
impl VerificationRequest {
/// The id of the other user that is participating in this verification
/// request.
pub fn other_user_id(&self) -> String {
self.inner.other_user().to_string()
}
/// The id of the other device that is participating in this verification.
pub fn other_device_id(&self) -> Option<String> {
self.inner.other_device_id().map(|d| d.to_string())
}
/// Get the unique ID of this verification request
pub fn flow_id(&self) -> String {
self.inner.flow_id().as_str().to_owned()
}
/// Get the room id if the verification is happening inside a room.
pub fn room_id(&self) -> Option<String> {
self.inner.room_id().map(|r| r.to_string())
}
/// Has the verification flow that was started with this request finished.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Is the verification request ready to start a verification flow.
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
/// Did we initiate the verification request
pub fn we_started(&self) -> bool {
self.inner.we_started()
}
/// Has the verification request been answered by another device.
pub fn is_passive(&self) -> bool {
self.inner.is_passive()
}
/// Has the verification flow that been cancelled.
pub fn is_cancelled(&self) -> bool {
self.inner.is_cancelled()
}
/// Get info about the cancellation if the verification request has been
/// cancelled.
pub fn cancel_info(&self) -> Option<CancelInfo> {
self.inner.cancel_info().map(|v| v.into())
}
/// Get the supported verification methods of the other side.
///
/// Will be present only if the other side requested the verification or if
/// we're in the ready state.
pub fn their_supported_methods(&self) -> Option<Vec<String>> {
self.inner.their_supported_methods().map(|m| m.iter().map(|m| m.to_string()).collect())
}
/// Get our own supported verification methods that we advertised.
///
/// Will be present only we requested the verification or if we're in the
/// ready state.
pub fn our_supported_methods(&self) -> Option<Vec<String>> {
self.inner.our_supported_methods().map(|m| m.iter().map(|m| m.to_string()).collect())
}
/// Accept a verification requests that we share with the given user with
/// the given flow id.
///
/// This will move the verification request into the ready state.
///
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to accept the
/// 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.
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())
}
/// Cancel a verification for the given user with the given flow id using
/// the given cancel code.
pub fn cancel(&self) -> Option<OutgoingVerificationRequest> {
self.inner.cancel().map(|r| r.into())
}
/// Transition from a verification request into short auth string based
/// verification.
///
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to start the
/// SAS verification.
///
/// * `flow_id` - The ID of the verification request that initiated the
/// 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: sas, runtime: self.runtime.clone() }),
request: r.into(),
}))
}
/// Transition from a verification request into QR code verification.
///
/// This method should be called when one wants to display a QR code so the
/// other side can scan it and move the QR code verification forward.
///
/// # Arguments
///
/// * `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.
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: qr, runtime: self.runtime.clone() }.into()))
}
/// Pass data from a scanned QR code to an active verification request and
/// transition into QR code verification.
///
/// This requires an active `VerificationRequest` to succeed, returns `None`
/// if no `VerificationRequest` is found or if the QR code data is invalid.
///
/// # Arguments
///
/// * `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.
///
/// * `data` - The data that was extracted from the scanned QR code as an
/// base64 encoded string, without padding.
pub fn scan_qr_code(&self, data: String) -> Option<ScanResult> {
let data = base64_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: qr, runtime: self.runtime.clone() }.into(),
request: request.into(),
})
} else {
None
}
}
/// Set a listener for changes in the verification request
///
/// The given callback will be called whenever the state changes.
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));
}
/// Get the current state of the verification request.
pub fn state(&self) -> VerificationRequestState {
Self::convert_verification_request(&self.inner, self.inner.state())
}
}
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_id: _,
} => 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>,
) {
while let Some(state) = stream.next().await {
// If we receive a done or a cancelled state we're at the end of our road, we
// break out of the loop to deallocate the stream and finish the
// task.
let should_break = matches!(
state,
RustVerificationRequestState::Done { .. }
| RustVerificationRequestState::Cancelled { .. }
);
let state = Self::convert_verification_request(&request, state);
listener.on_change(state);
if should_break {
break;
}
impl From<InnerVerificationRequest> for VerificationRequest {
fn from(v: InnerVerificationRequest) -> Self {
Self {
other_user_id: v.other_user().to_string(),
other_device_id: v.other_device_id().map(|d| d.to_string()),
flow_id: v.flow_id().as_str().to_owned(),
is_cancelled: v.is_cancelled(),
is_done: v.is_done(),
is_ready: v.is_ready(),
room_id: v.room_id().map(|r| r.to_string()),
we_started: v.we_started(),
is_passive: v.is_passive(),
cancel_info: v.cancel_info().map(|c| c.into()),
their_methods: v
.their_supported_methods()
.map(|v| v.into_iter().map(|m| m.to_string()).collect()),
our_methods: v
.our_supported_methods()
.map(|v| v.into_iter().map(|m| m.to_string()).collect()),
}
}
}
@@ -1,6 +1,2 @@
[bindings.kotlin]
package_name = "org.matrix.rustcomponents.sdk.crypto"
cdylib_name = "matrix_sdk_crypto_ffi"
[bindings.swift]
module_name = "MatrixSDKCrypto"
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
+3
View File
@@ -0,0 +1,3 @@
/docs
/node_modules
/package-lock.json
+45
View File
@@ -0,0 +1,45 @@
[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 = "1.60"
version = "0.1.0-alpha.0"
publish = false
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
[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]
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", features = ["experimental-nodejs"] }
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
ruma = { version = "0.7.0", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { version = "0.3.0", features = ["js"] }
wasm-bindgen = "0.2.80"
wasm-bindgen-futures = "0.4.30"
js-sys = "0.3.49"
console_error_panic_hook = "0.1.7"
serde_json = "1.0.79"
http = "0.2.6"
anyhow = "1.0.58"
tracing = { version = "0.1.35", default-features = false, features = ["attributes"] }
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
zeroize = "1.3.0"
+59
View File
@@ -0,0 +1,59 @@
# `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/
+61
View File
@@ -0,0 +1,61 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Matrix SDK Crypto JavaScript Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | filter(attribute="scope", value="crypto-js") | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^test", group = "Testing"},
{ message = "^doc", group = "Documentation"},
{ message = "^refactor", group = "Refactoring"},
{ message = "^ci", group = "Continuous Integration"},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = ""
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
@@ -0,0 +1,48 @@
{
"name": "@matrix-org/matrix-sdk-crypto-js",
"version": "0.1.0-alpha.0",
"homepage": "https://github.com/matrix-org/matrix-rust-sdk",
"description": "Matrix encryption library, for JavaScript",
"license": "Apache-2.0",
"collaborators": [
"Ivan Enderlin <ivane@element.io>"
],
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-rust-sdk"
},
"keywords": [
"matrix",
"chat",
"messaging",
"ruma",
"nio"
],
"main": "matrix_sdk_crypto.js",
"types": "pkg/matrix_sdk_crypto.d.ts",
"files": [
"pkg/matrix_sdk_crypto_bg.wasm",
"pkg/matrix_sdk_crypto.js",
"pkg/matrix_sdk_crypto.d.ts"
],
"devDependencies": {
"cross-env": "^7.0.3",
"fake-indexeddb": "^4.0",
"jest": "^28.1.0",
"typedoc": "^0.22.17",
"wasm-pack": "^0.10.2",
"yargs-parser": "~21.0.1"
},
"engines": {
"node": ">= 10"
},
"scripts": {
"build": "cross-env RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --scope matrix-org --out-dir ./pkg",
"test": "jest --verbose",
"doc": "typedoc --tsconfig .",
"prepack": "npm run build && npm run test",
"pack": "wasm-pack pack",
"prepublish": "npm run pack",
"publish": "wasm-pack publish"
}
}
@@ -0,0 +1,124 @@
//! 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 media_encryption_info = match attachment.media_encryption_info.take() {
Some(media_encryption_info) => media_encryption_info,
None => {
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()
}
}
+249
View File
@@ -0,0 +1,249 @@
//! Types for a `Device`.
use js_sys::{Array, Map, Promise};
use wasm_bindgen::prelude::*;
use crate::{
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()
}
/// Set the local trust state of the device to the given state.
///
/// This wont 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 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 wont 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,135 @@
//! Encryption types & siblings.
use std::time::Duration;
use wasm_bindgen::prelude::*;
use crate::events;
/// Settings for an encrypted room.
///
/// This determines the algorithm and rotation periods of a group
/// session.
#[wasm_bindgen(getter_with_clone)]
#[derive(Debug, Clone)]
pub struct EncryptionSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EncryptionAlgorithm,
/// How long the session should be used before changing it,
/// expressed in microseconds.
#[wasm_bindgen(js_name = "rotationPeriod")]
pub rotation_period: u64,
/// How many messages should be sent before changing the session.
#[wasm_bindgen(js_name = "rotationPeriodMessages")]
pub rotation_period_messages: u64,
/// The history visibility of the room when the session was
/// created.
#[wasm_bindgen(js_name = "historyVisibility")]
pub history_visibility: events::HistoryVisibility,
/// 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 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"),
}
}
}
/// The verification state of the device that sent an event to us.
#[wasm_bindgen]
#[derive(Debug)]
pub enum VerificationState {
/// The device is trusted.
Trusted,
/// The device is not trusted.
Untrusted,
/// The device is not known to us.
UnknownDevice,
}
impl From<&matrix_sdk_common::deserialized_responses::VerificationState> for VerificationState {
fn from(value: &matrix_sdk_common::deserialized_responses::VerificationState) -> Self {
use matrix_sdk_common::deserialized_responses::VerificationState::*;
match value {
Trusted => Self::Trusted,
Untrusted => Self::Untrusted,
UnknownDevice => Self::UnknownDevice,
}
}
}
@@ -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::{JsValue, UnwrapThrowExt};
use wasm_bindgen_futures::spawn_local;
pub(crate) fn future_to_promise<F, T>(future: F) -> Promise
where
F: Future<Output = Result<T, anyhow::Error>> + 'static,
T: Into<JsValue>,
{
let mut future = Some(future);
Promise::new(&mut |resolve, reject| {
let future = future.take().unwrap_throw();
spawn_local(async move {
match future.await {
Ok(value) => resolve.call1(&JsValue::UNDEFINED, &value.into()).unwrap_throw(),
Err(value) => {
reject.call1(&JsValue::UNDEFINED, &value.to_string().into()).unwrap_throw()
}
};
});
})
}
@@ -0,0 +1,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)?)
}))
}
}
+40
View File
@@ -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()
}
+50
View File
@@ -0,0 +1,50 @@
// 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)]
#![allow(clippy::drop_non_drop)] // triggered by wasm_bindgen code
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 wasm_bindgen::prelude::*;
/// 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,731 @@
//! The crypto specific Olm objects.
use std::collections::BTreeMap;
use js_sys::{Array, Function, Map, Promise, Set};
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use serde_json::{json, Value as JsonValue};
use wasm_bindgen::prelude::*;
use crate::{
device, encryption,
future::future_to_promise,
identifiers, identities,
js::downcast,
olm, requests,
requests::OutgoingRequest,
responses::{self, response_from_string},
store, 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 {
/// Create a new memory based `OlmMachine`.
///
/// The created machine will keep the encryption keys only in
/// memory and once the objects is dropped, the keys will be lost.
///
/// `user_id` represents the unique ID of the user that owns this
/// machine. `device_id` represents the unique ID of the device
/// that owns this machine.
///
/// `store_name` and `store_passphrase` are both optional, but
/// must be both set to have an effect. If they are both set, the
/// state of the machine will persist in a database named
/// `store_name` where its content is encrypted by the passphrase
/// given by `store_passphrase`. If they are not both set, the
/// created machine will keep the encryption keys only in memory,
/// and once the object is dropped, the keys will be lost.
#[wasm_bindgen(constructor)]
#[allow(clippy::new_ret_no_self)]
pub fn new(
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) {
// 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
// practise, it doesn't impact this crate because it's
// always compiled for `wasm32`.
#[cfg(target_arch = "wasm32")]
(Some(store_name), Some(mut store_passphrase)) => {
use std::sync::Arc;
use zeroize::Zeroize;
let store = Some(
matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase(
&store_name,
&store_passphrase,
)
.await
.map(Arc::new)?,
);
store_passphrase.zeroize();
store
}
(Some(_), None) => return Err(anyhow::Error::msg("The `store_name` has been set, and so, it expects a `store_passphrase`, which is not set; please provide one")),
(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,
};
Ok(OlmMachine {
inner: match store {
Some(store) => {
matrix_sdk_crypto::OlmMachine::with_store(
user_id.as_ref(),
device_id.as_ref(),
store,
)
.await?
}
None => {
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 all the tracked users of our own device.
///
/// Returns a `Set<UserId>`.
#[wasm_bindgen(js_name = "trackedUsers")]
pub fn tracked_users(&self) -> Set {
let set = Set::new(&JsValue::UNDEFINED);
for user in self.inner.tracked_users() {
set.add(&identifiers::UserId::from(user).into());
}
set
}
/// Update the tracked users.
///
/// `users` is an iterator over user IDs that should be marked for
/// tracking.
///
/// This will mark users that weren't seen before for a key query
/// and tracking.
///
/// If the user is already known to the Olm machine, it will not
/// be considered for a key query.
#[wasm_bindgen(js_name = "updateTrackedUsers")]
pub fn update_tracked_users(&self, users: &Array) -> Result<Promise, JsError> {
let users = users
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
let me = self.inner.clone();
Ok(future_to_promise(async move {
me.update_tracked_users(users.iter().map(AsRef::as_ref)).await;
Ok(JsValue::UNDEFINED)
}))
}
/// Handle to-device events and one-time key counts from a sync
/// response.
///
/// This will decrypt and handle to-device events returning the
/// decrypted versions of them.
///
/// To decrypt an event from the room timeline call
/// `decrypt_room_event`.
#[wasm_bindgen(js_name = "receiveSyncChanges")]
pub fn receive_sync_changes(
&self,
to_device_events: &str,
changed_devices: &sync_events::DeviceLists,
one_time_key_counts: &Map,
unused_fallback_keys: &Set,
) -> Result<Promise, JsError> {
let to_device_events = serde_json::from_str(to_device_events)?;
let changed_devices = changed_devices.inner.clone();
let one_time_key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = one_time_key_counts
.entries()
.into_iter()
.filter_map(|js_value| {
let pair = Array::from(&js_value.ok()?);
let (key, value) = (
DeviceKeyAlgorithm::from(pair.at(0).as_string()?),
UInt::new(pair.at(1).as_f64()? as u64)?,
);
Some((key, value))
})
.collect();
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> = Some(
unused_fallback_keys
.values()
.into_iter()
.filter_map(|js_value| Some(DeviceKeyAlgorithm::from(js_value.ok()?.as_string()?)))
.collect(),
);
let me = self.inner.clone();
Ok(future_to_promise(async move {
Ok(serde_json::to_string(
&me.receive_sync_changes(
to_device_events,
&changed_devices,
&one_time_key_counts,
unused_fallback_keys.as_deref(),
)
.await?,
)?)
}))
}
/// Get the outgoing requests that need to be sent out.
///
/// This returns a list of `JsValue` to represent either:
/// * `KeysUploadRequest`,
/// * `KeysQueryRequest`,
/// * `KeysClaimRequest`,
/// * `ToDeviceRequest`,
/// * `SignatureUploadRequest`,
/// * `RoomMessageRequest` or
/// * `KeysBackupRequest`.
///
/// Those requests need to be sent out to the server and the
/// responses need to be passed back to the state machine using
/// `mark_request_as_sent`.
#[wasm_bindgen(js_name = "outgoingRequests")]
pub fn outgoing_requests(&self) -> Promise {
let me = self.inner.clone();
future_to_promise(async move {
Ok(me
.outgoing_requests()
.await?
.into_iter()
.map(OutgoingRequest)
.map(TryFrom::try_from)
.collect::<Result<Vec<JsValue>, _>>()?
.into_iter()
.collect::<Array>())
})
}
/// Mark the request with the given request ID as sent (see
/// `outgoing_requests`).
///
/// Arguments are:
///
/// * `request_id` represents the unique ID of the request that was sent
/// out. This is needed to couple the response with the now sent out
/// request.
/// * `response_type` represents the type of the request that was sent out.
/// * `response` represents the response that was received from the server
/// after the outgoing request was sent out.
#[wasm_bindgen(js_name = "markRequestAsSent")]
pub fn mark_request_as_sent(
&self,
request_id: &str,
request_type: requests::RequestType,
response: &str,
) -> Result<Promise, JsError> {
let transaction_id = OwnedTransactionId::from(request_id);
let response = response_from_string(response)?;
let incoming_response = responses::OwnedResponse::try_from((request_type, response))?;
let me = self.inner.clone();
Ok(future_to_promise(async move {
Ok(me.mark_request_as_sent(&transaction_id, &incoming_response).await.map(|_| true)?)
}))
}
/// Encrypt a room message for the given room.
///
/// Beware that a 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 dont 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.
#[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 {
Ok(serde_json::to_string(
&me.share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings)
.await?,
)?)
}))
}
/// Get the a key claiming request for the user/device pairs that
/// we are missing Olm sessions for.
///
/// Returns `NULL` if no key claiming request needs to be sent
/// out, otherwise it returns an `Array` where the first key is
/// the transaction ID as a string, and the second key is the keys
/// claim request serialized to JSON.
///
/// Sessions need to be established between devices so group
/// sessions for a room can be shared with them.
///
/// This should be called every time a group session needs to be
/// shared as well as between sync calls. After a sync some
/// devices may request room keys without us having a valid Olm
/// session with them, making it impossible to server the room key
/// request, thus its necessary to check for missing sessions
/// between sync as well.
///
/// Note: Care should be taken that only one such request at a
/// time is in flight, e.g. using a lock.
///
/// The response of a successful key claiming requests needs to be
/// passed to the `OlmMachine` with the `mark_request_as_sent`.
///
/// `users` represents the list of users that we should check if
/// we lack a session with one of their devices. This can be an
/// empty iterator when calling this method between sync requests.
#[wasm_bindgen(js_name = "getMissingSessions")]
pub fn get_missing_sessions(&self, users: &Array) -> Result<Promise, JsError> {
let users = users
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
let me = self.inner.clone();
Ok(future_to_promise(async move {
match me.get_missing_sessions(users.iter().map(AsRef::as_ref)).await? {
Some((transaction_id, keys_claim_request)) => {
Ok(JsValue::from(requests::KeysClaimRequest::try_from((
transaction_id.to_string(),
&keys_claim_request,
))?))
}
None => Ok(JsValue::NULL),
}
}))
}
/// Get a map holding all the devices of a user.
///
/// `user_id` represents the unique ID of the user that the
/// devices belong to.
#[wasm_bindgen(js_name = "getUserDevices")]
pub fn get_user_devices(&self, user_id: &identifiers::UserId) -> Promise {
let user_id = user_id.inner.clone();
let me = self.inner.clone();
future_to_promise::<_, device::UserDevices>(async move {
Ok(me.get_user_devices(&user_id, None).await.map(Into::into)?)
})
}
/// Get a specific device of a user if one is found and the crypto store
/// didn't throw an error.
///
/// `user_id` represents the unique ID of the user that the
/// identity belongs to. `device_id` represents the unique ID of
/// the device.
#[wasm_bindgen(js_name = "getDevice")]
pub fn get_device(
&self,
user_id: &identifiers::UserId,
device_id: &identifiers::DeviceId,
) -> Promise {
let user_id = user_id.inner.clone();
let device_id = device_id.inner.clone();
let me = self.inner.clone();
future_to_promise::<_, Option<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 an unencrypted verification event.
///
/// This method can be used to pass verification events that are
/// happening in unencrypted rooms to the `OlmMachine`.
///
/// Note: This does not need to be called for encrypted events
/// since those will get passed to the `OlmMachine` during
/// decryption.
#[wasm_bindgen(js_name = "receiveUnencryptedVerificationEvent")]
pub fn receive_unencrypted_verification_event(&self, event: &str) -> Result<Promise, JsError> {
let event: ruma::events::AnyMessageLikeEvent = serde_json::from_str(event)?;
let me = self.inner.clone();
Ok(future_to_promise(async move {
Ok(me
.receive_unencrypted_verification_event(&event)
.await
.map(|_| JsValue::UNDEFINED)?)
}))
}
/// 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,
)?)?)
}
}
@@ -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 }
}
}
};
}
+72
View File
@@ -0,0 +1,72 @@
//! Olm types.
use wasm_bindgen::prelude::*;
use crate::{identifiers, impl_from_to_inner};
/// 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()
}
/// 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,419 @@
//! 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,
};
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 object of form:
///
/// ```json
/// {"device_keys": …, "one_time_keys": …, "fallback_keys": …}
/// ```
#[wasm_bindgen(readonly)]
pub body: JsString,
}
#[wasm_bindgen]
impl KeysUploadRequest {
/// Create a new `KeysUploadRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> KeysUploadRequest {
Self { id: 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 object of form:
///
/// ```json
/// {"timeout": …, "device_keys": …, "token": …}
/// ```
#[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 object of form:
///
/// ```json
/// {"timeout": …, "one_time_keys": …}
/// ```
#[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 JSON-encoded object of form:
///
/// ```json
/// {"event_type": …, "txn_id": …, "messages": …}
/// ```
#[wasm_bindgen(readonly)]
pub body: JsString,
}
#[wasm_bindgen]
impl ToDeviceRequest {
/// Create a new `ToDeviceRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> ToDeviceRequest {
Self { id: Some(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 object of form:
///
/// ```json
/// {"signed_keys": …, "txn_id": …, "messages": …}
/// ```
#[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 JSON-encoded object of form:
///
/// ```json
/// {"room_id": …, "txn_id": …, "content": …}
/// ```
#[wasm_bindgen(readonly)]
pub body: JsString,
}
#[wasm_bindgen]
impl RoomMessageRequest {
/// Create a new `RoomMessageRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> RoomMessageRequest {
Self { id: Some(id), body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::RoomMessage
}
}
/// A request that will back up a batch of room keys to the server
/// ([specification]).
///
/// [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 object of form:
///
/// ```json
/// {"rooms": …}
/// ```
#[wasm_bindgen(readonly)]
pub body: JsString,
}
/** Other Requests * */
/// Request that will publish a cross signing identity.
///
/// This uploads the public cross signing key triplet.
#[wasm_bindgen(getter_with_clone)]
#[derive(Debug)]
pub struct SigningKeysUploadRequest {
/// The request ID.
#[wasm_bindgen(readonly)]
pub id: Option<JsString>,
/// A JSON-encoded object of form:
///
/// ```json
/// {"master_key": …, "self_signing_key": …, "user_signing_key": …}
/// ```
#[wasm_bindgen(readonly)]
pub body: JsString,
}
#[wasm_bindgen]
impl KeysBackupRequest {
/// Create a new `KeysBackupRequest`.
#[wasm_bindgen(constructor)]
pub fn new(id: JsString, body: JsString) -> KeysBackupRequest {
Self { id: Some(id), body }
}
/// Get its request type.
#[wasm_bindgen(getter, js_name = "type")]
pub fn request_type(&self) -> RequestType {
RequestType::KeysBackup
}
}
macro_rules! request {
($destination_request:ident from $source_request:ident maps fields $( $field:ident ),+ $(,)? ) => {
impl $destination_request {
pub(crate) fn to_json(request: &$source_request) -> Result<String, serde_json::Error> {
let mut map = serde_json::Map::new();
$(
map.insert(stringify!($field).to_owned(), serde_json::to_value(&request.$field).unwrap());
)+
let object = serde_json::Value::Object(map);
serde_json::to_string(&object)
}
}
impl TryFrom<&$source_request> for $destination_request {
type Error = serde_json::Error;
fn try_from(request: &$source_request) -> Result<Self, Self::Error> {
Ok($destination_request {
id: None,
body: Self::to_json(request)?.into(),
})
}
}
impl TryFrom<(String, &$source_request)> for $destination_request {
type Error = serde_json::Error;
fn try_from(
(request_id, request): (String, &$source_request),
) -> Result<Self, Self::Error> {
Ok($destination_request {
id: Some(request_id.into()),
body: Self::to_json(request)?.into(),
})
}
}
};
}
// Outgoing Requests
request!(KeysUploadRequest from OriginalKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys);
request!(KeysQueryRequest from OriginalKeysQueryRequest maps fields timeout, device_keys, token);
request!(KeysClaimRequest from OriginalKeysClaimRequest maps fields timeout, one_time_keys);
request!(ToDeviceRequest from OriginalToDeviceRequest maps fields event_type, txn_id, messages);
request!(SignatureUploadRequest from OriginalSignatureUploadRequest maps fields signed_keys);
request!(RoomMessageRequest from OriginalRoomMessageRequest maps fields room_id, txn_id, content);
request!(KeysBackupRequest from OriginalKeysBackupRequest maps fields rooms);
// Other Requests
request!(SigningKeysUploadRequest from OriginalUploadSigningKeysRequest maps fields master_key, self_signing_key, user_signing_key);
// JavaScript has no complex enums like Rust. To return structs of
// different types, we have no choice that hiding everything behind a
// `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,206 @@
//! Types related to responses.
use std::borrow::Borrow;
use js_sys::{Array, JsString};
use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo};
use matrix_sdk_crypto::IncomingResponse;
pub(crate) use ruma::api::client::{
backup::add_backup_keys::v3::Response as KeysBackupResponse,
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(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone()))
}
/// The Curve25519 key of the device that created the megolm
/// decryption key originally.
#[wasm_bindgen(getter, js_name = "senderCurve25519Key")]
pub fn sender_curve25519_key(&self) -> Option<JsString> {
Some(match &self.encryption_info.as_ref()?.algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => curve25519_key.clone().into(),
})
}
/// The signing Ed25519 key that have created the megolm key that
/// was used to decrypt this session.
#[wasm_bindgen(getter, js_name = "senderClaimedEd25519Key")]
pub fn sender_claimed_ed25519_key(&self) -> Option<JsString> {
match &self.encryption_info.as_ref()?.algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { sender_claimed_keys, .. } => {
sender_claimed_keys.get(&ruma::DeviceKeyAlgorithm::Ed25519).cloned().map(Into::into)
}
}
}
/// Chain of Curve25519 keys through which this session was
/// forwarded, via `m.forwarded_room_key` events.
#[wasm_bindgen(getter, js_name = "forwardingCurve25519KeyChain")]
pub fn forwarding_curve25519_key_chain(&self) -> 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(getter, js_name = "verificationState")]
pub fn verification_state(&self) -> Option<encryption::VerificationState> {
Some((self.encryption_info.as_ref()?.verification_state.borrow()).into())
}
}
impl From<matrix_sdk_common::deserialized_responses::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,36 @@
//! Store types.
use wasm_bindgen::prelude::*;
use crate::impl_from_to_inner;
/// 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()
}
}
@@ -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::v3::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::v3::DeviceLists::default();
inner.changed = changed
.unwrap_or_default()
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
inner.left = left
.unwrap_or_default()
.iter()
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
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,290 @@
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 = "trace")]
fn log_trace(message: String);
#[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_trace(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::*;
+156
View File
@@ -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,77 @@
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,900 @@
const {
OlmMachine,
UserId,
DeviceId,
DeviceKeyId,
RoomId,
Device,
LocalTrust,
UserDevices,
DeviceKey,
DeviceKeyName,
DeviceKeyAlgorithmName,
Ed25519PublicKey,
Curve25519PublicKey,
Signatures,
VerificationMethod,
VerificationRequest,
ToDeviceRequest,
DeviceLists,
KeysUploadRequest,
RequestType,
KeysQueryRequest,
Sas,
Emoji,
SigningKeysUploadRequest,
SignatureUploadRequest,
Qr,
QrCode,
QrCodeScan,
} = require('../pkg/matrix_sdk_crypto_js');
const { zip, addMachineToMachine } = require('./helper');
describe('LocalTrust', () => {
test('has the correct variant values', () => {
expect(LocalTrust.Verified).toStrictEqual(0);
expect(LocalTrust.BlackListed).toStrictEqual(1);
expect(LocalTrust.Ignored).toStrictEqual(2);
expect(LocalTrust.Unset).toStrictEqual(3);
});
});
describe('DeviceKeyName', () => {
test('has the correct variant values', () => {
expect(DeviceKeyName.Curve25519).toStrictEqual(0);
expect(DeviceKeyName.Ed25519).toStrictEqual(1);
expect(DeviceKeyName.Unknown).toStrictEqual(2);
});
});
describe(OlmMachine.name, () => {
const user = new UserId('@alice:example.org');
const device = new DeviceId('foobar');
const room = new RoomId('!baz:matrix.org');
function machine(new_user, new_device) {
return new OlmMachine(new_user || user, new_device || device);
}
test('can read user devices', async () => {
const m = await machine();
const userDevices = await m.getUserDevices(user);
expect(userDevices).toBeInstanceOf(UserDevices);
expect(userDevices.get(device)).toBeInstanceOf(Device);
expect(userDevices.isAnyVerified()).toStrictEqual(false);
expect(userDevices.keys().map(device_id => device_id.toString())).toStrictEqual([device.toString()]);
expect(userDevices.devices().map(device => device.deviceId.toString())).toStrictEqual([device.toString()]);
});
test('can read a user device', async () => {
const m = await machine();
const dev = await m.getDevice(user, device);
expect(dev).toBeInstanceOf(Device);
expect(dev.isVerified()).toStrictEqual(false);
expect(dev.isCrossSigningTrusted()).toStrictEqual(false);
expect(dev.localTrustState).toStrictEqual(LocalTrust.Unset);
expect(dev.isLocallyTrusted()).toStrictEqual(false);
expect(await dev.setLocalTrust(LocalTrust.Verified)).toBeNull();
expect(dev.localTrustState).toStrictEqual(LocalTrust.Verified);
expect(dev.isLocallyTrusted()).toStrictEqual(true);
expect(dev.userId.toString()).toStrictEqual(user.toString());
expect(dev.deviceId.toString()).toStrictEqual(device.toString());
expect(dev.deviceName).toBeUndefined();
const deviceKey = dev.getKey(DeviceKeyAlgorithmName.Ed25519);
expect(deviceKey).toBeInstanceOf(DeviceKey);
expect(deviceKey.name).toStrictEqual(DeviceKeyName.Ed25519);
expect(deviceKey.curve25519).toBeUndefined();
expect(deviceKey.ed25519).toBeInstanceOf(Ed25519PublicKey);
expect(deviceKey.unknown).toBeUndefined();
expect(deviceKey.toBase64()).toMatch(/^[A-Za-z0-9\+/]+$/);
expect(dev.curve25519Key).toBeInstanceOf(Curve25519PublicKey);
expect(dev.ed25519Key).toBeInstanceOf(Ed25519PublicKey);
for (const [deviceKeyId, deviceKey] of dev.keys) {
expect(deviceKeyId).toBeInstanceOf(DeviceKeyId);
expect(deviceKey).toBeInstanceOf(DeviceKey);
}
expect(dev.signatures).toBeInstanceOf(Signatures);
expect(dev.isBlacklisted()).toStrictEqual(false);
expect(dev.isDeleted()).toStrictEqual(false);
});
});
describe('Key Verification', () => {
const userId1 = new UserId('@alice:example.org');
const deviceId1 = new DeviceId('alice_device');
const userId2 = new UserId('@bob:example.org');
const deviceId2 = new DeviceId('bob_device');
function machine(new_user, new_device) {
return new OlmMachine(new_user || userId1, new_device || deviceId1);
}
describe('SAS', () => {
// First Olm machine.
let m1;
// Second Olm machine.
let m2;
beforeAll(async () => {
m1 = await machine(userId1, deviceId1);
m2 = await machine(userId2, deviceId2);
});
// Verification request for `m1`.
let verificationRequest1;
// The flow ID.
let flowId;
test('can request verification (`m.key.verification.request`)', async () => {
// Make `m1` and `m2` be aware of each other.
{
await addMachineToMachine(m2, m1);
await addMachineToMachine(m1, m2);
}
// Pick the device we want to start the verification with.
const device2 = await m1.getDevice(userId2, deviceId2);
expect(device2).toBeInstanceOf(Device);
let outgoingVerificationRequest;
// Request a verification from `m1` to `device2`.
[verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification();
expect(verificationRequest1).toBeInstanceOf(VerificationRequest);
expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString());
expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString());
expect(verificationRequest1.otherDeviceId).toBeUndefined();
expect(verificationRequest1.roomId).toBeUndefined();
expect(verificationRequest1.cancelInfo).toBeUndefined();
expect(verificationRequest1.isPassive()).toStrictEqual(false);
expect(verificationRequest1.isReady()).toStrictEqual(false);
expect(verificationRequest1.timedOut()).toStrictEqual(false);
expect(verificationRequest1.theirSupportedMethods).toBeUndefined();
expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]));
expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/);
expect(verificationRequest1.isSelfVerification()).toStrictEqual(false);
expect(verificationRequest1.weStarted()).toStrictEqual(true);
expect(verificationRequest1.isDone()).toStrictEqual(false);
expect(verificationRequest1.isCancelled()).toStrictEqual(false);
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()],
}]
};
// Let's send the verification request to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
flowId = verificationRequest1.flowId;
});
// Verification request for `m2`.
let verificationRequest2;
test('can fetch received request verification', async () => {
// Oh, a new verification request.
verificationRequest2 = m2.getVerificationRequest(userId1, flowId);
expect(verificationRequest2).toBeInstanceOf(VerificationRequest);
expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString());
expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString());
expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
expect(verificationRequest2.roomId).toBeUndefined();
expect(verificationRequest2.cancelInfo).toBeUndefined();
expect(verificationRequest2.isPassive()).toStrictEqual(false);
expect(verificationRequest2.isReady()).toStrictEqual(false);
expect(verificationRequest2.timedOut()).toStrictEqual(false);
expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]));
expect(verificationRequest2.ourSupportedMethods).toBeUndefined();
expect(verificationRequest2.flowId).toStrictEqual(flowId);
expect(verificationRequest2.isSelfVerification()).toStrictEqual(false);
expect(verificationRequest2.weStarted()).toStrictEqual(false);
expect(verificationRequest2.isDone()).toStrictEqual(false);
expect(verificationRequest2.isCancelled()).toStrictEqual(false);
const verificationRequests = m2.getVerificationRequests(userId1);
expect(verificationRequests).toHaveLength(1);
expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same
});
test('can accept a verification request (`m.key.verification.ready`)', async () => {
// Accept the verification request.
let outgoingVerificationRequest = verificationRequest2.accept();
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
// The request verification is ready.
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()],
}],
};
// Let's send the verification ready to `m1`.
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
test('verification requests are synchronized and automatically updated', () => {
expect(verificationRequest1.isReady()).toStrictEqual(true);
expect(verificationRequest2.isReady()).toStrictEqual(true);
expect(verificationRequest1.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]));
expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]));
expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]));
expect(verificationRequest2.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]));
});
// SAS verification for the second machine.
let sas2;
test('can start a SAS verification (`m.key.verification.start`)', async () => {
// Let's start a SAS verification, from `m2` for example.
[sas2, outgoingVerificationRequest] = await verificationRequest2.startSas();
expect(sas2).toBeInstanceOf(Sas);
expect(sas2.userId.toString()).toStrictEqual(userId2.toString());
expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString());
expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString());
expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
expect(sas2.flowId).toStrictEqual(flowId);
expect(sas2.roomId).toBeUndefined();
expect(sas2.supportsEmoji()).toStrictEqual(false);
expect(sas2.startedFromRequest()).toStrictEqual(true);
expect(sas2.isSelfVerification()).toStrictEqual(false);
expect(sas2.haveWeConfirmed()).toStrictEqual(false);
expect(sas2.hasBeenAccepted()).toStrictEqual(false);
expect(sas2.cancelInfo()).toBeUndefined();
expect(sas2.weStarted()).toStrictEqual(false);
expect(sas2.timedOut()).toStrictEqual(false);
expect(sas2.canBePresented()).toStrictEqual(false);
expect(sas2.isDone()).toStrictEqual(false);
expect(sas2.isCancelled()).toStrictEqual(false);
expect(sas2.emoji()).toBeUndefined();
expect(sas2.emojiIndex()).toBeUndefined();
expect(sas2.decimals()).toBeUndefined();
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()],
}],
};
// Let's send the SAS start to `m1`.
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
// SAS verification for the second machine.
let sas1;
test('can fetch and accept an ongoing SAS verification (`m.key.verification.accept`)', async () => {
// Let's fetch the ongoing SAS verification.
sas1 = await m1.getVerification(userId2, flowId);
expect(sas1).toBeInstanceOf(Sas);
expect(sas1.userId.toString()).toStrictEqual(userId1.toString());
expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString());
expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString());
expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString());
expect(sas1.flowId).toStrictEqual(flowId);
expect(sas1.roomId).toBeUndefined();
expect(sas1.startedFromRequest()).toStrictEqual(true);
expect(sas1.isSelfVerification()).toStrictEqual(false);
expect(sas1.haveWeConfirmed()).toStrictEqual(false);
expect(sas1.hasBeenAccepted()).toStrictEqual(false);
expect(sas1.cancelInfo()).toBeUndefined();
expect(sas1.weStarted()).toStrictEqual(true);
expect(sas1.timedOut()).toStrictEqual(false);
expect(sas1.canBePresented()).toStrictEqual(false);
expect(sas1.isDone()).toStrictEqual(false);
expect(sas1.isCancelled()).toStrictEqual(false);
expect(sas1.emoji()).toBeUndefined();
expect(sas1.emojiIndex()).toBeUndefined();
expect(sas1.decimals()).toBeUndefined();
// Let's accept thet SAS start request.
let outgoingVerificationRequest = sas1.accept();
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()],
}],
};
// Let's send the SAS accept to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
test('emojis are supported by both sides', () => {
expect(sas1.supportsEmoji()).toStrictEqual(true);
expect(sas2.supportsEmoji()).toStrictEqual(true);
});
test('one side sends verification key (`m.key.verification.key`)', async () => {
// Let's send the verification keys from `m2` to `m1`.
const outgoingRequests = await m2.outgoingRequests();
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
const toDeviceRequestId = toDeviceRequest.id;
const toDeviceRequestType = toDeviceRequest.type;
toDeviceRequest = JSON.parse(toDeviceRequest.body);
expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: toDeviceRequest.event_type,
content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()],
}],
};
// Let's send te SAS key to `m1`.
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
m2.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}');
});
test('other side sends back verification key (`m.key.verification.key`)', async () => {
// Let's send the verification keys from `m1` to `m2`.
const outgoingRequests = await m1.outgoingRequests();
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
const toDeviceRequestId = toDeviceRequest.id;
const toDeviceRequestType = toDeviceRequest.type;
toDeviceRequest = JSON.parse(toDeviceRequest.body);
expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: toDeviceRequest.event_type,
content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()],
}],
};
// Let's send te SAS key to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}');
});
test('emojis match from both sides', () => {
const emojis1 = sas1.emoji();
const emojiIndexes1 = sas1.emojiIndex();
const emojis2 = sas2.emoji();
const emojiIndexes2 = sas2.emojiIndex();
expect(emojis1).toHaveLength(7);
expect(emojiIndexes1).toHaveLength(emojis1.length);
expect(emojis2).toHaveLength(emojis1.length);
expect(emojiIndexes2).toHaveLength(emojis1.length);
const isEmoji = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/;
for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(emojis1, emojiIndexes1, emojis2, emojiIndexes2)) {
expect(emoji1).toBeInstanceOf(Emoji);
expect(emoji1.symbol).toMatch(isEmoji);
expect(emoji1.description).toBeTruthy();
expect(emojiIndex1).toBeGreaterThanOrEqual(0);
expect(emojiIndex1).toBeLessThanOrEqual(63);
expect(emoji2).toBeInstanceOf(Emoji);
expect(emoji2.symbol).toStrictEqual(emoji1.symbol);
expect(emoji2.description).toStrictEqual(emoji1.description);
expect(emojiIndex2).toStrictEqual(emojiIndex1);
}
});
test('decimals match from both sides', () => {
const decimals1 = sas1.decimals();
const decimals2 = sas2.decimals();
expect(decimals1).toHaveLength(3);
expect(decimals2).toHaveLength(decimals1.length);
const isDecimal = /^[0-9]{4}$/;
for (const [decimal1, decimal2] of zip(decimals1, decimals2)) {
expect(decimal1.toString()).toMatch(isDecimal);
expect(decimal2).toStrictEqual(decimal1);
}
});
test('can confirm keys match (`m.key.verification.mac`)', async () => {
// `m1` confirms.
const [outgoingVerificationRequests, signatureUploadRequest] = await sas1.confirm();
expect(signatureUploadRequest).toBeUndefined();
expect(outgoingVerificationRequests).toHaveLength(1);
let outgoingVerificationRequest = outgoingVerificationRequests[0];
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()],
}],
};
// Let's send te SAS confirmation to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
test('can confirm back keys match (`m.key.verification.done`)', async () => {
// `m2` confirms.
const [outgoingVerificationRequests, signatureUploadRequest] = await sas2.confirm();
expect(signatureUploadRequest).toBeUndefined();
expect(outgoingVerificationRequests).toHaveLength(2);
// `.mac`
{
let outgoingVerificationRequest = outgoingVerificationRequests[0];
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()],
}],
};
// Let's send te SAS confirmation to `m1`.
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
}
// `.done`
{
let outgoingVerificationRequest = outgoingVerificationRequests[1];
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()],
}],
};
// Let's send te SAS done to `m1`.
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
}
});
test('can send final done (`m.key.verification.done`)', async () => {
const outgoingRequests = await m1.outgoingRequests();
expect(outgoingRequests).toHaveLength(4);
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
const toDeviceRequestId = toDeviceRequest.id;
const toDeviceRequestType = toDeviceRequest.type;
toDeviceRequest = JSON.parse(toDeviceRequest.body);
expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.done');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: toDeviceRequest.event_type,
content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()],
}],
};
// Let's send te SAS key to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
m1.markRequestAsSent(toDeviceRequestId, toDeviceRequestType, '{}');
});
test('can see if verification is done', () => {
expect(verificationRequest1.isDone()).toStrictEqual(true);
expect(verificationRequest2.isDone()).toStrictEqual(true);
expect(sas1.isDone()).toStrictEqual(true);
expect(sas2.isDone()).toStrictEqual(true);
});
});
describe('QR Code', () => {
if (undefined === Qr) {
// qrcode supports is not enabled
console.info('qrcode support is disabled, skip the associated test suite');
return;
}
// First Olm machine.
let m1;
// Second Olm machine.
let m2;
beforeAll(async () => {
m1 = await machine(userId1, deviceId1);
m2 = await machine(userId2, deviceId2);
});
// Verification request for `m1`.
let verificationRequest1;
// The flow ID.
let flowId;
test('can request verification (`m.key.verification.request`)', async () => {
// Make `m1` and `m2` be aware of each other.
{
await addMachineToMachine(m2, m1);
await addMachineToMachine(m1, m2);
}
// Pick the device we want to start the verification with.
const device2 = await m1.getDevice(userId2, deviceId2);
expect(device2).toBeInstanceOf(Device);
let outgoingVerificationRequest;
// Request a verification from `m1` to `device2`.
[verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification([
VerificationMethod.QrCodeScanV1, // by default
VerificationMethod.QrCodeShowV1, // the one we add
]);
expect(verificationRequest1).toBeInstanceOf(VerificationRequest);
expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString());
expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString());
expect(verificationRequest1.otherDeviceId).toBeUndefined();
expect(verificationRequest1.roomId).toBeUndefined();
expect(verificationRequest1.cancelInfo).toBeUndefined();
expect(verificationRequest1.isPassive()).toStrictEqual(false);
expect(verificationRequest1.isReady()).toStrictEqual(false);
expect(verificationRequest1.timedOut()).toStrictEqual(false);
expect(verificationRequest1.theirSupportedMethods).toBeUndefined();
expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeShowV1]));
expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/);
expect(verificationRequest1.isSelfVerification()).toStrictEqual(false);
expect(verificationRequest1.weStarted()).toStrictEqual(true);
expect(verificationRequest1.isDone()).toStrictEqual(false);
expect(verificationRequest1.isCancelled()).toStrictEqual(false);
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()],
}]
};
// Let's send the verification request to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
flowId = verificationRequest1.flowId;
});
// Verification request for `m2`.
let verificationRequest2;
test('can fetch received request verification', async () => {
// Oh, a new verification request.
verificationRequest2 = m2.getVerificationRequest(userId1, flowId);
expect(verificationRequest2).toBeInstanceOf(VerificationRequest);
expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString());
expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString());
expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
expect(verificationRequest2.roomId).toBeUndefined();
expect(verificationRequest2.cancelInfo).toBeUndefined();
expect(verificationRequest2.isPassive()).toStrictEqual(false);
expect(verificationRequest2.isReady()).toStrictEqual(false);
expect(verificationRequest2.timedOut()).toStrictEqual(false);
expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]));
expect(verificationRequest2.ourSupportedMethods).toBeUndefined();
expect(verificationRequest2.flowId).toStrictEqual(flowId);
expect(verificationRequest2.isSelfVerification()).toStrictEqual(false);
expect(verificationRequest2.weStarted()).toStrictEqual(false);
expect(verificationRequest2.isDone()).toStrictEqual(false);
expect(verificationRequest2.isCancelled()).toStrictEqual(false);
const verificationRequests = m2.getVerificationRequests(userId1);
expect(verificationRequests).toHaveLength(1);
expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same
});
test('can accept a verification request with methods (`m.key.verification.ready`)', async () => {
// Accept the verification request.
let outgoingVerificationRequest = verificationRequest2.acceptWithMethods([
VerificationMethod.QrCodeScanV1, // by default
VerificationMethod.QrCodeShowV1, // the one we add
]);
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
// The request verification is ready.
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()],
}],
};
// Let's send the verification ready to `m1`.
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
test('verification requests are synchronized and automatically updated', () => {
expect(verificationRequest1.isReady()).toStrictEqual(true);
expect(verificationRequest2.isReady()).toStrictEqual(true);
expect(verificationRequest1.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]));
expect(verificationRequest1.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]));
expect(verificationRequest2.theirSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]));
expect(verificationRequest2.ourSupportedMethods).toEqual(expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]));
});
// QR verification for the second machine.
let qr2;
test('can generate a QR code', async () => {
qr2 = await verificationRequest2.generateQrCode();
expect(qr2).toBeInstanceOf(Qr);
expect(qr2.hasBeenScanned()).toStrictEqual(false);
expect(qr2.hasBeenConfirmed()).toStrictEqual(false);
expect(qr2.userId.toString()).toStrictEqual(userId2.toString());
expect(qr2.otherUserId.toString()).toStrictEqual(userId1.toString());
expect(qr2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
expect(qr2.weStarted()).toStrictEqual(false);
expect(qr2.cancelInfo()).toBeUndefined();
expect(qr2.isDone()).toStrictEqual(false);
expect(qr2.isCancelled()).toStrictEqual(false);
expect(qr2.isSelfVerification()).toStrictEqual(false);
expect(qr2.reciprocated()).toStrictEqual(false);
expect(qr2.flowId).toMatch(/^[a-f0-9]+$/);
expect(qr2.roomId).toBeUndefined();
});
let qrCodeBytes;
test('can read QR code\'s bytes', async () => {
const qrCodeHeader = 'MATRIX';
const qrCodeVersion = '\x02';
qrCodeBytes = qr2.toBytes();
expect(qrCodeBytes).toHaveLength(122);
expect(qrCodeBytes.slice(0, 7)).toStrictEqual([...qrCodeHeader, ...qrCodeVersion].map(char => char.charCodeAt(0)));
});
test('can render QR code', async () => {
const qrCode = qr2.toQrCode();
expect(qrCode).toBeInstanceOf(QrCode);
// Want to get `canvasBuffer` to render the QR code? Install `npm install canvas` and uncomment the following blocks.
//let canvasBuffer;
{
const buffer = qrCode.renderIntoBuffer();
expect(buffer).toBeInstanceOf(Uint8ClampedArray);
// 45px ⨉ 45px
expect(buffer).toHaveLength(45 * 45);
// 0 for a white pixel, 1 for a black pixel.
expect(buffer.every(p => p == 0 || p == 1)).toStrictEqual(true);
/*
const { Canvas } = require('canvas');
const canvas = new Canvas(55, 55);
const context = canvas.getContext('2d');
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
// New image data, filled with black, transparent pixels.
const imageData = context.createImageData(45, 45);
const data = imageData.data;
const [r, g, b, a] = [0, 1, 2, 3];
for (
let dataNth = 0,
bufferNth = 0;
dataNth < data.length && bufferNth < buffer.length;
dataNth += 4,
bufferNth += 1
) {
data[dataNth + a] = 255;
// White pixel
if (buffer[bufferNth] == 0) {
data[dataNth + r] = 255;
data[dataNth + g] = 255;
data[dataNth + b] = 255;
}
}
context.putImageData(imageData, 5, 5);
canvasBuffer = canvas.toBuffer('image/png');
*/
}
// Want to see the QR code? Uncomment the following block.
/*
{
const fs = require('fs/promises');
const path = require('path');
const os = require('os');
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
const qrCodeFile = path.join(tempDirectory, 'qrcode.png');
console.log(`View the QR code at \`${qrCodeFile}\`.`);
expect(await fs.writeFile(qrCodeFile, canvasBuffer)).toBeUndefined();
}
*/
});
let qr1;
test('can scan a QR code from bytes', async () => {
const scan = QrCodeScan.fromBytes(qrCodeBytes);
expect(scan).toBeInstanceOf(QrCodeScan);
qr1 = await verificationRequest1.scanQrCode(scan);
expect(qr1).toBeInstanceOf(Qr);
expect(qr1.hasBeenScanned()).toStrictEqual(false);
expect(qr1.hasBeenConfirmed()).toStrictEqual(false);
expect(qr1.userId.toString()).toStrictEqual(userId1.toString());
expect(qr1.otherUserId.toString()).toStrictEqual(userId2.toString());
expect(qr1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString());
expect(qr1.weStarted()).toStrictEqual(true);
expect(qr1.cancelInfo()).toBeUndefined();
expect(qr1.isDone()).toStrictEqual(false);
expect(qr1.isCancelled()).toStrictEqual(false);
expect(qr1.isSelfVerification()).toStrictEqual(false);
expect(qr1.reciprocated()).toStrictEqual(true);
expect(qr1.flowId).toMatch(/^[a-f0-9]+$/);
expect(qr1.roomId).toBeUndefined();
});
test('can start a QR verification/reciprocate (`m.key.verification.start`)', async () => {
let outgoingVerificationRequest = qr1.reciprocate();
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start');
const toDeviceEvents = {
events: [{
sender: userId1.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()],
}]
};
// Let's send the verification request to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
test('can confirm QR code has been scanned', () => {
expect(qr2.hasBeenScanned()).toStrictEqual(true);
});
test('can confirm scanning (`m.key.verification.done`)', async () => {
let outgoingVerificationRequest = qr2.confirmScanning();
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body);
expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done');
const toDeviceEvents = {
events: [{
sender: userId2.toString(),
type: outgoingVerificationRequest.event_type,
content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()],
}]
};
// Let's send the verification request to `m2`.
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
});
test('can confirm QR code has been confirmed', () => {
expect(qr2.hasBeenConfirmed()).toStrictEqual(true);
});
});
});
describe('VerificationMethod', () => {
test('has the correct variant values', () => {
expect(VerificationMethod.SasV1).toStrictEqual(0);
expect(VerificationMethod.QrCodeScanV1).toStrictEqual(1);
expect(VerificationMethod.QrCodeShowV1).toStrictEqual(2);
expect(VerificationMethod.ReciprocateV1).toStrictEqual(3);
});
});
@@ -0,0 +1,36 @@
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();
});
});
describe('VerificationState', () => {
test('has the correct variant values', () => {
expect(VerificationState.Trusted).toStrictEqual(0);
expect(VerificationState.Untrusted).toStrictEqual(1);
expect(VerificationState.UnknownDevice).toStrictEqual(2);
});
});
@@ -0,0 +1,10 @@
const { HistoryVisibility } = require('../pkg/matrix_sdk_crypto_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,80 @@
const { DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest } = require('../pkg/matrix_sdk_crypto_js');
function* zip(...arrays) {
const len = Math.min(...arrays.map((array) => array.length));
for (let nth = 0; nth < len; ++nth) {
yield [...arrays.map((array) => array.at(nth))]
}
}
// Add a machine to another machine, i.e. be sure a machine knows
// another exists.
async function addMachineToMachine(machineToAdd, machine) {
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = new Map();
const unusedFallbackKeys = new Set();
const receiveSyncChanges = JSON.parse(await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys));
expect(receiveSyncChanges).toEqual({});
const outgoingRequests = await machineToAdd.outgoingRequests();
expect(outgoingRequests).toHaveLength(2);
let keysUploadRequest;
// Read the `KeysUploadRequest`.
{
expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest);
expect(outgoingRequests[0].id).toBeDefined();
expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload);
const body = JSON.parse(outgoingRequests[0].body);
expect(body.device_keys).toBeDefined();
expect(body.one_time_keys).toBeDefined();
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload
const hypothetical_response = JSON.stringify({
"one_time_key_counts": {
"curve25519": 10,
"signed_curve25519": 20
}
});
const marked = await machineToAdd.markRequestAsSent(outgoingRequests[0].id, outgoingRequests[0].type, hypothetical_response);
expect(marked).toStrictEqual(true);
keysUploadRequest = body;
}
{
expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest);
let [signingKeysUploadRequest, _] = await machineToAdd.bootstrapCrossSigning(true);
signingKeysUploadRequest = JSON.parse(signingKeysUploadRequest.body);
// Let's forge a `KeysQuery`'s response.
let keyQueryResponse = {
device_keys: {},
master_keys: {},
self_signing_keys: {},
user_signing_keys: {},
};
const userId = machineToAdd.userId.toString();
const deviceId = machineToAdd.deviceId.toString();
keyQueryResponse.device_keys[userId] = {};
keyQueryResponse.device_keys[userId][deviceId] = keysUploadRequest.device_keys;
keyQueryResponse.master_keys[userId] = signingKeysUploadRequest.master_key;
keyQueryResponse.self_signing_keys[userId] = signingKeysUploadRequest.self_signing_key;
keyQueryResponse.user_signing_keys[userId] = signingKeysUploadRequest.user_signing_key;
const marked = await machine.markRequestAsSent(outgoingRequests[1].id, outgoingRequests[1].type, JSON.stringify(keyQueryResponse));
expect(marked).toStrictEqual(true);
}
}
module.exports = {
zip,
addMachineToMachine,
};
@@ -0,0 +1,181 @@
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,569 @@
const {
CrossSigningStatus,
DecryptedRoomEvent,
DeviceId,
DeviceKeyId,
DeviceLists,
EncryptionSettings,
InboundGroupSession,
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
MaybeSignature,
OlmMachine,
OwnUserIdentity,
RequestType,
RoomId,
SignatureUploadRequest,
ToDeviceRequest,
UserId,
VerificationRequest,
VerificationState,
} = require('../pkg/matrix_sdk_crypto_js');
const { addMachineToMachine } = require('./helper');
require('fake-indexeddb/auto');
describe(OlmMachine.name, () => {
test('can be instantiated with the async initializer', async () => {
expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine);
});
test('can be instantiated with a store', async () => {
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
let store_name = 'hello';
let store_passphrase = 'world';
// Creating a new Olm machine.
expect(await new OlmMachine(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();
expect(databases).toHaveLength(2);
expect(databases).toStrictEqual([
{ name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 },
{ name: `${store_name}::matrix-sdk-crypto`, version: 1 },
]);
// Creating a new Olm machine, with the stored state.
expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase)).toBeInstanceOf(OlmMachine);
// Same number of databases.
expect(await indexedDB.databases()).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 new OlmMachine(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 new OlmMachine(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 new OlmMachine(new_user || user, new_device || device);
}
test('can read user ID', async () => {
expect((await machine()).userId.toString()).toStrictEqual(user.toString());
});
test('can read device ID', async () => {
expect((await machine()).deviceId.toString()).toStrictEqual(device.toString());
});
test('can read identity keys', async () => {
const identityKeys = (await machine()).identityKeys;
expect(identityKeys.ed25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
expect(identityKeys.curve25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
});
test('can read display name', async () => {
expect(await machine().displayName).toBeUndefined();
});
test('can read tracked users', async () => {
const trackedUsers = (await machine()).trackedUsers();
expect(trackedUsers).toBeInstanceOf(Set);
expect(trackedUsers.size).toStrictEqual(0);
});
test('can update tracked users', async () => {
const m = await machine();
expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined);
});
test('can receive sync changes', async () => {
const m = await machine();
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = 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);
const body = JSON.parse(outgoingRequests[0].body);
expect(body.device_keys).toBeDefined();
expect(body.one_time_keys).toBeDefined();
}
{
expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest);
expect(outgoingRequests[1].id).toBeDefined();
expect(outgoingRequests[1].type).toStrictEqual(RequestType.KeysQuery);
const body = JSON.parse(outgoingRequests[1].body);
expect(body.timeout).toBeDefined();
expect(body.device_keys).toBeDefined();
expect(body.token).toBeDefined();
}
});
describe('setup workflow to mark requests as sent', () => {
let m;
let ougoingRequests;
beforeAll(async () => {
m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID'));
const toDeviceEvents = JSON.stringify({});
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = 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 = JSON.parse(await m.shareRoomKey(room, other_users, new EncryptionSettings()));
expect(requests).toHaveLength(1);
expect(requests[0].event_type).toBeDefined();
expect(requests[0].txn_id).toBeDefined();
expect(requests[0].messages).toBeDefined();
expect(requests[0].messages['@example:localhost']).toBeDefined();
});
let encrypted;
test('can encrypt an event', async () => {
encrypted = JSON.parse(await m.encryptRoomEvent(
room,
'm.room.message',
JSON.stringify({
"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.verificationState).toStrictEqual(VerificationState.Trusted);
});
});
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 m;
let exportedRoomKeys;
test('can export room keys', async () => {
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.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);
};
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),
});
});
});
});
@@ -0,0 +1,35 @@
const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../pkg/matrix_sdk_crypto_js');
describe('RequestType', () => {
test('has the correct variant values', () => {
expect(RequestType.KeysUpload).toStrictEqual(0);
expect(RequestType.KeysQuery).toStrictEqual(1);
expect(RequestType.KeysClaim).toStrictEqual(2);
expect(RequestType.ToDevice).toStrictEqual(3);
expect(RequestType.SignatureUpload).toStrictEqual(4);
expect(RequestType.RoomMessage).toStrictEqual(5);
expect(RequestType.KeysBackup).toStrictEqual(6);
});
});
for (const [request, requestType] of [
[KeysUploadRequest, RequestType.KeysUpload],
[KeysQueryRequest, RequestType.KeysQuery],
[KeysClaimRequest, RequestType.KeysClaim],
[ToDeviceRequest, RequestType.ToDevice],
[SignatureUploadRequest, RequestType.SignatureUpload],
[RoomMessageRequest, RequestType.RoomMessage],
[KeysBackupRequest, RequestType.KeysBackup],
]) {
describe(request.name, () => {
test('can be instantiated', () => {
const r = new (request)('foo', '{"bar": "baz"}');
expect(r).toBeInstanceOf(request);
expect(r.id).toStrictEqual('foo');
expect(r.body).toStrictEqual('{"bar": "baz"}');
expect(r.type).toStrictEqual(requestType);
});
})
}
@@ -0,0 +1,31 @@
const { DeviceLists, UserId } = require('../pkg/matrix_sdk_crypto_js');
describe(DeviceLists.name, () => {
test('can be empty', () => {
const empty = new DeviceLists();
expect(empty.isEmpty()).toStrictEqual(true);
expect(empty.changed).toHaveLength(0);
expect(empty.left).toHaveLength(0);
});
test('can be coerced empty', () => {
const empty = new DeviceLists([], []);
expect(empty.isEmpty()).toStrictEqual(true);
expect(empty.changed).toHaveLength(0);
expect(empty.left).toHaveLength(0);
});
test('returns the correct `changed` and `left`', () => {
const list = new DeviceLists([new UserId('@foo:bar.org')], [new UserId('@baz:qux.org')]);
expect(list.isEmpty()).toStrictEqual(false);
expect(list.changed).toHaveLength(1);
expect(list.changed[0].toString()).toStrictEqual('@foo:bar.org');
expect(list.left).toHaveLength(1);
expect(list.left[0].toString()).toStrictEqual('@baz:qux.org');
});
});
@@ -0,0 +1,84 @@
const { Tracing, LoggerLevel, OlmMachine, UserId, DeviceId } = require('../pkg/matrix_sdk_crypto_js');
describe('LoggerLevel', () => {
test('has the correct variant values', () => {
expect(LoggerLevel.Trace).toStrictEqual(0);
expect(LoggerLevel.Debug).toStrictEqual(1);
expect(LoggerLevel.Info).toStrictEqual(2);
expect(LoggerLevel.Warn).toStrictEqual(3);
expect(LoggerLevel.Error).toStrictEqual(4);
});
});
describe(Tracing.name, () => {
if (Tracing.isAvailable()) {
let tracing = new Tracing(LoggerLevel.Debug);
test('can installed several times', () => {
new Tracing(LoggerLevel.Debug);
new Tracing(LoggerLevel.Warn);
new Tracing(LoggerLevel.Debug);
});
const originalConsoleDebug = console.debug;
for (const [testName, testPreState, testPostState, expectedGotcha] of [
[
'can log something',
() => {},
() => {},
true,
],
[
'can change the logger level',
() => { tracing.minLevel = LoggerLevel.Warn },
() => { tracing.minLevel = LoggerLevel.Debug },
false,
],
[
'can be turned off',
() => { tracing.turnOff() },
() => {},
false,
],
[
'can be turned on',
() => { tracing.turnOn() },
() => {},
true,
],
// This one *must* be the last. We are turning tracing off
// again for the other tests.
[
'can be turned off',
() => { tracing.turnOff() },
() => {},
false,
],
]) {
test(testName, async () => {
testPreState();
let gotcha = false;
console.debug = (msg) => {
gotcha = true;
expect(msg).not.toHaveLength(0);
};
// Do something that emits a `DEBUG` log.
await new OlmMachine(new UserId('@alice:example.org'), new DeviceId('foo'));
console.debug = originalConsoleDebug;
testPostState();
expect(gotcha).toStrictEqual(expectedGotcha);
});
}
} else {
test('cannot be constructed', () => {
expect(() => { new Tracing(LoggerLevel.Error) }).toThrow();
});
}
});
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"strict": true
},
"typedocOptions": {
"entryPoints": ["pkg/matrix_sdk_crypto_js.d.ts"],
"out": "docs",
"readme": "README.md",
}
}
@@ -0,0 +1,7 @@
/node_modules
/package-lock.json
/index.js
/index.d.ts
/matrix-sdk-crypto.*.node
/docs/*
*.tgz
@@ -0,0 +1,8 @@
src/
tests/
Cargo.toml
build.rs
*.node
*.tgz
tsconfig.json
cliff.toml
@@ -0,0 +1,32 @@
# Matrix-Rust-SDK Node.js Bindings
## 0.1.0-beta.1 - 2022-07-14
- Fixing broken download link, [#842](https://github.com/matrix-org/matrix-rust-sdk/issues/842)
## 0.1.0-beta.0 - 2022-07-12
Welcome to the first release of `matrix-sdk-crypto-nodejs`. This is a
Node.js binding for the Rust `matrix-sdk-crypto` library. This is a
no-network-IO implementation of a state machine, named `OlmMachine`,
that handles E2EE (End-to-End Encryption) for Matrix clients.
The goal of this binding is _not_ to cover the entirety of the
`matrix-sdk-crypto` API, but only what's required to build Matrix bots
or Matrix bridges (i.e. to connect different networks together via the
Matrix protocol).
This project replaces and deprecates a previous project, with the same
name and same goals, inside [the `matrix-rust-sdk-bindings`
repository](https://github.com/matrix-org/matrix-rust-sdk-bindings),
with the NPM package name `@turt2live/matrix-sdk-crypto-nodejs`. The
The new official package name is
`@matrix-org/matrix-sdk-crypto-nodejs`.
Note: All bindings are now part of [the `matrix-rust-sdk`
repository](https://github.com/matrix-org/matrix-rust-sdk) (see the
`bindings/` root directory).
[A documentation is available inside the new
`matrix-sdk-crypto-nodejs`
project](https://github.com/matrix-org/matrix-rust-sdk/tree/0bde5ccf38f8cda3865297a2d12ddcdaf4b80ca7/bindings/matrix-sdk-crypto-nodejs).
@@ -0,0 +1,42 @@
[package]
authors = ["Ivan Enderlin <ivane@element.io>"]
description = "Matrix encryption library, for NodeJS"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "Apache-2.0"
name = "matrix-sdk-crypto-nodejs"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.60"
version = "0.6.0"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
[lib]
crate-type = ["cdylib"]
[features]
default = []
qrcode = ["matrix-sdk-crypto/qrcode"]
tracing = ["dep:tracing-subscriber"]
[dependencies]
matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] }
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
matrix-sdk-sled = { version = "0.2.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] }
ruma = { version = "0.7.0", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] }
napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = "2.9.1"
serde_json = "1.0.79"
http = "0.2.6"
zeroize = "1.3.0"
tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true }
[dependencies.vodozemac]
version = "0.3.0"
features = ["js"]
[build-dependencies]
napi-build = "2.0.0"
+208
View File
@@ -0,0 +1,208 @@
# `matrix-sdk-crypto-nodejs`
Welcome to the [Node.js] binding for the Rust [`matrix-sdk-crypto`]
library! This binding is part of the [`matrix-rust-sdk`] project,
which is a library implementation of a [Matrix] client-server.
`matrix-sdk-crypto-nodejs` 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
Just add the latest release to your `package.json`:
```sh
$ npm install --save @matrix-org/matrix-sdk-crypto-nodejs
```
When installing, NPM will download the corresponding prebuilt Rust library for your current host system. The following are supported:
<table>
<thead>
<tr>
<th>Platform</th>
<th>Architecture</th>
<th>Triple</th>
<th>Prebuilt available</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="5">Linux</td>
<td rowspan="2"><code>aarch</code></td>
<td><code>aarch64-unknown-linux-gnu</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>arm-unknown-linux-gnueabihf</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="3"><code>amd</code></td>
<td><code>x86_64-unknown-linux-gnu</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>x86_64-unknown-linux-musl</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>i686-unknown-linux-gnu</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="2">macOS</td>
<td><code>aarch</code></td>
<td><code>arch64-apple-darwin</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>amd</code></td>
<td><code>x86_64-apple-darwin</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="3">Windows</td>
<td><code>aarch</code></td>
<td><code>aarch64-pc-windows-msvc</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="2"><code>amd</code></td>
<td><code>x86_64-pc-windows-msvc</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>i686-pc-windows-msvc</code></td>
<td>✅</td>
</tr>
</tbody>
</table>
## Development
This Node.js binding is written in [Rust]. To build this binding, 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).
The binding is compatible with, and tested against, the Node.js
versions that are in “current”, “active” or “maintenance” states,
according to [the Node.js Releases
Page](https://nodejs.org/en/about/releases/), _and_ which are
compatible with [NAPI v6 (Node.js
API)](https://nodejs.org/api/n-api.html#node-api-version-matrix). It
means that this binding will work with the following versions: 14.0.0,
16.0.0 and 18.0.0.
Once the Rust compiler, Node.js and npm are installed, you can run the
following commands:
```sh
$ npm install --ignore-scripts
$ npm run build
$ npm run test
```
An `index.js`, `index.d.ts` and a `*.node` files should be
generated. At the same level of those files, you can edit a file and
try this:
```javascript
const { OlmMachine } = require('./index.js');
// Let's see what we can do.
```
The `OlmMachine` state machine works in a push/pull manner:
* You push state changes and events retrieved from a Matrix homeserver
`/sync` response, into the state machine,
* You pull requests that you will need to send back to the homeserver
out of the state machine.
```javascript
const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists } = require('./index.js');
async function main() {
// Define a user ID.
const alice = new UserId('@alice:example.org');
// Define a device ID.
const device = new DeviceId('DEVICEID');
// Let's create the `OlmMachine` state machine.
const machine = await OlmMachine.initialize(alice, device);
// Let's pretend we have received changes and events from a
// `/sync` endpoint of a Matrix homeserver, …
const toDeviceEvents = "{}"; // JSON-encoded
const changedDevices = new DeviceLists();
const oneTimeKeyCounts = {};
const unusedFallbackKeys = [];
// … and push them into the state machine.
const decryptedToDevice = await machine.receiveSyncChanges(
toDeviceEvents,
changedDevices,
oneTimeKeyCounts,
unusedFallbackKeys,
);
// Now, let's pull requests that we need to send out to the Matrix
// homeserver.
const outgoingRequests = await machine.outgoingRequests();
// To complete the workflow, send the requests here out and call
// `machine.markRequestAsSent`.
}
main();
```
### With tracing (experimental)
If you want to enable [tracing](https://tracing.rs), i.e. to get the
logs, you should re-compile the extension with the `tracing` feature
turned on:
```sh
$ npm run build -- --features tracing
```
Now, you can use the `MATRIX_LOG` environment variable to tweak the log filtering, such as:
```sh
$ MATRIX_LOG=debug npm run test
```
See
[`tracing-subscriber`](https://tracing.rs/tracing_subscriber/index.html)
to learn more about the `RUST_LOG`/`MATRIX_LOG` environment variable.
## Documentation
[The documentation can be found
online](https://matrix-org.github.io/matrix-rust-sdk/bindings/matrix-sdk-crypto-nodejs/).
To generate the documentation locally, please run the following
command:
```sh
$ npm run doc
```
The documentation is generated in the `./docs` directory.
[Node.js]: https://nodejs.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,3 @@
fn main() {
napi_build::setup();
}
@@ -0,0 +1,61 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Matrix SDK Crypto Node.js Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | filter(attribute="scope", value="crypto-nodejs") | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.id | truncate(length=7, end="") }}{% if commit.breaking %} [**breaking**] {% endif %}: {{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^test", group = "Testing"},
{ message = "^doc", group = "Documentation"},
{ message = "^refactor", group = "Refactoring"},
{ message = "^ci", group = "Continuous Integration"},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
@@ -0,0 +1,113 @@
const { DownloaderHelper } = require('node-downloader-helper');
const { version } = require("./package.json");
const { platform, arch } = process
const DOWNLOADS_BASE_URL = "https://github.com/matrix-org/matrix-rust-sdk/releases/download";
const CURRENT_VERSION = `matrix-sdk-crypto-nodejs-v${version}`;
const byteHelper = function (value) {
if (value === 0) {
return '0 b';
}
const units = ['b', 'kB', 'MB', 'GB', 'TB'];
const number = Math.floor(Math.log(value) / Math.log(1024));
return (value / Math.pow(1024, Math.floor(number))).toFixed(1) + ' ' +
units[number];
};
function download_lib(libname) {
let startTime = new Date();
const url = `${DOWNLOADS_BASE_URL}/${CURRENT_VERSION}/${libname}`;
console.info(`Downloading lib ${libname} from ${url}`);
const dl = new DownloaderHelper(url, __dirname, {
override: true,
});
dl.on('end', () => console.info('Download Completed'));
dl.on('error', (err) => console.info('Download Failed', err));
dl.on('progress', stats => {
const progress = stats.progress.toFixed(1);
const speed = byteHelper(stats.speed);
const downloaded = byteHelper(stats.downloaded);
const total = byteHelper(stats.total);
// print every one second (`progress.throttled` can be used instead)
const currentTime = new Date();
const elaspsedTime = currentTime - startTime;
if (elaspsedTime > 1000) {
startTime = currentTime;
console.info(`${speed}/s - ${progress}% [${downloaded}/${total}]`);
}
});
dl.start().catch(err => console.error(err));
}
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
download_lib('matrix-sdk-crypto.win32-x64-msvc.node')
break
case 'ia32':
download_lib('matrix-sdk-crypto.win32-ia32-msvc.node')
break
case 'arm64':
download_lib('matrix-sdk-crypto.win32-arm64-msvc.node')
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
switch (arch) {
case 'x64':
download_lib('matrix-sdk-crypto.darwin-x64.node')
break
case 'arm64':
download_lib('matrix-sdk-crypto.darwin-arm64.node')
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
download_lib('matrix-sdk-crypto.linux-x64-musl.node')
} else {
download_lib('matrix-sdk-crypto.linux-x64-gnu.node')
}
break
case 'arm64':
if (isMusl()) {
throw new Error('Linux for arm64 musl isn\'t support at the moment')
} else {
download_lib('matrix-sdk-crypto.linux-arm64-gnu.node')
}
break
case 'arm':
download_lib('matrix-sdk-crypto.linux-arm-gnueabihf.node')
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
@@ -0,0 +1,34 @@
{
"name": "@matrix-org/matrix-sdk-crypto-nodejs",
"version": "0.1.0-beta.1",
"main": "index.js",
"types": "index.d.ts",
"napi": {
"name": "matrix-sdk-crypto",
"triples": {
"additional": [
"aarch64-apple-darwin"
]
}
},
"license": "Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.9.0",
"jest": "^28.1.0",
"typedoc": "^0.22.17",
"yargs-parser": "~21.0.1"
},
"engines": {
"node": ">= 14"
},
"scripts": {
"release-build": "napi build --platform --release --strip",
"build": "napi build --platform",
"postinstall": "node download-lib.js",
"test": "jest --verbose --testTimeout 10000",
"doc": "typedoc --tsconfig ."
},
"dependencies": {
"node-downloader-helper": "^2.1.1"
}
}
@@ -0,0 +1,126 @@
use std::{
io::{Cursor, Read},
ops::Deref,
};
use napi::bindgen_prelude::Uint8Array;
use napi_derive::*;
use crate::into_err;
/// A type to encrypt and to decrypt anything that can fit in an
/// `Uint8Array`, usually big buffer.
#[napi]
pub struct Attachment;
#[napi]
impl Attachment {
/// Encrypt the content of the `Uint8Array`.
///
/// It produces an `EncryptedAttachment`, which can be used to
/// retrieve the media encryption information, or the encrypted
/// data.
#[napi]
pub fn encrypt(array: Uint8Array) -> napi::Result<EncryptedAttachment> {
let buffer: &[u8] = array.deref();
let mut cursor = Cursor::new(buffer);
let mut encryptor = matrix_sdk_crypto::AttachmentEncryptor::new(&mut cursor);
let mut encrypted_data = Vec::new();
encryptor.read_to_end(&mut encrypted_data).map_err(into_err)?;
let media_encryption_info = Some(encryptor.finish());
Ok(EncryptedAttachment {
encrypted_data: Uint8Array::new(encrypted_data),
media_encryption_info,
})
}
/// Decrypt an `EncryptedAttachment`.
///
/// The encrypted attachment can be created manually, or from the
/// `encrypt` method.
///
/// **Warning**: The encrypted attachment can be used only
/// **once**! The encrypted data will still be present, but the
/// media encryption info (which contain secrets) will be
/// destroyed. It is still possible to get a JSON-encoded backup
/// by calling `EncryptedAttachment.mediaEncryptionInfo`.
#[napi]
pub fn decrypt(attachment: &mut EncryptedAttachment) -> napi::Result<Uint8Array> {
let media_encryption_info = match attachment.media_encryption_info.take() {
Some(media_encryption_info) => media_encryption_info,
None => {
return Err(napi::Error::from_reason(
"The media encryption info are absent from the given encrypted attachment"
.to_owned(),
))
}
};
let encrypted_data: &[u8] = attachment.encrypted_data.deref();
let mut cursor = Cursor::new(encrypted_data);
let mut decryptor =
matrix_sdk_crypto::AttachmentDecryptor::new(&mut cursor, media_encryption_info)
.map_err(into_err)?;
let mut decrypted_data = Vec::new();
decryptor.read_to_end(&mut decrypted_data).map_err(into_err)?;
Ok(Uint8Array::new(decrypted_data))
}
}
/// An encrypted attachment, usually created from `Attachment.encrypt`.
#[napi]
pub struct EncryptedAttachment {
media_encryption_info: Option<matrix_sdk_crypto::MediaEncryptionInfo>,
/// The actual encrypted data.
pub encrypted_data: Uint8Array,
}
#[napi]
impl EncryptedAttachment {
/// Create a new encrypted attachment manually.
///
/// It needs encrypted data, stored in an `Uint8Array`, and a
/// [media encryption
/// information](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/struct.MediaEncryptionInfo.html),
/// as a JSON-encoded string.
///
/// The media encryption information aren't stored as a string:
/// they are parsed, validated and fully deserialized.
///
/// See [the specification to learn
/// more](https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes).
#[napi(constructor)]
pub fn new(encrypted_data: Uint8Array, media_encryption_info: String) -> napi::Result<Self> {
Ok(Self {
encrypted_data,
media_encryption_info: Some(
serde_json::from_str(media_encryption_info.as_str()).map_err(into_err)?,
),
})
}
/// Return the media encryption info as a JSON-encoded string. The
/// structure is fully valid.
///
/// If the media encryption info have been consumed already, it
/// will return `null`.
#[napi(getter)]
pub fn media_encryption_info(&self) -> Option<String> {
serde_json::to_string(self.media_encryption_info.as_ref()?).ok()
}
/// Check whether the media encryption info has been consumed by
/// `Attachment.decrypt` already.
#[napi(getter)]
pub fn has_media_encryption_info_been_consumed(&self) -> bool {
self.media_encryption_info.is_none()
}
}
@@ -0,0 +1,133 @@
use std::time::Duration;
use napi::bindgen_prelude::{BigInt, ToNapiValue};
use napi_derive::*;
use crate::events;
/// An encryption algorithm to be used to encrypt messages sent to a
/// room.
#[napi]
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 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"),
}
}
}
/// Settings for an encrypted room.
///
/// This determines the algorithm and rotation periods of a group
/// session.
#[napi]
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.
pub rotation_period: BigInt,
/// How many messages should be sent before changing the session.
pub rotation_period_messages: BigInt,
/// The history visibility of the room when the session was
/// created.
pub history_visibility: events::HistoryVisibility,
/// Should untrusted devices receive the room key, or should they be
/// excluded from the conversation.
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: {
let n: u64 = default.rotation_period.as_micros().try_into().unwrap();
n.into()
},
rotation_period_messages: {
let n = default.rotation_period_msgs;
n.into()
},
history_visibility: default.history_visibility.into(),
only_allow_trusted_devices: default.only_allow_trusted_devices,
}
}
}
#[napi]
impl EncryptionSettings {
/// Create a new `EncryptionSettings` with default values.
#[napi(constructor)]
pub fn new() -> EncryptionSettings {
Self::default()
}
}
impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings {
fn from(value: &EncryptionSettings) -> Self {
Self {
algorithm: value.algorithm.into(),
rotation_period: Duration::from_micros(value.rotation_period.get_u64().1),
rotation_period_msgs: value.rotation_period_messages.get_u64().1,
history_visibility: value.history_visibility.into(),
only_allow_trusted_devices: value.only_allow_trusted_devices,
}
}
}
/// The verification state of the device that sent an event to us.
#[napi]
pub enum VerificationState {
/// The device is trusted.
Trusted,
/// The device is not trusted.
Untrusted,
/// The device is not known to us.
UnknownDevice,
}
impl From<&matrix_sdk_common::deserialized_responses::VerificationState> for VerificationState {
fn from(value: &matrix_sdk_common::deserialized_responses::VerificationState) -> Self {
use matrix_sdk_common::deserialized_responses::VerificationState::*;
match value {
Trusted => Self::Trusted,
Untrusted => Self::Untrusted,
UnknownDevice => Self::UnknownDevice,
}
}
}

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