Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8323efb04 |
+2
-15
@@ -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
|
||||
@@ -15,12 +15,7 @@ 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 +35,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]
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
* @matrix-org/rust
|
||||
@@ -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 }}
|
||||
|
||||
@@ -15,9 +15,8 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -18,6 +18,8 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
MATRIX_SDK_CRYPTO_NODEJS_PATH: bindings/matrix-sdk-crypto-nodejs
|
||||
MATRIX_SDK_CRYPTO_JS_PATH: bindings/matrix-sdk-crypto-js
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
@@ -33,23 +35,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
@@ -61,6 +56,142 @@ jobs:
|
||||
- name: Build library & generate bindings
|
||||
run: target/debug/xtask ci bindings
|
||||
|
||||
lint-js-bindings:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- name: "[m]-crypto-nodejs"
|
||||
path: "bindings/matrix-sdk-crypto-nodejs"
|
||||
- name: "[m]-crypto-js"
|
||||
path: "bindings/matrix-sdk-crypto-js"
|
||||
|
||||
name: lint ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ${{ matrix.path }}
|
||||
run: npm install
|
||||
|
||||
- name: run lint
|
||||
working-directory: ${{ matrix.path }}
|
||||
run: npm run lint
|
||||
|
||||
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, 19.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: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
|
||||
run: npm install
|
||||
|
||||
- name: Build the Node.js binding
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
|
||||
run: npm run release-build
|
||||
|
||||
- name: Test the Node.js binding
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
|
||||
run: npm run test
|
||||
|
||||
# Building in dev-mode and copy lib in failure case
|
||||
- name: Build the Node.js binding in non-release
|
||||
if: failure()
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
|
||||
run: |
|
||||
cp *.node release-mode-lib.node
|
||||
npm run build
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: Failure Files
|
||||
path: |
|
||||
bindings/matrix-sdk-crypto-nodejs/*.node
|
||||
/var/crash/*.crash
|
||||
|
||||
- if: ${{ matrix.build-doc }}
|
||||
name: Build the documentation
|
||||
working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }}
|
||||
run: npm run doc
|
||||
|
||||
test-matrix-sdk-crypto-js:
|
||||
name: 🕸 [m]-crypto-js
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.0
|
||||
|
||||
- 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
|
||||
@@ -72,10 +203,10 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -83,15 +214,8 @@ jobs:
|
||||
- name: Install aarch64-apple-ios target
|
||||
run: rustup target install aarch64-apple-ios
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
|
||||
+104
-32
@@ -34,9 +34,9 @@ jobs:
|
||||
matrix:
|
||||
name:
|
||||
- no-encryption
|
||||
- no-sqlite
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- no-sled
|
||||
- no-encryption-and-sled
|
||||
- sled-cryptostore
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# ... but only save the cache on the main branch
|
||||
# cf https://github.com/Swatinem/rust-cache/issues/95
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/head/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
@@ -89,8 +89,6 @@ jobs:
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
@@ -121,8 +119,6 @@ jobs:
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
@@ -163,20 +159,19 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
- name: Install Rust stable
|
||||
if: matrix.rust == 'stable'
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
@@ -184,7 +179,7 @@ jobs:
|
||||
- name: Test
|
||||
run: |
|
||||
cargo nextest run --workspace \
|
||||
--exclude matrix-sdk-integration-testing --features testing
|
||||
--exclude matrix-sdk-integration-testing --exclude sliding-sync-integration-test
|
||||
|
||||
- name: Test documentation
|
||||
run: |
|
||||
@@ -225,6 +220,9 @@ 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
|
||||
@@ -249,7 +247,7 @@ jobs:
|
||||
|
||||
# ... but only save the cache on the main branch
|
||||
# cf https://github.com/Swatinem/rust-cache/issues/95
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/head/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
@@ -269,6 +267,48 @@ jobs:
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
test-appservice:
|
||||
name: ${{ matrix.os-name }} [m]-appservice
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
os-name: 🐧
|
||||
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
|
||||
- os: macos-latest
|
||||
os-name: 🍏
|
||||
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ matrix.xtask-cachekey }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
target/debug/xtask ci test-appservice
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
@@ -279,9 +319,8 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -298,7 +337,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@v1.13.0
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
@@ -310,21 +349,18 @@ jobs:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
@@ -343,6 +379,39 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- uses: michaelkaye/setup-matrix-synapse@main
|
||||
with:
|
||||
uploadLogs: true
|
||||
httpPort: 8228
|
||||
disableRateLimiting: true
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
|
||||
sliding-sync-integration-tests:
|
||||
name: Sliding Sync Integration test
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
services:
|
||||
@@ -367,7 +436,7 @@ jobs:
|
||||
# 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
|
||||
image: "ghcr.io/matrix-org/sliding-sync:v0.99.0"
|
||||
env:
|
||||
SYNCV3_SERVER: "http://synapse:8008"
|
||||
SYNCV3_SECRET: "SUPER_CI_SECRET"
|
||||
@@ -378,13 +447,14 @@ jobs:
|
||||
# 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
|
||||
# Custom image built from https://github.com/matrix-org/synapse/tree/v1.72.0/docker/complement
|
||||
# with a dummy /complement/ca set
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.72.0
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
ports:
|
||||
- 8008:8008
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -394,12 +464,14 @@ jobs:
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
RUST_LOG: "hyper=trace"
|
||||
@@ -407,4 +479,4 @@ jobs:
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
cargo nextest run -p sliding-sync-integration-test
|
||||
|
||||
@@ -5,11 +5,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -17,9 +12,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# without matrix_sdk=trace, expressions in `trace!` fields are not evaluated
|
||||
# when the `trace!` statement is hit, and thus not covered
|
||||
RUST_LOG: info,matrix_sdk=trace
|
||||
|
||||
jobs:
|
||||
code_coverage:
|
||||
@@ -27,95 +19,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: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install tarpaulin
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: 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
|
||||
run: |
|
||||
cargo tarpaulin --out Xml -e sliding-sync-integration-test
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
@@ -4,21 +4,11 @@ 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:
|
||||
name: All crates
|
||||
@@ -29,42 +19,51 @@ 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
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- 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' }}
|
||||
|
||||
# Keep in sync with xtask docs
|
||||
- name: Build documentation
|
||||
- name: Build rust documentation
|
||||
env:
|
||||
# Work around https://github.com/rust-lang/cargo/issues/10744
|
||||
CARGO_TARGET_APPLIES_TO_HOST: "true"
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
cargo doc --no-deps --features docsrs
|
||||
|
||||
- name: Upload artifact
|
||||
- name: Build `matrix-sdk-crypto-nodejs` doc
|
||||
run: |
|
||||
cd bindings/matrix-sdk-crypto-nodejs
|
||||
npm install
|
||||
npm run build && npm run doc
|
||||
|
||||
- name: Build `matrix-sdk-crypto-js` doc
|
||||
run: |
|
||||
cd bindings/matrix-sdk-crypto-js
|
||||
npm install
|
||||
npm run build && npm run doc
|
||||
|
||||
- name: Prepare the doc hierarchy
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p doc/bindings/matrix-sdk-crypto-nodejs/
|
||||
mkdir -p doc/bindings/matrix-sdk-crypto-js/
|
||||
mv target/doc/* doc/
|
||||
mv bindings/matrix-sdk-crypto-nodejs/docs/* doc/bindings/matrix-sdk-crypto-nodejs/
|
||||
mv bindings/matrix-sdk-crypto-js/docs/* doc/bindings/matrix-sdk-crypto-js/
|
||||
|
||||
- name: Deploy documentation
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./doc/
|
||||
force_orphan: true
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
name: Prepare Crypto-Node.js Release
|
||||
#
|
||||
# This is a helper workflow to craft a new Node.js release, trigger this via
|
||||
# the Github Workflow UI by dispatching it manually. Provide the version, the
|
||||
# matrix-sdk-crypto-nodejs npm package should be set to, and a optionally the
|
||||
# old version (as used in the git tag) this release should be compared to.
|
||||
#
|
||||
# This will then:
|
||||
# 1. bump the npm version to the one you specified
|
||||
# 2. commit that change together with the changelog (if it changed, see below)
|
||||
# 3. create the appropriate tag on that commit
|
||||
# 4. create the Github draft release, including the changes (if given, see below)
|
||||
# 5. push these to a new branch, including tag, triggering the `release-crypto-nodejs` workflow
|
||||
# 6. create a PR to merge these back into `main`
|
||||
#
|
||||
# Additionally, if you provide a tag to comapare this tag to, this will:
|
||||
# 1. create a changelog between the two releases, used for the github release
|
||||
# 2. update the Changelog.md and include it in the commit
|
||||
#
|
||||
# The remaining tasks are done by the release-crypto-nodejs workflow.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'New Node.js SemVer version to create'
|
||||
required: true
|
||||
type: string
|
||||
previous_version:
|
||||
description: 'Create the changelog by comparing to this old SemVer Version (as used in the tag) '
|
||||
type: string
|
||||
|
||||
env:
|
||||
PKG_PATH: "bindings/matrix-sdk-crypto-nodejs"
|
||||
TAG_PREFIX: "matrix-sdk-crypto-nodejs-v"
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: "Preparing crypto-nodejs release tag"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: "${{ env.TAG_PREFIX }}${{ inputs.version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
# Generate changelog since last tag, if given
|
||||
- name: Generate a changelog for upload
|
||||
if: inputs.previous_version
|
||||
uses: orhun/git-cliff-action@v1
|
||||
with:
|
||||
config: "${{ env.PKG_PATH }}/cliff.toml"
|
||||
args: --strip header "${{env.TAG_PREFIX}}${{ inputs.previous_version }}..HEAD"
|
||||
env:
|
||||
GIT_CLIFF_TAG: "Changes ${{ inputs.previous_version }} -> ${{ inputs.version }}"
|
||||
GIT_CLIFF_OUTPUT: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md"
|
||||
|
||||
# Update changelog since last tag, if given
|
||||
- name: Update existing Changelog
|
||||
if: inputs.previous_version
|
||||
uses: orhun/git-cliff-action@v1
|
||||
with:
|
||||
config: "${{ env.PKG_PATH }}/cliff.toml"
|
||||
args: "${{env.TAG_PREFIX}}${{ inputs.previous_version }}..HEAD"
|
||||
env:
|
||||
GIT_CLIFF_TAG: "${{ inputs.version }}"
|
||||
GIT_CLIFF_PREPEND: "${{ env.PKG_PATH }}/CHANGELOG.md"
|
||||
|
||||
- name: Set version
|
||||
id: package_version
|
||||
working-directory: ${{ env.PKG_PATH }}
|
||||
run: npm version ${{ inputs.version }}
|
||||
|
||||
- uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
default_author: github_actions
|
||||
message: "Tagging Crypto-Node.js for release"
|
||||
tag: "${{env.TAG_PREFIX}}${{ inputs.version }}"
|
||||
new_branch: "gh-action/release-${{ env.TAG_PREFIX }}${{ inputs.version }}"
|
||||
push: true
|
||||
add: |
|
||||
${{ env.PKG_PATH }}/package.json
|
||||
${{ env.PKG_PATH }}/CHANGELOG.md
|
||||
|
||||
# if we have generated changes
|
||||
- name: Update Github Release notes
|
||||
if: inputs.previous_version
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }}
|
||||
body_path: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md"
|
||||
|
||||
# no changes, use the default changelog for the body
|
||||
- name: Update Github Release notes
|
||||
if: ${{!inputs.previous_version}}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }}
|
||||
body_path: "${{ env.PKG_PATH }}/CHANGELOG.md"
|
||||
|
||||
# let's create a PR for all this, too
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
title: "Preparing Release ${{ env.TAG_PREFIX }}${{ inputs.version }}"
|
||||
body: |
|
||||
Automatic Pull-Request to merge release ${{ env.TAG_PREFIX }}${{ inputs.version }}
|
||||
|
||||
trigger-release:
|
||||
# and trigger the tagging release workflow
|
||||
uses: 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}}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,136 @@
|
||||
name: Release Crypto-Node.js
|
||||
#
|
||||
# This workflow releases the crypto-bindings for nodejs
|
||||
#
|
||||
# It is triggered when seeing a tag prefixed matching `matrix-sdk-crypto-nodejs-v[0-9]+.*`,
|
||||
# which then build the native bindings for linux, mac and windows via the CI and uploads
|
||||
# them to the corresponding Github Release tag. Once they are finished, this workflow will
|
||||
# package the npm tar.gz and uploads that to the Github Release tag as well, before publishing
|
||||
# it to npmjs.com automatically.
|
||||
#
|
||||
# The usual way to trigger this is by manually triggering the `prep-crypto-nodejs-release`
|
||||
# workflow. See its documentation for instructions how to use it.
|
||||
|
||||
env:
|
||||
PKG_PATH: "bindings/matrix-sdk-crypto-nodejs"
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: 'aarch64-linux-gnu-gcc'
|
||||
CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER: 'i686-linux-gnu-gcc'
|
||||
CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: 'arm-linux-gnueabihf-gcc'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- matrix-sdk-crypto-nodejs-v[0-9]+.*
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "The tag to build with"
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
upload-assets:
|
||||
name: "Upload prebuilt libraries"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# ----------------------------------- Linux
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-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: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- if: ${{ matrix.apt_install }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ${{ matrix.apt_install }}
|
||||
- name: Build lib
|
||||
working-directory: ${{env.PKG_PATH}}
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
npx napi build --platform --release --strip --target ${{ matrix.target }}
|
||||
- name: Upload artifacts to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{env.PKG_PATH}}/*.node
|
||||
|
||||
publish-nodejs-package:
|
||||
name: "Package nodejs package"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- upload-assets
|
||||
steps:
|
||||
# use the given tag
|
||||
- uses: actions/checkout@v3
|
||||
name: "Checking out ${{ inputs.tag }}"
|
||||
if: "${{ inputs.tag }}"
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
# use the default
|
||||
- uses: actions/checkout@v3
|
||||
if: "${{ !inputs.tag }}"
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
- name: Build lib
|
||||
working-directory: ${{env.PKG_PATH}}
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
npm run build
|
||||
npm pack
|
||||
- name: Upload npm package to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{env.PKG_PATH}}/*tgz
|
||||
- name: Publish to npmjs.com
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
package: ${{env.PKG_PATH}}/package.json
|
||||
access: public
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,59 @@
|
||||
# This workflow releases the `matrix-sdk-crypto-js` project.
|
||||
#
|
||||
# It is triggered when a new tag appears that matches
|
||||
# `matrix-sdk-crypto-js-[0-9]+.*`. This workflow builds the package
|
||||
# for the bindings, runs its tests to ensure everything is still
|
||||
# correct, and publishes the package on NPM and on a newly created
|
||||
# Github release.
|
||||
|
||||
|
||||
name: Release `crypto-js`
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
PKG_PATH: "bindings/matrix-sdk-crypto-js"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- matrix-sdk-crypto-js-[0-9]+.*
|
||||
|
||||
jobs:
|
||||
publish-matrix-sdk-crypto-js:
|
||||
name: Publish 🕸 [m]-crypto-js
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.0
|
||||
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ${{ env.PKG_PATH }}
|
||||
run: npm install
|
||||
|
||||
- name: Publish the WebAssembly + JavaScript binding (imply building + testing)
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
package: ${{env.PKG_PATH}}/package.json
|
||||
access: public
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create the Github release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{ env.PKG_PATH }}/pkg/matrix-org-matrix-sdk-crypto-js-*.tgz
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
key: |
|
||||
${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }}
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
- name: Install rust stable toolchain
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
+14
-28
@@ -1,33 +1,19 @@
|
||||
[default]
|
||||
extend-ignore-re = [
|
||||
# base 58 strings with spaces every four chars.
|
||||
# this would also match regular sentence parts with eight or more words of
|
||||
# exactly four characters in row, but that doesn't really happen.
|
||||
"[1-9A-Za-z]{4}( [1-9A-Za-z]{4}){7,}",
|
||||
# some heuristics for base64 strings with no false matches found at the
|
||||
# time of writing.
|
||||
"[A-Za-z0-9+=]{72,}",
|
||||
"([A-Za-z0-9+=]|\\\\\\s\\*){72,}",
|
||||
"[0-9+][A-Za-z0-9+]{30,}[a-z0-9+]",
|
||||
"\\$[A-Z0-9+][A-Za-z0-9+]{6,}[a-z0-9+]",
|
||||
"\\b[a-z0-9+/=][A-Za-z0-9+/=]{7,}[a-z0-9+/=][A-Z]\\b",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
WeeChat = "WeeChat"
|
||||
|
||||
# all of these are valid words, but should never appear in this repo
|
||||
[default.extend-words]
|
||||
sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
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"
|
||||
OFO = "OFO"
|
||||
Ot = "Ot"
|
||||
ket = "ket"
|
||||
# This is the thead html tag, remove this once typos is updated in the github
|
||||
# action. 1.3.1 seems to work correctly, while 1.11.0 on the CI seems to get
|
||||
# this wrong
|
||||
thead = "thead"
|
||||
|
||||
[files]
|
||||
# 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"]
|
||||
|
||||
Generated
+1930
-2644
File diff suppressed because it is too large
Load Diff
+23
-41
@@ -2,68 +2,50 @@
|
||||
members = [
|
||||
"benchmarks",
|
||||
"bindings/matrix-sdk-crypto-ffi",
|
||||
"bindings/matrix-sdk-crypto-js",
|
||||
"bindings/matrix-sdk-crypto-nodejs",
|
||||
"bindings/matrix-sdk-ffi",
|
||||
"crates/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"uniffi-bindgen",
|
||||
"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"
|
||||
rust-version = "1.65"
|
||||
|
||||
[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"
|
||||
ctor = "0.1.26"
|
||||
dashmap = "5.2.0"
|
||||
eyeball = "0.4.0"
|
||||
eyeball-im = "0.2.0"
|
||||
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"
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "89e398fd062b4e763a3341fc7067428285d51d09", features = ["client-api-c"] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "89e398fd062b4e763a3341fc7067428285d51d09" }
|
||||
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 }
|
||||
tokio = { version = "1.24", default-features = false, features = ["sync"] }
|
||||
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.30"
|
||||
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "41e94c0a4834137d8d359b829e2d4b334f5ab5b5" }
|
||||
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "41e94c0a4834137d8d359b829e2d4b334f5ab5b5" }
|
||||
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "fb609ca1e4df5a7a818490ae86ac694119e41e71" }
|
||||
zeroize = "1.3.0"
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
@@ -89,9 +71,9 @@ 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
|
||||
inherits = "dev"
|
||||
incremental = false
|
||||
|
||||
[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" }
|
||||
# Compile all non-workspace crate in the dependency tree with optimizations
|
||||
[profile.reldbg.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
@@ -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.65`.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+5
-11
@@ -8,24 +8,18 @@ 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 }
|
||||
criterion = { version = "0.4.0", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.6.0"}
|
||||
matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.2.0", default-features = false, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { path = "../testing/matrix-sdk-test", version = "0.6.0"}
|
||||
ruma = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
pprof = { version = "0.11.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
|
||||
@@ -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(runtime.block_on(SledCryptoStore::open(dir.path(), None)).unwrap());
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(machine);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
@@ -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(runtime.block_on(SledCryptoStore::open(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(runtime.block_on(SledCryptoStore::open(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(runtime.block_on(SledCryptoStore::open(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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
+6
-11
@@ -7,24 +7,19 @@ maintained by the owners of the Matrix Rust SDK project.
|
||||
[`matrix-sdk`] crate via [`matrix-sdk-ffi`],
|
||||
* [`matrix-sdk-crypto-ffi`], UniFFI (Kotlin, Swift, Python, Ruby) bindings of the [`matrix-sdk-crypto`]
|
||||
crate,
|
||||
* [`matrix-sdk-ffi`], UniFFI bindings of the [`matrix-sdk`] crate.
|
||||
|
||||
There are also external bindings in other repositories:
|
||||
|
||||
* [`matrix-sdk-crypto-js`], JavaScript bindings of the
|
||||
[`matrix-sdk-crypto`] crate,
|
||||
* [`matrix-sdk-crypto-nodejs`], Node.js bindings of the
|
||||
[`matrix-sdk-crypto`] crate
|
||||
[`matrix-sdk-crypto`] crate,
|
||||
* [`matrix-sdk-ffi`], UniFFI bindings of the [`matrix-sdk`] crate.
|
||||
|
||||
[`apple`]: ./apple
|
||||
[`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi
|
||||
[`matrix-sdk-crypto-js`]: ./matrix-sdk-crypto-js
|
||||
[`matrix-sdk-crypto-nodejs`]: ./matrix-sdk-crypto-nodejs
|
||||
[`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto
|
||||
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
|
||||
[`matrix-sdk`]: ../crates/matrix-sdk
|
||||
|
||||
[`matrix-sdk-crypto-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).
|
||||
# Contributing
|
||||
To contribute read this [guide](./CONTRIBUTING.md).
|
||||
@@ -18,18 +18,19 @@ bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
futures-util = "0.3.28"
|
||||
base64 = { workspace = true }
|
||||
futures-util = "0.3.25"
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = { workspace = true }
|
||||
pbkdf2 = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
sha2 = "0.10.2"
|
||||
thiserror = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true }
|
||||
vodozemac = { workspace = true }
|
||||
@@ -39,23 +40,28 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
version = "0.2.2"
|
||||
features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-common]
|
||||
path = "../../crates/matrix-sdk-common"
|
||||
version = "0.6.0"
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
workspace = true
|
||||
features = ["qrcode", "automatic-room-key-forwarding"]
|
||||
path = "../../crates/matrix-sdk-crypto"
|
||||
version = "0.6.0"
|
||||
features = ["qrcode", "backups_v1"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
workspace = true
|
||||
path = "../../crates/matrix-sdk-sqlite"
|
||||
version = "0.1.0"
|
||||
default_features = false
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.33.0"
|
||||
version = "1.24.2"
|
||||
default_features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.8.0"
|
||||
assert_matches2 = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
|
||||
@@ -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::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,9 +12,8 @@ use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// The private part of the backup key, the one used for recovery.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct BackupRecoveryKey {
|
||||
pub(crate) inner: BackupDecryptionKey,
|
||||
pub(crate) inner: RecoveryKey,
|
||||
pub(crate) passphrase_info: Option<PassphraseInfo>,
|
||||
}
|
||||
|
||||
@@ -24,12 +23,11 @@ pub struct BackupRecoveryKey {
|
||||
pub enum PkDecryptionError {
|
||||
/// An internal libolm error happened during decryption.
|
||||
#[error("Error decryption a PkMessage {0}")]
|
||||
Olm(#[from] DecryptionError),
|
||||
Olm(#[from] OlmPkDecryptionError),
|
||||
}
|
||||
|
||||
/// Error type for the decoding and storing of the backup key.
|
||||
#[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 +40,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,
|
||||
@@ -63,40 +61,46 @@ pub struct MegolmV1BackupKey {
|
||||
pub backup_algorithm: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Convert the recovery key to a base 58 encoded string.
|
||||
pub fn to_base58(&self) -> String {
|
||||
self.inner.to_base58()
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupRecoveryKey {
|
||||
const KEY_SIZE: usize = 32;
|
||||
const SALT_SIZE: usize = 32;
|
||||
const PBKDF_ROUNDS: i32 = 500_000;
|
||||
}
|
||||
|
||||
#[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,40 +112,28 @@ impl BackupRecoveryKey {
|
||||
}
|
||||
|
||||
/// Restore a [`BackupRecoveryKey`] from the given passphrase.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Arc<Self> {
|
||||
pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Self {
|
||||
let mut key = Box::new([0u8; Self::KEY_SIZE]);
|
||||
let rounds = rounds as u32;
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut())
|
||||
.expect(
|
||||
"We should be able to expand a passphrase of any length due to \
|
||||
HMAC being able to be initialized with any input size",
|
||||
);
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut());
|
||||
|
||||
let backup_decryption_key = BackupDecryptionKey::from_bytes(&key);
|
||||
let recovery_key = RecoveryKey::from_bytes(&key);
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Arc::new(Self {
|
||||
inner: backup_decryption_key,
|
||||
Self {
|
||||
inner: recovery_key,
|
||||
passphrase_info: Some(PassphraseInfo {
|
||||
private_key_salt: salt,
|
||||
private_key_iterations: rounds as i32,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 58 encoded string.
|
||||
pub fn to_base58(&self) -> String {
|
||||
self.inner.to_base58()
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Get the public part of the backup key.
|
||||
pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
|
||||
let public_key = self.inner.megolm_v1_public_key();
|
||||
@@ -149,22 +141,7 @@ impl BackupRecoveryKey {
|
||||
let signatures: HashMap<String, HashMap<String, String>> = public_key
|
||||
.signatures()
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
match v {
|
||||
Ok(s) => s.to_base64(),
|
||||
Err(s) => s.source,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect()))
|
||||
.collect();
|
||||
|
||||
MegolmV1BackupKey {
|
||||
@@ -183,51 +160,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ use matrix_sdk_crypto::{
|
||||
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 +17,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 +25,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,8 +39,7 @@ pub enum SignatureError {
|
||||
UnknownUserIdentity(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CryptoStoreError {
|
||||
#[error("Failed to open the store")]
|
||||
OpenStore(#[from] OpenStoreError),
|
||||
@@ -59,17 +55,12 @@ pub enum CryptoStoreError {
|
||||
Identifier(#[from] IdParseError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[derive(Debug)]
|
||||
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 },
|
||||
}
|
||||
|
||||
@@ -78,7 +69,7 @@ impl From<MegolmError> for DecryptionError {
|
||||
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()),
|
||||
withheld_code: withheld_code.map(|w| w.to_string()),
|
||||
},
|
||||
_ => Self::Megolm { error: value.to_string() },
|
||||
}
|
||||
@@ -102,25 +93,3 @@ impl From<InnerStoreError> for DecryptionError {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#![allow(unused_qualifications)]
|
||||
|
||||
mod backup_recovery_key;
|
||||
mod dehydrated_devices;
|
||||
mod device;
|
||||
mod error;
|
||||
mod logger;
|
||||
@@ -16,9 +15,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, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
pub use backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
};
|
||||
@@ -33,7 +31,7 @@ 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},
|
||||
store::{Changes, CryptoStore, RoomSettings as RustRoomSettings},
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
EncryptionSettings as RustEncryptionSettings, LocalTrust,
|
||||
};
|
||||
@@ -43,9 +41,8 @@ pub use responses::{
|
||||
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId,
|
||||
SecondsSinceUnixEpoch, UserId,
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility, DeviceId,
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
@@ -58,7 +55,7 @@ pub use verification::{
|
||||
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,
|
||||
@@ -81,7 +78,6 @@ pub struct MigrationData {
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -103,7 +99,7 @@ pub struct SessionMigrationData {
|
||||
///
|
||||
/// 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 +117,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 +135,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 +156,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}")]
|
||||
@@ -191,16 +187,15 @@ impl From<anyhow::Error> for MigrationError {
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
pub fn migrate(
|
||||
data: MigrationData,
|
||||
path: String,
|
||||
path: &str,
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<(), MigrationError> {
|
||||
let runtime = Runtime::new().context("initializing tokio runtime")?;
|
||||
) -> anyhow::Result<()> {
|
||||
let runtime = Runtime::new()?;
|
||||
runtime.block_on(async move {
|
||||
migrate_data(data, &path, passphrase, progress_listener).await?;
|
||||
migrate_data(data, path, passphrase, progress_listener).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -211,7 +206,7 @@ async fn migrate_data(
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> anyhow::Result<()> {
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::BackupDecryptionKey};
|
||||
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::RecoveryKey};
|
||||
use vodozemac::olm::Account;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -234,21 +229,26 @@ async fn migrate_data(
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
let user_id = parse_user_id(&data.account.user_id)?;
|
||||
let device_id: OwnedDeviceId = data.account.device_id.into();
|
||||
let user_id: Arc<UserId> = {
|
||||
let user_id: OwnedUserId = parse_user_id(&data.account.user_id)?;
|
||||
let user_id: &UserId = user_id.borrow();
|
||||
|
||||
user_id.into()
|
||||
};
|
||||
let device_id: Box<DeviceId> = data.account.device_id.into();
|
||||
let device_id: Arc<DeviceId> = device_id.into();
|
||||
|
||||
let account = Account::from_libolm_pickle(&data.account.pickle, &data.pickle_key)?;
|
||||
let pickle = account.pickle();
|
||||
let identity_keys = Arc::new(account.identity_keys());
|
||||
let pickled_account = matrix_sdk_crypto::olm::PickledAccount {
|
||||
user_id: parse_user_id(&data.account.user_id)?,
|
||||
device_id: device_id.clone(),
|
||||
device_id: device_id.as_ref().to_owned(),
|
||||
pickle,
|
||||
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);
|
||||
@@ -265,10 +265,8 @@ async fn migrate_data(
|
||||
data.inbound_group_sessions,
|
||||
)?;
|
||||
|
||||
let backup_decryption_key = data
|
||||
.backup_recovery_key
|
||||
.map(|k| BackupDecryptionKey::from_base58(k.as_str()))
|
||||
.transpose()?;
|
||||
let recovery_key =
|
||||
data.backup_recovery_key.map(|k| RecoveryKey::from_base58(k.as_str())).transpose()?;
|
||||
|
||||
let cross_signing = PrivateCrossSigningIdentity::empty((*user_id).into());
|
||||
cross_signing
|
||||
@@ -289,8 +287,8 @@ async fn migrate_data(
|
||||
let tracked_users: Vec<_> = data
|
||||
.tracked_users
|
||||
.into_iter()
|
||||
.filter_map(|s| parse_user_id(&s).ok().map(|u| (u, true)))
|
||||
.collect();
|
||||
.map(|u| Ok(((parse_user_id(&u)?), true)))
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
|
||||
let tracked_users: Vec<_> = tracked_users.iter().map(|(u, d)| (&**u, *d)).collect();
|
||||
store.save_tracked_users(tracked_users.as_slice()).await?;
|
||||
@@ -304,13 +302,12 @@ async fn migrate_data(
|
||||
room_settings.insert(room_id, settings.into());
|
||||
}
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account) }).await?;
|
||||
|
||||
let changes = Changes {
|
||||
account: Some(account),
|
||||
private_identity: Some(cross_signing),
|
||||
sessions,
|
||||
inbound_group_sessions,
|
||||
backup_decryption_key,
|
||||
recovery_key,
|
||||
backup_version: data.backup_version,
|
||||
room_settings,
|
||||
..Default::default()
|
||||
@@ -352,16 +349,14 @@ async fn save_changes(
|
||||
///
|
||||
/// * `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,
|
||||
path: &str,
|
||||
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(())
|
||||
) -> anyhow::Result<()> {
|
||||
let runtime = Runtime::new()?;
|
||||
runtime.block_on(migrate_session_data(data, path, passphrase, progress_listener))
|
||||
}
|
||||
|
||||
async fn migrate_session_data(
|
||||
@@ -379,7 +374,7 @@ async fn migrate_session_data(
|
||||
let total_steps = 1 + data.sessions.len() + data.inbound_group_sessions.len();
|
||||
let processed_steps = 0;
|
||||
|
||||
let user_id = UserId::parse(data.user_id)?;
|
||||
let user_id = UserId::parse(data.user_id)?.into();
|
||||
let device_id: OwnedDeviceId = data.device_id.into();
|
||||
|
||||
let identity_keys = IdentityKeys {
|
||||
@@ -394,7 +389,7 @@ async fn migrate_session_data(
|
||||
&listener,
|
||||
&data.pickle_key,
|
||||
user_id,
|
||||
device_id,
|
||||
device_id.into(),
|
||||
identity_keys,
|
||||
data.sessions,
|
||||
data.inbound_group_sessions,
|
||||
@@ -410,8 +405,8 @@ fn collect_sessions(
|
||||
total_steps: usize,
|
||||
listener: &dyn Fn(usize, usize),
|
||||
pickle_key: &[u8],
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<DeviceId>,
|
||||
identity_keys: Arc<IdentityKeys>,
|
||||
session_pickles: Vec<PickledSession>,
|
||||
group_session_pickles: Vec<PickledInboundGroupSession>,
|
||||
@@ -460,7 +455,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))
|
||||
@@ -500,33 +495,29 @@ fn collect_sessions(
|
||||
/// * `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,
|
||||
path: &str,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<(), MigrationError> {
|
||||
let runtime = Runtime::new().context("initializing tokio runtime")?;
|
||||
) -> anyhow::Result<()> {
|
||||
let runtime = Runtime::new()?;
|
||||
runtime.block_on(async move {
|
||||
let store = SqliteCryptoStore::open(path, passphrase.as_deref())
|
||||
.await
|
||||
.context("opening sqlite crypto store")?;
|
||||
let store = SqliteCryptoStore::open(path, passphrase.as_deref()).await?;
|
||||
|
||||
let mut rust_settings = HashMap::new();
|
||||
for (room_id, settings) in room_settings {
|
||||
let room_id = RoomId::parse(room_id).context("parsing room ID")?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
rust_settings.insert(room_id, settings.into());
|
||||
}
|
||||
|
||||
let changes = Changes { room_settings: rust_settings, ..Default::default() };
|
||||
store.save_changes(changes).await.context("saving changes")?;
|
||||
store.save_changes(changes).await?;
|
||||
|
||||
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
|
||||
///
|
||||
@@ -545,7 +536,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)]
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum EventEncryptionAlgorithm {
|
||||
/// Olm version 1 using Curve25519, AES-256, and SHA-256.
|
||||
OlmV1Curve25519AesSha2,
|
||||
@@ -577,7 +568,6 @@ impl TryFrom<RustEventEncryptionAlgorithm> for EventEncryptionAlgorithm {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -723,7 +713,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>,
|
||||
@@ -770,7 +760,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(),
|
||||
@@ -816,7 +806,7 @@ 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)]
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct RoomSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
@@ -847,30 +837,6 @@ 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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@@ -883,8 +849,24 @@ fn vodozemac_version() -> String {
|
||||
|
||||
uniffi::include_scaffolding!("olm");
|
||||
|
||||
mod uniffi_types {
|
||||
pub use crate::{
|
||||
backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
},
|
||||
error::{CryptoStoreError, DecryptionError, SecretImportError},
|
||||
machine::{KeyRequestPair, OlmMachine},
|
||||
responses::{BootstrapCrossSigningResult, DeviceLists, Request},
|
||||
verification::{
|
||||
RequestVerificationResult, StartSasResult, Verification, VerificationRequest,
|
||||
},
|
||||
BackupKeys, CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, EncryptionSettings,
|
||||
EventEncryptionAlgorithm, RoomKeyCounts, RoomSettings, ShieldColor, ShieldState,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod test {
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tempfile::tempdir;
|
||||
@@ -972,8 +954,7 @@ mod tests {
|
||||
"@ganfra146:matrix.org",
|
||||
"@this-is-me:matrix.org",
|
||||
"@Amandine:matrix.org",
|
||||
"@ganfra:matrix.org",
|
||||
"NotAUser%ID"
|
||||
"@ganfra:matrix.org"
|
||||
],
|
||||
"room_settings": {
|
||||
"!AZkqtjvtwPAuyNOXEt:matrix.org": {
|
||||
@@ -990,20 +971,12 @@ mod tests {
|
||||
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"],
|
||||
@@ -1042,11 +1015,6 @@ mod tests {
|
||||
let settings3 = machine.get_room_settings("!XYZ:matrix.org".into())?;
|
||||
assert!(settings3.is_none());
|
||||
|
||||
assert!(machine.is_user_tracked("@ganfra146:matrix.org".into()).unwrap());
|
||||
assert!(machine.is_user_tracked("@Amandine:matrix.org".into()).unwrap());
|
||||
assert!(machine.is_user_tracked("@this-is-me:matrix.org".into()).unwrap());
|
||||
assert!(machine.is_user_tracked("@ganfra:matrix.org".into()).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
#[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,7 +41,6 @@ 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)) };
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use matrix_sdk_crypto::{
|
||||
},
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
store::{Changes, RecoveryKey},
|
||||
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
|
||||
};
|
||||
use ruma::{
|
||||
@@ -48,7 +48,6 @@ use tokio::runtime::Runtime;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError},
|
||||
parse_user_id,
|
||||
responses::{response_from_string, OwnedResponse},
|
||||
@@ -59,53 +58,7 @@ use crate::{
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
};
|
||||
|
||||
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
|
||||
///
|
||||
/// Will contain various information about the `/sync` changes the
|
||||
/// [`OlmMachine`] processed.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SyncChangesResult {
|
||||
/// The, now possibly decrypted, to-device events the [`OlmMachine`]
|
||||
/// received, decrypted, and processed.
|
||||
to_device_events: Vec<String>,
|
||||
|
||||
/// Information about the room keys that were extracted out of the to-device
|
||||
/// events.
|
||||
room_key_infos: Vec<RoomKeyInfo>,
|
||||
}
|
||||
|
||||
/// Information on a room key that has been received or imported.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomKeyInfo {
|
||||
/// The [messaging algorithm] that this key is used for. Will be one of the
|
||||
/// `m.megolm.*` algorithms.
|
||||
///
|
||||
/// [messaging algorithm]: https://spec.matrix.org/v1.6/client-server-api/#messaging-algorithms
|
||||
pub algorithm: String,
|
||||
|
||||
/// The room where the key is used.
|
||||
pub room_id: String,
|
||||
|
||||
/// The Curve25519 key of the device which initiated the session originally.
|
||||
pub sender_key: String,
|
||||
|
||||
/// The ID of the session that the key is for.
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyInfo> for RoomKeyInfo {
|
||||
fn from(value: matrix_sdk_crypto::store::RoomKeyInfo) -> Self {
|
||||
Self {
|
||||
algorithm: value.algorithm.to_string(),
|
||||
room_id: value.room_id.to_string(),
|
||||
sender_key: value.sender_key.to_base64(),
|
||||
session_id: value.session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A high level state machine that handles E2EE for Matrix.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct OlmMachine {
|
||||
pub(crate) inner: ManuallyDrop<InnerMachine>,
|
||||
pub(crate) runtime: Runtime,
|
||||
@@ -113,21 +66,19 @@ pub struct OlmMachine {
|
||||
|
||||
impl Drop for OlmMachine {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::take to be used safely.
|
||||
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
|
||||
let _guard = self.runtime.enter();
|
||||
// Dropping the inner OlmMachine must happen within a tokio context
|
||||
// because deadpool drops sqlite connections in the DB pool on tokio's
|
||||
// blocking threadpool to avoid blocking async worker threads.
|
||||
let _guard = self.runtime.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
drop(inner);
|
||||
}
|
||||
}
|
||||
|
||||
/// A pair of outgoing room key requests, both of those are sendToDevice
|
||||
/// requests.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct KeyRequestPair {
|
||||
/// The optional cancellation, this is None if no previous key request was
|
||||
/// sent out for this key, thus it doesn't need to be cancelled.
|
||||
@@ -137,7 +88,7 @@ pub struct KeyRequestPair {
|
||||
}
|
||||
|
||||
/// The result of a signature verification of a signed JSON object.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SignatureVerification {
|
||||
/// The result of the signature verification using the public key of our own
|
||||
/// device.
|
||||
@@ -178,41 +129,6 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
|
||||
|
||||
#[uniffi::export]
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The unique ID of the user that owns this machine.
|
||||
///
|
||||
/// * `device_id` - The unique ID of the device that owns this machine.
|
||||
///
|
||||
/// * `path` - The path where the state of the machine should be persisted.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data
|
||||
/// at rest in the crypto store. **Warning**, if no passphrase is given,
|
||||
/// the store and all its data will remain unencrypted.
|
||||
#[uniffi::constructor]
|
||||
pub fn new(
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
path: String,
|
||||
mut passphrase: Option<String>,
|
||||
) -> Result<Arc<Self>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
let runtime = Runtime::new().expect("Couldn't create a tokio runtime");
|
||||
|
||||
let store = runtime
|
||||
.block_on(matrix_sdk_sqlite::SqliteCryptoStore::open(path, passphrase.as_deref()))?;
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
let inner =
|
||||
runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?;
|
||||
|
||||
Ok(Arc::new(OlmMachine { inner: ManuallyDrop::new(inner), runtime }))
|
||||
}
|
||||
|
||||
/// Get the user ID of the owner of this `OlmMachine`.
|
||||
pub fn user_id(&self) -> String {
|
||||
self.inner.user_id().to_string()
|
||||
@@ -239,6 +155,47 @@ impl OlmMachine {
|
||||
pub fn cross_signing_status(&self) -> CrossSigningStatus {
|
||||
self.runtime.block_on(self.inner.cross_signing_status()).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The unique ID of the user that owns this machine.
|
||||
///
|
||||
/// * `device_id` - The unique ID of the device that owns this machine.
|
||||
///
|
||||
/// * `path` - The path where the state of the machine should be persisted.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data
|
||||
/// at rest in the Sled store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
pub fn new(
|
||||
user_id: &str,
|
||||
device_id: &str,
|
||||
path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
) -> Result<Self, CryptoStoreError> {
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
let device_id = device_id.into();
|
||||
let runtime = Runtime::new().expect("Couldn't create a tokio runtime");
|
||||
|
||||
let store = runtime
|
||||
.block_on(matrix_sdk_sqlite::SqliteCryptoStore::open(path, passphrase.as_deref()))?;
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
let inner =
|
||||
runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?;
|
||||
|
||||
Ok(OlmMachine { inner: ManuallyDrop::new(inner), runtime })
|
||||
}
|
||||
|
||||
/// Get the display name of our own device.
|
||||
pub fn display_name(&self) -> Result<Option<String>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.display_name())?)
|
||||
}
|
||||
|
||||
/// Get a cross signing user identity for the given user ID.
|
||||
///
|
||||
@@ -254,10 +211,10 @@ impl OlmMachine {
|
||||
/// received.
|
||||
pub fn get_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
user_id: &str,
|
||||
timeout: u32,
|
||||
) -> Result<Option<UserIdentity>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
|
||||
let timeout = if timeout == 0 { None } else { Some(Duration::from_secs(timeout.into())) };
|
||||
|
||||
@@ -273,8 +230,8 @@ impl OlmMachine {
|
||||
}
|
||||
|
||||
/// Check if a user identity is considered to be verified by us.
|
||||
pub fn is_identity_verified(&self, user_id: String) -> Result<bool, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
pub fn is_identity_verified(&self, user_id: &str) -> Result<bool, CryptoStoreError> {
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
|
||||
Ok(
|
||||
if let Some(identity) =
|
||||
@@ -301,10 +258,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// Returns a request that needs to be sent out for the user identity to be
|
||||
/// marked as verified.
|
||||
pub fn verify_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
pub fn verify_identity(&self, user_id: &str) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
|
||||
let user_identity = self.runtime.block_on(self.inner.get_identity(&user_id, None))?;
|
||||
@@ -336,17 +290,17 @@ impl OlmMachine {
|
||||
/// received.
|
||||
pub fn get_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
user_id: &str,
|
||||
device_id: &str,
|
||||
timeout: u32,
|
||||
) -> Result<Option<Device>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
|
||||
let timeout = if timeout == 0 { None } else { Some(Duration::from_secs(timeout.into())) };
|
||||
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.get_device(&user_id, device_id.as_str().into(), timeout))?
|
||||
.block_on(self.inner.get_device(&user_id, device_id.into(), timeout))?
|
||||
.map(|d| d.into()))
|
||||
}
|
||||
|
||||
@@ -365,20 +319,17 @@ impl OlmMachine {
|
||||
/// as verified.
|
||||
pub fn verify_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
user_id: &str,
|
||||
device_id: &str,
|
||||
) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let device = self.runtime.block_on(self.inner.get_device(
|
||||
&user_id,
|
||||
device_id.as_str().into(),
|
||||
None,
|
||||
))?;
|
||||
let device =
|
||||
self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
Ok(self.runtime.block_on(device.verify())?.into())
|
||||
} else {
|
||||
Err(SignatureError::UnknownDevice(user_id, device_id))
|
||||
Err(SignatureError::UnknownDevice(user_id, device_id.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,17 +337,14 @@ impl OlmMachine {
|
||||
/// or uploading any signatures if verified
|
||||
pub fn set_local_trust(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
user_id: &str,
|
||||
device_id: &str,
|
||||
trust_state: LocalTrust,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
|
||||
let device = self.runtime.block_on(self.inner.get_device(
|
||||
&user_id,
|
||||
device_id.as_str().into(),
|
||||
None,
|
||||
))?;
|
||||
let device =
|
||||
self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
self.runtime.block_on(device.set_local_trust(trust_state))?;
|
||||
@@ -419,10 +367,10 @@ impl OlmMachine {
|
||||
/// received.
|
||||
pub fn get_user_devices(
|
||||
&self,
|
||||
user_id: String,
|
||||
user_id: &str,
|
||||
timeout: u32,
|
||||
) -> Result<Vec<Device>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let user_id = parse_user_id(user_id)?;
|
||||
|
||||
let timeout = if timeout == 0 { None } else { Some(Duration::from_secs(timeout.into())) };
|
||||
Ok(self
|
||||
@@ -438,7 +386,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// After the request was sent out and a successful response was received
|
||||
/// the response body should be passed back to the state machine using the
|
||||
/// [mark_request_as_sent()](Self::mark_request_as_sent) method.
|
||||
/// [mark_request_as_sent()](#method.mark_request_as_sent) method.
|
||||
///
|
||||
/// **Note**: This method call should be locked per call.
|
||||
pub fn outgoing_requests(&self) -> Result<Vec<Request>, CryptoStoreError> {
|
||||
@@ -462,13 +410,13 @@ impl OlmMachine {
|
||||
/// * `response_body` - The body of the response that was received.
|
||||
pub fn mark_request_as_sent(
|
||||
&self,
|
||||
request_id: String,
|
||||
request_id: &str,
|
||||
request_type: RequestType,
|
||||
response_body: String,
|
||||
response_body: &str,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let id: OwnedTransactionId = request_id.into();
|
||||
|
||||
let response = response_from_string(&response_body);
|
||||
let response = response_from_string(response_body);
|
||||
|
||||
let response: OwnedResponse = match request_type {
|
||||
RequestType::KeysUpload => {
|
||||
@@ -499,7 +447,10 @@ impl OlmMachine {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl OlmMachine {
|
||||
/// Let the state machine know about E2EE related sync changes that we
|
||||
/// received from the server.
|
||||
///
|
||||
@@ -521,8 +472,7 @@ impl OlmMachine {
|
||||
device_changes: DeviceLists,
|
||||
key_counts: HashMap<String, i32>,
|
||||
unused_fallback_keys: Option<Vec<String>>,
|
||||
next_batch_token: String,
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
|
||||
@@ -540,21 +490,14 @@ impl OlmMachine {
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
}),
|
||||
)?;
|
||||
let events = self.runtime.block_on(self.inner.receive_sync_changes(
|
||||
to_device.events,
|
||||
&device_changes,
|
||||
&key_counts,
|
||||
unused_fallback_keys.as_deref(),
|
||||
))?;
|
||||
|
||||
let to_device_events =
|
||||
to_device_events.into_iter().map(|event| event.json().get().to_owned()).collect();
|
||||
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
|
||||
|
||||
Ok(SyncChangesResult { to_device_events, room_key_infos })
|
||||
Ok(serde_json::to_string(&events)?)
|
||||
}
|
||||
|
||||
/// Add the given list of users to be tracked, triggering a key query
|
||||
@@ -567,7 +510,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// *Note*: Only users that aren't already tracked will be considered for an
|
||||
/// update. It's safe to call this with already tracked users, it won't
|
||||
/// result in excessive `/keys/query` requests.
|
||||
/// result in excessive keys query requests.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -595,10 +538,10 @@ impl OlmMachine {
|
||||
///
|
||||
/// After the request was sent out and a successful response was received
|
||||
/// the response body should be passed back to the state machine using the
|
||||
/// [mark_request_as_sent()](Self::mark_request_as_sent) method.
|
||||
/// [mark_request_as_sent()](#method.mark_request_as_sent) method.
|
||||
///
|
||||
/// This method should be called every time before a call to
|
||||
/// [`share_room_key()`](Self::share_room_key) is made.
|
||||
/// [`share_room_key()`](#method.share_room_key) is made.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -621,9 +564,9 @@ impl OlmMachine {
|
||||
/// whether to encrypt only for trusted devices.
|
||||
///
|
||||
/// These settings can be modified via
|
||||
/// [set_room_algorithm()](Self::set_room_algorithm) and
|
||||
/// [set_room_only_allow_trusted_devices()](Self::set_room_only_allow_trusted_devices)
|
||||
/// methods.
|
||||
/// [set_room_algorithm()](#method.set_room_algorithm) and
|
||||
/// [set_room_only_allow_trusted_devices()](#method.
|
||||
/// set_room_only_allow_trusted_devices) methods.
|
||||
pub fn get_room_settings(
|
||||
&self,
|
||||
room_id: String,
|
||||
@@ -664,8 +607,8 @@ impl OlmMachine {
|
||||
/// devices, or whether they should be excluded from the conversation.
|
||||
///
|
||||
/// Note that per-room setting may be overridden by a global
|
||||
/// [set_only_allow_trusted_devices()](Self::set_only_allow_trusted_devices)
|
||||
/// method.
|
||||
/// [set_only_allow_trusted_devices()](#method.
|
||||
/// set_only_allow_trusted_devices) method.
|
||||
pub fn set_room_only_allow_trusted_devices(
|
||||
&self,
|
||||
room_id: String,
|
||||
@@ -716,10 +659,10 @@ impl OlmMachine {
|
||||
///
|
||||
/// After the request was sent out and a successful response was received
|
||||
/// the response body should be passed back to the state machine using the
|
||||
/// [mark_request_as_sent()](Self::mark_request_as_sent) method.
|
||||
/// [mark_request_as_sent()](#method.mark_request_as_sent) method.
|
||||
///
|
||||
/// This method should be called every time before a call to
|
||||
/// [`encrypt()`](Self::encrypt) with the given `room_id` is made.
|
||||
/// [`encrypt()`](#method.encrypt) with the given `room_id` is made.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -761,12 +704,12 @@ impl OlmMachine {
|
||||
///
|
||||
/// 1. Get the one-time key claim request to establish 1:1 Olm sessions for
|
||||
/// the room members of the room we wish to participate in. This is done
|
||||
/// using the [`get_missing_sessions()`](Self::get_missing_sessions)
|
||||
/// using the [`get_missing_sessions()`](#method.get_missing_sessions)
|
||||
/// method. This method call should be locked per call.
|
||||
///
|
||||
/// 2. Share a room key with all the room members using the
|
||||
/// [`share_room_key()`](Self::share_room_key). This method call should
|
||||
/// be locked per room.
|
||||
/// [`share_room_key()`](#method.share_room_key). This method
|
||||
/// call should be locked per room.
|
||||
///
|
||||
/// 3. Encrypt the event using this method.
|
||||
///
|
||||
@@ -790,11 +733,11 @@ impl OlmMachine {
|
||||
content: String,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let content = serde_json::from_str(&content)?;
|
||||
let content: Value = serde_json::from_str(&content)?;
|
||||
|
||||
let encrypted_content = self
|
||||
.runtime
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type))
|
||||
.expect("Encrypting an event produced an error");
|
||||
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
@@ -924,6 +867,37 @@ impl OlmMachine {
|
||||
|
||||
Ok(encrypted)
|
||||
}
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
fn import_room_keys_helper(
|
||||
&self,
|
||||
keys: Vec<ExportedRoomKey>,
|
||||
from_backup: bool,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
|
||||
Ok(KeysImportResult {
|
||||
imported: result.imported_count as i64,
|
||||
total: result.total_count as i64,
|
||||
keys: result
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(|(r, m)| {
|
||||
(
|
||||
r.to_string(),
|
||||
m.into_iter().map(|(s, k)| (s, k.into_iter().collect())).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Import room keys from the given serialized key export.
|
||||
///
|
||||
@@ -937,12 +911,12 @@ impl OlmMachine {
|
||||
/// progress of the key import.
|
||||
pub fn import_room_keys(
|
||||
&self,
|
||||
keys: String,
|
||||
passphrase: String,
|
||||
keys: &str,
|
||||
passphrase: &str,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys = Cursor::new(keys);
|
||||
let keys = decrypt_room_key_export(keys, &passphrase)?;
|
||||
let keys = decrypt_room_key_export(keys, passphrase)?;
|
||||
self.import_room_keys_helper(keys, false, progress_listener)
|
||||
}
|
||||
|
||||
@@ -961,16 +935,19 @@ impl OlmMachine {
|
||||
/// progress of the key import.
|
||||
pub fn import_decrypted_room_keys(
|
||||
&self,
|
||||
keys: String,
|
||||
keys: &str,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
let keys: Vec<Value> = serde_json::from_str(keys)?;
|
||||
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
|
||||
self.import_room_keys_helper(keys, true, progress_listener)
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl OlmMachine {
|
||||
/// Discard the currently active room key for the given room if there is
|
||||
/// one.
|
||||
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
|
||||
@@ -1103,7 +1080,7 @@ impl OlmMachine {
|
||||
/// * `methods` - The list of verification methods we advertised as
|
||||
/// supported in the `m.key.verification.request` event.
|
||||
///
|
||||
/// [verification_request_content()]: Self::verification_request_content
|
||||
/// [verification_request_content()]: #method.verification_request_content
|
||||
pub fn request_verification(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1239,7 +1216,7 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `device_id` - The ID of device we would like to verify.
|
||||
///
|
||||
/// [request_verification_with_device()]: Self::request_verification_with_device
|
||||
/// [request_verification_with_device()]: #method.request_verification_with_device
|
||||
pub fn start_sas_with_device(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -1277,10 +1254,8 @@ impl OlmMachine {
|
||||
///
|
||||
/// This method returns `None` if we don't have any private cross signing
|
||||
/// keys.
|
||||
pub fn export_cross_signing_keys(
|
||||
&self,
|
||||
) -> Result<Option<CrossSigningKeyExport>, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.export_cross_signing_keys())?.map(|e| e.into()))
|
||||
pub fn export_cross_signing_keys(&self) -> Option<CrossSigningKeyExport> {
|
||||
self.runtime.block_on(self.inner.export_cross_signing_keys()).map(|e| e.into())
|
||||
}
|
||||
|
||||
/// Import our private cross signing keys.
|
||||
@@ -1296,16 +1271,6 @@ impl OlmMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request missing local secrets from our devices (cross signing private
|
||||
/// keys, megolm backup). This will ask the sdk to create outgoing
|
||||
/// request to get the missing secrets.
|
||||
///
|
||||
/// The requests will be processed as soon as `outgoing_requests()` is
|
||||
/// called to process them.
|
||||
pub fn query_missing_secrets_from_other_sessions(&self) -> Result<bool, CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.query_missing_secrets_from_other_sessions())?)
|
||||
}
|
||||
|
||||
/// Activate the given backup key to be used with the given backup version.
|
||||
///
|
||||
/// **Warning**: The caller needs to make sure that the given `BackupKey` is
|
||||
@@ -1370,12 +1335,12 @@ impl OlmMachine {
|
||||
// We need to clone here due to FFI limitations but RecoveryKey does
|
||||
// not want to expose clone since it's private key material.
|
||||
let mut encoded = k.to_base64();
|
||||
let key = BackupDecryptionKey::from_base64(&encoded)
|
||||
let key = RecoveryKey::from_base64(&encoded)
|
||||
.expect("Encoding and decoding from base64 should always work");
|
||||
encoded.zeroize();
|
||||
key
|
||||
});
|
||||
Ok(self.runtime.block_on(self.inner.backup_machine().save_decryption_key(key, version))?)
|
||||
Ok(self.runtime.block_on(self.inner.backup_machine().save_recovery_key(key, version))?)
|
||||
}
|
||||
|
||||
/// Get the backup keys we have saved in our crypto store.
|
||||
@@ -1387,16 +1352,14 @@ impl OlmMachine {
|
||||
.ok()
|
||||
.map(Arc::new))
|
||||
}
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
/// Sign the given message using our device key and if available cross
|
||||
/// signing master key.
|
||||
pub fn sign(
|
||||
&self,
|
||||
message: String,
|
||||
) -> Result<HashMap<String, HashMap<String, String>>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.sign(&message))?
|
||||
pub fn sign(&self, message: &str) -> HashMap<String, HashMap<String, String>> {
|
||||
self.runtime
|
||||
.block_on(self.inner.sign(message))
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
@@ -1414,7 +1377,7 @@ impl OlmMachine {
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if the given backup has been verified by us or by another of our
|
||||
@@ -1434,54 +1397,13 @@ impl OlmMachine {
|
||||
/// ```
|
||||
pub fn verify_backup(
|
||||
&self,
|
||||
backup_info: String,
|
||||
backup_info: &str,
|
||||
) -> Result<SignatureVerification, CryptoStoreError> {
|
||||
let backup_info = serde_json::from_str(&backup_info)?;
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.backup_machine().verify_backup(backup_info, false))?
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Manage dehydrated devices.
|
||||
pub fn dehydrated_devices(&self) -> Arc<DehydratedDevices> {
|
||||
DehydratedDevices {
|
||||
inner: ManuallyDrop::new(self.inner.dehydrated_devices()),
|
||||
runtime: self.runtime.handle().to_owned(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
fn import_room_keys_helper(
|
||||
&self,
|
||||
keys: Vec<ExportedRoomKey>,
|
||||
from_backup: bool,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
|
||||
Ok(KeysImportResult {
|
||||
imported: result.imported_count as i64,
|
||||
total: result.total_count as i64,
|
||||
keys: result
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(|(r, m)| {
|
||||
(
|
||||
r.to_string(),
|
||||
m.into_iter().map(|(s, k)| (s, k.into_iter().collect())).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,309 @@
|
||||
namespace matrix_sdk_crypto_ffi {};
|
||||
namespace matrix_sdk_crypto_ffi {
|
||||
void set_logger(Logger logger);
|
||||
[Throws=MigrationError]
|
||||
void migrate(
|
||||
MigrationData data,
|
||||
[ByRef] string path,
|
||||
string? passphrase,
|
||||
ProgressListener progress_listener
|
||||
);
|
||||
[Throws=MigrationError]
|
||||
void migrate_sessions(
|
||||
SessionMigrationData data,
|
||||
[ByRef] string path,
|
||||
string? passphrase,
|
||||
ProgressListener progress_listener
|
||||
);
|
||||
[Throws=MigrationError]
|
||||
void migrate_room_settings(
|
||||
record<string, RoomSettings> room_settings,
|
||||
[ByRef] string path,
|
||||
string? passphrase
|
||||
);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface MigrationError {
|
||||
Generic(string error_message);
|
||||
};
|
||||
|
||||
callback interface Logger {
|
||||
void log(string log_line);
|
||||
};
|
||||
|
||||
callback interface ProgressListener {
|
||||
void on_progress(i32 progress, i32 total);
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum KeyImportError {
|
||||
"Export",
|
||||
"CryptoStore",
|
||||
"Json",
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum SignatureError {
|
||||
"Signature",
|
||||
"Identifier",
|
||||
"CryptoStore",
|
||||
"UnknownDevice",
|
||||
"UnknownUserIdentity",
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum SecretImportError {
|
||||
"Import",
|
||||
"CryptoStore",
|
||||
};
|
||||
|
||||
|
||||
[Error]
|
||||
enum CryptoStoreError {
|
||||
"OpenStore",
|
||||
"CryptoStore",
|
||||
"OlmError",
|
||||
"Serialization",
|
||||
"InvalidUserId",
|
||||
"Identifier",
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface DecryptionError {
|
||||
Identifier(string error);
|
||||
Serialization(string error);
|
||||
Megolm(string error);
|
||||
MissingRoomKey(string error, string? withheld_code);
|
||||
Store(string error);
|
||||
};
|
||||
|
||||
dictionary KeysImportResult {
|
||||
i64 imported;
|
||||
i64 total;
|
||||
record<DOMString, record<DOMString, sequence<string>>> keys;
|
||||
};
|
||||
|
||||
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 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;
|
||||
};
|
||||
|
||||
interface Sas {
|
||||
string other_user_id();
|
||||
string other_device_id();
|
||||
string flow_id();
|
||||
string? room_id();
|
||||
boolean we_started();
|
||||
boolean is_done();
|
||||
|
||||
OutgoingVerificationRequest? accept();
|
||||
[Throws=CryptoStoreError]
|
||||
ConfirmVerificationResult? confirm();
|
||||
OutgoingVerificationRequest? cancel([ByRef] string cancel_code);
|
||||
|
||||
sequence<i32>? get_emoji_indices();
|
||||
sequence<i32>? get_decimals();
|
||||
|
||||
void set_changes_listener(SasListener listener);
|
||||
SasState state();
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface SasState {
|
||||
Started();
|
||||
Accepted();
|
||||
KeysExchanged(sequence<i32>? emojis, sequence<i32> decimals);
|
||||
Confirmed();
|
||||
Done();
|
||||
Cancelled(CancelInfo cancel_info);
|
||||
};
|
||||
|
||||
callback interface SasListener {
|
||||
void on_change(SasState state);
|
||||
};
|
||||
|
||||
dictionary ScanResult {
|
||||
QrCode qr;
|
||||
OutgoingVerificationRequest request;
|
||||
};
|
||||
|
||||
interface QrCode {
|
||||
string other_user_id();
|
||||
string other_device_id();
|
||||
string flow_id();
|
||||
string? room_id();
|
||||
boolean we_started();
|
||||
boolean is_done();
|
||||
boolean is_cancelled();
|
||||
CancelInfo? cancel_info();
|
||||
|
||||
boolean reciprocated();
|
||||
boolean has_been_scanned();
|
||||
|
||||
ConfirmVerificationResult? confirm();
|
||||
OutgoingVerificationRequest? cancel([ByRef] string cancel_code);
|
||||
string? generate_qr_code();
|
||||
|
||||
void set_changes_listener(QrCodeListener listener);
|
||||
QrCodeState state();
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface QrCodeState {
|
||||
Started();
|
||||
Scanned();
|
||||
Confirmed();
|
||||
Reciprocated();
|
||||
Done();
|
||||
Cancelled(CancelInfo cancel_info);
|
||||
};
|
||||
|
||||
callback interface QrCodeListener {
|
||||
void on_change(QrCodeState state);
|
||||
};
|
||||
|
||||
interface VerificationRequest {
|
||||
string other_user_id();
|
||||
string? other_device_id();
|
||||
string flow_id();
|
||||
string? room_id();
|
||||
boolean we_started();
|
||||
boolean is_ready();
|
||||
boolean is_done();
|
||||
boolean is_passive();
|
||||
boolean is_cancelled();
|
||||
CancelInfo? cancel_info();
|
||||
|
||||
sequence<string>? their_supported_methods();
|
||||
sequence<string>? our_supported_methods();
|
||||
|
||||
OutgoingVerificationRequest? accept(sequence<string> methods);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
StartSasResult? start_sas_verification();
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
QrCode? start_qr_verification();
|
||||
ScanResult? scan_qr_code([ByRef] string data);
|
||||
|
||||
OutgoingVerificationRequest? cancel();
|
||||
|
||||
void set_changes_listener(VerificationRequestListener listener);
|
||||
VerificationRequestState state();
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface VerificationRequestState {
|
||||
Requested();
|
||||
Ready(sequence<string> their_methods, sequence<string> our_methods);
|
||||
Done();
|
||||
Cancelled(CancelInfo cancel_info);
|
||||
};
|
||||
|
||||
callback interface VerificationRequestListener {
|
||||
void on_change(VerificationRequestState state);
|
||||
};
|
||||
|
||||
dictionary RequestVerificationResult {
|
||||
VerificationRequest verification;
|
||||
OutgoingVerificationRequest request;
|
||||
};
|
||||
|
||||
dictionary ConfirmVerificationResult {
|
||||
sequence<OutgoingVerificationRequest> requests;
|
||||
SignatureUploadRequest? signature_request;
|
||||
};
|
||||
|
||||
interface Verification {
|
||||
QrCode? as_qr();
|
||||
Sas? as_sas();
|
||||
};
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
enum LocalTrust {
|
||||
"Verified",
|
||||
@@ -7,9 +312,156 @@ enum LocalTrust {
|
||||
"Unset",
|
||||
};
|
||||
|
||||
|
||||
enum EventEncryptionAlgorithm {
|
||||
"OlmV1Curve25519AesSha2",
|
||||
"MegolmV1AesSha2",
|
||||
};
|
||||
|
||||
enum HistoryVisibility {
|
||||
"Invited",
|
||||
"Joined",
|
||||
"Shared",
|
||||
"WorldReadable",
|
||||
};
|
||||
|
||||
interface OlmMachine {
|
||||
[Throws=CryptoStoreError]
|
||||
constructor(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string device_id,
|
||||
[ByRef] string path,
|
||||
string? passphrase
|
||||
);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
sequence<Request> outgoing_requests();
|
||||
[Throws=CryptoStoreError]
|
||||
void mark_request_as_sent(
|
||||
[ByRef] string request_id,
|
||||
RequestType request_type,
|
||||
[ByRef] string response
|
||||
);
|
||||
|
||||
[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 set_local_trust([ByRef] string user_id, [ByRef] string device_id, LocalTrust trust_state);
|
||||
[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=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]
|
||||
boolean is_identity_verified([ByRef] string user_id);
|
||||
|
||||
record<DOMString, record<DOMString, string>> sign([ByRef] string message);
|
||||
[Throws=CryptoStoreError]
|
||||
SignatureVerification verify_backup([ByRef] string auth_data);
|
||||
};
|
||||
|
||||
dictionary PassphraseInfo {
|
||||
string private_key_salt;
|
||||
i32 private_key_iterations;
|
||||
};
|
||||
|
||||
dictionary SignatureVerification {
|
||||
SignatureState device_signature;
|
||||
SignatureState user_identity_signature;
|
||||
record<DOMString, SignatureState> other_devices_signatures;
|
||||
boolean trusted;
|
||||
};
|
||||
|
||||
enum SignatureState {
|
||||
"Missing",
|
||||
"Invalid",
|
||||
"ValidButNotTrusted",
|
||||
"ValidAndTrusted",
|
||||
};
|
||||
|
||||
[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);
|
||||
};
|
||||
|
||||
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;
|
||||
record<string, RoomSettings> room_settings;
|
||||
};
|
||||
|
||||
dictionary SessionMigrationData {
|
||||
string user_id;
|
||||
string device_id;
|
||||
string curve25519_key;
|
||||
string ed25519_key;
|
||||
sequence<PickledSession> sessions;
|
||||
sequence<PickledInboundGroupSession> inbound_group_sessions;
|
||||
sequence<u8> pickle_key;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
dictionary RoomSettings {
|
||||
EventEncryptionAlgorithm algorithm;
|
||||
boolean only_allow_trusted_devices;
|
||||
};
|
||||
|
||||
@@ -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::{
|
||||
@@ -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,7 +112,7 @@ 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 },
|
||||
@@ -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,
|
||||
@@ -274,7 +254,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 {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64::{
|
||||
alphabet,
|
||||
engine::{general_purpose, GeneralPurpose},
|
||||
Engine,
|
||||
};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_crypto::{
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
@@ -9,13 +14,14 @@ use matrix_sdk_crypto::{
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use tokio::runtime::Handle;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
|
||||
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
|
||||
|
||||
const STANDARD_NO_PAD: GeneralPurpose =
|
||||
GeneralPurpose::new(&alphabet::STANDARD, general_purpose::NO_PAD);
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a SAS
|
||||
/// verification.
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait SasListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -26,7 +32,6 @@ pub trait SasListener: Send {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -72,13 +77,11 @@ impl From<RustSasState> for SasState {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -102,13 +105,11 @@ impl Verification {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -169,7 +170,7 @@ impl Sas {
|
||||
/// 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> {
|
||||
pub fn cancel(&self, cancel_code: &str) -> Option<OutgoingVerificationRequest> {
|
||||
self.inner.cancel_with_code(cancel_code.into()).map(|r| r.into())
|
||||
}
|
||||
|
||||
@@ -247,9 +248,7 @@ impl Sas {
|
||||
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>,
|
||||
@@ -272,7 +271,6 @@ impl Sas {
|
||||
|
||||
/// 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
|
||||
///
|
||||
@@ -283,7 +281,6 @@ pub trait QrCodeListener: Send {
|
||||
}
|
||||
|
||||
/// An Enum describing the state the QrCode verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum QrCodeState {
|
||||
/// The QR verification has been started.
|
||||
Started,
|
||||
@@ -318,13 +315,11 @@ impl From<QrVerificationState> for QrCodeState {
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl QrCode {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -391,7 +386,7 @@ impl QrCode {
|
||||
/// 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> {
|
||||
pub fn cancel(&self, cancel_code: &str) -> Option<OutgoingVerificationRequest> {
|
||||
self.inner.cancel_with_code(cancel_code.into()).map(|r| r.into())
|
||||
}
|
||||
|
||||
@@ -413,7 +408,7 @@ impl QrCode {
|
||||
/// decoded on the other side before it can be put through a QR code
|
||||
/// generator.
|
||||
pub fn generate_qr_code(&self) -> Option<String> {
|
||||
self.inner.to_bytes().map(base64_encode).ok()
|
||||
self.inner.to_bytes().map(|data| STANDARD_NO_PAD.encode(data)).ok()
|
||||
}
|
||||
|
||||
/// Set a listener for changes in the QrCode verification process.
|
||||
@@ -429,9 +424,7 @@ impl QrCode {
|
||||
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>,
|
||||
@@ -455,7 +448,6 @@ impl QrCode {
|
||||
}
|
||||
|
||||
/// Information on why a verification flow has been cancelled and by whom.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct CancelInfo {
|
||||
/// The textual representation of the cancel reason
|
||||
pub reason: String,
|
||||
@@ -476,7 +468,6 @@ 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>,
|
||||
@@ -486,7 +477,6 @@ pub struct StartSasResult {
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
@@ -496,7 +486,6 @@ pub struct ScanResult {
|
||||
}
|
||||
|
||||
/// A result type for requesting verifications.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RequestVerificationResult {
|
||||
/// The verification request object that got created.
|
||||
pub verification: Arc<VerificationRequest>,
|
||||
@@ -506,7 +495,6 @@ pub struct RequestVerificationResult {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -518,7 +506,6 @@ pub struct ConfirmVerificationResult {
|
||||
|
||||
/// 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
|
||||
///
|
||||
@@ -529,7 +516,6 @@ pub trait VerificationRequestListener: Send {
|
||||
}
|
||||
|
||||
/// An Enum describing the state the QrCode verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VerificationRequestState {
|
||||
/// The verification request was sent
|
||||
Requested,
|
||||
@@ -550,15 +536,33 @@ pub enum VerificationRequestState {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<RustVerificationRequestState> for VerificationRequestState {
|
||||
fn from(value: RustVerificationRequestState) -> Self {
|
||||
match value {
|
||||
// The clients do not need to distinguish `Created` and `Requested` state
|
||||
RustVerificationRequestState::Created { .. } => Self::Requested,
|
||||
RustVerificationRequestState::Requested { .. } => Self::Requested,
|
||||
RustVerificationRequestState::Ready {
|
||||
their_methods,
|
||||
our_methods,
|
||||
other_device_id: _,
|
||||
} => Self::Ready {
|
||||
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
|
||||
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
|
||||
},
|
||||
RustVerificationRequestState::Done => Self::Done,
|
||||
RustVerificationRequestState::Cancelled(c) => Self::Cancelled { cancel_info: c.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The verificatoin request object which then can transition into some concrete
|
||||
/// verification method
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct VerificationRequest {
|
||||
pub(crate) inner: InnerVerificationRequest,
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl VerificationRequest {
|
||||
/// The id of the other user that is participating in this verification
|
||||
/// request.
|
||||
@@ -705,8 +709,8 @@ impl VerificationRequest {
|
||||
///
|
||||
/// * `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()?;
|
||||
pub fn scan_qr_code(&self, data: &str) -> Option<ScanResult> {
|
||||
let data = STANDARD_NO_PAD.decode(data).ok()?;
|
||||
let data = QrVerificationData::from_bytes(data).ok()?;
|
||||
|
||||
if let Some(qr) = self.runtime.block_on(self.inner.scan_qr_code(data)).ok()? {
|
||||
@@ -727,57 +731,15 @@ impl VerificationRequest {
|
||||
pub fn set_changes_listener(&self, listener: Box<dyn VerificationRequestListener>) {
|
||||
let stream = self.inner.changes();
|
||||
|
||||
self.runtime.spawn(Self::changes_listener(self.inner.to_owned(), stream, listener));
|
||||
self.runtime.spawn(Self::changes_listener(stream, listener));
|
||||
}
|
||||
|
||||
/// Get the current state of the verification request.
|
||||
pub fn state(&self) -> VerificationRequestState {
|
||||
Self::convert_verification_request(&self.inner, self.inner.state())
|
||||
}
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
self.inner.state().into()
|
||||
}
|
||||
|
||||
async fn changes_listener(
|
||||
request: InnerVerificationRequest,
|
||||
mut stream: impl Stream<Item = RustVerificationRequestState> + std::marker::Unpin,
|
||||
listener: Box<dyn VerificationRequestListener>,
|
||||
) {
|
||||
@@ -791,9 +753,7 @@ impl VerificationRequest {
|
||||
| RustVerificationRequestState::Cancelled { .. }
|
||||
);
|
||||
|
||||
let state = Self::convert_verification_request(&request, state);
|
||||
|
||||
listener.on_change(state);
|
||||
listener.on_change(state.into());
|
||||
|
||||
if should_break {
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
@@ -0,0 +1,3 @@
|
||||
/docs
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
@@ -0,0 +1 @@
|
||||
/pkg
|
||||
@@ -0,0 +1,9 @@
|
||||
// prettier configuration: the same as the conventions used throughout Matrix.org
|
||||
// see: https://github.com/matrix-org/eslint-plugin-matrix-org/blob/main/.prettierrc.js
|
||||
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 4,
|
||||
quoteProps: "consistent",
|
||||
trailingComma: "all",
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix "matrix-sdk-crypto-js-"
|
||||
version-git-message "matrix-sdk-crypto-js v%s"
|
||||
@@ -0,0 +1,11 @@
|
||||
# v0.1.0-alpha.7
|
||||
|
||||
- Add new accessors `Device.algorithms` and `Device.isSignedByOwner`
|
||||
- In `OlmMachine.getUserDevices`, wait a limited time for any in-flight
|
||||
device-list updates to complete.
|
||||
|
||||
# v0.1.0-alpha.6
|
||||
|
||||
- Add new accessor `InboundGroupSession.senderKey`.
|
||||
- Add a new API, `OlmMachine.registerRoomKeyUpdatedCallback`, which
|
||||
applications can use to listen for received room keys.
|
||||
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "matrix-sdk-crypto-js"
|
||||
description = "Matrix encryption library, for JavaScript"
|
||||
authors = ["Ivan Enderlin <ivane@element.io>"]
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.1.0-alpha.0"
|
||||
publish = false
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.profiling]
|
||||
wasm-opt = false
|
||||
|
||||
[package.metadata.wasm-pack.profile.profiling.wasm-bindgen]
|
||||
debug-js-glue = false
|
||||
demangle-name-section = true
|
||||
dwarf-debug-info = true
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ['-Oz']
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["tracing", "qrcode"]
|
||||
qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"]
|
||||
tracing = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures-util = "0.3.27"
|
||||
http = { workspace = true }
|
||||
js-sys = "0.3.49"
|
||||
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
|
||||
matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] }
|
||||
matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-indexeddb" }
|
||||
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
|
||||
ruma = { workspace = true, features = ["js", "rand", "unstable-msc2677"] }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
|
||||
vodozemac = { workspace = true, features = ["js"] }
|
||||
wasm-bindgen = "0.2.83"
|
||||
wasm-bindgen-futures = "0.4.33"
|
||||
zeroize = { workspace = true }
|
||||
@@ -0,0 +1,57 @@
|
||||
# `matrix-sdk-crypto-js`
|
||||
|
||||
Welcome to the [WebAssembly] + JavaScript binding for the Rust
|
||||
[`matrix-sdk-crypto`] library! WebAssembly can run anywhere, but these
|
||||
bindings are designed to run on a JavaScript host. These bindings are
|
||||
part of the [`matrix-rust-sdk`] project, which is a library
|
||||
implementation of a [Matrix] client-server.
|
||||
|
||||
`matrix-sdk-crypto` is a no-network-IO implementation of a state
|
||||
machine, named `OlmMachine`, that handles E2EE ([End-to-End
|
||||
Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for
|
||||
[Matrix] clients.
|
||||
|
||||
## Usage
|
||||
|
||||
These WebAssembly bindings are written in [Rust]. To build them, you
|
||||
need to install the Rust compiler, see [the Install Rust
|
||||
Page](https://www.rust-lang.org/tools/install). Then, the workflow is
|
||||
pretty classical by using [npm], see [the Downloading and installing
|
||||
Node.js and npm
|
||||
Page](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
|
||||
|
||||
Once the Rust compiler, Node.js and npm are installed, you can run the
|
||||
following commands:
|
||||
|
||||
```sh
|
||||
$ npm install
|
||||
$ npm run build
|
||||
$ npm run test
|
||||
```
|
||||
|
||||
A `matrix_sdk_crypto.js`, `matrix_sdk_crypto.d.ts` and a
|
||||
`matrix_sdk_crypto_bg.wasm` files should be generated in the `pkg/`
|
||||
directory.
|
||||
|
||||
TBD
|
||||
|
||||
## Documentation
|
||||
|
||||
[The documentation can be found
|
||||
online](https://matrix-org.github.io/matrix-rust-sdk/bindings/matrix-sdk-crypto-js/).
|
||||
|
||||
To generate the documentation locally, please run the following
|
||||
command:
|
||||
|
||||
```sh
|
||||
$ npm run doc
|
||||
```
|
||||
|
||||
The documentation is generated in the `./docs` directory.
|
||||
|
||||
[WebAssembly]: https://webassembly.org/
|
||||
[`matrix-sdk-crypto`]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto
|
||||
[`matrix-rust-sdk`]: https://github.com/matrix-org/matrix-rust-sdk
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[npm]: https://www.npmjs.com/
|
||||
@@ -0,0 +1,13 @@
|
||||
# Steps for releasing `matrix-sdk-crypto-js`
|
||||
|
||||
1. Create a new branch, named `release-matrix-sdk-crypto-js-<version>`.
|
||||
2. Update `CHANGELOG.md`, if necessary.
|
||||
3. Run `yarn version` to bump the version number and create a tag.
|
||||
4. Push the branch, but not yet the tag.
|
||||
5. Create a PR to approve the changes.
|
||||
6. Once approved:
|
||||
1. Update the git tag to the new head of the branch, if necessary.
|
||||
2. Push the git tag; doing so triggers the github actions workflow which
|
||||
builds and publishes to npm, and creates a draft GH release.
|
||||
3. Merge the PR.
|
||||
7. Update the release on github and publish.
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@matrix-org/matrix-sdk-crypto-js",
|
||||
"version": "0.1.0-alpha.7",
|
||||
"homepage": "https://github.com/matrix-org/matrix-rust-sdk",
|
||||
"description": "Matrix encryption library, for JavaScript",
|
||||
"license": "Apache-2.0",
|
||||
"collaborators": [
|
||||
"Ivan Enderlin <ivane@element.io>"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix",
|
||||
"chat",
|
||||
"messaging",
|
||||
"ruma",
|
||||
"nio"
|
||||
],
|
||||
"main": "pkg/matrix_sdk_crypto_js.js",
|
||||
"types": "pkg/matrix_sdk_crypto_js.d.ts",
|
||||
"files": [
|
||||
"pkg/matrix_sdk_crypto_js_bg.wasm.js",
|
||||
"pkg/matrix_sdk_crypto_js_bg.wasm.d.ts",
|
||||
"pkg/matrix_sdk_crypto_js.js",
|
||||
"pkg/matrix_sdk_crypto_js.d.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fake-indexeddb": "^4.0",
|
||||
"jest": "^28.1.0",
|
||||
"prettier": "^2.8.3",
|
||||
"typedoc": "^0.22.17",
|
||||
"wasm-pack": "^0.10.2",
|
||||
"yargs-parser": "~21.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "prettier --check .",
|
||||
"build": "WASM_PACK_ARGS=--release ./scripts/build.sh",
|
||||
"build:dev": "WASM_PACK_ARGS=--dev ./scripts/build.sh",
|
||||
"test": "jest --verbose",
|
||||
"doc": "typedoc --tsconfig .",
|
||||
"prepack": "npm run build && npm run test"
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build the JavaScript modules
|
||||
#
|
||||
# This script is really a workaround for https://github.com/rustwasm/wasm-pack/issues/1074.
|
||||
#
|
||||
# Currently, the only reliable way to load WebAssembly in all the JS
|
||||
# environments we want to target (web-via-webpack, web-via-browserify, jest)
|
||||
# seems to be to pack the WASM into base64, and then unpack it and instantiate
|
||||
# it at runtime.
|
||||
#
|
||||
# Hopefully one day, https://github.com/rustwasm/wasm-pack/issues/1074 will be
|
||||
# fixed and this will be unnecessary.
|
||||
|
||||
set -e
|
||||
|
||||
cd $(dirname "$0")/..
|
||||
|
||||
RUSTFLAGS='-C opt-level=z' WASM_BINDGEN_WEAKREF=1 wasm-pack build --target nodejs --scope matrix-org --out-dir pkg "${WASM_PACK_ARGS[@]}"
|
||||
|
||||
# Convert the Wasm into a JS file that exports the base64'ed Wasm.
|
||||
echo "module.exports = \`$(base64 pkg/matrix_sdk_crypto_js_bg.wasm)\`;" > pkg/matrix_sdk_crypto_js_bg.wasm.js
|
||||
|
||||
# In the JavaScript:
|
||||
# 1. Strip out the lines that load the WASM, and our new epilogue.
|
||||
# 2. Remove the imports of `TextDecoder` and `TextEncoder`. We rely on the global defaults.
|
||||
{
|
||||
sed -e '/Text..coder.*= require(.util.)/d' \
|
||||
-e '/^const path = /,$d' pkg/matrix_sdk_crypto_js.js
|
||||
cat scripts/epilogue.js
|
||||
} > pkg/matrix_sdk_crypto_js.js.new
|
||||
mv pkg/matrix_sdk_crypto_js.js.new pkg/matrix_sdk_crypto_js.js
|
||||
|
||||
# also extend the typescript
|
||||
cat scripts/epilogue.d.ts >> pkg/matrix_sdk_crypto_js.d.ts
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Load the WebAssembly module in the background, if it has not already been loaded.
|
||||
*
|
||||
* Returns a promise which will resolve once the other methods are ready.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function initAsync(): Promise<void>;
|
||||
@@ -0,0 +1,88 @@
|
||||
// Initially, 'wasm' is set to a Proxy object which will synchronously load the WebAssembly module and
|
||||
// replace 'wasm' with a reference to the exports from the wasm module.
|
||||
//
|
||||
// Ideally this will never get used because the application will call initAsync instead.
|
||||
wasm = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop, receiver) => __initSync()[prop],
|
||||
},
|
||||
);
|
||||
|
||||
let inited = false;
|
||||
__initSync = function () {
|
||||
if (inited) {
|
||||
return;
|
||||
}
|
||||
if (initPromise) {
|
||||
throw new Error("Asynchronous initialisation already in progress: cannot initialise synchronously");
|
||||
}
|
||||
const bytes = unbase64(require("./matrix_sdk_crypto_js_bg.wasm.js"));
|
||||
const mod = new WebAssembly.Module(bytes);
|
||||
const instance = new WebAssembly.Instance(mod, imports);
|
||||
wasm = instance.exports;
|
||||
wasm.__wbindgen_start();
|
||||
inited = true;
|
||||
return wasm;
|
||||
};
|
||||
|
||||
let initPromise = null;
|
||||
|
||||
/**
|
||||
* Load the WebAssembly module in the background, if it has not already been loaded.
|
||||
*
|
||||
* Returns a promise which will resolve once the other methods are ready.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
module.exports.initAsync = function () {
|
||||
if (inited) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!initPromise) {
|
||||
initPromise = Promise.resolve()
|
||||
.then(() => require("./matrix_sdk_crypto_js_bg.wasm.js"))
|
||||
.then((b64) => WebAssembly.instantiate(unbase64(b64), imports))
|
||||
.then((result) => {
|
||||
wasm = result.instance.exports;
|
||||
wasm.__wbindgen_start();
|
||||
inited = true;
|
||||
});
|
||||
}
|
||||
return initPromise;
|
||||
};
|
||||
|
||||
const b64lookup = new Uint8Array([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 62, 0, 62, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7,
|
||||
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32,
|
||||
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
|
||||
]);
|
||||
|
||||
// base64 decoder, based on the code at https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_2_%E2%80%93_rewriting_atob_and_btoa_using_typedarrays_and_utf-8
|
||||
function unbase64(sBase64) {
|
||||
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, "");
|
||||
const nInLen = sB64Enc.length;
|
||||
const nOutLen = (nInLen * 3 + 1) >> 2;
|
||||
const taBytes = new Uint8Array(nOutLen);
|
||||
|
||||
let nMod3;
|
||||
let nMod4;
|
||||
let nUint24 = 0;
|
||||
let nOutIdx = 0;
|
||||
for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
|
||||
nMod4 = nInIdx & 3;
|
||||
nUint24 |= b64lookup[sB64Enc.charCodeAt(nInIdx)] << (6 * (3 - nMod4));
|
||||
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
||||
nMod3 = 0;
|
||||
while (nMod3 < 3 && nOutIdx < nOutLen) {
|
||||
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
|
||||
nMod3++;
|
||||
nOutIdx++;
|
||||
}
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return taBytes;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Attachment API.
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// A type to encrypt and to decrypt anything that can fit in an
|
||||
/// `Uint8Array`, usually big buffer.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Attachment;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Attachment {
|
||||
/// Encrypt the content of the `Uint8Array`.
|
||||
///
|
||||
/// It produces an `EncryptedAttachment`, which can be used to
|
||||
/// retrieve the media encryption information, or the encrypted
|
||||
/// data.
|
||||
#[wasm_bindgen]
|
||||
pub fn encrypt(array: &[u8]) -> Result<EncryptedAttachment, JsError> {
|
||||
let mut cursor = Cursor::new(array);
|
||||
let mut encryptor = matrix_sdk_crypto::AttachmentEncryptor::new(&mut cursor);
|
||||
|
||||
let mut encrypted_data = Vec::new();
|
||||
encryptor.read_to_end(&mut encrypted_data)?;
|
||||
|
||||
let media_encryption_info = Some(encryptor.finish());
|
||||
|
||||
Ok(EncryptedAttachment { encrypted_data, media_encryption_info })
|
||||
}
|
||||
|
||||
/// Decrypt an `EncryptedAttachment`.
|
||||
///
|
||||
/// The encrypted attachment can be created manually, or from the
|
||||
/// `encrypt` method.
|
||||
///
|
||||
/// **Warning**: The encrypted attachment can be used only
|
||||
/// **once**! The encrypted data will still be present, but the
|
||||
/// media encryption info (which contain secrets) will be
|
||||
/// destroyed. It is still possible to get a JSON-encoded backup
|
||||
/// by calling `EncryptedAttachment.mediaEncryptionInfo`.
|
||||
pub fn decrypt(attachment: &mut EncryptedAttachment) -> Result<Vec<u8>, JsError> {
|
||||
let Some(media_encryption_info) = attachment.media_encryption_info.take() else {
|
||||
return Err(JsError::new(
|
||||
"The media encryption info are absent from the given encrypted attachment",
|
||||
));
|
||||
};
|
||||
|
||||
let encrypted_data: &[u8] = attachment.encrypted_data.as_slice();
|
||||
|
||||
let mut cursor = Cursor::new(encrypted_data);
|
||||
let mut decryptor =
|
||||
matrix_sdk_crypto::AttachmentDecryptor::new(&mut cursor, media_encryption_info)?;
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
decryptor.read_to_end(&mut decrypted_data)?;
|
||||
|
||||
Ok(decrypted_data)
|
||||
}
|
||||
}
|
||||
|
||||
/// An encrypted attachment, usually created from `Attachment.encrypt`.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptedAttachment {
|
||||
media_encryption_info: Option<matrix_sdk_crypto::MediaEncryptionInfo>,
|
||||
encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EncryptedAttachment {
|
||||
/// Create a new encrypted attachment manually.
|
||||
///
|
||||
/// It needs encrypted data, stored in an `Uint8Array`, and a
|
||||
/// [media encryption
|
||||
/// information](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/struct.MediaEncryptionInfo.html),
|
||||
/// as a JSON-encoded string.
|
||||
///
|
||||
/// The media encryption information aren't stored as a string:
|
||||
/// they are parsed, validated and fully deserialized.
|
||||
///
|
||||
/// See [the specification to learn
|
||||
/// more](https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes).
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
encrypted_data: Vec<u8>,
|
||||
media_encryption_info: &str,
|
||||
) -> Result<EncryptedAttachment, JsError> {
|
||||
Ok(Self {
|
||||
encrypted_data,
|
||||
media_encryption_info: Some(serde_json::from_str(media_encryption_info)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// The actual encrypted data.
|
||||
///
|
||||
/// **Warning**: It returns a **copy** of the entire encrypted
|
||||
/// data; be nice with your memory.
|
||||
#[wasm_bindgen(getter, js_name = "encryptedData")]
|
||||
pub fn encrypted_data(&self) -> Vec<u8> {
|
||||
self.encrypted_data.clone()
|
||||
}
|
||||
|
||||
/// Return the media encryption info as a JSON-encoded string. The
|
||||
/// structure is fully valid.
|
||||
///
|
||||
/// If the media encryption info have been consumed already, it
|
||||
/// will return `null`.
|
||||
#[wasm_bindgen(getter, js_name = "mediaEncryptionInfo")]
|
||||
pub fn media_encryption_info(&self) -> Option<String> {
|
||||
serde_json::to_string(self.media_encryption_info.as_ref()?).ok()
|
||||
}
|
||||
|
||||
/// Check whether the media encryption info has been consumed by
|
||||
/// `Attachment.decrypt` already.
|
||||
#[wasm_bindgen(getter, js_name = "hasMediaEncryptionInfoBeenConsumed")]
|
||||
pub fn has_media_encryption_info_been_consumed(&self) -> bool {
|
||||
self.media_encryption_info.is_none()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//! Types for a `Device`.
|
||||
|
||||
use js_sys::{Array, Map, Promise};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
encryption::EncryptionAlgorithm,
|
||||
future::future_to_promise,
|
||||
identifiers::{self, DeviceId, UserId},
|
||||
impl_from_to_inner,
|
||||
js::try_array_to_vec,
|
||||
types, verification, vodozemac,
|
||||
};
|
||||
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
pub(crate) inner: matrix_sdk_crypto::Device,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::Device => Device);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Device {
|
||||
/// Request an interactive verification with this device.
|
||||
#[wasm_bindgen(js_name = "requestVerification")]
|
||||
pub fn request_verification(&self, methods: Option<Array>) -> Result<Promise, JsError> {
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let tuple = Array::new();
|
||||
let (verification_request, outgoing_verification_request) = match methods {
|
||||
Some(methods) => me.request_verification_with_methods(methods).await,
|
||||
None => me.request_verification().await,
|
||||
};
|
||||
|
||||
tuple.set(0, verification::VerificationRequest::from(verification_request).into());
|
||||
tuple.set(
|
||||
1,
|
||||
verification::OutgoingVerificationRequest::from(outgoing_verification_request)
|
||||
.try_into()?,
|
||||
);
|
||||
|
||||
Ok(tuple)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Is this device considered to be verified.
|
||||
///
|
||||
/// This method returns true if either the `is_locally_trusted`
|
||||
/// method returns `true` or if the `is_cross_signing_trusted`
|
||||
/// method returns `true`.
|
||||
#[wasm_bindgen(js_name = "isVerified")]
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// Is this device considered to be verified using cross signing.
|
||||
#[wasm_bindgen(js_name = "isCrossSigningTrusted")]
|
||||
pub fn is_cross_signing_trusted(&self) -> bool {
|
||||
self.inner.is_cross_signing_trusted()
|
||||
}
|
||||
|
||||
/// Is this device cross-signed by its owner?
|
||||
#[wasm_bindgen(js_name = "isCrossSignedByOwner")]
|
||||
pub fn is_cross_signed_by_owner(&self) -> bool {
|
||||
self.inner.is_cross_signed_by_owner()
|
||||
}
|
||||
|
||||
/// Set the local trust state of the device to the given state.
|
||||
///
|
||||
/// This won’t affect any cross signing trust state, this only
|
||||
/// sets a flag marking to have the given trust state.
|
||||
///
|
||||
/// `trust_state` represents the new trust state that should be
|
||||
/// set for the device.
|
||||
#[wasm_bindgen(js_name = "setLocalTrust")]
|
||||
pub fn set_local_trust(&self, local_state: LocalTrust) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
me.set_local_trust(local_state.into()).await?;
|
||||
|
||||
Ok(JsValue::NULL)
|
||||
})
|
||||
}
|
||||
|
||||
/// The user ID of the device owner.
|
||||
#[wasm_bindgen(getter, js_name = "userId")]
|
||||
pub fn user_id(&self) -> UserId {
|
||||
self.inner.user_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// The unique ID of the device.
|
||||
#[wasm_bindgen(getter, js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> DeviceId {
|
||||
self.inner.device_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// Get the human readable name of the device.
|
||||
#[wasm_bindgen(getter, js_name = "displayName")]
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
self.inner.display_name().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the key of the given key algorithm belonging to this device.
|
||||
#[wasm_bindgen(js_name = "getKey")]
|
||||
pub fn get_key(
|
||||
&self,
|
||||
algorithm: identifiers::DeviceKeyAlgorithmName,
|
||||
) -> Result<Option<vodozemac::DeviceKey>, JsError> {
|
||||
Ok(self.inner.get_key(algorithm.try_into()?).cloned().map(Into::into))
|
||||
}
|
||||
|
||||
/// Get the Curve25519 key of the given device.
|
||||
#[wasm_bindgen(getter, js_name = "curve25519Key")]
|
||||
pub fn curve25519_key(&self) -> Option<vodozemac::Curve25519PublicKey> {
|
||||
self.inner.curve25519_key().map(Into::into)
|
||||
}
|
||||
|
||||
/// Get the Ed25519 key of the given device.
|
||||
#[wasm_bindgen(getter, js_name = "ed25519Key")]
|
||||
pub fn ed25519_key(&self) -> Option<vodozemac::Ed25519PublicKey> {
|
||||
self.inner.ed25519_key().map(Into::into)
|
||||
}
|
||||
|
||||
/// Get a map containing all the device keys.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn keys(&self) -> Map {
|
||||
let map = Map::new();
|
||||
|
||||
for (device_key_id, device_key) in self.inner.keys() {
|
||||
map.set(
|
||||
&identifiers::DeviceKeyId::from(device_key_id.clone()).into(),
|
||||
&vodozemac::DeviceKey::from(device_key.clone()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
/// Get the list of algorithms this device supports.
|
||||
///
|
||||
/// Returns `Array<EncryptionAlgorithm>`.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn algorithms(&self) -> Array {
|
||||
self.inner
|
||||
.algorithms()
|
||||
.iter()
|
||||
.map(|alg| EncryptionAlgorithm::from(alg.clone()))
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a map containing all the device signatures.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn signatures(&self) -> types::Signatures {
|
||||
self.inner.signatures().clone().into()
|
||||
}
|
||||
|
||||
/// Get the trust state of the device.
|
||||
#[wasm_bindgen(getter, js_name = "localTrustState")]
|
||||
pub fn local_trust_state(&self) -> LocalTrust {
|
||||
self.inner.local_trust_state().into()
|
||||
}
|
||||
|
||||
/// Is the device locally marked as trusted?
|
||||
#[wasm_bindgen(js_name = "isLocallyTrusted")]
|
||||
pub fn is_locally_trusted(&self) -> bool {
|
||||
self.inner.is_locally_trusted()
|
||||
}
|
||||
|
||||
/// Is the device locally marked as blacklisted?
|
||||
///
|
||||
/// Blacklisted devices won’t receive any group sessions.
|
||||
#[wasm_bindgen(js_name = "isBlacklisted")]
|
||||
pub fn is_blacklisted(&self) -> bool {
|
||||
self.inner.is_blacklisted()
|
||||
}
|
||||
|
||||
/// Is the device deleted?
|
||||
#[wasm_bindgen(js_name = "isDeleted")]
|
||||
pub fn is_deleted(&self) -> bool {
|
||||
self.inner.is_deleted()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local trust state of a device.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum LocalTrust {
|
||||
/// The device has been verified and is trusted.
|
||||
Verified,
|
||||
|
||||
/// The device been blacklisted from communicating.
|
||||
BlackListed,
|
||||
|
||||
/// The trust state of the device is being ignored.
|
||||
Ignored,
|
||||
|
||||
/// The trust state is unset.
|
||||
Unset,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::LocalTrust> for LocalTrust {
|
||||
fn from(value: matrix_sdk_crypto::LocalTrust) -> Self {
|
||||
use matrix_sdk_crypto::LocalTrust::*;
|
||||
|
||||
match value {
|
||||
Verified => Self::Verified,
|
||||
BlackListed => Self::BlackListed,
|
||||
Ignored => Self::Ignored,
|
||||
Unset => Self::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalTrust> for matrix_sdk_crypto::LocalTrust {
|
||||
fn from(value: LocalTrust) -> Self {
|
||||
use LocalTrust::*;
|
||||
|
||||
match value {
|
||||
Verified => Self::Verified,
|
||||
BlackListed => Self::BlackListed,
|
||||
Ignored => Self::Ignored,
|
||||
Unset => Self::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct UserDevices {
|
||||
pub(crate) inner: matrix_sdk_crypto::UserDevices,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::UserDevices => UserDevices);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserDevices {
|
||||
/// Get the specific device with the given device ID.
|
||||
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
|
||||
self.inner.get(&device_id.inner).map(Into::into)
|
||||
}
|
||||
|
||||
/// Returns true if there is at least one devices of this user
|
||||
/// that is considered to be verified, false otherwise.
|
||||
///
|
||||
/// This won't consider your own device as verified, as your own
|
||||
/// device is always implicitly verified.
|
||||
#[wasm_bindgen(js_name = "isAnyVerified")]
|
||||
pub fn is_any_verified(&self) -> bool {
|
||||
self.inner.is_any_verified()
|
||||
}
|
||||
|
||||
/// Array over all the device IDs of the user devices.
|
||||
pub fn keys(&self) -> Array {
|
||||
self.inner.keys().map(ToOwned::to_owned).map(DeviceId::from).map(JsValue::from).collect()
|
||||
}
|
||||
|
||||
/// Iterator over all the devices of the user devices.
|
||||
pub fn devices(&self) -> Array {
|
||||
self.inner.devices().map(Device::from).map(JsValue::from).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Encryption types & siblings.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::events;
|
||||
|
||||
/// Settings for an encrypted room.
|
||||
///
|
||||
/// This determines the algorithm and rotation periods of a group
|
||||
/// session.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncryptionSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EncryptionAlgorithm,
|
||||
|
||||
/// How long the session should be used before changing it,
|
||||
/// expressed in microseconds.
|
||||
#[wasm_bindgen(js_name = "rotationPeriod")]
|
||||
pub rotation_period: u64,
|
||||
|
||||
/// How many messages should be sent before changing the session.
|
||||
#[wasm_bindgen(js_name = "rotationPeriodMessages")]
|
||||
pub rotation_period_messages: u64,
|
||||
|
||||
/// The history visibility of the room when the session was
|
||||
/// created.
|
||||
#[wasm_bindgen(js_name = "historyVisibility")]
|
||||
pub history_visibility: events::HistoryVisibility,
|
||||
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
#[wasm_bindgen(js_name = "onlyAllowTrustedDevices")]
|
||||
pub only_allow_trusted_devices: bool,
|
||||
}
|
||||
|
||||
impl Default for EncryptionSettings {
|
||||
fn default() -> Self {
|
||||
let default = matrix_sdk_crypto::olm::EncryptionSettings::default();
|
||||
|
||||
Self {
|
||||
algorithm: default.algorithm.into(),
|
||||
rotation_period: default.rotation_period.as_micros().try_into().unwrap(),
|
||||
rotation_period_messages: default.rotation_period_msgs,
|
||||
history_visibility: default.history_visibility.into(),
|
||||
only_allow_trusted_devices: default.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EncryptionSettings {
|
||||
/// Create a new `EncryptionSettings` with default values.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> EncryptionSettings {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings {
|
||||
fn from(value: &EncryptionSettings) -> Self {
|
||||
let algorithm = value.algorithm.clone().into();
|
||||
|
||||
Self {
|
||||
algorithm,
|
||||
rotation_period: Duration::from_micros(value.rotation_period),
|
||||
rotation_period_msgs: value.rotation_period_messages,
|
||||
history_visibility: value.history_visibility.clone().into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An encryption algorithm to be used to encrypt messages sent to a
|
||||
/// room.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EncryptionAlgorithm {
|
||||
/// Olm version 1 using Curve25519, AES-256, and SHA-256.
|
||||
OlmV1Curve25519AesSha2,
|
||||
|
||||
/// Megolm version 1 using AES-256 and SHA-256.
|
||||
MegolmV1AesSha2,
|
||||
}
|
||||
|
||||
impl From<EncryptionAlgorithm> for JsValue {
|
||||
fn from(value: EncryptionAlgorithm) -> Self {
|
||||
use EncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => JsValue::from(0),
|
||||
MegolmV1AesSha2 => JsValue::from(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptionAlgorithm> for matrix_sdk_crypto::types::EventEncryptionAlgorithm {
|
||||
fn from(value: EncryptionAlgorithm) -> Self {
|
||||
use EncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
|
||||
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::types::EventEncryptionAlgorithm> for EncryptionAlgorithm {
|
||||
fn from(value: matrix_sdk_crypto::types::EventEncryptionAlgorithm) -> Self {
|
||||
use matrix_sdk_crypto::types::EventEncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
|
||||
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
|
||||
/// for more info.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ShieldColor {
|
||||
/// Important warning
|
||||
Red,
|
||||
/// Low warning
|
||||
Grey,
|
||||
/// No warning
|
||||
None,
|
||||
}
|
||||
|
||||
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
|
||||
/// for more info.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShieldState {
|
||||
/// The shield color
|
||||
pub color: ShieldColor,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ShieldState {
|
||||
/// Error message that can be displayed as a tooltip
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn message(&self) -> Option<String> {
|
||||
self.message.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RustShieldState> for ShieldState {
|
||||
fn from(value: RustShieldState) -> Self {
|
||||
match value {
|
||||
RustShieldState::Red { message } => {
|
||||
Self { color: ShieldColor::Red, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::Grey { message } => {
|
||||
Self { color: ShieldColor::Grey, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::None => Self { color: ShieldColor::None, message: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Types related to events.
|
||||
|
||||
use ruma::events::room::history_visibility::HistoryVisibility as RumaHistoryVisibility;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Who can see a room's history.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they joined the room onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when
|
||||
/// the member was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be
|
||||
/// shared by any participating homeserver with anyone, regardless
|
||||
/// of whether they have ever joined the room.
|
||||
WorldReadable,
|
||||
}
|
||||
|
||||
impl From<HistoryVisibility> for RumaHistoryVisibility {
|
||||
fn from(value: HistoryVisibility) -> Self {
|
||||
use HistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
use RumaHistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::future::Future;
|
||||
|
||||
use js_sys::Promise;
|
||||
use wasm_bindgen::{JsError, JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
pub(crate) fn future_to_promise<F, T>(future: F) -> Promise
|
||||
where
|
||||
F: Future<Output = Result<T, anyhow::Error>> + 'static,
|
||||
T: Into<JsValue>,
|
||||
{
|
||||
let mut future = Some(future);
|
||||
|
||||
Promise::new(&mut |resolve, reject| {
|
||||
let future = future.take().unwrap_throw();
|
||||
|
||||
spawn_local(async move {
|
||||
match future.await {
|
||||
Ok(value) => resolve.call1(&JsValue::UNDEFINED, &value.into()).unwrap_throw(),
|
||||
Err(value) => reject
|
||||
.call1(&JsValue::UNDEFINED, &JsError::new(&value.to_string()).into())
|
||||
.unwrap_throw(),
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Types for [Matrix](https://matrix.org/) identifiers for devices,
|
||||
//! events, keys, rooms, servers, users and URIs.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::impl_from_to_inner;
|
||||
|
||||
/// A Matrix [user ID].
|
||||
///
|
||||
/// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserId {
|
||||
pub(crate) inner: ruma::OwnedUserId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedUserId => UserId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserId {
|
||||
/// Parse/validate and create a new `UserId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<UserId, JsError> {
|
||||
Ok(Self::from(ruma::UserId::parse(id)?))
|
||||
}
|
||||
|
||||
/// Returns the user's localpart.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the user ID.
|
||||
#[wasm_bindgen(getter, js_name = "serverName")]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Whether this user ID is a historical one.
|
||||
///
|
||||
/// A historical user ID is one that doesn't conform to the latest
|
||||
/// specification of the user ID grammar but is still accepted
|
||||
/// because it was previously allowed.
|
||||
#[wasm_bindgen(js_name = "isHistorical")]
|
||||
pub fn is_historical(&self) -> bool {
|
||||
self.inner.is_historical()
|
||||
}
|
||||
|
||||
/// Return the user ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix key ID.
|
||||
///
|
||||
/// Device identifiers in Matrix are completely opaque character
|
||||
/// sequences. This type is provided simply for its semantic value.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceId {
|
||||
pub(crate) inner: ruma::OwnedDeviceId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedDeviceId => DeviceId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceId {
|
||||
/// Create a new `DeviceId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> DeviceId {
|
||||
Self::from(ruma::OwnedDeviceId::from(id))
|
||||
}
|
||||
|
||||
/// Return the device ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix device key ID.
|
||||
///
|
||||
/// A key algorithm and a device ID, combined with a ‘:’.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceKeyId {
|
||||
pub(crate) inner: ruma::OwnedDeviceKeyId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedDeviceKeyId => DeviceKeyId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceKeyId {
|
||||
/// Parse/validate and create a new `DeviceKeyId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: String) -> Result<DeviceKeyId, JsError> {
|
||||
Ok(Self::from(ruma::DeviceKeyId::parse(id.as_str())?))
|
||||
}
|
||||
|
||||
/// Returns key algorithm of the device key ID.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn algorithm(&self) -> DeviceKeyAlgorithm {
|
||||
self.inner.algorithm().into()
|
||||
}
|
||||
|
||||
/// Returns device ID of the device key ID.
|
||||
#[wasm_bindgen(getter, js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> DeviceId {
|
||||
self.inner.device_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// Return the device key ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The basic key algorithms in the specification.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceKeyAlgorithm {
|
||||
pub(crate) inner: ruma::DeviceKeyAlgorithm,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::DeviceKeyAlgorithm => DeviceKeyAlgorithm);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceKeyAlgorithm {
|
||||
/// Read the device key algorithm's name. If the name is
|
||||
/// `Unknown`, one may be interested by the `to_string` method to
|
||||
/// read the original name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn name(&self) -> DeviceKeyAlgorithmName {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
/// Return the device key algorithm as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The basic key algorithm names in the specification.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum DeviceKeyAlgorithmName {
|
||||
/// The Ed25519 signature algorithm.
|
||||
Ed25519,
|
||||
|
||||
/// The Curve25519 ECDH algorithm.
|
||||
Curve25519,
|
||||
|
||||
/// The Curve25519 ECDH algorithm, but the key also contains
|
||||
/// signatures.
|
||||
SignedCurve25519,
|
||||
|
||||
/// An unknown device key algorithm.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl TryFrom<DeviceKeyAlgorithmName> for ruma::DeviceKeyAlgorithm {
|
||||
type Error = JsError;
|
||||
|
||||
fn try_from(value: DeviceKeyAlgorithmName) -> Result<Self, Self::Error> {
|
||||
use DeviceKeyAlgorithmName::*;
|
||||
|
||||
Ok(match value {
|
||||
Ed25519 => Self::Ed25519,
|
||||
Curve25519 => Self::Curve25519,
|
||||
SignedCurve25519 => Self::SignedCurve25519,
|
||||
Unknown => {
|
||||
return Err(JsError::new(
|
||||
"The `DeviceKeyAlgorithmName.Unknown` variant cannot be converted",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithmName {
|
||||
fn from(value: ruma::DeviceKeyAlgorithm) -> Self {
|
||||
use ruma::DeviceKeyAlgorithm::*;
|
||||
|
||||
match value {
|
||||
Ed25519 => Self::Ed25519,
|
||||
Curve25519 => Self::Curve25519,
|
||||
SignedCurve25519 => Self::SignedCurve25519,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix [room ID].
|
||||
///
|
||||
/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoomId {
|
||||
pub(crate) inner: ruma::OwnedRoomId,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(ruma::OwnedRoomId => RoomId);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomId {
|
||||
/// Parse/validate and create a new `RoomId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<RoomId, JsError> {
|
||||
Ok(Self::from(ruma::RoomId::parse(id)?))
|
||||
}
|
||||
|
||||
/// Returns the user's localpart.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the room ID.
|
||||
#[wasm_bindgen(getter, js_name = "serverName")]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Return the room ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix-spec compliant [server name].
|
||||
///
|
||||
/// It consists of a host and an optional port (separated by a colon if
|
||||
/// present).
|
||||
///
|
||||
/// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct ServerName {
|
||||
inner: ruma::OwnedServerName,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ServerName {
|
||||
/// Parse/validate and create a new `ServerName`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(name: &str) -> Result<ServerName, JsError> {
|
||||
Ok(Self { inner: ruma::ServerName::parse(name)? })
|
||||
}
|
||||
|
||||
/// Returns the host of the server name.
|
||||
///
|
||||
/// That is: Return the part of the server before `:<port>` or the
|
||||
/// full server name if there is no port.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn host(&self) -> String {
|
||||
self.inner.host().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the port of the server name if any.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn port(&self) -> Option<u16> {
|
||||
self.inner.port()
|
||||
}
|
||||
|
||||
/// Returns true if and only if the server name is an IPv4 or IPv6
|
||||
/// address.
|
||||
#[wasm_bindgen(js_name = "isIpLiteral")]
|
||||
pub fn is_ip_literal(&self) -> bool {
|
||||
self.inner.is_ip_literal()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix [event ID].
|
||||
///
|
||||
/// An `EventId` is generated randomly or converted from a string
|
||||
/// slice, and can be converted back into a string as needed.
|
||||
///
|
||||
/// [event ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct EventId {
|
||||
pub(crate) inner: ruma::OwnedEventId,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EventId {
|
||||
/// Parse/validate and create a new `EventId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<EventId, JsError> {
|
||||
Ok(Self { inner: <&ruma::EventId>::try_from(id)?.to_owned() })
|
||||
}
|
||||
|
||||
/// Returns the event's localpart.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the event ID.
|
||||
#[wasm_bindgen(getter, js_name = "serverName")]
|
||||
pub fn server_name(&self) -> Option<ServerName> {
|
||||
Some(ServerName { inner: self.inner.server_name()?.to_owned() })
|
||||
}
|
||||
|
||||
/// Return the event ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! User identities.
|
||||
|
||||
use js_sys::{Array, Promise};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
future::future_to_promise, identifiers, impl_from_to_inner, js::try_array_to_vec, requests,
|
||||
verification,
|
||||
};
|
||||
|
||||
pub(crate) struct UserIdentities {
|
||||
inner: matrix_sdk_crypto::UserIdentities,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::UserIdentities => UserIdentities);
|
||||
|
||||
impl From<UserIdentities> for JsValue {
|
||||
fn from(user_identities: UserIdentities) -> Self {
|
||||
use matrix_sdk_crypto::UserIdentities::*;
|
||||
|
||||
match user_identities.inner {
|
||||
Own(own) => JsValue::from(OwnUserIdentity::from(own)),
|
||||
Other(other) => JsValue::from(UserIdentity::from(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a cross signing identity of a user.
|
||||
///
|
||||
/// This is the user identity of a user that is our own.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct OwnUserIdentity {
|
||||
inner: matrix_sdk_crypto::OwnUserIdentity,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::OwnUserIdentity => OwnUserIdentity);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OwnUserIdentity {
|
||||
/// Mark our user identity as verified.
|
||||
///
|
||||
/// This will mark the identity locally as verified and sign it with our own
|
||||
/// device.
|
||||
///
|
||||
/// Returns a signature upload request that needs to be sent out.
|
||||
pub fn verify(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(requests::SignatureUploadRequest::try_from(&me.verify().await?)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a verification request to our other devices.
|
||||
#[wasm_bindgen(js_name = "requestVerification")]
|
||||
pub fn request_verification(&self, methods: Option<Array>) -> Result<Promise, JsError> {
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let tuple = Array::new();
|
||||
let (verification_request, outgoing_verification_request) = match methods {
|
||||
Some(methods) => me.request_verification_with_methods(methods).await?,
|
||||
None => me.request_verification().await?,
|
||||
};
|
||||
|
||||
tuple.set(0, verification::VerificationRequest::from(verification_request).into());
|
||||
tuple.set(
|
||||
1,
|
||||
verification::OutgoingVerificationRequest::from(outgoing_verification_request)
|
||||
.try_into()?,
|
||||
);
|
||||
|
||||
Ok(tuple)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Does our user identity trust our own device, i.e. have we signed our own
|
||||
/// device keys with our self-signing key?
|
||||
#[wasm_bindgen(js_name = "trustsOurOwnDevice")]
|
||||
pub fn trusts_our_own_device(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.trusts_our_own_device().await?) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a cross signing identity of a user.
|
||||
///
|
||||
/// This is the user identity of a user that isn't our own. Other users will
|
||||
/// only contain a master key and a self signing key, meaning that only device
|
||||
/// signatures can be checked with this identity.
|
||||
///
|
||||
/// This struct wraps a read-only version of the struct and allows verifications
|
||||
/// to be requested to verify our own device with the user identity.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct UserIdentity {
|
||||
inner: matrix_sdk_crypto::UserIdentity,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::UserIdentity => UserIdentity);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserIdentity {
|
||||
/// Is this user identity verified?
|
||||
#[wasm_bindgen(js_name = "isVerified")]
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// Manually verify this user.
|
||||
///
|
||||
/// This method will attempt to sign the user identity using our private
|
||||
/// cross signing key.
|
||||
///
|
||||
/// This method fails if we don't have the private part of our user-signing
|
||||
/// key.
|
||||
///
|
||||
/// Returns a request that needs to be sent out for the user to be marked as
|
||||
/// verified.
|
||||
pub fn verify(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(requests::SignatureUploadRequest::try_from(&me.verify().await?)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `VerificationRequest` object after the verification
|
||||
/// request content has been sent out.
|
||||
#[wasm_bindgen(js_name = "requestVerification")]
|
||||
pub fn request_verification(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
request_event_id: &identifiers::EventId,
|
||||
methods: Option<Array>,
|
||||
) -> Result<Promise, JsError> {
|
||||
let me = self.inner.clone();
|
||||
let room_id = room_id.inner.clone();
|
||||
let request_event_id = request_event_id.inner.clone();
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
|
||||
Ok(future_to_promise::<_, verification::VerificationRequest>(async move {
|
||||
Ok(me
|
||||
.request_verification(room_id.as_ref(), request_event_id.as_ref(), methods)
|
||||
.await
|
||||
.into())
|
||||
}))
|
||||
}
|
||||
|
||||
/// Send a verification request to the given user.
|
||||
///
|
||||
/// The returned content needs to be sent out into a DM room with the given
|
||||
/// user.
|
||||
///
|
||||
/// After the content has been sent out a VerificationRequest can be started
|
||||
/// with the `request_verification` method.
|
||||
#[wasm_bindgen(js_name = "verificationRequestContent")]
|
||||
pub fn verification_request_content(&self, methods: Option<Array>) -> Result<Promise, JsError> {
|
||||
let me = self.inner.clone();
|
||||
let methods =
|
||||
methods.map(try_array_to_vec::<verification::VerificationMethod, _>).transpose()?;
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(&me.verification_request_content(methods).await)?)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use js_sys::{Array, Object, Reflect};
|
||||
use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};
|
||||
|
||||
/// A really hacky and dirty code to downcast a `JsValue` to `T:
|
||||
/// RefFromWasmAbi`, inspired by
|
||||
/// https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288.
|
||||
///
|
||||
/// The returned value is likely to be a `wasm_bindgen::__ref::Ref<T>`.
|
||||
pub(crate) fn downcast<T>(value: &JsValue, classname: &str) -> Result<T::Anchor, JsError>
|
||||
where
|
||||
T: RefFromWasmAbi<Abi = u32>,
|
||||
{
|
||||
let constructor_name = Object::get_prototype_of(value).constructor().name();
|
||||
|
||||
if constructor_name == classname {
|
||||
let pointer = Reflect::get(value, &JsValue::from_str("ptr"))
|
||||
.map_err(|_| JsError::new("Failed to read the `JsValue` pointer"))?;
|
||||
let pointer = pointer
|
||||
.as_f64()
|
||||
.ok_or_else(|| JsError::new("Failed to read the `JsValue` pointer as a `f64`"))?
|
||||
as u32;
|
||||
|
||||
Ok(unsafe { T::ref_from_abi(pointer) })
|
||||
} else {
|
||||
Err(JsError::new(&format!(
|
||||
"Expect an `{classname}` instance, received `{constructor_name}` instead",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform a value `JS` from JavaScript to a Rust wrapper, to a
|
||||
/// Rust wrapped type.
|
||||
pub(crate) fn try_array_to_vec<JS, Rust>(
|
||||
array: Array,
|
||||
) -> Result<Vec<Rust>, <JS as TryFrom<JsValue>>::Error>
|
||||
where
|
||||
JS: TryFrom<JsValue> + Into<Rust>,
|
||||
{
|
||||
array.iter().map(|item| JS::try_from(item).map(Into::into)).collect()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
// triggered by wasm_bindgen code
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
|
||||
pub mod attachment;
|
||||
pub mod device;
|
||||
pub mod encryption;
|
||||
pub mod events;
|
||||
mod future;
|
||||
pub mod identifiers;
|
||||
pub mod identities;
|
||||
mod js;
|
||||
pub mod machine;
|
||||
mod macros;
|
||||
pub mod olm;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
pub mod store;
|
||||
pub mod sync_events;
|
||||
mod tracing;
|
||||
pub mod types;
|
||||
pub mod verification;
|
||||
pub mod vodozemac;
|
||||
|
||||
use js_sys::JsString;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Object containing the versions of the Rust libraries we are using.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Versions {
|
||||
/// The version of the vodozemac crate.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub vodozemac: JsString,
|
||||
/// The version of the matrix-sdk-crypto crate.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub matrix_sdk_crypto: JsString,
|
||||
}
|
||||
|
||||
/// Get the versions of the Rust libraries we are using.
|
||||
#[wasm_bindgen(js_name = "getVersions")]
|
||||
pub fn get_versions() -> Versions {
|
||||
Versions {
|
||||
vodozemac: matrix_sdk_crypto::vodozemac::VERSION.into(),
|
||||
matrix_sdk_crypto: matrix_sdk_crypto::VERSION.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run some stuff when the Wasm module is instantiated.
|
||||
///
|
||||
/// Right now, it does the following:
|
||||
///
|
||||
/// * Redirect Rust panics to JavaScript console.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
@@ -0,0 +1,843 @@
|
||||
//! The crypto specific Olm objects.
|
||||
|
||||
use std::{collections::BTreeMap, ops::Deref, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use js_sys::{Array, Function, Map, Promise, Set};
|
||||
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
use tracing::warn;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
|
||||
use crate::{
|
||||
device, encryption,
|
||||
future::future_to_promise,
|
||||
identifiers, identities,
|
||||
js::downcast,
|
||||
olm, requests,
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
responses::{self, response_from_string},
|
||||
store,
|
||||
store::RoomKeyInfo,
|
||||
sync_events, types, verification, vodozemac,
|
||||
};
|
||||
|
||||
/// State machine implementation of the Olm/Megolm encryption protocol
|
||||
/// used for Matrix end to end encryption.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OlmMachine {
|
||||
inner: matrix_sdk_crypto::OlmMachine,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OlmMachine {
|
||||
/// Constructor will always fail. To create a new `OlmMachine`, please use
|
||||
/// the `initialize` method.
|
||||
///
|
||||
/// Why this pattern? `initialize` returns a `Promise`. Returning a
|
||||
// `Promise` from a constructor is not idiomatic in JavaScript.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Result<OlmMachine, JsError> {
|
||||
Err(JsError::new("To build an `OlmMachine`, please use the `initialize` method"))
|
||||
}
|
||||
|
||||
/// Create a new `OlmMachine`.
|
||||
///
|
||||
/// The created machine will keep the encryption keys either in a IndexedDB
|
||||
/// based store, or in a memory store and once the objects is dropped,
|
||||
/// the keys will be lost.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - represents the unique ID of the user that owns this
|
||||
/// machine.
|
||||
///
|
||||
/// * `device_id` - represents the unique ID of the device
|
||||
/// that owns this machine.
|
||||
///
|
||||
/// * `store_name` - The name that should be used to open the IndexedDB
|
||||
/// based database. If this isn't provided, a memory-only store will be
|
||||
/// used. *Note* the memory-only store will lose your E2EE keys when the
|
||||
/// `OlmMachine` gets dropped.
|
||||
///
|
||||
/// * `store_passphrase` - The passphrase that should be used to encrypt the
|
||||
/// IndexedDB based
|
||||
pub fn initialize(
|
||||
user_id: &identifiers::UserId,
|
||||
device_id: &identifiers::DeviceId,
|
||||
store_name: Option<String>,
|
||||
store_passphrase: Option<String>,
|
||||
) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
let device_id = device_id.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match (store_name, store_passphrase) {
|
||||
(Some(store_name), Some(mut store_passphrase)) => {
|
||||
use zeroize::Zeroize;
|
||||
|
||||
let store = Some(
|
||||
matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase(
|
||||
&store_name,
|
||||
&store_passphrase,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
store_passphrase.zeroize();
|
||||
|
||||
store
|
||||
}
|
||||
|
||||
(Some(store_name), None) => Some(
|
||||
matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_name(&store_name).await?,
|
||||
),
|
||||
|
||||
(None, Some(_)) => {
|
||||
return Err(anyhow::Error::msg(
|
||||
"The `store_passphrase` has been set, but it has an effect only if \
|
||||
`store_name` is set, which is not; please provide one",
|
||||
))
|
||||
}
|
||||
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
Ok(OlmMachine {
|
||||
inner: match store {
|
||||
// We need this `#[cfg]` because `IndexeddbCryptoStore`
|
||||
// implements `CryptoStore` only on `target_arch =
|
||||
// "wasm32"`. Without that, we could have a compilation
|
||||
// error when checking the entire workspace. In practice,
|
||||
// it doesn't impact this crate because it's always
|
||||
// compiled for `wasm32`.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Some(store) => {
|
||||
matrix_sdk_crypto::OlmMachine::with_store(
|
||||
user_id.as_ref(),
|
||||
device_id.as_ref(),
|
||||
store,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
_ => {
|
||||
matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref())
|
||||
.await
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The unique user ID that owns this `OlmMachine` instance.
|
||||
#[wasm_bindgen(getter, js_name = "userId")]
|
||||
pub fn user_id(&self) -> identifiers::UserId {
|
||||
identifiers::UserId::from(self.inner.user_id().to_owned())
|
||||
}
|
||||
|
||||
/// The unique device ID that identifies this `OlmMachine`.
|
||||
#[wasm_bindgen(getter, js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> identifiers::DeviceId {
|
||||
identifiers::DeviceId::from(self.inner.device_id().to_owned())
|
||||
}
|
||||
|
||||
/// Get the public parts of our Olm identity keys.
|
||||
#[wasm_bindgen(getter, js_name = "identityKeys")]
|
||||
pub fn identity_keys(&self) -> vodozemac::IdentityKeys {
|
||||
self.inner.identity_keys().into()
|
||||
}
|
||||
|
||||
/// Get the display name of our own device.
|
||||
#[wasm_bindgen(getter, js_name = "displayName")]
|
||||
pub fn display_name(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.display_name().await?) })
|
||||
}
|
||||
|
||||
/// Get the list of users whose devices we are currently tracking.
|
||||
///
|
||||
/// A user can be marked for tracking using the
|
||||
/// [`update_tracked_users`](#method.update_tracked_users) method.
|
||||
///
|
||||
/// Returns a `Set<UserId>`.
|
||||
#[wasm_bindgen(js_name = "trackedUsers")]
|
||||
pub fn tracked_users(&self) -> Result<Promise, JsError> {
|
||||
let set = Set::new(&JsValue::UNDEFINED);
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
for user in me.tracked_users().await? {
|
||||
set.add(&identifiers::UserId::from(user).into());
|
||||
}
|
||||
Ok(set)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Update the list of tracked users.
|
||||
///
|
||||
/// The OlmMachine maintains a list of users whose devices we are keeping
|
||||
/// track of: these are known as "tracked users". These must be users
|
||||
/// that we share a room with, so that the server sends us updates for
|
||||
/// their device lists.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users` - An array of user ids that should be added to the list of
|
||||
/// tracked users
|
||||
///
|
||||
/// Any users that hadn't been seen before will be flagged for a key query
|
||||
/// immediately, and whenever `receive_sync_changes` receives a
|
||||
/// "changed" notification for that user in the future.
|
||||
///
|
||||
/// Users that were already in the list are unaffected.
|
||||
#[wasm_bindgen(js_name = "updateTrackedUsers")]
|
||||
pub fn update_tracked_users(&self, users: &Array) -> Result<Promise, JsError> {
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
me.update_tracked_users(users.iter().map(AsRef::as_ref)).await?;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Handle to-device events and one-time key counts from a sync
|
||||
/// response.
|
||||
///
|
||||
/// This will decrypt and handle to-device events returning the
|
||||
/// decrypted versions of them.
|
||||
///
|
||||
/// To decrypt an event from the room timeline call
|
||||
/// `decrypt_room_event`.
|
||||
#[wasm_bindgen(js_name = "receiveSyncChanges")]
|
||||
pub fn receive_sync_changes(
|
||||
&self,
|
||||
to_device_events: &str,
|
||||
changed_devices: &sync_events::DeviceLists,
|
||||
one_time_key_counts: &Map,
|
||||
unused_fallback_keys: &Set,
|
||||
) -> Result<Promise, JsError> {
|
||||
let to_device_events = serde_json::from_str(to_device_events)?;
|
||||
let changed_devices = changed_devices.inner.clone();
|
||||
let one_time_key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = one_time_key_counts
|
||||
.entries()
|
||||
.into_iter()
|
||||
.filter_map(|js_value| {
|
||||
let pair = Array::from(&js_value.ok()?);
|
||||
let (key, value) = (
|
||||
DeviceKeyAlgorithm::from(pair.at(0).as_string()?),
|
||||
UInt::new(pair.at(1).as_f64()? as u64)?,
|
||||
);
|
||||
|
||||
Some((key, value))
|
||||
})
|
||||
.collect();
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> = Some(
|
||||
unused_fallback_keys
|
||||
.values()
|
||||
.into_iter()
|
||||
.filter_map(|js_value| Some(DeviceKeyAlgorithm::from(js_value.ok()?.as_string()?)))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.receive_sync_changes(
|
||||
to_device_events,
|
||||
&changed_devices,
|
||||
&one_time_key_counts,
|
||||
unused_fallback_keys.as_deref(),
|
||||
)
|
||||
.await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the outgoing requests that need to be sent out.
|
||||
///
|
||||
/// This returns a list of `JsValue` to represent either:
|
||||
/// * `KeysUploadRequest`,
|
||||
/// * `KeysQueryRequest`,
|
||||
/// * `KeysClaimRequest`,
|
||||
/// * `ToDeviceRequest`,
|
||||
/// * `SignatureUploadRequest`,
|
||||
/// * `RoomMessageRequest` or
|
||||
/// * `KeysBackupRequest`.
|
||||
///
|
||||
/// Those requests need to be sent out to the server and the
|
||||
/// responses need to be passed back to the state machine using
|
||||
/// `mark_request_as_sent`.
|
||||
#[wasm_bindgen(js_name = "outgoingRequests")]
|
||||
pub fn outgoing_requests(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me
|
||||
.outgoing_requests()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(OutgoingRequest)
|
||||
.map(TryFrom::try_from)
|
||||
.collect::<Result<Vec<JsValue>, _>>()?
|
||||
.into_iter()
|
||||
.collect::<Array>())
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark the request with the given request ID as sent (see
|
||||
/// `outgoing_requests`).
|
||||
///
|
||||
/// Arguments are:
|
||||
///
|
||||
/// * `request_id` represents the unique ID of the request that was sent
|
||||
/// out. This is needed to couple the response with the now sent out
|
||||
/// request.
|
||||
/// * `response_type` represents the type of the request that was sent out.
|
||||
/// * `response` represents the response that was received from the server
|
||||
/// after the outgoing request was sent out.
|
||||
#[wasm_bindgen(js_name = "markRequestAsSent")]
|
||||
pub fn mark_request_as_sent(
|
||||
&self,
|
||||
request_id: &str,
|
||||
request_type: requests::RequestType,
|
||||
response: &str,
|
||||
) -> Result<Promise, JsError> {
|
||||
let transaction_id = OwnedTransactionId::from(request_id);
|
||||
let response = response_from_string(response)?;
|
||||
let incoming_response = responses::OwnedResponse::try_from((request_type, response))?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(me.mark_request_as_sent(&transaction_id, &incoming_response).await.map(|_| true)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encrypt a room message for the given room.
|
||||
///
|
||||
/// Beware that a room key needs to be shared before this
|
||||
/// method can be called using the `share_room_key` method.
|
||||
///
|
||||
/// `room_id` is the ID of the room for which the message should
|
||||
/// be encrypted. `event_type` is the type of the event. `content`
|
||||
/// is the plaintext content of the message that should be
|
||||
/// encrypted.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if a group session for the given room wasn't shared
|
||||
/// beforehand.
|
||||
#[wasm_bindgen(js_name = "encryptRoomEvent")]
|
||||
pub fn encrypt_room_event(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
event_type: String,
|
||||
content: &str,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let content: JsonValue = serde_json::from_str(content)?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.encrypt_room_event_raw(&room_id, content, event_type.as_ref()).await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Decrypt an event from a room timeline.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event`, the event that should be decrypted.
|
||||
/// * `room_id`, the ID of the room where the event was sent to.
|
||||
#[wasm_bindgen(js_name = "decryptRoomEvent")]
|
||||
pub fn decrypt_room_event(
|
||||
&self,
|
||||
event: &str,
|
||||
room_id: &identifiers::RoomId,
|
||||
) -> Result<Promise, JsError> {
|
||||
let event: Raw<_> = serde_json::from_str(event)?;
|
||||
let room_id = room_id.inner.clone();
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let room_event = me.decrypt_room_event(&event, room_id.as_ref()).await?;
|
||||
|
||||
Ok(responses::DecryptedRoomEvent::from(room_event))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the status of the private cross signing keys.
|
||||
///
|
||||
/// This can be used to check which private cross signing keys we
|
||||
/// have stored locally.
|
||||
#[wasm_bindgen(js_name = "crossSigningStatus")]
|
||||
pub fn cross_signing_status(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, olm::CrossSigningStatus>(async move {
|
||||
Ok(me.cross_signing_status().await.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Export all the private cross signing keys we have.
|
||||
///
|
||||
/// The export will contain the seed for the ed25519 keys as a
|
||||
/// unpadded base64 encoded string.
|
||||
///
|
||||
/// This method returns None if we don’t have any private cross
|
||||
/// signing keys.
|
||||
#[wasm_bindgen(js_name = "exportCrossSigningKeys")]
|
||||
pub fn export_cross_signing_keys(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me.export_cross_signing_keys().await.map(store::CrossSigningKeyExport::from))
|
||||
})
|
||||
}
|
||||
|
||||
/// Import our private cross signing keys.
|
||||
///
|
||||
/// The export needs to contain the seed for the ed25519 keys as
|
||||
/// an unpadded base64 encoded string.
|
||||
#[wasm_bindgen(js_name = "importCrossSigningKeys")]
|
||||
pub fn import_cross_signing_keys(&self, export: store::CrossSigningKeyExport) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
let export = export.inner;
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me.import_cross_signing_keys(export).await.map(olm::CrossSigningStatus::from)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new cross signing identity and get the upload request
|
||||
/// to push the new public keys to the server.
|
||||
///
|
||||
/// Warning: This will delete any existing cross signing keys that
|
||||
/// might exist on the server and thus will reset the trust
|
||||
/// between all the devices.
|
||||
///
|
||||
/// Uploading these keys will require user interactive auth.
|
||||
#[wasm_bindgen(js_name = "bootstrapCrossSigning")]
|
||||
pub fn bootstrap_cross_signing(&self, reset: bool) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
let (upload_signing_keys_request, upload_signatures_request) =
|
||||
me.bootstrap_cross_signing(reset).await?;
|
||||
|
||||
let tuple = Array::new();
|
||||
tuple.set(
|
||||
0,
|
||||
requests::SigningKeysUploadRequest::try_from(&upload_signing_keys_request)?.into(),
|
||||
);
|
||||
tuple.set(
|
||||
1,
|
||||
requests::SignatureUploadRequest::try_from(&upload_signatures_request)?.into(),
|
||||
);
|
||||
|
||||
Ok(tuple)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the cross signing user identity of a user.
|
||||
#[wasm_bindgen(js_name = "getIdentity")]
|
||||
pub fn get_identity(&self, user_id: &identifiers::UserId) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
let user_id = user_id.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me.get_identity(user_id.as_ref(), None).await?.map(identities::UserIdentities::from))
|
||||
})
|
||||
}
|
||||
|
||||
/// Sign the given message using our device key and if available
|
||||
/// cross-signing master key.
|
||||
pub fn sign(&self, message: String) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, types::Signatures>(async move { Ok(me.sign(&message).await.into()) })
|
||||
}
|
||||
|
||||
/// Invalidate the currently active outbound group session for the
|
||||
/// given room.
|
||||
///
|
||||
/// Returns true if a session was invalidated, false if there was
|
||||
/// no session to invalidate.
|
||||
#[wasm_bindgen(js_name = "invalidateGroupSession")]
|
||||
pub fn invalidate_group_session(&self, room_id: &identifiers::RoomId) -> Promise {
|
||||
let room_id = room_id.inner.clone();
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.invalidate_group_session(&room_id).await?) })
|
||||
}
|
||||
|
||||
/// Get to-device requests to share a room key with users in a room.
|
||||
///
|
||||
/// `room_id` is the room ID. `users` is an array of `UserId`
|
||||
/// objects. `encryption_settings` are an `EncryptionSettings`
|
||||
/// object.
|
||||
///
|
||||
/// Returns an array of `ToDeviceRequest`s.
|
||||
#[wasm_bindgen(js_name = "shareRoomKey")]
|
||||
pub fn share_room_key(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
users: &Array,
|
||||
encryption_settings: &encryption::EncryptionSettings,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
let encryption_settings =
|
||||
matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let to_device_requests = me
|
||||
.share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings)
|
||||
.await?;
|
||||
|
||||
// convert each request to our own ToDeviceRequest struct, and then wrap it in a
|
||||
// JsValue.
|
||||
//
|
||||
// Then collect the results into a javascript Array, throwing any errors into
|
||||
// the promise.
|
||||
Ok(to_device_requests
|
||||
.into_iter()
|
||||
.map(|td| ToDeviceRequest::try_from(td.deref()).map(JsValue::from))
|
||||
.collect::<Result<Array, _>>()?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the a key claiming request for the user/device pairs that
|
||||
/// we are missing Olm sessions for.
|
||||
///
|
||||
/// Returns `null` if no key claiming request needs to be sent
|
||||
/// out, otherwise it returns a `KeysClaimRequest` object.
|
||||
///
|
||||
/// Sessions need to be established between devices so group
|
||||
/// sessions for a room can be shared with them.
|
||||
///
|
||||
/// This should be called every time a group session needs to be
|
||||
/// shared as well as between sync calls. After a sync some
|
||||
/// devices may request room keys without us having a valid Olm
|
||||
/// session with them, making it impossible to server the room key
|
||||
/// request, thus it’s necessary to check for missing sessions
|
||||
/// between sync as well.
|
||||
///
|
||||
/// Note: Care should be taken that only one such request at a
|
||||
/// time is in flight, e.g. using a lock.
|
||||
///
|
||||
/// The response of a successful key claiming requests needs to be
|
||||
/// passed to the `OlmMachine` with the `mark_request_as_sent`.
|
||||
///
|
||||
/// `users` represents the list of users that we should check if
|
||||
/// we lack a session with one of their devices. This can be an
|
||||
/// empty iterator when calling this method between sync requests.
|
||||
#[wasm_bindgen(js_name = "getMissingSessions")]
|
||||
pub fn get_missing_sessions(&self, users: &Array) -> Result<Promise, JsError> {
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
match me.get_missing_sessions(users.iter().map(AsRef::as_ref)).await? {
|
||||
Some((transaction_id, keys_claim_request)) => {
|
||||
Ok(JsValue::from(requests::KeysClaimRequest::try_from((
|
||||
transaction_id.to_string(),
|
||||
&keys_claim_request,
|
||||
))?))
|
||||
}
|
||||
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a map holding all the devices of a user.
|
||||
///
|
||||
/// `user_id` represents the unique ID of the user that the
|
||||
/// devices belong to.
|
||||
#[wasm_bindgen(js_name = "getUserDevices")]
|
||||
pub fn get_user_devices(&self, user_id: &identifiers::UserId) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, device::UserDevices>(async move {
|
||||
// wait for up to a second for any in-flight device list requests to complete.
|
||||
// The reason for this isn't so much to avoid races (some level of raciness is
|
||||
// inevitable for this method) but to make testing easier.
|
||||
Ok(me.get_user_devices(&user_id, Some(Duration::from_secs(1))).await.map(Into::into)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a specific device of a user if one is found and the crypto store
|
||||
/// didn't throw an error.
|
||||
///
|
||||
/// `user_id` represents the unique ID of the user that the
|
||||
/// identity belongs to. `device_id` represents the unique ID of
|
||||
/// the device.
|
||||
#[wasm_bindgen(js_name = "getDevice")]
|
||||
pub fn get_device(
|
||||
&self,
|
||||
user_id: &identifiers::UserId,
|
||||
device_id: &identifiers::DeviceId,
|
||||
) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
let device_id = device_id.inner.clone();
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise::<_, Option<device::Device>>(async move {
|
||||
Ok(me.get_device(&user_id, &device_id, None).await?.map(Into::into))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a verification object for the given user ID with the given
|
||||
/// flow ID (a to-device request ID if the verification has been
|
||||
/// requested by a to-device request, or a room event ID if the
|
||||
/// verification has been requested by a room event).
|
||||
///
|
||||
/// It returns a “`Verification` object”, which is either a `Sas`
|
||||
/// or `Qr` object.
|
||||
#[wasm_bindgen(js_name = "getVerification")]
|
||||
pub fn get_verification(
|
||||
&self,
|
||||
user_id: &identifiers::UserId,
|
||||
flow_id: &str,
|
||||
) -> Result<JsValue, JsError> {
|
||||
self.inner
|
||||
.get_verification(&user_id.inner, flow_id)
|
||||
.map(verification::Verification)
|
||||
.map(JsValue::try_from)
|
||||
.transpose()
|
||||
.map(JsValue::from)
|
||||
}
|
||||
|
||||
/// Get a verification request object with the given flow ID.
|
||||
#[wasm_bindgen(js_name = "getVerificationRequest")]
|
||||
pub fn get_verification_request(
|
||||
&self,
|
||||
user_id: &identifiers::UserId,
|
||||
flow_id: &str,
|
||||
) -> Option<verification::VerificationRequest> {
|
||||
self.inner.get_verification_request(&user_id.inner, flow_id).map(Into::into)
|
||||
}
|
||||
|
||||
/// Get all the verification requests of a given user.
|
||||
#[wasm_bindgen(js_name = "getVerificationRequests")]
|
||||
pub fn get_verification_requests(&self, user_id: &identifiers::UserId) -> Array {
|
||||
self.inner
|
||||
.get_verification_requests(&user_id.inner)
|
||||
.into_iter()
|
||||
.map(verification::VerificationRequest::from)
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Receive a verification event.
|
||||
///
|
||||
/// This method can be used to pass verification events that are happening
|
||||
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
|
||||
#[wasm_bindgen(js_name = "receiveVerificationEvent")]
|
||||
pub fn receive_verification_event(
|
||||
&self,
|
||||
event: &str,
|
||||
room_id: &identifiers::RoomId,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let event: ruma::events::AnySyncMessageLikeEvent = serde_json::from_str(event)?;
|
||||
let event = event.into_full_event(room_id);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(me.receive_verification_event(&event).await.map(|_| JsValue::UNDEFINED)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Export the keys that match the given predicate.
|
||||
///
|
||||
/// `predicate` is a closure that will be called for every known
|
||||
/// `InboundGroupSession`, which represents a room key. If the closure
|
||||
/// returns `true`, the `InboundGroupSession` will be included in the
|
||||
/// export, otherwise it won't.
|
||||
#[wasm_bindgen(js_name = "exportRoomKeys")]
|
||||
pub fn export_room_keys(&self, predicate: Function) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.export_room_keys(|session| {
|
||||
let session = session.clone();
|
||||
|
||||
predicate
|
||||
.call1(&JsValue::NULL, &olm::InboundGroupSession::from(session).into())
|
||||
.expect("Predicate function passed to `export_room_keys` failed")
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.await?,
|
||||
)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
///
|
||||
/// `exported_keys` is a list of previously exported keys that should be
|
||||
/// imported into our store. If we already have a better version of a key,
|
||||
/// the key will _not_ be imported.
|
||||
///
|
||||
/// `progress_listener` is a closure that takes 2 arguments: `progress` and
|
||||
/// `total`, and returns nothing.
|
||||
#[wasm_bindgen(js_name = "importRoomKeys")]
|
||||
pub fn import_room_keys(
|
||||
&self,
|
||||
exported_room_keys: &str,
|
||||
progress_listener: Function,
|
||||
) -> Result<Promise, JsError> {
|
||||
let me = self.inner.clone();
|
||||
let exported_room_keys: Vec<matrix_sdk_crypto::olm::ExportedRoomKey> =
|
||||
serde_json::from_str(exported_room_keys)?;
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
let matrix_sdk_crypto::RoomKeyImportResult { imported_count, total_count, keys } = me
|
||||
.import_room_keys(exported_room_keys, false, |progress, total| {
|
||||
let progress: u64 = progress.try_into().unwrap();
|
||||
let total: u64 = total.try_into().unwrap();
|
||||
|
||||
progress_listener
|
||||
.call2(&JsValue::NULL, &JsValue::from(progress), &JsValue::from(total))
|
||||
.expect("Progress listener passed to `import_room_keys` failed");
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(serde_json::to_string(&json!({
|
||||
"imported_count": imported_count,
|
||||
"total_count": total_count,
|
||||
"keys": keys,
|
||||
}))?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encrypt the list of exported room keys using the given passphrase.
|
||||
///
|
||||
/// `exported_room_keys` is a list of sessions that should be encrypted
|
||||
/// (it's generally returned by `export_room_keys`). `passphrase` is the
|
||||
/// passphrase that will be used to encrypt the exported room keys. And
|
||||
/// `rounds` is the number of rounds that should be used for the key
|
||||
/// derivation when the passphrase gets turned into an AES key. More rounds
|
||||
/// are increasingly computationnally intensive and as such help against
|
||||
/// brute-force attacks. Should be at least `10_000`, while values in the
|
||||
/// `100_000` ranges should be preferred.
|
||||
#[wasm_bindgen(js_name = "encryptExportedRoomKeys")]
|
||||
pub fn encrypt_exported_room_keys(
|
||||
exported_room_keys: &str,
|
||||
passphrase: &str,
|
||||
rounds: u32,
|
||||
) -> Result<String, JsError> {
|
||||
let exported_room_keys: Vec<matrix_sdk_crypto::olm::ExportedRoomKey> =
|
||||
serde_json::from_str(exported_room_keys)?;
|
||||
|
||||
Ok(matrix_sdk_crypto::encrypt_room_key_export(&exported_room_keys, passphrase, rounds)?)
|
||||
}
|
||||
|
||||
/// Try to decrypt a reader into a list of exported room keys.
|
||||
///
|
||||
/// `encrypted_exported_room_keys` is the result from
|
||||
/// `encrypt_exported_room_keys`. `passphrase` is the passphrase that was
|
||||
/// used when calling `encrypt_exported_room_keys`.
|
||||
#[wasm_bindgen(js_name = "decryptExportedRoomKeys")]
|
||||
pub fn decrypt_exported_room_keys(
|
||||
encrypted_exported_room_keys: &str,
|
||||
passphrase: &str,
|
||||
) -> Result<String, JsError> {
|
||||
Ok(serde_json::to_string(&matrix_sdk_crypto::decrypt_room_key_export(
|
||||
encrypted_exported_room_keys.as_bytes(),
|
||||
passphrase,
|
||||
)?)?)
|
||||
}
|
||||
|
||||
/// Register a callback which will be called whenever there is an update to
|
||||
/// a room key.
|
||||
///
|
||||
/// `callback` should be a function that takes a single argument (an array
|
||||
/// of {@link RoomKeyInfo}) and returns a Promise.
|
||||
#[wasm_bindgen(js_name = "registerRoomKeyUpdatedCallback")]
|
||||
pub async fn register_room_key_updated_callback(&self, callback: Function) {
|
||||
let stream = self.inner.store().room_keys_received_stream();
|
||||
|
||||
// fire up a promise chain which will call `cb` on each result from the stream
|
||||
spawn_local(async move {
|
||||
// take a reference to `callback` (which we then pass into the closure), to stop
|
||||
// the callback being moved into the closure (which would mean we could only
|
||||
// call the closure once)
|
||||
let callback_ref = &callback;
|
||||
stream.for_each(move |item| send_room_key_info_to_callback(callback_ref, item)).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Shut down the `OlmMachine`.
|
||||
///
|
||||
/// The `OlmMachine` cannot be used after this method has been called.
|
||||
///
|
||||
/// All associated resources will be closed too, like IndexedDB
|
||||
/// connections.
|
||||
pub fn close(self) {}
|
||||
}
|
||||
|
||||
// helper for register_room_key_received_callback: wraps the key info
|
||||
// into our own RoomKeyInfo struct, and passes it into the javascript
|
||||
// function
|
||||
async fn send_room_key_info_to_callback(
|
||||
callback: &Function,
|
||||
room_key_info: Vec<matrix_sdk_crypto::store::RoomKeyInfo>,
|
||||
) {
|
||||
let rki: Array = room_key_info.into_iter().map(RoomKeyInfo::from).map(JsValue::from).collect();
|
||||
match promise_result_to_future(callback.call1(&JsValue::NULL, &rki)).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
warn!("Error calling room-key-received callback: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a result from a javascript function which returns a Promise (or throws
|
||||
/// an exception before returning one), convert the result to a rust Future
|
||||
/// which completes with the result of the promise
|
||||
async fn promise_result_to_future(res: Result<JsValue, JsValue>) -> Result<JsValue, JsValue> {
|
||||
match res {
|
||||
Ok(retval) => {
|
||||
if !retval.has_type::<Promise>() {
|
||||
panic!("not a promise");
|
||||
}
|
||||
let prom: Promise = retval.dyn_into().map_err(|v| {
|
||||
JsError::new(&format!("function returned a non-Promise value {v:?}"))
|
||||
})?;
|
||||
JsFuture::from(prom).await
|
||||
}
|
||||
Err(e) => {
|
||||
// the function threw an exception before it returned the promise. We can just
|
||||
// return the error as an error result.
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// We have the following pattern quite often in our code:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct Foo {
|
||||
/// inner: Bar,
|
||||
/// }
|
||||
///
|
||||
/// impl From<Bar> for Foo {
|
||||
/// fn from(inner: Bar) -> Self {
|
||||
/// Self { inner }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Because I feel lazy, let's do a macro to write this:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct Foo {
|
||||
/// inner: Bar,
|
||||
/// }
|
||||
///
|
||||
/// impl_from_to_inner!(Bar => Foo);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! impl_from_to_inner {
|
||||
($from:ty => $to:ty) => {
|
||||
impl From<$from> for $to {
|
||||
fn from(inner: $from) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Olm types.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{identifiers, impl_from_to_inner, vodozemac::Curve25519PublicKey};
|
||||
|
||||
/// Struct representing the state of our private cross signing keys,
|
||||
/// it shows which private cross signing keys we have locally stored.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct CrossSigningStatus {
|
||||
inner: matrix_sdk_crypto::olm::CrossSigningStatus,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::olm::CrossSigningStatus => CrossSigningStatus);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl CrossSigningStatus {
|
||||
/// Do we have the master key?
|
||||
#[wasm_bindgen(getter, js_name = "hasMaster")]
|
||||
pub fn has_master(&self) -> bool {
|
||||
self.inner.has_master
|
||||
}
|
||||
|
||||
/// Do we have the self signing key? This one is necessary to sign
|
||||
/// our own devices.
|
||||
#[wasm_bindgen(getter, js_name = "hasSelfSigning")]
|
||||
pub fn has_self_signing(&self) -> bool {
|
||||
self.inner.has_self_signing
|
||||
}
|
||||
|
||||
/// Do we have the user signing key? This one is necessary to sign
|
||||
/// other users.
|
||||
#[wasm_bindgen(getter, js_name = "hasUserSigning")]
|
||||
pub fn has_user_signing(&self) -> bool {
|
||||
self.inner.has_user_signing
|
||||
}
|
||||
}
|
||||
|
||||
/// Inbound group session.
|
||||
///
|
||||
/// Inbound group sessions are used to exchange room messages between a group of
|
||||
/// participants. Inbound group sessions are used to decrypt the room messages.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct InboundGroupSession {
|
||||
inner: matrix_sdk_crypto::olm::InboundGroupSession,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::olm::InboundGroupSession => InboundGroupSession);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl InboundGroupSession {
|
||||
/// The room where this session is used in.
|
||||
#[wasm_bindgen(getter, js_name = "roomId")]
|
||||
pub fn room_id(&self) -> identifiers::RoomId {
|
||||
self.inner.room_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// The Curve25519 key of the sender of this session, as a
|
||||
/// [Curve25519PublicKey].
|
||||
#[wasm_bindgen(getter, js_name = "senderKey")]
|
||||
pub fn sender_key(&self) -> Curve25519PublicKey {
|
||||
self.inner.sender_key().into()
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
#[wasm_bindgen(getter, js_name = "sessionId")]
|
||||
pub fn session_id(&self) -> String {
|
||||
self.inner.session_id().to_owned()
|
||||
}
|
||||
|
||||
/// Has the session been imported from a file or server-side backup? As
|
||||
/// opposed to being directly received as an `m.room_key` event.
|
||||
#[wasm_bindgen(js_name = "hasBeenImported")]
|
||||
pub fn has_been_imported(&self) -> bool {
|
||||
self.inner.has_been_imported()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
//! Types to handle requests.
|
||||
|
||||
use js_sys::JsString;
|
||||
use matrix_sdk_crypto::{
|
||||
requests::{
|
||||
KeysBackupRequest as OriginalKeysBackupRequest,
|
||||
KeysQueryRequest as OriginalKeysQueryRequest,
|
||||
RoomMessageRequest as OriginalRoomMessageRequest,
|
||||
ToDeviceRequest as OriginalToDeviceRequest,
|
||||
UploadSigningKeysRequest as OriginalUploadSigningKeysRequest,
|
||||
},
|
||||
OutgoingRequests,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::keys::{
|
||||
claim_keys::v3::Request as OriginalKeysClaimRequest,
|
||||
upload_keys::v3::Request as OriginalKeysUploadRequest,
|
||||
upload_signatures::v3::Request as OriginalSignatureUploadRequest,
|
||||
},
|
||||
events::EventContent,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/** Outgoing Requests * */
|
||||
|
||||
/// Data for a request to the `/keys/upload` API endpoint
|
||||
/// ([specification]).
|
||||
///
|
||||
/// Publishes end-to-end encryption keys for the device.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysupload
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysUploadRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `device_keys`,
|
||||
/// `one_time_keys`, `fallback_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysUploadRequest {
|
||||
/// Create a new `KeysUploadRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysUploadRequest {
|
||||
Self { id: Some(id), body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::KeysUpload
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for a request to the `/keys/query` API endpoint
|
||||
/// ([specification]).
|
||||
///
|
||||
/// Returns the current devices and identity keys for the given users.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysquery
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysQueryRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `timeout`,
|
||||
/// `device_keys`, `token`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysQueryRequest {
|
||||
/// Create a new `KeysQueryRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysQueryRequest {
|
||||
Self { id: Some(id), body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::KeysQuery
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for a request to the `/keys/claim` API endpoint
|
||||
/// ([specification]).
|
||||
///
|
||||
/// Claims one-time keys that can be used to establish 1-to-1 E2EE
|
||||
/// sessions.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysclaim
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysClaimRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `timeout`,
|
||||
/// `one_time_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysClaimRequest {
|
||||
/// Create a new `KeysClaimRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysClaimRequest {
|
||||
Self { id: Some(id), body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::KeysClaim
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for a request to the `/sendToDevice` API endpoint
|
||||
/// ([specification]).
|
||||
///
|
||||
/// Send an event to a single device or to a group of devices.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3sendtodeviceeventtypetxnid
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct ToDeviceRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A string representing the type of event being sent to each devices.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub event_type: JsString,
|
||||
|
||||
/// A string representing a request identifier unique to the access token
|
||||
/// used to send the request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub txn_id: JsString,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `messages`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ToDeviceRequest {
|
||||
/// Create a new `ToDeviceRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
id: JsString,
|
||||
event_type: JsString,
|
||||
txn_id: JsString,
|
||||
body: JsString,
|
||||
) -> ToDeviceRequest {
|
||||
Self { id: Some(id), event_type, txn_id, body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::ToDevice
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for a request to the `/keys/signatures/upload` API endpoint
|
||||
/// ([specification]).
|
||||
///
|
||||
/// Publishes cross-signing signatures for the user.
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keyssignaturesupload
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct SignatureUploadRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `signed_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SignatureUploadRequest {
|
||||
/// Create a new `SignatureUploadRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> SignatureUploadRequest {
|
||||
Self { id: Some(id), body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::SignatureUpload
|
||||
}
|
||||
}
|
||||
|
||||
/// A customized owned request type for sending out room messages
|
||||
/// ([specification]).
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct RoomMessageRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A string representing the room to send the event to.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub room_id: JsString,
|
||||
|
||||
/// A string representing the transaction ID for this event.
|
||||
///
|
||||
/// Clients should generate an ID unique across requests with the same
|
||||
/// access token; it will be used by the server to ensure idempotency of
|
||||
/// requests.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub txn_id: JsString,
|
||||
|
||||
/// A string representing the type of event to be sent.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub event_type: JsString,
|
||||
|
||||
/// A JSON-encoded string containing the message's content.
|
||||
#[wasm_bindgen(readonly, js_name = "body")]
|
||||
pub content: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomMessageRequest {
|
||||
/// Create a new `RoomMessageRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
id: JsString,
|
||||
room_id: JsString,
|
||||
txn_id: JsString,
|
||||
event_type: JsString,
|
||||
content: JsString,
|
||||
) -> RoomMessageRequest {
|
||||
Self { id: Some(id), room_id, txn_id, event_type, content }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::RoomMessage
|
||||
}
|
||||
}
|
||||
|
||||
/// A request that will back up a batch of room keys to the server
|
||||
/// ([specification]).
|
||||
///
|
||||
/// [specification]: https://spec.matrix.org/unstable/client-server-api/#put_matrixclientv3room_keyskeys
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub struct KeysBackupRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `rooms`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl KeysBackupRequest {
|
||||
/// Create a new `KeysBackupRequest`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: JsString, body: JsString) -> KeysBackupRequest {
|
||||
Self { id: Some(id), body }
|
||||
}
|
||||
|
||||
/// Get its request type.
|
||||
#[wasm_bindgen(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::KeysBackup
|
||||
}
|
||||
}
|
||||
|
||||
/** Other Requests * */
|
||||
|
||||
/// Request that will publish a cross signing identity.
|
||||
///
|
||||
/// This uploads the public cross signing key triplet.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct SigningKeysUploadRequest {
|
||||
/// The request ID.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub id: Option<JsString>,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `master_key`,
|
||||
/// `self_signing_key`, `user_signing_key`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub body: JsString,
|
||||
}
|
||||
|
||||
macro_rules! request {
|
||||
(
|
||||
$destination_request:ident from $source_request:ident
|
||||
$( extracts $( $field_name:ident : $field_type:tt ),+ $(,)? )?
|
||||
$( $( and )? groups $( $grouped_field_name:ident ),+ $(,)? )?
|
||||
) => {
|
||||
impl TryFrom<&$source_request> for $destination_request {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(request: &$source_request) -> Result<Self, Self::Error> {
|
||||
request!(
|
||||
@__try_from $destination_request from $source_request
|
||||
(request_id = None, request = request)
|
||||
$( extracts [ $( $field_name : $field_type, )+ ] )?
|
||||
$( groups [ $( $grouped_field_name, )+ ] )?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(String, &$source_request)> for $destination_request {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(
|
||||
(request_id, request): (String, &$source_request),
|
||||
) -> Result<Self, Self::Error> {
|
||||
request!(
|
||||
@__try_from $destination_request from $source_request
|
||||
(request_id = Some(request_id.into()), request = request)
|
||||
$( extracts [ $( $field_name : $field_type, )+ ] )?
|
||||
$( groups [ $( $grouped_field_name, )+ ] )?
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@__try_from $destination_request:ident from $source_request:ident
|
||||
(request_id = $request_id:expr, request = $request:expr)
|
||||
$( extracts [ $( $field_name:ident : $field_type:tt ),* $(,)? ] )?
|
||||
$( groups [ $( $grouped_field_name:ident ),* $(,)? ] )?
|
||||
) => {
|
||||
{
|
||||
Ok($destination_request {
|
||||
id: $request_id,
|
||||
$(
|
||||
$(
|
||||
$field_name: request!(@__field $field_name : $field_type ; request = $request),
|
||||
)*
|
||||
)?
|
||||
$(
|
||||
body: {
|
||||
let mut map = serde_json::Map::new();
|
||||
$(
|
||||
map.insert(stringify!($grouped_field_name).to_owned(), serde_json::to_value(&$request.$grouped_field_name).unwrap());
|
||||
)*
|
||||
let object = serde_json::Value::Object(map);
|
||||
|
||||
serde_json::to_string(&object)?.into()
|
||||
}
|
||||
)?
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
( @__field $field_name:ident : $field_type:ident ; request = $request:expr ) => {
|
||||
request!(@__field_type as $field_type ; request = $request, field_name = $field_name)
|
||||
};
|
||||
|
||||
( @__field_type as string ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
$request.$field_name.to_string().into()
|
||||
};
|
||||
|
||||
( @__field_type as json ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
serde_json::to_string(&$request.$field_name)?.into()
|
||||
};
|
||||
|
||||
( @__field_type as event_type ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
$request.content.event_type().to_string().into()
|
||||
};
|
||||
}
|
||||
|
||||
// Outgoing Requests
|
||||
request!(KeysUploadRequest from OriginalKeysUploadRequest groups device_keys, one_time_keys, fallback_keys);
|
||||
request!(KeysQueryRequest from OriginalKeysQueryRequest groups timeout, device_keys, token);
|
||||
request!(KeysClaimRequest from OriginalKeysClaimRequest groups timeout, one_time_keys);
|
||||
request!(ToDeviceRequest from OriginalToDeviceRequest extracts event_type: string, txn_id: string and groups messages);
|
||||
request!(SignatureUploadRequest from OriginalSignatureUploadRequest groups signed_keys);
|
||||
request!(RoomMessageRequest from OriginalRoomMessageRequest extracts room_id: string, txn_id: string, event_type: event_type, content: json);
|
||||
request!(KeysBackupRequest from OriginalKeysBackupRequest groups rooms);
|
||||
|
||||
// Other Requests
|
||||
request!(SigningKeysUploadRequest from OriginalUploadSigningKeysRequest groups master_key, self_signing_key, user_signing_key);
|
||||
|
||||
// JavaScript has no complex enums like Rust. To return structs of
|
||||
// different types, we have no choice that hiding everything behind a
|
||||
// `JsValue`.
|
||||
pub(crate) struct OutgoingRequest(pub(crate) matrix_sdk_crypto::OutgoingRequest);
|
||||
|
||||
impl TryFrom<OutgoingRequest> for JsValue {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(outgoing_request: OutgoingRequest) -> Result<Self, Self::Error> {
|
||||
let request_id = outgoing_request.0.request_id().to_string();
|
||||
|
||||
Ok(match outgoing_request.0.request() {
|
||||
OutgoingRequests::KeysUpload(request) => {
|
||||
JsValue::from(KeysUploadRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::KeysQuery(request) => {
|
||||
JsValue::from(KeysQueryRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::KeysClaim(request) => {
|
||||
JsValue::from(KeysClaimRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
JsValue::from(ToDeviceRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::SignatureUpload(request) => {
|
||||
JsValue::from(SignatureUploadRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::RoomMessage(request) => {
|
||||
JsValue::from(RoomMessageRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
OutgoingRequests::KeysBackup(request) => {
|
||||
JsValue::from(KeysBackupRequest::try_from((request_id, request))?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the type of a request.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum RequestType {
|
||||
/// Represents a `KeysUploadRequest`.
|
||||
KeysUpload,
|
||||
|
||||
/// Represents a `KeysQueryRequest`.
|
||||
KeysQuery,
|
||||
|
||||
/// Represents a `KeysClaimRequest`.
|
||||
KeysClaim,
|
||||
|
||||
/// Represents a `ToDeviceRequest`.
|
||||
ToDevice,
|
||||
|
||||
/// Represents a `SignatureUploadRequest`.
|
||||
SignatureUpload,
|
||||
|
||||
/// Represents a `RoomMessageRequest`.
|
||||
RoomMessage,
|
||||
|
||||
/// Represents a `KeysBackupRequest`.
|
||||
KeysBackup,
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Types related to responses.
|
||||
|
||||
use js_sys::{Array, JsString};
|
||||
use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo};
|
||||
use matrix_sdk_crypto::IncomingResponse;
|
||||
pub(crate) use ruma::api::client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
claim_keys::v3::Response as KeysClaimResponse, get_keys::v3::Response as KeysQueryResponse,
|
||||
upload_keys::v3::Response as KeysUploadResponse,
|
||||
upload_signatures::v3::Response as SignatureUploadResponse,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
};
|
||||
use ruma::api::IncomingResponse as RumaIncomingResponse;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{encryption, identifiers, requests::RequestType};
|
||||
|
||||
pub(crate) fn response_from_string(body: &str) -> http::Result<http::Response<Vec<u8>>> {
|
||||
http::Response::builder().status(200).body(body.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
/// Intermediate private type to store an incoming owned response,
|
||||
/// without the need to manage lifetime.
|
||||
pub(crate) enum OwnedResponse {
|
||||
KeysUpload(KeysUploadResponse),
|
||||
KeysQuery(KeysQueryResponse),
|
||||
KeysClaim(KeysClaimResponse),
|
||||
ToDevice(ToDeviceResponse),
|
||||
SignatureUpload(SignatureUploadResponse),
|
||||
RoomMessage(RoomMessageResponse),
|
||||
KeysBackup(KeysBackupResponse),
|
||||
}
|
||||
|
||||
impl From<KeysUploadResponse> for OwnedResponse {
|
||||
fn from(response: KeysUploadResponse) -> Self {
|
||||
OwnedResponse::KeysUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysQueryResponse> for OwnedResponse {
|
||||
fn from(response: KeysQueryResponse) -> Self {
|
||||
OwnedResponse::KeysQuery(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysClaimResponse> for OwnedResponse {
|
||||
fn from(response: KeysClaimResponse) -> Self {
|
||||
OwnedResponse::KeysClaim(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceResponse> for OwnedResponse {
|
||||
fn from(response: ToDeviceResponse) -> Self {
|
||||
OwnedResponse::ToDevice(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureUploadResponse> for OwnedResponse {
|
||||
fn from(response: SignatureUploadResponse) -> Self {
|
||||
Self::SignatureUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomMessageResponse> for OwnedResponse {
|
||||
fn from(response: RoomMessageResponse) -> Self {
|
||||
OwnedResponse::RoomMessage(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysBackupResponse> for OwnedResponse {
|
||||
fn from(r: KeysBackupResponse) -> Self {
|
||||
Self::KeysBackup(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(RequestType, http::Response<Vec<u8>>)> for OwnedResponse {
|
||||
type Error = JsError;
|
||||
|
||||
fn try_from(
|
||||
(request_type, response): (RequestType, http::Response<Vec<u8>>),
|
||||
) -> Result<Self, Self::Error> {
|
||||
match request_type {
|
||||
RequestType::KeysUpload => {
|
||||
KeysUploadResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::KeysQuery => {
|
||||
KeysQueryResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::KeysClaim => {
|
||||
KeysClaimResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::ToDevice => {
|
||||
ToDeviceResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::SignatureUpload => {
|
||||
SignatureUploadResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::RoomMessage => {
|
||||
RoomMessageResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
|
||||
RequestType::KeysBackup => {
|
||||
KeysBackupResponse::try_from_http_response(response).map(Into::into)
|
||||
}
|
||||
}
|
||||
.map_err(JsError::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a OwnedResponse) -> Self {
|
||||
match response {
|
||||
OwnedResponse::KeysUpload(response) => IncomingResponse::KeysUpload(response),
|
||||
OwnedResponse::KeysQuery(response) => IncomingResponse::KeysQuery(response),
|
||||
OwnedResponse::KeysClaim(response) => IncomingResponse::KeysClaim(response),
|
||||
OwnedResponse::ToDevice(response) => IncomingResponse::ToDevice(response),
|
||||
OwnedResponse::SignatureUpload(response) => IncomingResponse::SignatureUpload(response),
|
||||
OwnedResponse::RoomMessage(response) => IncomingResponse::RoomMessage(response),
|
||||
OwnedResponse::KeysBackup(response) => IncomingResponse::KeysBackup(response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A decrypted room event.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct DecryptedRoomEvent {
|
||||
/// The JSON-encoded decrypted event.
|
||||
#[wasm_bindgen(readonly)]
|
||||
pub event: JsString,
|
||||
|
||||
encryption_info: Option<EncryptionInfo>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DecryptedRoomEvent {
|
||||
/// The user ID of the event sender, note this is untrusted data
|
||||
/// unless the `verification_state` is as well trusted.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sender(&self) -> Option<identifiers::UserId> {
|
||||
Some(identifiers::UserId::from(self.encryption_info.as_ref()?.sender.clone()))
|
||||
}
|
||||
|
||||
/// The device ID of the device that sent us the event, note this
|
||||
/// is untrusted data unless `verification_state` is as well
|
||||
/// trusted.
|
||||
#[wasm_bindgen(getter, js_name = "senderDevice")]
|
||||
pub fn sender_device(&self) -> Option<identifiers::DeviceId> {
|
||||
Some(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into())
|
||||
}
|
||||
|
||||
/// The Curve25519 key of the device that created the megolm
|
||||
/// decryption key originally.
|
||||
#[wasm_bindgen(getter, js_name = "senderCurve25519Key")]
|
||||
pub fn sender_curve25519_key(&self) -> Option<JsString> {
|
||||
Some(match &self.encryption_info.as_ref()?.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => curve25519_key.clone().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The signing Ed25519 key that have created the megolm key that
|
||||
/// was used to decrypt this session.
|
||||
#[wasm_bindgen(getter, js_name = "senderClaimedEd25519Key")]
|
||||
pub fn sender_claimed_ed25519_key(&self) -> Option<JsString> {
|
||||
match &self.encryption_info.as_ref()?.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 { sender_claimed_keys, .. } => {
|
||||
sender_claimed_keys.get(&ruma::DeviceKeyAlgorithm::Ed25519).cloned().map(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chain of Curve25519 keys through which this session was
|
||||
/// forwarded, via `m.forwarded_room_key` events.
|
||||
#[wasm_bindgen(getter, js_name = "forwardingCurve25519KeyChain")]
|
||||
pub fn forwarding_curve25519_key_chain(&self) -> Array {
|
||||
Array::new()
|
||||
}
|
||||
|
||||
/// The verification state of the device that sent us the event,
|
||||
/// note this is the state of the device at the time of
|
||||
/// decryption. It may change in the future if a device gets
|
||||
/// verified or deleted.
|
||||
#[wasm_bindgen(js_name = "shieldState")]
|
||||
pub fn shield_state(&self, strict: bool) -> Option<encryption::ShieldState> {
|
||||
let state = &self.encryption_info.as_ref()?.verification_state;
|
||||
|
||||
if strict {
|
||||
Some(state.to_shield_state_strict().into())
|
||||
} else {
|
||||
Some(state.to_shield_state_lax().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_common::deserialized_responses::TimelineEvent> for DecryptedRoomEvent {
|
||||
fn from(value: matrix_sdk_common::deserialized_responses::TimelineEvent) -> Self {
|
||||
Self {
|
||||
event: value.event.json().get().to_owned().into(),
|
||||
encryption_info: value.encryption_info,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Store types.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
encryption::EncryptionAlgorithm, identifiers::RoomId, impl_from_to_inner,
|
||||
vodozemac::Curve25519PublicKey,
|
||||
};
|
||||
|
||||
/// A struct containing private cross signing keys that can be backed
|
||||
/// up or uploaded to the secret store.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct CrossSigningKeyExport {
|
||||
pub(crate) inner: matrix_sdk_crypto::store::CrossSigningKeyExport,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::store::CrossSigningKeyExport => CrossSigningKeyExport);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl CrossSigningKeyExport {
|
||||
/// The seed of the master key encoded as unpadded base64.
|
||||
#[wasm_bindgen(getter, js_name = "masterKey")]
|
||||
pub fn master_key(&self) -> Option<String> {
|
||||
self.inner.master_key.clone()
|
||||
}
|
||||
|
||||
/// The seed of the self signing key encoded as unpadded base64.
|
||||
#[wasm_bindgen(getter, js_name = "self_signing_key")]
|
||||
pub fn self_signing_key(&self) -> Option<String> {
|
||||
self.inner.self_signing_key.clone()
|
||||
}
|
||||
|
||||
/// The seed of the user signing key encoded as unpadded base64.
|
||||
#[wasm_bindgen(getter, js_name = "userSigningKey")]
|
||||
pub fn user_signing_key(&self) -> Option<String> {
|
||||
self.inner.user_signing_key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on a room key that has been received or imported.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct RoomKeyInfo {
|
||||
pub(crate) inner: matrix_sdk_crypto::store::RoomKeyInfo,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::store::RoomKeyInfo => RoomKeyInfo);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomKeyInfo {
|
||||
/// The {@link EncryptionAlgorithm} that this key is used for. Will be one
|
||||
/// of the `m.megolm.*` algorithms.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn algorithm(&self) -> EncryptionAlgorithm {
|
||||
self.inner.algorithm.clone().into()
|
||||
}
|
||||
|
||||
/// The room where the key is used.
|
||||
#[wasm_bindgen(getter, js_name = "roomId")]
|
||||
pub fn room_id(&self) -> RoomId {
|
||||
self.inner.room_id.clone().into()
|
||||
}
|
||||
|
||||
/// The Curve25519 key of the device which initiated the session originally.
|
||||
#[wasm_bindgen(getter, js_name = "senderKey")]
|
||||
pub fn sender_key(&self) -> Curve25519PublicKey {
|
||||
self.inner.sender_key.into()
|
||||
}
|
||||
|
||||
/// The ID of the session that the key is for.
|
||||
#[wasm_bindgen(getter, js_name = "sessionId")]
|
||||
pub fn session_id(&self) -> String {
|
||||
self.inner.session_id.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//! `GET /_matrix/client/*/sync`
|
||||
|
||||
use js_sys::Array;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{identifiers, js::downcast};
|
||||
|
||||
/// Information on E2E device updates.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceLists {
|
||||
pub(crate) inner: ruma::api::client::sync::sync_events::DeviceLists,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceLists {
|
||||
/// Create an empty `DeviceLists`.
|
||||
///
|
||||
/// `changed` and `left` must be an array of `UserId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(changed: Option<Array>, left: Option<Array>) -> Result<DeviceLists, JsError> {
|
||||
let mut inner = ruma::api::client::sync::sync_events::DeviceLists::default();
|
||||
|
||||
inner.changed = changed
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
inner.left = left
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/// Returns true if there are no device list updates.
|
||||
#[wasm_bindgen(js_name = "isEmpty")]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// List of users who have updated their device identity keys or
|
||||
/// who now share an encrypted room with the client since the
|
||||
/// previous sync
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn changed(&self) -> Array {
|
||||
self.inner
|
||||
.changed
|
||||
.iter()
|
||||
.map(|user| identifiers::UserId::from(user.clone()))
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// List of users who no longer share encrypted rooms since the
|
||||
/// previous sync response.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn left(&self) -> Array {
|
||||
self.inner
|
||||
.left
|
||||
.iter()
|
||||
.map(|user| identifiers::UserId::from(user.clone()))
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Logger level.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoggerLevel {
|
||||
/// `TRACE` level.
|
||||
///
|
||||
/// Designate very low priority, often extremely verbose,
|
||||
/// information.
|
||||
Trace,
|
||||
|
||||
/// `DEBUG` level.
|
||||
///
|
||||
/// Designate lower priority information.
|
||||
Debug,
|
||||
|
||||
/// `INFO` level.
|
||||
///
|
||||
/// Designate useful information.
|
||||
Info,
|
||||
|
||||
/// `WARN` level.
|
||||
///
|
||||
/// Designate hazardous situations.
|
||||
Warn,
|
||||
|
||||
/// `ERROR` level.
|
||||
///
|
||||
/// Designate very serious errors.
|
||||
Error,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
mod inner {
|
||||
use std::{
|
||||
fmt,
|
||||
fmt::Write as _,
|
||||
sync::{Arc, Once},
|
||||
};
|
||||
|
||||
use tracing::{
|
||||
field::{Field, Visit},
|
||||
metadata::LevelFilter,
|
||||
Event, Level, Metadata, Subscriber,
|
||||
};
|
||||
use tracing_subscriber::{
|
||||
layer::{Context, Layer as TracingLayer},
|
||||
prelude::*,
|
||||
reload, Registry,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
type TracingInner = Arc<reload::Handle<Layer, Registry>>;
|
||||
|
||||
/// Type to install and to manipulate the tracing layer.
|
||||
#[wasm_bindgen]
|
||||
pub struct Tracing {
|
||||
handle: TracingInner,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Tracing {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Tracing").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Tracing {
|
||||
/// Check whether the `tracing` feature has been enabled.
|
||||
#[wasm_bindgen(js_name = "isAvailable")]
|
||||
pub fn is_available() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Install the tracing layer.
|
||||
///
|
||||
/// `Tracing` is a singleton. Once it is installed,
|
||||
/// consecutive calls to the constructor will construct a new
|
||||
/// `Tracing` object but with the exact same inner
|
||||
/// state. Calling the constructor with a new `min_level` will
|
||||
/// just update the `min_level` parameter; in that regard, it
|
||||
/// is similar to calling the `min_level` method on an
|
||||
/// existing `Tracing` object.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(min_level: LoggerLevel) -> Result<Tracing, JsError> {
|
||||
static mut INSTALL: Option<TracingInner> = None;
|
||||
static INSTALLED: Once = Once::new();
|
||||
|
||||
INSTALLED.call_once(|| {
|
||||
let (filter, reload_handle) = reload::Layer::new(Layer::new(min_level.clone()));
|
||||
|
||||
tracing_subscriber::registry().with(filter).init();
|
||||
|
||||
unsafe { INSTALL = Some(Arc::new(reload_handle)) };
|
||||
});
|
||||
|
||||
let tracing = Tracing {
|
||||
handle: unsafe { INSTALL.as_ref() }
|
||||
.cloned()
|
||||
.expect("`Tracing` has not been installed correctly"),
|
||||
};
|
||||
|
||||
// If it's not the first call to `install`, the
|
||||
// `min_level` can be different. Let's update it.
|
||||
tracing.min_level(min_level);
|
||||
|
||||
Ok(tracing)
|
||||
}
|
||||
|
||||
/// Re-define the minimum logger level.
|
||||
#[wasm_bindgen(setter, js_name = "minLevel")]
|
||||
pub fn min_level(&self, min_level: LoggerLevel) {
|
||||
let _ = self.handle.modify(|layer| layer.min_level = min_level.into());
|
||||
}
|
||||
|
||||
/// Turn the logger on, i.e. it emits logs again if it was turned
|
||||
/// off.
|
||||
#[wasm_bindgen(js_name = "turnOn")]
|
||||
pub fn turn_on(&self) {
|
||||
let _ = self.handle.modify(|layer| layer.turn_on());
|
||||
}
|
||||
|
||||
/// Turn the logger off, i.e. it no long emits logs.
|
||||
#[wasm_bindgen(js_name = "turnOff")]
|
||||
pub fn turn_off(&self) {
|
||||
let _ = self.handle.modify(|layer| layer.turn_off());
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LoggerLevel> for Level {
|
||||
fn from(value: LoggerLevel) -> Self {
|
||||
use LoggerLevel::*;
|
||||
|
||||
match value {
|
||||
Trace => Self::TRACE,
|
||||
Debug => Self::DEBUG,
|
||||
Info => Self::INFO,
|
||||
Warn => Self::WARN,
|
||||
Error => Self::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "debug")]
|
||||
fn log_debug(message: String);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "info")]
|
||||
fn log_info(message: String);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "warn")]
|
||||
fn log_warn(message: String);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = "error")]
|
||||
fn log_error(message: String);
|
||||
}
|
||||
|
||||
struct Layer {
|
||||
min_level: Level,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Layer {
|
||||
fn new<L>(min_level: L) -> Self
|
||||
where
|
||||
L: Into<Level>,
|
||||
{
|
||||
Self { min_level: min_level.into(), enabled: true }
|
||||
}
|
||||
|
||||
fn turn_on(&mut self) {
|
||||
self.enabled = true;
|
||||
}
|
||||
|
||||
fn turn_off(&mut self) {
|
||||
self.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> TracingLayer<S> for Layer
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
fn enabled(&self, metadata: &Metadata<'_>, _: Context<'_, S>) -> bool {
|
||||
self.enabled && metadata.level() <= &self.min_level
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<LevelFilter> {
|
||||
if !self.enabled {
|
||||
Some(LevelFilter::OFF)
|
||||
} else {
|
||||
Some(LevelFilter::from_level(self.min_level))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) {
|
||||
let mut recorder = StringVisitor::new();
|
||||
event.record(&mut recorder);
|
||||
let metadata = event.metadata();
|
||||
let level = metadata.level();
|
||||
|
||||
let origin = metadata
|
||||
.file()
|
||||
.and_then(|file| metadata.line().map(|ln| format!("{file}:{ln}")))
|
||||
.unwrap_or_default();
|
||||
|
||||
let message = format!("{level} {origin}{recorder}");
|
||||
|
||||
match *level {
|
||||
Level::TRACE => log_debug(message),
|
||||
Level::DEBUG => log_debug(message),
|
||||
Level::INFO => log_info(message),
|
||||
Level::WARN => log_warn(message),
|
||||
Level::ERROR => log_error(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StringVisitor {
|
||||
string: String,
|
||||
}
|
||||
|
||||
impl StringVisitor {
|
||||
fn new() -> Self {
|
||||
Self { string: String::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for StringVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
|
||||
match field.name() {
|
||||
"message" => {
|
||||
if !self.string.is_empty() {
|
||||
self.string.push('\n');
|
||||
}
|
||||
|
||||
let _ = write!(self.string, "{value:?}");
|
||||
}
|
||||
|
||||
field_name => {
|
||||
let _ = write!(self.string, "\n{field_name} = {value:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StringVisitor {
|
||||
fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if !self.string.is_empty() {
|
||||
write!(&mut f, " {}", self.string)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
mod inner {
|
||||
use super::*;
|
||||
|
||||
/// Type to install and to manipulate the tracing layer.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Tracing;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Tracing {
|
||||
/// Check whether the `tracing` feature has been enabled.
|
||||
#[wasm_bindgen(js_name = "isAvailable")]
|
||||
pub fn is_available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The `tracing` feature is not enabled, so this constructor
|
||||
/// will raise an error.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(_min_level: LoggerLevel) -> Result<Tracing, JsError> {
|
||||
Err(JsError::new("The `tracing` feature is disabled. Check `Tracing.isAvailable` before constructing `Tracing`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use inner::*;
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Extra types, like `Signatures`.
|
||||
|
||||
use js_sys::Map;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
identifiers::{DeviceKeyId, UserId},
|
||||
impl_from_to_inner,
|
||||
vodozemac::Ed25519Signature,
|
||||
};
|
||||
|
||||
/// A collection of `Signature`.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Signatures {
|
||||
inner: matrix_sdk_crypto::types::Signatures,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::types::Signatures => Signatures);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Signatures {
|
||||
/// Creates a new, empty, signatures collection.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
matrix_sdk_crypto::types::Signatures::new().into()
|
||||
}
|
||||
|
||||
/// Add the given signature from the given signer and the given key ID to
|
||||
/// the collection.
|
||||
#[wasm_bindgen(js_name = "addSignature")]
|
||||
pub fn add_signature(
|
||||
&mut self,
|
||||
signer: &UserId,
|
||||
key_id: &DeviceKeyId,
|
||||
signature: &Ed25519Signature,
|
||||
) -> Option<MaybeSignature> {
|
||||
self.inner
|
||||
.add_signature(signer.inner.clone(), key_id.inner.clone(), signature.inner)
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
/// Try to find an Ed25519 signature from the given signer with
|
||||
/// the given key ID.
|
||||
#[wasm_bindgen(js_name = "getSignature")]
|
||||
pub fn get_signature(&self, signer: &UserId, key_id: &DeviceKeyId) -> Option<Ed25519Signature> {
|
||||
self.inner.get_signature(signer.inner.as_ref(), key_id.inner.as_ref()).map(Into::into)
|
||||
}
|
||||
|
||||
/// Get the map of signatures that belong to the given user.
|
||||
pub fn get(&self, signer: &UserId) -> Option<Map> {
|
||||
let map = Map::new();
|
||||
|
||||
for (device_key_id, maybe_signature) in
|
||||
self.inner.get(signer.inner.as_ref()).map(|map| {
|
||||
map.iter().map(|(device_key_id, maybe_signature)| {
|
||||
(
|
||||
device_key_id.as_str().to_owned(),
|
||||
MaybeSignature::from(maybe_signature.clone()),
|
||||
)
|
||||
})
|
||||
})?
|
||||
{
|
||||
map.set(&device_key_id.into(), &maybe_signature.into());
|
||||
}
|
||||
|
||||
Some(map)
|
||||
}
|
||||
|
||||
/// Remove all the signatures we currently hold.
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.clear();
|
||||
}
|
||||
|
||||
/// Do we hold any signatures or is our collection completely
|
||||
/// empty.
|
||||
#[wasm_bindgen(js_name = "isEmpty")]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// How many signatures do we currently hold.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn count(&self) -> usize {
|
||||
self.inner.signature_count()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a potentially decoded signature (but not a validated
|
||||
/// one).
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Signature {
|
||||
inner: matrix_sdk_crypto::types::Signature,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::types::Signature => Signature);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Signature {
|
||||
/// Get the Ed25519 signature, if this is one.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn ed25519(&self) -> Option<Ed25519Signature> {
|
||||
self.inner.ed25519().map(Into::into)
|
||||
}
|
||||
|
||||
/// Convert the signature to a base64 encoded string.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
type MaybeSignatureInner =
|
||||
Result<matrix_sdk_crypto::types::Signature, matrix_sdk_crypto::types::InvalidSignature>;
|
||||
|
||||
/// Represents a signature that is either valid _or_ that could not be
|
||||
/// decoded.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct MaybeSignature {
|
||||
inner: MaybeSignatureInner,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(MaybeSignatureInner => MaybeSignature);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MaybeSignature {
|
||||
/// Check whether the signature has been successfully decoded.
|
||||
#[wasm_bindgen(js_name = "isValid")]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.inner.is_ok()
|
||||
}
|
||||
|
||||
/// Check whether the signature could not be successfully decoded.
|
||||
#[wasm_bindgen(js_name = "isInvalid")]
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
self.inner.is_err()
|
||||
}
|
||||
|
||||
/// The signature, if successfully decoded.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn signature(&self) -> Option<Signature> {
|
||||
self.inner.as_ref().cloned().map(Into::into).ok()
|
||||
}
|
||||
|
||||
/// The base64 encoded string that is claimed to contain a
|
||||
/// signature but could not be decoded, if any.
|
||||
#[wasm_bindgen(getter, js_name = "invalidSignatureSource")]
|
||||
pub fn invalid_signature_source(&self) -> Option<String> {
|
||||
match &self.inner {
|
||||
Ok(_) => None,
|
||||
Err(signature) => Some(signature.source.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,193 @@
|
||||
//! Vodozemac types.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::impl_from_to_inner;
|
||||
|
||||
/// An Ed25519 public key, used to verify digital signatures.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ed25519PublicKey {
|
||||
inner: vodozemac::Ed25519PublicKey,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Ed25519PublicKey {
|
||||
/// The number of bytes an Ed25519 public key has.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
vodozemac::Ed25519PublicKey::LENGTH
|
||||
}
|
||||
|
||||
/// Serialize an Ed25519 public key to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_to_inner!(vodozemac::Ed25519PublicKey => Ed25519PublicKey);
|
||||
|
||||
/// An Ed25519 digital signature, can be used to verify the
|
||||
/// authenticity of a message.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Ed25519Signature {
|
||||
pub(crate) inner: vodozemac::Ed25519Signature,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(vodozemac::Ed25519Signature => Ed25519Signature);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Ed25519Signature {
|
||||
/// Try to create an Ed25519 signature from an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(signature: String) -> Result<Ed25519Signature, JsError> {
|
||||
Ok(Self { inner: vodozemac::Ed25519Signature::from_base64(signature.as_str())? })
|
||||
}
|
||||
|
||||
/// Serialize a Ed25519 signature to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Curve25519 public key.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Curve25519PublicKey {
|
||||
inner: vodozemac::Curve25519PublicKey,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Curve25519PublicKey {
|
||||
/// The number of bytes a Curve25519 public key has.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
vodozemac::Curve25519PublicKey::LENGTH
|
||||
}
|
||||
|
||||
/// Serialize an Curve25519 public key to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_to_inner!(vodozemac::Curve25519PublicKey => Curve25519PublicKey);
|
||||
|
||||
/// Struct holding the two public identity keys of an account.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct IdentityKeys {
|
||||
/// The Ed25519 public key, used for signing.
|
||||
pub ed25519: Ed25519PublicKey,
|
||||
|
||||
/// The Curve25519 public key, used for establish shared secrets.
|
||||
pub curve25519: Curve25519PublicKey,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::olm::IdentityKeys> for IdentityKeys {
|
||||
fn from(value: matrix_sdk_crypto::olm::IdentityKeys) -> Self {
|
||||
Self {
|
||||
ed25519: Ed25519PublicKey { inner: value.ed25519 },
|
||||
curve25519: Curve25519PublicKey { inner: value.curve25519 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum over the different key types a device can have.
|
||||
///
|
||||
/// Currently devices have a curve25519 and ed25519 keypair. The keys
|
||||
/// transport format is a base64 encoded string, any unknown key type
|
||||
/// will be left as such a string.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceKey {
|
||||
inner: matrix_sdk_crypto::types::DeviceKey,
|
||||
}
|
||||
|
||||
impl_from_to_inner!(matrix_sdk_crypto::types::DeviceKey => DeviceKey);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceKey {
|
||||
/// Get the name of the device key.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn name(&self) -> DeviceKeyName {
|
||||
(&self.inner).into()
|
||||
}
|
||||
|
||||
/// Get the value associated to the `Curve25519` device key name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn curve25519(&self) -> Option<Curve25519PublicKey> {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match &self.inner {
|
||||
Curve25519(key) => Some((*key).into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value associated to the `Ed25519` device key name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn ed25519(&self) -> Option<Ed25519PublicKey> {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match &self.inner {
|
||||
Ed25519(key) => Some((*key).into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value associated to the `Unknown` device key name.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn unknown(&self) -> Option<String> {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match &self.inner {
|
||||
Unknown(key) => Some(key.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the `DeviceKey` into a base64 encoded string.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_crypto::types::DeviceKey> for DeviceKeyName {
|
||||
fn from(device_key: &matrix_sdk_crypto::types::DeviceKey) -> Self {
|
||||
use matrix_sdk_crypto::types::DeviceKey::*;
|
||||
|
||||
match device_key {
|
||||
Curve25519(_) => Self::Curve25519,
|
||||
Ed25519(_) => Self::Ed25519,
|
||||
Unknown(_) => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum over the different key types a device can have.
|
||||
///
|
||||
/// Currently devices have a curve25519 and ed25519 keypair. The keys
|
||||
/// transport format is a base64 encoded string, any unknown key type
|
||||
/// will be left as such a string.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum DeviceKeyName {
|
||||
/// The curve25519 device key.
|
||||
Curve25519,
|
||||
|
||||
/// The ed25519 device key.
|
||||
Ed25519,
|
||||
|
||||
/// An unknown device key.
|
||||
Unknown,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const { UserId, initAsync } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
test("can instantiate rust objects with async initialiser", async () => {
|
||||
initUserId = () => new UserId("@foo:bar.org");
|
||||
|
||||
// stub out the synchronous WebAssembly loader with one that raises an error
|
||||
jest.spyOn(WebAssembly, "Module").mockImplementation(() => {
|
||||
throw new Error("synchronous WebAssembly.Module() not allowed");
|
||||
});
|
||||
|
||||
// this should fail
|
||||
expect(initUserId).toThrow(/synchronous/);
|
||||
|
||||
// but once we init with async, it should work
|
||||
await initAsync();
|
||||
initUserId();
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const { Attachment, EncryptedAttachment } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
describe(Attachment.name, () => {
|
||||
const originalData = "hello";
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
let encryptedAttachment;
|
||||
|
||||
test("can encrypt data", () => {
|
||||
encryptedAttachment = Attachment.encrypt(textEncoder.encode(originalData));
|
||||
|
||||
const mediaEncryptionInfo = JSON.parse(encryptedAttachment.mediaEncryptionInfo);
|
||||
|
||||
expect(mediaEncryptionInfo).toMatchObject({
|
||||
v: "v2",
|
||||
key: {
|
||||
kty: expect.any(String),
|
||||
key_ops: expect.arrayContaining(["encrypt", "decrypt"]),
|
||||
alg: expect.any(String),
|
||||
k: expect.any(String),
|
||||
ext: expect.any(Boolean),
|
||||
},
|
||||
iv: expect.stringMatching(/^[A-Za-z0-9\+/]+$/),
|
||||
hashes: {
|
||||
sha256: expect.stringMatching(/^[A-Za-z0-9\+/]+$/),
|
||||
},
|
||||
});
|
||||
|
||||
const encryptedData = encryptedAttachment.encryptedData;
|
||||
expect(
|
||||
encryptedData.every((i) => {
|
||||
i != 0;
|
||||
}),
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
|
||||
test("can decrypt data", () => {
|
||||
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false);
|
||||
|
||||
const decryptedAttachment = Attachment.decrypt(encryptedAttachment);
|
||||
|
||||
expect(textDecoder.decode(decryptedAttachment)).toStrictEqual(originalData);
|
||||
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true);
|
||||
});
|
||||
|
||||
test("can only decrypt once", () => {
|
||||
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true);
|
||||
|
||||
expect(() => {
|
||||
textDecoder.decode(decryptedAttachment);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe(EncryptedAttachment.name, () => {
|
||||
const originalData = "hello";
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
test("can be created manually", () => {
|
||||
const encryptedAttachment = new EncryptedAttachment(
|
||||
new Uint8Array([24, 150, 67, 37, 144]),
|
||||
JSON.stringify({
|
||||
v: "v2",
|
||||
key: {
|
||||
kty: "oct",
|
||||
key_ops: ["encrypt", "decrypt"],
|
||||
alg: "A256CTR",
|
||||
k: "QbNXUjuukFyEJ8cQZjJuzN6mMokg0HJIjx0wVMLf5BM",
|
||||
ext: true,
|
||||
},
|
||||
iv: "xk2AcWkomiYAAAAAAAAAAA",
|
||||
hashes: {
|
||||
sha256: "JsRbDXgOja4xvDiF3DwBuLHdxUzIrVYIuj7W/t3aEok",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false);
|
||||
expect(textDecoder.decode(Attachment.decrypt(encryptedAttachment))).toStrictEqual(originalData);
|
||||
expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,957 @@
|
||||
const {
|
||||
OlmMachine,
|
||||
UserId,
|
||||
DeviceId,
|
||||
DeviceKeyId,
|
||||
RoomId,
|
||||
Device,
|
||||
LocalTrust,
|
||||
UserDevices,
|
||||
DeviceKey,
|
||||
DeviceKeyName,
|
||||
DeviceKeyAlgorithmName,
|
||||
Ed25519PublicKey,
|
||||
EncryptionAlgorithm,
|
||||
Curve25519PublicKey,
|
||||
Signatures,
|
||||
VerificationMethod,
|
||||
VerificationRequest,
|
||||
ToDeviceRequest,
|
||||
DeviceLists,
|
||||
KeysUploadRequest,
|
||||
RequestType,
|
||||
KeysQueryRequest,
|
||||
Sas,
|
||||
Emoji,
|
||||
SigningKeysUploadRequest,
|
||||
SignatureUploadRequest,
|
||||
Qr,
|
||||
QrCode,
|
||||
QrCodeScan,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
const { zip, addMachineToMachine } = require("./helper");
|
||||
|
||||
describe("LocalTrust", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(LocalTrust.Verified).toStrictEqual(0);
|
||||
expect(LocalTrust.BlackListed).toStrictEqual(1);
|
||||
expect(LocalTrust.Ignored).toStrictEqual(2);
|
||||
expect(LocalTrust.Unset).toStrictEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DeviceKeyName", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(DeviceKeyName.Curve25519).toStrictEqual(0);
|
||||
expect(DeviceKeyName.Ed25519).toStrictEqual(1);
|
||||
expect(DeviceKeyName.Unknown).toStrictEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe(OlmMachine.name, () => {
|
||||
const user = new UserId("@alice:example.org");
|
||||
const device = new DeviceId("foobar");
|
||||
const room = new RoomId("!baz:matrix.org");
|
||||
|
||||
function machine(new_user, new_device) {
|
||||
return OlmMachine.initialize(new_user || user, new_device || device);
|
||||
}
|
||||
|
||||
test("can read user devices", async () => {
|
||||
const m = await machine();
|
||||
const userDevices = await m.getUserDevices(user);
|
||||
|
||||
expect(userDevices).toBeInstanceOf(UserDevices);
|
||||
expect(userDevices.get(device)).toBeInstanceOf(Device);
|
||||
expect(userDevices.isAnyVerified()).toStrictEqual(false);
|
||||
expect(userDevices.keys().map((device_id) => device_id.toString())).toStrictEqual([device.toString()]);
|
||||
expect(userDevices.devices().map((device) => device.deviceId.toString())).toStrictEqual([device.toString()]);
|
||||
});
|
||||
|
||||
test("can read a user device", async () => {
|
||||
const m = await machine();
|
||||
|
||||
const hypothetical_response = JSON.stringify({
|
||||
device_keys: {
|
||||
"@alice:example.org": {
|
||||
JLAFKJWSCS: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "JLAFKJWSCS",
|
||||
keys: {
|
||||
"curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
|
||||
"ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM",
|
||||
},
|
||||
signatures: {
|
||||
"@alice:example.org": {
|
||||
"ed25519:JLAFKJWSCS":
|
||||
"m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA",
|
||||
},
|
||||
},
|
||||
unsigned: {
|
||||
device_display_name: "Alice's mobile phone",
|
||||
},
|
||||
user_id: "@alice:example.org",
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
// Insert another device into the store
|
||||
await m.markRequestAsSent("ID", RequestType.KeysQuery, hypothetical_response);
|
||||
|
||||
const secondDeviceId = new DeviceId("JLAFKJWSCS");
|
||||
const dev = await m.getDevice(user, secondDeviceId);
|
||||
|
||||
expect(dev).toBeInstanceOf(Device);
|
||||
expect(dev.isVerified()).toStrictEqual(false);
|
||||
expect(dev.isCrossSigningTrusted()).toStrictEqual(false);
|
||||
expect(dev.isCrossSignedByOwner()).toStrictEqual(false);
|
||||
|
||||
expect(dev.localTrustState).toStrictEqual(LocalTrust.Unset);
|
||||
expect(dev.isLocallyTrusted()).toStrictEqual(false);
|
||||
expect(await dev.setLocalTrust(LocalTrust.Verified)).toBeNull();
|
||||
expect(dev.localTrustState).toStrictEqual(LocalTrust.Verified);
|
||||
expect(dev.isLocallyTrusted()).toStrictEqual(true);
|
||||
|
||||
expect(dev.userId.toString()).toStrictEqual(user.toString());
|
||||
expect(dev.deviceId.toString()).toStrictEqual(secondDeviceId.toString());
|
||||
expect(dev.deviceName).toBeUndefined();
|
||||
expect(dev.algorithms).toEqual([
|
||||
EncryptionAlgorithm.OlmV1Curve25519AesSha2,
|
||||
EncryptionAlgorithm.MegolmV1AesSha2,
|
||||
]);
|
||||
|
||||
const deviceKey = dev.getKey(DeviceKeyAlgorithmName.Ed25519);
|
||||
|
||||
expect(deviceKey).toBeInstanceOf(DeviceKey);
|
||||
expect(deviceKey.name).toStrictEqual(DeviceKeyName.Ed25519);
|
||||
expect(deviceKey.curve25519).toBeUndefined();
|
||||
expect(deviceKey.ed25519).toBeInstanceOf(Ed25519PublicKey);
|
||||
expect(deviceKey.unknown).toBeUndefined();
|
||||
expect(deviceKey.toBase64()).toMatch(/^[A-Za-z0-9\+/]+$/);
|
||||
|
||||
expect(dev.curve25519Key).toBeInstanceOf(Curve25519PublicKey);
|
||||
expect(dev.ed25519Key).toBeInstanceOf(Ed25519PublicKey);
|
||||
|
||||
for (const [deviceKeyId, deviceKey] of dev.keys) {
|
||||
expect(deviceKeyId).toBeInstanceOf(DeviceKeyId);
|
||||
expect(deviceKey).toBeInstanceOf(DeviceKey);
|
||||
}
|
||||
|
||||
expect(dev.signatures).toBeInstanceOf(Signatures);
|
||||
expect(dev.isBlacklisted()).toStrictEqual(false);
|
||||
expect(dev.isDeleted()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Key Verification", () => {
|
||||
const userId1 = new UserId("@alice:example.org");
|
||||
const deviceId1 = new DeviceId("alice_device");
|
||||
|
||||
const userId2 = new UserId("@bob:example.org");
|
||||
const deviceId2 = new DeviceId("bob_device");
|
||||
|
||||
function machine(new_user, new_device) {
|
||||
return OlmMachine.initialize(new_user || userId1, new_device || deviceId1);
|
||||
}
|
||||
|
||||
describe("SAS", () => {
|
||||
// First Olm machine.
|
||||
let m1;
|
||||
|
||||
// Second Olm machine.
|
||||
let m2;
|
||||
|
||||
beforeAll(async () => {
|
||||
m1 = await machine(userId1, deviceId1);
|
||||
m2 = await machine(userId2, deviceId2);
|
||||
});
|
||||
|
||||
// Verification request for `m1`.
|
||||
let verificationRequest1;
|
||||
|
||||
// The flow ID.
|
||||
let flowId;
|
||||
|
||||
test("can request verification (`m.key.verification.request`)", async () => {
|
||||
// Make `m1` and `m2` be aware of each other.
|
||||
{
|
||||
await addMachineToMachine(m2, m1);
|
||||
await addMachineToMachine(m1, m2);
|
||||
}
|
||||
|
||||
// Pick the device we want to start the verification with.
|
||||
const device2 = await m1.getDevice(userId2, deviceId2);
|
||||
|
||||
expect(device2).toBeInstanceOf(Device);
|
||||
|
||||
let outgoingVerificationRequest;
|
||||
// Request a verification from `m1` to `device2`.
|
||||
[verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification();
|
||||
|
||||
expect(verificationRequest1).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest1.otherDeviceId).toBeUndefined();
|
||||
expect(verificationRequest1.roomId).toBeUndefined();
|
||||
expect(verificationRequest1.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest1.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest1.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest1.theirSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(verificationRequest1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest1.weStarted()).toStrictEqual(true);
|
||||
expect(verificationRequest1.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isCancelled()).toStrictEqual(false);
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.request");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
flowId = verificationRequest1.flowId;
|
||||
});
|
||||
|
||||
// Verification request for `m2`.
|
||||
let verificationRequest2;
|
||||
|
||||
test("can fetch received request verification", async () => {
|
||||
// Oh, a new verification request.
|
||||
verificationRequest2 = m2.getVerificationRequest(userId1, flowId);
|
||||
|
||||
expect(verificationRequest2).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(verificationRequest2.roomId).toBeUndefined();
|
||||
expect(verificationRequest2.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest2.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest2.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest2.flowId).toStrictEqual(flowId);
|
||||
expect(verificationRequest2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest2.weStarted()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isCancelled()).toStrictEqual(false);
|
||||
|
||||
const verificationRequests = m2.getVerificationRequests(userId1);
|
||||
expect(verificationRequests).toHaveLength(1);
|
||||
expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same
|
||||
});
|
||||
|
||||
test("can accept a verification request (`m.key.verification.ready`)", async () => {
|
||||
// Accept the verification request.
|
||||
let outgoingVerificationRequest = verificationRequest2.accept();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
|
||||
// The request verification is ready.
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.ready");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification ready to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("verification requests are synchronized and automatically updated", () => {
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(true);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(true);
|
||||
|
||||
expect(verificationRequest1.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.SasV1, VerificationMethod.ReciprocateV1]),
|
||||
);
|
||||
});
|
||||
|
||||
// SAS verification for the second machine.
|
||||
let sas2;
|
||||
|
||||
test("can start a SAS verification (`m.key.verification.start`)", async () => {
|
||||
// Let's start a SAS verification, from `m2` for example.
|
||||
[sas2, outgoingVerificationRequest] = await verificationRequest2.startSas();
|
||||
expect(sas2).toBeInstanceOf(Sas);
|
||||
|
||||
expect(sas2.userId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(sas2.deviceId.toString()).toStrictEqual(deviceId2.toString());
|
||||
expect(sas2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(sas2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(sas2.flowId).toStrictEqual(flowId);
|
||||
expect(sas2.roomId).toBeUndefined();
|
||||
expect(sas2.supportsEmoji()).toStrictEqual(false);
|
||||
expect(sas2.startedFromRequest()).toStrictEqual(true);
|
||||
expect(sas2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(sas2.haveWeConfirmed()).toStrictEqual(false);
|
||||
expect(sas2.hasBeenAccepted()).toStrictEqual(false);
|
||||
expect(sas2.cancelInfo()).toBeUndefined();
|
||||
expect(sas2.weStarted()).toStrictEqual(false);
|
||||
expect(sas2.timedOut()).toStrictEqual(false);
|
||||
expect(sas2.canBePresented()).toStrictEqual(false);
|
||||
expect(sas2.isDone()).toStrictEqual(false);
|
||||
expect(sas2.isCancelled()).toStrictEqual(false);
|
||||
expect(sas2.emoji()).toBeUndefined();
|
||||
expect(sas2.emojiIndex()).toBeUndefined();
|
||||
expect(sas2.decimals()).toBeUndefined();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.start");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the SAS start to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
// SAS verification for the second machine.
|
||||
let sas1;
|
||||
|
||||
test("can fetch and accept an ongoing SAS verification (`m.key.verification.accept`)", async () => {
|
||||
// Let's fetch the ongoing SAS verification.
|
||||
sas1 = await m1.getVerification(userId2, flowId);
|
||||
|
||||
expect(sas1).toBeInstanceOf(Sas);
|
||||
|
||||
expect(sas1.userId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(sas1.deviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(sas1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(sas1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString());
|
||||
expect(sas1.flowId).toStrictEqual(flowId);
|
||||
expect(sas1.roomId).toBeUndefined();
|
||||
expect(sas1.startedFromRequest()).toStrictEqual(true);
|
||||
expect(sas1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(sas1.haveWeConfirmed()).toStrictEqual(false);
|
||||
expect(sas1.hasBeenAccepted()).toStrictEqual(false);
|
||||
expect(sas1.cancelInfo()).toBeUndefined();
|
||||
expect(sas1.weStarted()).toStrictEqual(true);
|
||||
expect(sas1.timedOut()).toStrictEqual(false);
|
||||
expect(sas1.canBePresented()).toStrictEqual(false);
|
||||
expect(sas1.isDone()).toStrictEqual(false);
|
||||
expect(sas1.isCancelled()).toStrictEqual(false);
|
||||
expect(sas1.emoji()).toBeUndefined();
|
||||
expect(sas1.emojiIndex()).toBeUndefined();
|
||||
expect(sas1.decimals()).toBeUndefined();
|
||||
|
||||
// Let's accept thet SAS start request.
|
||||
let outgoingVerificationRequest = sas1.accept();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.accept");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the SAS accept to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("emojis are supported by both sides", () => {
|
||||
expect(sas1.supportsEmoji()).toStrictEqual(true);
|
||||
expect(sas2.supportsEmoji()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
test("one side sends verification key (`m.key.verification.key`)", async () => {
|
||||
// Let's send the verification keys from `m2` to `m1`.
|
||||
const outgoingRequests = await m2.outgoingRequests();
|
||||
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
|
||||
|
||||
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(toDeviceRequest.event_type).toStrictEqual("m.key.verification.key");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: toDeviceRequest.event_type,
|
||||
content: JSON.parse(toDeviceRequest.body).messages[userId1.toString()][deviceId1.toString()],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS key to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
m2.markRequestAsSent(toDeviceRequest.id, toDeviceRequest.type, "{}");
|
||||
});
|
||||
|
||||
test("other side sends back verification key (`m.key.verification.key`)", async () => {
|
||||
// Let's send the verification keys from `m1` to `m2`.
|
||||
const outgoingRequests = await m1.outgoingRequests();
|
||||
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
|
||||
|
||||
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(toDeviceRequest.event_type).toStrictEqual("m.key.verification.key");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: toDeviceRequest.event_type,
|
||||
content: JSON.parse(toDeviceRequest.body).messages[userId2.toString()][deviceId2.toString()],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS key to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
m1.markRequestAsSent(toDeviceRequest.id, toDeviceRequest.type, "{}");
|
||||
});
|
||||
|
||||
test("emojis match from both sides", () => {
|
||||
const emojis1 = sas1.emoji();
|
||||
const emojiIndexes1 = sas1.emojiIndex();
|
||||
const emojis2 = sas2.emoji();
|
||||
const emojiIndexes2 = sas2.emojiIndex();
|
||||
|
||||
expect(emojis1).toHaveLength(7);
|
||||
expect(emojiIndexes1).toHaveLength(emojis1.length);
|
||||
expect(emojis2).toHaveLength(emojis1.length);
|
||||
expect(emojiIndexes2).toHaveLength(emojis1.length);
|
||||
|
||||
const isEmoji =
|
||||
/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/;
|
||||
|
||||
for (const [emoji1, emojiIndex1, emoji2, emojiIndex2] of zip(
|
||||
emojis1,
|
||||
emojiIndexes1,
|
||||
emojis2,
|
||||
emojiIndexes2,
|
||||
)) {
|
||||
expect(emoji1).toBeInstanceOf(Emoji);
|
||||
expect(emoji1.symbol).toMatch(isEmoji);
|
||||
expect(emoji1.description).toBeTruthy();
|
||||
|
||||
expect(emojiIndex1).toBeGreaterThanOrEqual(0);
|
||||
expect(emojiIndex1).toBeLessThanOrEqual(63);
|
||||
|
||||
expect(emoji2).toBeInstanceOf(Emoji);
|
||||
expect(emoji2.symbol).toStrictEqual(emoji1.symbol);
|
||||
expect(emoji2.description).toStrictEqual(emoji1.description);
|
||||
|
||||
expect(emojiIndex2).toStrictEqual(emojiIndex1);
|
||||
}
|
||||
});
|
||||
|
||||
test("decimals match from both sides", () => {
|
||||
const decimals1 = sas1.decimals();
|
||||
const decimals2 = sas2.decimals();
|
||||
|
||||
expect(decimals1).toHaveLength(3);
|
||||
expect(decimals2).toHaveLength(decimals1.length);
|
||||
|
||||
const isDecimal = /^[0-9]{4}$/;
|
||||
|
||||
for (const [decimal1, decimal2] of zip(decimals1, decimals2)) {
|
||||
expect(decimal1.toString()).toMatch(isDecimal);
|
||||
|
||||
expect(decimal2).toStrictEqual(decimal1);
|
||||
}
|
||||
});
|
||||
|
||||
test("can confirm keys match (`m.key.verification.mac`)", async () => {
|
||||
// `m1` confirms.
|
||||
const [outgoingVerificationRequests, signatureUploadRequest] = await sas1.confirm();
|
||||
|
||||
expect(signatureUploadRequest).toBeUndefined();
|
||||
expect(outgoingVerificationRequests).toHaveLength(1);
|
||||
|
||||
let outgoingVerificationRequest = outgoingVerificationRequests[0];
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.mac");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS confirmation to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("can confirm back keys match (`m.key.verification.done`)", async () => {
|
||||
// `m2` confirms.
|
||||
const [outgoingVerificationRequests, signatureUploadRequest] = await sas2.confirm();
|
||||
|
||||
expect(signatureUploadRequest).toBeUndefined();
|
||||
expect(outgoingVerificationRequests).toHaveLength(2);
|
||||
|
||||
// `.mac`
|
||||
{
|
||||
let outgoingVerificationRequest = outgoingVerificationRequests[0];
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.mac");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS confirmation to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
}
|
||||
|
||||
// `.done`
|
||||
{
|
||||
let outgoingVerificationRequest = outgoingVerificationRequests[1];
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.done");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS done to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
}
|
||||
});
|
||||
|
||||
test("can send final done (`m.key.verification.done`)", async () => {
|
||||
const outgoingRequests = await m1.outgoingRequests();
|
||||
expect(outgoingRequests).toHaveLength(4);
|
||||
|
||||
let toDeviceRequest = outgoingRequests.find((request) => request.type == RequestType.ToDevice);
|
||||
|
||||
expect(toDeviceRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(toDeviceRequest.event_type).toStrictEqual("m.key.verification.done");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: toDeviceRequest.event_type,
|
||||
content: JSON.parse(toDeviceRequest.body).messages[userId2.toString()][deviceId2.toString()],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send te SAS key to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
m1.markRequestAsSent(toDeviceRequest.id, toDeviceRequest.type, "{}");
|
||||
});
|
||||
|
||||
test("can see if verification is done", () => {
|
||||
expect(verificationRequest1.isDone()).toStrictEqual(true);
|
||||
expect(verificationRequest2.isDone()).toStrictEqual(true);
|
||||
|
||||
expect(sas1.isDone()).toStrictEqual(true);
|
||||
expect(sas2.isDone()).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("QR Code", () => {
|
||||
if (undefined === Qr) {
|
||||
// qrcode supports is not enabled
|
||||
console.info("qrcode support is disabled, skip the associated test suite");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// First Olm machine.
|
||||
let m1;
|
||||
|
||||
// Second Olm machine.
|
||||
let m2;
|
||||
|
||||
beforeAll(async () => {
|
||||
m1 = await machine(userId1, deviceId1);
|
||||
m2 = await machine(userId2, deviceId2);
|
||||
});
|
||||
|
||||
// Verification request for `m1`.
|
||||
let verificationRequest1;
|
||||
|
||||
// The flow ID.
|
||||
let flowId;
|
||||
|
||||
test("can request verification (`m.key.verification.request`)", async () => {
|
||||
// Make `m1` and `m2` be aware of each other.
|
||||
{
|
||||
await addMachineToMachine(m2, m1);
|
||||
await addMachineToMachine(m1, m2);
|
||||
}
|
||||
|
||||
// Pick the device we want to start the verification with.
|
||||
const device2 = await m1.getDevice(userId2, deviceId2);
|
||||
|
||||
expect(device2).toBeInstanceOf(Device);
|
||||
|
||||
let outgoingVerificationRequest;
|
||||
// Request a verification from `m1` to `device2`.
|
||||
[verificationRequest1, outgoingVerificationRequest] = await device2.requestVerification([
|
||||
VerificationMethod.QrCodeScanV1, // by default
|
||||
VerificationMethod.QrCodeShowV1, // the one we add
|
||||
]);
|
||||
|
||||
expect(verificationRequest1).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest1.ownUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest1.otherDeviceId).toBeUndefined();
|
||||
expect(verificationRequest1.roomId).toBeUndefined();
|
||||
expect(verificationRequest1.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest1.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest1.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest1.theirSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest1.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(verificationRequest1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest1.weStarted()).toStrictEqual(true);
|
||||
expect(verificationRequest1.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest1.isCancelled()).toStrictEqual(false);
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.request");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
|
||||
flowId = verificationRequest1.flowId;
|
||||
});
|
||||
|
||||
// Verification request for `m2`.
|
||||
let verificationRequest2;
|
||||
|
||||
test("can fetch received request verification", async () => {
|
||||
// Oh, a new verification request.
|
||||
verificationRequest2 = m2.getVerificationRequest(userId1, flowId);
|
||||
|
||||
expect(verificationRequest2).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
expect(verificationRequest2.ownUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(verificationRequest2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(verificationRequest2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(verificationRequest2.roomId).toBeUndefined();
|
||||
expect(verificationRequest2.cancelInfo).toBeUndefined();
|
||||
expect(verificationRequest2.isPassive()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(false);
|
||||
expect(verificationRequest2.timedOut()).toStrictEqual(false);
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toBeUndefined();
|
||||
expect(verificationRequest2.flowId).toStrictEqual(flowId);
|
||||
expect(verificationRequest2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(verificationRequest2.weStarted()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isDone()).toStrictEqual(false);
|
||||
expect(verificationRequest2.isCancelled()).toStrictEqual(false);
|
||||
|
||||
const verificationRequests = m2.getVerificationRequests(userId1);
|
||||
expect(verificationRequests).toHaveLength(1);
|
||||
expect(verificationRequests[0].flowId).toStrictEqual(verificationRequest2.flowId); // there are the same
|
||||
});
|
||||
|
||||
test("can accept a verification request with methods (`m.key.verification.ready`)", async () => {
|
||||
// Accept the verification request.
|
||||
let outgoingVerificationRequest = verificationRequest2.acceptWithMethods([
|
||||
VerificationMethod.QrCodeScanV1, // by default
|
||||
VerificationMethod.QrCodeShowV1, // the one we add
|
||||
]);
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
|
||||
// The request verification is ready.
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.ready");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification ready to `m1`.
|
||||
await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("verification requests are synchronized and automatically updated", () => {
|
||||
expect(verificationRequest1.isReady()).toStrictEqual(true);
|
||||
expect(verificationRequest2.isReady()).toStrictEqual(true);
|
||||
|
||||
expect(verificationRequest1.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest1.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
|
||||
expect(verificationRequest2.theirSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
expect(verificationRequest2.ourSupportedMethods).toEqual(
|
||||
expect.arrayContaining([VerificationMethod.QrCodeScanV1, VerificationMethod.QrCodeShowV1]),
|
||||
);
|
||||
});
|
||||
|
||||
// QR verification for the second machine.
|
||||
let qr2;
|
||||
|
||||
test("can generate a QR code", async () => {
|
||||
qr2 = await verificationRequest2.generateQrCode();
|
||||
|
||||
expect(qr2).toBeInstanceOf(Qr);
|
||||
|
||||
expect(qr2.hasBeenScanned()).toStrictEqual(false);
|
||||
expect(qr2.hasBeenConfirmed()).toStrictEqual(false);
|
||||
expect(qr2.userId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(qr2.otherUserId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(qr2.otherDeviceId.toString()).toStrictEqual(deviceId1.toString());
|
||||
expect(qr2.weStarted()).toStrictEqual(false);
|
||||
expect(qr2.cancelInfo()).toBeUndefined();
|
||||
expect(qr2.isDone()).toStrictEqual(false);
|
||||
expect(qr2.isCancelled()).toStrictEqual(false);
|
||||
expect(qr2.isSelfVerification()).toStrictEqual(false);
|
||||
expect(qr2.reciprocated()).toStrictEqual(false);
|
||||
expect(qr2.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(qr2.roomId).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read QR code's bytes", async () => {
|
||||
const qrCodeHeader = "MATRIX";
|
||||
const qrCodeVersion = "\x02";
|
||||
|
||||
const qrCodeBytes = qr2.toBytes();
|
||||
|
||||
expect(qrCodeBytes).toHaveLength(122);
|
||||
expect(Array.from(qrCodeBytes.slice(0, 7))).toEqual(
|
||||
[...qrCodeHeader, ...qrCodeVersion].map((char) => char.charCodeAt(0)),
|
||||
);
|
||||
});
|
||||
|
||||
test("can render QR code", async () => {
|
||||
const qrCode = qr2.toQrCode();
|
||||
|
||||
expect(qrCode).toBeInstanceOf(QrCode);
|
||||
|
||||
// Want to get `canvasBuffer` to render the QR code? Install `npm install canvas` and uncomment the following blocks.
|
||||
|
||||
//let canvasBuffer;
|
||||
|
||||
{
|
||||
const buffer = qrCode.renderIntoBuffer();
|
||||
|
||||
expect(buffer).toBeInstanceOf(Uint8ClampedArray);
|
||||
// 45px ⨉ 45px
|
||||
expect(buffer).toHaveLength(45 * 45);
|
||||
// 0 for a white pixel, 1 for a black pixel.
|
||||
expect(buffer.every((p) => p == 0 || p == 1)).toStrictEqual(true);
|
||||
|
||||
/*
|
||||
const { Canvas } = require('canvas');
|
||||
const canvas = new Canvas(55, 55);
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = 'white';
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// New image data, filled with black, transparent pixels.
|
||||
const imageData = context.createImageData(45, 45);
|
||||
const data = imageData.data;
|
||||
|
||||
const [r, g, b, a] = [0, 1, 2, 3];
|
||||
|
||||
for (
|
||||
let dataNth = 0,
|
||||
bufferNth = 0;
|
||||
dataNth < data.length && bufferNth < buffer.length;
|
||||
dataNth += 4,
|
||||
bufferNth += 1
|
||||
) {
|
||||
data[dataNth + a] = 255;
|
||||
|
||||
// White pixel
|
||||
if (buffer[bufferNth] == 0) {
|
||||
data[dataNth + r] = 255;
|
||||
data[dataNth + g] = 255;
|
||||
data[dataNth + b] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
context.putImageData(imageData, 5, 5);
|
||||
canvasBuffer = canvas.toBuffer('image/png');
|
||||
*/
|
||||
}
|
||||
|
||||
// Want to see the QR code? Uncomment the following block.
|
||||
/*
|
||||
{
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--'));
|
||||
const qrCodeFile = path.join(tempDirectory, 'qrcode.png');
|
||||
|
||||
console.log(`View the QR code at \`${qrCodeFile}\`.`);
|
||||
|
||||
expect(await fs.writeFile(qrCodeFile, canvasBuffer)).toBeUndefined();
|
||||
}
|
||||
*/
|
||||
});
|
||||
|
||||
let qr1;
|
||||
|
||||
test("can scan a QR code from bytes", async () => {
|
||||
const scan = QrCodeScan.fromBytes(qr2.toBytes());
|
||||
|
||||
expect(scan).toBeInstanceOf(QrCodeScan);
|
||||
|
||||
qr1 = await verificationRequest1.scanQrCode(scan);
|
||||
|
||||
expect(qr1).toBeInstanceOf(Qr);
|
||||
|
||||
expect(qr1.hasBeenScanned()).toStrictEqual(false);
|
||||
expect(qr1.hasBeenConfirmed()).toStrictEqual(false);
|
||||
expect(qr1.userId.toString()).toStrictEqual(userId1.toString());
|
||||
expect(qr1.otherUserId.toString()).toStrictEqual(userId2.toString());
|
||||
expect(qr1.otherDeviceId.toString()).toStrictEqual(deviceId2.toString());
|
||||
expect(qr1.weStarted()).toStrictEqual(true);
|
||||
expect(qr1.cancelInfo()).toBeUndefined();
|
||||
expect(qr1.isDone()).toStrictEqual(false);
|
||||
expect(qr1.isCancelled()).toStrictEqual(false);
|
||||
expect(qr1.isSelfVerification()).toStrictEqual(false);
|
||||
expect(qr1.reciprocated()).toStrictEqual(true);
|
||||
expect(qr1.flowId).toMatch(/^[a-f0-9]+$/);
|
||||
expect(qr1.roomId).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can start a QR verification/reciprocate (`m.key.verification.start`)", async () => {
|
||||
let outgoingVerificationRequest = qr1.reciprocate();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.start");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId1.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId2.toString()][
|
||||
deviceId2.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("can confirm QR code has been scanned", () => {
|
||||
expect(qr2.hasBeenScanned()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
test("can confirm scanning (`m.key.verification.done`)", async () => {
|
||||
let outgoingVerificationRequest = qr2.confirmScanning();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.done");
|
||||
|
||||
const toDeviceEvents = [
|
||||
{
|
||||
sender: userId2.toString(),
|
||||
type: outgoingVerificationRequest.event_type,
|
||||
content: JSON.parse(outgoingVerificationRequest.body).messages[userId1.toString()][
|
||||
deviceId1.toString()
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Let's send the verification request to `m2`.
|
||||
await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set());
|
||||
});
|
||||
|
||||
test("can confirm QR code has been confirmed", () => {
|
||||
expect(qr2.hasBeenConfirmed()).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("VerificationMethod", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(VerificationMethod.SasV1).toStrictEqual(0);
|
||||
expect(VerificationMethod.QrCodeScanV1).toStrictEqual(1);
|
||||
expect(VerificationMethod.QrCodeShowV1).toStrictEqual(2);
|
||||
expect(VerificationMethod.ReciprocateV1).toStrictEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const {
|
||||
EncryptionAlgorithm,
|
||||
EncryptionSettings,
|
||||
HistoryVisibility,
|
||||
VerificationState,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
describe("EncryptionAlgorithm", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(EncryptionAlgorithm.OlmV1Curve25519AesSha2).toStrictEqual(0);
|
||||
expect(EncryptionAlgorithm.MegolmV1AesSha2).toStrictEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(EncryptionSettings.name, () => {
|
||||
test("can be instantiated with default values", () => {
|
||||
const es = new EncryptionSettings();
|
||||
|
||||
expect(es.algorithm).toStrictEqual(EncryptionAlgorithm.MegolmV1AesSha2);
|
||||
expect(es.rotationPeriod).toStrictEqual(604800000000n);
|
||||
expect(es.rotationPeriodMessages).toStrictEqual(100n);
|
||||
expect(es.historyVisibility).toStrictEqual(HistoryVisibility.Shared);
|
||||
});
|
||||
|
||||
test("checks the history visibility values", () => {
|
||||
const es = new EncryptionSettings();
|
||||
|
||||
es.historyVisibility = HistoryVisibility.Invited;
|
||||
|
||||
expect(es.historyVisibility).toStrictEqual(HistoryVisibility.Invited);
|
||||
expect(() => {
|
||||
es.historyVisibility = 42;
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
const { HistoryVisibility } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
describe("HistoryVisibility", () => {
|
||||
test("has the correct variant values", () => {
|
||||
expect(HistoryVisibility.Invited).toStrictEqual(0);
|
||||
expect(HistoryVisibility.Joined).toStrictEqual(1);
|
||||
expect(HistoryVisibility.Shared).toStrictEqual(2);
|
||||
expect(HistoryVisibility.WorldReadable).toStrictEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
const { DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest } = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
function* zip(...arrays) {
|
||||
const len = Math.min(...arrays.map((array) => array.length));
|
||||
|
||||
for (let nth = 0; nth < len; ++nth) {
|
||||
yield [...arrays.map((array) => array.at(nth))];
|
||||
}
|
||||
}
|
||||
|
||||
// Add a machine to another machine, i.e. be sure a machine knows
|
||||
// another exists.
|
||||
async function addMachineToMachine(machineToAdd, machine) {
|
||||
const toDeviceEvents = JSON.stringify([]);
|
||||
const changedDevices = new DeviceLists();
|
||||
const oneTimeKeyCounts = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
const receiveSyncChanges = JSON.parse(
|
||||
await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys),
|
||||
);
|
||||
|
||||
expect(receiveSyncChanges).toEqual([]);
|
||||
|
||||
const outgoingRequests = await machineToAdd.outgoingRequests();
|
||||
|
||||
expect(outgoingRequests).toHaveLength(2);
|
||||
|
||||
let keysUploadRequest;
|
||||
// Read the `KeysUploadRequest`.
|
||||
{
|
||||
expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest);
|
||||
expect(outgoingRequests[0].id).toBeDefined();
|
||||
expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload);
|
||||
expect(outgoingRequests[0].body).toBeDefined();
|
||||
|
||||
const body = JSON.parse(outgoingRequests[0].body);
|
||||
expect(body.device_keys).toBeDefined();
|
||||
expect(body.one_time_keys).toBeDefined();
|
||||
|
||||
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload
|
||||
const hypothetical_response = JSON.stringify({
|
||||
one_time_key_counts: {
|
||||
curve25519: 10,
|
||||
signed_curve25519: 20,
|
||||
},
|
||||
});
|
||||
const marked = await machineToAdd.markRequestAsSent(
|
||||
outgoingRequests[0].id,
|
||||
outgoingRequests[0].type,
|
||||
hypothetical_response,
|
||||
);
|
||||
expect(marked).toStrictEqual(true);
|
||||
|
||||
keysUploadRequest = outgoingRequests[0];
|
||||
}
|
||||
|
||||
{
|
||||
expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest);
|
||||
|
||||
let [signingKeysUploadRequest, _] = await machineToAdd.bootstrapCrossSigning(true);
|
||||
|
||||
// Let's forge a `KeysQuery`'s response.
|
||||
let keyQueryResponse = {
|
||||
device_keys: {},
|
||||
master_keys: {},
|
||||
self_signing_keys: {},
|
||||
user_signing_keys: {},
|
||||
};
|
||||
const userId = machineToAdd.userId.toString();
|
||||
const deviceId = machineToAdd.deviceId.toString();
|
||||
keyQueryResponse.device_keys[userId] = {};
|
||||
keyQueryResponse.device_keys[userId][deviceId] = JSON.parse(keysUploadRequest.body).device_keys;
|
||||
|
||||
const keys = JSON.parse(signingKeysUploadRequest.body);
|
||||
keyQueryResponse.master_keys[userId] = keys.master_key;
|
||||
keyQueryResponse.self_signing_keys[userId] = keys.self_signing_key;
|
||||
keyQueryResponse.user_signing_keys[userId] = keys.user_signing_key;
|
||||
|
||||
const marked = await machine.markRequestAsSent(
|
||||
outgoingRequests[1].id,
|
||||
outgoingRequests[1].type,
|
||||
JSON.stringify(keyQueryResponse),
|
||||
);
|
||||
expect(marked).toStrictEqual(true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
zip,
|
||||
addMachineToMachine,
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
const {
|
||||
DeviceId,
|
||||
DeviceKeyAlgorithm,
|
||||
DeviceKeyAlgorithmName,
|
||||
DeviceKeyId,
|
||||
EventId,
|
||||
RoomId,
|
||||
ServerName,
|
||||
UserId,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
|
||||
describe(UserId.name, () => {
|
||||
test("cannot be invalid", () => {
|
||||
expect(() => {
|
||||
new UserId("@foobar");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
const user = new UserId("@foo:bar.org");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(user.localpart).toStrictEqual("foo");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(user.serverName).toBeInstanceOf(ServerName);
|
||||
});
|
||||
|
||||
test("user ID is not historical", () => {
|
||||
expect(user.isHistorical()).toStrictEqual(false);
|
||||
});
|
||||
|
||||
test("can read the user ID as a string", () => {
|
||||
expect(user.toString()).toStrictEqual("@foo:bar.org");
|
||||
});
|
||||
});
|
||||
|
||||
describe(DeviceId.name, () => {
|
||||
const device = new DeviceId("foo");
|
||||
|
||||
test("can read the device ID as a string", () => {
|
||||
expect(device.toString()).toStrictEqual("foo");
|
||||
});
|
||||
});
|
||||
|
||||
describe(DeviceKeyId.name, () => {
|
||||
for (const deviceKey of [
|
||||
{
|
||||
name: "ed25519",
|
||||
id: "ed25519:foobar",
|
||||
algorithmName: DeviceKeyAlgorithmName.Ed25519,
|
||||
algorithm: "ed25519",
|
||||
deviceId: "foobar",
|
||||
},
|
||||
|
||||
{
|
||||
name: "curve25519",
|
||||
id: "curve25519:foobar",
|
||||
algorithmName: DeviceKeyAlgorithmName.Curve25519,
|
||||
algorithm: "curve25519",
|
||||
deviceId: "foobar",
|
||||
},
|
||||
|
||||
{
|
||||
name: "signed curve25519",
|
||||
id: "signed_curve25519:foobar",
|
||||
algorithmName: DeviceKeyAlgorithmName.SignedCurve25519,
|
||||
algorithm: "signed_curve25519",
|
||||
deviceId: "foobar",
|
||||
},
|
||||
|
||||
{
|
||||
name: "unknown",
|
||||
id: "hello:foobar",
|
||||
algorithmName: DeviceKeyAlgorithmName.Unknown,
|
||||
algorithm: "hello",
|
||||
deviceId: "foobar",
|
||||
},
|
||||
]) {
|
||||
test(`${deviceKey.name} algorithm`, () => {
|
||||
const dk = new DeviceKeyId(deviceKey.id);
|
||||
|
||||
expect(dk.algorithm.name).toStrictEqual(deviceKey.algorithmName);
|
||||
expect(dk.algorithm.toString()).toStrictEqual(deviceKey.algorithm);
|
||||
expect(dk.deviceId.toString()).toStrictEqual(deviceKey.deviceId);
|
||||
expect(dk.toString()).toStrictEqual(deviceKey.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("DeviceKeyAlgorithmName", () => {
|
||||
test("has the correct variants", () => {
|
||||
expect(DeviceKeyAlgorithmName.Ed25519).toStrictEqual(0);
|
||||
expect(DeviceKeyAlgorithmName.Curve25519).toStrictEqual(1);
|
||||
expect(DeviceKeyAlgorithmName.SignedCurve25519).toStrictEqual(2);
|
||||
expect(DeviceKeyAlgorithmName.Unknown).toStrictEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe(RoomId.name, () => {
|
||||
test("cannot be invalid", () => {
|
||||
expect(() => {
|
||||
new RoomId("!foo");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
const room = new RoomId("!foo:bar.org");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("foo");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeInstanceOf(ServerName);
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("!foo:bar.org");
|
||||
});
|
||||
});
|
||||
|
||||
describe(ServerName.name, () => {
|
||||
test("cannot be invalid", () => {
|
||||
expect(() => {
|
||||
new ServerName("@foobar");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("host is present", () => {
|
||||
expect(new ServerName("foo.org").host).toStrictEqual("foo.org");
|
||||
});
|
||||
|
||||
test("port can be optional", () => {
|
||||
expect(new ServerName("foo.org").port).toStrictEqual(undefined);
|
||||
expect(new ServerName("foo.org:1234").port).toStrictEqual(1234);
|
||||
});
|
||||
|
||||
test("server is not an IP literal", () => {
|
||||
expect(new ServerName("foo.org").isIpLiteral()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(EventId.name, () => {
|
||||
test("cannot be invalid", () => {
|
||||
expect(() => {
|
||||
new EventId("%foo");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe("Versions 1 & 2", () => {
|
||||
const room = new EventId("$h29iv0s8:foo.org");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("h29iv0s8");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeInstanceOf(ServerName);
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("$h29iv0s8:foo.org");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Version 3", () => {
|
||||
const room = new EventId("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Version 4", () => {
|
||||
const room = new EventId("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
|
||||
test("localpart is present", () => {
|
||||
expect(room.localpart).toStrictEqual("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
});
|
||||
|
||||
test("server name is present", () => {
|
||||
expect(room.serverName).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read the room ID as string", () => {
|
||||
expect(room.toString()).toStrictEqual("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,872 @@
|
||||
const {
|
||||
CrossSigningStatus,
|
||||
DecryptedRoomEvent,
|
||||
DeviceId,
|
||||
DeviceKeyId,
|
||||
DeviceLists,
|
||||
EncryptionSettings,
|
||||
EventId,
|
||||
InboundGroupSession,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
MaybeSignature,
|
||||
OlmMachine,
|
||||
OwnUserIdentity,
|
||||
RequestType,
|
||||
RoomId,
|
||||
RoomMessageRequest,
|
||||
ShieldColor,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
UserId,
|
||||
UserIdentity,
|
||||
VerificationRequest,
|
||||
VerificationState,
|
||||
Versions,
|
||||
getVersions,
|
||||
} = require("../pkg/matrix_sdk_crypto_js");
|
||||
const { addMachineToMachine } = require("./helper");
|
||||
require("fake-indexeddb/auto");
|
||||
|
||||
describe("Versions", () => {
|
||||
test("can find out the crate versions", async () => {
|
||||
const versions = getVersions();
|
||||
|
||||
expect(versions).toBeInstanceOf(Versions);
|
||||
expect(versions.vodozemac).toBeDefined();
|
||||
expect(versions.matrix_sdk_crypto).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe(OlmMachine.name, () => {
|
||||
test("can be instantiated with the async initializer", async () => {
|
||||
expect(await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"))).toBeInstanceOf(OlmMachine);
|
||||
});
|
||||
|
||||
test("can be instantiated with a store", async () => {
|
||||
let store_name = "hello";
|
||||
let store_passphrase = "world";
|
||||
|
||||
const by_store_name = (db) => db.name.startsWith(store_name);
|
||||
|
||||
// No databases.
|
||||
expect((await indexedDB.databases()).filter(by_store_name)).toHaveLength(0);
|
||||
|
||||
// Creating a new Olm machine.
|
||||
expect(
|
||||
await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), store_name, store_passphrase),
|
||||
).toBeInstanceOf(OlmMachine);
|
||||
|
||||
// Oh, there is 2 databases now, prefixed by `store_name`.
|
||||
let databases = (await indexedDB.databases()).filter(by_store_name);
|
||||
|
||||
expect(databases).toHaveLength(2);
|
||||
expect(databases).toStrictEqual([
|
||||
{ name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 },
|
||||
{ name: `${store_name}::matrix-sdk-crypto`, version: 3 },
|
||||
]);
|
||||
|
||||
// Creating a new Olm machine, with the stored state.
|
||||
expect(
|
||||
await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"), store_name, store_passphrase),
|
||||
).toBeInstanceOf(OlmMachine);
|
||||
|
||||
// Same number of databases.
|
||||
expect((await indexedDB.databases()).filter(by_store_name)).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe("cannot be instantiated with a store", () => {
|
||||
test("store name is missing", async () => {
|
||||
let store_name = null;
|
||||
let store_passphrase = "world";
|
||||
|
||||
let err = null;
|
||||
|
||||
try {
|
||||
await OlmMachine.initialize(
|
||||
new UserId("@foo:bar.org"),
|
||||
new DeviceId("baz"),
|
||||
store_name,
|
||||
store_passphrase,
|
||||
);
|
||||
} catch (error) {
|
||||
err = error;
|
||||
}
|
||||
|
||||
expect(err).toBeDefined();
|
||||
});
|
||||
|
||||
test("store passphrase is missing", async () => {
|
||||
let store_name = "hello";
|
||||
let store_passphrase = null;
|
||||
|
||||
let err = null;
|
||||
|
||||
try {
|
||||
await OlmMachine.initialize(
|
||||
new UserId("@foo:bar.org"),
|
||||
new DeviceId("baz"),
|
||||
store_name,
|
||||
store_passphrase,
|
||||
);
|
||||
} catch (error) {
|
||||
err = error;
|
||||
}
|
||||
|
||||
expect(err).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
const user = new UserId("@alice:example.org");
|
||||
const device = new DeviceId("foobar");
|
||||
const room = new RoomId("!baz:matrix.org");
|
||||
|
||||
function machine(new_user, new_device) {
|
||||
return OlmMachine.initialize(new_user || user, new_device || device);
|
||||
}
|
||||
|
||||
test("can drop/close", async () => {
|
||||
m = await machine();
|
||||
m.close();
|
||||
});
|
||||
|
||||
test("can drop/close with a store", async () => {
|
||||
let store_name = "temporary";
|
||||
let store_passphrase = "temporary";
|
||||
|
||||
const by_store_name = (db) => db.name.startsWith(store_name);
|
||||
|
||||
// No databases.
|
||||
expect((await indexedDB.databases()).filter(by_store_name)).toHaveLength(0);
|
||||
|
||||
// Creating a new Olm machine.
|
||||
const m = await OlmMachine.initialize(
|
||||
new UserId("@foo:bar.org"),
|
||||
new DeviceId("baz"),
|
||||
store_name,
|
||||
store_passphrase,
|
||||
);
|
||||
expect(m).toBeInstanceOf(OlmMachine);
|
||||
|
||||
// Oh, there is 2 databases now, prefixed by `store_name`.
|
||||
let databases = (await indexedDB.databases()).filter(by_store_name);
|
||||
|
||||
expect(databases).toHaveLength(2);
|
||||
expect(databases).toStrictEqual([
|
||||
{ name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 },
|
||||
{ name: `${store_name}::matrix-sdk-crypto`, version: 3 },
|
||||
]);
|
||||
|
||||
// Let's force to close the `OlmMachine`.
|
||||
m.close();
|
||||
|
||||
// Now we can delete the databases!
|
||||
for (const database_name of [`${store_name}::matrix-sdk-crypto`, `${store_name}::matrix-sdk-crypto-meta`]) {
|
||||
const deleting = indexedDB.deleteDatabase(database_name);
|
||||
deleting.onsuccess = () => {};
|
||||
deleting.onerror = () => {
|
||||
throw new Error("failed to remove the database (error)");
|
||||
};
|
||||
deleting.onblocked = () => {
|
||||
throw new Error("failed to remove the database (blocked)");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
test("can read user ID", async () => {
|
||||
expect((await machine()).userId.toString()).toStrictEqual(user.toString());
|
||||
});
|
||||
|
||||
test("can read device ID", async () => {
|
||||
expect((await machine()).deviceId.toString()).toStrictEqual(device.toString());
|
||||
});
|
||||
|
||||
test("can read identity keys", async () => {
|
||||
const identityKeys = (await machine()).identityKeys;
|
||||
|
||||
expect(identityKeys.ed25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
|
||||
expect(identityKeys.curve25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/);
|
||||
});
|
||||
|
||||
test("can read display name", async () => {
|
||||
expect(await machine().displayName).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can read tracked users", async () => {
|
||||
const m = await machine();
|
||||
const trackedUsers = await m.trackedUsers();
|
||||
|
||||
expect(trackedUsers).toBeInstanceOf(Set);
|
||||
expect(trackedUsers.size).toStrictEqual(0);
|
||||
});
|
||||
|
||||
test("can update tracked users", async () => {
|
||||
const m = await machine();
|
||||
|
||||
expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
test("can receive sync changes", async () => {
|
||||
const m = await machine();
|
||||
const toDeviceEvents = JSON.stringify([]);
|
||||
const changedDevices = new DeviceLists();
|
||||
const oneTimeKeyCounts = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
const receiveSyncChanges = JSON.parse(
|
||||
await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys),
|
||||
);
|
||||
|
||||
expect(receiveSyncChanges).toEqual([]);
|
||||
});
|
||||
|
||||
test("can get the outgoing requests that need to be send out", async () => {
|
||||
const m = await machine();
|
||||
const toDeviceEvents = JSON.stringify([]);
|
||||
const changedDevices = new DeviceLists();
|
||||
const oneTimeKeyCounts = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
const receiveSyncChanges = JSON.parse(
|
||||
await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys),
|
||||
);
|
||||
|
||||
expect(receiveSyncChanges).toEqual([]);
|
||||
|
||||
const outgoingRequests = await m.outgoingRequests();
|
||||
|
||||
expect(outgoingRequests).toHaveLength(2);
|
||||
|
||||
{
|
||||
expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest);
|
||||
expect(outgoingRequests[0].id).toBeDefined();
|
||||
expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload);
|
||||
expect(outgoingRequests[0].body).toBeDefined();
|
||||
|
||||
const body = JSON.parse(outgoingRequests[0].body);
|
||||
expect(body.device_keys).toBeDefined();
|
||||
expect(body.one_time_keys).toBeDefined();
|
||||
}
|
||||
|
||||
{
|
||||
expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest);
|
||||
expect(outgoingRequests[1].id).toBeDefined();
|
||||
expect(outgoingRequests[1].type).toStrictEqual(RequestType.KeysQuery);
|
||||
expect(outgoingRequests[1].body).toBeDefined();
|
||||
|
||||
const body = JSON.parse(outgoingRequests[1].body);
|
||||
expect(body.timeout).toBeDefined();
|
||||
expect(body.device_keys).toBeDefined();
|
||||
expect(body.token).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
describe("setup workflow to mark requests as sent", () => {
|
||||
let m;
|
||||
let ougoingRequests;
|
||||
|
||||
beforeAll(async () => {
|
||||
m = await machine(new UserId("@alice:example.org"), new DeviceId("DEVICEID"));
|
||||
|
||||
const toDeviceEvents = JSON.stringify([]);
|
||||
const changedDevices = new DeviceLists();
|
||||
const oneTimeKeyCounts = new Map();
|
||||
const unusedFallbackKeys = new Set();
|
||||
|
||||
const receiveSyncChanges = await m.receiveSyncChanges(
|
||||
toDeviceEvents,
|
||||
changedDevices,
|
||||
oneTimeKeyCounts,
|
||||
unusedFallbackKeys,
|
||||
);
|
||||
outgoingRequests = await m.outgoingRequests();
|
||||
|
||||
expect(outgoingRequests).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("can mark requests as sent", async () => {
|
||||
{
|
||||
const request = outgoingRequests[0];
|
||||
expect(request).toBeInstanceOf(KeysUploadRequest);
|
||||
|
||||
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload
|
||||
const hypothetical_response = JSON.stringify({
|
||||
one_time_key_counts: {
|
||||
curve25519: 10,
|
||||
signed_curve25519: 20,
|
||||
},
|
||||
});
|
||||
const marked = await m.markRequestAsSent(request.id, request.type, hypothetical_response);
|
||||
expect(marked).toStrictEqual(true);
|
||||
}
|
||||
|
||||
{
|
||||
const request = outgoingRequests[1];
|
||||
expect(request).toBeInstanceOf(KeysQueryRequest);
|
||||
|
||||
// https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysquery
|
||||
const hypothetical_response = JSON.stringify({
|
||||
device_keys: {
|
||||
"@alice:example.org": {
|
||||
JLAFKJWSCS: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "JLAFKJWSCS",
|
||||
keys: {
|
||||
"curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
|
||||
"ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM",
|
||||
},
|
||||
signatures: {
|
||||
"@alice:example.org": {
|
||||
"ed25519:JLAFKJWSCS":
|
||||
"m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA",
|
||||
},
|
||||
},
|
||||
unsigned: {
|
||||
device_display_name: "Alice's mobile phone",
|
||||
},
|
||||
user_id: "@alice:example.org",
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
const marked = await m.markRequestAsSent(request.id, request.type, hypothetical_response);
|
||||
expect(marked).toStrictEqual(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("setup workflow to encrypt/decrypt events", () => {
|
||||
let m;
|
||||
const user = new UserId("@alice:example.org");
|
||||
const device = new DeviceId("JLAFKJWSCS");
|
||||
const room = new RoomId("!test:localhost");
|
||||
|
||||
beforeAll(async () => {
|
||||
m = await machine(user, device);
|
||||
});
|
||||
|
||||
test("can pass keysquery and keysclaim requests directly", async () => {
|
||||
{
|
||||
// derived from https://github.com/matrix-org/matrix-rust-sdk/blob/7f49618d350fab66b7e1dc4eaf64ec25ceafd658/benchmarks/benches/crypto_bench/keys_query.json
|
||||
const hypothetical_response = JSON.stringify({
|
||||
device_keys: {
|
||||
"@example:localhost": {
|
||||
AFGUOBTZWM: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "AFGUOBTZWM",
|
||||
keys: {
|
||||
"curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo",
|
||||
"ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec",
|
||||
},
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:AFGUOBTZWM":
|
||||
"RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA",
|
||||
},
|
||||
},
|
||||
user_id: "@example:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "rust-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@example:localhost": {
|
||||
user_id: "@example:localhost",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU":
|
||||
"n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU",
|
||||
},
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:TCSJXPWGVS":
|
||||
"+j9G3L41I1fe0++wwusTTQvbboYW0yDtRWUEujhwZz4MAltjLSfJvY0hxhnz+wHHmuEXvQDen39XOpr1p29sAg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@example:localhost": {
|
||||
user_id: "@example:localhost",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:kQXOuy639Yt47mvNTdrIluoC6DMvfbZLYbxAmwiDyhI":
|
||||
"kQXOuy639Yt47mvNTdrIluoC6DMvfbZLYbxAmwiDyhI",
|
||||
},
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU":
|
||||
"q32ifix/qyRpvmegw2BEJklwoBCAJldDNkcX+fp+lBA4Rpyqtycxge6BA4hcJdxYsy3oV0IHRuugS8rJMMFyAA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@example:localhost": {
|
||||
user_id: "@example:localhost",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:g4ED07Fnqf3GzVWNN1pZ0IFrPQVdqQf+PYoJNH4eE0s":
|
||||
"g4ED07Fnqf3GzVWNN1pZ0IFrPQVdqQf+PYoJNH4eE0s",
|
||||
},
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU":
|
||||
"nKQu8alQKDefNbZz9luYPcNj+Z+ouQSot4fU/A23ELl1xrI06QVBku/SmDx0sIW1ytso0Cqwy1a+3PzCa1XABg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const marked = await m.markRequestAsSent("foo", RequestType.KeysQuery, hypothetical_response);
|
||||
}
|
||||
|
||||
{
|
||||
// derived from https://github.com/matrix-org/matrix-rust-sdk/blob/7f49618d350fab66b7e1dc4eaf64ec25ceafd658/benchmarks/benches/crypto_bench/keys_claim.json
|
||||
const hypothetical_response = JSON.stringify({
|
||||
one_time_keys: {
|
||||
"@example:localhost": {
|
||||
AFGUOBTZWM: {
|
||||
"signed_curve25519:AAAABQ": {
|
||||
key: "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws",
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:AFGUOBTZWM":
|
||||
"2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
const marked = await m.markRequestAsSent("bar", RequestType.KeysClaim, hypothetical_response);
|
||||
}
|
||||
});
|
||||
|
||||
test("can share a room key", async () => {
|
||||
const other_users = [new UserId("@example:localhost")];
|
||||
|
||||
const requests = await m.shareRoomKey(room, other_users, new EncryptionSettings());
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]).toBeInstanceOf(ToDeviceRequest);
|
||||
expect(requests[0].event_type).toEqual("m.room.encrypted");
|
||||
expect(requests[0].txn_id).toBeDefined();
|
||||
const content = JSON.parse(requests[0].body);
|
||||
expect(Object.keys(content.messages)).toEqual(["@example:localhost"]);
|
||||
});
|
||||
|
||||
let encrypted;
|
||||
|
||||
test("can encrypt an event", async () => {
|
||||
encrypted = JSON.parse(
|
||||
await m.encryptRoomEvent(
|
||||
room,
|
||||
"m.room.message",
|
||||
JSON.stringify({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World!",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(encrypted.algorithm).toBeDefined();
|
||||
expect(encrypted.ciphertext).toBeDefined();
|
||||
expect(encrypted.sender_key).toBeDefined();
|
||||
expect(encrypted.device_id).toStrictEqual(device.toString());
|
||||
expect(encrypted.session_id).toBeDefined();
|
||||
});
|
||||
|
||||
test("can decrypt an event", async () => {
|
||||
const decrypted = await m.decryptRoomEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.encrypted",
|
||||
event_id: "$xxxxx:example.org",
|
||||
origin_server_ts: Date.now(),
|
||||
sender: user.toString(),
|
||||
content: encrypted,
|
||||
unsigned: {
|
||||
age: 1234,
|
||||
},
|
||||
}),
|
||||
room,
|
||||
);
|
||||
|
||||
expect(decrypted).toBeInstanceOf(DecryptedRoomEvent);
|
||||
|
||||
const event = JSON.parse(decrypted.event);
|
||||
expect(event.content.msgtype).toStrictEqual("m.text");
|
||||
expect(event.content.body).toStrictEqual("Hello, World!");
|
||||
|
||||
expect(decrypted.sender.toString()).toStrictEqual(user.toString());
|
||||
expect(decrypted.senderDevice.toString()).toStrictEqual(device.toString());
|
||||
expect(decrypted.senderCurve25519Key).toBeDefined();
|
||||
expect(decrypted.senderClaimedEd25519Key).toBeDefined();
|
||||
expect(decrypted.forwardingCurve25519KeyChain).toHaveLength(0);
|
||||
expect(decrypted.shieldState(true).color).toStrictEqual(ShieldColor.Red);
|
||||
expect(decrypted.shieldState(false).color).toStrictEqual(ShieldColor.Red);
|
||||
});
|
||||
});
|
||||
|
||||
test("failure to decrypt returns a valid error", async () => {
|
||||
const m = await machine();
|
||||
const evt = {
|
||||
type: "m.room.encrypted",
|
||||
event_id: "$xxxxx:example.org",
|
||||
origin_server_ts: Date.now(),
|
||||
sender: user.toString(),
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blah",
|
||||
},
|
||||
};
|
||||
await expect(() => m.decryptRoomEvent(JSON.stringify(evt), room)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test("can read cross-signing status", async () => {
|
||||
const m = await machine();
|
||||
const crossSigningStatus = await m.crossSigningStatus();
|
||||
|
||||
expect(crossSigningStatus).toBeInstanceOf(CrossSigningStatus);
|
||||
expect(crossSigningStatus.hasMaster).toStrictEqual(false);
|
||||
expect(crossSigningStatus.hasSelfSigning).toStrictEqual(false);
|
||||
expect(crossSigningStatus.hasUserSigning).toStrictEqual(false);
|
||||
});
|
||||
|
||||
test("can sign a message", async () => {
|
||||
const m = await machine();
|
||||
const signatures = await m.sign("foo");
|
||||
|
||||
expect(signatures.isEmpty()).toStrictEqual(false);
|
||||
expect(signatures.count).toStrictEqual(1);
|
||||
|
||||
let base64;
|
||||
|
||||
// `get`
|
||||
{
|
||||
const signature = signatures.get(user);
|
||||
|
||||
expect(signature.has("ed25519:foobar")).toStrictEqual(true);
|
||||
|
||||
const s = signature.get("ed25519:foobar");
|
||||
|
||||
expect(s).toBeInstanceOf(MaybeSignature);
|
||||
|
||||
expect(s.isValid()).toStrictEqual(true);
|
||||
expect(s.isInvalid()).toStrictEqual(false);
|
||||
expect(s.invalidSignatureSource).toBeUndefined();
|
||||
|
||||
base64 = s.signature.toBase64();
|
||||
|
||||
expect(base64).toMatch(/^[A-Za-z0-9\+/]+$/);
|
||||
expect(s.signature.ed25519.toBase64()).toStrictEqual(base64);
|
||||
}
|
||||
|
||||
// `getSignature`
|
||||
{
|
||||
const signature = signatures.getSignature(user, new DeviceKeyId("ed25519:foobar"));
|
||||
expect(signature.toBase64()).toStrictEqual(base64);
|
||||
}
|
||||
|
||||
// Unknown signatures.
|
||||
{
|
||||
expect(signatures.get(new UserId("@hello:example.org"))).toBeUndefined();
|
||||
expect(signatures.getSignature(user, new DeviceKeyId("world:foobar"))).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("can get a user identities", async () => {
|
||||
const m = await machine();
|
||||
let _ = m.bootstrapCrossSigning(true);
|
||||
|
||||
const identity = await m.getIdentity(user);
|
||||
|
||||
expect(identity).toBeInstanceOf(OwnUserIdentity);
|
||||
|
||||
const signatureUploadRequest = await identity.verify();
|
||||
expect(signatureUploadRequest).toBeInstanceOf(SignatureUploadRequest);
|
||||
|
||||
const [verificationRequest, outgoingVerificationRequest] = await identity.requestVerification();
|
||||
expect(verificationRequest).toBeInstanceOf(VerificationRequest);
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
|
||||
|
||||
const isTrusted = await identity.trustsOurOwnDevice();
|
||||
|
||||
expect(isTrusted).toStrictEqual(false);
|
||||
});
|
||||
|
||||
describe("can export/import room keys", () => {
|
||||
let exportedRoomKeys;
|
||||
|
||||
test("can export room keys", async () => {
|
||||
let m = await machine();
|
||||
await m.shareRoomKey(room, [new UserId("@bob:example.org")], new EncryptionSettings());
|
||||
|
||||
exportedRoomKeys = await m.exportRoomKeys((session) => {
|
||||
expect(session).toBeInstanceOf(InboundGroupSession);
|
||||
expect(session.senderKey.toBase64()).toEqual(m.identityKeys.curve25519.toBase64());
|
||||
expect(session.roomId.toString()).toStrictEqual(room.toString());
|
||||
expect(session.sessionId).toBeDefined();
|
||||
expect(session.hasBeenImported()).toStrictEqual(false);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const roomKeys = JSON.parse(exportedRoomKeys);
|
||||
expect(roomKeys).toHaveLength(1);
|
||||
expect(roomKeys[0]).toMatchObject({
|
||||
algorithm: expect.any(String),
|
||||
room_id: room.toString(),
|
||||
sender_key: expect.any(String),
|
||||
session_id: expect.any(String),
|
||||
session_key: expect.any(String),
|
||||
sender_claimed_keys: {
|
||||
ed25519: expect.any(String),
|
||||
},
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
});
|
||||
|
||||
let encryptedExportedRoomKeys;
|
||||
let encryptionPassphrase = "Hello, Matrix!";
|
||||
|
||||
test("can encrypt the exported room keys", () => {
|
||||
encryptedExportedRoomKeys = OlmMachine.encryptExportedRoomKeys(
|
||||
exportedRoomKeys,
|
||||
encryptionPassphrase,
|
||||
100_000,
|
||||
);
|
||||
|
||||
expect(encryptedExportedRoomKeys).toMatch(/^-----BEGIN MEGOLM SESSION DATA-----/);
|
||||
});
|
||||
|
||||
test("can decrypt the exported room keys", () => {
|
||||
const decryptedExportedRoomKeys = OlmMachine.decryptExportedRoomKeys(
|
||||
encryptedExportedRoomKeys,
|
||||
encryptionPassphrase,
|
||||
);
|
||||
|
||||
expect(decryptedExportedRoomKeys).toStrictEqual(exportedRoomKeys);
|
||||
});
|
||||
|
||||
test("can import room keys", async () => {
|
||||
const progressListener = (progress, total) => {
|
||||
expect(progress).toBeLessThan(total);
|
||||
|
||||
// Since it's called only once, let's be crazy.
|
||||
expect(progress).toStrictEqual(0n);
|
||||
expect(total).toStrictEqual(1n);
|
||||
};
|
||||
|
||||
let m = await machine();
|
||||
const result = JSON.parse(await m.importRoomKeys(exportedRoomKeys, progressListener));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
imported_count: expect.any(Number),
|
||||
total_count: expect.any(Number),
|
||||
keys: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test("importing room keys calls RoomKeyUpdatedCallback", async () => {
|
||||
const callback = jest.fn();
|
||||
callback.mockImplementation(() => Promise.resolve(undefined));
|
||||
let m = await machine();
|
||||
m.registerRoomKeyUpdatedCallback(callback);
|
||||
await m.importRoomKeys(exportedRoomKeys, (_, _1) => {});
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
let keyInfoList = callback.mock.calls[0][0];
|
||||
expect(keyInfoList.length).toEqual(1);
|
||||
expect(keyInfoList[0].roomId.toString()).toStrictEqual(room.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("can do in-room verification", () => {
|
||||
let m;
|
||||
const user = new UserId("@alice:example.org");
|
||||
const device = new DeviceId("JLAFKJWSCS");
|
||||
const room = new RoomId("!test:localhost");
|
||||
|
||||
beforeAll(async () => {
|
||||
m = await machine(user, device);
|
||||
});
|
||||
|
||||
test("can inject devices from someone else", async () => {
|
||||
{
|
||||
const hypothetical_response = JSON.stringify({
|
||||
device_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
ATRLDCRXAC: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "ATRLDCRXAC",
|
||||
keys: {
|
||||
"curve25519:ATRLDCRXAC": "cAVT5Es3Z3F5pFD+2w3HT7O9+R3PstzYVkzD51X/FWQ",
|
||||
"ed25519:ATRLDCRXAC": "V2w/T/x7i7AXiCCtS6JldrpbvRliRoef3CqTUNqMRHA",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:ATRLDCRXAC":
|
||||
"ro2BjO5J6089B/JOANHnFmGrogrC2TIdMlgJbJO00DjOOcGxXfvOezCFIORTwZNHvkHU617YIGl/4keTDIWvBQ",
|
||||
},
|
||||
},
|
||||
user_id: "@example:morpheus.localhost",
|
||||
unsigned: {
|
||||
device_display_name: "Element Desktop: Linux",
|
||||
},
|
||||
},
|
||||
EYYGYTCTNC: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "EYYGYTCTNC",
|
||||
keys: {
|
||||
"curve25519:EYYGYTCTNC": "Pqu50fo472wgb6NjKkaUxjuqoAIEAmhln2gw/zSQ7Ek",
|
||||
"ed25519:EYYGYTCTNC": "Pf/2QPvui8lDty6TCTglVPRVM+irNHYavNNkyv5yFpU",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:EYYGYTCTNC":
|
||||
"pnP5BYLEUUaxDgrvdzCznkjNDbvY1/MFBr1JejdnLiXlcmxRULQpIWZUCO7QTbULsCwMsYQNGn50nfmjBQX3CQ",
|
||||
},
|
||||
},
|
||||
user_id: "@example:morpheus.localhost",
|
||||
unsigned: {
|
||||
device_display_name: "WeeChat-Matrix-rs",
|
||||
},
|
||||
},
|
||||
SUMODVLSIU: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "SUMODVLSIU",
|
||||
keys: {
|
||||
"curve25519:SUMODVLSIU": "geQXWGWc++gcUHk0JcFmEVSjyzDOnk2mjVsUQwbNqQU",
|
||||
"ed25519:SUMODVLSIU": "ccktaQ3g+B18E6FwVhTBYie26OlHbvDUzDEtxOQ4Qcs",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:SUMODVLSIU":
|
||||
"Yn+AOxHRt1GQpY2xT2Jcqqn8jh5+Vw23ctA7NXyDiWPsLPLNTpjGWHMjZdpUqflQvpiKfhODPICoIa7Pu0iSAg",
|
||||
"ed25519:rUiMNDjIu6gqsrhJPbj3phyIzuEtuQGrLOEa9mCbtTM":
|
||||
"Cio6k/sq289XNTOvTCWre7Q6zg+A3euzMUe7Uy1T3gPqYFzX+kt7EAxrhbPqx1HyXAEz9zD0D/uw9VEXFCvWBQ",
|
||||
},
|
||||
},
|
||||
user_id: "@example:morpheus.localhost",
|
||||
unsigned: {
|
||||
device_display_name: "Element Desktop (Linux)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
user_id: "@example:morpheus.localhost",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A":
|
||||
"ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:SUMODVLSIU":
|
||||
"RL6WOuuzB/mZ+edfUFG/KeEcmKh+NaWpM6m2bUYmDnJrtTCYyoU+pgHJuL2/6nynemmONo18JEHBuqtNcMq2AQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
user_id: "@example:morpheus.localhost",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:rUiMNDjIu6gqsrhJPbj3phyIzuEtuQGrLOEa9mCbtTM":
|
||||
"rUiMNDjIu6gqsrhJPbj3phyIzuEtuQGrLOEa9mCbtTM",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A":
|
||||
"uCBn9rpeg6umY8H97ejN26UMp6QDwNL98869t1DoVGL50J8adLN05OZd8lYk9QzwTr2d56ZTGYSYX8kv28SDDA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@example:morpheus.localhost": {
|
||||
user_id: "@example:morpheus.localhost",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:GLhEKLQ50jnF6IMEPsO2ucpHUNIUEnbBXs5gYbHg4Aw":
|
||||
"GLhEKLQ50jnF6IMEPsO2ucpHUNIUEnbBXs5gYbHg4Aw",
|
||||
},
|
||||
signatures: {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:ZzU4WCyBfOFitdGmfKCq6F39iQCDk/zhNNTsi+tWH7A":
|
||||
"4fIyWlVzuz1pgoegNLZASycORXqKycVS0dNq5vmmwsVEudp1yrPhndnaIJ3fjF8LDHvwzXTvohOid7DiU1j0AA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const marked = await m.markRequestAsSent("foo", RequestType.KeysQuery, hypothetical_response);
|
||||
}
|
||||
});
|
||||
|
||||
test("can start an in-room SAS verification", async () => {
|
||||
let _ = m.bootstrapCrossSigning(true);
|
||||
const identity = await m.getIdentity(new UserId("@example:morpheus.localhost"));
|
||||
|
||||
expect(identity).toBeInstanceOf(UserIdentity);
|
||||
expect(identity.isVerified()).toStrictEqual(false);
|
||||
|
||||
const eventId = new EventId("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg");
|
||||
const verificationRequest = await identity.requestVerification(room, eventId);
|
||||
expect(verificationRequest).toBeInstanceOf(VerificationRequest);
|
||||
|
||||
await m.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
sender: "@example:morpheus.localhost",
|
||||
type: "m.key.verification.ready",
|
||||
event_id: "$QguWmaeMt6Hao7Ea6XHDInvr8ndknev79t9a2eBxlz0",
|
||||
origin_server_ts: 1674037263075,
|
||||
content: {
|
||||
"methods": ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"],
|
||||
"from_device": "SUMODVLSIU",
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: eventId.toString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
room,
|
||||
);
|
||||
|
||||
expect(verificationRequest.roomId.toString()).toStrictEqual(room.toString());
|
||||
|
||||
const [_sas, outgoingVerificationRequest] = await verificationRequest.startSas();
|
||||
|
||||
expect(outgoingVerificationRequest).toBeInstanceOf(RoomMessageRequest);
|
||||
expect(outgoingVerificationRequest.id).toBeDefined();
|
||||
expect(outgoingVerificationRequest.room_id).toStrictEqual(room.toString());
|
||||
expect(outgoingVerificationRequest.txn_id).toBeDefined();
|
||||
expect(outgoingVerificationRequest.event_type).toStrictEqual("m.key.verification.start");
|
||||
expect(outgoingVerificationRequest.body).toBeDefined();
|
||||
|
||||
const body = JSON.parse(outgoingVerificationRequest.body);
|
||||
expect(body).toMatchObject({
|
||||
"from_device": expect.any(String),
|
||||
"method": "m.sas.v1",
|
||||
"key_agreement_protocols": [expect.any(String)],
|
||||
"hashes": [expect.any(String)],
|
||||
"message_authentication_codes": [expect.any(String), expect.any(String)],
|
||||
"short_authentication_string": ["decimal", "emoji"],
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: eventId.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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,91 @@
|
||||
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 OlmMachine.initialize(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,7 @@
|
||||
src/
|
||||
tests/
|
||||
Cargo.toml
|
||||
build.rs
|
||||
*.node
|
||||
*.tgz
|
||||
tsconfig.json
|
||||
@@ -0,0 +1 @@
|
||||
/pkg
|
||||
@@ -0,0 +1,9 @@
|
||||
// prettier configuration: the same as the conventions used throughout Matrix.org
|
||||
// see: https://github.com/matrix-org/eslint-plugin-matrix-org/blob/main/.prettierrc.js
|
||||
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 4,
|
||||
quoteProps: "consistent",
|
||||
trailingComma: "all",
|
||||
};
|
||||
@@ -0,0 +1,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,41 @@
|
||||
[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 = { workspace = true }
|
||||
version = "0.6.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
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"] }
|
||||
matrix-sdk-sqlite = { version = "0.1.0", path = "../../crates/matrix-sdk-sqlite", features = ["crypto-store"] }
|
||||
ruma = { workspace = true, features = ["rand", "unstable-msc2677"] }
|
||||
napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] }
|
||||
napi-derive = "2.9.1"
|
||||
serde_json = { workspace = true }
|
||||
http = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true }
|
||||
vodozemac = { workspace = true, features = ["js"] }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.0.0"
|
||||
@@ -0,0 +1,206 @@
|
||||
# `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 list of events
|
||||
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,121 @@
|
||||
const { HttpsProxyAgent } = require("https-proxy-agent");
|
||||
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,
|
||||
});
|
||||
|
||||
const proxy = process.env.https_proxy ?? process.env.HTTPS_PROXY;
|
||||
if (proxy) {
|
||||
const proxyAgent = new HttpsProxyAgent(proxy);
|
||||
dl.updateOptions({
|
||||
httpsRequestOptions: { agent: proxyAgent },
|
||||
});
|
||||
}
|
||||
|
||||
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,37 @@
|
||||
{
|
||||
"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",
|
||||
"prettier": "^2.8.3",
|
||||
"typedoc": "^0.22.17",
|
||||
"yargs-parser": "~21.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "prettier --check .",
|
||||
"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": {
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"node-downloader-helper": "^2.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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 Some(media_encryption_info) = attachment.media_encryption_info.take() else {
|
||||
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,140 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
|
||||
/// for more info.
|
||||
#[napi]
|
||||
pub enum ShieldColor {
|
||||
Red,
|
||||
Grey,
|
||||
None,
|
||||
}
|
||||
|
||||
/// Take a look at [`matrix_sdk_common::deserialized_responses::ShieldState`]
|
||||
/// for more info.
|
||||
#[napi]
|
||||
pub struct ShieldState {
|
||||
pub color: ShieldColor,
|
||||
pub message: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl From<RustShieldState> for ShieldState {
|
||||
fn from(value: RustShieldState) -> Self {
|
||||
match value {
|
||||
RustShieldState::Red { message } => {
|
||||
ShieldState { color: ShieldColor::Red, message: Some(message) }
|
||||
}
|
||||
RustShieldState::Grey { message } => {
|
||||
ShieldState { color: ShieldColor::Grey, message: Some(message) }
|
||||
}
|
||||
RustShieldState::None => ShieldState { color: ShieldColor::None, message: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/// Generic error wrapping `napi::Error`.
|
||||
#[derive(Debug)]
|
||||
pub struct Error(napi::Error);
|
||||
|
||||
impl<E> From<E> for Error
|
||||
where
|
||||
E: std::error::Error,
|
||||
{
|
||||
fn from(error: E) -> Self {
|
||||
Self(napi::Error::from_reason(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for napi::Error {
|
||||
#[inline]
|
||||
fn from(value: Error) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to replace the `E` to `Error` to `napi::Error` conversion.
|
||||
#[inline]
|
||||
pub fn into_err<E>(error: E) -> napi::Error
|
||||
where
|
||||
E: std::error::Error,
|
||||
{
|
||||
Error::from(error).into()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Types related to events.
|
||||
|
||||
use napi::bindgen_prelude::ToNapiValue;
|
||||
use napi_derive::*;
|
||||
use ruma::events::room::history_visibility::HistoryVisibility as RumaHistoryVisibility;
|
||||
|
||||
/// Who can see a room's history.
|
||||
#[napi]
|
||||
#[derive(Debug)]
|
||||
pub enum HistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they joined the room onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when
|
||||
/// the member was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be
|
||||
/// shared by any participating homeserver with anyone, regardless
|
||||
/// of whether they have ever joined the room.
|
||||
WorldReadable,
|
||||
}
|
||||
|
||||
impl From<HistoryVisibility> for RumaHistoryVisibility {
|
||||
fn from(value: HistoryVisibility) -> Self {
|
||||
use HistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
use RumaHistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
//! Types for [Matrix](https://matrix.org/) identifiers for devices,
|
||||
//! events, keys, rooms, servers, users and URIs.
|
||||
|
||||
use napi::bindgen_prelude::ToNapiValue;
|
||||
use napi_derive::*;
|
||||
|
||||
use crate::into_err;
|
||||
|
||||
/// A Matrix [user ID].
|
||||
///
|
||||
/// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers
|
||||
#[napi]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserId {
|
||||
pub(crate) inner: ruma::OwnedUserId,
|
||||
}
|
||||
|
||||
impl From<ruma::OwnedUserId> for UserId {
|
||||
fn from(inner: ruma::OwnedUserId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl UserId {
|
||||
/// Parse/validate and create a new `UserId`.
|
||||
#[napi(constructor, strict)]
|
||||
pub fn new(id: String) -> napi::Result<Self> {
|
||||
Ok(Self::from(ruma::UserId::parse(id.as_str()).map_err(into_err)?))
|
||||
}
|
||||
|
||||
/// Returns the user's localpart.
|
||||
#[napi(getter)]
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the user ID.
|
||||
#[napi(getter)]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Whether this user ID is a historical one.
|
||||
///
|
||||
/// A historical user ID is one that doesn't conform to the latest
|
||||
/// specification of the user ID grammar but is still accepted
|
||||
/// because it was previously allowed.
|
||||
#[napi]
|
||||
pub fn is_historical(&self) -> bool {
|
||||
self.inner.is_historical()
|
||||
}
|
||||
|
||||
/// Return the user ID as a string.
|
||||
#[napi]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix device ID.
|
||||
///
|
||||
/// Device identifiers in Matrix are completely opaque character
|
||||
/// sequences. This type is provided simply for its semantic value.
|
||||
#[napi]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceId {
|
||||
pub(crate) inner: ruma::OwnedDeviceId,
|
||||
}
|
||||
|
||||
impl From<ruma::OwnedDeviceId> for DeviceId {
|
||||
fn from(inner: ruma::OwnedDeviceId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DeviceId {
|
||||
/// Create a new `DeviceId`.
|
||||
#[napi(constructor, strict)]
|
||||
pub fn new(id: String) -> Self {
|
||||
Self::from(Into::<ruma::OwnedDeviceId>::into(id))
|
||||
}
|
||||
|
||||
/// Return the device ID as a string.
|
||||
#[napi]
|
||||
#[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 ‘:’.
|
||||
#[napi]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceKeyId {
|
||||
pub(crate) inner: ruma::OwnedDeviceKeyId,
|
||||
}
|
||||
|
||||
impl From<ruma::OwnedDeviceKeyId> for DeviceKeyId {
|
||||
fn from(inner: ruma::OwnedDeviceKeyId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DeviceKeyId {
|
||||
/// Parse/validate and create a new `DeviceKeyId`.
|
||||
#[napi(constructor, strict)]
|
||||
pub fn new(id: String) -> napi::Result<Self> {
|
||||
Ok(Self::from(ruma::DeviceKeyId::parse(id.as_str()).map_err(into_err)?))
|
||||
}
|
||||
|
||||
/// Returns key algorithm of the device key ID.
|
||||
#[napi(getter)]
|
||||
pub fn algorithm(&self) -> DeviceKeyAlgorithm {
|
||||
self.inner.algorithm().into()
|
||||
}
|
||||
|
||||
/// Returns device ID of the device key ID.
|
||||
#[napi(getter)]
|
||||
pub fn device_id(&self) -> DeviceId {
|
||||
self.inner.device_id().to_owned().into()
|
||||
}
|
||||
|
||||
/// Return the device key ID as a string.
|
||||
#[napi]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The basic key algorithms in the specification.
|
||||
#[napi]
|
||||
pub struct DeviceKeyAlgorithm {
|
||||
inner: ruma::DeviceKeyAlgorithm,
|
||||
}
|
||||
|
||||
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithm {
|
||||
fn from(inner: ruma::DeviceKeyAlgorithm) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DeviceKeyAlgorithm {
|
||||
/// Read the device key algorithm's name. If the name is
|
||||
/// `Unknown`, one may be interested by the `to_string` method to
|
||||
/// read the original name.
|
||||
#[napi(getter)]
|
||||
pub fn name(&self) -> DeviceKeyAlgorithmName {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
/// Return the device key algorithm as a string.
|
||||
#[napi]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The basic key algorithm names in the specification.
|
||||
#[napi]
|
||||
pub enum DeviceKeyAlgorithmName {
|
||||
/// The Ed25519 signature algorithm.
|
||||
Ed25519,
|
||||
|
||||
/// The Curve25519 ECDH algorithm.
|
||||
Curve25519,
|
||||
|
||||
/// The Curve25519 ECDH algorithm, but the key also contains
|
||||
/// signatures.
|
||||
SignedCurve25519,
|
||||
|
||||
/// An unknown device key algorithm.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithmName {
|
||||
fn from(value: ruma::DeviceKeyAlgorithm) -> Self {
|
||||
use ruma::DeviceKeyAlgorithm::*;
|
||||
|
||||
match value {
|
||||
Ed25519 => Self::Ed25519,
|
||||
Curve25519 => Self::Curve25519,
|
||||
SignedCurve25519 => Self::SignedCurve25519,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix [room ID].
|
||||
///
|
||||
/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
|
||||
#[napi]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoomId {
|
||||
pub(crate) inner: ruma::OwnedRoomId,
|
||||
}
|
||||
|
||||
impl From<ruma::OwnedRoomId> for RoomId {
|
||||
fn from(inner: ruma::OwnedRoomId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl RoomId {
|
||||
/// Parse/validate and create a new `RoomId`.
|
||||
#[napi(constructor, strict)]
|
||||
pub fn new(id: String) -> napi::Result<Self> {
|
||||
Ok(Self::from(ruma::RoomId::parse(id).map_err(into_err)?))
|
||||
}
|
||||
|
||||
/// Returns the user's localpart.
|
||||
#[napi(getter)]
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the room ID.
|
||||
#[napi(getter)]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Return the room ID as a string.
|
||||
#[napi]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix-spec compliant [server name].
|
||||
///
|
||||
/// It consists of a host and an optional port (separated by a colon if
|
||||
/// present).
|
||||
///
|
||||
/// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name
|
||||
#[napi]
|
||||
#[derive(Debug)]
|
||||
pub struct ServerName {
|
||||
inner: ruma::OwnedServerName,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ServerName {
|
||||
/// Parse/validate and create a new `ServerName`.
|
||||
#[napi(constructor, strict)]
|
||||
pub fn new(name: String) -> napi::Result<Self> {
|
||||
Ok(Self { inner: ruma::ServerName::parse(name).map_err(into_err)? })
|
||||
}
|
||||
|
||||
/// Returns the host of the server name.
|
||||
///
|
||||
/// That is: Return the part of the server before `:<port>` or the
|
||||
/// full server name if there is no port.
|
||||
#[napi(getter)]
|
||||
pub fn host(&self) -> String {
|
||||
self.inner.host().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the port of the server name if any.
|
||||
#[napi(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.
|
||||
#[napi]
|
||||
pub fn is_ip_literal(&self) -> bool {
|
||||
self.inner.is_ip_literal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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)]
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
pub mod attachment;
|
||||
pub mod encryption;
|
||||
mod errors;
|
||||
pub mod events;
|
||||
pub mod identifiers;
|
||||
pub mod machine;
|
||||
pub mod olm;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
pub mod sync_events;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub mod tracing;
|
||||
pub mod types;
|
||||
pub mod vodozemac;
|
||||
|
||||
/// Object containing the versions of the Rust libraries we are using.
|
||||
#[napi]
|
||||
pub struct Versions {
|
||||
/// The version of the vodozemac crate.
|
||||
#[napi(getter)]
|
||||
pub vodozemac: String,
|
||||
|
||||
/// The version of the matrix-sdk-crypto crate.
|
||||
#[napi(getter)]
|
||||
pub matrix_sdk_crypto: String,
|
||||
}
|
||||
|
||||
/// Get the versions of the Rust libraries we are using.
|
||||
#[napi(js_name = "getVersions")]
|
||||
pub fn get_versions() -> Versions {
|
||||
Versions {
|
||||
vodozemac: matrix_sdk_crypto::vodozemac::VERSION.to_owned(),
|
||||
matrix_sdk_crypto: matrix_sdk_crypto::VERSION.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
use crate::errors::into_err;
|
||||
@@ -0,0 +1,497 @@
|
||||
//! The crypto specific Olm objects.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
mem::ManuallyDrop,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use napi::bindgen_prelude::{within_runtime_if_available, Either7, ToNapiValue};
|
||||
use napi_derive::*;
|
||||
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
|
||||
use serde_json::{value::RawValue, Value as JsonValue};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
encryption, identifiers, into_err, olm, requests, responses, responses::response_from_string,
|
||||
sync_events, types, vodozemac,
|
||||
};
|
||||
|
||||
/// The value used by the `OlmMachine` JS class.
|
||||
///
|
||||
/// It has 2 states: `Opened` and `Closed`. Why maintaining the state here?
|
||||
/// Because NodeJS has no way to drop an object explicitly, and we want to be
|
||||
/// able to “close” the `OlmMachine` to free all associated data. More over,
|
||||
/// `napi-rs` doesn't allow a function to take the ownership of the type itself
|
||||
/// (`fn close(self) { … }`). So we manage the state ourselves.
|
||||
///
|
||||
/// Using the `OlmMachine` when its state is `Closed` will panic.
|
||||
enum OlmMachineInner {
|
||||
Opened(ManuallyDrop<matrix_sdk_crypto::OlmMachine>),
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl Drop for OlmMachineInner {
|
||||
fn drop(&mut self) {
|
||||
if let Self::Opened(machine) = self {
|
||||
// SAFETY: `self` won't be used anymore after this `take`, so it's safe to do it
|
||||
// here.
|
||||
let machine = unsafe { ManuallyDrop::take(machine) };
|
||||
within_runtime_if_available(move || drop(machine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for OlmMachineInner {
|
||||
type Target = matrix_sdk_crypto::OlmMachine;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Opened(machine) => machine,
|
||||
Self::Closed => panic!("The `OlmMachine` has been closed, cannot use it anymore"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the type of store an `OlmMachine` can use.
|
||||
#[derive(Default)]
|
||||
#[napi]
|
||||
pub enum StoreType {
|
||||
/// Use `matrix-sdk-sled`.
|
||||
#[default]
|
||||
Sled,
|
||||
|
||||
/// Use `matrix-sdk-sqlite`.
|
||||
Sqlite,
|
||||
}
|
||||
|
||||
/// State machine implementation of the Olm/Megolm encryption protocol
|
||||
/// used for Matrix end to end encryption.
|
||||
// #[napi(custom_finalize)]
|
||||
#[napi]
|
||||
pub struct OlmMachine {
|
||||
inner: OlmMachineInner,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OlmMachine {
|
||||
// JavaScript doesn't support asynchronous constructor. So let's
|
||||
// use a factory pattern, where the constructor cannot be used (it
|
||||
// returns an error), and a new method is provided to construct
|
||||
// the object. napi provides `#[napi(factory)]` to address those
|
||||
// needs automatically. Unfortunately, it doesn't support
|
||||
// asynchronous factory methods.
|
||||
//
|
||||
// So let's do this manually. The `initialize` async method _is_
|
||||
// the factory function. We also manually implement the
|
||||
// constructor to raise an error when called.
|
||||
|
||||
/// Create a new `OlmMachine` asynchronously.
|
||||
///
|
||||
/// The persistence of the encryption keys and all the inner
|
||||
/// objects are controlled by the `store_path` argument.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id`, the unique ID of the user that owns this machine.
|
||||
/// * `device_id`, the unique id of the device that owns this machine.
|
||||
/// * `store_path`, the path to a directory where the state of the machine
|
||||
/// should be persisted; if not set, the created machine will keep the
|
||||
/// encryption keys only in memory, and once the object is dropped, the
|
||||
/// keys will be lost.
|
||||
/// * `store_passphrase`, the passphrase that should be used to encrypt the
|
||||
/// data at rest in the store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted. This argument is
|
||||
/// ignored if `store_path` is not set.
|
||||
#[napi(strict)]
|
||||
pub async fn initialize(
|
||||
user_id: &identifiers::UserId,
|
||||
device_id: &identifiers::DeviceId,
|
||||
store_path: Option<String>,
|
||||
mut store_passphrase: Option<String>,
|
||||
store_type: Option<StoreType>,
|
||||
) -> napi::Result<OlmMachine> {
|
||||
let user_id = user_id.clone().inner;
|
||||
let device_id = device_id.clone().inner;
|
||||
|
||||
let user_id = user_id.as_ref();
|
||||
let device_id = device_id.as_ref();
|
||||
|
||||
Ok(OlmMachine {
|
||||
inner: OlmMachineInner::Opened(ManuallyDrop::new(match store_path {
|
||||
Some(store_path) => {
|
||||
let machine = match store_type.unwrap_or_default() {
|
||||
StoreType::Sled => {
|
||||
matrix_sdk_crypto::OlmMachine::with_store(
|
||||
user_id,
|
||||
device_id,
|
||||
matrix_sdk_sled::SledCryptoStore::open(
|
||||
store_path,
|
||||
store_passphrase.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
.map_err(into_err)?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
StoreType::Sqlite => {
|
||||
matrix_sdk_crypto::OlmMachine::with_store(
|
||||
user_id,
|
||||
device_id,
|
||||
matrix_sdk_sqlite::SqliteCryptoStore::open(
|
||||
store_path,
|
||||
store_passphrase.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
.map_err(into_err)?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
store_passphrase.zeroize();
|
||||
|
||||
machine.map_err(into_err)?
|
||||
}
|
||||
|
||||
None => matrix_sdk_crypto::OlmMachine::new(user_id, device_id).await,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/// It's not possible to construct an `OlmMachine` with its
|
||||
/// constructor because building an `OlmMachine` is
|
||||
/// asynchronous. Please use the `finalize` method.
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> napi::Result<Self> {
|
||||
Err(napi::Error::from_reason(
|
||||
"To build an `OldMachine`, please use the `initialize` method",
|
||||
))
|
||||
}
|
||||
|
||||
/// The unique user ID that owns this `OlmMachine` instance.
|
||||
#[napi(getter)]
|
||||
pub fn user_id(&self) -> identifiers::UserId {
|
||||
identifiers::UserId::from(self.inner.user_id().to_owned())
|
||||
}
|
||||
|
||||
/// The unique device ID that identifies this `OlmMachine`.
|
||||
#[napi(getter)]
|
||||
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.
|
||||
#[napi(getter)]
|
||||
pub fn identity_keys(&self) -> vodozemac::IdentityKeys {
|
||||
self.inner.identity_keys().into()
|
||||
}
|
||||
|
||||
/// Handle a to-device and one-time key counts from a sync response.
|
||||
///
|
||||
/// This will decrypt and handle to-device events returning the
|
||||
/// decrypted versions of them, as a JSON-encoded string.
|
||||
///
|
||||
/// To decrypt an event from the room timeline, please use
|
||||
/// `decrypt_room_event`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `to_device_events`, the to-device events of the current sync response.
|
||||
/// * `changed_devices`, the list of devices that changed in this sync
|
||||
/// response.
|
||||
/// * `one_time_keys_count`, the current one-time keys counts that the sync
|
||||
/// response returned.
|
||||
#[napi(strict)]
|
||||
pub async fn receive_sync_changes(
|
||||
&self,
|
||||
to_device_events: String,
|
||||
changed_devices: &sync_events::DeviceLists,
|
||||
one_time_key_counts: HashMap<String, u32>,
|
||||
unused_fallback_keys: Vec<String>,
|
||||
) -> napi::Result<String> {
|
||||
let to_device_events = serde_json::from_str(to_device_events.as_ref()).map_err(into_err)?;
|
||||
let changed_devices = changed_devices.inner.clone();
|
||||
let one_time_key_counts = one_time_key_counts
|
||||
.iter()
|
||||
.map(|(key, value)| (DeviceKeyAlgorithm::from(key.as_str()), UInt::from(*value)))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let unused_fallback_keys = Some(
|
||||
unused_fallback_keys
|
||||
.into_iter()
|
||||
.map(|key| DeviceKeyAlgorithm::from(key.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
serde_json::to_string(
|
||||
&self
|
||||
.inner
|
||||
.receive_sync_changes(
|
||||
to_device_events,
|
||||
&changed_devices,
|
||||
&one_time_key_counts,
|
||||
unused_fallback_keys.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(into_err)?,
|
||||
)
|
||||
.map_err(into_err)
|
||||
}
|
||||
|
||||
/// Get the outgoing requests that need to be sent out.
|
||||
///
|
||||
/// This returns a list of `KeysUploadRequest`, or
|
||||
/// `KeysQueryRequest`, or `KeysClaimRequest`, or
|
||||
/// `ToDeviceRequest`, or `SignatureUploadRequest`, or
|
||||
/// `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`.
|
||||
#[napi]
|
||||
pub async fn outgoing_requests(
|
||||
&self,
|
||||
) -> napi::Result<
|
||||
Vec<
|
||||
// We could be tempted to use `requests::OutgoingRequests` as its
|
||||
// a type alias for this giant `Either7`. But `napi` won't unfold
|
||||
// it properly into a valid TypeScript definition, so… let's
|
||||
// copy-paste :-(.
|
||||
Either7<
|
||||
requests::KeysUploadRequest,
|
||||
requests::KeysQueryRequest,
|
||||
requests::KeysClaimRequest,
|
||||
requests::ToDeviceRequest,
|
||||
requests::SignatureUploadRequest,
|
||||
requests::RoomMessageRequest,
|
||||
requests::KeysBackupRequest,
|
||||
>,
|
||||
>,
|
||||
> {
|
||||
self.inner
|
||||
.outgoing_requests()
|
||||
.await
|
||||
.map_err(into_err)?
|
||||
.into_iter()
|
||||
.map(requests::OutgoingRequest)
|
||||
.map(TryFrom::try_from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Mark the request with the given request ID as sent.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request_id`, the unique ID of the request that was sent out. This is
|
||||
/// needed to couple the response with the now sent out request.
|
||||
/// * `request_type`, the request type associated to the request ID.
|
||||
/// * `response`, the response that was received from the server after the
|
||||
/// outgoing request was sent out.
|
||||
#[napi(strict)]
|
||||
pub async fn mark_request_as_sent(
|
||||
&self,
|
||||
request_id: String,
|
||||
request_type: requests::RequestType,
|
||||
response: String,
|
||||
) -> napi::Result<bool> {
|
||||
let transaction_id = OwnedTransactionId::from(request_id);
|
||||
let response = response_from_string(response.as_str()).map_err(into_err)?;
|
||||
let incoming_response = responses::OwnedResponse::try_from((request_type, response))?;
|
||||
|
||||
self.inner
|
||||
.mark_request_as_sent(&transaction_id, &incoming_response)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.map_err(into_err)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Sessions need to be established between devices so group
|
||||
/// sessions for a room can be shared with them.
|
||||
///
|
||||
/// This should be called every time a group session needs to be
|
||||
/// shared as well as between sync calls. After a sync some
|
||||
/// devices may request room keys without us having a valid Olm
|
||||
/// session with them, making it impossible to server the room key
|
||||
/// request, thus it’s necessary to check for missing sessions
|
||||
/// between sync as well.
|
||||
///
|
||||
/// Note: Care should be taken that only one such request at a
|
||||
/// time is in flight, e.g. using a lock.
|
||||
///
|
||||
/// The response of a successful key claiming requests needs to be
|
||||
/// passed to the `OlmMachine` with the `mark_request_as_sent`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users`, the list of users that we should check if we lack a session
|
||||
/// with one of their devices. This can be an empty array or `null` when
|
||||
/// calling this method between sync requests.
|
||||
#[napi(strict)]
|
||||
pub async fn get_missing_sessions(
|
||||
&self,
|
||||
users: Option<Vec<&identifiers::UserId>>,
|
||||
) -> napi::Result<Option<requests::KeysClaimRequest>> {
|
||||
let users = users
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|user| user.inner.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match self
|
||||
.inner
|
||||
.get_missing_sessions(users.iter().map(AsRef::as_ref))
|
||||
.await
|
||||
.map_err(into_err)?
|
||||
{
|
||||
Some((transaction_id, keys_claim_request)) => Ok(Some(
|
||||
requests::KeysClaimRequest::try_from((
|
||||
transaction_id.to_string(),
|
||||
&keys_claim_request,
|
||||
))
|
||||
.map_err(into_err)?,
|
||||
)),
|
||||
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the tracked users.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users`, an array over user IDs that should be marked for tracking.
|
||||
#[napi(strict)]
|
||||
pub async fn update_tracked_users(&self, users: Vec<&identifiers::UserId>) -> napi::Result<()> {
|
||||
let users = users.into_iter().map(|user| user.inner.clone()).collect::<Vec<_>>();
|
||||
|
||||
self.inner.update_tracked_users(users.iter().map(AsRef::as_ref)).await.map_err(into_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get to-device requests to share a room key with users in a room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id`, the room ID of the room where the room key will be used.
|
||||
/// * `users`, the list of users that should receive the room key.
|
||||
/// * `encryption_settings`, the encryption settings.
|
||||
#[napi(strict)]
|
||||
pub async fn share_room_key(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
users: Vec<&identifiers::UserId>,
|
||||
encryption_settings: &encryption::EncryptionSettings,
|
||||
) -> napi::Result<String> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let users = users.into_iter().map(|user| user.inner.clone()).collect::<Vec<_>>();
|
||||
let encryption_settings =
|
||||
matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings);
|
||||
|
||||
serde_json::to_string(
|
||||
&self
|
||||
.inner
|
||||
.share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings)
|
||||
.await
|
||||
.map_err(into_err)?,
|
||||
)
|
||||
.map_err(into_err)
|
||||
}
|
||||
|
||||
/// Encrypt a JSON-encoded content for the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id`, the ID of the room for which the message should be
|
||||
/// encrypted.
|
||||
/// * `event_type`, the plaintext type of the event.
|
||||
/// * `content`, the JSON-encoded content of the message that should be
|
||||
/// encrypted.
|
||||
#[napi(strict)]
|
||||
pub async fn encrypt_room_event(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
event_type: String,
|
||||
content: String,
|
||||
) -> napi::Result<String> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let content: JsonValue = serde_json::from_str(content.as_str()).map_err(into_err)?;
|
||||
|
||||
serde_json::to_string(
|
||||
&self
|
||||
.inner
|
||||
.encrypt_room_event_raw(&room_id, content, event_type.as_ref())
|
||||
.await
|
||||
.map_err(into_err)?,
|
||||
)
|
||||
.map_err(into_err)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[napi(strict)]
|
||||
pub async fn decrypt_room_event(
|
||||
&self,
|
||||
event: String,
|
||||
room_id: &identifiers::RoomId,
|
||||
) -> napi::Result<responses::DecryptedRoomEvent> {
|
||||
let event = Raw::from_json(RawValue::from_string(event).map_err(into_err)?);
|
||||
let room_id = room_id.inner.clone();
|
||||
|
||||
let room_event = self.inner.decrypt_room_event(&event, &room_id).await.map_err(into_err)?;
|
||||
|
||||
Ok(room_event.into())
|
||||
}
|
||||
|
||||
/// Get the status of the private cross signing keys.
|
||||
///
|
||||
/// This can be used to check which private cross signing keys we
|
||||
/// have stored locally.
|
||||
#[napi]
|
||||
pub async fn cross_signing_status(&self) -> olm::CrossSigningStatus {
|
||||
self.inner.cross_signing_status().await.into()
|
||||
}
|
||||
|
||||
/// Sign the given message using our device key and if available
|
||||
/// cross-signing master key.
|
||||
#[napi(strict)]
|
||||
pub async fn sign(&self, message: String) -> types::Signatures {
|
||||
self.inner.sign(message.as_str()).await.into()
|
||||
}
|
||||
|
||||
/// Shut down the `OlmMachine`.
|
||||
///
|
||||
/// The `OlmMachine` cannot be used after this method has been called,
|
||||
/// otherwise it will panic.
|
||||
///
|
||||
/// All associated resources will be closed too, like the crypto storage
|
||||
/// connections.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller is responsible to **not** use any objects that came from this
|
||||
/// `OlmMachine` after this `close` method has been called.
|
||||
#[napi(strict)]
|
||||
pub fn close(&mut self) {
|
||||
self.inner = OlmMachineInner::Closed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Olm types.
|
||||
|
||||
use napi_derive::*;
|
||||
|
||||
/// Struct representing the state of our private cross signing keys,
|
||||
/// it shows which private cross signing keys we have locally stored.
|
||||
#[napi]
|
||||
#[derive(Debug)]
|
||||
pub struct CrossSigningStatus {
|
||||
inner: matrix_sdk_crypto::olm::CrossSigningStatus,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::olm::CrossSigningStatus> for CrossSigningStatus {
|
||||
fn from(inner: matrix_sdk_crypto::olm::CrossSigningStatus) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl CrossSigningStatus {
|
||||
/// Do we have the master key.
|
||||
#[napi(getter)]
|
||||
pub fn has_master(&self) -> bool {
|
||||
self.inner.has_master
|
||||
}
|
||||
|
||||
/// Do we have the self signing key, this one is necessary to sign
|
||||
/// our own devices.
|
||||
#[napi(getter)]
|
||||
pub fn has_self_signing(&self) -> bool {
|
||||
self.inner.has_self_signing
|
||||
}
|
||||
|
||||
/// Do we have the user signing key, this one is necessary to sign
|
||||
/// other users.
|
||||
#[napi(getter)]
|
||||
pub fn has_user_signing(&self) -> bool {
|
||||
self.inner.has_user_signing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
//! Types to handle requests.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_crypto::requests::{
|
||||
KeysBackupRequest as RumaKeysBackupRequest, KeysQueryRequest as RumaKeysQueryRequest,
|
||||
RoomMessageRequest as RumaRoomMessageRequest, ToDeviceRequest as RumaToDeviceRequest,
|
||||
};
|
||||
use napi::bindgen_prelude::{Either7, ToNapiValue};
|
||||
use napi_derive::*;
|
||||
use ruma::{
|
||||
api::client::keys::{
|
||||
claim_keys::v3::Request as RumaKeysClaimRequest,
|
||||
upload_keys::v3::Request as RumaKeysUploadRequest,
|
||||
upload_signatures::v3::Request as RumaSignatureUploadRequest,
|
||||
},
|
||||
events::EventContent,
|
||||
};
|
||||
|
||||
use crate::into_err;
|
||||
|
||||
/// 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
|
||||
#[napi]
|
||||
pub struct KeysUploadRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `device_keys`,
|
||||
/// `one_time_keys`, `fallback_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[napi(readonly)]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KeysUploadRequest {
|
||||
/// Get its request type.
|
||||
#[napi(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
|
||||
#[napi]
|
||||
pub struct KeysQueryRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A JSON-encoded object of form:
|
||||
///
|
||||
/// ```json
|
||||
/// {"timeout": …, "one_time_keys": …}
|
||||
/// ```
|
||||
#[napi(readonly)]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KeysQueryRequest {
|
||||
/// Get its request type.
|
||||
#[napi(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
|
||||
#[napi]
|
||||
pub struct KeysClaimRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A JSON-encoded object of form:
|
||||
///
|
||||
/// ```json
|
||||
/// {"event_type": …, "txn_id": …, "messages": …}
|
||||
/// ```
|
||||
#[napi(readonly)]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KeysClaimRequest {
|
||||
/// Get its request type.
|
||||
#[napi(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
|
||||
#[napi]
|
||||
pub struct ToDeviceRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A string representing the type of event being sent to each devices.
|
||||
#[napi(readonly)]
|
||||
pub event_type: String,
|
||||
|
||||
/// A string representing a request identifier unique to the access token
|
||||
/// used to send the request.
|
||||
#[napi(readonly)]
|
||||
pub txn_id: String,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `messages`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[napi(readonly)]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ToDeviceRequest {
|
||||
/// Get its request type.
|
||||
#[napi(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
|
||||
#[napi]
|
||||
pub struct SignatureUploadRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `signed_keys`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[napi(readonly)]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl SignatureUploadRequest {
|
||||
/// Get its request type.
|
||||
#[napi(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
|
||||
#[napi]
|
||||
pub struct RoomMessageRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A string representing the room to send the event to.
|
||||
#[napi(readonly)]
|
||||
pub room_id: String,
|
||||
|
||||
/// A string representing the transaction ID for this event.
|
||||
///
|
||||
/// Clients should generate an ID unique across requests with the same
|
||||
/// access token; it will be used by the server to ensure idempotency of
|
||||
/// requests.
|
||||
#[napi(readonly)]
|
||||
pub txn_id: String,
|
||||
|
||||
/// A string representing the type of event to be sent.
|
||||
#[napi(readonly)]
|
||||
pub event_type: String,
|
||||
|
||||
/// A JSON-encoded string containing the message's content.
|
||||
#[napi(readonly, js_name = "body")]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl RoomMessageRequest {
|
||||
/// Get its request type.
|
||||
#[napi(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
|
||||
#[napi]
|
||||
pub struct KeysBackupRequest {
|
||||
/// The request ID.
|
||||
#[napi(readonly)]
|
||||
pub id: String,
|
||||
|
||||
/// A JSON-encoded string containing the rest of the payload: `rooms`.
|
||||
///
|
||||
/// It represents the body of the HTTP request.
|
||||
#[napi(readonly)]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KeysBackupRequest {
|
||||
/// Get its request type.
|
||||
#[napi(getter, js_name = "type")]
|
||||
pub fn request_type(&self) -> RequestType {
|
||||
RequestType::KeysBackup
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! request {
|
||||
(
|
||||
$destination_request:ident from $source_request:ident
|
||||
$( extracts $( $field_name:ident : $field_type:tt ),+ $(,)? )?
|
||||
$( $( and )? groups $( $grouped_field_name:ident $( { $grouped_field_transformation:expr } )? ),+ $(,)? )?
|
||||
) => {
|
||||
impl TryFrom<(String, &$source_request)> for $destination_request {
|
||||
type Error = napi::Error;
|
||||
|
||||
fn try_from(
|
||||
(request_id, request): (String, &$source_request),
|
||||
) -> Result<Self, Self::Error> {
|
||||
request!(
|
||||
@__try_from $destination_request from $source_request
|
||||
(request_id = request_id.into(), request = request)
|
||||
$( extracts [ $( $field_name : $field_type, )+ ] )?
|
||||
$( groups [ $( $grouped_field_name $( { $grouped_field_transformation } )? , )+ ] )?
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@__try_from $destination_request:ident from $source_request:ident
|
||||
(request_id = $request_id:expr, request = $request:expr)
|
||||
$( extracts [ $( $field_name:ident : $field_type:tt ),* $(,)? ] )?
|
||||
$( groups [ $( $grouped_field_name:ident $( { $grouped_field_transformation:expr } )? ),* $(,)? ] )?
|
||||
) => {
|
||||
{
|
||||
Ok($destination_request {
|
||||
id: $request_id,
|
||||
$(
|
||||
$(
|
||||
$field_name: request!(@__field $field_name : $field_type ; request = $request),
|
||||
)*
|
||||
)?
|
||||
$(
|
||||
body: {
|
||||
let mut map = serde_json::Map::new();
|
||||
$(
|
||||
|
||||
let field = &$request.$grouped_field_name;
|
||||
$(
|
||||
let field = {
|
||||
let $grouped_field_name = field;
|
||||
|
||||
$grouped_field_transformation
|
||||
};
|
||||
)?
|
||||
map.insert(stringify!($grouped_field_name).to_owned(), serde_json::to_value(field).map_err(into_err)?);
|
||||
)*
|
||||
let object = serde_json::Value::Object(map);
|
||||
|
||||
serde_json::to_string(&object).map_err(into_err)?.into()
|
||||
}
|
||||
)?
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
( @__field $field_name:ident : $field_type:ident ; request = $request:expr ) => {
|
||||
request!(@__field_type as $field_type ; request = $request, field_name = $field_name)
|
||||
};
|
||||
|
||||
( @__field_type as string ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
$request.$field_name.to_string().into()
|
||||
};
|
||||
|
||||
( @__field_type as json ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
serde_json::to_string(&$request.$field_name).map_err(into_err)?.into()
|
||||
};
|
||||
|
||||
( @__field_type as event_type ; request = $request:expr, field_name = $field_name:ident ) => {
|
||||
$request.content.event_type().to_string().into()
|
||||
};
|
||||
}
|
||||
|
||||
request!(KeysUploadRequest from RumaKeysUploadRequest groups device_keys, one_time_keys, fallback_keys);
|
||||
request!(KeysQueryRequest from RumaKeysQueryRequest groups timeout { timeout.as_ref().map(Duration::as_millis).map(u64::try_from).transpose().map_err(into_err)? }, device_keys, token);
|
||||
request!(KeysClaimRequest from RumaKeysClaimRequest groups timeout { timeout.as_ref().map(Duration::as_millis).map(u64::try_from).transpose().map_err(into_err)? }, one_time_keys);
|
||||
request!(ToDeviceRequest from RumaToDeviceRequest extracts event_type: string, txn_id: string and groups messages);
|
||||
request!(SignatureUploadRequest from RumaSignatureUploadRequest groups signed_keys);
|
||||
request!(RoomMessageRequest from RumaRoomMessageRequest extracts room_id: string, txn_id: string, event_type: event_type, content: json);
|
||||
request!(KeysBackupRequest from RumaKeysBackupRequest groups rooms);
|
||||
|
||||
pub type OutgoingRequests = Either7<
|
||||
KeysUploadRequest,
|
||||
KeysQueryRequest,
|
||||
KeysClaimRequest,
|
||||
ToDeviceRequest,
|
||||
SignatureUploadRequest,
|
||||
RoomMessageRequest,
|
||||
KeysBackupRequest,
|
||||
>;
|
||||
|
||||
pub(crate) struct OutgoingRequest(pub(crate) matrix_sdk_crypto::OutgoingRequest);
|
||||
|
||||
impl TryFrom<OutgoingRequest> for OutgoingRequests {
|
||||
type Error = napi::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() {
|
||||
matrix_sdk_crypto::OutgoingRequests::KeysUpload(request) => {
|
||||
Either7::A(KeysUploadRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
matrix_sdk_crypto::OutgoingRequests::KeysQuery(request) => {
|
||||
Either7::B(KeysQueryRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
matrix_sdk_crypto::OutgoingRequests::KeysClaim(request) => {
|
||||
Either7::C(KeysClaimRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
matrix_sdk_crypto::OutgoingRequests::ToDeviceRequest(request) => {
|
||||
Either7::D(ToDeviceRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
matrix_sdk_crypto::OutgoingRequests::SignatureUpload(request) => {
|
||||
Either7::E(SignatureUploadRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
matrix_sdk_crypto::OutgoingRequests::RoomMessage(request) => {
|
||||
Either7::F(RoomMessageRequest::try_from((request_id, request))?)
|
||||
}
|
||||
|
||||
matrix_sdk_crypto::OutgoingRequests::KeysBackup(request) => {
|
||||
Either7::G(KeysBackupRequest::try_from((request_id, request))?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the type of a request.
|
||||
#[napi]
|
||||
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,202 @@
|
||||
use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo};
|
||||
use matrix_sdk_crypto::IncomingResponse;
|
||||
use napi_derive::*;
|
||||
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 crate::{encryption, identifiers, into_err, 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 = napi::Error;
|
||||
|
||||
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(into_err)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
#[napi]
|
||||
pub struct DecryptedRoomEvent {
|
||||
/// The JSON-encoded decrypted event.
|
||||
#[napi(readonly)]
|
||||
pub event: String,
|
||||
|
||||
encryption_info: Option<EncryptionInfo>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DecryptedRoomEvent {
|
||||
/// The user ID of the event sender, note this is untrusted data
|
||||
/// unless the `verification_state` is as well trusted.
|
||||
#[napi(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.
|
||||
#[napi(getter)]
|
||||
pub fn sender_device(&self) -> Option<identifiers::DeviceId> {
|
||||
Some(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into())
|
||||
}
|
||||
|
||||
/// The Curve25519 key of the device that created the megolm
|
||||
/// decryption key originally.
|
||||
#[napi(getter)]
|
||||
pub fn sender_curve25519_key(&self) -> Option<String> {
|
||||
Some(match &self.encryption_info.as_ref()?.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => curve25519_key.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The signing Ed25519 key that have created the megolm key that
|
||||
/// was used to decrypt this session.
|
||||
#[napi(getter)]
|
||||
pub fn sender_claimed_ed25519_key(&self) -> Option<String> {
|
||||
match &self.encryption_info.as_ref()?.algorithm_info {
|
||||
AlgorithmInfo::MegolmV1AesSha2 { sender_claimed_keys, .. } => {
|
||||
sender_claimed_keys.get(&ruma::DeviceKeyAlgorithm::Ed25519).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chain of Curve25519 keys through which this session was
|
||||
/// forwarded, via `m.forwarded_room_key` events.
|
||||
#[napi(getter)]
|
||||
pub fn forwarding_curve25519_key_chain(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[napi]
|
||||
pub fn shield_state(&self, strict: bool) -> Option<encryption::ShieldState> {
|
||||
let state = &self.encryption_info.as_ref()?.verification_state;
|
||||
if strict {
|
||||
Some(state.to_shield_state_strict().into())
|
||||
} else {
|
||||
Some(state.to_shield_state_lax().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_common::deserialized_responses::TimelineEvent> for DecryptedRoomEvent {
|
||||
fn from(value: matrix_sdk_common::deserialized_responses::TimelineEvent) -> Self {
|
||||
Self { event: value.event.json().get().to_owned(), encryption_info: value.encryption_info }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user