diff --git a/.github/workflows/appservice.yml b/.github/workflows/appservice.yml index 42fd7dce1..1f16b4725 100644 --- a/.github/workflows/appservice.yml +++ b/.github/workflows/appservice.yml @@ -1,4 +1,4 @@ -name: Appservice +name: AppService on: push: @@ -17,13 +17,18 @@ env: jobs: test-appservice: if: github.event_name == 'push' || !github.event.pull_request.draft - name: ${{ matrix.os }} / appservice / stable + name: ${{ matrix.os-name }} [m]-appservice - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - os: [ubuntu, macOS] + include: + - os: ubuntu-latest + os-name: 🐧 + + - os: macos-latest + os-name: 🍏 steps: - name: Checkout @@ -39,6 +44,9 @@ jobs: - name: Load cache uses: Swatinem/rust-cache@v1 + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Run checks uses: actions-rs/cargo@v1 with: diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml new file mode 100644 index 000000000..a3cb09f8e --- /dev/null +++ b/.github/workflows/bindings_ci.yml @@ -0,0 +1,172 @@ +name: Bindings tests + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + types: + - opened + - reopened + - synchronize + - ready_for_review + +env: + CARGO_TERM_COLOR: always + MATRIX_SDK_CRYPTO_NODEJS_PATH: bindings/matrix-sdk-crypto-nodejs + MATRIX_SDK_CRYPTO_JS_PATH: bindings/matrix-sdk-crypto-js + +jobs: + test-matrix-sdk-crypto-nodejs: + name: ${{ matrix.os-name }} [m]-crypto-nodejs, v${{ matrix.node-version }} + if: github.event_name == 'push' || !github.event.pull_request.draft + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, macos-latest] + node-version: [14.0, 16.0, 18.0] + include: + - os: ubuntu-latest + os-name: 🐧 + + - os: macos-latest + os-name: 🍏 + + - node-version: 18.0 + build-doc: true + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Load cache + uses: Swatinem/rust-cache@v1 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install NPM dependencies + working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }} + run: npm install + + - name: Build the Node.js binding + working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }} + run: npm run release-build + + - name: Test the Node.js binding + working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }} + run: npm run test + + # Building in dev-mode and copy lib in failure case + - name: Build the Node.js binding in non-release + if: failure() + working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }} + run: | + cp *.node release-mode-lib.node + npm run build + + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: Failure Files + path: | + bindings/matrix-sdk-crypto-nodejs/*.node + /var/crash/*.crash + + - if: ${{ matrix.build-doc }} + name: Build the documentation + working-directory: ${{ env.MATRIX_SDK_CRYPTO_NODEJS_PATH }} + run: npm run doc + + test-matrix-sdk-crypto-js: + name: πŸ•Έ [m]-crypto-js + if: github.event_name == 'push' || !github.event.pull_request.draft + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + profile: minimal + override: true + + - name: Load cache + uses: Swatinem/rust-cache@v1 + + - name: Install Node.js + uses: actions/setup-node@v3 + + - name: Install NPM dependencies + working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }} + run: npm install + + - name: Build the WebAssembly + JavaScript binding + working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }} + run: npm run build + + - name: Test the JavaScript binding + working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }} + run: npm run test + + - name: Build the documentation + working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }} + run: npm run doc + + test-apple: + name: matrix-rust-components-swift + runs-on: macos-12 + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + override: true + + - name: Install targets + run: | + rustup target add aarch64-apple-ios-sim --toolchain nightly + rustup target add x86_64-apple-ios --toolchain nightly + + - name: Load cache + uses: Swatinem/rust-cache@v1 + + - name: Install Uniffi + uses: actions-rs/cargo@v1 + with: + command: install + # keep in sync with uniffi dependency in Cargo.toml's + args: uniffi_bindgen --version ^0.18 + + - name: Generate .xcframework + run: sh bindings/apple/debug_build_xcframework.sh ci + + - name: Run XCTests + run: | + xcodebuild test \ + -project bindings/apple/MatrixRustSDK.xcodeproj \ + -scheme MatrixRustSDK \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.4' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d00586b2..fb813b1a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: Rust tests on: workflow_dispatch: @@ -16,8 +16,8 @@ env: CARGO_TERM_COLOR: always jobs: - test-features: - name: linux / features-${{ matrix.name }} + test-matrix-sdk-features: + name: 🐧 [m], ${{ matrix.name }} if: github.event_name == 'push' || !github.event.pull_request.draft runs-on: ubuntu-latest @@ -48,14 +48,17 @@ jobs: - name: Load cache uses: Swatinem/rust-cache@v1 + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Test uses: actions-rs/cargo@v1 with: command: run args: -p xtask -- ci test-features ${{ matrix.name }} - test-crypto-features: - name: linux / crypto-crate features + test-matrix-sdk-crypto: + name: 🐧 [m]-crypto runs-on: ubuntu-latest if: github.event_name == 'push' || !github.event.pull_request.draft @@ -73,33 +76,35 @@ jobs: - name: Load cache uses: Swatinem/rust-cache@v1 - - name: Clippy + - name: Install nextest + uses: taiki-e/install-action@nextest + + - name: Test uses: actions-rs/cargo@v1 with: command: run args: -p xtask -- ci test-crypto - test: + test-all-crates: name: ${{ matrix.name }} if: github.event_name == 'push' || !github.event.pull_request.draft - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - name: - - linux / stable - - linux / beta - - macOS / stable - include: - - name: linux / stable + - name: 🐧 all crates, πŸ¦€ stable + rust: stable + os: ubuntu-latest - - name: linux / beta + - name: 🐧 all crates, πŸ¦€ beta rust: beta + os: ubuntu-latest - - name: macOS / stable - os: macOS-latest + - name: 🍏 all crates, πŸ¦€ stable + rust: stable + os: macos-latest steps: - name: Checkout @@ -108,30 +113,64 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust || 'stable' }} + toolchain: ${{ matrix.rust }} profile: minimal override: true - name: Load cache uses: Swatinem/rust-cache@v1 + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Test uses: actions-rs/cargo@v1 with: - command: test + command: nextest + args: run --workspace - test-nodejs: - name: linux / node.js (${{ matrix.node-version }}) + - name: Test documentation + uses: actions-rs/cargo@v1 + with: + command: test + args: --doc + + test-wasm: + name: πŸ•ΈοΈ ${{ matrix.name }} if: github.event_name == 'push' || !github.event.pull_request.draft runs-on: ubuntu-latest + strategy: fail-fast: true matrix: - node-version: [14.0, 16.0, 18.0] include: - - node-version: 18.0 - build-doc: true + - name: '[m]-qrcode' + cmd: matrix-sdk-qrcode + + - name: '[m]-base' + cmd: matrix-sdk-base + + - name: '[m]-common' + cmd: matrix-sdk-common + + - name: '[m]-indexeddb, no crypto' + cmd: indexeddb-no-crypto + + - name: '[m]-indexeddb, with crypto' + cmd: indexeddb-with-crypto + + - name: '[m], no-default, wasm-flags' + cmd: matrix-sdk-no-default + + - name: '[m], indexeddb stores' + cmd: matrix-sdk-indexeddb-stores + + - name: '[m], indexeddb stores, no crypto' + cmd: matrix-sdk-indexeddb-stores-no-crypto + + - name: '[m], wasm-example' + cmd: matrix-sdk-command-bot steps: - name: Checkout the repo @@ -141,26 +180,30 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + target: wasm32-unknown-unknown + components: clippy profile: minimal override: true + - name: Install wasm-pack + uses: jetli/wasm-pack-action@v0.3.0 + with: + version: latest + - name: Load cache uses: Swatinem/rust-cache@v1 - - name: Install Node.js - uses: actions/setup-node@v3 + - name: Install nextest + uses: taiki-e/install-action@nextest + + - name: Rust Check + uses: actions-rs/cargo@v1 with: - node-version: ${{ matrix.node-version }} + command: run + args: -p xtask -- ci wasm ${{ matrix.cmd }} - - name: Install NPM dependencies - run: cd crates/matrix-sdk-crypto-nodejs && npm install - - - name: Build the Node.js binding - run: cd crates/matrix-sdk-crypto-nodejs && npm run build - - - name: Test the Node.js binding - run: cd crates/matrix-sdk-crypto-nodejs && npm run test - - - if: ${{ matrix.build-doc }} - name: Build the documentation - run: cd crates/matrix-sdk-crypto-nodejs && npm run doc + - name: Wasm-Pack test + uses: actions-rs/cargo@v1 + with: + command: run + args: -p xtask -- ci wasm-pack ${{ matrix.cmd }} diff --git a/.github/workflows/docs.yml b/.github/workflows/documentation.yml similarity index 84% rename from .github/workflows/docs.yml rename to .github/workflows/documentation.yml index d754d5867..9a1642dd8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/documentation.yml @@ -1,4 +1,4 @@ -name: Docs +name: Documentation on: push: @@ -7,7 +7,7 @@ on: jobs: docs: - name: Docs + name: All crates runs-on: ubuntu-latest if: github.event_name == 'push' || !github.event.pull_request.draft @@ -26,7 +26,7 @@ jobs: uses: Swatinem/rust-cache@v1 # Keep in sync with xtask docs - - name: Build docs + - name: Build documentation uses: actions-rs/cargo@v1 env: # Work around https://github.com/rust-lang/cargo/issues/10744 @@ -34,9 +34,9 @@ jobs: RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings" with: command: doc - args: --no-deps --workspace --exclude matrix-sdk-crypto-js --exclude matrix-sdk-crypto-nodejs --features docsrs + args: --no-deps --workspace --features docsrs - - name: Deploy docs + - name: Deploy documentation if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: peaceiris/actions-gh-pages@v3 with: diff --git a/.github/workflows/ffi.yml b/.github/workflows/ffi.yml deleted file mode 100644 index 1803742d6..000000000 --- a/.github/workflows/ffi.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: FFI - -on: - workflow_dispatch: - push: - branches: [main] - pull_request: - branches: [main] - types: - - opened - - reopened - - synchronize - - ready_for_review - -env: - CARGO_TERM_COLOR: always - -jobs: - test: - name: Run Apple platform tests - runs-on: macos-12 - - steps: - - name: Checkout - uses: actions/checkout@v1 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - profile: minimal - override: true - - - name: Install targets - run: | - rustup target add aarch64-apple-ios-sim --toolchain nightly - rustup target add x86_64-apple-ios --toolchain nightly - - - name: Load cache - uses: Swatinem/rust-cache@v1 - - - name: Install Uniffi - uses: actions-rs/cargo@v1 - with: - command: install - args: uniffi_bindgen - - - - name: Generate .xcframework - run: sh bindings/apple/debug_build_xcframework.sh ci - - - name: Run XCTests - run: | - xcodebuild test \ - -project bindings/apple/MatrixRustSDK.xcodeproj \ - -scheme MatrixRustSDK \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.4' \ No newline at end of file diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml deleted file mode 100644 index 11e293676..000000000 --- a/.github/workflows/wasm.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: WASM - -on: - push: - branches: [main] - pull_request: - branches: [main] - types: - - opened - - reopened - - synchronize - - ready_for_review - -env: - CARGO_TERM_COLOR: always - -jobs: - check-wasm: - name: Build test / ${{ matrix.name }} - runs-on: ubuntu-latest - if: github.event_name == 'push' || !github.event.pull_request.draft - - strategy: - fail-fast: true - matrix: - name: - - matrix-sdk-qrcode - - matrix-sdk-base - - matrix-sdk-common - - matrix-sdk-crypto-js - - indexeddb-no-crypto - - indexeddb-with-crypto - - include: - - name: matrix-sdk (no-default, wasm-flags) - cmd: matrix-sdk-no-default - - name: matrix-sdk / indexeddb_stores - cmd: matrix-sdk-indexeddb-stores - - name: matrix-sdk / indexeddb_stores / no crypto - cmd: matrix-sdk-indexeddb-stores-no-crypto - - name: matrix-sdk / wasm-example - cmd: matrix-sdk-command-bot - - steps: - - name: Checkout the repo - uses: actions/checkout@v2 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: wasm32-unknown-unknown - components: clippy - profile: minimal - override: true - - - name: Install wasm-pack - uses: jetli/wasm-pack-action@v0.3.0 - with: - version: 'latest' - - - name: Load cache - uses: Swatinem/rust-cache@v1 - - - name: Rust Check - uses: actions-rs/cargo@v1 - with: - command: run - args: -p xtask -- ci wasm ${{ matrix.cmd || matrix.name }} - - - name: Wasm-Pack test - uses: actions-rs/cargo@v1 - with: - command: run - args: -p xtask -- ci wasm-pack ${{ matrix.cmd || matrix.name }} diff --git a/.gitignore b/.gitignore index a6abb2fb4..86294a3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ emsdk-* ## User settings xcuserdata/ .vscode/ + +## OS garbage +.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 38ab76bd0..25b490bb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,15 @@ [workspace] -members = ["benchmarks", "crates/*", "labs/*", "xtask"] -# xtask and labs should only be compiled when invoked explicitly +members = [ + "benchmarks", + "bindings/matrix-sdk-crypto-ffi", + "bindings/matrix-sdk-crypto-js", + "bindings/matrix-sdk-crypto-nodejs", + "bindings/matrix-sdk-ffi", + "crates/*", + "labs/*", + "xtask", +] +# xtask, labs and the bindings should only be built when invoked explicitly. default-members = ["benchmarks", "crates/*"] resolver = "2" @@ -11,3 +20,4 @@ lto = true # Optimize quote even in debug mode. Speeds up proc-macros enough to account # for the extra time of optimizing it for a clean build of matrix-sdk-ffi. quote = { opt-level = 2 } +sha2 = { opt-level = 2 } diff --git a/README.md b/README.md index fdf41f5c1..d6e310cf6 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ the API will change in breaking ways. If you are interested in using the matrix-sdk now is the time to try it out and provide feedback. +## Bindings + +Some crates of the **matrix-rust-sdk** can be embedded inside other +environments, like Swift, Kotlin, JavaScript, Node.js etc. Please, +explore the [`bindings/`](./bindings/) directory to learn more. + ## License [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 6735ec35e..df6e400a8 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -12,7 +12,7 @@ criterion = { version = "0.3.5", features = ["async", "async_tokio", "html_repor matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.5.0" } matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.1.0", default-features = false, features = ["crypto-store"] } matrix-sdk-test = { path = "../crates/matrix-sdk-test", version = "0.5.0" } -ruma = "0.6.1" +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } serde_json = "1.0.79" tempfile = "3.3.0" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] } diff --git a/bindings/README.md b/bindings/README.md new file mode 100644 index 000000000..ff55b5573 --- /dev/null +++ b/bindings/README.md @@ -0,0 +1,22 @@ +# Matrix Rust SDK bindings + +In this directory, one can find bindings to the Rust SDK that are +maintained by the owners of the Matrix Rust SDK project. + +* [`apple`] or `matrix-rust-components-swift`, Swift bindings of the + [`matrix-sdk`] crate via [`matrix-sdk-ffi`], +* [`matrix-sdk-crypto-ffi`], bindings of the [`matrix-sdk-crypto`] + crate, +* [`matrix-sdk-crypto-js`], JavaScript bindings of the + [`matrix-sdk-crypto`] crate, +* [`matrix-sdk-crypto-nodejs`], Node.js bindings of the + [`matrix-sdk-crypto`] crate, +* [`matrix-sdk-ffi`], bindings of the [`matrix-sdk`] crate, + +[`apple`]: ./apple +[`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi +[`matrix-sdk-crypto-js`]: ../crates/matrix-sdk-crypto +[`matrix-sdk-crypto-nodejs`]: ../crates/matrix-sdk-crypto +[`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto +[`matrix-sdk-ffi`]: ./matrix-sdk-ffi +[`matrix-sdk`]: ../crates/matrix-sdk diff --git a/bindings/apple/MatrixRustSDKTests/MatrixRustSDKTests.swift b/bindings/apple/MatrixRustSDKTests/MatrixRustSDKTests.swift index 17e7edd68..2a4960dbf 100644 --- a/bindings/apple/MatrixRustSDKTests/MatrixRustSDKTests.swift +++ b/bindings/apple/MatrixRustSDKTests/MatrixRustSDKTests.swift @@ -10,23 +10,14 @@ import XCTest class MatrixRustSDKTests: XCTestCase { - static var client: Client! - - override class func setUp() { - client = try! guestClient(basePath: basePath, homeserver: "https://matrix.org") - } - - func testClientProperties() { - XCTAssertTrue(Self.client.isGuest()) - - XCTAssertNotNil(try? Self.client.restoreToken()) - XCTAssertNotNil(try? Self.client.deviceId()) - XCTAssertNotNil(try? Self.client.displayName()) - } - func testReadOnlyFileSystemError() { do { - let _ = try loginNewClient(basePath: "", username: "test", password: "test") + let client = try ClientBuilder() + .basePath(path: "") + .username(username: "@test:domain") + .build() + + try client.login(username: "@test:domain", password: "test") } catch ClientError.Generic(let message) { XCTAssertNotNil(message.range(of: "Read-only file system")) } catch { diff --git a/bindings/apple/MatrixSDKCrypto.podspec b/bindings/apple/MatrixSDKCrypto.podspec new file mode 100644 index 000000000..2d098fd3f --- /dev/null +++ b/bindings/apple/MatrixSDKCrypto.podspec @@ -0,0 +1,17 @@ +Pod::Spec.new do |s| + + s.name = "MatrixSDKCrypto" + s.version = "0.1.0" + s.summary = "Uniffi based bindings for the Rust SDK crypto crate." + s.homepage = "https://github.com/matrix-org/matrix-rust-sdk" + s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" } + s.author = { "matrix.org" => "support@matrix.org" } + + s.ios.deployment_target = "11.0" + s.swift_versions = ['5.0'] + + s.source = { :http => "https://github.com/matrix-org/matrix-rust-sdk/releases/download/matrix-sdk-crypto-ffi-#{s.version}/MatrixSDKCryptoFFI.zip" } + s.vendored_frameworks = "MatrixSDKCryptoFFI.xcframework" + s.source_files = "Sources/**/*.{swift}" + +end diff --git a/bindings/apple/README.md b/bindings/apple/README.md index 56f749356..29688a01e 100644 --- a/bindings/apple/README.md +++ b/bindings/apple/README.md @@ -1,29 +1,42 @@ # Apple platforms support -This project and build script demonstrate how to create an XCFramework that can be imported into an Xcode project and run on Apple platforms. +This project and build script demonstrate how to create an XCFramework that can be imported into an Xcode project and run on Apple platforms. It can compile and bundle an [entire SDK](#Building-the-SDK), or only a smaller [Crypto module](#Building-only-the-Crypto-SDK) that provides end-to-end encryption for clients that already depend on an SDK (e.g. [Matrix iOS SDK](https://github.com/matrix-org/matrix-ios-sdk)) -## Building the universal framework +## Prerequisites for building universal frameworks + +* the Rust toolchain +* UniFFI - `cargo install uniffi_bindgen` +* Apple targets (e.g. `rustup target add aarch64-apple-ios`) +* `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html) +* `lipo` for creating the fat static libs + +## Building the SDK ``` sh build_xcframework.sh ``` -**Prerequisites** - -* the Rust toolchain -* UniFFI - `cargo install uniffi_bindge` -* Apple targets (e.g. `rustup target add aarch64-apple-ios`) -* `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html) -* `lipo` for creating the fat static libs - - The `build_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`: 1. compile `matrix-sdk-ffi` libraries for iOS, the iOS simulator, MacOS, and Mac Catalyst under `/target`. Some targets are not part of the standard library and they will be built using the nightly toolchain. -* `lipo` together the libraries for the same platform under `/generated` -* run `uniffi` and generate the C header, module map and swift files -* `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKFFI.xcframework` -* cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework) +2. `lipo` together the libraries for the same platform under `/generated` +3. run `uniffi` and generate the C header, module map and swift files +4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKFFI.xcframework` +5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework) + +## Building only the Crypto SDK + +``` +sh build_crypto_xcframework.sh +``` + +The `build_crypto_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`: + +1. compile `matrix-sdk-crypto-ffi` libraries for iOS and the iOS simulator under `/target` +2. `lipo` together the libraries for the same platform under `/generated` +3. run `uniffi` and generate the C header, module map and swift files +4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKCryptoFFI.xcframework` +5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework) ## Running the Xcode project @@ -36,4 +49,5 @@ It makes the compiled code available to swift by importing the C header through Once all the generated components are available running it should be as easy as choosing a platform and clicking run. ## Distribution -The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a Swift package available [TBD](here) \ No newline at end of file + +The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a Swift package available [TBD](here) in the case of SDK, and as CocoaPods podspec in the case of Crypto SDK. diff --git a/bindings/apple/build_crypto_xcframework.sh b/bindings/apple/build_crypto_xcframework.sh new file mode 100755 index 000000000..843b2ec7d --- /dev/null +++ b/bindings/apple/build_crypto_xcframework.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -eEu + +cd "$(dirname "$0")" + +# Path to the repo root +SRC_ROOT=../.. + +TARGET_DIR="${SRC_ROOT}/target" + +GENERATED_DIR="${SRC_ROOT}/generated" +if [ -d "${GENERATED_DIR}" ]; then rm -rf "${GENERATED_DIR}"; fi +mkdir -p ${GENERATED_DIR} + +REL_FLAG="--release" +REL_TYPE_DIR="release" + +TARGET_CRATE=matrix-sdk-crypto-ffi + +# Build static libs for all the different architectures + +# iOS +cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios" + +# iOS Simulator +cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim" +cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios" + +# Lipo together the libraries for the same platform + +# iOS Simulator +lipo -create \ + "${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \ + "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \ + -output "${GENERATED_DIR}/libmatrix_crypto_ffi.a" + +# Generate uniffi files +uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" --language swift --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" --out-dir ${GENERATED_DIR} + +# Move headers to the right place +HEADERS_DIR=${GENERATED_DIR}/headers +mkdir -p ${HEADERS_DIR} +mv ${GENERATED_DIR}/*.h ${HEADERS_DIR} + +# Rename and move modulemap to the right place +mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap + +# Move source files to the right place +SWIFT_DIR="${GENERATED_DIR}/Sources" +mkdir -p ${SWIFT_DIR} +mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR} + +# Build the xcframework + +if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi + +xcodebuild -create-xcframework \ + -library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" \ + -headers ${HEADERS_DIR} \ + -library "${GENERATED_DIR}/libmatrix_crypto_ffi.a" \ + -headers ${HEADERS_DIR} \ + -output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" + +# Cleanup + +if [ -f "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a" ]; then rm -rf "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_crypto_ffi.a"; fi +if [ -f "${GENERATED_DIR}/libmatrix_crypto_ffi.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_crypto_ffi.a"; fi +if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi + +# Zip up framework, sources and LICENSE, ready to be uploaded to GitHub Releases and used by MatrixSDKCrypto.podspec +cp ${SRC_ROOT}/LICENSE $GENERATED_DIR +cd $GENERATED_DIR +zip -r MatrixSDKCryptoFFI.zip MatrixSDKCryptoFFI.xcframework Sources LICENSE diff --git a/bindings/apple/build_xcframework.sh b/bindings/apple/build_xcframework.sh index 220f76a05..c0d2919e9 100755 --- a/bindings/apple/build_xcframework.sh +++ b/bindings/apple/build_xcframework.sh @@ -53,7 +53,7 @@ lipo -create \ # Generate uniffi files -uniffi-bindgen generate "${SRC_ROOT}/crates/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR} +uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR} # Move them to the right place HEADERS_DIR=${GENERATED_DIR}/headers diff --git a/bindings/apple/debug_build_xcframework.sh b/bindings/apple/debug_build_xcframework.sh index d78c0fba5..975e62cf7 100755 --- a/bindings/apple/debug_build_xcframework.sh +++ b/bindings/apple/debug_build_xcframework.sh @@ -33,7 +33,7 @@ lipo -create \ -output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" # Generate uniffi files -uniffi-bindgen generate "${SRC_ROOT}/crates/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR} +uniffi-bindgen generate "${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR} # Move them to the right place HEADERS_DIR=${GENERATED_DIR}/headers @@ -64,7 +64,7 @@ if [ "$IS_CI" = false ] ; then echo "Preparing matrix-rust-components-swift" # Debug -> Copy generated files over to ../../../matrix-rust-components-swift - echo "$(echo "import MatrixSDKFFIWrapper\n"; cat "${SWIFT_DIR}/sdk.swift")" > "${SWIFT_DIR}/sdk.swift" + echo "$(printf "import MatrixSDKFFIWrapper\n\n"; cat "${SWIFT_DIR}/sdk.swift")" > "${SWIFT_DIR}/sdk.swift" rsync -a --delete "${GENERATED_DIR}/MatrixSDKFFI.xcframework" "${SRC_ROOT}/../matrix-rust-components-swift/" rsync -a --delete "${GENERATED_DIR}/swift/" "${SRC_ROOT}/../matrix-rust-components-swift/Sources/MatrixRustSDK" diff --git a/crates/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml similarity index 74% rename from crates/matrix-sdk-crypto-ffi/Cargo.toml rename to bindings/matrix-sdk-crypto-ffi/Cargo.toml index b18573963..6e18376c8 100644 --- a/crates/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -20,14 +20,15 @@ hmac = "0.12.1" http = "0.2.6" pbkdf2 = "0.11.0" rand = "0.8.5" -ruma = { version = "0.6.1", features = ["client-api-c"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] } serde = "1.0.136" serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" tracing = "0.1.34" tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } -uniffi = "0.17.0" +# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job +uniffi = "0.18.0" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [dependencies.js_int] @@ -35,16 +36,16 @@ version = "0.2.2" features = ["lax_deserialize"] [dependencies.matrix-sdk-common] -path = "../matrix-sdk-common" +path = "../../crates/matrix-sdk-common" version = "0.5.0" [dependencies.matrix-sdk-crypto] -path = "../matrix-sdk-crypto" +path = "../../crates/matrix-sdk-crypto" version = "0.5.0" features = ["qrcode", "backups_v1"] [dependencies.matrix-sdk-sled] -path = "../matrix-sdk-sled" +path = "../../crates/matrix-sdk-sled" version = "0.1.0" default_features = false features = ["crypto-store"] @@ -56,10 +57,10 @@ features = ["rt-multi-thread"] [dependencies.vodozemac] git = "https://github.com/matrix-org/vodozemac/" -rev = "d0e744287a14319c2a9148fef3747548c740fc36" +rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" [build-dependencies] -uniffi_build = { version = "0.17.0", features = ["builtin-bindgen"] } +uniffi_build = { version = "0.18.0", features = ["builtin-bindgen"] } [dev-dependencies] tempfile = "3.3.0" diff --git a/crates/matrix-sdk-crypto-ffi/README.md b/bindings/matrix-sdk-crypto-ffi/README.md similarity index 100% rename from crates/matrix-sdk-crypto-ffi/README.md rename to bindings/matrix-sdk-crypto-ffi/README.md diff --git a/crates/matrix-sdk-crypto-ffi/build.rs b/bindings/matrix-sdk-crypto-ffi/build.rs similarity index 100% rename from crates/matrix-sdk-crypto-ffi/build.rs rename to bindings/matrix-sdk-crypto-ffi/build.rs diff --git a/crates/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs b/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs similarity index 98% rename from crates/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs rename to bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs index 91b250cf8..bb7cce325 100644 --- a/crates/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, iter}; +use std::{collections::HashMap, iter, ops::DerefMut}; use hmac::Hmac; use matrix_sdk_crypto::{ @@ -101,7 +101,7 @@ impl BackupRecoveryKey { let mut key = Box::new([0u8; Self::KEY_SIZE]); let rounds = rounds as u32; - pbkdf2::>(passphrase.as_bytes(), salt.as_bytes(), rounds, &mut *key); + pbkdf2::>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut()); let recovery_key = RecoveryKey::from_bytes(&key); diff --git a/crates/matrix-sdk-crypto-ffi/src/device.rs b/bindings/matrix-sdk-crypto-ffi/src/device.rs similarity index 100% rename from crates/matrix-sdk-crypto-ffi/src/device.rs rename to bindings/matrix-sdk-crypto-ffi/src/device.rs diff --git a/crates/matrix-sdk-crypto-ffi/src/error.rs b/bindings/matrix-sdk-crypto-ffi/src/error.rs similarity index 100% rename from crates/matrix-sdk-crypto-ffi/src/error.rs rename to bindings/matrix-sdk-crypto-ffi/src/error.rs diff --git a/crates/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs similarity index 98% rename from crates/matrix-sdk-crypto-ffi/src/lib.rs rename to bindings/matrix-sdk-crypto-ffi/src/lib.rs index 945584fd9..ce1f22d1e 100644 --- a/crates/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -14,7 +14,7 @@ mod responses; mod users; mod verification; -use std::{collections::HashMap, convert::TryFrom, str::FromStr, sync::Arc}; +use std::{borrow::Borrow, collections::HashMap, convert::TryFrom, str::FromStr, sync::Arc}; pub use backup_recovery_key::{ BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError, @@ -67,7 +67,7 @@ pub struct MigrationData { pub struct PickledAccount { /// The user id of the account owner. pub user_id: String, - /// The device id of the account owner. + /// The device ID of the account owner. pub device_id: String, /// The pickled version of the Olm account. pub pickle: String, @@ -190,7 +190,12 @@ pub fn migrate( processed_steps += 1; listener(processed_steps, total_steps); - let user_id: Arc = (&*parse_user_id(&data.account.user_id)?).into(); + let user_id: Arc = { + 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 = data.account.device_id.into(); let device_id: Arc = device_id.into(); diff --git a/crates/matrix-sdk-crypto-ffi/src/logger.rs b/bindings/matrix-sdk-crypto-ffi/src/logger.rs similarity index 100% rename from crates/matrix-sdk-crypto-ffi/src/logger.rs rename to bindings/matrix-sdk-crypto-ffi/src/logger.rs diff --git a/crates/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs similarity index 99% rename from crates/matrix-sdk-crypto-ffi/src/machine.rs rename to bindings/matrix-sdk-crypto-ffi/src/machine.rs index 922339223..79c595450 100644 --- a/crates/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -270,7 +270,7 @@ impl OlmMachine { } } - /// Mark the device of the given user with the given device id as trusted. + /// Mark the device of the given user with the given device ID as trusted. pub fn mark_device_as_trusted( &self, user_id: &str, @@ -526,7 +526,7 @@ impl OlmMachine { EncryptionSettings::default(), ))?; - Ok(requests.into_iter().map(|r| (&*r).into()).collect()) + Ok(requests.into_iter().map(|r| r.as_ref().into()).collect()) } /// Encrypt the given event with the given type and content for the given diff --git a/crates/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl similarity index 99% rename from crates/matrix-sdk-crypto-ffi/src/olm.udl rename to bindings/matrix-sdk-crypto-ffi/src/olm.udl index ae4fbce5f..e9499b417 100644 --- a/crates/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -15,7 +15,7 @@ interface MigrationError { }; callback interface Logger { - void log(string log_line); + void log(string logLine); }; callback interface ProgressListener { diff --git a/crates/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs similarity index 99% rename from crates/matrix-sdk-crypto-ffi/src/responses.rs rename to bindings/matrix-sdk-crypto-ffi/src/responses.rs index 03caff5c1..feb6b89c9 100644 --- a/crates/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -132,6 +132,7 @@ impl From for Request { let body = json!({ "device_keys": u.device_keys, "one_time_keys": u.one_time_keys, + "fallback_keys": u.fallback_keys, }); Request::KeysUpload { diff --git a/crates/matrix-sdk-crypto-ffi/src/users.rs b/bindings/matrix-sdk-crypto-ffi/src/users.rs similarity index 100% rename from crates/matrix-sdk-crypto-ffi/src/users.rs rename to bindings/matrix-sdk-crypto-ffi/src/users.rs diff --git a/crates/matrix-sdk-crypto-ffi/src/verification.rs b/bindings/matrix-sdk-crypto-ffi/src/verification.rs similarity index 100% rename from crates/matrix-sdk-crypto-ffi/src/verification.rs rename to bindings/matrix-sdk-crypto-ffi/src/verification.rs diff --git a/bindings/matrix-sdk-crypto-ffi/uniffi.toml b/bindings/matrix-sdk-crypto-ffi/uniffi.toml new file mode 100644 index 000000000..f94dc9f3b --- /dev/null +++ b/bindings/matrix-sdk-crypto-ffi/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.swift] +module_name = "MatrixSDKCrypto" diff --git a/crates/matrix-sdk-crypto-js/.cargo/config b/bindings/matrix-sdk-crypto-js/.cargo/config similarity index 100% rename from crates/matrix-sdk-crypto-js/.cargo/config rename to bindings/matrix-sdk-crypto-js/.cargo/config diff --git a/bindings/matrix-sdk-crypto-js/.gitignore b/bindings/matrix-sdk-crypto-js/.gitignore new file mode 100644 index 000000000..4029dd2a8 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/.gitignore @@ -0,0 +1,3 @@ +/docs +/node_modules +/package-lock.json diff --git a/crates/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml similarity index 64% rename from crates/matrix-sdk-crypto-js/Cargo.toml rename to bindings/matrix-sdk-crypto-js/Cargo.toml index 8a918b105..94d14468c 100644 --- a/crates/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -27,9 +27,10 @@ qrcode = ["matrix-sdk-crypto/qrcode"] docsrs = [] [dependencies] -matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto" } -ruma = { version = "0.6.2", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } -vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36", features = ["js"] } +matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" } +matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" js-sys = "0.3.49" diff --git a/bindings/matrix-sdk-crypto-js/README.md b/bindings/matrix-sdk-crypto-js/README.md new file mode 100644 index 000000000..7ff2e718a --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/README.md @@ -0,0 +1,55 @@ +# `matrix-sdk-crypto-js` + +Welcome to the [WebAssembly]Β +Β JavaScript binding for the Rust +[`matrix-sdk-crypto`] library! WebAssembly can run anywhere, but these +bindings are designed to run on a JavaScript host. These bindings are +part of the [`matrix-rust-sdk`] project, which is a library +implementation of a [Matrix] client-server. + +`matrix-sdk-crypto` is a no-network-IO implementation of a state +machine, named `OlmMachine`, that handles E2EE ([End-to-End +Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for +[Matrix] clients. + +## Usage + +These WebAssembly bindings are written in [Rust]. To build them, you +need to install the Rust compiler, see [the Install Rust +Page](https://www.rust-lang.org/tools/install). Then, the workflow is +pretty classical by using [npm], see [the Downloading and installing +Node.js and npm +Page](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). + +Once the Rust compiler, Node.js and npm are installed, you can run the +following commands: + +```sh +$ npm install +$ npm run build +$ npm run test +``` + +A `matrix_sdk_crypto.js`, `matrix_sdk_crypto.d.ts` and a +`matrix_sdk_crypto_bg.wasm` files should be generated in the `pkg/` +directory. + +TBD + +## Documentation + +To generate the documentation, please run the following command: + +```sh +$ npm run doc +``` + +The documentation is generated in the `./docs` directory. + + + +[WebAssembly]: https://webassembly.org/ +[`matrix-sdk-crypto`]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto +[`matrix-rust-sdk`]: https://github.com/matrix-org/matrix-rust-sdk +[Matrix]: https://matrix.org/ +[Rust]: https://www.rust-lang.org/ +[npm]: https://www.npmjs.com/ diff --git a/bindings/matrix-sdk-crypto-js/package.json b/bindings/matrix-sdk-crypto-js/package.json new file mode 100644 index 000000000..3328f325d --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/package.json @@ -0,0 +1,41 @@ +{ + "name": "matrix-sdk-crypto-js", + "version": "0.5.0", + "homepage": "https://github.com/matrix-org/matrix-rust-sdk", + "description": "Matrix encryption library, for JavaScript", + "license": "Apache-2.0", + "collaborators": [ + "Ivan Enderlin " + ], + "repository": { + "type": "git", + "url": "https://github.com/matrix-org/matrix-rust-sdk" + }, + "keywords": [ + "matrix", + "chat", + "messaging", + "ruma", + "nio" + ], + "main": "matrix_sdk_crypto.js", + "types": "pkg/matrix_sdk_crypto.d.ts", + "files": [ + "pkg/matrix_sdk_crypto_bg.wasm", + "pkg/matrix_sdk_crypto.js", + "pkg/matrix_sdk_crypto.d.ts" + ], + "devDependencies": { + "wasm-pack": "^0.10.2", + "jest": "^28.1.0", + "typedoc": "^0.22.17" + }, + "engines": { + "node": ">= 10" + }, + "scripts": { + "build": "RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./pkg", + "test": "jest --verbose", + "doc": "typedoc --tsconfig ." + } +} diff --git a/bindings/matrix-sdk-crypto-js/src/encryption.rs b/bindings/matrix-sdk-crypto-js/src/encryption.rs new file mode 100644 index 000000000..a817e7cd9 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/encryption.rs @@ -0,0 +1,126 @@ +//! Encryption types & siblings. + +use std::time::Duration; + +use wasm_bindgen::prelude::*; + +use crate::events; + +/// Settings for an encrypted room. +/// +/// This determines the algorithm and rotation periods of a group +/// session. +#[wasm_bindgen(getter_with_clone)] +#[derive(Debug, Clone)] +pub struct EncryptionSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EncryptionAlgorithm, + + /// How long the session should be used before changing it, + /// expressed in microseconds. + #[wasm_bindgen(js_name = "rotationPeriod")] + pub rotation_period: u64, + + /// How many messages should be sent before changing the session. + #[wasm_bindgen(js_name = "rotationPeriodMessages")] + pub rotation_period_messages: u64, + + /// The history visibility of the room when the session was + /// created. + #[wasm_bindgen(js_name = "historyVisibility")] + pub history_visibility: events::HistoryVisibility, +} + +impl Default for EncryptionSettings { + fn default() -> Self { + let default = matrix_sdk_crypto::olm::EncryptionSettings::default(); + + Self { + algorithm: default.algorithm.into(), + rotation_period: default.rotation_period.as_micros().try_into().unwrap(), + rotation_period_messages: default.rotation_period_msgs, + history_visibility: default.history_visibility.into(), + } + } +} + +#[wasm_bindgen] +impl EncryptionSettings { + /// Create a new `EncryptionSettings` with default values. + #[wasm_bindgen(constructor)] + pub fn new() -> EncryptionSettings { + Self::default() + } +} + +impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings { + fn from(value: &EncryptionSettings) -> Self { + Self { + algorithm: value.algorithm.clone().into(), + rotation_period: Duration::from_micros(value.rotation_period), + rotation_period_msgs: value.rotation_period_messages, + history_visibility: value.history_visibility.clone().into(), + } + } +} + +/// An encryption algorithm to be used to encrypt messages sent to a +/// room. +#[wasm_bindgen] +#[derive(Debug, Clone)] +pub enum EncryptionAlgorithm { + /// Olm version 1 using Curve25519, AES-256, and SHA-256. + OlmV1Curve25519AesSha2, + + /// Megolm version 1 using AES-256 and SHA-256. + MegolmV1AesSha2, +} + +impl From for ruma::EventEncryptionAlgorithm { + fn from(value: EncryptionAlgorithm) -> Self { + use EncryptionAlgorithm::*; + + match value { + OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, + MegolmV1AesSha2 => Self::MegolmV1AesSha2, + } + } +} + +impl From for EncryptionAlgorithm { + fn from(value: ruma::EventEncryptionAlgorithm) -> Self { + use ruma::EventEncryptionAlgorithm::*; + + match value { + OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, + MegolmV1AesSha2 => Self::MegolmV1AesSha2, + _ => unreachable!("Unknown variant"), + } + } +} + +/// The verification state of the device that sent an event to us. +#[wasm_bindgen] +#[derive(Debug)] +pub enum VerificationState { + /// The device is trusted. + Trusted, + + /// The device is not trusted. + Untrusted, + + /// The device is not known to us. + UnknownDevice, +} + +impl From<&matrix_sdk_common::deserialized_responses::VerificationState> for VerificationState { + fn from(value: &matrix_sdk_common::deserialized_responses::VerificationState) -> Self { + use matrix_sdk_common::deserialized_responses::VerificationState::*; + + match value { + Trusted => Self::Trusted, + Untrusted => Self::Untrusted, + UnknownDevice => Self::UnknownDevice, + } + } +} diff --git a/crates/matrix-sdk-crypto-js/src/events.rs b/bindings/matrix-sdk-crypto-js/src/events.rs similarity index 100% rename from crates/matrix-sdk-crypto-js/src/events.rs rename to bindings/matrix-sdk-crypto-js/src/events.rs diff --git a/crates/matrix-sdk-crypto-js/src/future.rs b/bindings/matrix-sdk-crypto-js/src/future.rs similarity index 100% rename from crates/matrix-sdk-crypto-js/src/future.rs rename to bindings/matrix-sdk-crypto-js/src/future.rs diff --git a/crates/matrix-sdk-crypto-js/src/identifiers.rs b/bindings/matrix-sdk-crypto-js/src/identifiers.rs similarity index 85% rename from crates/matrix-sdk-crypto-js/src/identifiers.rs rename to bindings/matrix-sdk-crypto-js/src/identifiers.rs index 3ff2f9068..e594d1a6a 100644 --- a/crates/matrix-sdk-crypto-js/src/identifiers.rs +++ b/bindings/matrix-sdk-crypto-js/src/identifiers.rs @@ -12,8 +12,8 @@ pub struct UserId { pub(crate) inner: ruma::OwnedUserId, } -impl UserId { - pub(crate) fn new_with(inner: ruma::OwnedUserId) -> Self { +impl From for UserId { + fn from(inner: ruma::OwnedUserId) -> Self { Self { inner } } } @@ -23,16 +23,17 @@ impl UserId { /// Parse/validate and create a new `UserId`. #[wasm_bindgen(constructor)] pub fn new(id: &str) -> Result { - Ok(Self::new_with(ruma::UserId::parse(id)?)) + Ok(Self::from(ruma::UserId::parse(id)?)) } /// Returns the user's localpart. + #[wasm_bindgen(getter)] pub fn localpart(&self) -> String { self.inner.localpart().to_owned() } /// Returns the server name of the user ID. - #[wasm_bindgen(js_name = "serverName")] + #[wasm_bindgen(getter, js_name = "serverName")] pub fn server_name(&self) -> ServerName { ServerName { inner: self.inner.server_name().to_owned() } } @@ -42,7 +43,7 @@ impl UserId { /// A historical user ID is one that doesn't conform to the latest /// specification of the user ID grammar but is still accepted /// because it was previously allowed. - #[wasm_bindgen(getter, js_name = "isHistorical")] + #[wasm_bindgen(js_name = "isHistorical")] pub fn is_historical(&self) -> bool { self.inner.is_historical() } @@ -65,8 +66,8 @@ pub struct DeviceId { pub(crate) inner: ruma::OwnedDeviceId, } -impl DeviceId { - pub(crate) fn new_with(inner: ruma::OwnedDeviceId) -> Self { +impl From for DeviceId { + fn from(inner: ruma::OwnedDeviceId) -> Self { Self { inner } } } @@ -76,7 +77,7 @@ impl DeviceId { /// Create a new `DeviceId`. #[wasm_bindgen(constructor)] pub fn new(id: &str) -> DeviceId { - Self::new_with(id.into()) + Self::from(ruma::OwnedDeviceId::from(id)) } /// Return the device ID as a string. @@ -96,8 +97,8 @@ pub struct RoomId { pub(crate) inner: ruma::OwnedRoomId, } -impl RoomId { - pub(crate) fn new_with(inner: ruma::OwnedRoomId) -> Self { +impl From for RoomId { + fn from(inner: ruma::OwnedRoomId) -> Self { Self { inner } } } @@ -107,16 +108,17 @@ impl RoomId { /// Parse/validate and create a new `RoomId`. #[wasm_bindgen(constructor)] pub fn new(id: &str) -> Result { - Ok(Self::new_with(ruma::RoomId::parse(id)?)) + Ok(Self::from(ruma::RoomId::parse(id)?)) } /// Returns the user's localpart. + #[wasm_bindgen(getter)] pub fn localpart(&self) -> String { self.inner.localpart().to_owned() } /// Returns the server name of the room ID. - #[wasm_bindgen(js_name = "serverName")] + #[wasm_bindgen(getter, js_name = "serverName")] pub fn server_name(&self) -> ServerName { ServerName { inner: self.inner.server_name().to_owned() } } @@ -153,11 +155,13 @@ impl ServerName { /// /// That is: Return the part of the server before `:` 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 { self.inner.port() } diff --git a/crates/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs similarity index 95% rename from crates/matrix-sdk-crypto-js/src/lib.rs rename to bindings/matrix-sdk-crypto-js/src/lib.rs index 03a1fc8fe..a9b59426f 100644 --- a/crates/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -15,7 +15,9 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs, missing_debug_implementations)] +#![allow(clippy::drop_non_drop)] // triggered by wasm_bindgen code +pub mod encryption; pub mod events; mod future; pub mod identifiers; diff --git a/crates/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs similarity index 79% rename from crates/matrix-sdk-crypto-js/src/machine.rs rename to bindings/matrix-sdk-crypto-js/src/machine.rs index fa2301965..e6f45175d 100644 --- a/crates/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -1,14 +1,17 @@ //! The crypto specific Olm objects. -use std::{collections::BTreeMap, time::Duration}; +use std::collections::BTreeMap; use js_sys::{Array, Map, Promise, Set}; -use ruma::{DeviceKeyAlgorithm, OwnedTransactionId, UInt}; +use ruma::{ + events::room::encrypted::OriginalSyncRoomEncryptedEvent, DeviceKeyAlgorithm, + OwnedTransactionId, UInt, +}; use serde_json::Value as JsonValue; use wasm_bindgen::prelude::*; use crate::{ - downcast, events, + downcast, encryption, future::future_to_promise, identifiers, requests, requests::OutgoingRequest, @@ -49,25 +52,25 @@ impl OlmMachine { } /// The unique user ID that owns this `OlmMachine` instance. - #[wasm_bindgen(js_name = "userId")] + #[wasm_bindgen(getter, js_name = "userId")] pub fn user_id(&self) -> identifiers::UserId { - identifiers::UserId::new_with(self.inner.user_id().to_owned()) + identifiers::UserId::from(self.inner.user_id().to_owned()) } /// The unique device ID that identifies this `OlmMachine`. - #[wasm_bindgen(js_name = "deviceId")] + #[wasm_bindgen(getter, js_name = "deviceId")] pub fn device_id(&self) -> identifiers::DeviceId { - identifiers::DeviceId::new_with(self.inner.device_id().to_owned()) + identifiers::DeviceId::from(self.inner.device_id().to_owned()) } /// Get the public parts of our Olm identity keys. - #[wasm_bindgen(js_name = "identityKeys")] + #[wasm_bindgen(getter, js_name = "identityKeys")] pub fn identity_keys(&self) -> IdentityKeys { self.inner.identity_keys().into() } /// Get the display name of our own device. - #[wasm_bindgen(js_name = "displayName")] + #[wasm_bindgen(getter, js_name = "displayName")] pub fn display_name(&self) -> Promise { let me = self.inner.clone(); @@ -81,11 +84,9 @@ impl OlmMachine { pub fn tracked_users(&self) -> Set { let set = Set::new(&JsValue::UNDEFINED); - self.inner.tracked_users().into_iter().map(identifiers::UserId::new_with).for_each( - |user| { - set.add(&user.into()); - }, - ); + for user in self.inner.tracked_users() { + set.add(&identifiers::UserId::from(user).into()); + } set } @@ -261,6 +262,29 @@ impl OlmMachine { })) } + /// Decrypt an event from a room timeline. + /// + /// # Arguments + /// + /// * `event`, the event that should be decrypted. + /// * `room_id`, the ID of the room where the event was sent to. + #[wasm_bindgen(js_name = "decryptRoomEvent")] + pub fn decrypt_room_event( + &self, + event: &str, + room_id: &identifiers::RoomId, + ) -> Result { + let event: OriginalSyncRoomEncryptedEvent = serde_json::from_str(event)?; + let room_id = room_id.inner.clone(); + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + let room_event = me.decrypt_room_event(&event, room_id.as_ref()).await?; + + Ok(responses::DecryptedRoomEvent::from(room_event)) + })) + } + /// Invalidate the currently active outbound group session for the /// given room. /// @@ -284,7 +308,7 @@ impl OlmMachine { &self, room_id: &identifiers::RoomId, users: &Array, - encryption_settings: &EncryptionSettings, + encryption_settings: &encryption::EncryptionSettings, ) -> Result { let room_id = room_id.inner.clone(); let users = users @@ -420,96 +444,3 @@ impl From for IdentityKeys { } } } - -/// An encryption algorithm to be used to encrypt messages sent to a -/// room. -#[wasm_bindgen] -#[derive(Debug, Clone)] -pub enum EncryptionAlgorithm { - /// Olm version 1 using Curve25519, AES-256, and SHA-256. - OlmV1Curve25519AesSha2, - - /// Megolm version 1 using AES-256 and SHA-256. - MegolmV1AesSha2, -} - -impl From for ruma::EventEncryptionAlgorithm { - fn from(value: EncryptionAlgorithm) -> Self { - use EncryptionAlgorithm::*; - - match value { - OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, - MegolmV1AesSha2 => Self::MegolmV1AesSha2, - } - } -} - -impl From for EncryptionAlgorithm { - fn from(value: ruma::EventEncryptionAlgorithm) -> Self { - use ruma::EventEncryptionAlgorithm::*; - - match value { - OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, - MegolmV1AesSha2 => Self::MegolmV1AesSha2, - _ => unreachable!("Unknown variant"), - } - } -} - -/// Settings for an encrypted room. -/// -/// This determines the algorithm and rotation periods of a group -/// session. -#[wasm_bindgen(getter_with_clone)] -#[derive(Debug, Clone)] -pub struct EncryptionSettings { - /// The encryption algorithm that should be used in the room. - pub algorithm: EncryptionAlgorithm, - - /// How long the session should be used before changing it, - /// expressed in microseconds. - #[wasm_bindgen(js_name = "rotationPeriod")] - pub rotation_period: u64, - - /// How many messages should be sent before changing the session. - #[wasm_bindgen(js_name = "rotationPeriodMessages")] - pub rotation_period_messages: u64, - - /// The history visibility of the room when the session was - /// created. - #[wasm_bindgen(js_name = "historyVisibility")] - pub history_visibility: events::HistoryVisibility, -} - -impl Default for EncryptionSettings { - fn default() -> Self { - let default = matrix_sdk_crypto::olm::EncryptionSettings::default(); - - Self { - algorithm: default.algorithm.into(), - rotation_period: default.rotation_period.as_micros().try_into().unwrap(), - rotation_period_messages: default.rotation_period_msgs, - history_visibility: default.history_visibility.into(), - } - } -} - -#[wasm_bindgen] -impl EncryptionSettings { - /// Create a new `EncryptionSettings` with default values. - #[wasm_bindgen(constructor)] - pub fn new() -> EncryptionSettings { - Self::default() - } -} - -impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings { - fn from(value: &EncryptionSettings) -> Self { - Self { - algorithm: value.algorithm.clone().into(), - rotation_period: Duration::from_micros(value.rotation_period), - rotation_period_msgs: value.rotation_period_messages, - history_visibility: value.history_visibility.clone().into(), - } - } -} diff --git a/crates/matrix-sdk-crypto-js/src/requests.rs b/bindings/matrix-sdk-crypto-js/src/requests.rs similarity index 72% rename from crates/matrix-sdk-crypto-js/src/requests.rs rename to bindings/matrix-sdk-crypto-js/src/requests.rs index 539c62b7e..99d02a40c 100644 --- a/crates/matrix-sdk-crypto-js/src/requests.rs +++ b/bindings/matrix-sdk-crypto-js/src/requests.rs @@ -26,17 +26,32 @@ use wasm_bindgen::prelude::*; pub struct KeysUploadRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// /// ```json - /// {"device_keys": …, "one_time_keys": …} + /// {"device_keys": …, "one_time_keys": …, "fallback_keys": …} /// ``` #[wasm_bindgen(readonly)] pub body: JsString, } +#[wasm_bindgen] +impl KeysUploadRequest { + /// Create a new `KeysUploadRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> KeysUploadRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::KeysUpload + } +} + /// Data for a request to the `/keys/query` API endpoint /// ([specification]). /// @@ -48,7 +63,7 @@ pub struct KeysUploadRequest { pub struct KeysQueryRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// @@ -59,6 +74,21 @@ pub struct KeysQueryRequest { pub body: JsString, } +#[wasm_bindgen] +impl KeysQueryRequest { + /// Create a new `KeysQueryRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> KeysQueryRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::KeysQuery + } +} + /// Data for a request to the `/keys/claim` API endpoint /// ([specification]). /// @@ -71,7 +101,7 @@ pub struct KeysQueryRequest { pub struct KeysClaimRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// @@ -82,6 +112,21 @@ pub struct KeysClaimRequest { pub body: JsString, } +#[wasm_bindgen] +impl KeysClaimRequest { + /// Create a new `KeysClaimRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> KeysClaimRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::KeysClaim + } +} + /// Data for a request to the `/sendToDevice` API endpoint /// ([specification]). /// @@ -93,7 +138,7 @@ pub struct KeysClaimRequest { pub struct ToDeviceRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// @@ -104,6 +149,21 @@ pub struct ToDeviceRequest { pub body: JsString, } +#[wasm_bindgen] +impl ToDeviceRequest { + /// Create a new `ToDeviceRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> ToDeviceRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::ToDevice + } +} + /// Data for a request to the `/keys/signatures/upload` API endpoint /// ([specification]). /// @@ -115,7 +175,7 @@ pub struct ToDeviceRequest { pub struct SignatureUploadRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// @@ -126,6 +186,21 @@ pub struct SignatureUploadRequest { pub body: JsString, } +#[wasm_bindgen] +impl SignatureUploadRequest { + /// Create a new `SignatureUploadRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> SignatureUploadRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::SignatureUpload + } +} + /// A customized owned request type for sending out room messages /// ([specification]). /// @@ -135,7 +210,7 @@ pub struct SignatureUploadRequest { pub struct RoomMessageRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// @@ -146,6 +221,21 @@ pub struct RoomMessageRequest { pub body: JsString, } +#[wasm_bindgen] +impl RoomMessageRequest { + /// Create a new `RoomMessageRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> RoomMessageRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::RoomMessage + } +} + /// A request that will back up a batch of room keys to the server /// ([specification]). /// @@ -155,7 +245,7 @@ pub struct RoomMessageRequest { pub struct KeysBackupRequest { /// The request ID. #[wasm_bindgen(readonly)] - pub request_id: JsString, + pub id: JsString, /// A JSON-encoded object of form: /// @@ -166,6 +256,21 @@ pub struct KeysBackupRequest { pub body: JsString, } +#[wasm_bindgen] +impl KeysBackupRequest { + /// Create a new `KeysBackupRequest`. + #[wasm_bindgen(constructor)] + pub fn new(id: JsString, body: JsString) -> KeysBackupRequest { + Self { id, body } + } + + /// Get its request type. + #[wasm_bindgen(getter, js_name = "type")] + pub fn request_type(&self) -> RequestType { + RequestType::KeysBackup + } +} + macro_rules! request { ($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => { impl TryFrom<(String, &$ruma_request)> for $request { @@ -181,7 +286,7 @@ macro_rules! request { let value = serde_json::Value::Object(map); Ok($request { - request_id: request_id.into(), + id: request_id.into(), body: serde_json::to_string(&value)?.into(), }) } @@ -189,7 +294,7 @@ macro_rules! request { }; } -request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys); +request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys); request!(KeysQueryRequest from RumaKeysQueryRequest maps fields timeout, device_keys, token); request!(KeysClaimRequest from RumaKeysClaimRequest maps fields timeout, one_time_keys); request!(ToDeviceRequest from RumaToDeviceRequest maps fields event_type, txn_id, messages); diff --git a/crates/matrix-sdk-crypto-js/src/responses.rs b/bindings/matrix-sdk-crypto-js/src/responses.rs similarity index 56% rename from crates/matrix-sdk-crypto-js/src/responses.rs rename to bindings/matrix-sdk-crypto-js/src/responses.rs index 080347dbb..3452bdee3 100644 --- a/crates/matrix-sdk-crypto-js/src/responses.rs +++ b/bindings/matrix-sdk-crypto-js/src/responses.rs @@ -1,5 +1,9 @@ //! Types related to responses. +use std::borrow::Borrow; + +use js_sys::{Array, JsString}; +use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo}; use matrix_sdk_crypto::IncomingResponse; pub(crate) use ruma::api::client::{ backup::add_backup_keys::v3::Response as KeysBackupResponse, @@ -14,7 +18,7 @@ pub(crate) use ruma::api::client::{ use ruma::api::IncomingResponse as RumaIncomingResponse; use wasm_bindgen::prelude::*; -use crate::requests::RequestType; +use crate::{encryption, identifiers, requests::RequestType}; pub(crate) fn response_from_string(body: &str) -> http::Result>> { http::Response::builder().status(200).body(body.as_bytes().to_vec()) @@ -126,3 +130,81 @@ impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> { } } } + +/// A decrypted room event. +#[wasm_bindgen(getter_with_clone)] +#[derive(Debug)] +pub struct DecryptedRoomEvent { + /// The JSON-encoded decrypted event. + #[wasm_bindgen(readonly)] + pub event: JsString, + + encryption_info: Option, +} + +#[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 { + 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 { + Some(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone())) + } + + /// The Curve25519 key of the device that created the megolm + /// decryption key originally. + #[wasm_bindgen(getter, js_name = "senderCurve25519Key")] + pub fn sender_curve25519_key(&self) -> Option { + 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 { + match &self.encryption_info.as_ref()?.algorithm_info { + AlgorithmInfo::MegolmV1AesSha2 { sender_claimed_keys, .. } => { + sender_claimed_keys.get(&ruma::DeviceKeyAlgorithm::Ed25519).cloned().map(Into::into) + } + } + } + + /// Chain of Curve25519 keys through which this session was + /// forwarded, via `m.forwarded_room_key` events. + #[wasm_bindgen(getter, js_name = "forwardingCurve25519KeyChain")] + pub fn forwarding_curve25519_key_chain(&self) -> Option { + Some(match &self.encryption_info.as_ref()?.algorithm_info { + AlgorithmInfo::MegolmV1AesSha2 { forwarding_curve25519_key_chain, .. } => { + forwarding_curve25519_key_chain.iter().map(JsValue::from).collect() + } + }) + } + + /// The verification state of the device that sent us the event, + /// note this is the state of the device at the time of + /// decryption. It may change in the future if a device gets + /// verified or deleted. + #[wasm_bindgen(getter, js_name = "verificationState")] + pub fn verification_state(&self) -> Option { + Some((self.encryption_info.as_ref()?.verification_state.borrow()).into()) + } +} + +impl From for DecryptedRoomEvent { + fn from(value: matrix_sdk_common::deserialized_responses::RoomEvent) -> Self { + Self { + event: value.event.json().get().to_owned().into(), + encryption_info: value.encryption_info, + } + } +} diff --git a/crates/matrix-sdk-crypto-js/src/sync_events.rs b/bindings/matrix-sdk-crypto-js/src/sync_events.rs similarity index 77% rename from crates/matrix-sdk-crypto-js/src/sync_events.rs rename to bindings/matrix-sdk-crypto-js/src/sync_events.rs index 0c7c91ddc..d58906eb2 100644 --- a/crates/matrix-sdk-crypto-js/src/sync_events.rs +++ b/bindings/matrix-sdk-crypto-js/src/sync_events.rs @@ -18,15 +18,17 @@ impl DeviceLists { /// /// `changed` and `left` must be an array of `UserId`. #[wasm_bindgen(constructor)] - pub fn new(changed: Array, left: Array) -> Result { + pub fn new(changed: Option, left: Option) -> Result { let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default(); inner.changed = changed + .unwrap_or_default() .iter() .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) .collect::, JsError>>()?; inner.left = left + .unwrap_or_default() .iter() .map(|user| Ok(downcast::(&user, "UserId")?.inner.clone())) .collect::, JsError>>()?; @@ -40,24 +42,27 @@ impl DeviceLists { self.inner.is_empty() } - /// List of users who have updated their device identity keys or who now - /// share an encrypted room with the client since the previous sync + /// List of users who have updated their device identity keys or + /// who now share an encrypted room with the client since the + /// previous sync + #[wasm_bindgen(getter)] pub fn changed(&self) -> Array { self.inner .changed .iter() - .map(|user| identifiers::UserId::new_with(user.clone())) + .map(|user| identifiers::UserId::from(user.clone())) .map(JsValue::from) .collect() } - /// List of users who no longer share encrypted rooms since the previous - /// sync response. + /// List of users who no longer share encrypted rooms since the + /// previous sync response. + #[wasm_bindgen(getter)] pub fn left(&self) -> Array { self.inner .left .iter() - .map(|user| identifiers::UserId::new_with(user.clone())) + .map(|user| identifiers::UserId::from(user.clone())) .map(JsValue::from) .collect() } diff --git a/bindings/matrix-sdk-crypto-js/tests/encryption.test.js b/bindings/matrix-sdk-crypto-js/tests/encryption.test.js new file mode 100644 index 000000000..75374822b --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/encryption.test.js @@ -0,0 +1,36 @@ +const { EncryptionAlgorithm, EncryptionSettings, HistoryVisibility, VerificationState } = require('../pkg/matrix_sdk_crypto'); + +describe('EncryptionAlgorithm', () => { + test('has the correct variant values', () => { + expect(EncryptionAlgorithm.OlmV1Curve25519AesSha2).toStrictEqual(0); + expect(EncryptionAlgorithm.MegolmV1AesSha2).toStrictEqual(1); + }); +}); + +describe(EncryptionSettings.name, () => { + test('can be instantiated with default values', () => { + const es = new EncryptionSettings(); + + expect(es.algorithm).toStrictEqual(EncryptionAlgorithm.MegolmV1AesSha2); + expect(es.rotationPeriod).toStrictEqual(604800000000n); + expect(es.rotationPeriodMessages).toStrictEqual(100n); + expect(es.historyVisibility).toStrictEqual(HistoryVisibility.Shared); + }); + + test('checks the history visibility values', () => { + const es = new EncryptionSettings(); + + es.historyVisibility = HistoryVisibility.Invited; + + expect(es.historyVisibility).toStrictEqual(HistoryVisibility.Invited); + expect(() => { es.historyVisibility = 42 }).toThrow(); + }); +}); + +describe('VerificationState', () => { + test('has the correct variant values', () => { + expect(VerificationState.Trusted).toStrictEqual(0); + expect(VerificationState.Untrusted).toStrictEqual(1); + expect(VerificationState.UnknownDevice).toStrictEqual(2); + }); +}); diff --git a/bindings/matrix-sdk-crypto-js/tests/events.test.js b/bindings/matrix-sdk-crypto-js/tests/events.test.js new file mode 100644 index 000000000..b478e5158 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/events.test.js @@ -0,0 +1,10 @@ +const { HistoryVisibility } = require('../pkg/matrix_sdk_crypto'); + +describe('HistoryVisibility', () => { + test('has the correct variant values', () => { + expect(HistoryVisibility.Invited).toStrictEqual(0); + expect(HistoryVisibility.Joined).toStrictEqual(1); + expect(HistoryVisibility.Shared).toStrictEqual(2); + expect(HistoryVisibility.WorldReadable).toStrictEqual(3); + }); +}); diff --git a/crates/matrix-sdk-crypto-nodejs/tests/identifiers.test.js b/bindings/matrix-sdk-crypto-js/tests/identifiers.test.js similarity index 92% rename from crates/matrix-sdk-crypto-nodejs/tests/identifiers.test.js rename to bindings/matrix-sdk-crypto-js/tests/identifiers.test.js index fd16b4aa1..3c4668f5d 100644 --- a/crates/matrix-sdk-crypto-nodejs/tests/identifiers.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/identifiers.test.js @@ -1,4 +1,4 @@ -const { UserId, DeviceId, RoomId, ServerName } = require('../'); +const { UserId, DeviceId, RoomId, ServerName } = require('../pkg/matrix_sdk_crypto'); describe(UserId.name, () => { test('cannot be invalid', () => { @@ -62,7 +62,7 @@ describe(ServerName.name, () => { }); test('port can be optional', () => { - expect(new ServerName('foo.org').port).toStrictEqual(null); + expect(new ServerName('foo.org').port).toStrictEqual(undefined); expect(new ServerName('foo.org:1234').port).toStrictEqual(1234); }); diff --git a/crates/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js similarity index 91% rename from crates/matrix-sdk-crypto-nodejs/tests/machine.test.js rename to bindings/matrix-sdk-crypto-js/tests/machine.test.js index 47cf62483..7a851dc4c 100644 --- a/crates/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -1,37 +1,16 @@ -const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../'); -const path = require('path'); -const os = require('os'); -const fs = require('fs/promises'); +const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../pkg/matrix_sdk_crypto'); describe(OlmMachine.name, () => { - test('cannot be instantiated with the constructor', () => { - expect(() => { new OlmMachine() }).toThrow(); - }); - test('can be instantiated with the async initializer', async () => { - expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine); + expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine); }); - describe('can be instantiated with a store', () => { - test('with no passphrase', async () => { - const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--')); - - expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory)).toBeInstanceOf(OlmMachine); - }); - - test('with a passphrase', async () => { - const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--')); - - expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory, 'hello')).toBeInstanceOf(OlmMachine); - }); - }); - const user = new UserId('@alice:example.org'); const device = new DeviceId('foobar'); const room = new RoomId('!baz:matrix.org'); function machine(new_user, new_device) { - return OlmMachine.initialize(new_user || user, new_device || device); + return new OlmMachine(new_user || user, new_device || device); } test('can read user ID', async () => { @@ -49,12 +28,29 @@ describe(OlmMachine.name, () => { expect(identityKeys.curve25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/); }); + test('can read display name', async () => { + expect(await machine().displayName).toBeUndefined(); + }); + + test('can read tracked users', async () => { + const trackedUsers = (await machine()).trackedUsers(); + + expect(trackedUsers).toBeInstanceOf(Set); + expect(trackedUsers.size).toStrictEqual(0); + }); + + test('can update tracked users', async () => { + const m = await machine(); + + expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined); + }); + test('can receive sync changes', async () => { const m = await machine(); const toDeviceEvents = JSON.stringify({}); const changedDevices = new DeviceLists(); - const oneTimeKeyCounts = {}; - const unusedFallbackKeys = []; + const oneTimeKeyCounts = new Map(); + const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); @@ -65,8 +61,8 @@ describe(OlmMachine.name, () => { const m = await machine(); const toDeviceEvents = JSON.stringify({}); const changedDevices = new DeviceLists(); - const oneTimeKeyCounts = {}; - const unusedFallbackKeys = []; + const oneTimeKeyCounts = new Map(); + const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); @@ -107,8 +103,8 @@ describe(OlmMachine.name, () => { const toDeviceEvents = JSON.stringify({}); const changedDevices = new DeviceLists(); - const oneTimeKeyCounts = {}; - const unusedFallbackKeys = []; + const oneTimeKeyCounts = new Map(); + const unusedFallbackKeys = new Set(); const receiveSyncChanges = await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys); outgoingRequests = await m.outgoingRequests(); @@ -342,10 +338,4 @@ describe(OlmMachine.name, () => { expect(decrypted.verificationState).toStrictEqual(VerificationState.Trusted); }); }); - - test('can update tracked users', async () => { - const m = await machine(); - - expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined); - }); }); diff --git a/bindings/matrix-sdk-crypto-js/tests/requests.test.js b/bindings/matrix-sdk-crypto-js/tests/requests.test.js new file mode 100644 index 000000000..1f806f4ae --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/requests.test.js @@ -0,0 +1,35 @@ +const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../pkg/matrix_sdk_crypto'); + +describe('RequestType', () => { + test('has the correct variant values', () => { + expect(RequestType.KeysUpload).toStrictEqual(0); + expect(RequestType.KeysQuery).toStrictEqual(1); + expect(RequestType.KeysClaim).toStrictEqual(2); + expect(RequestType.ToDevice).toStrictEqual(3); + expect(RequestType.SignatureUpload).toStrictEqual(4); + expect(RequestType.RoomMessage).toStrictEqual(5); + expect(RequestType.KeysBackup).toStrictEqual(6); + }); +}); + +for (const [request, request_type] of [ + [KeysUploadRequest, RequestType.KeysUpload], + [KeysQueryRequest, RequestType.KeysQuery], + [KeysClaimRequest, RequestType.KeysClaim], + [ToDeviceRequest, RequestType.ToDevice], + [SignatureUploadRequest, RequestType.SignatureUpload], + [RoomMessageRequest, RequestType.RoomMessage], + [KeysBackupRequest, RequestType.KeysBackup], +]) { + describe(request.name, () => { + test('can be instantiated', () => { + const r = new (request)('foo', '{"bar": "baz"}'); + + expect(r).toBeInstanceOf(request); + expect(r.id).toStrictEqual('foo'); + expect(r.body).toStrictEqual('{"bar": "baz"}'); + expect(r.type).toStrictEqual(request_type); + }); + }) + +} diff --git a/bindings/matrix-sdk-crypto-js/tests/sync_events.test.js b/bindings/matrix-sdk-crypto-js/tests/sync_events.test.js new file mode 100644 index 000000000..0322d1317 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tests/sync_events.test.js @@ -0,0 +1,31 @@ +const { DeviceLists, UserId } = require('../pkg/matrix_sdk_crypto'); + +describe(DeviceLists.name, () => { + test('can be empty', () => { + const empty = new DeviceLists(); + + expect(empty.isEmpty()).toStrictEqual(true); + expect(empty.changed).toHaveLength(0); + expect(empty.left).toHaveLength(0); + }); + + test('can be coerced empty', () => { + const empty = new DeviceLists([], []); + + expect(empty.isEmpty()).toStrictEqual(true); + expect(empty.changed).toHaveLength(0); + expect(empty.left).toHaveLength(0); + }); + + test('returns the correct `changed` and `left`', () => { + const list = new DeviceLists([new UserId('@foo:bar.org')], [new UserId('@baz:qux.org')]); + + expect(list.isEmpty()).toStrictEqual(false); + + expect(list.changed).toHaveLength(1); + expect(list.changed[0].toString()).toStrictEqual('@foo:bar.org'); + + expect(list.left).toHaveLength(1); + expect(list.left[0].toString()).toStrictEqual('@baz:qux.org'); + }); +}); diff --git a/bindings/matrix-sdk-crypto-js/tsconfig.json b/bindings/matrix-sdk-crypto-js/tsconfig.json new file mode 100644 index 000000000..0f9ea102a --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true + }, + "typedocOptions": { + "entryPoints": ["pkg/matrix_sdk_crypto.d.ts"], + "out": "docs", + "readme": "README.md", + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/.gitignore b/bindings/matrix-sdk-crypto-nodejs/.gitignore similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/.gitignore rename to bindings/matrix-sdk-crypto-nodejs/.gitignore diff --git a/crates/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml similarity index 59% rename from crates/matrix-sdk-crypto-nodejs/Cargo.toml rename to bindings/matrix-sdk-crypto-nodejs/Cargo.toml index 58242b295..ee16a92b0 100644 --- a/crates/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -25,13 +25,13 @@ docsrs = [] tracing = ["tracing-subscriber"] [dependencies] -matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto" } -matrix-sdk-common = { version = "0.5.0", path = "../matrix-sdk-common" } -matrix-sdk-sled = { version = "0.1.0", path = "../matrix-sdk-sled", default-features = false, features = ["crypto-store"] } -ruma = { version = "0.6.2", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } -vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36" } -napi = { git = "https://github.com/Hywan/napi-rs", branch = "feat-either-n-up-to-26", default-features = false, features = ["napi6", "tokio_rt"] } -napi-derive = { git = "https://github.com/Hywan/napi-rs", branch = "feat-either-n-up-to-26" } +matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" } +matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" } +matrix-sdk-sled = { version = "0.1.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" } +napi = { version = "2.6.1", default-features = false, features = ["napi6", "tokio_rt"] } +napi-derive = "2.6.0" serde_json = "1.0.79" http = "0.2.6" zeroize = "1.3.0" diff --git a/crates/matrix-sdk-crypto-nodejs/README.md b/bindings/matrix-sdk-crypto-nodejs/README.md similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/README.md rename to bindings/matrix-sdk-crypto-nodejs/README.md diff --git a/crates/matrix-sdk-crypto-nodejs/build.rs b/bindings/matrix-sdk-crypto-nodejs/build.rs similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/build.rs rename to bindings/matrix-sdk-crypto-nodejs/build.rs diff --git a/crates/matrix-sdk-crypto-nodejs/package.json b/bindings/matrix-sdk-crypto-nodejs/package.json similarity index 66% rename from crates/matrix-sdk-crypto-nodejs/package.json rename to bindings/matrix-sdk-crypto-nodejs/package.json index e83ba25e9..46b18cbcd 100644 --- a/crates/matrix-sdk-crypto-nodejs/package.json +++ b/bindings/matrix-sdk-crypto-nodejs/package.json @@ -1,5 +1,5 @@ { - "name": "matrix-sdk-crypto", + "name": "@matrix-org/matrix-sdk-crypto", "version": "0.5.0", "main": "index.js", "types": "index.d.ts", @@ -11,7 +11,7 @@ ] } }, - "license": "MIT", + "license": "Apache-2.0", "devDependencies": { "@napi-rs/cli": "^2.9.0", "jest": "^28.1.0", @@ -21,8 +21,9 @@ "node": ">= 14" }, "scripts": { - "build": "napi build --platform --release --strip", - "test": "jest --verbose", + "release-build": "napi build --platform --release --strip", + "build": "napi build --platform", + "test": "jest --verbose --testTimeout 10000", "doc": "typedoc --tsconfig ." } } diff --git a/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs b/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs new file mode 100644 index 000000000..9432c19b2 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs @@ -0,0 +1,126 @@ +use std::{ + io::{Cursor, Read}, + ops::Deref, +}; + +use napi::bindgen_prelude::Uint8Array; +use napi_derive::*; + +use crate::into_err; + +/// A type to encrypt and to decrypt anything that can fit in an +/// `Uint8Array`, usually big buffer. +#[napi] +pub struct Attachment; + +#[napi] +impl Attachment { + /// Encrypt the content of the `Uint8Array`. + /// + /// It produces an `EncryptedAttachment`, we can be used to + /// retrieve the media encryption information, or the encrypted + /// data. + #[napi] + pub fn encrypt(array: Uint8Array) -> napi::Result { + 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 { + let media_encryption_info = match attachment.media_encryption_info.take() { + Some(media_encryption_info) => media_encryption_info, + None => { + return Err(napi::Error::from_reason( + "The media encryption info are absent from the given encrypted attachment" + .to_string(), + )) + } + }; + + let encrypted_data: &[u8] = attachment.encrypted_data.deref(); + + let mut cursor = Cursor::new(encrypted_data); + let mut decryptor = + matrix_sdk_crypto::AttachmentDecryptor::new(&mut cursor, media_encryption_info) + .map_err(into_err)?; + + let mut decrypted_data = Vec::new(); + decryptor.read_to_end(&mut decrypted_data).map_err(into_err)?; + + Ok(Uint8Array::new(decrypted_data)) + } +} + +/// An encrypted attachment, usually created from `Attachment.encrypt`. +#[napi] +pub struct EncryptedAttachment { + media_encryption_info: Option, + + /// 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 { + 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 { + 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() + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/src/encryption.rs b/bindings/matrix-sdk-crypto-nodejs/src/encryption.rs similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/src/encryption.rs rename to bindings/matrix-sdk-crypto-nodejs/src/encryption.rs diff --git a/crates/matrix-sdk-crypto-nodejs/src/errors.rs b/bindings/matrix-sdk-crypto-nodejs/src/errors.rs similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/src/errors.rs rename to bindings/matrix-sdk-crypto-nodejs/src/errors.rs diff --git a/crates/matrix-sdk-crypto-nodejs/src/events.rs b/bindings/matrix-sdk-crypto-nodejs/src/events.rs similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/src/events.rs rename to bindings/matrix-sdk-crypto-nodejs/src/events.rs diff --git a/crates/matrix-sdk-crypto-nodejs/src/identifiers.rs b/bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs similarity index 62% rename from crates/matrix-sdk-crypto-nodejs/src/identifiers.rs rename to bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs index 397664e6a..eb45873b8 100644 --- a/crates/matrix-sdk-crypto-nodejs/src/identifiers.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/identifiers.rs @@ -1,6 +1,7 @@ //! Types for [Matrix](https://matrix.org/) identifiers for devices, //! events, keys, rooms, servers, users and URIs. +use napi::bindgen_prelude::ToNapiValue; use napi_derive::*; use crate::into_err; @@ -58,11 +59,7 @@ impl UserId { } } -pub(crate) fn lower_user_ids_to_ruma(users: Vec<&UserId>) -> impl Iterator { - users.into_iter().map(|user| user.inner.as_ref()) -} - -/// A Matrix key ID. +/// A Matrix device ID. /// /// Device identifiers in Matrix are completely opaque character /// sequences. This type is provided simply for its semantic value. @@ -94,6 +91,109 @@ impl DeviceId { } } +/// A Matrix device key ID. +/// +/// A key algorithm and a device ID, combined with a β€˜:’. +#[napi] +#[derive(Debug, Clone)] +pub struct DeviceKeyId { + pub(crate) inner: ruma::OwnedDeviceKeyId, +} + +impl From for DeviceKeyId { + fn from(inner: ruma::OwnedDeviceKeyId) -> Self { + Self { inner } + } +} + +#[napi] +impl DeviceKeyId { + /// Parse/validate and create a new `DeviceKeyId`. + #[napi(constructor)] + pub fn new(id: String) -> napi::Result { + 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 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 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 diff --git a/crates/matrix-sdk-crypto-nodejs/src/lib.rs b/bindings/matrix-sdk-crypto-nodejs/src/lib.rs similarity index 93% rename from crates/matrix-sdk-crypto-nodejs/src/lib.rs rename to bindings/matrix-sdk-crypto-nodejs/src/lib.rs index 77f541470..26e165fd4 100644 --- a/crates/matrix-sdk-crypto-nodejs/src/lib.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/lib.rs @@ -16,15 +16,19 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] //#![warn(missing_docs, missing_debug_implementations)] +pub mod attachment; pub mod encryption; mod errors; pub mod events; pub mod identifiers; pub mod machine; +pub mod olm; pub mod requests; pub mod responses; pub mod sync_events; #[cfg(feature = "tracing")] pub mod tracing; +pub mod types; +pub mod vodozemac; use crate::errors::into_err; diff --git a/crates/matrix-sdk-crypto-nodejs/src/machine.rs b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs similarity index 82% rename from crates/matrix-sdk-crypto-nodejs/src/machine.rs rename to bindings/matrix-sdk-crypto-nodejs/src/machine.rs index 6d66809f8..9128f2f4f 100644 --- a/crates/matrix-sdk-crypto-nodejs/src/machine.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs @@ -15,8 +15,8 @@ use serde_json::Value as JsonValue; use zeroize::Zeroize; use crate::{ - encryption, identifiers, into_err, requests, responses, responses::response_from_string, - sync_events, + encryption, identifiers, into_err, olm, requests, responses, responses::response_from_string, + sync_events, types, vodozemac, }; /// State machine implementation of the Olm/Megolm encryption protocol @@ -63,6 +63,9 @@ impl OlmMachine { store_path: Option, mut store_passphrase: Option, ) -> napi::Result { + let user_id = user_id.clone(); + let device_id = device_id.clone(); + let store = store_path .map(|store_path| { matrix_sdk_sled::CryptoStore::open_with_passphrase( @@ -121,7 +124,7 @@ impl OlmMachine { /// Get the public parts of our Olm identity keys. #[napi(getter)] - pub fn identity_keys(&self) -> IdentityKeys { + pub fn identity_keys(&self) -> vodozemac::IdentityKeys { self.inner.identity_keys().into() } @@ -150,7 +153,7 @@ impl OlmMachine { unused_fallback_keys: Vec, ) -> napi::Result { let to_device_events = serde_json::from_str(to_device_events.as_ref()).map_err(into_err)?; - let changed_devices = &changed_devices.inner; + let changed_devices = changed_devices.inner.clone(); let one_time_key_counts = one_time_key_counts .iter() .map(|(key, value)| (DeviceKeyAlgorithm::from(key.as_str()), UInt::from(*value))) @@ -167,7 +170,7 @@ impl OlmMachine { .inner .receive_sync_changes( to_device_events, - changed_devices, + &changed_devices, &one_time_key_counts, unused_fallback_keys.as_deref(), ) @@ -212,8 +215,7 @@ impl OlmMachine { .into_iter() .map(requests::OutgoingRequest) .map(TryFrom::try_from) - .collect::, _>>() - .map_err(into_err) + .collect() } /// Mark the request with the given request ID as sent. @@ -275,9 +277,15 @@ impl OlmMachine { &self, users: Option>, ) -> napi::Result> { + let users = users + .unwrap_or_default() + .into_iter() + .map(|user| user.inner.clone()) + .collect::>(); + match self .inner - .get_missing_sessions(identifiers::lower_user_ids_to_ruma(users.unwrap_or_default())) + .get_missing_sessions(users.iter().map(AsRef::as_ref)) .await .map_err(into_err)? { @@ -306,7 +314,9 @@ impl OlmMachine { /// * `users`, an array over user IDs that should be marked for tracking. #[napi] pub async fn update_tracked_users(&self, users: Vec<&identifiers::UserId>) { - self.inner.update_tracked_users(identifiers::lower_user_ids_to_ruma(users)).await; + let users = users.into_iter().map(|user| user.inner.clone()).collect::>(); + + self.inner.update_tracked_users(users.iter().map(AsRef::as_ref)).await; } /// Get to-device requests to share a room key with users in a room. @@ -323,15 +333,15 @@ impl OlmMachine { users: Vec<&identifiers::UserId>, encryption_settings: &encryption::EncryptionSettings, ) -> napi::Result { - let room_id = room_id.inner.as_ref(); - let users = identifiers::lower_user_ids_to_ruma(users); + let room_id = room_id.inner.clone(); + let users = users.into_iter().map(|user| user.inner.clone()).collect::>(); let encryption_settings = matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings); serde_json::to_string( &self .inner - .share_room_key(room_id, users, encryption_settings) + .share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings) .await .map_err(into_err)?, ) @@ -354,13 +364,13 @@ impl OlmMachine { event_type: String, content: String, ) -> napi::Result { - let room_id = room_id.inner.as_ref(); + let room_id = room_id.inner.clone(); let content: JsonValue = serde_json::from_str(content.as_str()).map_err(into_err)?; serde_json::to_string( &self .inner - .encrypt_room_event_raw(room_id, content, event_type.as_ref()) + .encrypt_room_event_raw(&room_id, content, event_type.as_ref()) .await .map_err(into_err)?, ) @@ -381,86 +391,26 @@ impl OlmMachine { ) -> napi::Result { let event: OriginalSyncRoomEncryptedEvent = serde_json::from_str(event.as_str()).map_err(into_err)?; - let room_id = room_id.inner.as_ref(); - let room_event = self.inner.decrypt_room_event(&event, room_id).await.map_err(into_err)?; + let room_id = room_id.inner.clone(); + + let room_event = self.inner.decrypt_room_event(&event, &room_id).await.map_err(into_err)?; Ok(room_event.into()) } -} -/// An Ed25519 public key, used to verify digital signatures. -#[napi] -#[derive(Clone)] -pub struct Ed25519PublicKey { - inner: vodozemac::Ed25519PublicKey, -} - -#[napi] -impl Ed25519PublicKey { - /// The number of bytes an Ed25519 public key has. - #[napi(getter)] - pub fn length(&self) -> u32 { - vodozemac::Ed25519PublicKey::LENGTH as u32 - } - - /// Serialize an Ed25519 public key to an unpadded base64 - /// representation. + /// Get the status of the private cross signing keys. + /// + /// This can be used to check which private cross signing keys we + /// have stored locally. #[napi] - pub fn to_base64(&self) -> String { - self.inner.to_base64() - } -} - -/// A Curve25519 public key. -#[napi] -#[derive(Clone)] -pub struct Curve25519PublicKey { - inner: vodozemac::Curve25519PublicKey, -} - -#[napi] -impl Curve25519PublicKey { - /// The number of bytes a Curve25519 public key has. - #[napi(getter)] - pub fn length(&self) -> u32 { - vodozemac::Curve25519PublicKey::LENGTH as u32 + pub async fn cross_signing_status(&self) -> olm::CrossSigningStatus { + self.inner.cross_signing_status().await.into() } - /// Serialize an Curve25519 public key to an unpadded base64 - /// representation. + /// Sign the given message using our device key and if available + /// cross-signing master key. #[napi] - pub fn to_base64(&self) -> String { - self.inner.to_base64() - } -} - -/// Struct holding the two public identity keys of an account. -#[napi] -pub struct IdentityKeys { - ed25519: Ed25519PublicKey, - curve25519: Curve25519PublicKey, -} - -#[napi] -impl IdentityKeys { - /// The Ed25519 public key, used for signing. - #[napi(getter)] - pub fn ed25519(&self) -> Ed25519PublicKey { - self.ed25519.clone() - } - - /// The Curve25519 public key, used for establish shared secrets. - #[napi(getter)] - pub fn curve25519(&self) -> Curve25519PublicKey { - self.curve25519.clone() - } -} - -impl From for IdentityKeys { - fn from(value: matrix_sdk_crypto::olm::IdentityKeys) -> Self { - Self { - ed25519: Ed25519PublicKey { inner: value.ed25519 }, - curve25519: Curve25519PublicKey { inner: value.curve25519 }, - } + pub async fn sign(&self, message: String) -> types::Signatures { + self.inner.sign(message.as_str()).await.into() } } diff --git a/bindings/matrix-sdk-crypto-nodejs/src/olm.rs b/bindings/matrix-sdk-crypto-nodejs/src/olm.rs new file mode 100644 index 000000000..c45c79c29 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/src/olm.rs @@ -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 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 + } +} diff --git a/crates/matrix-sdk-crypto-nodejs/src/requests.rs b/bindings/matrix-sdk-crypto-nodejs/src/requests.rs similarity index 89% rename from crates/matrix-sdk-crypto-nodejs/src/requests.rs rename to bindings/matrix-sdk-crypto-nodejs/src/requests.rs index 6ebcae1f6..9d1e6376d 100644 --- a/crates/matrix-sdk-crypto-nodejs/src/requests.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/requests.rs @@ -1,5 +1,7 @@ //! Types to handle requests. +use std::time::Duration; + use matrix_sdk_crypto::requests::{ KeysBackupRequest as RumaKeysBackupRequest, KeysQueryRequest as RumaKeysQueryRequest, RoomMessageRequest as RumaRoomMessageRequest, ToDeviceRequest as RumaToDeviceRequest, @@ -12,6 +14,8 @@ use ruma::api::client::keys::{ upload_signatures::v3::Request as RumaSignatureUploadRequest, }; +use crate::into_err; + /// Data for a request to the `/keys/upload` API endpoint /// ([specification]). /// @@ -27,7 +31,7 @@ pub struct KeysUploadRequest { /// A JSON-encoded object of form: /// /// ```json - /// {"device_keys": …, "one_time_keys": …} + /// {"device_keys": …, "one_time_keys": …, "fallback_keys": …} /// ``` #[napi(readonly)] pub body: String, @@ -56,7 +60,7 @@ pub struct KeysQueryRequest { /// A JSON-encoded object of form: /// - /// ``` + /// ```json /// {"timeout": …, "device_keys": …, "token": …} /// ``` #[napi(readonly)] @@ -87,7 +91,7 @@ pub struct KeysClaimRequest { /// A JSON-encoded object of form: /// - /// ``` + /// ```json /// {"timeout": …, "one_time_keys": …} /// ``` #[napi(readonly)] @@ -117,7 +121,7 @@ pub struct ToDeviceRequest { /// A JSON-encoded object of form: /// - /// ``` + /// ```json /// {"event_type": …, "txn_id": …, "messages": …} /// ``` #[napi(readonly)] @@ -147,7 +151,7 @@ pub struct SignatureUploadRequest { /// A JSON-encoded object of form: /// - /// ``` + /// ```json /// {"signed_keys": …, "txn_id": …, "messages": …} /// ``` #[napi(readonly)] @@ -175,7 +179,7 @@ pub struct RoomMessageRequest { /// A JSON-encoded object of form: /// - /// ``` + /// ```json /// {"room_id": …, "txn_id": …, "content": …} /// ``` #[napi(readonly)] @@ -203,7 +207,7 @@ pub struct KeysBackupRequest { /// A JSON-encoded object of form: /// - /// ``` + /// ```json /// {"rooms": …} /// ``` #[napi(readonly)] @@ -220,31 +224,39 @@ impl KeysBackupRequest { } macro_rules! request { - ($request:ident from $ruma_request:ident maps fields $( $field:ident ),+ $(,)? ) => { + ($request:ident from $ruma_request:ident maps fields $( $field:ident $( { $transformation:expr } )? ),+ $(,)? ) => { impl TryFrom<(String, &$ruma_request)> for $request { - type Error = serde_json::Error; + type Error = napi::Error; fn try_from( (request_id, request): (String, &$ruma_request), ) -> Result { let mut map = serde_json::Map::new(); $( - map.insert(stringify!($field).to_owned(), serde_json::to_value(&request.$field)?); + let field = &request.$field; + $( + let field = { + let $field = field; + + $transformation + }; + )? + map.insert(stringify!($field).to_owned(), serde_json::to_value(field).map_err(into_err)?); )+ let value = serde_json::Value::Object(map); Ok($request { id: request_id, - body: serde_json::to_string(&value)?.into(), + body: serde_json::to_string(&value).map_err(into_err)?.into(), }) } } }; } -request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys); -request!(KeysQueryRequest from RumaKeysQueryRequest maps fields timeout, device_keys, token); -request!(KeysClaimRequest from RumaKeysClaimRequest maps fields timeout, one_time_keys); +request!(KeysUploadRequest from RumaKeysUploadRequest maps fields device_keys, one_time_keys, fallback_keys); +request!(KeysQueryRequest from RumaKeysQueryRequest maps fields timeout { timeout.as_ref().map(Duration::as_millis).map(u64::try_from).transpose().map_err(into_err)? }, device_keys, token); +request!(KeysClaimRequest from RumaKeysClaimRequest maps fields timeout { timeout.as_ref().map(Duration::as_millis).map(u64::try_from).transpose().map_err(into_err)? }, one_time_keys); request!(ToDeviceRequest from RumaToDeviceRequest maps fields event_type, txn_id, messages); request!(SignatureUploadRequest from RumaSignatureUploadRequest maps fields signed_keys); request!(RoomMessageRequest from RumaRoomMessageRequest maps fields room_id, txn_id, content); @@ -263,7 +275,7 @@ pub type OutgoingRequests = Either7< pub(crate) struct OutgoingRequest(pub(crate) matrix_sdk_crypto::OutgoingRequest); impl TryFrom for OutgoingRequests { - type Error = serde_json::Error; + type Error = napi::Error; fn try_from(outgoing_request: OutgoingRequest) -> Result { let request_id = outgoing_request.0.request_id().to_string(); diff --git a/crates/matrix-sdk-crypto-nodejs/src/responses.rs b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs similarity index 98% rename from crates/matrix-sdk-crypto-nodejs/src/responses.rs rename to bindings/matrix-sdk-crypto-nodejs/src/responses.rs index 7d8b54101..3797ddf1a 100644 --- a/crates/matrix-sdk-crypto-nodejs/src/responses.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use matrix_sdk_common::deserialized_responses::{AlgorithmInfo, EncryptionInfo}; use matrix_sdk_crypto::IncomingResponse; use napi_derive::*; @@ -190,7 +192,7 @@ impl DecryptedRoomEvent { /// verified or deleted. #[napi(getter)] pub fn verification_state(&self) -> Option { - Some((&self.encryption_info.as_ref()?.verification_state).into()) + Some(self.encryption_info.as_ref()?.verification_state.borrow().into()) } } diff --git a/crates/matrix-sdk-crypto-nodejs/src/sync_events.rs b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/src/sync_events.rs rename to bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs diff --git a/crates/matrix-sdk-crypto-nodejs/src/tracing.rs b/bindings/matrix-sdk-crypto-nodejs/src/tracing.rs similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/src/tracing.rs rename to bindings/matrix-sdk-crypto-nodejs/src/tracing.rs diff --git a/bindings/matrix-sdk-crypto-nodejs/src/types.rs b/bindings/matrix-sdk-crypto-nodejs/src/types.rs new file mode 100644 index 000000000..ca68adab4 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/src/types.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; + +use napi_derive::*; + +use crate::{ + identifiers::{DeviceKeyId, UserId}, + vodozemac::Ed25519Signature, +}; + +#[napi] +#[derive(Default)] +pub struct Signatures { + inner: matrix_sdk_crypto::types::Signatures, +} + +impl From for Signatures { + fn from(inner: matrix_sdk_crypto::types::Signatures) -> Self { + Self { inner } + } +} + +#[napi] +impl Signatures { + /// Creates a new, empty, signatures collection. + #[napi(constructor)] + pub fn new() -> Self { + matrix_sdk_crypto::types::Signatures::new().into() + } + + /// Add the given signature from the given signer and the given key ID to + /// the collection. + #[napi] + pub fn add_signature( + &mut self, + signer: &UserId, + key_id: &DeviceKeyId, + signature: &Ed25519Signature, + ) -> Option { + self.inner + .add_signature(signer.inner.clone(), key_id.inner.clone(), signature.inner) + .map(Into::into) + } + + /// Try to find an Ed25519 signature from the given signer with + /// the given key ID. + #[napi] + pub fn get_signature(&self, signer: &UserId, key_id: &DeviceKeyId) -> Option { + self.inner.get_signature(signer.inner.as_ref(), key_id.inner.as_ref()).map(Into::into) + } + + /// Get the map of signatures that belong to the given user. + #[napi] + pub fn get(&self, signer: &UserId) -> Option> { + self.inner.get(signer.inner.as_ref()).map(|map| { + map.iter() + .map(|(device_key_id, maybe_signature)| { + (device_key_id.as_str().to_owned(), maybe_signature.clone().into()) + }) + .collect() + }) + } + + /// Remove all the signatures we currently hold. + #[napi] + pub fn clear(&mut self) { + self.inner.clear(); + } + + /// Do we hold any signatures or is our collection completely + /// empty. + #[napi(getter)] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// How many signatures do we currently hold. + #[napi(getter)] + pub fn count(&self) -> usize { + self.inner.signature_count() + } +} + +/// Represents a potentially decoded signature (but not a validated +/// one). +#[napi] +pub struct Signature { + inner: matrix_sdk_crypto::types::Signature, +} + +impl From for Signature { + fn from(inner: matrix_sdk_crypto::types::Signature) -> Self { + Self { inner } + } +} + +#[napi] +impl Signature { + /// Get the Ed25519 signature, if this is one. + #[napi(getter)] + pub fn ed25519(&self) -> Option { + self.inner.ed25519().map(Into::into) + } + + /// Convert the signature to a base64 encoded string. + #[napi] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +type MaybeSignatureInner = + Result; + +/// Represents a signature that is either valid _or_ that could not be +/// decoded. +#[napi] +pub struct MaybeSignature { + inner: MaybeSignatureInner, +} + +impl From for MaybeSignature { + fn from(inner: MaybeSignatureInner) -> Self { + Self { inner } + } +} + +#[napi] +impl MaybeSignature { + /// Check whether the signature has been successfully decoded. + #[napi(getter)] + pub fn is_valid(&self) -> bool { + self.inner.is_ok() + } + + /// Check whether the signature could not be successfully decoded. + #[napi(getter)] + pub fn is_invalid(&self) -> bool { + self.inner.is_err() + } + + /// The signature, if successfully decoded. + #[napi(getter)] + pub fn signature(&self) -> Option { + self.inner.as_ref().cloned().map(Into::into).ok() + } + + /// The base64 encoded string that is claimed to contain a + /// signature but could not be decoded, if any. + #[napi(getter)] + pub fn invalid_signature_source(&self) -> Option { + match &self.inner { + Ok(_) => None, + Err(signature) => Some(signature.source.clone()), + } + } +} diff --git a/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs b/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs new file mode 100644 index 000000000..eacc15196 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/src/vodozemac.rs @@ -0,0 +1,113 @@ +use napi_derive::*; + +use crate::into_err; + +/// An Ed25519 public key, used to verify digital signatures. +#[napi] +#[derive(Clone)] +pub struct Ed25519PublicKey { + inner: vodozemac::Ed25519PublicKey, +} + +#[napi] +impl Ed25519PublicKey { + /// The number of bytes an Ed25519 public key has. + #[napi(getter)] + pub fn length(&self) -> u32 { + vodozemac::Ed25519PublicKey::LENGTH as u32 + } + + /// Serialize an Ed25519 public key to an unpadded base64 + /// representation. + #[napi] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +/// An Ed25519 digital signature, can be used to verify the +/// authenticity of a message. +#[napi] +pub struct Ed25519Signature { + pub(crate) inner: vodozemac::Ed25519Signature, +} + +impl From for Ed25519Signature { + fn from(inner: vodozemac::Ed25519Signature) -> Self { + Self { inner } + } +} + +#[napi] +impl Ed25519Signature { + /// Try to create an Ed25519 signature from an unpadded base64 + /// representation. + #[napi(constructor)] + pub fn new(signature: String) -> napi::Result { + Ok(Self { + inner: vodozemac::Ed25519Signature::from_base64(signature.as_str()) + .map_err(into_err)?, + }) + } + + /// Serialize a Ed25519 signature to an unpadded base64 + /// representation. + #[napi] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +/// A Curve25519 public key. +#[napi] +#[derive(Clone)] +pub struct Curve25519PublicKey { + inner: vodozemac::Curve25519PublicKey, +} + +#[napi] +impl Curve25519PublicKey { + /// The number of bytes a Curve25519 public key has. + #[napi(getter)] + pub fn length(&self) -> u32 { + vodozemac::Curve25519PublicKey::LENGTH as u32 + } + + /// Serialize an Curve25519 public key to an unpadded base64 + /// representation. + #[napi] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +/// Struct holding the two public identity keys of an account. +#[napi] +pub struct IdentityKeys { + ed25519: Ed25519PublicKey, + curve25519: Curve25519PublicKey, +} + +#[napi] +impl IdentityKeys { + /// The Ed25519 public key, used for signing. + #[napi(getter)] + pub fn ed25519(&self) -> Ed25519PublicKey { + self.ed25519.clone() + } + + /// The Curve25519 public key, used for establish shared secrets. + #[napi(getter)] + pub fn curve25519(&self) -> Curve25519PublicKey { + self.curve25519.clone() + } +} + +impl From for IdentityKeys { + fn from(value: matrix_sdk_crypto::olm::IdentityKeys) -> Self { + Self { + ed25519: Ed25519PublicKey { inner: value.ed25519 }, + curve25519: Curve25519PublicKey { inner: value.curve25519 }, + } + } +} diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js new file mode 100644 index 000000000..86e3eaf8b --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js @@ -0,0 +1,77 @@ +const { Attachment, EncryptedAttachment } = require('../'); + +describe(Attachment.name, () => { + const originalData = 'hello'; + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); + + let encryptedAttachment; + + test('can encrypt data', () => { + encryptedAttachment = Attachment.encrypt(textEncoder.encode(originalData)); + + const mediaEncryptionInfo = JSON.parse(encryptedAttachment.mediaEncryptionInfo); + + expect(mediaEncryptionInfo).toMatchObject({ + v: 'v2', + key: { + kty: expect.any(String), + key_ops: expect.arrayContaining(['encrypt', 'decrypt']), + alg: expect.any(String), + k: expect.any(String), + ext: expect.any(Boolean), + }, + iv: expect.stringMatching(/^[A-Za-z0-9\+/]+$/), + hashes: { + sha256: expect.stringMatching(/^[A-Za-z0-9\+/]+$/) + } + }); + + const encryptedData = encryptedAttachment.encryptedData; + expect(encryptedData.every((i) => { i != 0 })).toStrictEqual(false); + }); + + test('can decrypt data', () => { + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false); + + const decryptedAttachment = Attachment.decrypt(encryptedAttachment); + + expect(textDecoder.decode(decryptedAttachment)).toStrictEqual(originalData); + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true); + }); + + test('can only decrypt once', () => { + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true); + + expect(() => { textDecoder.decode(decryptedAttachment) }).toThrow() + }); +}); + +describe(EncryptedAttachment.name, () => { + const originalData = 'hello'; + const textDecoder = new TextDecoder(); + + test('can be created manually', () => { + const encryptedAttachment = new EncryptedAttachment( + new Uint8Array([24, 150, 67, 37, 144]), + JSON.stringify({ + v: 'v2', + key: { + kty: 'oct', + key_ops: [ 'encrypt', 'decrypt' ], + alg: 'A256CTR', + k: 'QbNXUjuukFyEJ8cQZjJuzN6mMokg0HJIjx0wVMLf5BM', + ext: true + }, + iv: 'xk2AcWkomiYAAAAAAAAAAA', + hashes: { + sha256: 'JsRbDXgOja4xvDiF3DwBuLHdxUzIrVYIuj7W/t3aEok' + } + }) + ); + + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false); + expect(textDecoder.decode(Attachment.decrypt(encryptedAttachment))).toStrictEqual(originalData); + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true); + }); +}); diff --git a/crates/matrix-sdk-crypto-nodejs/tests/encryption.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/encryption.test.js similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/tests/encryption.test.js rename to bindings/matrix-sdk-crypto-nodejs/tests/encryption.test.js diff --git a/crates/matrix-sdk-crypto-nodejs/tests/events.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/events.test.js similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/tests/events.test.js rename to bindings/matrix-sdk-crypto-nodejs/tests/events.test.js diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/identifiers.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/identifiers.test.js new file mode 100644 index 000000000..ef8a17943 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/tests/identifiers.test.js @@ -0,0 +1,118 @@ +const { UserId, DeviceId, DeviceKeyId, DeviceKeyAlgorithm, DeviceKeyAlgorithmName, RoomId, ServerName } = require('../'); + +describe(UserId.name, () => { + test('cannot be invalid', () => { + expect(() => { new UserId('@foobar') }).toThrow(); + }); + + const user = new UserId('@foo:bar.org'); + + test('localpart is present', () => { + expect(user.localpart).toStrictEqual('foo'); + }); + + test('server name is present', () => { + expect(user.serverName).toBeInstanceOf(ServerName); + }); + + test('user ID is not historical', () => { + expect(user.isHistorical()).toStrictEqual(false); + }); + + test('can read the user ID as a string', () => { + expect(user.toString()).toStrictEqual('@foo:bar.org'); + }) +}); + +describe(DeviceId.name, () => { + const device = new DeviceId('foo'); + + test('can read the device ID as a string', () => { + expect(device.toString()).toStrictEqual('foo'); + }) +}); + +describe(DeviceKeyId.name, () => { + for (const deviceKey of [ + { name: 'ed25519', + id: 'ed25519:foobar', + algorithmName: DeviceKeyAlgorithmName.Ed25519, + algorithm: 'ed25519', + deviceId: 'foobar' }, + + { name: 'curve25519', + id: 'curve25519:foobar', + algorithmName: DeviceKeyAlgorithmName.Curve25519, + algorithm: 'curve25519', + deviceId: 'foobar' }, + + { name: 'signed curve25519', + id: 'signed_curve25519:foobar', + algorithmName: DeviceKeyAlgorithmName.SignedCurve25519, + algorithm: 'signed_curve25519', + deviceId: 'foobar' }, + + { name: 'unknown', + id: 'hello:foobar', + algorithmName: DeviceKeyAlgorithmName.Unknown, + algorithm: 'hello', + deviceId: 'foobar' }, + ]) { + test(`${deviceKey.name} algorithm`, () => { + const dk = new DeviceKeyId(deviceKey.id); + + expect(dk.algorithm.name).toStrictEqual(deviceKey.algorithmName); + expect(dk.algorithm.toString()).toStrictEqual(deviceKey.algorithm); + expect(dk.deviceId.toString()).toStrictEqual(deviceKey.deviceId); + expect(dk.toString()).toStrictEqual(deviceKey.id); + }); + } +}); + +describe('DeviceKeyAlgorithmName', () => { + test('has the correct variants', () => { + expect(DeviceKeyAlgorithmName.Ed25519).toStrictEqual(0); + expect(DeviceKeyAlgorithmName.Curve25519).toStrictEqual(1); + expect(DeviceKeyAlgorithmName.SignedCurve25519).toStrictEqual(2); + expect(DeviceKeyAlgorithmName.Unknown).toStrictEqual(3); + }); +}); + +describe(RoomId.name, () => { + test('cannot be invalid', () => { + expect(() => { new RoomId('!foo') }).toThrow(); + }); + + const room = new RoomId('!foo:bar.org'); + + test('localpart is present', () => { + expect(room.localpart).toStrictEqual('foo'); + }); + + test('server name is present', () => { + expect(room.serverName).toBeInstanceOf(ServerName); + }); + + test('can read the room ID as string', () => { + expect(room.toString()).toStrictEqual('!foo:bar.org'); + }); +}); + +describe(ServerName.name, () => { + test('cannot be invalid', () => { + expect(() => { new ServerName('@foobar') }).toThrow() + }); + + test('host is present', () => { + expect(new ServerName('foo.org').host).toStrictEqual('foo.org'); + }); + + test('port can be optional', () => { + expect(new ServerName('foo.org').port).toStrictEqual(null); + expect(new ServerName('foo.org:1234').port).toStrictEqual(1234); + }); + + test('server is not an IP literal', () => { + expect(new ServerName('foo.org').isIpLiteral()).toStrictEqual(false); + }); +}); diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js new file mode 100644 index 000000000..020486bd2 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -0,0 +1,400 @@ +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState, CrossSigningStatus, MaybeSignature } = require('../'); +const path = require('path'); +const os = require('os'); +const fs = require('fs/promises'); + +describe(OlmMachine.name, () => { + test('cannot be instantiated with the constructor', () => { + expect(() => { new OlmMachine() }).toThrow(); + }); + + test('can be instantiated with the async initializer', async () => { + expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine); + }); + + describe('can be instantiated with a store', () => { + test('with no passphrase', async () => { + const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--')); + + expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory)).toBeInstanceOf(OlmMachine); + }); + + test('with a passphrase', async () => { + const temp_directory = await fs.mkdtemp(path.join(os.tmpdir(), 'matrix-sdk-crypto--')); + + expect(await OlmMachine.initialize(new UserId('@foo:bar.org'), new DeviceId('baz'), temp_directory, 'hello')).toBeInstanceOf(OlmMachine); + }); + }); + + const user = new UserId('@alice:example.org'); + const device = new DeviceId('foobar'); + const room = new RoomId('!baz:matrix.org'); + + function machine(new_user, new_device) { + return OlmMachine.initialize(new_user || user, new_device || device); + } + + test('can read user ID', async () => { + expect((await machine()).userId.toString()).toStrictEqual(user.toString()); + }); + + test('can read device ID', async () => { + expect((await machine()).deviceId.toString()).toStrictEqual(device.toString()); + }); + + test('can read identity keys', async () => { + const identityKeys = (await machine()).identityKeys; + + expect(identityKeys.ed25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/); + expect(identityKeys.curve25519.toBase64()).toMatch(/^[A-Za-z0-9+/]+$/); + }); + + test('can receive sync changes', async () => { + const m = await machine(); + const toDeviceEvents = JSON.stringify({}); + const changedDevices = new DeviceLists(); + const oneTimeKeyCounts = {}; + const unusedFallbackKeys = []; + + const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); + + expect(receiveSyncChanges).toEqual({}); + }); + + test('can get the outgoing requests that need to be send out', async () => { + const m = await machine(); + const toDeviceEvents = JSON.stringify({}); + const changedDevices = new DeviceLists(); + const oneTimeKeyCounts = {}; + const unusedFallbackKeys = []; + + const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); + + expect(receiveSyncChanges).toEqual({}); + + const outgoingRequests = await m.outgoingRequests(); + + expect(outgoingRequests).toHaveLength(2); + + { + expect(outgoingRequests[0]).toBeInstanceOf(KeysUploadRequest); + expect(outgoingRequests[0].id).toBeDefined(); + expect(outgoingRequests[0].type).toStrictEqual(RequestType.KeysUpload); + + const body = JSON.parse(outgoingRequests[0].body); + expect(body.device_keys).toBeDefined(); + expect(body.one_time_keys).toBeDefined(); + } + + { + expect(outgoingRequests[1]).toBeInstanceOf(KeysQueryRequest); + expect(outgoingRequests[1].id).toBeDefined(); + expect(outgoingRequests[1].type).toStrictEqual(RequestType.KeysQuery); + + const body = JSON.parse(outgoingRequests[1].body); + expect(body.timeout).toBeDefined(); + expect(body.device_keys).toBeDefined(); + expect(body.token).toBeDefined(); + } + }); + + describe('setup workflow to mark requests as sent', () => { + let m; + let ougoingRequests; + + beforeAll(async () => { + m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID')); + + const toDeviceEvents = JSON.stringify({}); + const changedDevices = new DeviceLists(); + const oneTimeKeyCounts = {}; + const unusedFallbackKeys = []; + + const receiveSyncChanges = await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys); + outgoingRequests = await m.outgoingRequests(); + + expect(outgoingRequests).toHaveLength(2); + }); + + test('can mark requests as sent', async () => { + { + const request = outgoingRequests[0]; + expect(request).toBeInstanceOf(KeysUploadRequest); + + // https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysupload + const hypothetical_response = JSON.stringify({ + "one_time_key_counts": { + "curve25519": 10, + "signed_curve25519": 20 + } + }); + const marked = await m.markRequestAsSent(request.id, request.type, hypothetical_response); + expect(marked).toStrictEqual(true); + } + + { + const request = outgoingRequests[1]; + expect(request).toBeInstanceOf(KeysQueryRequest); + + // https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3keysquery + const hypothetical_response = JSON.stringify({ + "device_keys": { + "@alice:example.org": { + "JLAFKJWSCS": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "JLAFKJWSCS", + "keys": { + "curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4", + "ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM" + }, + "signatures": { + "@alice:example.org": { + "ed25519:JLAFKJWSCS": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA" + } + }, + "unsigned": { + "device_display_name": "Alice's mobile phone" + }, + "user_id": "@alice:example.org" + } + } + }, + "failures": {} + }); + const marked = await m.markRequestAsSent(request.id, request.type, hypothetical_response); + expect(marked).toStrictEqual(true); + } + }); + }); + + describe('setup workflow to encrypt/decrypt events', () => { + let m; + const user = new UserId('@alice:example.org'); + const device = new DeviceId('JLAFKJWSCS'); + const room = new RoomId('!test:localhost'); + + beforeAll(async () => { + m = await machine(user, device); + }); + + test('can pass keysquery and keysclaim requests directly', async () => { + { + // derived from https://github.com/matrix-org/matrix-rust-sdk/blob/7f49618d350fab66b7e1dc4eaf64ec25ceafd658/benchmarks/benches/crypto_bench/keys_query.json + const hypothetical_response = JSON.stringify({ + "device_keys": { + "@example:localhost": { + "AFGUOBTZWM": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "AFGUOBTZWM", + "keys": { + "curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo", + "ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec" + }, + "signatures": { + "@example:localhost": { + "ed25519:AFGUOBTZWM": "RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA" + } + }, + "user_id": "@example:localhost", + "unsigned": { + "device_display_name": "rust-sdk" + } + }, + } + }, + "failures": {}, + "master_keys": { + "@example:localhost": { + "user_id": "@example:localhost", + "usage": [ + "master" + ], + "keys": { + "ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU": "n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU" + }, + "signatures": { + "@example:localhost": { + "ed25519:TCSJXPWGVS": "+j9G3L41I1fe0++wwusTTQvbboYW0yDtRWUEujhwZz4MAltjLSfJvY0hxhnz+wHHmuEXvQDen39XOpr1p29sAg" + } + } + } + }, + "self_signing_keys": { + "@example:localhost": { + "user_id": "@example:localhost", + "usage": [ + "self_signing" + ], + "keys": { + "ed25519:kQXOuy639Yt47mvNTdrIluoC6DMvfbZLYbxAmwiDyhI": "kQXOuy639Yt47mvNTdrIluoC6DMvfbZLYbxAmwiDyhI" + }, + "signatures": { + "@example:localhost": { + "ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU": "q32ifix/qyRpvmegw2BEJklwoBCAJldDNkcX+fp+lBA4Rpyqtycxge6BA4hcJdxYsy3oV0IHRuugS8rJMMFyAA" + } + } + } + }, + "user_signing_keys": { + "@example:localhost": { + "user_id": "@example:localhost", + "usage": [ + "user_signing" + ], + "keys": { + "ed25519:g4ED07Fnqf3GzVWNN1pZ0IFrPQVdqQf+PYoJNH4eE0s": "g4ED07Fnqf3GzVWNN1pZ0IFrPQVdqQf+PYoJNH4eE0s" + }, + "signatures": { + "@example:localhost": { + "ed25519:n2lpJGx0LiKnuNE1IucZP3QExrD4SeRP0veBHPe3XUU": "nKQu8alQKDefNbZz9luYPcNj+Z+ouQSot4fU/A23ELl1xrI06QVBku/SmDx0sIW1ytso0Cqwy1a+3PzCa1XABg" + } + } + } + } + }); + const marked = await m.markRequestAsSent('foo', RequestType.KeysQuery, hypothetical_response); + } + + { + // derived from https://github.com/matrix-org/matrix-rust-sdk/blob/7f49618d350fab66b7e1dc4eaf64ec25ceafd658/benchmarks/benches/crypto_bench/keys_claim.json + const hypothetical_response = JSON.stringify({ + "one_time_keys": { + "@example:localhost": { + "AFGUOBTZWM": { + "signed_curve25519:AAAABQ": { + "key": "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws", + "signatures": { + "@example:localhost": { + "ed25519:AFGUOBTZWM": "2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg" + } + } + } + }, + } + }, + "failures": {} + }); + const marked = await m.markRequestAsSent('bar', RequestType.KeysClaim, hypothetical_response); + } + }); + + test('can share a room key', async () => { + const other_users = [new UserId('@example:localhost')]; + + const requests = JSON.parse(await m.shareRoomKey(room, other_users, new EncryptionSettings())); + + expect(requests).toHaveLength(1); + expect(requests[0].event_type).toBeDefined(); + expect(requests[0].txn_id).toBeDefined(); + expect(requests[0].messages).toBeDefined(); + expect(requests[0].messages['@example:localhost']).toBeDefined(); + }); + + let encrypted; + + test('can encrypt an event', async () => { + encrypted = JSON.parse(await m.encryptRoomEvent( + room, + 'm.room.message', + JSON.stringify({ + "hello": "world" + }), + )); + + expect(encrypted.algorithm).toBeDefined(); + expect(encrypted.ciphertext).toBeDefined(); + expect(encrypted.sender_key).toBeDefined(); + expect(encrypted.device_id).toStrictEqual(device.toString()); + expect(encrypted.session_id).toBeDefined(); + }); + + test('can decrypt an event', async () => { + const decrypted = await m.decryptRoomEvent( + JSON.stringify({ + "type": "m.room.encrypted", + "event_id": "$xxxxx:example.org", + "origin_server_ts": Date.now(), + "sender": user.toString(), + content: encrypted, + unsigned: { + "age": 1234 + } + }), + room, + ); + + expect(decrypted).toBeInstanceOf(DecryptedRoomEvent); + + const event = JSON.parse(decrypted.event); + expect(event.content.hello).toStrictEqual("world"); + + expect(decrypted.sender.toString()).toStrictEqual(user.toString()); + expect(decrypted.senderDevice.toString()).toStrictEqual(device.toString()); + expect(decrypted.senderCurve25519Key).toBeDefined(); + expect(decrypted.senderClaimedEd25519Key).toBeDefined(); + expect(decrypted.forwardingCurve25519KeyChain).toHaveLength(0); + expect(decrypted.verificationState).toStrictEqual(VerificationState.Trusted); + }); + }); + + test('can update tracked users', async () => { + const m = await machine(); + + expect(await m.updateTrackedUsers([user])).toStrictEqual(undefined); + }); + + test('can read cross-signing status', async () => { + const m = await machine(); + const crossSigningStatus = await m.crossSigningStatus(); + + expect(crossSigningStatus).toBeInstanceOf(CrossSigningStatus); + expect(crossSigningStatus.hasMaster).toStrictEqual(false); + expect(crossSigningStatus.hasSelfSigning).toStrictEqual(false); + expect(crossSigningStatus.hasUserSigning).toStrictEqual(false); + }); + + test('can sign a message', async () => { + const m = await machine(); + const signatures = await m.sign('foo'); + + expect(signatures.isEmpty).toStrictEqual(false); + expect(signatures.count).toStrictEqual(1n); + + let base64; + + // `get` + { + const signature = signatures.get(user); + + expect(signature).toMatchObject({ + "ed25519:foobar": expect.any(MaybeSignature), + }); + expect(signature['ed25519:foobar'].isValid).toStrictEqual(true); + expect(signature['ed25519:foobar'].isInvalid).toStrictEqual(false); + expect(signature['ed25519:foobar'].invalidSignatureSource).toBeNull(); + + base64 = signature['ed25519:foobar'].signature.toBase64(); + + expect(base64).toMatch(/^[A-Za-z0-9\+/]+$/); + expect(signature['ed25519:foobar'].signature.ed25519.toBase64()).toStrictEqual(base64); + } + + // `getSignature` + { + const signature = signatures.getSignature(user, new DeviceKeyId('ed25519:foobar')); + expect(signature.toBase64()).toStrictEqual(base64); + } + + // Unknown signatures. + { + expect(signatures.get(new UserId('@hello:example.org'))).toBeNull(); + expect(signatures.getSignature(user, new DeviceKeyId('world:foobar'))).toBeNull(); + } + }); +}); diff --git a/crates/matrix-sdk-crypto-nodejs/tests/requests.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/requests.test.js similarity index 99% rename from crates/matrix-sdk-crypto-nodejs/tests/requests.test.js rename to bindings/matrix-sdk-crypto-nodejs/tests/requests.test.js index 79b43e662..96cf946b3 100644 --- a/crates/matrix-sdk-crypto-nodejs/tests/requests.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/requests.test.js @@ -26,5 +26,4 @@ for (const request of [ expect(() => { new (request)() }).toThrow(); }); }) - } diff --git a/crates/matrix-sdk-crypto-nodejs/tests/responses.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/responses.test.js similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/tests/responses.test.js rename to bindings/matrix-sdk-crypto-nodejs/tests/responses.test.js diff --git a/crates/matrix-sdk-crypto-nodejs/tests/sync_events.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/sync_events.test.js similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/tests/sync_events.test.js rename to bindings/matrix-sdk-crypto-nodejs/tests/sync_events.test.js diff --git a/crates/matrix-sdk-crypto-nodejs/tsconfig.json b/bindings/matrix-sdk-crypto-nodejs/tsconfig.json similarity index 100% rename from crates/matrix-sdk-crypto-nodejs/tsconfig.json rename to bindings/matrix-sdk-crypto-nodejs/tsconfig.json diff --git a/crates/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml similarity index 79% rename from crates/matrix-sdk-ffi/Cargo.toml rename to bindings/matrix-sdk-ffi/Cargo.toml index 3dfd6897c..bc2f81bb1 100644 --- a/crates/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.56" repository = "https://github.com/matrix-org/matrix-rust-sdk" [lib] -crate-type = ["cdylib", "staticlib"] +crate-type = ["staticlib"] [build-dependencies] @@ -21,7 +21,7 @@ anyhow = "1.0.51" extension-trait = "1.0.1" futures-core = "0.3.17" futures-util = { version = "0.3.17", default-features = false } -matrix-sdk = { path = "../matrix-sdk", features = ["experimental-timeline", "markdown"] } +matrix-sdk = { path = "../../crates/matrix-sdk", features = ["experimental-timeline", "markdown"] } once_cell = "1.10.0" parking_lot = "0.12.0" sanitize-filename-reader-friendly = "2.2.1" @@ -31,5 +31,6 @@ thiserror = "1.0.30" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tokio-stream = "0.1.8" tracing = "0.1.32" +# keep in sync with uniffi dependency in matrix-sdk-crypto-ffi, and uniffi_bindgen in ffi CI job uniffi = "0.18.0" uniffi_macros = "0.18.0" diff --git a/crates/matrix-sdk-ffi/README.md b/bindings/matrix-sdk-ffi/README.md similarity index 100% rename from crates/matrix-sdk-ffi/README.md rename to bindings/matrix-sdk-ffi/README.md diff --git a/crates/matrix-sdk-ffi/build.rs b/bindings/matrix-sdk-ffi/build.rs similarity index 100% rename from crates/matrix-sdk-ffi/build.rs rename to bindings/matrix-sdk-ffi/build.rs diff --git a/crates/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl similarity index 60% rename from crates/matrix-sdk-ffi/src/api.udl rename to bindings/matrix-sdk-ffi/src/api.udl index a2af0c032..80a24aa20 100644 --- a/crates/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -1,13 +1,4 @@ namespace sdk { - [Throws=ClientError] - Client login_new_client(string base_path, string username, string password); - - [Throws=ClientError] - Client guest_client(string base_path, string homeserver); - - [Throws=ClientError] - Client login_with_token(string base_path, string restore_token); - MediaSource media_source_from_url(string url); MessageEventContent message_event_content_from_markdown(string md); string gen_transaction_id(); @@ -22,9 +13,33 @@ callback interface ClientDelegate { void did_receive_sync_update(); }; +interface ClientBuilder { + constructor(); + + [Self=ByArc] + ClientBuilder base_path(string path); + + [Self=ByArc] + ClientBuilder username(string username); + + [Self=ByArc] + ClientBuilder homeserver_url(string url); + + [Throws=ClientError, Self=ByArc] + Client build(); +}; + interface Client { void set_delegate(ClientDelegate? delegate); + [Throws=ClientError] + void login(string username, string password); + + [Throws=ClientError] + void restore_login(string restore_token); + + string homeserver(); + void start_sync(); [Throws=ClientError] @@ -52,6 +67,9 @@ interface Client { [Throws=ClientError] sequence get_media_content(MediaSource source); + + [Throws=ClientError] + SessionVerificationController get_session_verification_controller(); }; callback interface RoomDelegate { @@ -135,3 +153,58 @@ interface EmoteMessage { interface MediaSource { string url(); }; + +[Error] +enum AuthenticationError { + "ClientMissing", + "Generic", +}; + +interface AuthenticationService { + constructor(string base_path); + + [Throws=AuthenticationError] + string homeserver(); + + [Throws=AuthenticationError] + string? authentication_issuer(); + + [Throws=AuthenticationError] + boolean supports_password_login(); + + [Throws=AuthenticationError] + void use_server(string server_name); + + [Throws=AuthenticationError] + Client login(string username, string password); +}; + +interface SessionVerificationEmoji { + string symbol(); + string description(); +}; + +callback interface SessionVerificationControllerDelegate { + void did_receive_verification_data(sequence data); + void did_fail(); + void did_cancel(); + void did_finish(); +}; + +interface SessionVerificationController { + void set_delegate(SessionVerificationControllerDelegate? delegate); + + boolean is_verified(); + + [Throws=ClientError] + void request_verification(); + + [Throws=ClientError] + void approve_verification(); + + [Throws=ClientError] + void decline_verification(); + + [Throws=ClientError] + void cancel_verification(); +}; diff --git a/bindings/matrix-sdk-ffi/src/authentication_service.rs b/bindings/matrix-sdk-ffi/src/authentication_service.rs new file mode 100644 index 000000000..7ed148d82 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/authentication_service.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use parking_lot::RwLock; + +use super::{client::Client, client_builder::ClientBuilder}; + +pub struct AuthenticationService { + base_path: String, + client: RwLock>>, +} + +#[derive(Debug, thiserror::Error)] +pub enum AuthenticationError { + #[error("A successful call to use_server must be made first.")] + ClientMissing, + #[error("An error occurred: {message}")] + Generic { message: String }, +} + +impl From for AuthenticationError { + fn from(e: anyhow::Error) -> AuthenticationError { + AuthenticationError::Generic { message: e.to_string() } + } +} + +impl AuthenticationService { + /// Creates a new service to authenticate a user with. + pub fn new(base_path: String) -> Self { + AuthenticationService { base_path, client: RwLock::new(None) } + } + + /// The currently configured homeserver. + pub fn homeserver(&self) -> Result { + self.client + .read() + .as_ref() + .ok_or(AuthenticationError::ClientMissing) + .map(|client| client.homeserver()) + } + + /// The OIDC Provider that is trusted by the homeserver. `None` when + /// not configured. + pub fn authentication_issuer(&self) -> Result, AuthenticationError> { + self.client + .read() + .as_ref() + .ok_or(AuthenticationError::ClientMissing) + .map(|client| client.authentication_issuer()) + } + + /// Whether the current homeserver supports the password login flow. + pub fn supports_password_login(&self) -> Result { + self.client + .read() + .as_ref() + .ok_or(AuthenticationError::ClientMissing) + .and_then(|client| client.supports_password_login().map_err(AuthenticationError::from)) + } + + /// Updates the server to authenticate with the specified homeserver. + pub fn use_server(&self, server_name: String) -> Result<(), AuthenticationError> { + // Construct a username as the builder currently requires one. + let username = format!("@auth:{}", server_name); + let client = Arc::new(ClientBuilder::new()) + .base_path(self.base_path.clone()) + .username(username) + .build() + .map_err(AuthenticationError::from)?; + + *self.client.write() = Some(client); + Ok(()) + } + + /// Performs a password login using the current homeserver. + pub fn login( + &self, + username: String, + password: String, + ) -> Result, AuthenticationError> { + match self.client.read().as_ref() { + Some(client) => { + let homeserver_url = client.homeserver(); + + // Create a new client to setup the store path for the username + let client = Arc::new(ClientBuilder::new()) + .base_path(self.base_path.clone()) + .homeserver_url(homeserver_url) + .username(username.clone()) + .build() + .map_err(AuthenticationError::from)?; + + client + .login(username, password) + .map(|_| client.clone()) + .map_err(AuthenticationError::from) + } + None => Err(AuthenticationError::ClientMissing), + } + } +} diff --git a/crates/matrix-sdk-ffi/src/backward_stream.rs b/bindings/matrix-sdk-ffi/src/backward_stream.rs similarity index 100% rename from crates/matrix-sdk-ffi/src/backward_stream.rs rename to bindings/matrix-sdk-ffi/src/backward_stream.rs diff --git a/crates/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs similarity index 58% rename from crates/matrix-sdk-ffi/src/client.rs rename to bindings/matrix-sdk-ffi/src/client.rs index b8b7e056a..0dda08d72 100644 --- a/crates/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -6,6 +6,7 @@ use matrix_sdk::{ ruma::{ api::client::{ filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, + session::get_login_types, sync::sync_events::v3::Filter, }, events::room::MediaSource, @@ -15,7 +16,10 @@ use matrix_sdk::{ }; use parking_lot::RwLock; -use super::{room::Room, ClientState, RestoreToken, RUNTIME}; +use super::{ + room::Room, session_verification::SessionVerificationController, ClientState, RestoreToken, + RUNTIME, +}; impl std::ops::Deref for Client { type Target = MatrixClient; @@ -33,6 +37,8 @@ pub struct Client { client: MatrixClient, state: Arc>, delegate: Arc>>>, + session_verification_controller: + Arc>>, } impl Client { @@ -41,17 +47,60 @@ impl Client { client, state: Arc::new(RwLock::new(state)), delegate: Arc::new(RwLock::new(None)), + session_verification_controller: Arc::new(matrix_sdk::locks::RwLock::new(None)), } } + pub fn login(&self, username: String, password: String) -> anyhow::Result<()> { + RUNTIME.block_on(async move { + self.client.login_username(&username, &password).send().await?; + Ok(()) + }) + } + + pub fn restore_login(&self, restore_token: String) -> anyhow::Result<()> { + let RestoreToken { session, homeurl: _, is_guest: _ } = + serde_json::from_str(&restore_token)?; + + RUNTIME.block_on(async move { + self.client.restore_login(session).await?; + Ok(()) + }) + } + pub fn set_delegate(&self, delegate: Option>) { *self.delegate.write() = delegate; } + /// The homeserver this client is configured to use. + pub fn homeserver(&self) -> String { + RUNTIME.block_on(async move { self.client.homeserver().await.to_string() }) + } + + /// The OIDC Provider that is trusted by the homeserver. `None` when + /// not configured. + pub fn authentication_issuer(&self) -> Option { + RUNTIME.block_on(async move { + self.client.authentication_issuer().await.map(|server| server.to_string()) + }) + } + + /// Whether or not the client's homeserver supports the password login flow. + pub fn supports_password_login(&self) -> anyhow::Result { + RUNTIME.block_on(async move { + let login_types = self.client.get_login_types().await?; + let supports_password = login_types.flows.iter().any(|login_type| { + matches!(login_type, get_login_types::v3::LoginType::Password(_)) + }); + Ok(supports_password) + }) + } + pub fn start_sync(&self) { let client = self.client.clone(); let state = self.state.clone(); let delegate = self.delegate.clone(); + let session_verification_controller = self.session_verification_controller.clone(); RUNTIME.spawn(async move { let mut filter = FilterDefinition::default(); let mut room_filter = RoomFilter::default(); @@ -67,7 +116,7 @@ impl Client { let sync_settings = SyncSettings::new().filter(Filter::FilterId(&filter_id)); client - .sync_with_callback(sync_settings, |_| async { + .sync_with_callback(sync_settings, |sync_response| async { if !state.read().has_first_synced { state.write().has_first_synced = true } @@ -79,9 +128,18 @@ impl Client { state.write().is_syncing = true; } - if let Some(ref delegate) = *delegate.read() { + if let Some(delegate) = &*delegate.read() { delegate.did_receive_sync_update() } + + if let Some(session_verification_controller) = + &*session_verification_controller.read().await + { + session_verification_controller + .process_to_device_messages(sync_response.to_device) + .await; + } + LoopCtrl::Continue }) .await; @@ -155,6 +213,33 @@ impl Client { .await?) }) } + + pub fn get_session_verification_controller( + &self, + ) -> anyhow::Result> { + RUNTIME.block_on(async move { + if let Some(session_verification_controller) = + &*self.session_verification_controller.read().await + { + return Ok(Arc::new(session_verification_controller.clone())); + } + + let user_id = self.client.user_id().expect("Failed retrieving current user_id"); + let user_identity = self + .client + .encryption() + .get_user_identity(user_id) + .await? + .expect("Failed retrieving user identity"); + + let session_verification_controller = SessionVerificationController::new(user_identity); + + *self.session_verification_controller.write().await = + Some(session_verification_controller.clone()); + + Ok(Arc::new(session_verification_controller)) + }) + } } pub fn gen_transaction_id() -> String { diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs new file mode 100644 index 000000000..786fbdf18 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -0,0 +1,87 @@ +use std::{fs, path::PathBuf, sync::Arc}; + +use anyhow::Context; +use matrix_sdk::{ + ruma::UserId, store::make_store_config, Client as MatrixClient, + ClientBuilder as MatrixClientBuilder, +}; +use sanitize_filename_reader_friendly::sanitize; + +use super::{client::Client, ClientState, RUNTIME}; + +#[derive(Clone)] +pub struct ClientBuilder { + base_path: Option, + username: Option, + homeserver_url: Option, + inner: MatrixClientBuilder, +} + +impl ClientBuilder { + pub fn new() -> Self { + Self { + base_path: None, + username: None, + homeserver_url: None, + inner: MatrixClient::builder().user_agent("rust-sdk-ios"), + } + } + + pub fn base_path(self: Arc, path: String) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.base_path = Some(path); + Arc::new(builder) + } + + pub fn username(self: Arc, username: String) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.username = Some(username); + Arc::new(builder) + } + + pub fn homeserver_url(self: Arc, url: String) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.homeserver_url = Some(url); + Arc::new(builder) + } + + pub fn build(self: Arc) -> anyhow::Result> { + let builder = unwrap_or_clone_arc(self); + + let base_path = builder.base_path.context("Base path was not set")?; + let username = builder + .username + .context("Username to determine homeserver and home path was not set")?; + + // Determine store path + let data_path = PathBuf::from(base_path).join(sanitize(&username)); + fs::create_dir_all(&data_path)?; + let store_config = make_store_config(&data_path, None)?; + + let mut inner_builder = builder.inner.store_config(store_config); + + // Determine server either from explicitly set homeserver or from userId + if let Some(homeserver_url) = builder.homeserver_url { + inner_builder = inner_builder.homeserver_url(homeserver_url); + } else { + let user = UserId::parse(username)?; + inner_builder = inner_builder.server_name(user.server_name()); + } + + RUNTIME.block_on(async move { + let client = inner_builder.build().await?; + let c = Client::new(client, ClientState::default()); + Ok(Arc::new(c)) + }) + } +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self::new() + } +} + +fn unwrap_or_clone_arc(arc: Arc) -> T { + Arc::try_unwrap(arc).unwrap_or_else(|x| (*x).clone()) +} diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs new file mode 100644 index 000000000..7fb23d268 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -0,0 +1,57 @@ +// TODO: target-os conditional would be good. + +#![allow(unused_qualifications)] + +pub mod authentication_service; +pub mod backward_stream; +pub mod client; +pub mod client_builder; +pub mod messages; +pub mod room; +pub mod session_verification; +mod uniffi_api; + +use client::Client; +use client_builder::ClientBuilder; +use matrix_sdk::Session; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use tokio::runtime::Runtime; +pub use uniffi_api::*; + +pub static RUNTIME: Lazy = + Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime")); + +pub use matrix_sdk::ruma::{api::client::account::register, UserId}; + +pub use self::{ + authentication_service::*, backward_stream::*, client::*, messages::*, room::*, + session_verification::*, +}; + +#[derive(Default, Debug)] +pub struct ClientState { + is_guest: bool, + has_first_synced: bool, + is_syncing: bool, + should_stop_syncing: bool, +} + +#[derive(Serialize, Deserialize)] +struct RestoreToken { + is_guest: bool, + homeurl: String, + session: Session, +} + +#[derive(thiserror::Error, Debug)] +pub enum ClientError { + #[error("client error: {msg}")] + Generic { msg: String }, +} + +impl From for ClientError { + fn from(e: anyhow::Error) -> ClientError { + ClientError::Generic { msg: e.to_string() } + } +} diff --git a/crates/matrix-sdk-ffi/src/messages.rs b/bindings/matrix-sdk-ffi/src/messages.rs similarity index 100% rename from crates/matrix-sdk-ffi/src/messages.rs rename to bindings/matrix-sdk-ffi/src/messages.rs diff --git a/crates/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs similarity index 100% rename from crates/matrix-sdk-ffi/src/room.rs rename to bindings/matrix-sdk-ffi/src/room.rs diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs new file mode 100644 index 000000000..ea3997567 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -0,0 +1,193 @@ +use std::sync::Arc; + +use matrix_sdk::{ + encryption::{ + identities::UserIdentity, + verification::{SasVerification, VerificationRequest}, + }, + ruma::{ + api::client::sync::sync_events::v3::ToDevice, + events::{key::verification::VerificationMethod, AnyToDeviceEvent}, + }, +}; +use parking_lot::RwLock; + +use super::RUNTIME; + +pub struct SessionVerificationEmoji { + symbol: String, + description: String, +} + +impl SessionVerificationEmoji { + pub fn symbol(&self) -> String { + self.symbol.clone() + } + + pub fn description(&self) -> String { + self.description.clone() + } +} + +pub trait SessionVerificationControllerDelegate: Sync + Send { + fn did_receive_verification_data(&self, data: Vec>); + fn did_fail(&self); + fn did_cancel(&self); + fn did_finish(&self); +} + +#[derive(Clone)] +pub struct SessionVerificationController { + user_identity: UserIdentity, + delegate: Arc>>>, + verification_request: Arc>>, + sas_verification: Arc>>, +} + +impl SessionVerificationController { + pub fn new(user_identity: UserIdentity) -> Self { + SessionVerificationController { + user_identity, + delegate: Arc::new(RwLock::new(None)), + verification_request: Arc::new(RwLock::new(None)), + sas_verification: Arc::new(RwLock::new(None)), + } + } + + pub fn set_delegate(&self, delegate: Option>) { + *self.delegate.write() = delegate; + } + + pub fn is_verified(&self) -> bool { + self.user_identity.verified() + } + + pub fn request_verification(&self) -> anyhow::Result<()> { + RUNTIME.block_on(async move { + let methods = vec![VerificationMethod::SasV1]; + let verification_request = + self.user_identity.request_verification_with_methods(methods).await?; + *self.verification_request.write() = Some(verification_request); + + Ok(()) + }) + } + + pub fn approve_verification(&self) -> anyhow::Result<()> { + RUNTIME.block_on(async move { + let sas_verification = self.sas_verification.read().clone(); + if let Some(sas_verification) = sas_verification { + sas_verification.confirm().await?; + } + + Ok(()) + }) + } + + pub fn decline_verification(&self) -> anyhow::Result<()> { + RUNTIME.block_on(async move { + let sas_verification = self.sas_verification.read().clone(); + if let Some(sas_verification) = sas_verification { + sas_verification.mismatch().await?; + } + + Ok(()) + }) + } + + pub fn cancel_verification(&self) -> anyhow::Result<()> { + RUNTIME.block_on(async move { + let verification_request = self.verification_request.read().clone(); + if let Some(verification) = verification_request { + verification.cancel().await?; + } + + Ok(()) + }) + } + + pub async fn process_to_device_messages(&self, to_device: ToDevice) { + let sas_verification = self.sas_verification.clone(); + + for event in to_device.events.into_iter().filter_map(|e| e.deserialize().ok()) { + match event { + AnyToDeviceEvent::KeyVerificationReady(event) => { + if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { + return; + } + self.start_sas_verification().await; + } + AnyToDeviceEvent::KeyVerificationCancel(event) => { + if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { + return; + } + + if let Some(delegate) = &*self.delegate.read() { + delegate.did_cancel() + } + } + AnyToDeviceEvent::KeyVerificationKey(event) => { + if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { + return; + } + + if let Some(sas_verification) = &*sas_verification.read() { + if let Some(emojis) = sas_verification.emoji() { + if let Some(delegate) = &*self.delegate.read() { + let emojis = emojis + .iter() + .map(|e| { + Arc::new(SessionVerificationEmoji { + symbol: e.symbol.to_owned(), + description: e.description.to_owned(), + }) + }) + .collect::>(); + + delegate.did_receive_verification_data(emojis); + } + } else if let Some(delegate) = &*self.delegate.read() { + delegate.did_fail() + } + } else if let Some(delegate) = &*self.delegate.read() { + delegate.did_fail() + } + } + AnyToDeviceEvent::KeyVerificationDone(event) => { + if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { + return; + } + + if let Some(delegate) = &*self.delegate.read() { + delegate.did_finish() + } + } + _ => (), + } + } + } + + fn is_transaction_id_valid(&self, transaction_id: String) -> bool { + if let Some(verification) = &*self.verification_request.read() { + return verification.flow_id() == transaction_id; + } + + false + } + + async fn start_sas_verification(&self) { + let verification_request = self.verification_request.read().clone(); + if let Some(verification) = verification_request { + match verification.start_sas().await { + Ok(verification) => { + *self.sas_verification.write() = verification; + } + Err(_) => { + if let Some(delegate) = &*self.delegate.read() { + delegate.did_fail() + } + } + } + } + } +} diff --git a/crates/matrix-sdk-ffi/src/uniffi_api.rs b/bindings/matrix-sdk-ffi/src/uniffi_api.rs similarity index 100% rename from crates/matrix-sdk-ffi/src/uniffi_api.rs rename to bindings/matrix-sdk-ffi/src/uniffi_api.rs diff --git a/codecov.yaml b/codecov.yaml index a5e047977..221ff7062 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -24,14 +24,12 @@ coverage: informational: true paths: - "bindings/" - - "crates/matrix-sdk-crypto-ffi/" - - "crates/matrix-sdk-ffi/" patch: off ignore: - - "crates/matrix-sdk-crypto-ffi" - - "crates/matrix-sdk-crypto-js" - - "crates/matrix-sdk-crypto-nodejs" - - "crates/matrix-sdk-ffi" + - "bindings/matrix-sdk-crypto-ffi" + - "bindings/matrix-sdk-crypto-js" + - "bindings/matrix-sdk-crypto-nodejs" + - "bindings/matrix-sdk-ffi" - "crates/matrix-sdk-indexeddb" - "crates/matrix-sdk-test" - "crates/matrix-sdk-test-macros" diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index c5a2840df..d3f43ba66 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -34,7 +34,7 @@ http = "0.2.6" matrix-sdk = { version = "0.5.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] } percent-encoding = "2.1.0" regex = "1.5.5" -ruma = { version = "0.6.1", features = ["client-api-c", "appservice-api-s"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "appservice-api-s"] } serde = "1.0.136" serde_json = "1.0.79" serde_yaml = "0.8.23" @@ -46,6 +46,6 @@ warp = { version = "0.3.2", default-features = false } [dev-dependencies] matrix-sdk-test = { version = "0.5.0", path = "../matrix-sdk-test", features = ["appservice"] } -mockito = "0.31.0" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] } tracing-subscriber = "0.3.11" +wiremock = "0.5.13" diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index 9f50d4bb6..5060f2b8e 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -161,7 +161,7 @@ impl<'a> VirtualUserBuilder<'a> { } } - /// Set the device id of the virtual user + /// Set the device ID of the virtual user pub fn device_id(mut self, device_id: Option) -> Self { self.device_id = device_id; self diff --git a/crates/matrix-sdk-appservice/tests/tests.rs b/crates/matrix-sdk-appservice/tests/tests.rs index 2d20fd074..61180ed97 100644 --- a/crates/matrix-sdk-appservice/tests/tests.rs +++ b/crates/matrix-sdk-appservice/tests/tests.rs @@ -18,15 +18,22 @@ use ruma::{ }; use serde_json::json; use warp::{Filter, Reply}; +use wiremock::{ + matchers::{body_json, header, method, path}, + Mock, MockServer, ResponseTemplate, +}; fn registration_string() -> String { include_str!("../tests/registration.yaml").to_owned() } -async fn appservice(registration: Option) -> Result { +async fn appservice( + homeserver_url: Option, + registration: Option, +) -> Result { // env::set_var( // "RUST_LOG", - // "mockito=debug,matrix_sdk=debug,ruma=debug,warp=debug", + // "wiremock=debug,matrix_sdk=debug,ruma=debug,warp=debug", // ); let _ = tracing_subscriber::fmt::try_init(); @@ -35,7 +42,7 @@ async fn appservice(registration: Option) -> Result { None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(), }; - let homeserver_url = mockito::server_url(); + let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned()); let server_name = "localhost"; let client_builder = Client::builder() @@ -53,28 +60,27 @@ async fn appservice(registration: Option) -> Result { #[async_test] async fn test_register_virtual_user() -> Result<()> { - let appservice = appservice(None).await?; + let server = MockServer::start().await; + let appservice = appservice(Some(server.uri()), None).await?; let localpart = "someone"; - let _mock = mockito::mock("POST", "/_matrix/client/r0/register") - .match_query(mockito::Matcher::Missing) - .match_header( + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/register")) + .and(header( "authorization", - mockito::Matcher::Exact(format!("Bearer {}", appservice.registration().as_token)), - ) - .match_body(mockito::Matcher::Json(json!({ + format!("Bearer {}", appservice.registration().as_token).as_str(), + )) + .and(body_json(json!({ "username": localpart.to_owned(), "type": "m.login.application_service" }))) - .with_body(format!( - r#"{{ + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "access_token": "abc123", "device_id": "GHTYAJCE", - "user_id": "@{localpart}:localhost" - }}"#, - localpart = localpart - )) - .create(); + "user_id": format!("@{localpart}:localhost"), + }))) + .mount(&server) + .await; appservice.register_virtual_user(localpart).await?; @@ -89,7 +95,7 @@ async fn test_put_transaction() -> Result<()> { transaction_builder.add_room_event(EventsJson::Member); let transaction = transaction_builder.build_json_transaction(); - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; let status = warp::test::request() .method("PUT") @@ -106,9 +112,75 @@ async fn test_put_transaction() -> Result<()> { Ok(()) } +#[async_test] +async fn test_put_transaction_with_repeating_txn_id() -> Result<()> { + let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(EventsJson::Member); + let transaction = transaction_builder.build_json_transaction(); + + let appservice = appservice(None, None).await?; + + #[allow(clippy::mutex_atomic)] + let on_state_member = Arc::new(Mutex::new(false)); + appservice + .register_event_handler({ + let on_state_member = on_state_member.clone(); + move |_ev: OriginalSyncRoomMemberEvent| { + *on_state_member.lock().unwrap() = true; + future::ready(()) + } + }) + .await?; + + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + assert_eq!(status, 200); + { + let on_room_member_called = *on_state_member.lock().unwrap(); + assert!(on_room_member_called); + } + + // Reset this to check that next time it doesnt get called + { + let mut on_room_member_called = on_state_member.lock().unwrap(); + *on_room_member_called = false; + } + + let status = warp::test::request() + .method("PUT") + .path(uri) + .json(&transaction) + .filter(&appservice.warp_filter()) + .await + .unwrap() + .into_response() + .status(); + + // According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events + // This should noop and return 200. + assert_eq!(status, 200); + { + let on_room_member_called = *on_state_member.lock().unwrap(); + // This time we should not have called the event handler. + assert!(!on_room_member_called); + } + + Ok(()) +} + #[async_test] async fn test_get_user() -> Result<()> { - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await; let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token"; @@ -129,7 +201,7 @@ async fn test_get_user() -> Result<()> { #[async_test] async fn test_get_room() -> Result<()> { - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await; let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token"; @@ -156,7 +228,7 @@ async fn test_invalid_access_token() -> Result<()> { let transaction = transaction_builder.add_room_event(EventsJson::Member).build_json_transaction(); - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; let status = warp::test::request() .method("PUT") @@ -181,7 +253,7 @@ async fn test_no_access_token() -> Result<()> { transaction_builder.add_room_event(EventsJson::Member); let transaction = transaction_builder.build_json_transaction(); - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; { let status = warp::test::request() @@ -202,7 +274,7 @@ async fn test_no_access_token() -> Result<()> { #[async_test] async fn test_event_handler() -> Result<()> { - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; #[allow(clippy::mutex_atomic)] let on_state_member = Arc::new(Mutex::new(false)); @@ -238,7 +310,7 @@ async fn test_event_handler() -> Result<()> { #[async_test] async fn test_unrelated_path() -> Result<()> { - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; let status = { let consumer_filter = warp::any() @@ -274,7 +346,7 @@ async fn test_appservice_on_sub_path() -> Result<()> { transaction_builder.add_room_event(EventsJson::MemberNameChange); let transaction_2 = transaction_builder.build_json_transaction(); - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; { warp::test::request() @@ -381,7 +453,7 @@ async fn test_receive_transaction() -> Result<()> { }))? .cast::(), ]; - let appservice = appservice(None).await?; + let appservice = appservice(None, None).await?; let alice = appservice.virtual_user_client("_appservice_alice").await?; let bob = appservice.virtual_user_client("_appservice_bob").await?; diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index a606c996e..7c6834753 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -48,10 +48,10 @@ tracing = "0.1.34" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -ruma = { version = "0.6.1", features = ["client-api-c", "js", "signatures"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "canonical-json"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ruma = { version = "0.6.2", features = ["client-api-c", "signatures"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "canonical-json"] } [dev-dependencies] futures = { version = "0.3.21", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e2d0d055d..5741a9ef0 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -14,6 +14,7 @@ // limitations under the License. use std::{ + borrow::Borrow, collections::{BTreeMap, BTreeSet}, fmt, }; @@ -111,7 +112,7 @@ impl BaseClient { let store = config.state_store.map(Store::new).unwrap_or_else(Store::open_memory_store); #[cfg(feature = "e2e-encryption")] let crypto_store = - config.crypto_store.unwrap_or_else(|| Box::new(MemoryCryptoStore::default())).into(); + config.crypto_store.unwrap_or_else(|| Arc::new(MemoryCryptoStore::default())); BaseClient { store, @@ -149,7 +150,7 @@ impl BaseClient { /// # Arguments /// /// * `response` - A successful login response that contains our access - /// token and device id. + /// token and device ID. pub async fn receive_login_response( &self, response: &api::session::login::v3::Response, @@ -329,25 +330,19 @@ impl BaseClient { } if let Some(context) = &push_context { - if event - .event - .get_field::("sender")? - .map_or(false, |id| id != user_id) - { - let actions = push_rules.get_actions(&event.event, context).to_vec(); + let actions = push_rules.get_actions(&event.event, context); - if actions.iter().any(|a| matches!(a, Action::Notify)) { - changes.add_notification( - room_id, - Notification::new( - actions, - event.event.clone(), - false, - room_id.to_owned(), - MilliSecondsSinceUnixEpoch::now(), - ), - ); - } + if actions.iter().any(|a| matches!(a, Action::Notify)) { + changes.add_notification( + room_id, + Notification::new( + actions.to_owned(), + event.event.clone(), + false, + room_id.to_owned(), + MilliSecondsSinceUnixEpoch::now(), + ), + ); } // TODO if there is an // Action::SetTweak(Tweak::Highlight) we need to store @@ -447,7 +442,7 @@ impl BaseClient { // having confusing profile changes when a member gets // kicked/banned. if member.state_key() == member.sender() { - profiles.insert(member.sender().to_owned(), (&member).into()); + profiles.insert(member.sender().to_owned(), member.borrow().into()); } members.insert(member.state_key().to_owned(), member); @@ -573,7 +568,7 @@ impl BaseClient { }; let mut changes = StateChanges::new(next_batch.clone()); - let mut ambiguity_cache = AmbiguityCache::new(self.store.clone()); + let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); self.handle_account_data(&account_data.events, &mut changes).await; @@ -830,7 +825,7 @@ impl BaseClient { }) .collect(); - let mut ambiguity_cache = AmbiguityCache::new(self.store.clone()); + let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); if let Some(room) = self.store.get_room(room_id) { let mut room_info = room.clone_info(); @@ -860,7 +855,7 @@ impl BaseClient { .profiles .entry(room_id.to_owned()) .or_default() - .insert(member.sender().to_owned(), (&member).into()); + .insert(member.sender().to_owned(), member.borrow().into()); } changes @@ -1053,6 +1048,7 @@ impl BaseClient { }; Ok(Some(PushConditionRoomCtx { + user_id: user_id.to_owned(), room_id: room_id.to_owned(), member_count: UInt::new(member_count).unwrap_or(UInt::MAX), user_display_name, diff --git a/crates/matrix-sdk-base/src/rooms/members.rs b/crates/matrix-sdk-base/src/rooms/members.rs index aa967a0a8..2308f3a30 100644 --- a/crates/matrix-sdk-base/src/rooms/members.rs +++ b/crates/matrix-sdk-base/src/rooms/members.rs @@ -46,6 +46,11 @@ impl RoomMember { self.event.user_id() } + /// Get the original member event + pub fn event(&self) -> &Arc { + &self.event + } + /// Get the display name of the member if there is one. pub fn display_name(&self) -> Option<&str> { if let Some(p) = self.profile.as_ref() { diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 512cce85c..53156f060 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -191,6 +191,11 @@ impl Room { self.inner.read().unwrap().canonical_alias().map(ToOwned::to_owned) } + /// Get the canonical alias of this room. + pub fn alt_aliases(&self) -> Vec { + self.inner.read().unwrap().alt_aliases().to_owned() + } + /// Get the `m.room.create` content of this room. /// /// This usually isn't optional but some servers might not send an @@ -757,6 +762,16 @@ impl RoomInfo { self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref() } + /// Get the alternative aliases of this room. + pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] { + self.base_info + .canonical_alias + .as_ref() + .and_then(|ev| ev.as_original()) + .map(|ev| ev.content.alt_aliases.as_ref()) + .unwrap_or_default() + } + /// Get the room ID of this room. pub fn room_id(&self) -> &RoomId { &self.room_id diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 420b50310..57e8b18b0 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; use matrix_sdk_common::deserialized_responses::{AmbiguityChange, MemberEvent}; use ruma::{ @@ -22,11 +25,11 @@ use ruma::{ use tracing::trace; use super::{Result, StateChanges}; -use crate::Store; +use crate::StateStore; #[derive(Debug)] pub(crate) struct AmbiguityCache { - pub store: Store, + pub store: Arc, pub cache: BTreeMap>>, pub changes: BTreeMap>, } @@ -67,7 +70,7 @@ impl AmbiguityMap { } impl AmbiguityCache { - pub fn new(store: Store) -> Self { + pub fn new(store: Arc) -> Self { Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() } } diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 2ea3448b6..4960110b7 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -32,6 +32,10 @@ macro_rules! statestore_integration_tests { ($($name:ident)*) => { $( mod $name { + use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, + }; #[cfg(feature = "experimental-timeline")] use futures_util::StreamExt; @@ -73,8 +77,6 @@ macro_rules! statestore_integration_tests { }; use serde_json::{json, Value as JsonValue}; - use std::collections::{BTreeMap, BTreeSet}; - #[cfg(feature = "experimental-timeline")] use $crate::{ http::Response, @@ -93,7 +95,6 @@ macro_rules! statestore_integration_tests { use super::get_store; - fn user_id() -> &'static UserId { user_id!("@example:localhost") } @@ -114,7 +115,7 @@ macro_rules! statestore_integration_tests { } /// Populate the given `StateStore`. - pub(crate) async fn populated_store(inner: Box) -> StoreResult { + pub(crate) async fn populated_store(inner: Arc) -> StoreResult { let mut changes = StateChanges::default(); let store = Store::new(inner); @@ -304,7 +305,7 @@ macro_rules! statestore_integration_tests { let user_id = user_id(); let inner_store = get_store().await?; - let store = populated_store(Box::new(inner_store)).await?; + let store = populated_store(Arc::new(inner_store)).await?; assert!(store.get_sync_token().await?.is_some()); assert!(store.get_presence_event(user_id).await?.is_some()); @@ -581,7 +582,7 @@ macro_rules! statestore_integration_tests { async fn test_persist_invited_room() -> StoreResult<()> { let stripped_room_id = stripped_room_id(); let inner_store = get_store().await?; - let store = populated_store(Box::new(inner_store)).await?; + let store = populated_store(Arc::new(inner_store)).await?; assert_eq!(store.get_stripped_room_infos().await?.len(), 1); assert!(store.get_stripped_room(stripped_room_id).is_some()); @@ -597,7 +598,7 @@ macro_rules! statestore_integration_tests { let inner_store = get_store().await?; let stripped_room_id = stripped_room_id(); - let store = populated_store(Box::new(inner_store)).await?; + let store = populated_store(Arc::new(inner_store)).await?; store.remove_room(room_id).await?; diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index a93fa9bcb..76bbf4108 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -26,6 +26,12 @@ use dashmap::{DashMap, DashSet}; use lru::LruCache; #[allow(unused_imports)] use matrix_sdk_common::{instant::Instant, locks::Mutex}; +#[cfg(feature = "experimental-timeline")] +use ruma::{ + canonical_json::redact_in_place, + events::{room::redaction::SyncRoomRedactionEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent}, + CanonicalJsonObject, RoomVersionId, +}; use ruma::{ events::{ presence::PresenceEvent, @@ -38,12 +44,6 @@ use ruma::{ serde::Raw, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; -#[cfg(feature = "experimental-timeline")] -use ruma::{ - events::{room::redaction::SyncRoomRedactionEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent}, - signatures::{redact_in_place, CanonicalJsonObject}, - RoomVersionId, -}; #[cfg(feature = "experimental-timeline")] use super::BoxStream; diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index c126a757d..c31926e69 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -38,7 +38,7 @@ use async_trait::async_trait; use dashmap::DashMap; use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps}; #[cfg(feature = "e2e-encryption")] -use matrix_sdk_crypto::store::CryptoStore; +use matrix_sdk_crypto::store::{CryptoStore, IntoCryptoStore}; use ruma::{ api::client::push::get_notifications::v3::Notification, events::{ @@ -107,7 +107,7 @@ pub enum StoreError { /// /// This should never happen. #[error("Redaction failed: {0}")] - Redaction(#[source] ruma::signatures::Error), + Redaction(#[source] ruma::canonical_json::RedactionError), } impl StoreError { @@ -375,13 +375,41 @@ pub trait StateStore: AsyncTraitDeps { ) -> Result>, Option)>>; } +/// A type that can be type-erased into `Arc`. +/// +/// This trait is not meant to be implemented directly outside +/// `matrix-sdk-crypto`, but it is automatically implemented for everything that +/// implements `StateStore`. +pub trait IntoStateStore { + #[doc(hidden)] + fn into_state_store(self) -> Arc; +} + +impl IntoStateStore for T +where + T: StateStore + Sized + 'static, +{ + fn into_state_store(self) -> Arc { + Arc::new(self) + } +} + +impl IntoStateStore for Arc +where + T: StateStore + 'static, +{ + fn into_state_store(self) -> Arc { + self + } +} + /// A state store wrapper for the SDK. /// /// This adds additional higher level store functionality on top of a /// `StateStore` implementation. #[derive(Debug, Clone)] pub struct Store { - inner: Arc, + pub(super) inner: Arc, session: Arc>, /// The current sync token that should be used for the next sync call. pub(super) sync_token: Arc>>, @@ -392,7 +420,7 @@ pub struct Store { impl Store { /// Create a new Store with the default `MemoryStore` pub fn open_memory_store() -> Self { - let inner = Box::new(MemoryStore::new()); + let inner = Arc::new(MemoryStore::new()); Self::new(inner) } @@ -400,9 +428,9 @@ impl Store { impl Store { /// Create a new store, wrappning the given `StateStore` - pub fn new(inner: Box) -> Self { + pub fn new(inner: Arc) -> Self { Self { - inner: inner.into(), + inner, session: Default::default(), sync_token: Default::default(), rooms: Default::default(), @@ -431,7 +459,7 @@ impl Store { Ok(()) } - /// The current [`Session`] containing our user id, device id and access + /// The current [`Session`] containing our user id, device ID and access /// token. pub fn session(&self) -> Option<&Session> { self.session.get() @@ -495,7 +523,7 @@ impl Deref for Store { type Target = dyn StateStore; fn deref(&self) -> &Self::Target { - &*self.inner + self.inner.deref() } } @@ -651,11 +679,11 @@ impl StateChanges { /// /// let store_config = StoreConfig::new(); /// ``` -#[derive(Default)] +#[derive(Clone, Default)] pub struct StoreConfig { #[cfg(feature = "e2e-encryption")] - pub(crate) crypto_store: Option>, - pub(crate) state_store: Option>, + pub(crate) crypto_store: Option>, + pub(crate) state_store: Option>, } #[cfg(not(tarpaulin_include))] @@ -676,14 +704,14 @@ impl StoreConfig { /// /// The crypto store must be opened before being set. #[cfg(feature = "e2e-encryption")] - pub fn crypto_store(mut self, store: Box) -> Self { - self.crypto_store = Some(store); + pub fn crypto_store(mut self, store: impl IntoCryptoStore) -> Self { + self.crypto_store = Some(store.into_crypto_store()); self } /// Set a custom implementation of a `StateStore`. - pub fn state_store(mut self, store: Box) -> Self { - self.state_store = Some(store); + pub fn state_store(mut self, store: impl IntoStateStore) -> Self { + self.state_store = Some(store.into_state_store()); self } } diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index c0e8a97bb..f40bbce17 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -16,7 +16,7 @@ default-target = "x86_64-unknown-linux-gnu" targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] -ruma = { version = "0.6.2", features = ["client-api-c"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] } serde = "1.0.136" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 3c0cdb94b..2311dead1 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{borrow::Borrow, collections::BTreeMap}; use ruma::{ api::client::{ @@ -16,7 +16,8 @@ use ruma::{ AnyRoomEvent, AnySyncRoomEvent, }, serde::Raw, - DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, UserId, + DeviceKeyAlgorithm, EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, + OwnedRoomId, OwnedUserId, UserId, }; use serde::{Deserialize, Serialize}; @@ -312,6 +313,27 @@ impl MemberEvent { MemberEvent::Stripped(e) => Some(&e.content), } } + /// The sender of this event. + pub fn sender(&self) -> &UserId { + match self { + MemberEvent::Sync(e) => e.sender(), + MemberEvent::Stripped(e) => e.sender.borrow(), + } + } + /// The ID of this event. + pub fn event_id(&self) -> Option<&EventId> { + match self { + MemberEvent::Sync(e) => Some(e.event_id()), + MemberEvent::Stripped(_) => None, + } + } + /// The Server Timestamp of this event. + pub fn origin_server_ts(&self) -> Option { + match self { + MemberEvent::Sync(e) => Some(e.origin_server_ts()), + MemberEvent::Stripped(_) => None, + } + } /// The membership state of the user pub fn membership(&self) -> &MembershipState { diff --git a/crates/matrix-sdk-crypto-js/README.md b/crates/matrix-sdk-crypto-js/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/crates/matrix-sdk-crypto-js/js/Makefile b/crates/matrix-sdk-crypto-js/js/Makefile deleted file mode 100644 index 71e9e1c82..000000000 --- a/crates/matrix-sdk-crypto-js/js/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -build: - RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./js/pkg ../ - -test: - node --test ../tests/js/**.js diff --git a/crates/matrix-sdk-crypto-js/tests/js/events.js b/crates/matrix-sdk-crypto-js/tests/js/events.js deleted file mode 100644 index 881d86e06..000000000 --- a/crates/matrix-sdk-crypto-js/tests/js/events.js +++ /dev/null @@ -1,10 +0,0 @@ -const { HistoryVisibility } = require('../../js/pkg/matrix_sdk_crypto'); -const test = require('node:test'); -const assert = require('node:assert/strict'); - -test('HistoryVisibility', (t) => { - assert.equal(HistoryVisibility.Invited, 0); - assert.equal(HistoryVisibility.Joined, 1); - assert.equal(HistoryVisibility.Shared, 2); - assert.equal(HistoryVisibility.WorldReadable, 3); -}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/identifiers.js b/crates/matrix-sdk-crypto-js/tests/js/identifiers.js deleted file mode 100644 index 38442111a..000000000 --- a/crates/matrix-sdk-crypto-js/tests/js/identifiers.js +++ /dev/null @@ -1,37 +0,0 @@ -const { UserId, DeviceId, RoomId, ServerName } = require('../../js/pkg/matrix_sdk_crypto'); -const test = require('node:test'); -const assert = require('node:assert/strict'); - -test('UserId', (t) => { - assert.throws(() => { new UserId('@foobar') }, Error, 'An invalid user ID must throw an error'); - - const user = new UserId('@foo:bar.org'); - - assert.equal(user.localpart(), 'foo', 'Localpart is present'); - assert.ok(user.serverName() instanceof ServerName, 'Server name is present'); - assert.equal(user.isHistorical, false, 'User ID is not historical'); - assert.equal(user.toString(), '@foo:bar.org', 'Can read the user ID as a string'); -}); - -test('DeviceId', (t) => { - assert.equal(new DeviceId('foo').toString(), 'foo', 'Can read the device ID as a string'); -}); - -test('RoomId', (t) => { - assert.throws(() => { new UserId('!foo') }, Error, 'An invalid room ID must throw an error'); - - const room = new RoomId('!foo:bar.org'); - - assert.equal(room.localpart(), 'foo', 'Localpart is present'); - assert.ok(room.serverName() instanceof ServerName, 'Server name is present'); - assert.equal(room.toString(), '!foo:bar.org', 'Can read the room ID as a string'); -}); - -test('ServerName', (t) => { - assert.throws(() => { new ServerName('@foobar') }, Error, 'An invalid server name must throw an error'); - - assert.equal(new ServerName('foo.org').host(), 'foo.org', 'Host is present'); - assert.equal(new ServerName('foo.org').port(), undefined, 'Port is absent'); - assert.equal(new ServerName('foo.org:1234').port(), 1234, 'Port is present'); - assert.equal(new ServerName('foo.org').isIpLiteral(), false, 'Server name is not an IP literal'); -}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/machine.js b/crates/matrix-sdk-crypto-js/tests/js/machine.js deleted file mode 100644 index d887a3ba4..000000000 --- a/crates/matrix-sdk-crypto-js/tests/js/machine.js +++ /dev/null @@ -1,119 +0,0 @@ -const { EncryptionAlgorithm, EncryptionSettings, HistoryVisibility, UserId, DeviceId, OlmMachine, DeviceLists, KeysUploadRequest, KeysQueryRequest } = require('../../js/pkg/matrix_sdk_crypto'); -const test = require('node:test'); -const assert = require('node:assert/strict'); - -test('EncryptionAlgorithm', (t) => { - assert.equal(EncryptionAlgorithm.OlmV1Curve25519AesSha2, 0); - assert.equal(EncryptionAlgorithm.MegolmV1AesSha2, 1); -}); - -test('EncryptionSettings', (t) => { - let es = new EncryptionSettings(); - - assert.equal(es.algorithm, EncryptionAlgorithm.MegolmV1AesSha2, 'It has a default algorithm'); - assert.equal(es.rotationPeriod, 604800000000n, 'It has a default rotation period'); - assert.equal(es.rotationPeriodMessages, 100n, 'It has a default message rotation period'); - assert.equal(es.historyVisibility, HistoryVisibility.Shared, 'It has a default history visibility'); - - es.algorithm = EncryptionSettings.OlmV1Curve25519AesSha2; - assert.equal(es.algorithm, EncryptionAlgorithm.OlmV1Curve25519AesSha2, 'It has a new algorithm'); - assert.throws(() => { es.algorithm = 42 }, Error, 'Enum values are validated'); - - es.rotationPeriod = 42n; - assert.equal(es.rotationPeriod, 42n, 'It has a new rotation period'); - - es.rotationPeriodMessages = 153n; - assert.equal(es.rotationPeriodMessages, 153n, 'It has a new message rotation period'); - - es.historyVisibility = HistoryVisibility.WorldReadable; - assert.equal(es.historyVisibility, HistoryVisibility.WorldReadable, 'It has a new history visibility'); - assert.throws(() => { es.historyVisibility = 42 }, Error, 'Enum values are validated'); -}); - -test('OlmMachine', async (t) => { - const user_id = new UserId('@foo:bar.org'); - const device_id = new DeviceId('baz'); - - await t.test('Construct', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - - assert.ok(machine instanceof OlmMachine); - assert.equal(machine.userId().toString(), '@foo:bar.org', 'User ID is present'); - assert.equal(machine.deviceId().toString(), 'baz', 'Device ID is present'); - }); - - await t.test('Identity keys', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - const identity_keys = machine.identityKeys(); - - assert.match(identity_keys.ed25519.toBase64(), /^[A-Za-z0-9+/]+$/, 'Ed25519 can be base64-encoded'); - assert.match(identity_keys.curve25519.toBase64(), /^[A-Za-z0-9+/]+$/, 'Curve25519 can be base64-encoded'); - assert.ok(identity_keys.curve25519.length > 0, 'Curve25519\'s length is greater than zero'); - }); - - await t.test('Display name', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - - assert.equal(await machine.displayName(), undefined, 'Display name is absent by default'); - }); - - await t.test('Tracked users', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - const tracked_users = machine.trackedUsers(); - - assert.ok(tracked_users instanceof Set, 'Tracket users are stored in a `Set`'); - assert.equal(tracked_users.size, 0, 'No tracked users by default'); - }); - - await t.test('Update tracked users', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - const update_tracked_users = await machine.updateTrackedUsers([new UserId('@foo:matrix.org'), new UserId('@bar:matrix.org')]); - - assert.equal(update_tracked_users, undefined, 'Updating tracked users returns nothing'); - }); - - await t.test('Receive sync changes', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - const to_device_events = JSON.stringify({}); - const changed_devices = new DeviceLists( - [new UserId('@foo:matrix.org'), new UserId('@bar:matrix.org')], - [new UserId('@baz:matrix.org'), new UserId('@qux:matrix.org')], - ); - const one_time_key_counts = new Map(); - one_time_key_counts.set('foo', 42); - one_time_key_counts.set('bar', 153); - const unused_fallback_keys = new Set(); - unused_fallback_keys.add('baz'); - unused_fallback_keys.add('qux'); - - const decrypted_to_device = JSON.parse( - await machine.receiveSyncChanges( - to_device_events, - changed_devices, - one_time_key_counts, - unused_fallback_keys, - ) - ); - - assert.deepEqual(decrypted_to_device, {}, 'Nothing to do by default'); - }); - - await t.test('Outgoing requests', async (t) => { - const machine = await new OlmMachine(user_id, device_id); - const outgoing_requests = await machine.outgoingRequests(); - - assert.ok(outgoing_requests instanceof Array, 'Outgoing requests are stored in an `Array`'); - assert.equal(outgoing_requests.length, 2, 'There is 2 outgoing requests'); - - const request1 = outgoing_requests[0]; - const request2 = outgoing_requests[1]; - - assert.ok(request1 instanceof KeysUploadRequest, 'First request is `KeysUploadRequest'); - assert.ok(request1.request_id.length > 0, 'First request has an ID'); - assert.ok(JSON.parse(request1.body) instanceof Object, 'First request has a valid body'); - - assert.ok(request2 instanceof KeysQueryRequest, 'Second request is `KeysQueryRequest`'); - assert.ok(request2.request_id.length > 0, 'Second request has an ID'); - assert.ok(JSON.parse(request2.body) instanceof Object, 'Second request has a valid body'); - }); -}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/requests.js b/crates/matrix-sdk-crypto-js/tests/js/requests.js deleted file mode 100644 index 00c82511a..000000000 --- a/crates/matrix-sdk-crypto-js/tests/js/requests.js +++ /dev/null @@ -1,41 +0,0 @@ -const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../../js/pkg/matrix_sdk_crypto'); -const test = require('node:test'); -const assert = require('node:assert/strict'); - -test('RequestType', (t) => { - assert.equal(RequestType.KeysUpload, 0); - assert.equal(RequestType.KeysQuery, 1); - assert.equal(RequestType.KeysClaim, 2); - assert.equal(RequestType.ToDevice, 3); - assert.equal(RequestType.SignatureUpload, 4); - assert.equal(RequestType.RoomMessage, 5); - assert.equal(RequestType.KeysBackup, 6); -}); - -test('KeysUploadRequest', (t) => { - assert.ok(new KeysUploadRequest()); -}); - -test('KeysQueryRequest', (t) => { - assert.ok(new KeysQueryRequest()); -}); - -test('KeysClaimRequest', (t) => { - assert.ok(new KeysClaimRequest()); -}); - -test('ToDeviceRequest', (t) => { - assert.ok(new ToDeviceRequest()); -}); - -test('SignatureUploadRequest', (t) => { - assert.ok(new SignatureUploadRequest()); -}); - -test('RoomMessageRequest', (t) => { - assert.ok(new RoomMessageRequest()); -}); - -test('KeysBackupRequest', (t) => { - assert.ok(new KeysBackupRequest()); -}); diff --git a/crates/matrix-sdk-crypto-js/tests/js/sync_events.js b/crates/matrix-sdk-crypto-js/tests/js/sync_events.js deleted file mode 100644 index 46cf63e38..000000000 --- a/crates/matrix-sdk-crypto-js/tests/js/sync_events.js +++ /dev/null @@ -1,23 +0,0 @@ -const { DeviceLists, UserId } = require('../../js/pkg/matrix_sdk_crypto'); -const test = require('node:test'); -const assert = require('node:assert/strict'); - -test('DeviceLists', (t) => { - const empty = new DeviceLists([], []); - - assert.equal(empty.isEmpty(), true, 'List is empty'); - assert.equal(empty.changed().length, 0, 'No user ID changed'); - assert.equal(empty.left().length, 0, 'No user ID left'); - - const list = new DeviceLists([new UserId('@foo:bar.org')], [new UserId('@baz:qux.org')]); - - assert.equal(list.isEmpty(), false, 'List is not empty'); - - const changed = list.changed(); - assert.equal(changed.length, 1, 'There is one user ID changed'); - assert.equal(changed[0].toString(), '@foo:bar.org', 'The user ID changed is correct'); - - const left = list.left(); - assert.equal(left.length, 1, 'There is one user ID left'); - assert.equal(left[0].toString(), '@baz:qux.org', 'The user ID left is correct'); -}); diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index d4881aa09..03221c856 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -51,12 +51,12 @@ zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.18", default-features = false, features = ["time"] } -ruma = { version = "0.6.2", features = ["client-api-c", "rand", "signatures", "unstable-msc2676", "unstable-msc2677"] } -vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36" } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" } [target.'cfg(target_arch = "wasm32")'.dependencies] -ruma = { version = "0.6.2", features = ["client-api-c", "js", "rand", "signatures", "unstable-msc2676", "unstable-msc2677"] } -vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36", features = ["js"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "js", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] } [dev-dependencies] futures = { version = "0.3.21", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-crypto/src/backups/keys/recovery.rs b/crates/matrix-sdk-crypto/src/backups/keys/recovery.rs index 993bc6760..42ef34259 100644 --- a/crates/matrix-sdk-crypto/src/backups/keys/recovery.rs +++ b/crates/matrix-sdk-crypto/src/backups/keys/recovery.rs @@ -15,6 +15,7 @@ use std::{ convert::TryFrom, io::{Cursor, Read}, + ops::DerefMut, }; use bs58; @@ -144,7 +145,7 @@ impl RecoveryKey { let mut expected_parity = [0u8; 1]; decoded.read_exact(&mut prefix)?; - decoded.read_exact(&mut *key)?; + decoded.read_exact(key.deref_mut())?; decoded.read_exact(&mut expected_parity)?; let expected_parity = expected_parity[0]; diff --git a/crates/matrix-sdk-crypto/src/backups/mod.rs b/crates/matrix-sdk-crypto/src/backups/mod.rs index 98d6402ae..9ab713daa 100644 --- a/crates/matrix-sdk-crypto/src/backups/mod.rs +++ b/crates/matrix-sdk-crypto/src/backups/mod.rs @@ -564,9 +564,7 @@ impl BackupMachine { #[cfg(test)] mod tests { use matrix_sdk_test::async_test; - use ruma::{ - device_id, room_id, signatures::CanonicalJsonValue, user_id, DeviceId, RoomId, UserId, - }; + use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId}; use serde_json::json; use crate::{store::RecoveryKey, types::RoomKeyBackupInfo, OlmError, OlmMachine}; diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 0320ce066..9bc6574e1 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use ruma::{signatures::CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId}; +use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId}; use serde_json::Error as SerdeError; use thiserror::Error; diff --git a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs index 90d031cee..5f9facfe2 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs @@ -136,7 +136,7 @@ impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> { let hash = info.hashes.get("sha256").ok_or(DecryptorError::MissingHash)?.as_bytes().to_owned(); - let mut key = info.web_key.k.into_inner(); + let mut key = info.key.k.into_inner(); let iv = info.iv.into_inner(); if key.len() != KEY_SIZE { @@ -270,7 +270,7 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { version: VERSION.to_owned(), hashes: self.hashes, iv: self.iv, - web_key: self.web_key, + key: self.web_key, } } } @@ -279,11 +279,11 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { /// file. #[derive(Debug, Serialize, Deserialize)] pub struct MediaEncryptionInfo { - #[serde(rename = "v")] /// The version of the encryption scheme. + #[serde(rename = "v")] pub version: String, /// The web key that was used to encrypt the file. - pub web_key: JsonWebKey, + pub key: JsonWebKey, /// The initialization vector that was used to encrypt the file. pub iv: Base64, /// The hashes that can be used to check the validity of the file. @@ -292,7 +292,7 @@ pub struct MediaEncryptionInfo { impl From for MediaEncryptionInfo { fn from(file: EncryptedFile) -> Self { - Self { version: file.v, web_key: file.key, iv: file.iv, hashes: file.hashes } + Self { version: file.v, key: file.key, iv: file.iv, hashes: file.hashes } } } @@ -312,7 +312,7 @@ mod tests { fn example_key() -> MediaEncryptionInfo { let info = json!({ "v": "v2", - "web_key": { + "key": { "kty": "oct", "alg": "A256CTR", "ext": true, diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 7cef27cdf..8b49d86b7 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -32,12 +32,9 @@ use ruma::{ request::{ RequestAction, SecretName, ToDeviceSecretRequestEvent as SecretRequestEvent, }, - send::{ - ToDeviceSecretSendEvent as SecretSendEvent, - ToDeviceSecretSendEventContent as SecretSendEventContent, - }, + send::ToDeviceSecretSendEventContent as SecretSendEventContent, }, - AnyToDeviceEvent, AnyToDeviceEventContent, + AnyToDeviceEventContent, }, DeviceId, DeviceKeyAlgorithm, EventEncryptionAlgorithm, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, @@ -51,6 +48,7 @@ use crate::{ requests::{OutgoingRequest, ToDeviceRequest}, session_manager::GroupSessionCache, store::{Changes, CryptoStoreError, SecretImportError, Store}, + types::events::secret_send::SecretSendEvent, Device, }; @@ -103,7 +101,7 @@ impl GossipMachine { &self.user_id } - /// Our own device id. + /// Our own device ID. pub fn device_id(&self) -> &DeviceId { &self.device_id } @@ -207,7 +205,7 @@ impl GossipMachine { /// * `user_id` - The user id of the device that we created the Olm session /// with. /// - /// * `device_id` - The device id of the device that got the Olm session. + /// * `device_id` - The device ID of the device that got the Olm session. pub fn retry_keyshare(&self, user_id: &UserId, device_id: &DeviceId) { if let Entry::Occupied(e) = self.users_for_key_claim.entry(user_id.to_owned()) { e.get().remove(device_id); @@ -343,6 +341,7 @@ impl GossipMachine { .store .get_inbound_group_session( &key_info.room_id, + #[allow(deprecated)] &key_info.sender_key, &key_info.session_id, ) @@ -763,7 +762,7 @@ impl GossipMachine { &self, sender_key: &str, event: &mut SecretSendEvent, - ) -> Result, CryptoStoreError> { + ) -> Result<(), CryptoStoreError> { debug!( sender = event.sender.as_str(), request_id = event.content.request_id.as_str(), @@ -772,8 +771,6 @@ impl GossipMachine { let request_id = <&TransactionId>::from(event.content.request_id.as_str()); - let secret = std::mem::take(&mut event.content.secret); - if let Some(request) = self.store.get_outgoing_secret_requests(request_id).await? { match &request.info { SecretInfo::KeyRequest(_) => { @@ -784,6 +781,10 @@ impl GossipMachine { ); } SecretInfo::SecretRequest(secret_name) => { + // Set the secret name so other consumers of the event know + // what this event is about. + event.content.secret_name = Some(secret_name.to_owned()); + debug!( sender = event.sender.as_str(), request_id = event.content.request_id.as_str(), @@ -796,7 +797,11 @@ impl GossipMachine { { if device.verified() { if secret_name != &SecretName::RecoveryKey { - match self.store.import_secret(secret_name, secret).await { + match self + .store + .import_secret(secret_name, &event.content.secret) + .await + { Ok(_) => self.mark_as_done(request).await?, Err(e) => { // If this is a store error propagate it up @@ -817,10 +822,10 @@ impl GossipMachine { } else { // Skip importing the recovery key here since // we'll want to check if the public key matches - // to the latest version on the server. We + // to the latest version on the server. The key + // will not be zeroized and // instead leave the key in the event and let // the user import it later. - event.content.secret = secret; } } else { warn!( @@ -843,19 +848,19 @@ impl GossipMachine { } } - Ok(Some(AnyToDeviceEvent::SecretSend(event.clone()))) + Ok(()) } /// Receive a forwarded room key event. pub async fn receive_forwarded_room_key( &self, sender_key: &str, - event: &mut ToDeviceForwardedRoomKeyEvent, - ) -> Result<(Option, Option), CryptoStoreError> { + event: &ToDeviceForwardedRoomKeyEvent, + ) -> Result, CryptoStoreError> { let key_info = self.get_key_info(&event.content).await?; if let Some(info) = key_info { - match InboundGroupSession::from_forwarded_key(sender_key, &mut event.content) { + match InboundGroupSession::from_forwarded_key(sender_key, &event.content) { Ok(session) => { let old_session = self .store @@ -906,7 +911,7 @@ impl GossipMachine { ); } - Ok((Some(AnyToDeviceEvent::ForwardedRoomKey(event.clone())), session)) + Ok(session) } Err(e) => { warn!( @@ -928,7 +933,7 @@ impl GossipMachine { claimed_sender_key = event.content.sender_key.as_str(), "Received a forwarded room key that we didn't request", ); - Ok((None, None)) + Ok(None) } } } @@ -1150,7 +1155,7 @@ mod tests { let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap(); - let mut event = ToDeviceEvent { sender: alice_id().to_owned(), content }; + let event = ToDeviceEvent { sender: alice_id().to_owned(), content }; assert!( machine @@ -1165,8 +1170,8 @@ mod tests { .is_none() ); - let (_, first_session) = - machine.receive_forwarded_room_key(&session.sender_key, &mut event).await.unwrap(); + let first_session = + machine.receive_forwarded_room_key(&session.sender_key, &event).await.unwrap(); let first_session = first_session.unwrap(); assert_eq!(first_session.first_known_index(), 10); @@ -1197,10 +1202,10 @@ mod tests { let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap(); - let mut event = ToDeviceEvent { sender: alice_id().to_owned(), content }; + let event = ToDeviceEvent { sender: alice_id().to_owned(), content }; - let (_, second_session) = - machine.receive_forwarded_room_key(&session.sender_key, &mut event).await.unwrap(); + let second_session = + machine.receive_forwarded_room_key(&session.sender_key, &event).await.unwrap(); assert!(second_session.is_none()); @@ -1208,10 +1213,10 @@ mod tests { let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap(); - let mut event = ToDeviceEvent { sender: alice_id().to_owned(), content }; + let event = ToDeviceEvent { sender: alice_id().to_owned(), content }; - let (_, second_session) = - machine.receive_forwarded_room_key(&session.sender_key, &mut event).await.unwrap(); + let second_session = + machine.receive_forwarded_room_key(&session.sender_key, &event).await.unwrap(); assert_eq!(second_session.unwrap().first_known_index(), 0); } @@ -1446,11 +1451,9 @@ mod tests { let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap(); - if let AnyToDeviceEvent::ForwardedRoomKey(mut e) = decrypted.event.deserialize().unwrap() { - let (_, session) = alice_machine - .receive_forwarded_room_key(&decrypted.sender_key, &mut e) - .await - .unwrap(); + if let AnyToDeviceEvent::ForwardedRoomKey(e) = decrypted.event.deserialize().unwrap() { + let session = + alice_machine.receive_forwarded_room_key(&decrypted.sender_key, &e).await.unwrap(); alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap(); } else { panic!("Invalid decrypted event type"); @@ -1670,11 +1673,9 @@ mod tests { let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap(); - if let AnyToDeviceEvent::ForwardedRoomKey(mut e) = decrypted.event.deserialize().unwrap() { - let (_, session) = alice_machine - .receive_forwarded_room_key(&decrypted.sender_key, &mut e) - .await - .unwrap(); + if let AnyToDeviceEvent::ForwardedRoomKey(e) = decrypted.event.deserialize().unwrap() { + let session = + alice_machine.receive_forwarded_room_key(&decrypted.sender_key, &e).await.unwrap(); alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap(); } else { panic!("Invalid decrypted event type"); diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index d998081a1..58b35f9ff 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -89,6 +89,7 @@ impl SecretInfo { /// comparison pub fn as_key(&self) -> String { match &self { + #[allow(deprecated)] SecretInfo::KeyRequest(ref info) => format!( "keyRequest:{:}:{:}:{:}:{:}", info.room_id.as_str(), diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index adf01429e..b4ed0d601 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -299,7 +299,7 @@ pub struct UserDevices { } impl UserDevices { - /// Get the specific device with the given device id. + /// Get the specific device with the given device ID. pub fn get(&self, device_id: &DeviceId) -> Option { self.inner.get(device_id).map(|d| Device { inner: d.clone(), diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 2400f7829..6ca61fd9d 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -686,7 +686,7 @@ impl ReadOnlyUserIdentity { ) -> Result { master_key.verify_subkey(&self_signing_key)?; - Ok(Self { user_id: (&*master_key.0.user_id).into(), master_key, self_signing_key }) + Ok(Self { user_id: (*master_key.0.user_id).into(), master_key, self_signing_key }) } #[cfg(test)] @@ -799,7 +799,7 @@ impl ReadOnlyOwnUserIdentity { master_key.verify_subkey(&user_signing_key)?; Ok(Self { - user_id: (&*master_key.0.user_id).into(), + user_id: (*master_key.0.user_id).into(), master_key, self_signing_key, user_signing_key, diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 2f3416752..ada12d5b8 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -39,17 +39,16 @@ use ruma::{ EncryptedEventScheme, MegolmV1AesSha2Content, OriginalSyncRoomEncryptedEvent, RoomEncryptedEventContent, ToDeviceRoomEncryptedEvent, }, - room_key::ToDeviceRoomKeyEvent, secret::request::SecretName, - AnyMessageLikeEvent, AnyRoomEvent, AnyToDeviceEvent, MessageLikeEventContent, + AnyMessageLikeEvent, AnyRoomEvent, MessageLikeEventContent, }, - DeviceId, DeviceKeyAlgorithm, EventEncryptionAlgorithm, OwnedDeviceKeyId, OwnedTransactionId, - OwnedUserId, RoomId, TransactionId, UInt, UserId, + serde::Raw, + DeviceId, DeviceKeyAlgorithm, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, RoomId, + TransactionId, UInt, UserId, }; -use serde_json::Value; +use serde_json::{value::to_raw_value, Value}; use tracing::{debug, error, info, trace, warn}; use vodozemac::Ed25519Signature; -use zeroize::Zeroize; #[cfg(feature = "backups_v1")] use crate::backups::BackupMachine; @@ -60,7 +59,7 @@ use crate::{ olm::{ Account, CrossSigningStatus, EncryptionSettings, ExportedRoomKey, IdentityKeys, InboundGroupSession, OlmDecryptionInfo, PrivateCrossSigningIdentity, ReadOnlyAccount, - SessionKey, SessionType, + SessionType, }, requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest}, session_manager::{GroupSessionManager, SessionManager}, @@ -68,7 +67,13 @@ use crate::{ Changes, CryptoStore, DeviceChanges, IdentityChanges, MemoryStore, Result as StoreResult, SecretImportError, Store, }, - types::Signatures, + types::{ + events::{ + room_key::{RoomKeyContent, RoomKeyEvent}, + ToDeviceEvents, + }, + Signatures, + }, verification::{Verification, VerificationMachine, VerificationRequest}, CrossSigningKeyExport, ReadOnlyDevice, RoomKeyImportResult, SignatureError, ToDeviceRequest, }; @@ -79,7 +84,7 @@ use crate::{ pub struct OlmMachine { /// The unique user id that owns this account. user_id: Arc, - /// The unique device id of the device that holds this account. + /// The unique device ID of the device that holds this account. device_id: Arc, /// Our underlying Olm Account holding our identity keys. account: Account, @@ -272,7 +277,7 @@ impl OlmMachine { &self.user_id } - /// The unique device id that identifies this `OlmMachine`. + /// The unique device ID that identifies this `OlmMachine`. pub fn device_id(&self) -> &DeviceId { &self.device_id } @@ -538,17 +543,7 @@ impl OlmMachine { let mut decrypted = self.account.decrypt_to_device_event(event).await?; // Handle the decrypted event, e.g. fetch out Megolm sessions out of // the event. - if let (Some(event), group_session) = - self.handle_decrypted_to_device_event(&decrypted).await? - { - // Some events may have sensitive data e.g. private keys, while we - // want to notify our users that a private key was received we - // don't want them to be able to do silly things with it. Handling - // events modifies them and returns a modified one, so replace it - // here if we get one. - decrypted.deserialized_event = Some(event); - decrypted.inbound_group_session = group_session; - } + self.handle_decrypted_to_device_event(&mut decrypted).await?; Ok(decrypted) } @@ -558,53 +553,36 @@ impl OlmMachine { &self, sender_key: &str, signing_key: &str, - event: &mut ToDeviceRoomKeyEvent, - ) -> OlmResult<(Option, Option)> { - match event.content.algorithm { - EventEncryptionAlgorithm::MegolmV1AesSha2 => { - match SessionKey::from_base64(&event.content.session_key) { - Ok(session_key) => { - event.content.session_key.zeroize(); - let session = InboundGroupSession::new( - sender_key, - signing_key, - &event.content.room_id, - session_key, - None, - ); + event: &RoomKeyEvent, + ) -> OlmResult> { + match &event.content { + RoomKeyContent::MegolmV1AesSha2(content) => { + let session = InboundGroupSession::new( + sender_key, + signing_key, + &content.room_id, + &content.session_key, + None, + ); - info!( - sender = event.sender.as_str(), - sender_key = sender_key, - room_id = event.content.room_id.as_str(), - session_id = session.session_id(), - "Received a new room key", - ); - - let event = AnyToDeviceEvent::RoomKey(event.clone()); - - Ok((Some(event), Some(session))) - } - Err(e) => { - warn!( - sender = event.sender.as_str(), - sender_key = sender_key, - room_id = event.content.room_id.as_str(), - "Couldn't create a group session from a received room key" - ); - Err(e.into()) - } - } - } - _ => { - warn!( - sender = event.sender.as_str(), + info!( + sender = %event.sender, sender_key = sender_key, - room_id = event.content.room_id.as_str(), - algorithm = ?event.content.algorithm, + room_id = %content.room_id, + session_id = session.session_id(), + "Received a new room key", + ); + + Ok(Some(session)) + } + RoomKeyContent::Unknown(content) => { + warn!( + sender = %event.sender, + sender_key = sender_key, + algorithm = ?content.algorithm, "Received room key with unsupported key algorithm", ); - Ok((None, None)) + Ok(None) } } } @@ -739,9 +717,9 @@ impl OlmMachine { /// * `decrypted` - The decrypted event and some associated metadata. async fn handle_decrypted_to_device_event( &self, - decrypted: &OlmDecryptionInfo, - ) -> OlmResult<(Option, Option)> { - let event = match decrypted.event.deserialize() { + decrypted: &mut OlmDecryptionInfo, + ) -> OlmResult<()> { + let event: ToDeviceEvents = match decrypted.event.deserialize_as() { Ok(e) => e, Err(e) => { warn!( @@ -750,7 +728,8 @@ impl OlmMachine { error = ?e, "Decrypted to-device event failed to be deserialized correctly" ); - return Ok((None, None)); + + return Ok(()); } }; @@ -762,25 +741,34 @@ impl OlmMachine { ); match event { - AnyToDeviceEvent::RoomKey(mut e) => { - Ok(self.add_room_key(&decrypted.sender_key, &decrypted.signing_key, &mut e).await?) + ToDeviceEvents::RoomKey(e) => { + let session = + self.add_room_key(&decrypted.sender_key, &decrypted.signing_key, &e).await?; + decrypted.inbound_group_session = session; + } + ToDeviceEvents::ForwardedRoomKey(e) => { + let session = self + .key_request_machine + .receive_forwarded_room_key(&decrypted.sender_key, &e) + .await?; + decrypted.inbound_group_session = session; + } + ToDeviceEvents::SecretSend(mut e) => { + self.key_request_machine.receive_secret(&decrypted.sender_key, &mut e).await?; + decrypted.event = Raw::from_json(to_raw_value(&e)?) } - AnyToDeviceEvent::ForwardedRoomKey(mut e) => Ok(self - .key_request_machine - .receive_forwarded_room_key(&decrypted.sender_key, &mut e) - .await?), - AnyToDeviceEvent::SecretSend(mut e) => Ok(( - self.key_request_machine.receive_secret(&decrypted.sender_key, &mut e).await?, - None, - )), _ => { - warn!(event_type = ?event.event_type(), "Received an unexpected encrypted to-device event"); - Ok((Some(event), None)) + warn!( + event_type = ?event.event_type(), + "Received an unexpected encrypted to-device event" + ); } } + + Ok(()) } - async fn handle_verification_event(&self, event: &AnyToDeviceEvent) { + async fn handle_verification_event(&self, event: &ToDeviceEvents) { if let Err(e) = self.verification_machine.receive_any_event(event).await { error!("Error handling a verification event: {:?}", e); } @@ -823,28 +811,23 @@ impl OlmMachine { self.account.update_key_counts(one_time_key_count, unused_fallback_keys).await; } - async fn handle_to_device_event(&self, event: &AnyToDeviceEvent) { + async fn handle_to_device_event(&self, event: &ToDeviceEvents) { + use crate::types::events::ToDeviceEvents::*; + match event { - AnyToDeviceEvent::RoomKeyRequest(e) => { - self.key_request_machine.receive_incoming_key_request(e) - } - AnyToDeviceEvent::SecretRequest(e) => { - self.key_request_machine.receive_incoming_secret_request(e) - } - AnyToDeviceEvent::KeyVerificationAccept(..) - | AnyToDeviceEvent::KeyVerificationCancel(..) - | AnyToDeviceEvent::KeyVerificationKey(..) - | AnyToDeviceEvent::KeyVerificationMac(..) - | AnyToDeviceEvent::KeyVerificationRequest(..) - | AnyToDeviceEvent::KeyVerificationReady(..) - | AnyToDeviceEvent::KeyVerificationDone(..) - | AnyToDeviceEvent::KeyVerificationStart(..) => { + RoomKeyRequest(e) => self.key_request_machine.receive_incoming_key_request(e), + SecretRequest(e) => self.key_request_machine.receive_incoming_secret_request(e), + KeyVerificationAccept(..) + | KeyVerificationCancel(..) + | KeyVerificationKey(..) + | KeyVerificationMac(..) + | KeyVerificationRequest(..) + | KeyVerificationReady(..) + | KeyVerificationDone(..) + | KeyVerificationStart(..) => { self.handle_verification_event(event).await; } - AnyToDeviceEvent::Dummy(_) - | AnyToDeviceEvent::RoomKey(_) - | AnyToDeviceEvent::ForwardedRoomKey(_) - | AnyToDeviceEvent::RoomEncrypted(_) => {} + Dummy(_) | RoomKey(_) | ForwardedRoomKey(_) | RoomEncrypted(_) => {} _ => {} } } @@ -892,7 +875,7 @@ impl OlmMachine { } for mut raw_event in to_device_events.events { - let event = match raw_event.deserialize() { + let event: ToDeviceEvents = match raw_event.deserialize_as() { Ok(e) => e, Err(e) => { // Skip invalid events. @@ -900,6 +883,7 @@ impl OlmMachine { error = ?e, "Received an invalid to-device event" ); + events.push(raw_event); continue; } }; @@ -911,7 +895,7 @@ impl OlmMachine { ); match event { - AnyToDeviceEvent::RoomEncrypted(e) => { + ToDeviceEvents::RoomEncrypted(e) => { let decrypted = match self.decrypt_to_device_event(&e).await { Ok(e) => e, Err(err) => { @@ -950,11 +934,23 @@ impl OlmMachine { changes.inbound_group_sessions.push(group_session); } - if let Some(event) = decrypted.deserialized_event { - self.handle_to_device_event(&event).await; - } + match decrypted.event.deserialize_as() { + Ok(event) => { + self.handle_to_device_event(&event).await; - raw_event = decrypted.event; + raw_event = event + .serialize_zeroized() + .expect("Zeroizing and reserializing our events should always work") + .cast(); + } + Err(e) => { + warn!( + error = ?e, + "Received an invalid encrypted to-device event" + ); + raw_event = decrypted.event; + } + } } e => self.handle_to_device_event(&e).await, } @@ -1002,7 +998,12 @@ impl OlmMachine { Ok(self .key_request_machine - .request_key(room_id, &content.sender_key, &content.session_id) + .request_key( + room_id, + #[allow(deprecated)] + &content.sender_key, + &content.session_id, + ) .await?) } @@ -1050,7 +1051,12 @@ impl OlmMachine { ) -> MegolmResult { if let Some(session) = self .store - .get_inbound_group_session(room_id, &content.sender_key, &content.session_id) + .get_inbound_group_session( + room_id, + #[allow(deprecated)] + &content.sender_key, + &content.session_id, + ) .await? { // TODO check the message index. @@ -1084,13 +1090,24 @@ impl OlmMachine { } } - let encryption_info = - self.get_encryption_info(&session, &event.sender, &content.device_id).await?; + let encryption_info = self + .get_encryption_info( + &session, + &event.sender, + #[allow(deprecated)] + &content.device_id, + ) + .await?; Ok(RoomEvent { encryption_info: Some(encryption_info), event: decrypted_event }) } else { self.key_request_machine - .create_outgoing_key_request(room_id, &content.sender_key, &content.session_id) + .create_outgoing_key_request( + room_id, + #[allow(deprecated)] + &content.sender_key, + &content.session_id, + ) .await?; Err(MegolmError::MissingRoomKey) @@ -1114,6 +1131,7 @@ impl OlmMachine { match self.decrypt_megolm_v1_event(room_id, event, c).await { Ok(r) => Ok(r), Err(e) => { + #[allow(deprecated)] if let MegolmError::MissingRoomKey = e { // TODO log the withheld reason if we have one. debug!( @@ -1541,7 +1559,10 @@ pub(crate) mod tests { use matrix_sdk_test::{async_test, test_json}; use ruma::{ api::{ - client::keys::{claim_keys, get_keys, upload_keys}, + client::{ + keys::{claim_keys, get_keys, upload_keys}, + sync::sync_events::v3::ToDevice, + }, IncomingResponse, }, device_id, @@ -1563,6 +1584,7 @@ pub(crate) mod tests { uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceKeyId, UserId, }; + use serde_json::value::to_raw_value; use vodozemac::Ed25519PublicKey; use super::testing::response_from_file; @@ -1933,18 +1955,20 @@ pub(crate) mod tests { sender: alice.user_id().to_owned(), content: to_device_requests_to_content(to_device_requests), }; + let event = Raw::from_json(to_raw_value(&event).unwrap()); let alice_session = alice.group_session_manager.get_outbound_group_session(room_id).unwrap(); - let decrypted = bob.decrypt_to_device_event(&event).await.unwrap(); + let mut to_device = ToDevice::new(); + to_device.events.push(event); - bob.store.save_sessions(&[decrypted.session.session()]).await.unwrap(); - bob.store - .save_inbound_group_sessions(&[decrypted.inbound_group_session.unwrap()]) + let decrypted = bob + .receive_sync_changes(to_device, &Default::default(), &Default::default(), None) .await .unwrap(); - let event = decrypted.deserialized_event.unwrap(); + + let event = decrypted.events[0].deserialize().unwrap(); if let AnyToDeviceEvent::RoomKey(event) = event { assert_eq!(&event.sender, alice.user_id()); diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 002c6c6e3..2c12ef75b 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -100,7 +100,6 @@ pub(crate) struct OlmDecryptionInfo { pub sender: OwnedUserId, pub session: SessionType, pub message_hash: OlmMessageHash, - pub deserialized_event: Option, pub event: Raw, pub signing_key: String, pub sender_key: String, @@ -189,7 +188,6 @@ impl Account { event, signing_key, sender_key: content.sender_key.clone(), - deserialized_event: None, inbound_group_session: None, }), Err(OlmError::SessionWedged(user_id, sender_key)) => { @@ -459,7 +457,7 @@ pub struct ReadOnlyAccount { pub struct PickledAccount { /// The user id of the account owner. pub user_id: OwnedUserId, - /// The device id of the account owner. + /// The device ID of the account owner. pub device_id: OwnedDeviceId, /// The pickled version of the Olm account. pub pickle: AccountPickle, @@ -506,7 +504,7 @@ impl ReadOnlyAccount { &self.user_id } - /// Get the device id that owns this account. + /// Get the device ID that owns this account. pub fn device_id(&self) -> &DeviceId { &self.device_id } @@ -729,8 +727,8 @@ impl ReadOnlyAccount { let identity_keys = account.identity_keys(); Ok(Self { - user_id: (&*pickle.user_id).into(), - device_id: (&*pickle.device_id).into(), + user_id: (*pickle.user_id).into(), + device_id: (*pickle.device_id).into(), inner: Arc::new(Mutex::new(account)), identity_keys: Arc::new(identity_keys), shared: Arc::new(AtomicBool::from(pickle.shared)), @@ -955,7 +953,7 @@ impl ReadOnlyAccount { /// # Arguments /// * `device` - The other account's device. /// - /// * `key_map` - A map from the algorithm and device id to the one-time key + /// * `key_map` - A map from the algorithm and device ID to the one-time key /// that the other account created and shared with us. pub async fn create_outbound_session( &self, @@ -1040,7 +1038,9 @@ impl ReadOnlyAccount { last_use_time: now, }; - Ok(InboundCreationResult { session, plaintext: result.plaintext }) + let plaintext = String::from_utf8_lossy(&result.plaintext).to_string(); + + Ok(InboundCreationResult { session, plaintext }) } /// Create a group session pair. @@ -1083,7 +1083,7 @@ impl ReadOnlyAccount { &sender_key, &signing_key, room_id, - outbound.session_key().await, + &outbound.session_key().await, Some(visibility), ); diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 035ff6099..fa22f4a02 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -45,7 +45,6 @@ use vodozemac::{ }, PickleError, }; -use zeroize::Zeroize; use super::{BackedUpRoomKey, ExportedRoomKey, SessionKey}; use crate::error::{EventError, MegolmResult}; @@ -97,10 +96,10 @@ impl InboundGroupSession { sender_key: &str, signing_key: &str, room_id: &RoomId, - session_key: SessionKey, + session_key: &SessionKey, history_visibility: Option, ) -> Self { - let session = InnerSession::new(&session_key); + let session = InnerSession::new(session_key); let session_id = session.session_id(); let first_known_index = session.first_known_index(); @@ -159,10 +158,9 @@ impl InboundGroupSession { /// to create the `InboundGroupSession`. pub fn from_forwarded_key( sender_key: &str, - content: &mut ToDeviceForwardedRoomKeyEventContent, + content: &ToDeviceForwardedRoomKeyEventContent, ) -> Result { let key = ExportedSessionKey::from_base64(&content.session_key)?; - content.session_key.zeroize(); let session = InnerSession::import(&key); let first_known_index = session.first_known_index(); @@ -180,7 +178,7 @@ impl InboundGroupSession { first_known_index, history_visibility: None.into(), signing_keys: sender_claimed_key.into(), - room_id: (&*content.room_id).into(), + room_id: (*content.room_id).into(), forwarding_chains: forwarding_chains.into(), imported: true, backed_up: AtomicBool::new(false).into(), @@ -292,7 +290,7 @@ impl InboundGroupSession { history_visibility: pickle.history_visibility.into(), first_known_index, signing_keys: pickle.signing_key.into(), - room_id: (&*pickle.room_id).into(), + room_id: (*pickle.room_id).into(), forwarding_chains: pickle.forwarding_chains.into(), backed_up: AtomicBool::from(pickle.backed_up).into(), imported: pickle.imported, @@ -353,8 +351,9 @@ impl InboundGroupSession { let message = MegolmMessage::from_base64(&content.ciphertext)?; let decrypted = self.decrypt_helper(&message).await?; + let plaintext = String::from_utf8_lossy(&decrypted.plaintext); - let mut decrypted_value = serde_json::from_str::(&decrypted.plaintext)?; + let mut decrypted_value = serde_json::from_str::(&plaintext)?; let decrypted_object = decrypted_value.as_object_mut().ok_or(EventError::NotAnObject)?; let server_ts: i64 = event.origin_server_ts.0.into(); @@ -447,7 +446,7 @@ impl From for InboundGroupSession { history_visibility: None.into(), first_known_index, signing_keys: key.sender_claimed_keys.into(), - room_id: (&*key.room_id).into(), + room_id: (*key.room_id).into(), forwarding_chains: key.forwarding_curve25519_key_chain.into(), imported: true, backed_up: AtomicBool::from(false).into(), diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index 81dd1ae43..912356d87 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -402,7 +402,7 @@ impl OutboundGroupSession { // the session. // Find the first request that contains the given user id and - // device id. + // device ID. let shared = self.to_share_with_set.iter().find_map(|item| { let share_info = &item.value().1; @@ -470,8 +470,8 @@ impl OutboundGroupSession { /// /// # Arguments /// - /// * `device_id` - The device id of the device that created this session. - /// Put differently, our own device id. + /// * `device_id` - The device ID of the device that created this session. + /// Put differently, our own device ID. /// /// * `identity_keys` - The identity keys of the device that created this /// session, our own identity keys. diff --git a/crates/matrix-sdk-crypto/src/olm/mod.rs b/crates/matrix-sdk-crypto/src/olm/mod.rs index acdf29b95..de4db40e1 100644 --- a/crates/matrix-sdk-crypto/src/olm/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/mod.rs @@ -162,7 +162,7 @@ pub(crate) mod tests { "test_key", "test_key", room_id, - outbound.session_key().await, + &outbound.session_key().await, None, ); @@ -173,7 +173,10 @@ pub(crate) mod tests { let plaintext = "This is a secret to everybody".to_owned(); let ciphertext = outbound.encrypt_helper(plaintext.clone()).await; - assert_eq!(plaintext, inbound.decrypt_helper(&ciphertext).await.unwrap().plaintext); + assert_eq!( + plaintext.as_bytes(), + inbound.decrypt_helper(&ciphertext).await.unwrap().plaintext + ); } #[async_test] @@ -199,7 +202,7 @@ pub(crate) mod tests { "test_key", "test_key", room_id, - outbound.session_key().await, + &outbound.session_key().await, None, ); diff --git a/crates/matrix-sdk-crypto/src/olm/session.rs b/crates/matrix-sdk-crypto/src/olm/session.rs index e9d2a2a70..6120ef5da 100644 --- a/crates/matrix-sdk-crypto/src/olm/session.rs +++ b/crates/matrix-sdk-crypto/src/olm/session.rs @@ -83,6 +83,7 @@ impl Session { /// * `message` - The Olm message that should be decrypted. pub async fn decrypt(&mut self, message: &OlmMessage) -> Result { let plaintext = self.inner.lock().await.decrypt(message)?; + let plaintext = String::from_utf8_lossy(&plaintext).to_string(); self.last_use_time = SecondsSinceUnixEpoch::now(); Ok(plaintext) } @@ -187,7 +188,7 @@ impl Session { /// /// * `user_id` - Our own user id that the session belongs to. /// - /// * `device_id` - Our own device id that the session belongs to. + /// * `device_id` - Our own device ID that the session belongs to. /// /// * `our_idenity_keys` - An clone of the Arc to our own identity keys. /// diff --git a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs index bec371f41..1b6ddb3ea 100644 --- a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs @@ -633,7 +633,7 @@ impl PrivateCrossSigningIdentity { let user_signing = keys.user_signing_key.map(UserSigning::from_pickle).transpose()?; Ok(Self { - user_id: (&*pickle.user_id).into(), + user_id: (*pickle.user_id).into(), shared: Arc::new(AtomicBool::from(pickle.shared)), master_key: Arc::new(Mutex::new(master)), self_signing_key: Arc::new(Mutex::new(self_signing)), @@ -660,9 +660,7 @@ impl PrivateCrossSigningIdentity { #[cfg(test)] mod tests { use matrix_sdk_test::async_test; - use ruma::{ - device_id, signatures::CanonicalJsonValue, user_id, DeviceKeyAlgorithm, DeviceKeyId, UserId, - }; + use ruma::{device_id, user_id, CanonicalJsonValue, DeviceKeyAlgorithm, DeviceKeyId, UserId}; use serde_json::json; use super::{PrivateCrossSigningIdentity, Signing}; diff --git a/crates/matrix-sdk-crypto/src/olm/utility.rs b/crates/matrix-sdk-crypto/src/olm/utility.rs index 89328843c..dcca398d5 100644 --- a/crates/matrix-sdk-crypto/src/olm/utility.rs +++ b/crates/matrix-sdk-crypto/src/olm/utility.rs @@ -14,7 +14,7 @@ use std::convert::TryInto; -use ruma::{serde::CanonicalJsonValue, DeviceKeyAlgorithm, DeviceKeyId, UserId}; +use ruma::{CanonicalJsonValue, DeviceKeyAlgorithm, DeviceKeyId, UserId}; use serde::Serialize; use serde_json::Value; use vodozemac::{olm::Account, Ed25519PublicKey, Ed25519SecretKey, Ed25519Signature}; diff --git a/crates/matrix-sdk-crypto/src/store/caches.rs b/crates/matrix-sdk-crypto/src/store/caches.rs index c5680019b..271868007 100644 --- a/crates/matrix-sdk-crypto/src/store/caches.rs +++ b/crates/matrix-sdk-crypto/src/store/caches.rs @@ -239,7 +239,7 @@ mod tests { "test_key", "test_key", room_id, - outbound.session_key().await, + &outbound.session_key().await, None, ); diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index bb8111ae0..a82874627 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -37,6 +37,7 @@ use crate::{ fn encode_key_info(info: &SecretInfo) -> String { match info { + #[allow(deprecated)] SecretInfo::KeyRequest(info) => { format!("{}{}{}{}", info.room_id, info.sender_key, info.algorithm, info.session_id) } @@ -348,7 +349,7 @@ mod tests { "test_key", "test_key", room_id, - outbound.session_key().await, + &outbound.session_key().await, None, ); diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index f168ee264..574416cd4 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -535,10 +535,8 @@ impl Store { pub async fn import_secret( &self, secret_name: &SecretName, - secret: String, + secret: &str, ) -> Result<(), SecretImportError> { - let secret = zeroize::Zeroizing::new(secret); - match secret_name { SecretName::CrossSigningMasterKey | SecretName::CrossSigningUserSigningKey @@ -548,7 +546,7 @@ impl Store { { let identity = self.identity.lock().await; - identity.import_secret(public_identity, secret_name, &secret).await?; + identity.import_secret(public_identity, secret_name, secret).await?; info!( secret_name = secret_name.as_ref(), "Successfully imported a private cross signing key" @@ -579,7 +577,7 @@ impl Deref for Store { type Target = dyn CryptoStore; fn deref(&self) -> &Self::Target { - &*self.inner + self.inner.deref() } } @@ -737,7 +735,7 @@ pub trait CryptoStore: AsyncTraitDeps { /// * `dirty` - Should the user be also marked for a key query. async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result; - /// Get the device for the given user with the given device id. + /// Get the device for the given user with the given device ID. /// /// # Arguments /// @@ -805,3 +803,31 @@ pub trait CryptoStore: AsyncTraitDeps { /// request. async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()>; } + +/// A type that can be type-erased into `Arc`. +/// +/// This trait is not meant to be implemented directly outside +/// `matrix-sdk-crypto`, but it is automatically implemented for everything that +/// implements `CryptoStore`. +pub trait IntoCryptoStore { + #[doc(hidden)] + fn into_crypto_store(self) -> Arc; +} + +impl IntoCryptoStore for T +where + T: CryptoStore + Sized + 'static, +{ + fn into_crypto_store(self) -> Arc { + Arc::new(self) + } +} + +impl IntoCryptoStore for Arc +where + T: CryptoStore + 'static, +{ + fn into_crypto_store(self) -> Arc { + self + } +} diff --git a/crates/matrix-sdk-crypto/src/types/device_keys.rs b/crates/matrix-sdk-crypto/src/types/device_keys.rs index 50e23147e..e4f5a98cb 100644 --- a/crates/matrix-sdk-crypto/src/types/device_keys.rs +++ b/crates/matrix-sdk-crypto/src/types/device_keys.rs @@ -63,7 +63,7 @@ pub struct DeviceKeys { } impl DeviceKeys { - /// Creates a new `DeviceKeys` from the given user id, device id, + /// Creates a new `DeviceKeys` from the given user id, device ID, /// algorithms, keys and signatures. pub fn new( user_id: OwnedUserId, diff --git a/crates/matrix-sdk-crypto/src/types/events/mod.rs b/crates/matrix-sdk-crypto/src/types/events/mod.rs new file mode 100644 index 000000000..a85fecef6 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/events/mod.rs @@ -0,0 +1,39 @@ +// 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. + +//! Types modeling end-to-end encryption related Matrix events +//! +//! These types aim to provide a more strict variant of the equivalent Ruma +//! types. Once deserialized they aim to zeroize all the secret material once +//! the type is dropped. + +pub mod room_key; +pub mod secret_send; +mod to_device; + +pub use to_device::{ToDeviceCustomEvent, ToDeviceEvent, ToDeviceEvents}; + +/// A trait for event contents to define their event type. +pub trait EventType { + /// Get the event type of the event content. + fn event_type(&self) -> &str; +} + +fn from_str<'a, T, E>(string: &'a str) -> Result +where + T: serde::Deserialize<'a>, + E: serde::de::Error, +{ + serde_json::from_str(string).map_err(serde::de::Error::custom) +} diff --git a/crates/matrix-sdk-crypto/src/types/events/room_key.rs b/crates/matrix-sdk-crypto/src/types/events/room_key.rs new file mode 100644 index 000000000..5ce6ecc2b --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/events/room_key.rs @@ -0,0 +1,203 @@ +// 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. + +//! Types for `m.room_key` to-device events. + +use std::collections::BTreeMap; + +use ruma::{serde::Raw, EventEncryptionAlgorithm, OwnedRoomId, RoomId}; +use serde::{Deserialize, Serialize}; +use serde_json::{value::to_raw_value, Value}; +use vodozemac::megolm::SessionKey; + +use super::{EventType, ToDeviceEvent}; + +/// The `m.room_key` to-device event. +pub type RoomKeyEvent = ToDeviceEvent; + +impl EventType for RoomKeyContent { + fn event_type(&self) -> &str { + "m.room_key" + } +} + +/// The `m.room_key` event content. +/// +/// This is an enum over the different room key algorithms we support. +/// +/// This event type is used to exchange keys for end-to-end encryption. +/// Typically it is encrypted as an m.room.encrypted event, then sent as a +/// to-device event. +#[derive(Debug, Deserialize)] +#[serde(try_from = "RoomKeyHelper")] +pub enum RoomKeyContent { + /// The `m.megolm.v1.aes-sha2` variant of the `m.room_key` content. + MegolmV1AesSha2(Box), + /// An unknown and unsupported variant of the `m.room_key` content. + Unknown(UnknownRoomKey), +} + +impl RoomKeyContent { + pub(super) fn serialize_zeroized(&self) -> Result, serde_json::Error> { + #[derive(Serialize)] + struct Helper<'a> { + pub room_id: &'a RoomId, + pub session_id: &'a str, + pub session_key: &'a str, + #[serde(flatten)] + other: &'a BTreeMap, + } + + match self { + RoomKeyContent::MegolmV1AesSha2(c) => { + let helper = Helper { + room_id: &c.room_id, + session_id: &c.session_id, + session_key: "", + other: &c.other, + }; + + let helper = RoomKeyHelper { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + other: serde_json::to_value(helper)?, + }; + + Ok(Raw::from_json(to_raw_value(&helper)?)) + } + RoomKeyContent::Unknown(c) => Ok(Raw::from_json(to_raw_value(&c)?)), + } + } +} + +/// The `m.megolm.v1.aes-sha2` variant of the `m.room_key` content. +#[derive(Deserialize, Serialize)] +pub struct MegolmV1AesSha2Content { + /// The room where the key is used. + pub room_id: OwnedRoomId, + /// The ID of the session that the key is for. + pub session_id: String, + /// The key to be exchanged. Can be used to create a [`InboundGroupSession`] + /// that can be used to decrypt room events. + /// + /// [`InboundGroupSession`]: vodozemac::megolm::InboundGroupSession + pub session_key: SessionKey, + /// Any other, custom and non-specced fields of the content. + #[serde(flatten)] + other: BTreeMap, +} + +impl std::fmt::Debug for MegolmV1AesSha2Content { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MegolmV1AesSha2Content") + .field("room_id", &self.room_id) + .field("session_id", &self.session_id) + .finish_non_exhaustive() + } +} + +/// An unknown and unsupported `m.room_key` algorithm. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UnknownRoomKey { + /// The algorithm of the unknown room key. + pub algorithm: EventEncryptionAlgorithm, + /// The other data of the unknown room key. + #[serde(flatten)] + other: BTreeMap, +} + +#[derive(Deserialize, Serialize)] +struct RoomKeyHelper { + algorithm: EventEncryptionAlgorithm, + #[serde(flatten)] + other: Value, +} + +impl TryFrom for RoomKeyContent { + type Error = serde_json::Error; + + fn try_from(value: RoomKeyHelper) -> Result { + Ok(match value.algorithm { + EventEncryptionAlgorithm::MegolmV1AesSha2 => { + let content: MegolmV1AesSha2Content = serde_json::from_value(value.other)?; + Self::MegolmV1AesSha2(content.into()) + } + _ => Self::Unknown(UnknownRoomKey { + algorithm: value.algorithm, + other: serde_json::from_value(value.other)?, + }), + }) + } +} + +impl Serialize for RoomKeyContent { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let helper = match self { + Self::MegolmV1AesSha2(r) => RoomKeyHelper { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + other: serde_json::to_value(r).map_err(serde::ser::Error::custom)?, + }, + Self::Unknown(r) => RoomKeyHelper { + algorithm: r.algorithm.clone(), + other: serde_json::to_value(r.other.clone()).map_err(serde::ser::Error::custom)?, + }, + }; + + helper.serialize(serializer) + } +} + +#[cfg(test)] +pub(super) mod test { + use matches::assert_matches; + use serde_json::{json, Value}; + + use super::RoomKeyEvent; + use crate::types::events::room_key::RoomKeyContent; + + pub fn json() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "m.custom": "something custom", + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!Cuyf34gef24t:localhost", + "session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I", + "session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjv\ + SQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXi\ + jVv80fMPHfGc49hPdu8A+xnwD4SQiYdFmSWJOIqsxeo/fiH\ + tino//CDQENtcKuEt0I9s0+Kk4YSH310Szse2RQ+vjple31\ + QrCexmqfFJzkR/BJ5ogJHrPBQL0LgsPyglIbMTLg7qygIaY\ + U5Fe2QdKMH7nTZPNIRHh1RaMfHVETAUJBax88EWZBoifk80\ + gdHUwHSgMk77vCc2a5KHKLDA" + }, + "type": "m.room_key", + "m.custom.top": "something custom in the top", + }) + } + + #[test] + fn deserialization() -> Result<(), serde_json::Error> { + let json = json(); + let event: RoomKeyEvent = serde_json::from_value(json.clone())?; + + assert_matches!(event.content, RoomKeyContent::MegolmV1AesSha2(_)); + let serialized = serde_json::to_value(event)?; + assert_eq!(json, serialized); + + Ok(()) + } +} diff --git a/crates/matrix-sdk-crypto/src/types/events/secret_send.rs b/crates/matrix-sdk-crypto/src/types/events/secret_send.rs new file mode 100644 index 000000000..d288fe09d --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/events/secret_send.rs @@ -0,0 +1,106 @@ +// 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. + +//! Types for `m.secret.send` to-device events. + +use std::collections::BTreeMap; + +use ruma::events::secret::request::SecretName; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use zeroize::Zeroize; + +use super::{EventType, ToDeviceEvent}; + +/// The `m.secret.send` to-device event. +pub type SecretSendEvent = ToDeviceEvent; + +/// The `m.secret.send` event content. +/// +/// Sent by a client to share a secret with another device, in response to an +/// `m.secret.request` event. It must be encrypted as an `m.room.encrypted` +/// event, then sent as a to-device event. +#[derive(Serialize, Deserialize)] +pub struct SecretSendContent { + /// The ID of the request that this a response to. + pub request_id: String, + /// The contents of the secret. + pub secret: String, + /// The name of the secret, typically not part of the event but can be + /// inserted when processing `m.secret.send` events so other event consumers + /// know which secret this event contains. + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub secret_name: Option, + /// Any other, custom and non-specced fields of the content. + #[serde(flatten)] + other: BTreeMap, +} + +impl Zeroize for SecretSendContent { + fn zeroize(&mut self) { + self.secret.zeroize(); + } +} + +impl Drop for SecretSendContent { + fn drop(&mut self) { + self.zeroize() + } +} + +impl std::fmt::Debug for SecretSendContent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SecretSendContent") + .field("request_id", &self.request_id) + .field("secret_name", &self.secret_name) + .finish_non_exhaustive() + } +} + +impl EventType for SecretSendContent { + fn event_type(&self) -> &str { + "m.secret.send" + } +} + +#[cfg(test)] +pub(crate) mod test { + use serde_json::{json, Value}; + + use super::SecretSendEvent; + + pub(crate) fn json() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "request_id": "randomly_generated_id_9573", + "secret": "ThisIsASecretDon'tTellAnyone" + }, + "type": "m.secret.send", + }) + } + + #[test] + fn deserialization() -> Result<(), serde_json::Error> { + let json = json(); + let event: SecretSendEvent = serde_json::from_value(json.clone())?; + + assert_eq!(&event.content.secret, "ThisIsASecretDon'tTellAnyone"); + + let serialized = serde_json::to_value(event)?; + assert_eq!(json, serialized); + + Ok(()) + } +} diff --git a/crates/matrix-sdk-crypto/src/types/events/to_device.rs b/crates/matrix-sdk-crypto/src/types/events/to_device.rs new file mode 100644 index 000000000..8918f2bc9 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/events/to_device.rs @@ -0,0 +1,498 @@ +// 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. + +use std::{collections::BTreeMap, fmt::Debug}; + +use ruma::{ + events::{ + dummy::ToDeviceDummyEvent, + forwarded_room_key::ToDeviceForwardedRoomKeyEvent, + key::verification::{ + accept::ToDeviceKeyVerificationAcceptEvent, cancel::ToDeviceKeyVerificationCancelEvent, + done::ToDeviceKeyVerificationDoneEvent, key::ToDeviceKeyVerificationKeyEvent, + mac::ToDeviceKeyVerificationMacEvent, ready::ToDeviceKeyVerificationReadyEvent, + request::ToDeviceKeyVerificationRequestEvent, start::ToDeviceKeyVerificationStartEvent, + }, + room::encrypted::ToDeviceRoomEncryptedEvent, + room_key_request::ToDeviceRoomKeyRequestEvent, + secret::request::{SecretName, ToDeviceSecretRequestEvent}, + EventContent, ToDeviceEventType, + }, + serde::Raw, + OwnedUserId, UserId, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{ + value::{to_raw_value, RawValue}, + Value, +}; +use zeroize::Zeroize; + +use super::{room_key::RoomKeyEvent, secret_send::SecretSendEvent, EventType}; +use crate::types::events::from_str; + +/// An enum over the various to-device events we support. +#[derive(Debug)] +pub enum ToDeviceEvents { + /// A to-device event of an unknown or custom type. + Custom(ToDeviceCustomEvent), + /// The `m.dummy` to-device event. + Dummy(ToDeviceDummyEvent), + + /// The `m.key.verification.accept` to-device event. + KeyVerificationAccept(ToDeviceKeyVerificationAcceptEvent), + /// The `m.key.verification.cancel` to-device event. + KeyVerificationCancel(ToDeviceKeyVerificationCancelEvent), + /// The `m.key.verification.key` to-device event. + KeyVerificationKey(ToDeviceKeyVerificationKeyEvent), + /// The `m.key.verification.mac` to-device event. + KeyVerificationMac(ToDeviceKeyVerificationMacEvent), + /// The `m.key.verification.done` to-device event. + KeyVerificationDone(ToDeviceKeyVerificationDoneEvent), + /// The `m.key.verification.start` to-device event. + KeyVerificationStart(ToDeviceKeyVerificationStartEvent), + /// The `m.key.verification.ready` to-device event. + KeyVerificationReady(ToDeviceKeyVerificationReadyEvent), + /// The `m.key.verification.request` to-device event. + KeyVerificationRequest(ToDeviceKeyVerificationRequestEvent), + + /// The `m.room.encrypted` to-device event. + RoomEncrypted(ToDeviceRoomEncryptedEvent), + /// The `m.room_key` to-device event. + RoomKey(RoomKeyEvent), + /// The `m.room_key_request` to-device event. + RoomKeyRequest(ToDeviceRoomKeyRequestEvent), + /// The `m.forwarded_room_key` to-device event. + ForwardedRoomKey(ToDeviceForwardedRoomKeyEvent), + /// The `m.secret.send` to-device event. + SecretSend(SecretSendEvent), + /// The `m.secret.request` to-device event. + SecretRequest(ToDeviceSecretRequestEvent), +} + +impl ToDeviceEvents { + /// The sender of the to-device event. + pub fn sender(&self) -> &UserId { + match self { + ToDeviceEvents::Custom(e) => &e.sender, + ToDeviceEvents::Dummy(e) => &e.sender, + + ToDeviceEvents::KeyVerificationAccept(e) => &e.sender, + ToDeviceEvents::KeyVerificationCancel(e) => &e.sender, + ToDeviceEvents::KeyVerificationKey(e) => &e.sender, + ToDeviceEvents::KeyVerificationMac(e) => &e.sender, + ToDeviceEvents::KeyVerificationDone(e) => &e.sender, + ToDeviceEvents::KeyVerificationStart(e) => &e.sender, + ToDeviceEvents::KeyVerificationReady(e) => &e.sender, + ToDeviceEvents::KeyVerificationRequest(e) => &e.sender, + + ToDeviceEvents::RoomEncrypted(e) => &e.sender, + ToDeviceEvents::RoomKey(e) => &e.sender, + ToDeviceEvents::RoomKeyRequest(e) => &e.sender, + ToDeviceEvents::ForwardedRoomKey(e) => &e.sender, + + ToDeviceEvents::SecretSend(e) => &e.sender, + ToDeviceEvents::SecretRequest(e) => &e.sender, + } + } + + /// The event type of the to-device event. + pub fn event_type(&self) -> ToDeviceEventType { + match self { + ToDeviceEvents::Custom(e) => ToDeviceEventType::from(e.event_type.to_owned()), + ToDeviceEvents::Dummy(e) => e.content.event_type(), + + ToDeviceEvents::KeyVerificationAccept(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationCancel(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationKey(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationMac(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationDone(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationStart(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationReady(e) => e.content.event_type(), + ToDeviceEvents::KeyVerificationRequest(e) => e.content.event_type(), + + ToDeviceEvents::RoomEncrypted(e) => e.content.event_type(), + ToDeviceEvents::RoomKey(_) => ToDeviceEventType::RoomKey, + ToDeviceEvents::RoomKeyRequest(e) => e.content.event_type(), + ToDeviceEvents::ForwardedRoomKey(e) => e.content.event_type(), + + ToDeviceEvents::SecretSend(_) => ToDeviceEventType::SecretSend, + ToDeviceEvents::SecretRequest(e) => e.content.event_type(), + } + } + + /// Serialize this event into a Raw variant while zeroizing any secrets it + /// might contain. + /// + /// Secrets in Matrix are usually base64 encoded strings, zeroizing in this + /// context means that the secret will be converted into an empty string. + /// + /// The following secrets will be zeroized by this method: + /// + /// * `m.room_key` - The `session_key` field. + /// * `m.forwarded_room_key` - The `session_key` field. + /// * `m.secret.send` - The `secret` field will be zeroized, unless the + /// secret name of the matching `m.secret.request` event was + /// `m.megolm_backup.v1`. + /// + /// **Warning**: Some events won't be able to be deserialized into the + /// `ToDeviceEvents` type again since they might expect a valid `SessionKey` + /// for `m.room.key` events or valid base64 for some other secrets. + /// + /// You can do a couple of things to avoid this problem: + /// + /// 1. Call `Raw::cast()` to convert the event to another, less strict type. + /// [`AnyToDeviceEvent`] from Ruma will work. + /// + /// 2. Call `Raw::deserialize_as()` to deserialize into a less strict type. + /// + /// 3. Pass the event over FFI, losing the exact type information, this will + /// mostl likely end up using a less strict type naturally. + /// + /// [`AnyToDeviceEvent`]: ruma::events::AnyToDeviceEvent + pub(crate) fn serialize_zeroized(self) -> Result, serde_json::Error> { + let serialized = match self { + ToDeviceEvents::Custom(_) + | ToDeviceEvents::Dummy(_) + | ToDeviceEvents::KeyVerificationAccept(_) + | ToDeviceEvents::KeyVerificationCancel(_) + | ToDeviceEvents::KeyVerificationKey(_) + | ToDeviceEvents::KeyVerificationMac(_) + | ToDeviceEvents::KeyVerificationDone(_) + | ToDeviceEvents::KeyVerificationStart(_) + | ToDeviceEvents::KeyVerificationReady(_) + | ToDeviceEvents::KeyVerificationRequest(_) + | ToDeviceEvents::RoomEncrypted(_) + | ToDeviceEvents::RoomKeyRequest(_) + | ToDeviceEvents::SecretRequest(_) => Raw::from_json(to_raw_value(&self)?), + ToDeviceEvents::RoomKey(e) => { + let event_type = e.content.event_type(); + let content = e.content.serialize_zeroized()?; + + #[derive(Serialize)] + struct Helper<'a, C> { + sender: &'a UserId, + content: &'a Raw, + #[serde(rename = "type")] + event_type: &'a str, + } + + let helper = Helper { sender: &e.sender, content: &content, event_type }; + + let raw_value = to_raw_value(&helper)?; + + Raw::from_json(raw_value) + } + ToDeviceEvents::ForwardedRoomKey(mut e) => { + e.content.session_key.zeroize(); + Raw::from_json(to_raw_value(&e)?) + } + ToDeviceEvents::SecretSend(mut e) => { + if let Some(SecretName::RecoveryKey) = e.content.secret_name { + // We don't zeroize the recovery key since it requires + // additional requests and possibly user-interaction to be + // verified. We let the user deal with this. + } else { + e.content.secret.zeroize(); + } + Raw::from_json(to_raw_value(&e)?) + } + }; + + Ok(serialized) + } +} + +/// A to-device event with an unknown type and content. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ToDeviceCustomEvent { + /// The sender of the to-device event. + pub sender: OwnedUserId, + /// The content of the to-device event. + pub content: BTreeMap, + /// The type of the to-device event. + #[serde(rename = "type")] + pub event_type: String, + /// Any other unknown data of the to-device event. + #[serde(flatten)] + other: BTreeMap, +} + +/// Generic to-device event with a known type and content. +#[derive(Debug, Deserialize)] +pub struct ToDeviceEvent +where + C: EventType + Debug + Sized + Serialize, +{ + /// The sender of the to-device event. + pub sender: OwnedUserId, + /// The content of the to-device event. + pub content: C, + /// Any other unknown data of the to-device event. + #[serde(flatten)] + other: BTreeMap, +} + +impl Serialize for ToDeviceEvent +where + C: EventType + Debug + Sized + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + struct Helper<'a, C> { + sender: &'a UserId, + content: &'a C, + #[serde(rename = "type")] + event_type: &'a str, + #[serde(flatten)] + other: &'a BTreeMap, + } + + let event_type = self.content.event_type(); + + let helper = + Helper { sender: &self.sender, content: &self.content, event_type, other: &self.other }; + + helper.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ToDeviceEvents { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Debug, Deserialize)] + struct Helper<'a> { + #[serde(rename = "type")] + event_type: &'a str, + } + + let json = Box::::deserialize(deserializer)?; + let helper: Helper<'_> = + serde_json::from_str(json.get()).map_err(serde::de::Error::custom)?; + + let json = json.get(); + + Ok(match helper.event_type { + "m.dummy" => ToDeviceEvents::Dummy(from_str(json)?), + + "m.key.verification.accept" => ToDeviceEvents::KeyVerificationAccept(from_str(json)?), + "m.key.verification.cancel" => ToDeviceEvents::KeyVerificationCancel(from_str(json)?), + "m.key.verification.done" => ToDeviceEvents::KeyVerificationDone(from_str(json)?), + "m.key.verification.key" => ToDeviceEvents::KeyVerificationKey(from_str(json)?), + "m.key.verification.mac" => ToDeviceEvents::KeyVerificationMac(from_str(json)?), + "m.key.verification.start" => ToDeviceEvents::KeyVerificationStart(from_str(json)?), + "m.key.verification.ready" => ToDeviceEvents::KeyVerificationReady(from_str(json)?), + "m.key.verification.request" => ToDeviceEvents::KeyVerificationRequest(from_str(json)?), + + "m.room.encrypted" => ToDeviceEvents::RoomEncrypted(from_str(json)?), + "m.room_key" => ToDeviceEvents::RoomKey(from_str(json)?), + "m.forwarded_room_key" => ToDeviceEvents::ForwardedRoomKey(from_str(json)?), + "m.room_key_request" => ToDeviceEvents::RoomKeyRequest(from_str(json)?), + + "m.secret.send" => ToDeviceEvents::SecretSend(from_str(json)?), + "m.secret.request" => ToDeviceEvents::SecretRequest(from_str(json)?), + + _ => ToDeviceEvents::Custom(from_str(json)?), + }) + } +} + +impl Serialize for ToDeviceEvents { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + ToDeviceEvents::Custom(e) => e.serialize(serializer), + ToDeviceEvents::Dummy(e) => e.serialize(serializer), + + ToDeviceEvents::KeyVerificationAccept(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationCancel(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationKey(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationMac(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationDone(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationStart(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationReady(e) => e.serialize(serializer), + ToDeviceEvents::KeyVerificationRequest(e) => e.serialize(serializer), + + ToDeviceEvents::RoomEncrypted(e) => e.serialize(serializer), + ToDeviceEvents::RoomKey(e) => e.serialize(serializer), + ToDeviceEvents::RoomKeyRequest(e) => e.serialize(serializer), + ToDeviceEvents::ForwardedRoomKey(e) => e.serialize(serializer), + + ToDeviceEvents::SecretSend(e) => e.serialize(serializer), + ToDeviceEvents::SecretRequest(e) => e.serialize(serializer), + } + } +} + +#[cfg(test)] +mod test { + use matches::assert_matches; + use serde_json::{json, Value}; + + use super::ToDeviceEvents; + + fn custom_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "custom_key": "custom_value", + }, + "m.custom.top": "something custom in the top", + "type": "m.custom.event", + }) + } + + fn key_verification_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "from_device": "AliceDevice2", + "methods": [ + "m.sas.v1" + ], + "timestamp": 1559598944869u64, + "transaction_id": "S0meUniqueAndOpaqueString" + }, + "type": "m.key.verification.request" + }) + } + + fn dummy_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": {}, + "type": "m.dummy" + }) + } + + fn secret_request_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "name": "org.example.some.secret", + "action": "request", + "requesting_device_id": "ABCDEFG", + "request_id": "randomly_generated_id_9573" + }, + "type": "m.secret.request" + }) + } + + fn room_encrypted_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "algorithm": "m.olm.v1.curve25519-aes-sha2", + "sender_key": "", + "ciphertext": { + "": { + "type": 0, + "body": "" + } + } + }, + "type": "m.room.encrypted", + }) + } + + fn forwarded_room_key_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "forwarding_curve25519_key_chain": [ + "hPQNcabIABgGnx3/ACv/jmMmiQHoeFfuLB17tzWp6Hw" + ], + "room_id": "!Cuyf34gef24t:localhost", + "sender_claimed_ed25519_key": "aj40p+aw64yPIdsxoog8jhPu9i7l7NcFRecuOQblE3Y", + "sender_key": "RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU", + "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ", + "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8Llf..." + }, + "type": "m.forwarded_room_key" + }) + } + + fn room_key_request_event() -> Value { + json!({ + "sender": "@alice:example.org", + "content": { + "action": "request", + "body": { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!Cuyf34gef24t:localhost", + "sender_key": "RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU", + "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ" + }, + "request_id": "1495474790150.19", + "requesting_device_id": "RJYKSTBOIE" + }, + "type": "m.room_key_request" + }) + } + + #[test] + fn deserialization() -> Result<(), serde_json::Error> { + macro_rules! assert_serialization_roundtrip { + ( $( $json:path => $to_device_events:ident ),* $(,)? ) => { + $( + let json = $json(); + let event: ToDeviceEvents = serde_json::from_value(json.clone())?; + + assert_matches!(event, ToDeviceEvents::$to_device_events(_)); + let serialized = serde_json::to_value(event)?; + assert_eq!(json, serialized); + )* + } + } + + assert_serialization_roundtrip!( + // `m.room_key + crate::types::events::room_key::test::json => RoomKey, + + // `m.forwarded_room_key` + forwarded_room_key_event => ForwardedRoomKey, + + // `m.room_key_request` + room_key_request_event => RoomKeyRequest, + + // `m.secret.send` + crate::types::events::secret_send::test::json => SecretSend, + + // `m.secret.request` + secret_request_event => SecretRequest, + + // Unknown event + custom_event => Custom, + + // `m.key.verification.request` + key_verification_event => KeyVerificationRequest, + + // `m.dummy` + dummy_event => Dummy, + + // `m.room.encrypted` + room_encrypted_event => RoomEncrypted, + ); + + Ok(()) + } +} diff --git a/crates/matrix-sdk-crypto/src/types/mod.rs b/crates/matrix-sdk-crypto/src/types/mod.rs index 7f4326285..9d966b73c 100644 --- a/crates/matrix-sdk-crypto/src/types/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/mod.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Module containing customized types modeling Matrix keys. +//! Module containing customized types modeling Matrix keys and events. //! //! These types were mostly taken from the Ruma project. The types differ in two //! important ways to the Ruma types of the same name: //! //! 1. They are using vodozemac types so we directly deserialize into a -//! vodozemac curve25519 or ed25519 key. +//! vodozemac Curve25519 or Ed25519 key. //! 2. They support lossless serialization cycles in a canonical JSON supported //! way, meaning the white-space and field order won't be preserved but the //! data will. @@ -26,6 +26,7 @@ mod backup; mod cross_signing_key; mod device_keys; +pub mod events; mod one_time_keys; use std::collections::BTreeMap; diff --git a/crates/matrix-sdk-crypto/src/verification/event_enums.rs b/crates/matrix-sdk-crypto/src/verification/event_enums.rs index 137b1bdec..340050c9f 100644 --- a/crates/matrix-sdk-crypto/src/verification/event_enums.rs +++ b/crates/matrix-sdk-crypto/src/verification/event_enums.rs @@ -40,11 +40,10 @@ use ruma::{ VerificationMethod, }, room::message::{KeyVerificationRequestEventContent, MessageType}, - AnyMessageLikeEvent, AnyMessageLikeEventContent, AnyToDeviceEvent, AnyToDeviceEventContent, - MessageLikeEvent, + AnyMessageLikeEvent, AnyMessageLikeEventContent, AnyToDeviceEventContent, MessageLikeEvent, }, - serde::{Base64, CanonicalJsonValue}, - DeviceId, MilliSecondsSinceUnixEpoch, OwnedRoomId, UserId, + serde::Base64, + CanonicalJsonValue, DeviceId, MilliSecondsSinceUnixEpoch, OwnedRoomId, UserId, }; use super::FlowId; @@ -52,7 +51,7 @@ use super::FlowId; #[derive(Debug)] pub enum AnyEvent<'a> { Room(&'a AnyMessageLikeEvent), - ToDevice(&'a AnyToDeviceEvent), + ToDevice(&'a ToDeviceEvents), } impl AnyEvent<'_> { @@ -67,7 +66,7 @@ impl AnyEvent<'_> { match self { AnyEvent::Room(e) => Some(e.origin_server_ts()), AnyEvent::ToDevice(e) => match e { - AnyToDeviceEvent::KeyVerificationRequest(e) => Some(e.content.timestamp), + ToDeviceEvents::KeyVerificationRequest(e) => Some(e.content.timestamp), _ => None, }, } @@ -111,28 +110,24 @@ impl AnyEvent<'_> { _ => None, }, AnyEvent::ToDevice(e) => match e { - AnyToDeviceEvent::KeyVerificationRequest(e) => { + ToDeviceEvents::KeyVerificationRequest(e) => { Some(RequestContent::from(&e.content).into()) } - AnyToDeviceEvent::KeyVerificationReady(e) => { + ToDeviceEvents::KeyVerificationReady(e) => { Some(ReadyContent::from(&e.content).into()) } - AnyToDeviceEvent::KeyVerificationStart(e) => { + ToDeviceEvents::KeyVerificationStart(e) => { Some(StartContent::from(&e.content).into()) } - AnyToDeviceEvent::KeyVerificationCancel(e) => { + ToDeviceEvents::KeyVerificationCancel(e) => { Some(CancelContent::from(&e.content).into()) } - AnyToDeviceEvent::KeyVerificationAccept(e) => { + ToDeviceEvents::KeyVerificationAccept(e) => { Some(AcceptContent::from(&e.content).into()) } - AnyToDeviceEvent::KeyVerificationKey(e) => { - Some(KeyContent::from(&e.content).into()) - } - AnyToDeviceEvent::KeyVerificationMac(e) => { - Some(MacContent::from(&e.content).into()) - } - AnyToDeviceEvent::KeyVerificationDone(e) => { + ToDeviceEvents::KeyVerificationKey(e) => Some(KeyContent::from(&e.content).into()), + ToDeviceEvents::KeyVerificationMac(e) => Some(MacContent::from(&e.content).into()), + ToDeviceEvents::KeyVerificationDone(e) => { Some(DoneContent::from(&e.content).into()) } _ => None, @@ -147,8 +142,8 @@ impl<'a> From<&'a AnyMessageLikeEvent> for AnyEvent<'a> { } } -impl<'a> From<&'a AnyToDeviceEvent> for AnyEvent<'a> { - fn from(e: &'a AnyToDeviceEvent) -> Self { +impl<'a> From<&'a ToDeviceEvents> for AnyEvent<'a> { + fn from(e: &'a ToDeviceEvents) -> Self { Self::ToDevice(e) } } @@ -198,33 +193,33 @@ impl TryFrom<&AnyMessageLikeEvent> for FlowId { } } -impl TryFrom<&AnyToDeviceEvent> for FlowId { +impl TryFrom<&ToDeviceEvents> for FlowId { type Error = (); - fn try_from(value: &AnyToDeviceEvent) -> Result { + fn try_from(value: &ToDeviceEvents) -> Result { match value { - AnyToDeviceEvent::KeyVerificationRequest(e) => { + ToDeviceEvents::KeyVerificationRequest(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationReady(e) => { + ToDeviceEvents::KeyVerificationReady(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationStart(e) => { + ToDeviceEvents::KeyVerificationStart(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationCancel(e) => { + ToDeviceEvents::KeyVerificationCancel(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationAccept(e) => { + ToDeviceEvents::KeyVerificationAccept(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationKey(e) => { + ToDeviceEvents::KeyVerificationKey(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationMac(e) => { + ToDeviceEvents::KeyVerificationMac(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } - AnyToDeviceEvent::KeyVerificationDone(e) => { + ToDeviceEvents::KeyVerificationDone(e) => { Ok(FlowId::from(e.content.transaction_id.to_owned())) } _ => Err(()), @@ -683,7 +678,10 @@ impl From<(OwnedRoomId, AnyMessageLikeEventContent)> for OutgoingContent { } } -use crate::{OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest}; +use crate::{ + types::events::ToDeviceEvents, OutgoingRequest, OutgoingVerificationRequest, + RoomMessageRequest, ToDeviceRequest, +}; impl TryFrom for OutgoingContent { type Error = String; diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index c3ea8c66c..6a296a3ec 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -20,7 +20,7 @@ mod qrcode; mod requests; mod sas; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use event_enums::OutgoingContent; pub use machine::VerificationMachine; @@ -148,7 +148,7 @@ impl VerificationStore { } pub fn inner(&self) -> &dyn CryptoStore { - &*self.inner + self.inner.deref() } } @@ -710,20 +710,21 @@ pub(crate) mod tests { use std::convert::TryInto; use ruma::{ - events::{AnyToDeviceEvent, AnyToDeviceEventContent, ToDeviceEvent}, + events::{AnyToDeviceEventContent, ToDeviceEvent}, UserId, }; use super::event_enums::OutgoingContent; use crate::{ requests::{OutgoingRequest, OutgoingRequests}, + types::events::ToDeviceEvents, OutgoingVerificationRequest, }; pub(crate) fn request_to_event( sender: &UserId, request: &OutgoingVerificationRequest, - ) -> AnyToDeviceEvent { + ) -> ToDeviceEvents { let content = request.to_owned().try_into().expect("Can't fetch content out of the request"); wrap_any_to_device_content(sender, content) @@ -732,7 +733,7 @@ pub(crate) mod tests { pub(crate) fn outgoing_request_to_event( sender: &UserId, request: &OutgoingRequest, - ) -> AnyToDeviceEvent { + ) -> ToDeviceEvents { match request.request() { OutgoingRequests::ToDeviceRequest(r) => request_to_event(sender, &r.clone().into()), _ => panic!("Unsupported outgoing request"), @@ -742,31 +743,31 @@ pub(crate) mod tests { pub(crate) fn wrap_any_to_device_content( sender: &UserId, content: OutgoingContent, - ) -> AnyToDeviceEvent { + ) -> ToDeviceEvents { let content = if let OutgoingContent::ToDevice(c) = content { c } else { unreachable!() }; let sender = sender.to_owned(); match content { AnyToDeviceEventContent::KeyVerificationRequest(c) => { - AnyToDeviceEvent::KeyVerificationRequest(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationRequest(ToDeviceEvent { sender, content: c }) } AnyToDeviceEventContent::KeyVerificationReady(c) => { - AnyToDeviceEvent::KeyVerificationReady(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationReady(ToDeviceEvent { sender, content: c }) } AnyToDeviceEventContent::KeyVerificationKey(c) => { - AnyToDeviceEvent::KeyVerificationKey(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationKey(ToDeviceEvent { sender, content: c }) } AnyToDeviceEventContent::KeyVerificationStart(c) => { - AnyToDeviceEvent::KeyVerificationStart(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationStart(ToDeviceEvent { sender, content: c }) } AnyToDeviceEventContent::KeyVerificationAccept(c) => { - AnyToDeviceEvent::KeyVerificationAccept(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationAccept(ToDeviceEvent { sender, content: c }) } AnyToDeviceEventContent::KeyVerificationMac(c) => { - AnyToDeviceEvent::KeyVerificationMac(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationMac(ToDeviceEvent { sender, content: c }) } AnyToDeviceEventContent::KeyVerificationDone(c) => { - AnyToDeviceEvent::KeyVerificationDone(ToDeviceEvent { sender, content: c }) + ToDeviceEvents::KeyVerificationDone(ToDeviceEvent { sender, content: c }) } _ => unreachable!(), diff --git a/crates/matrix-sdk-crypto/src/verification/qrcode.rs b/crates/matrix-sdk-crypto/src/verification/qrcode.rs index 4969a81cf..8969df1fa 100644 --- a/crates/matrix-sdk-crypto/src/verification/qrcode.rs +++ b/crates/matrix-sdk-crypto/src/verification/qrcode.rs @@ -134,7 +134,7 @@ impl QrVerification { self.identities.other_user_id() } - /// Get the device id of the other side. + /// Get the device ID of the other side. pub fn other_device_id(&self) -> &DeviceId { self.identities.other_device_id() } diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index d1c86bac3..fd1450e16 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -823,7 +823,7 @@ struct Requested { /// The verification methods supported by the sender. pub their_methods: Vec, - /// The device id of the device that responded to the verification request. + /// The device ID of the device that responded to the verification request. pub other_device_id: OwnedDeviceId, } @@ -905,7 +905,7 @@ struct Ready { /// The verification methods supported by the us. pub our_methods: Vec, - /// The device id of the device that responded to the verification request. + /// The device ID of the device that responded to the verification request. pub other_device_id: OwnedDeviceId, } @@ -1200,7 +1200,7 @@ impl RequestState { #[derive(Clone, Debug)] struct Passive { - /// The device id of the device that responded to the verification request. + /// The device ID of the device that responded to the verification request. #[allow(dead_code)] pub other_device_id: OwnedDeviceId, } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index 2b9b9f155..12aacdb2e 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -60,7 +60,7 @@ impl Sas { self.account.user_id() } - /// Get our own device id. + /// Get our own device ID. pub fn device_id(&self) -> &DeviceId { self.account.device_id() } @@ -70,7 +70,7 @@ impl Sas { self.identities_being_verified.other_user_id() } - /// Get the device id of the other side. + /// Get the device ID of the other side. pub fn other_device_id(&self) -> &DeviceId { self.identities_being_verified.other_device_id() } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs index f71b2bfac..e9133eb50 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs @@ -329,7 +329,7 @@ impl SasState { self.ids.account.user_id() } - /// Get our own device id. + /// Get our own device ID. pub fn device_id(&self) -> &DeviceId { self.ids.account.device_id() } diff --git a/crates/matrix-sdk-ffi/src/lib.rs b/crates/matrix-sdk-ffi/src/lib.rs deleted file mode 100644 index 947762007..000000000 --- a/crates/matrix-sdk-ffi/src/lib.rs +++ /dev/null @@ -1,110 +0,0 @@ -// TODO: target-os conditional would be good. - -#![allow(unused_qualifications)] - -pub mod backward_stream; -pub mod client; -pub mod messages; -pub mod room; -mod uniffi_api; - -use std::{fs, path, sync::Arc}; - -use client::Client; -use matrix_sdk::{store::make_store_config, Client as MatrixClient, ClientBuilder, Session}; -use once_cell::sync::Lazy; -use sanitize_filename_reader_friendly::sanitize; -use serde::{Deserialize, Serialize}; -use tokio::runtime::Runtime; -pub use uniffi_api::*; - -pub static RUNTIME: Lazy = - Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime")); - -pub use matrix_sdk::ruma::{api::client::account::register, UserId}; - -pub use self::{backward_stream::*, client::*, messages::*, room::*}; - -pub fn guest_client(base_path: String, homeurl: String) -> anyhow::Result> { - let builder = new_client_builder(base_path, homeurl.clone())?.homeserver_url(&homeurl); - let mut guest_registration = register::v3::Request::new(); - guest_registration.kind = register::RegistrationKind::Guest; - RUNTIME.block_on(async move { - let client = builder.build().await?; - let register = client.register(guest_registration).await?; - let session = Session { - access_token: register.access_token.expect("no access token given"), - user_id: register.user_id, - device_id: register.device_id.clone().expect("device id is given by server"), - }; - client.restore_login(session).await?; - let c = Client::new(client, ClientState { is_guest: true, ..ClientState::default() }); - Ok(Arc::new(c)) - }) -} - -pub fn login_with_token(base_path: String, restore_token: String) -> anyhow::Result> { - let RestoreToken { session, homeurl, is_guest } = serde_json::from_str(&restore_token)?; - let builder = new_client_builder(base_path, session.user_id.to_string())? - .homeserver_url(&homeurl) - .user_id(&session.user_id); - // First we need to log in. - RUNTIME.block_on(async move { - let client = builder.build().await?; - client.restore_login(session).await?; - let c = Client::new(client, ClientState { is_guest, ..ClientState::default() }); - Ok(Arc::new(c)) - }) -} - -pub fn login_new_client( - base_path: String, - username: String, - password: String, -) -> anyhow::Result> { - let builder = new_client_builder(base_path, username.clone())?; - let user = UserId::parse(username)?; - // First we need to log in. - RUNTIME.block_on(async move { - let client = builder.user_id(&user).build().await?; - client.login(user, &password, None, None).await?; - let c = Client::new(client, ClientState { is_guest: false, ..ClientState::default() }); - Ok(Arc::new(c)) - }) -} - -fn new_client_builder(base_path: String, home: String) -> anyhow::Result { - let data_path = path::PathBuf::from(base_path).join(sanitize(&home)); - - fs::create_dir_all(&data_path)?; - let store_config = make_store_config(&data_path, None)?; - - Ok(MatrixClient::builder().user_agent("rust-sdk-ios").store_config(store_config)) -} - -#[derive(Default, Debug)] -pub struct ClientState { - is_guest: bool, - has_first_synced: bool, - is_syncing: bool, - should_stop_syncing: bool, -} - -#[derive(Serialize, Deserialize)] -struct RestoreToken { - is_guest: bool, - homeurl: String, - session: Session, -} - -#[derive(thiserror::Error, Debug)] -pub enum ClientError { - #[error("client error: {msg}")] - Generic { msg: String }, -} - -impl From for ClientError { - fn from(e: anyhow::Error) -> ClientError { - ClientError::Generic { msg: e.to_string() } - } -} diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index f92221160..8552a1474 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -30,7 +30,7 @@ indexed_db_futures = "0.2.3" matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base" } matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true } matrix-sdk-store-encryption = { version = "0.1.0", path = "../matrix-sdk-store-encryption" } -ruma = "0.6.1" +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" diff --git a/crates/matrix-sdk-indexeddb/src/lib.rs b/crates/matrix-sdk-indexeddb/src/lib.rs index 31d9b597a..511a8e9f8 100644 --- a/crates/matrix-sdk-indexeddb/src/lib.rs +++ b/crates/matrix-sdk-indexeddb/src/lib.rs @@ -28,7 +28,7 @@ pub use state_store::{IndexeddbStore as StateStore, IndexeddbStoreBuilder as Sta async fn open_stores_with_name( name: impl Into, passphrase: Option<&str>, -) -> Result<(Box, Box), OpenStoreError> { +) -> Result<(StateStore, CryptoStore), OpenStoreError> { let name = name.into(); let mut builder = StateStoreBuilder::default(); builder.name(name.clone()); @@ -41,7 +41,7 @@ async fn open_stores_with_name( let crypto_store = CryptoStore::open_with_store_cipher(name, state_store.store_cipher.clone()).await?; - Ok((Box::new(state_store), Box::new(crypto_store))) + Ok((state_store, crypto_store)) } #[cfg(target_arch = "wasm32")] @@ -71,7 +71,7 @@ pub async fn make_store_config( let state_store = builder.build().await.map_err(StoreError::from)?; - Ok(StoreConfig::new().state_store(Box::new(state_store))) + Ok(StoreConfig::new().state_store(state_store)) } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index e765d5b27..5f215d766 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -1220,10 +1220,7 @@ impl IndexeddbStore { .object_store(KEYS::DISPLAY_NAMES)? .get(&self.encode_key(KEYS::DISPLAY_NAMES, (room_id, display_name)))? .await? - .map(|f| { - self.deserialize_event::>(f) - .map_err::(|e| e) - }) + .map(|f| self.deserialize_event::>(f)) .unwrap_or_else(|| Ok(Default::default())) } @@ -1236,7 +1233,7 @@ impl IndexeddbStore { .object_store(KEYS::ACCOUNT_DATA)? .get(&self.encode_key(KEYS::ACCOUNT_DATA, event_type))? .await? - .map(|f| self.deserialize_event(f).map_err::(|e| e)) + .map(|f| self.deserialize_event(f)) .transpose() } @@ -1250,7 +1247,7 @@ impl IndexeddbStore { .object_store(KEYS::ROOM_ACCOUNT_DATA)? .get(&self.encode_key(KEYS::ROOM_ACCOUNT_DATA, (room_id, event_type)))? .await? - .map(|f| self.deserialize_event(f).map_err::(|e| e)) + .map(|f| self.deserialize_event(f)) .transpose() } diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index 5cb5f1de7..8823bff77 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -25,9 +25,9 @@ byteorder = "1.4.3" image = { version = "0.23.0", optional = true } qrcode = { version = "0.12.0", default-features = false } rqrr = { version = "0.4.0", optional = true } -ruma-common = "0.9.0" +ruma-common = { git = "https://github.com/ruma/ruma", rev = "96155915f" } thiserror = "1.0.30" [dependencies.vodozemac] git = "https://github.com/matrix-org/vodozemac/" -rev = "d0e744287a14319c2a9148fef3747548c740fc36" +rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml index 8edc42c78..a85b79890 100644 --- a/crates/matrix-sdk-sled/Cargo.toml +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -34,7 +34,7 @@ matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base", optional = t matrix-sdk-common = { version = "0.5.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true } matrix-sdk-store-encryption = { version = "0.1.0", path = "../matrix-sdk-store-encryption" } -ruma = "0.6.1" +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } serde = "1.0.136" serde_json = "1.0.79" sled = "0.34.7" diff --git a/crates/matrix-sdk-sled/src/cryptostore.rs b/crates/matrix-sdk-sled/src/cryptostore.rs index b9119240c..871c2d2e0 100644 --- a/crates/matrix-sdk-sled/src/cryptostore.rs +++ b/crates/matrix-sdk-sled/src/cryptostore.rs @@ -118,10 +118,12 @@ impl EncodeKey for SecretInfo { impl EncodeKey for RequestedKeyInfo { fn encode(&self) -> Vec { + #[allow(deprecated)] (&self.room_id, &self.sender_key, &self.algorithm, &self.session_id).encode() } fn encode_secure(&self, table_name: &str, store_cipher: &StoreCipher) -> Vec { let room_id = store_cipher.hash_key(table_name, self.room_id.as_bytes()); + #[allow(deprecated)] let sender_key = store_cipher.hash_key(table_name, self.sender_key.as_bytes()); let algorithm = store_cipher.hash_key(table_name, self.algorithm.as_ref().as_bytes()); let session_id = store_cipher.hash_key(table_name, self.session_id.as_bytes()); diff --git a/crates/matrix-sdk-sled/src/lib.rs b/crates/matrix-sdk-sled/src/lib.rs index 3fbbfb69c..856818cdc 100644 --- a/crates/matrix-sdk-sled/src/lib.rs +++ b/crates/matrix-sdk-sled/src/lib.rs @@ -56,7 +56,7 @@ pub fn make_store_config( #[cfg(all(feature = "crypto-store", not(feature = "state-store")))] { let crypto_store = CryptoStore::open_with_passphrase(path, passphrase)?; - Ok(StoreConfig::new().crypto_store(Box::new(crypto_store))) + Ok(StoreConfig::new().crypto_store(crypto_store)) } #[cfg(not(feature = "crypto-store"))] @@ -69,7 +69,7 @@ pub fn make_store_config( }; let state_store = store_builder.build().map_err(StoreError::backend)?; - Ok(StoreConfig::new().state_store(Box::new(state_store))) + Ok(StoreConfig::new().state_store(state_store)) } } @@ -79,7 +79,7 @@ pub fn make_store_config( fn open_stores_with_path( path: impl AsRef, passphrase: Option<&str>, -) -> Result<(Box, Box), OpenStoreError> { +) -> Result<(StateStore, CryptoStore), OpenStoreError> { let mut store_builder = SledStateStoreBuilder::default(); store_builder.path(path.as_ref().to_path_buf()); @@ -89,5 +89,5 @@ fn open_stores_with_path( let state_store = store_builder.build().map_err(StoreError::backend)?; let crypto_store = state_store.open_crypto_store()?; - Ok((Box::new(state_store), Box::new(crypto_store))) + Ok((state_store, crypto_store)) } diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index b640d4d76..f178c8d9c 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -34,6 +34,12 @@ use matrix_sdk_base::{ #[cfg(feature = "experimental-timeline")] use matrix_sdk_base::{deserialized_responses::SyncRoomEvent, store::BoxStream}; use matrix_sdk_store_encryption::{Error as KeyEncryptionError, StoreCipher}; +#[cfg(feature = "experimental-timeline")] +use ruma::{ + canonical_json::redact_in_place, + events::{room::redaction::SyncRoomRedactionEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent}, + CanonicalJsonObject, RoomVersionId, +}; use ruma::{ events::{ presence::PresenceEvent, @@ -47,12 +53,6 @@ use ruma::{ EventId, IdParseError, MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId, }; #[cfg(feature = "experimental-timeline")] -use ruma::{ - events::{room::redaction::SyncRoomRedactionEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent}, - signatures::{redact_in_place, CanonicalJsonObject}, - RoomVersionId, -}; -#[cfg(feature = "experimental-timeline")] use serde::Deserialize; use serde::{de::DeserializeOwned, Serialize}; use sled::{ diff --git a/crates/matrix-sdk-store-encryption/src/lib.rs b/crates/matrix-sdk-store-encryption/src/lib.rs index f582a9470..5533b4944 100644 --- a/crates/matrix-sdk-store-encryption/src/lib.rs +++ b/crates/matrix-sdk-store-encryption/src/lib.rs @@ -16,6 +16,8 @@ #![doc = include_str!("../README.md")] #![warn(missing_debug_implementations, missing_docs)] +use std::ops::DerefMut; + use blake3::{derive_key, Hash}; use chacha20poly1305::{ aead::{Aead, Error as EncryptionError, NewAead}, @@ -482,7 +484,7 @@ impl StoreCipher { /// Expand the given passphrase into a KEY_SIZE long key. fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Box<[u8; 32]> { let mut key = Box::new([0u8; 32]); - pbkdf2::>(passphrase.as_bytes(), salt, rounds, &mut *key); + pbkdf2::>(passphrase.as_bytes(), salt, rounds, key.deref_mut()); key } diff --git a/crates/matrix-sdk-test/Cargo.toml b/crates/matrix-sdk-test/Cargo.toml index 57df8c0f0..323888e79 100644 --- a/crates/matrix-sdk-test/Cargo.toml +++ b/crates/matrix-sdk-test/Cargo.toml @@ -18,6 +18,6 @@ appservice = [] http = "0.2.6" matrix-sdk-test-macros = { version = "0.2.0", path = "../matrix-sdk-test-macros" } once_cell = "1.10.0" -ruma = { version = "0.6.1", features = ["client-api-c"] } +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c"] } serde = "1.0.136" serde_json = "1.0.79" diff --git a/crates/matrix-sdk-test/src/test_json/events.rs b/crates/matrix-sdk-test/src/test_json/events.rs index ae5e36d68..5bb5a4d99 100644 --- a/crates/matrix-sdk-test/src/test_json/events.rs +++ b/crates/matrix-sdk-test/src/test_json/events.rs @@ -327,7 +327,7 @@ pub static LOGIN_TYPES: Lazy = Lazy::new(|| { }) }); -pub static LOGOUT: Lazy = Lazy::new(|| json!({})); +pub static EMPTY: Lazy = Lazy::new(|| json!({})); pub static EVENT_ID: Lazy = Lazy::new(|| { json!({ diff --git a/crates/matrix-sdk-test/src/test_json/mod.rs b/crates/matrix-sdk-test/src/test_json/mod.rs index 2f6c9cc5c..c05d7f3b7 100644 --- a/crates/matrix-sdk-test/src/test_json/mod.rs +++ b/crates/matrix-sdk-test/src/test_json/mod.rs @@ -12,12 +12,12 @@ pub mod members; pub mod sync; pub use events::{ - ALIAS, ALIASES, ENCRYPTION, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, - LOGIN_TYPES, LOGIN_WITH_DISCOVERY, LOGOUT, MEMBER, MEMBER_INVITE, MEMBER_NAME_CHANGE, - MEMBER_STRIPPED, MESSAGE_EDIT, MESSAGE_TEXT, NAME, NAME_STRIPPED, POWER_LEVELS, PRESENCE, - PUBLIC_ROOMS, PUSH_RULES, REACTION, READ_RECEIPT, READ_RECEIPT_OTHER, REDACTED, - REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR, ROOM_ID, ROOM_MESSAGES, - SYNC_ROOM_MESSAGES_BATCH_1, SYNC_ROOM_MESSAGES_BATCH_2, TAG, TOPIC, TYPING, + ALIAS, ALIASES, EMPTY, ENCRYPTION, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, + LOGIN_RESPONSE_ERR, LOGIN_TYPES, LOGIN_WITH_DISCOVERY, MEMBER, MEMBER_INVITE, + MEMBER_NAME_CHANGE, MEMBER_STRIPPED, MESSAGE_EDIT, MESSAGE_TEXT, NAME, NAME_STRIPPED, + POWER_LEVELS, PRESENCE, PUBLIC_ROOMS, PUSH_RULES, REACTION, READ_RECEIPT, READ_RECEIPT_OTHER, + REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR, ROOM_ID, + ROOM_MESSAGES, SYNC_ROOM_MESSAGES_BATCH_1, SYNC_ROOM_MESSAGES_BATCH_2, TAG, TOPIC, TYPING, }; pub use members::MEMBERS; pub use sync::{ diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 56e55d69b..b8421c629 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -37,7 +37,7 @@ native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso-login = ["warp", "rand", "tokio-stream"] -appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper"] +appservice = ["ruma/appservice-api-s"] image-proc = ["image"] image-rayon = ["image-proc", "image/jpeg_rayon"] @@ -109,9 +109,15 @@ version = "0.11.10" default_features = false [dependencies.ruma] -version = "0.6.1" +git = "https://github.com/ruma/ruma" +rev = "96155915f" features = ["client-api-c", "compat", "rand", "unstable-msc2448"] +[dependencies.ruma-client-api] +git = "https://github.com/ruma/ruma" +rev = "96155915f" +features = ["unstable-msc2965"] + [dependencies.tokio-stream] version = "0.1.8" features = ["net"] @@ -136,7 +142,6 @@ dirs = "4.0.0" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matches = "0.1.9" matrix-sdk-test = { version = "0.5.0", path = "../matrix-sdk-test" } -mockito = "0.31.0" once_cell = "1.10.0" serde_json = "1.0.79" tempfile = "3.3.0" @@ -148,6 +153,7 @@ wasm-bindgen-test = "0.3.30" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] } +wiremock = "0.5.13" [[example]] name = "cross_signing_bootstrap" diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index c0376c32c..f8e99151c 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -37,7 +37,7 @@ async fn main() -> anyhow::Result<()> { let client = Client::builder().user_id(alice).build().await?; // First we need to log in. - client.login(alice, "password", None, None).await?; + client.login_username(alice, "password").send().await?; client .register_event_handler(|ev: SyncRoomMessageEvent| async move { diff --git a/crates/matrix-sdk/examples/autojoin.rs b/crates/matrix-sdk/examples/autojoin.rs index e641f80b3..4968f638e 100644 --- a/crates/matrix-sdk/examples/autojoin.rs +++ b/crates/matrix-sdk/examples/autojoin.rs @@ -50,18 +50,22 @@ async fn login_and_sync( let mut home = dirs::home_dir().expect("no home directory found"); home.push("autojoin_bot"); let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?; - client_builder = client_builder.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(state_store); } #[cfg(feature = "indexeddb")] { let state_store = matrix_sdk_indexeddb::StateStore::open(); - client_builder = client_builder.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(state_store); } let client = client_builder.build().await?; - client.login(username, password, None, Some("autojoin bot")).await?; + client + .login_username(username, password) + .initial_device_display_name("autojoin bot") + .send() + .await?; println!("logged in as {}", username); diff --git a/crates/matrix-sdk/examples/command_bot.rs b/crates/matrix-sdk/examples/command_bot.rs index 4620a1cab..7308f02dc 100644 --- a/crates/matrix-sdk/examples/command_bot.rs +++ b/crates/matrix-sdk/examples/command_bot.rs @@ -45,17 +45,21 @@ async fn login_and_sync( let mut home = dirs::home_dir().expect("no home directory found"); home.push("party_bot"); let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?; - client_builder = client_builder.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(state_store); } #[cfg(feature = "indexeddb")] { let state_store = matrix_sdk_indexeddb::StateStore::open(); - client_builder = client_builder.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(state_store); } let client = client_builder.build().await.unwrap(); - client.login(&username, &password, None, Some("command bot")).await?; + client + .login_username(&username, &password) + .initial_device_display_name("command bot") + .send() + .await?; println!("logged in as {}", username); diff --git a/crates/matrix-sdk/examples/cross_signing_bootstrap.rs b/crates/matrix-sdk/examples/cross_signing_bootstrap.rs index 3247297ee..8b2532af9 100644 --- a/crates/matrix-sdk/examples/cross_signing_bootstrap.rs +++ b/crates/matrix-sdk/examples/cross_signing_bootstrap.rs @@ -39,7 +39,11 @@ async fn login(homeserver_url: String, username: &str, password: &str) -> matrix let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new(homeserver_url).await.unwrap(); - let response = client.login(username, password, None, Some("rust-sdk")).await?; + let response = client + .login_username(username, password) + .initial_device_display_name("rust-sdk") + .send() + .await?; let user_id = &response.user_id; let client_ref = &client; diff --git a/crates/matrix-sdk/examples/emoji_verification.rs b/crates/matrix-sdk/examples/emoji_verification.rs index 5bd5b9b06..0004a2c6f 100644 --- a/crates/matrix-sdk/examples/emoji_verification.rs +++ b/crates/matrix-sdk/examples/emoji_verification.rs @@ -13,13 +13,13 @@ use matrix_sdk::{ encryption::verification::{SasVerification, Verification}, ruma::{ events::{ - room::message::MessageType, AnySyncMessageLikeEvent, AnySyncRoomEvent, AnyToDeviceEvent, + room::message::MessageType, AnySyncMessageLikeEvent, AnySyncRoomEvent, + AnyToDeviceEvent, SyncMessageLikeEvent, }, UserId, }, Client, LoopCtrl, }; -use ruma::events::SyncMessageLikeEvent; use url::Url; async fn wait_for_confirmation(client: Client, sas: SasVerification) { @@ -69,7 +69,11 @@ async fn login(homeserver_url: String, username: &str, password: &str) -> matrix let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new(homeserver_url).await.unwrap(); - client.login(username, password, None, Some("rust-sdk")).await?; + client + .login_username(username, password) + .initial_device_display_name("rust-sdk") + .send() + .await?; let client_ref = &client; let initial_sync = Arc::new(AtomicBool::from(true)); diff --git a/crates/matrix-sdk/examples/get_profiles.rs b/crates/matrix-sdk/examples/get_profiles.rs index 62314b24a..5db5d032a 100644 --- a/crates/matrix-sdk/examples/get_profiles.rs +++ b/crates/matrix-sdk/examples/get_profiles.rs @@ -39,7 +39,11 @@ async fn login( let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new(homeserver_url).await.unwrap(); - client.login(username, password, None, Some("rust-sdk")).await?; + client + .login_username(username, password) + .initial_device_display_name("rust-sdk") + .send() + .await?; Ok(client) } diff --git a/crates/matrix-sdk/examples/image_bot.rs b/crates/matrix-sdk/examples/image_bot.rs index 4dda9554c..b6aab0931 100644 --- a/crates/matrix-sdk/examples/image_bot.rs +++ b/crates/matrix-sdk/examples/image_bot.rs @@ -60,7 +60,11 @@ async fn login_and_sync( let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new(homeserver_url).await.unwrap(); - client.login(&username, &password, None, Some("command bot")).await?; + client + .login_username(&username, &password) + .initial_device_display_name("command bot") + .send() + .await?; client.sync_once(SyncSettings::default()).await.unwrap(); diff --git a/crates/matrix-sdk/examples/login.rs b/crates/matrix-sdk/examples/login.rs index d9967530f..c935dc206 100644 --- a/crates/matrix-sdk/examples/login.rs +++ b/crates/matrix-sdk/examples/login.rs @@ -36,7 +36,11 @@ async fn login(homeserver_url: String, username: &str, password: &str) -> matrix client.register_event_handler(on_room_message).await; - client.login(username, password, None, Some("rust-sdk")).await?; + client + .login_username(username, password) + .initial_device_display_name("rust-sdk") + .send() + .await?; client.sync(SyncSettings::new()).await; Ok(()) diff --git a/crates/matrix-sdk/examples/timeline.rs b/crates/matrix-sdk/examples/timeline.rs index 7cdd77241..eb1c7b39d 100644 --- a/crates/matrix-sdk/examples/timeline.rs +++ b/crates/matrix-sdk/examples/timeline.rs @@ -26,7 +26,12 @@ async fn login(homeserver_url: String, username: &str, password: &str) -> Client .await .unwrap(); - client.login(username, password, None, Some("rust-sdk")).await.unwrap(); + client + .login_username(username, password) + .initial_device_display_name("rust-sdk") + .send() + .await + .unwrap(); client } diff --git a/crates/matrix-sdk/examples/wasm_command_bot/src/lib.rs b/crates/matrix-sdk/examples/wasm_command_bot/src/lib.rs index ed291ad35..b8a8baa60 100644 --- a/crates/matrix-sdk/examples/wasm_command_bot/src/lib.rs +++ b/crates/matrix-sdk/examples/wasm_command_bot/src/lib.rs @@ -85,7 +85,12 @@ pub async fn run() -> Result { let homeserver_url = Url::parse(homeserver_url).unwrap(); let client = Client::new(homeserver_url).await.unwrap(); - client.login(username, password, None, Some("rust-sdk-wasm")).await.unwrap(); + client + .login_username(username, password) + .initial_device_display_name("rust-sdk-wasm") + .send() + .await + .unwrap(); let bot = WasmBot(client.clone()); diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index 0a22267df..77e5c639f 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -58,7 +58,7 @@ use crate::{ /// # anyhow::Ok(()) /// ``` #[must_use] -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ClientBuilder { homeserver_cfg: Option, http_cfg: Option, @@ -127,7 +127,7 @@ impl ClientBuilder { /// /// ``` /// # use matrix_sdk_base::store::MemoryStore; - /// # let custom_state_store = Box::new(MemoryStore::new()); + /// # let custom_state_store = MemoryStore::new(); /// use matrix_sdk::{Client, config::StoreConfig}; /// /// let store_config = StoreConfig::new().state_store(custom_state_store); @@ -143,7 +143,7 @@ impl ClientBuilder { /// Set a custom implementation of a `StateStore`. /// /// The state store should be opened before being set. - pub fn state_store(mut self, store: Box) -> Self { + pub fn state_store(mut self, store: impl StateStore + 'static) -> Self { self.store_config = self.store_config.state_store(store); self } @@ -154,7 +154,7 @@ impl ClientBuilder { #[cfg(feature = "e2e-encryption")] pub fn crypto_store( mut self, - store: Box, + store: impl matrix_sdk_base::crypto::store::CryptoStore + 'static, ) -> Self { self.store_config = self.store_config.crypto_store(store); self @@ -295,6 +295,7 @@ impl ClientBuilder { let base_client = BaseClient::with_store_config(self.store_config); let http_client = HttpClient::new(inner_http_client.clone(), self.request_config); + let mut authentication_issuer: Option = None; let homeserver = match homeserver_cfg { HomeserverConfig::Url(url) => url, HomeserverConfig::ServerName(server_name) => { @@ -313,14 +314,20 @@ impl ClientBuilder { err => ClientBuildError::Http(err), })?; + if let Some(issuer) = well_known.authentication.map(|auth| auth.issuer) { + authentication_issuer = Url::parse(&issuer).ok(); + }; + well_known.homeserver.base_url } }; let homeserver = RwLock::new(Url::parse(&homeserver)?); + let authentication_issuer = authentication_issuer.map(RwLock::new); let inner = Arc::new(ClientInner { homeserver, + authentication_issuer, http_client, base_client, server_versions: OnceCell::new_with(self.server_versions), @@ -346,19 +353,19 @@ fn homeserver_from_name(server_name: &ServerName) -> String { #[cfg(not(test))] return format!("https://{}", server_name); - // Mockito only knows how to test http endpoints: - // https://github.com/lipanski/mockito/issues/127 + // Wiremock only knows how to test http endpoints: + // https://github.com/LukeMathWalker/wiremock-rs/issues/58 #[cfg(test)] return format!("http://{}", server_name); } -#[derive(Debug)] +#[derive(Clone, Debug)] enum HomeserverConfig { Url(String), ServerName(OwnedServerName), } -#[derive(Debug)] +#[derive(Clone, Debug)] enum HttpConfig { Settings(HttpSettings), Custom(Arc), diff --git a/crates/matrix-sdk/src/client/login_builder.rs b/crates/matrix-sdk/src/client/login_builder.rs new file mode 100644 index 000000000..e01b8d90d --- /dev/null +++ b/crates/matrix-sdk/src/client/login_builder.rs @@ -0,0 +1,309 @@ +#![cfg_attr(not(target_arch = "wasm32"), deny(clippy::future_not_send))] + +#[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] +use std::future::Future; + +use ruma::{ + api::client::{session::login, uiaa::UserIdentifier}, + assign, +}; +use tracing::{info, instrument}; + +use super::Client; +use crate::{config::RequestConfig, Result}; + +/// The login method. +/// +/// See also [`LoginInfo`][login::v3::LoginInfo] and [the spec]. +/// +/// [the spec]: https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3login +enum LoginMethod<'a> { + /// Login type `m.login.password` + UserPassword { id: UserIdentifier<'a>, password: &'a str }, + /// Login type `m.token` + Token(&'a str), +} + +impl<'a> LoginMethod<'a> { + fn id(&self) -> Option<&UserIdentifier<'a>> { + match self { + LoginMethod::UserPassword { id, .. } => Some(id), + LoginMethod::Token(_) => None, + } + } + + fn tracing_desc(&self) -> &'static str { + match self { + LoginMethod::UserPassword { .. } => "identifier and password", + LoginMethod::Token(_) => "token", + } + } + + fn to_login_info(&self) -> login::v3::LoginInfo<'a> { + match self { + LoginMethod::UserPassword { id, password } => { + login::v3::LoginInfo::Password(login::v3::Password::new(id.clone(), password)) + } + LoginMethod::Token(token) => login::v3::LoginInfo::Token(login::v3::Token::new(token)), + } + } +} + +/// Builder type used to configure optional settings for logging in with a +/// username or token. +/// +/// Created with [`Client::login_username`] or [`Client::login_token`]. +/// Finalized with [`.send()`](Self::send). +#[allow(missing_debug_implementations)] +pub struct LoginBuilder<'a> { + client: Client, + login_method: LoginMethod<'a>, + device_id: Option<&'a str>, + initial_device_display_name: Option<&'a str>, +} + +impl<'a> LoginBuilder<'a> { + fn new(client: Client, login_method: LoginMethod<'a>) -> Self { + Self { client, login_method, device_id: None, initial_device_display_name: None } + } + + pub(super) fn new_password(client: Client, id: UserIdentifier<'a>, password: &'a str) -> Self { + Self::new(client, LoginMethod::UserPassword { id, password }) + } + + pub(super) fn new_token(client: Client, token: &'a str) -> Self { + Self::new(client, LoginMethod::Token(token)) + } + + /// Set the device ID. + /// + /// The device ID is a unique ID that will be associated with this session. + /// If not set, the homeserver will create one. Can be an existing device ID + /// from a previous login call. Note that this should be done only if the + /// client also holds the corresponding encryption keys. + pub fn device_id(mut self, value: &'a str) -> Self { + self.device_id = Some(value); + self + } + + /// Set the initial device display name. + /// + /// The device display name is the public name that will be associated with + /// the device ID. Only necessary the first time you login with this device + /// ID. It can be changed later. + pub fn initial_device_display_name(mut self, value: &'a str) -> Self { + self.initial_device_display_name = Some(value); + self + } + + /// Send the login request. + #[instrument( + target = "matrix_sdk::client", + name = "login", + skip_all, + fields(method = self.login_method.tracing_desc()), + )] + pub async fn send(self) -> Result { + let homeserver = self.client.homeserver().await; + info!(homeserver = homeserver.as_str(), identifier = ?self.login_method.id(), "Logging in"); + + let request = assign!(login::v3::Request::new(self.login_method.to_login_info()), { + device_id: self.device_id.map(Into::into), + initial_device_display_name: self.initial_device_display_name, + }); + + let response = self.client.send(request, Some(RequestConfig::short_retry())).await?; + self.client.receive_login_response(&response).await?; + + Ok(response) + } +} + +/// Builder type used to configure optional settings for logging in via SSO. +/// +/// Created with [`Client::login_sso`]. +/// Finalized with [`.send()`](Self::send). +#[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] +#[allow(missing_debug_implementations)] +pub struct SsoLoginBuilder<'a, F> { + client: Client, + use_sso_login_url: F, + device_id: Option<&'a str>, + initial_device_display_name: Option<&'a str>, + server_url: Option<&'a str>, + server_response: Option<&'a str>, + identity_provider_id: Option<&'a str>, +} + +#[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] +impl<'a, F, Fut> SsoLoginBuilder<'a, F> +where + F: FnOnce(String) -> Fut + Send, + Fut: Future> + Send, +{ + pub(super) fn new(client: Client, use_sso_login_url: F) -> Self { + Self { + client, + use_sso_login_url, + device_id: None, + initial_device_display_name: None, + server_url: None, + server_response: None, + identity_provider_id: None, + } + } + + /// Set the device ID. + /// + /// The device ID is a unique ID that will be associated with this session. + /// If not set, the homeserver will create one. Can be an existing device ID + /// from a previous login call. Note that this should be done only if the + /// client also holds the corresponding encryption keys. + pub fn device_id(mut self, value: &'a str) -> Self { + self.device_id = Some(value); + self + } + + /// Set the initial device display name. + /// + /// The device display name is the public name that will be associated with + /// the device ID. Only necessary the first time you login with this device + /// ID. It can be changed later. + pub fn initial_device_display_name(mut self, value: &'a str) -> Self { + self.initial_device_display_name = Some(value); + self + } + + /// Set the local URL the server is going to try to bind to. + /// + /// Usually something like `http://localhost:3030`. If not set, the server + /// will try to open a random port on `127.0.0.1`. + pub fn server_url(mut self, value: &'a str) -> Self { + self.server_url = Some(value); + self + } + + /// Set the text to be shown at the end of the login process. + /// + /// This configures the text that will be shown on the webpage at the end of + /// the login process. This can be an HTML page. If not set, a default text + /// will be displayed. + pub fn server_response(mut self, value: &'a str) -> Self { + self.server_response = Some(value); + self + } + + /// Set the ID of the identity provider to log in with. + pub fn identity_provider_id(mut self, value: &'a str) -> Self { + self.identity_provider_id = Some(value); + self + } + + /// Send the login request. + #[instrument(target = "matrix_sdk::client", name = "login", skip_all, fields(method = "sso"))] + pub async fn send(self) -> Result { + use std::{ + collections::HashMap, + io::{Error as IoError, ErrorKind as IoErrorKind}, + ops::Range, + sync::{Arc, Mutex}, + }; + + use rand::{thread_rng, Rng}; + use tokio::{net::TcpListener, sync::oneshot}; + use tokio_stream::wrappers::TcpListenerStream; + use url::Url; + use warp::Filter; + + /// The range of ports the SSO server will try to bind to randomly. + /// + /// This is used to avoid binding to a port blocked by browsers. + /// See . + const SSO_SERVER_BIND_RANGE: Range = 20000..30000; + /// The number of times the SSO server will try to bind to a random port + const SSO_SERVER_BIND_TRIES: u8 = 10; + + let homeserver = self.client.homeserver().await; + info!("Logging in to {}", homeserver); + + let (signal_tx, signal_rx) = oneshot::channel(); + let (data_tx, data_rx) = oneshot::channel(); + let data_tx_mutex = Arc::new(Mutex::new(Some(data_tx))); + + let mut redirect_url = match self.server_url { + Some(s) => Url::parse(s)?, + None => { + Url::parse("http://127.0.0.1:0/").expect("Couldn't parse good known localhost URL") + } + }; + + let response = self + .server_response + .unwrap_or("The Single Sign-On login process is complete. You can close this page now.") + .to_owned(); + + let route = warp::get().and(warp::query::>()).map( + move |p: HashMap| { + if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { + data_tx.send(p.get("loginToken").cloned()).unwrap(); + } + http::Response::builder().body(response.clone()) + }, + ); + + let listener = { + if redirect_url.port().expect("The redirect URL doesn't include a port") == 0 { + let host = redirect_url.host_str().expect("The redirect URL doesn't have a host"); + let mut n = 0u8; + + loop { + let port = thread_rng().gen_range(SSO_SERVER_BIND_RANGE); + match TcpListener::bind((host, port)).await { + Ok(l) => { + redirect_url + .set_port(Some(port)) + .expect("Could not set new port on redirect URL"); + break l; + } + Err(_) if n < SSO_SERVER_BIND_TRIES => { + n += 1; + } + Err(e) => { + return Err(e.into()); + } + } + } + } else { + TcpListener::bind(redirect_url.as_str()).await? + } + }; + + let server = warp::serve(route).serve_incoming_with_graceful_shutdown( + TcpListenerStream::new(listener), + async { + signal_rx.await.ok(); + }, + ); + + tokio::spawn(server); + + let sso_url = + self.client.get_sso_login_url(redirect_url.as_str(), self.identity_provider_id).await?; + + (self.use_sso_login_url)(sso_url).await?; + + let token = data_rx + .await + .map_err(|e| IoError::new(IoErrorKind::Other, format!("{e}")))? + .ok_or_else(|| IoError::new(IoErrorKind::Other, "Could not get the loginToken"))?; + + let _ = signal_tx.send(()); + + let login_builder = LoginBuilder { + device_id: self.device_id, + initial_device_display_name: self.initial_device_display_name, + ..LoginBuilder::new_token(self.client, &token) + }; + login_builder.send().await + } +} diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 9b49bc7c2..57d312382 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -24,7 +24,7 @@ use std::{ use anymap2::any::CloneAnySendSync; #[cfg(target_arch = "wasm32")] -pub use async_once_cell::OnceCell; +use async_once_cell::OnceCell; use dashmap::DashMap; use futures_core::stream::Stream; use matrix_sdk_base::{ @@ -70,7 +70,7 @@ use ruma::{ }; use serde::de::DeserializeOwned; #[cfg(not(target_arch = "wasm32"))] -pub use tokio::sync::OnceCell; +use tokio::sync::OnceCell; use tracing::{debug, error, info, instrument, warn}; use url::Url; @@ -86,8 +86,14 @@ use crate::{ }; mod builder; +mod login_builder; -pub use self::builder::{ClientBuildError, ClientBuilder}; +#[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] +pub use self::login_builder::SsoLoginBuilder; +pub use self::{ + builder::{ClientBuildError, ClientBuilder}, + login_builder::LoginBuilder, +}; /// A conservative upload speed of 1Mbps const DEFAULT_UPLOAD_SPEED: u64 = 125_000; @@ -129,6 +135,8 @@ pub struct Client { pub(crate) struct ClientInner { /// The URL of the homeserver to connect to. homeserver: RwLock, + /// The OIDC Provider that is trusted by the homeserver. + authentication_issuer: Option>, /// The underlying HTTP client. http_client: HttpClient, /// User session data. @@ -247,10 +255,30 @@ impl Client { #[cfg(feature = "appservice")] pub async fn receive_transaction( &self, - _transaction_id: &TransactionId, + transaction_id: &TransactionId, sync_response: sync_events::v3::Response, ) -> Result<()> { - // TODO: transaction id checking, see PR #560 + const TXN_ID_KEY: &[u8] = b"appservice.txn_id"; + + let store = self.store(); + let store_tokens = store.get_custom_value(TXN_ID_KEY).await?; + let mut txn_id_bytes = transaction_id.as_bytes().to_vec(); + if let Some(mut store_tokens) = store_tokens { + // The data is separated by a NULL byte. + let mut store_tokens_split = store_tokens.split(|x| *x == b'\0'); + if store_tokens_split.any(|x| x == transaction_id.as_bytes()) { + // We already encountered this transaction id before, so we exit early instead + // of processing further. + // + // Spec: https://spec.matrix.org/v1.3/application-service-api/#pushing-events + return Ok(()); + } + store_tokens.push(b'\0'); + store_tokens.append(&mut txn_id_bytes); + self.store().set_custom_value(TXN_ID_KEY, store_tokens).await?; + } else { + self.store().set_custom_value(TXN_ID_KEY, txn_id_bytes).await?; + } self.process_sync(sync_response).await?; Ok(()) @@ -266,12 +294,21 @@ impl Client { self.inner.homeserver.read().await.clone() } + /// The OIDC Provider that is trusted by the homeserver. + pub async fn authentication_issuer(&self) -> Option { + if let Some(server) = &self.inner.authentication_issuer { + Some(server.read().await.clone()) + } else { + None + } + } + /// Get the user id of the current owner of the client. pub fn user_id(&self) -> Option<&UserId> { self.session().map(|s| s.user_id.as_ref()) } - /// Get the device id that identifies the current session. + /// Get the device ID that identifies the current session. pub fn device_id(&self) -> Option<&DeviceId> { self.session().map(|s| s.device_id.as_ref()) } @@ -281,7 +318,7 @@ impl Client { /// Will be `None` if the client has not been logged in. /// /// Can be used with [`Client::restore_login`] to restore a previously - /// logged in session. + /// logged-in session. pub fn session(&self) -> Option<&Session> { self.store().session() } @@ -665,29 +702,26 @@ impl Client { } } - /// Login to the server. + /// Login to the server with a username and password. /// /// This can be used for the first login as well as for subsequent logins, - /// note that if the device id isn't provided a new device will be created. + /// note that if the device ID isn't provided a new device will be created. /// - /// If this isn't the first login a device id should be provided to restore - /// the correct stores. + /// If this isn't the first login, a device ID should be provided through + /// [`LoginBuilder::device_id`] to restore the correct stores. /// /// Alternatively the [`restore_login`] method can be used to restore a - /// logged in client without the password. + /// logged-in client without the password. /// /// # Arguments /// - /// * `user` - The user that should be logged in to the homeserver. + /// * `user` - The user ID or user ID localpart of the user that should be + /// logged into the homeserver. /// /// * `password` - The password of the user. /// - /// * `device_id` - A unique id that will be associated with this session. - /// If not given the homeserver will create one. Can be an existing - /// device_id from a previous login call. Note that this should be done - /// only if the client also holds the encryption keys for this device. - /// /// # Example + /// /// ```no_run /// # use std::convert::TryFrom; /// # use futures::executor::block_on; @@ -700,246 +734,38 @@ impl Client { /// let user = "example"; /// /// let response = client - /// .login(user, "wordpass", None, Some("My bot")).await?; + /// .login_username(user, "wordpass") + /// .initial_device_display_name("My bot") + /// .send() + /// .await?; /// /// println!( /// "Logged in as {}, got device_id {} and access_token {}", - /// user, response.device_id, response.access_token + /// user, response.device_id, response.access_token, /// ); /// # anyhow::Ok(()) }); /// ``` /// /// [`restore_login`]: #method.restore_login - #[instrument(skip(self, user, password))] - pub async fn login( + pub fn login_username<'a>( &self, - user: impl AsRef, - password: &str, - device_id: Option<&str>, - initial_device_display_name: Option<&str>, - ) -> Result { - let homeserver = self.homeserver().await; - info!(homeserver = homeserver.as_str(), user = user.as_ref(), "Logging in"); - - let login_info = login::v3::LoginInfo::Password(login::v3::Password::new( - UserIdentifier::UserIdOrLocalpart(user.as_ref()), - password, - )); - - let request = assign!(login::v3::Request::new(login_info), { - device_id: device_id.map(|d| d.into()), - initial_device_display_name, - }); - - let response = self.send(request, Some(RequestConfig::short_retry())).await?; - self.receive_login_response(&response).await?; - - Ok(response) + id: &'a (impl AsRef + ?Sized), + password: &'a str, + ) -> LoginBuilder<'a> { + self.login_identifier(UserIdentifier::UserIdOrLocalpart(id.as_ref()), password) } - /// Login to the server via Single Sign-On. + /// Login to the server with a user identifier and password. /// - /// This takes care of the whole SSO flow: - /// * Spawn a local http server - /// * Provide a callback to open the SSO login URL in a web browser - /// * Wait for the local http server to get the loginToken - /// * Call [`login_with_token`] - /// - /// If cancellation is needed the method should be wrapped in a cancellable - /// task. **Note** that users with root access to the system have the - /// ability to snoop in on the data/token that is passed to the local - /// HTTP server that will be spawned. - /// - /// If you need more control over the SSO login process, you should use - /// [`get_sso_login_url`] and [`login_with_token`] directly. - /// - /// This should only be used for the first login. - /// - /// The [`restore_login`] method should be used to restore a - /// logged in client after the first login. - /// - /// A device id should be provided to restore the correct stores, if the - /// device id isn't provided a new device will be created. - /// - /// # Arguments - /// - /// * `use_sso_login_url` - A callback that will receive the SSO Login URL. - /// It should usually be used to open the SSO URL in a browser and must - /// return `Ok(())` if the URL was successfully opened. If it returns - /// `Err`, the error will be forwarded. - /// - /// * `server_url` - The local URL the server is going to try to bind to, e.g. `http://localhost:3030`. - /// If `None`, the server will try to open a random port on `127.0.0.1`. - /// - /// * `server_response` - The text that will be shown on the webpage at the - /// end of the login process. This can be an HTML page. If `None`, a - /// default text will be displayed. - /// - /// * `device_id` - A unique id that will be associated with this session. - /// If not given the homeserver will create one. Can be an existing - /// device_id from a previous login call. Note that this should be - /// provided only if the client also holds the encryption keys for this - /// device. - /// - /// * `initial_device_display_name` - A public display name that will be - /// associated with the device_id. Only necessary the first time you login - /// with this device_id. It can be changed later. - /// - /// * `idp_id` - The optional ID of the identity provider to login with. - /// - /// # Example - /// ```no_run - /// # use matrix_sdk::Client; - /// # use futures::executor::block_on; - /// # use url::Url; - /// # let homeserver = Url::parse("https://example.com").unwrap(); - /// # block_on(async { - /// let client = Client::new(homeserver).await.unwrap(); - /// - /// let response = client - /// .login_with_sso( - /// |sso_url| async move { - /// // Open sso_url - /// Ok(()) - /// }, - /// None, - /// None, - /// None, - /// Some("My app"), - /// None, - /// ) - /// .await - /// .unwrap(); - /// - /// println!("Logged in as {}, got device_id {} and access_token {}", - /// response.user_id, response.device_id, response.access_token); - /// # }) - /// ``` - /// - /// [`get_sso_login_url`]: #method.get_sso_login_url - /// [`login_with_token`]: #method.login_with_token - /// [`restore_login`]: #method.restore_login - #[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] - #[deny(clippy::future_not_send)] - pub async fn login_with_sso( + /// This is more general form of [`login_username`][Self::login_username] + /// that also accepts third-party identifiers instead of just the user ID or + /// its localpart. + pub fn login_identifier<'a>( &self, - use_sso_login_url: impl FnOnce(String) -> C + Send, - server_url: Option<&str>, - server_response: Option<&str>, - device_id: Option<&str>, - initial_device_display_name: Option<&str>, - idp_id: Option<&str>, - ) -> Result - where - C: Future> + Send, - { - use std::{ - collections::HashMap, - io::{Error as IoError, ErrorKind as IoErrorKind}, - ops::Range, - }; - - use rand::{thread_rng, Rng}; - use warp::Filter; - - /// The range of ports the SSO server will try to bind to randomly. - /// - /// This is used to avoid binding to a port blocked by browsers. - /// See . - const SSO_SERVER_BIND_RANGE: Range = 20000..30000; - /// The number of times the SSO server will try to bind to a random port - const SSO_SERVER_BIND_TRIES: u8 = 10; - - let homeserver = self.homeserver().await; - info!("Logging in to {}", homeserver); - - let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); - let (data_tx, data_rx) = tokio::sync::oneshot::channel(); - let data_tx_mutex = Arc::new(std::sync::Mutex::new(Some(data_tx))); - - let mut redirect_url = match server_url { - Some(s) => Url::parse(s)?, - None => { - Url::parse("http://127.0.0.1:0/").expect("Couldn't parse good known localhost URL") - } - }; - - let response = match server_response { - Some(s) => s.to_string(), - None => String::from( - "The Single Sign-On login process is complete. You can close this page now.", - ), - }; - - let route = warp::get().and(warp::query::>()).map( - move |p: HashMap| { - if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { - if let Some(token) = p.get("loginToken") { - data_tx.send(Some(token.to_owned())).unwrap(); - } else { - data_tx.send(None).unwrap(); - } - } - http::Response::builder().body(response.clone()) - }, - ); - - let listener = { - if redirect_url.port().expect("The redirect URL doesn't include a port") == 0 { - let host = redirect_url.host_str().expect("The redirect URL doesn't have a host"); - let mut n = 0u8; - let mut port = 0u16; - let mut res = Err(IoError::new(IoErrorKind::Other, "")); - - while res.is_err() && n < SSO_SERVER_BIND_TRIES { - port = thread_rng().gen_range(SSO_SERVER_BIND_RANGE); - res = tokio::net::TcpListener::bind((host, port)).await; - n += 1; - } - match res { - Ok(s) => { - redirect_url - .set_port(Some(port)) - .expect("Could not set new port on redirect URL"); - s - } - Err(err) => return Err(err.into()), - } - } else { - match tokio::net::TcpListener::bind(redirect_url.as_str()).await { - Ok(s) => s, - Err(err) => return Err(err.into()), - } - } - }; - - let server = warp::serve(route).serve_incoming_with_graceful_shutdown( - tokio_stream::wrappers::TcpListenerStream::new(listener), - async { - signal_rx.await.ok(); - }, - ); - - tokio::spawn(server); - - let sso_url = self.get_sso_login_url(redirect_url.as_str(), idp_id).await?; - - match use_sso_login_url(sso_url).await { - Ok(t) => t, - Err(err) => return Err(err), - }; - - let token = match data_rx.await { - Ok(Some(t)) => t, - Ok(None) => { - return Err(IoError::new(IoErrorKind::Other, "Could not get the loginToken").into()) - } - Err(err) => return Err(IoError::new(IoErrorKind::Other, format!("{}", err)).into()), - }; - - let _ = signal_tx.send(()); - - self.login_with_token(token.as_str(), device_id, initial_device_display_name).await + id: UserIdentifier<'a>, + password: &'a str, + ) -> LoginBuilder<'a> { + LoginBuilder::new_password(self.clone(), id, password) } /// Login to the server with a token. @@ -950,27 +776,19 @@ impl Client { /// /// This should only be used for the first login. /// - /// The [`restore_login`] method should be used to restore a - /// logged in client after the first login. + /// The [`restore_login`] method should be used to restore a logged-in + /// client after the first login. /// - /// A device id should be provided to restore the correct stores, if the - /// device id isn't provided a new device will be created. + /// A device ID should be provided through [`LoginBuilder::device_id`] to + /// restore the correct stores, if the device ID isn't provided a new + /// device will be created. /// /// # Arguments /// /// * `token` - A login token. /// - /// * `device_id` - A unique id that will be associated with this session. - /// If not given the homeserver will create one. Can be an existing - /// device_id from a previous login call. Note that this should be - /// provided only if the client also holds the encryption keys for this - /// device. - /// - /// * `initial_device_display_name` - A public display name that will be - /// associated with the device_id. Only necessary the first time you login - /// with this device_id. It can be changed later. - /// /// # Example + /// /// ```no_run /// # use std::convert::TryFrom; /// # use matrix_sdk::Client; @@ -988,7 +806,71 @@ impl Client { /// // Receive the loginToken param at redirect_url /// /// let response = client - /// .login_with_token(login_token, None, Some("My app")).await + /// .login_token(login_token) + /// .initial_device_display_name("My app") + /// .send() + /// .await + /// .unwrap(); + /// + /// println!( + /// "Logged in as {}, got device_id {} and access_token {}", + /// response.user_id, response.device_id, response.access_token, + /// ); + /// # }) + /// ``` + /// + /// [`get_sso_login_url`]: #method.get_sso_login_url + /// [`restore_login`]: #method.restore_login + pub fn login_token<'a>(&self, token: &'a str) -> LoginBuilder<'a> { + LoginBuilder::new_token(self.clone(), token) + } + + /// Login to the server via Single Sign-On. + /// + /// This takes care of the whole SSO flow: + /// * Spawn a local http server + /// * Provide a callback to open the SSO login URL in a web browser + /// * Wait for the local http server to get the loginToken + /// * Call [`login_token`] + /// + /// If cancellation is needed the method should be wrapped in a cancellable + /// task. **Note** that users with root access to the system have the + /// ability to snoop in on the data/token that is passed to the local + /// HTTP server that will be spawned. + /// + /// If you need more control over the SSO login process, you should use + /// [`get_sso_login_url`] and [`login_token`] directly. + /// + /// This should only be used for the first login. + /// + /// The [`restore_login`] method should be used to restore a logged-in + /// client after the first login. + /// + /// # Arguments + /// + /// * `use_sso_login_url` - A callback that will receive the SSO Login URL. + /// It should usually be used to open the SSO URL in a browser and must + /// return `Ok(())` if the URL was successfully opened. If it returns + /// `Err`, the error will be forwarded. + /// + /// # Example + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use futures::executor::block_on; + /// # use url::Url; + /// # let homeserver = Url::parse("https://example.com").unwrap(); + /// # block_on(async { + /// let client = Client::new(homeserver).await.unwrap(); + /// + /// let response = client + /// .login_sso(|sso_url| async move { + /// // Open sso_url + /// Ok(()) + /// }) + /// .initial_device_display_name("My app") + /// .send() + /// .await /// .unwrap(); /// /// println!("Logged in as {}, got device_id {} and access_token {}", @@ -997,7 +879,76 @@ impl Client { /// ``` /// /// [`get_sso_login_url`]: #method.get_sso_login_url + /// [`login_token`]: #method.login_token /// [`restore_login`]: #method.restore_login + #[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] + pub fn login_sso<'a, F, Fut>(&self, use_sso_login_url: F) -> SsoLoginBuilder<'a, F> + where + F: FnOnce(String) -> Fut + Send, + Fut: Future> + Send, + { + SsoLoginBuilder::new(self.clone(), use_sso_login_url) + } + + /// Login to the server with a username and password. + #[deprecated = "Replaced by [`Client::login_username`](#method.login_username)"] + #[instrument(skip(self, user, password))] + pub async fn login( + &self, + user: impl AsRef, + password: &str, + device_id: Option<&str>, + initial_device_display_name: Option<&str>, + ) -> Result { + let mut builder = self.login_username(&user, password); + if let Some(value) = device_id { + builder = builder.device_id(value); + } + if let Some(value) = initial_device_display_name { + builder = builder.initial_device_display_name(value); + } + + builder.send().await + } + + /// Login to the server via Single Sign-On. + #[deprecated = "Replaced by [`Client::login_sso`](#method.login_sso)"] + #[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] + #[deny(clippy::future_not_send)] + pub async fn login_with_sso( + &self, + use_sso_login_url: impl FnOnce(String) -> C + Send, + server_url: Option<&str>, + server_response: Option<&str>, + device_id: Option<&str>, + initial_device_display_name: Option<&str>, + idp_id: Option<&str>, + ) -> Result + where + C: Future> + Send, + { + let mut builder = self.login_sso(use_sso_login_url); + if let Some(value) = server_url { + builder = builder.server_url(value); + } + if let Some(value) = server_response { + builder = builder.server_response(value); + } + if let Some(value) = device_id { + builder = builder.device_id(value); + } + if let Some(value) = initial_device_display_name { + builder = builder.initial_device_display_name(value); + } + if let Some(value) = idp_id { + builder = builder.identity_provider_id(value); + } + + builder.send().await + } + + /// Login to the server with a token. + #[deprecated = "Replaced by [`Client::login_token`](#method.login_token)"] #[instrument(skip(self, token))] #[cfg_attr(not(target_arch = "wasm32"), deny(clippy::future_not_send))] pub async fn login_with_token( @@ -1006,22 +957,15 @@ impl Client { device_id: Option<&str>, initial_device_display_name: Option<&str>, ) -> Result { - let homeserver = self.homeserver().await; - info!("Logging in to {}", homeserver); + let mut builder = self.login_token(token); + if let Some(value) = device_id { + builder = builder.device_id(value); + } + if let Some(value) = initial_device_display_name { + builder = builder.initial_device_display_name(value); + } - let request = assign!( - login::v3::Request::new( - login::v3::LoginInfo::Token(login::v3::Token::new(token)), - ), { - device_id: device_id.map(|d| d.into()), - initial_device_display_name, - } - ); - - let response = self.send(request, Some(RequestConfig::short_retry())).await?; - self.receive_login_response(&response).await?; - - Ok(response) + builder.send().await } /// Receive a login response and update the homeserver and the base client @@ -1050,7 +994,7 @@ impl Client { /// the stored state and encryption keys. /// /// Alternatively, if the whole session isn't stored the [`login`] method - /// can be used with a device id. + /// can be used with a device ID. /// /// # Arguments /// @@ -2257,356 +2201,61 @@ impl Client { } } -// mockito (the http mocking library) is not supported for wasm32 +// The http mocking library is not supported for wasm32 #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod tests { - use matrix_sdk_test::async_test; + use std::time::Duration; + + use matrix_sdk_test::{async_test, test_json, EventBuilder, EventsJson}; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use std::{collections::BTreeMap, convert::TryInto, io::Cursor, str::FromStr, time::Duration}; - - use matrix_sdk_base::{ - media::{MediaFormat, MediaRequest, MediaThumbnailSize}, - DisplayName, - }; - #[cfg(feature = "experimental-timeline")] - use matrix_sdk_common::deserialized_responses::SyncRoomEvent; - use matrix_sdk_test::{test_json, EventBuilder, EventsJson}; - use mockito::{mock, Matcher}; - use ruma::{ - api::{ - client::{ - self as client_api, - account::register::{v3::Request as RegistrationRequest, RegistrationKind}, - directory::{ - get_public_rooms, - get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest}, - }, - media::get_content_thumbnail::v3::Method, - membership::Invite3pidInit, - session::get_login_types::v3::LoginType, - uiaa::{self, UiaaResponse}, - }, - error::{FromHttpResponseError, ServerError}, - MatrixVersion, - }, - assign, device_id, - directory::Filter, - event_id, - events::{ - room::{ - message::{ImageMessageEventContent, RoomMessageEventContent}, - ImageInfo, MediaSource, - }, - AnySyncStateEvent, StateEventType, - }, - mxc_uri, room_id, thirdparty, uint, user_id, TransactionId, UserId, - }; - use serde_json::json; + use ruma::{api::MatrixVersion, device_id, room_id, user_id, UserId}; use url::Url; + use wiremock::{ + matchers::{header, method, path}, + Mock, MockServer, ResponseTemplate, + }; use super::{Client, ClientBuilder, Session}; - use crate::{ - attachment::{ - AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, - Thumbnail, - }, - config::{RequestConfig, SyncSettings}, - error::RumaApiError, - HttpError, RoomMember, - }; + use crate::config::{RequestConfig, SyncSettings}; - fn test_client_builder() -> ClientBuilder { - let homeserver = Url::parse(&mockito::server_url()).unwrap(); + fn test_client_builder(homeserver_url: Option) -> ClientBuilder { + let homeserver = homeserver_url.as_deref().unwrap_or("http://localhost:1234"); Client::builder().homeserver_url(homeserver).server_versions([MatrixVersion::V1_0]) } - async fn no_retry_test_client() -> Client { - test_client_builder() + async fn no_retry_test_client(homeserver_url: Option) -> Client { + test_client_builder(homeserver_url) .request_config(RequestConfig::new().disable_retry()) .build() .await .unwrap() } - pub(crate) async fn logged_in_client() -> Client { + pub(crate) async fn logged_in_client(homeserver_url: Option) -> Client { let session = Session { access_token: "1234".to_owned(), user_id: user_id!("@example:localhost").to_owned(), device_id: device_id!("DEVICEID").to_owned(), }; - let client = no_retry_test_client().await; + let client = no_retry_test_client(homeserver_url).await; client.restore_login(session).await.unwrap(); client } - #[async_test] - async fn set_homeserver() { - let client = no_retry_test_client().await; - let homeserver = Url::from_str("http://example.com/").unwrap(); - client.set_homeserver(homeserver.clone()).await; - - assert_eq!(client.homeserver().await, homeserver); - } - - #[async_test] - async fn successful_discovery() { - let server_url = mockito::server_url(); - let domain = server_url.strip_prefix("http://").unwrap(); - let alice = UserId::parse("@alice:".to_owned() + domain).unwrap(); - - let _m_well_known = mock("GET", "/.well-known/matrix/client") - .with_status(200) - .with_body( - test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()), - ) - .create(); - - let _m_versions = mock("GET", "/_matrix/client/versions") - .with_status(200) - .with_body(test_json::VERSIONS.to_string()) - .create(); - let client = Client::builder().user_id(&alice).build().await.unwrap(); - - assert_eq!(client.homeserver().await, Url::parse(server_url.as_ref()).unwrap()); - } - - #[async_test] - async fn discovery_broken_server() { - let server_url = mockito::server_url(); - let domain = server_url.strip_prefix("http://").unwrap(); - let alice = UserId::parse("@alice:".to_owned() + domain).unwrap(); - - let _m = mock("GET", "/.well-known/matrix/client").with_status(404).create(); - - assert!( - Client::builder().user_id(&alice).build().await.is_err(), - "Creating a client from a user ID should fail when the .well-known request fails." - ); - } - - #[async_test] - async fn login() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = no_retry_test_client().await; - - let _m_types = mock("GET", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN_TYPES.to_string()) - .create(); - - let can_password = client - .get_login_types() - .await - .unwrap() - .flows - .iter() - .any(|flow| matches!(flow, LoginType::Password(_))); - assert!(can_password); - - let _m_login = mock("POST", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN.to_string()) - .create(); - - client.login("example", "wordpass", None, None).await.unwrap(); - - let logged_in = client.logged_in(); - assert!(logged_in, "Client should be logged in"); - - assert_eq!(client.homeserver().await, homeserver); - } - - #[async_test] - async fn login_with_discovery() { - let client = no_retry_test_client().await; - - let _m_login = mock("POST", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN_WITH_DISCOVERY.to_string()) - .create(); - - client.login("example", "wordpass", None, None).await.unwrap(); - - let logged_in = client.logged_in(); - assert!(logged_in, "Client should be logged in"); - - assert_eq!(client.homeserver().await.as_str(), "https://example.org/"); - } - - #[async_test] - async fn login_no_discovery() { - let client = no_retry_test_client().await; - - let _m_login = mock("POST", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN.to_string()) - .create(); - - client.login("example", "wordpass", None, None).await.unwrap(); - - let logged_in = client.logged_in(); - assert!(logged_in, "Client should be logged in"); - - assert_eq!(client.homeserver().await, Url::parse(&mockito::server_url()).unwrap()); - } - - #[async_test] - #[cfg(feature = "sso-login")] - async fn login_with_sso() { - let _m_login = mock("POST", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN.to_string()) - .create(); - - let _homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = no_retry_test_client().await; - let idp = crate::client::get_login_types::v3::IdentityProvider::new( - "some-id".to_owned(), - "idp-name".to_owned(), - ); - client - .login_with_sso( - |sso_url| async move { - let sso_url = Url::parse(sso_url.as_str()).unwrap(); - - let (_, redirect) = - sso_url.query_pairs().find(|(key, _)| key == "redirectUrl").unwrap(); - - let mut redirect_url = Url::parse(redirect.into_owned().as_str()).unwrap(); - redirect_url.set_query(Some("loginToken=tinytoken")); - - reqwest::get(redirect_url.to_string()).await.unwrap(); - - Ok(()) - }, - None, - None, - None, - None, - Some(&idp.id), - ) - .await - .unwrap(); - - let logged_in = client.logged_in(); - assert!(logged_in, "Client should be logged in"); - } - - #[async_test] - async fn login_with_sso_token() { - let client = no_retry_test_client().await; - - let _m = mock("GET", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN_TYPES.to_string()) - .create(); - - let can_sso = client - .get_login_types() - .await - .unwrap() - .flows - .iter() - .any(|flow| matches!(flow, LoginType::Sso(_))); - assert!(can_sso); - - let sso_url = client.get_sso_login_url("http://127.0.0.1:3030", None).await; - assert!(sso_url.is_ok()); - - let _m = mock("POST", "/_matrix/client/r0/login") - .with_status(200) - .with_body(test_json::LOGIN.to_string()) - .create(); - - client.login_with_token("averysmalltoken", None, None).await.unwrap(); - - let logged_in = client.logged_in(); - assert!(logged_in, "Client should be logged in"); - } - - #[async_test] - async fn devices() { - let client = logged_in_client().await; - - let _m = mock("GET", "/_matrix/client/r0/devices") - .with_status(200) - .with_body(test_json::DEVICES.to_string()) - .create(); - - assert!(client.devices().await.is_ok()); - } - - #[async_test] - async fn resolve_room_alias() { - let client = no_retry_test_client().await; - - let _m = mock("GET", "/_matrix/client/r0/directory/room/%23alias%3Aexample%2Eorg") - .with_status(200) - .with_body(test_json::GET_ALIAS.to_string()) - .create(); - - let alias = ruma::room_alias_id!("#alias:example.org"); - assert!(client.resolve_room_alias(alias).await.is_ok()); - } - - #[async_test] - async fn test_join_leave_room() { - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(test_json::SYNC.to_string()) - .create(); - - let client = logged_in_client().await; - let session = client.session().unwrap().clone(); - - let room = client.get_joined_room(room_id); - assert!(room.is_none()); - - client.sync_once(SyncSettings::default()).await.unwrap(); - - let room = client.get_left_room(room_id); - assert!(room.is_none()); - - let room = client.get_joined_room(room_id); - assert!(room.is_some()); - - // test store reloads with correct room state from the state store - let joined_client = no_retry_test_client().await; - joined_client.restore_login(session).await.unwrap(); - - // joined room reloaded from state store - joined_client.sync_once(SyncSettings::default()).await.unwrap(); - let room = joined_client.get_joined_room(room_id); - assert!(room.is_some()); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(test_json::LEAVE_SYNC_EVENT.to_string()) - .create(); - - joined_client.sync_once(SyncSettings::default()).await.unwrap(); - - let room = joined_client.get_joined_room(room_id); - assert!(room.is_none()); - - let room = joined_client.get_left_room(room_id); - assert!(room.is_some()); - } - #[async_test] async fn account_data() { - let client = logged_in_client().await; + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(test_json::SYNC.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/sync".to_owned())) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::SYNC)) + .mount(&server) + .await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let _response = client.sync_once(sync_settings).await.unwrap(); @@ -2616,9 +2265,55 @@ pub(crate) mod tests { // assert_eq!(1, ignored_users.len()) } + #[async_test] + async fn successful_discovery() { + let server = MockServer::start().await; + let server_url = server.uri(); + let domain = server_url.strip_prefix("http://").unwrap(); + let alice = UserId::parse("@alice:".to_owned() + domain).unwrap(); + + Mock::given(method("GET")) + .and(path("/.well-known/matrix/client")) + .respond_with(ResponseTemplate::new(200).set_body_raw( + test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()), + "application/json", + )) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/versions")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS)) + .mount(&server) + .await; + let client = Client::builder().user_id(&alice).build().await.unwrap(); + + assert_eq!(client.homeserver().await, Url::parse(server_url.as_ref()).unwrap()); + } + + #[async_test] + async fn discovery_broken_server() { + let server = MockServer::start().await; + let server_url = server.uri(); + let domain = server_url.strip_prefix("http://").unwrap(); + let alice = UserId::parse("@alice:".to_owned() + domain).unwrap(); + + Mock::given(method("GET")) + .and(path("/.well-known/matrix/client")) + .respond_with(ResponseTemplate::new(404)) + .mount(&server) + .await; + + assert!( + Client::builder().user_id(&alice).build().await.is_err(), + "Creating a client from a user ID should fail when the .well-known request fails." + ); + } + #[async_test] async fn room_creation() { - let client = logged_in_client().await; + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; let response = EventBuilder::default() .add_state_event(EventsJson::Member) @@ -2628,937 +2323,16 @@ pub(crate) mod tests { client.inner.base_client.receive_sync_response(response).await.unwrap(); let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - assert_eq!(client.homeserver().await, Url::parse(&mockito::server_url()).unwrap()); + assert_eq!(client.homeserver().await, Url::parse(&server.uri()).unwrap()); let room = client.get_joined_room(room_id); assert!(room.is_some()); } - #[async_test] - async fn login_error() { - let client = no_retry_test_client().await; - - let _m = mock("POST", "/_matrix/client/r0/login") - .with_status(403) - .with_body(test_json::LOGIN_RESPONSE_ERR.to_string()) - .create(); - - if let Err(err) = client.login("example", "wordpass", None, None).await { - if let crate::Error::Http(HttpError::Api(FromHttpResponseError::Server( - ServerError::Known(RumaApiError::ClientApi(client_api::Error { - kind, - message, - status_code, - })), - ))) = err - { - if let client_api::error::ErrorKind::Forbidden = kind { - } else { - panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); - } - assert_eq!(message, "Invalid password".to_owned()); - assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); - } else { - panic!("found the wrong `Error` type {:?}, expected `Error::RumaResponse", err); - } - } else { - panic!("this request should return an `Err` variant") - } - } - - #[async_test] - async fn register_error() { - let client = no_retry_test_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/register\?.*$".to_owned())) - .with_status(403) - .with_body(test_json::REGISTRATION_RESPONSE_ERR.to_string()) - .create(); - - let user = assign!(RegistrationRequest::new(), { - username: Some("user"), - password: Some("password"), - auth: Some(uiaa::AuthData::FallbackAcknowledgement( - uiaa::FallbackAcknowledgement::new("foobar"), - )), - kind: RegistrationKind::User, - }); - - if let Err(err) = client.register(user).await { - if let HttpError::UiaaError(FromHttpResponseError::Server(ServerError::Known( - UiaaResponse::MatrixError(client_api::Error { kind, message, status_code }), - ))) = err - { - if let client_api::error::ErrorKind::Forbidden = kind { - } else { - panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); - } - assert_eq!(message, "Invalid password".to_owned()); - assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); - } else { - panic!("found the wrong `Error` type {:#?}, expected `UiaaResponse`", err); - } - } else { - panic!("this request should return an `Err` variant") - } - } - - #[async_test] - async fn join_room_by_id() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/join".to_owned())) - .with_status(200) - .with_body(test_json::ROOM_ID.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let room_id = room_id!("!testroom:example.org"); - - assert_eq!( - // this is the `join_by_room_id::Response` but since no PartialEq we check the RoomId - // field - client.join_room_by_id(room_id).await.unwrap().room_id, - room_id - ); - } - - #[async_test] - async fn join_room_by_id_or_alias() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/join/".to_owned())) - .with_status(200) - .with_body(test_json::ROOM_ID.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let room_id = room_id!("!testroom:example.org").into(); - - assert_eq!( - // this is the `join_by_room_id::Response` but since no PartialEq we check the RoomId - // field - client - .join_room_by_id_or_alias(room_id, &["server.com".try_into().unwrap()]) - .await - .unwrap() - .room_id, - room_id!("!testroom:example.org") - ); - } - - #[async_test] - async fn invite_user_by_id() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/invite".to_owned())) - .with_status(200) - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let user = user_id!("@example:localhost"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.invite_user_by_id(user).await.unwrap(); - } - - #[async_test] - async fn invite_user_by_3pid() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/invite".to_owned())) - .with_status(200) - // empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.invite_user_by_3pid( - Invite3pidInit { - id_server: "example.org", - id_access_token: "IdToken", - medium: thirdparty::Medium::Email, - address: "address", - } - .into(), - ) - .await - .unwrap(); - } - - #[async_test] - async fn room_search_all() { - let client = no_retry_test_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/publicRooms".to_owned())) - .with_status(200) - .with_body(test_json::PUBLIC_ROOMS.to_string()) - .create(); - - let get_public_rooms::v3::Response { chunk, .. } = - client.public_rooms(Some(10), None, None).await.unwrap(); - assert_eq!(chunk.len(), 1); - } - - #[async_test] - async fn room_search_filtered() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/publicRooms".to_owned())) - .with_status(200) - .with_body(test_json::PUBLIC_ROOMS.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let generic_search_term = Some("cheese"); - let filter = assign!(Filter::new(), { generic_search_term }); - let request = assign!(PublicRoomsFilterRequest::new(), { filter }); - - let get_public_rooms_filtered::v3::Response { chunk, .. } = - client.public_rooms_filtered(request).await.unwrap(); - assert_eq!(chunk.len(), 1); - } - - #[async_test] - async fn leave_room() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/leave".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.leave().await.unwrap(); - } - - #[async_test] - async fn ban_user() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/ban".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let user = user_id!("@example:localhost"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.ban_user(user, None).await.unwrap(); - } - - #[async_test] - async fn kick_user() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/kick".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let user = user_id!("@example:localhost"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.kick_user(user, None).await.unwrap(); - } - - #[async_test] - async fn forget_room() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/forget".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::LEAVE_SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_left_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.forget().await.unwrap(); - } - - #[async_test] - async fn read_receipt() { - let client = logged_in_client().await; - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/receipt".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let event_id = event_id!("$xxxxxx:example.org"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.read_receipt(event_id).await.unwrap(); - } - - #[async_test] - async fn read_marker() { - let client = logged_in_client().await; - - let _m = - mock("POST", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/read_markers".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let event_id = event_id!("$xxxxxx:example.org"); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.read_marker(event_id, None).await.unwrap(); - } - - #[async_test] - async fn typing_notice() { - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/typing".to_owned())) - .with_status(200) - // this is an empty JSON object - .with_body(test_json::LOGOUT.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - room.typing_notice(true).await.unwrap(); - } - - #[async_test] - async fn room_state_event_send() { - use ruma::events::room::member::{MembershipState, RoomMemberEventContent}; - - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/state/.*".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - - let room = client.get_joined_room(room_id).unwrap(); - - let avatar_url = mxc_uri!("mxc://example.org/avA7ar"); - let member_event = assign!(RoomMemberEventContent::new(MembershipState::Join), { - avatar_url: Some(avatar_url.to_owned()) - }); - let response = room.send_state_event(member_event, "").await.unwrap(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); - } - - #[async_test] - async fn room_message_send() { - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - let content = RoomMessageEventContent::text_plain("Hello world"); - let txn_id = TransactionId::new(); - let response = room.send(content, Some(&txn_id)).await.unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) - } - - #[async_test] - async fn room_attachment_send() { - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .match_body(Matcher::PartialJson(json!({ - "info": { - "mimetype": "image/jpeg" - } - }))) - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_owned())) - .with_status(200) - .match_header("content-type", "image/jpeg") - .with_body( - json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }) - .to_string(), - ) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - let mut media = Cursor::new("Hello world"); - - let response = room - .send_attachment("image", &mime::IMAGE_JPEG, &mut media, AttachmentConfig::new()) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) - } - - #[async_test] - async fn room_attachment_send_info() { - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .match_body(Matcher::PartialJson(json!({ - "info": { - "mimetype": "image/jpeg", - "h": 600, - "w": 800, - } - }))) - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let upload_mock = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_owned())) - .with_status(200) - .match_header("content-type", "image/jpeg") - .with_body( - json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }) - .to_string(), - ) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - let mut media = Cursor::new("Hello world"); - - let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - size: None, - blurhash: None, - })); - - let response = - room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); - - upload_mock.assert(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) - } - - #[async_test] - async fn room_attachment_send_wrong_info() { - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .match_body(Matcher::PartialJson(json!({ - "info": { - "mimetype": "image/jpeg", - "h": 600, - "w": 800, - } - }))) - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let _m = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_owned())) - .with_status(200) - .match_header("content-type", "image/jpeg") - .with_body( - json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }) - .to_string(), - ) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - let mut media = Cursor::new("Hello world"); - - let config = AttachmentConfig::new().info(AttachmentInfo::Video(BaseVideoInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - duration: Some(Duration::from_millis(3600)), - size: None, - blurhash: None, - })); - - let response = room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await; - - assert!(response.is_err()) - } - - #[async_test] - async fn room_attachment_send_info_thumbnail() { - let client = logged_in_client().await; - - let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .match_body(Matcher::PartialJson(json!({ - "info": { - "mimetype": "image/jpeg", - "h": 600, - "w": 800, - "thumbnail_info": { - "h": 360, - "w": 480, - "mimetype":"image/jpeg", - "size": 3600, - }, - "thumbnail_url": "mxc://example.com/AQwafuaFswefuhsfAFAgsw", - } - }))) - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let upload_mock = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_owned())) - .with_status(200) - .match_header("content-type", "image/jpeg") - .with_body( - json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }) - .to_string(), - ) - .expect(2) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - let mut media = Cursor::new("Hello world"); - - let mut thumbnail_reader = Cursor::new("Thumbnail"); - - let config = AttachmentConfig::with_thumbnail(Thumbnail { - reader: &mut thumbnail_reader, - content_type: &mime::IMAGE_JPEG, - info: Some(BaseThumbnailInfo { - height: Some(uint!(360)), - width: Some(uint!(480)), - size: Some(uint!(3600)), - }), - }) - .info(AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - size: None, - blurhash: None, - })); - - let response = - room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); - - upload_mock.assert(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) - } - - #[async_test] - async fn room_redact() { - let client = logged_in_client().await; - - let _m = - mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::EVENT_ID.to_string()) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - let event_id = event_id!("$xxxxxxxx:example.com"); - - let txn_id = TransactionId::new(); - let reason = Some("Indecent material"); - let response = room.redact(event_id, reason, Some(txn_id)).await.unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) - } - - #[async_test] - async fn user_presence() { - let client = logged_in_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .create(); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/members".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::MEMBERS.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - let members: Vec = room.active_members().await.unwrap(); - - assert_eq!(2, members.len()); - // assert!(room.power_levels.is_some()) - } - - #[async_test] - async fn calculate_room_names_from_summary() { - let client = logged_in_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::DEFAULT_SYNC_SUMMARY.to_string()) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - let _response = client.sync_once(sync_settings).await.unwrap(); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - assert_eq!( - DisplayName::Calculated("example2".to_owned()), - room.display_name().await.unwrap() - ); - } - - #[async_test] - async fn invited_rooms() { - let client = logged_in_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::INVITE_SYNC.to_string()) - .create(); - - let _response = client.sync_once(SyncSettings::default()).await.unwrap(); - - assert!(client.joined_rooms().is_empty()); - assert!(client.left_rooms().is_empty()); - assert!(!client.invited_rooms().is_empty()); - - assert!(client.get_invited_room(room_id!("!696r7674:example.com")).is_some()); - } - - #[async_test] - async fn left_rooms() { - let client = logged_in_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::LEAVE_SYNC.to_string()) - .create(); - - let _response = client.sync_once(SyncSettings::default()).await.unwrap(); - - assert!(client.joined_rooms().is_empty()); - assert!(!client.left_rooms().is_empty()); - assert!(client.invited_rooms().is_empty()); - - assert!(client.get_left_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).is_some()) - } - - #[async_test] - async fn sync() { - let client = logged_in_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(test_json::SYNC.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let response = client.sync_once(sync_settings).await.unwrap(); - - assert_ne!(response.next_batch, ""); - - assert!(client.sync_token().await.is_some()); - } - - #[async_test] - async fn room_names() { - let client = logged_in_client().await; - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::SYNC.to_string()) - .expect_at_least(1) - .create(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - assert_eq!(client.rooms().len(), 1); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - - assert_eq!(DisplayName::Aliased("tutorial".to_owned()), room.display_name().await.unwrap()); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .match_header("authorization", "Bearer 1234") - .with_body(test_json::INVITE_SYNC.to_string()) - .expect_at_least(1) - .create(); - - let _response = client.sync_once(SyncSettings::new()).await.unwrap(); - - assert_eq!(client.rooms().len(), 1); - let invited_room = client.get_invited_room(room_id!("!696r7674:example.com")).unwrap(); - - assert_eq!( - DisplayName::Named("My Room Name".to_owned()), - invited_room.display_name().await.unwrap() - ); - } - - #[async_test] - async fn delete_devices() { - let client = no_retry_test_client().await; - - let _m = mock("POST", "/_matrix/client/r0/delete_devices") - .with_status(401) - .with_body( - json!({ - "flows": [ - { - "stages": [ - "m.login.password" - ] - } - ], - "params": {}, - "session": "vBslorikviAjxzYBASOBGfPp" - }) - .to_string(), - ) - .create(); - - let _m = mock("POST", "/_matrix/client/r0/delete_devices") - .with_status(401) - // empty response - // TODO rename that response type. - .with_body(test_json::LOGOUT.to_string()) - .create(); - - let devices = &[device_id!("DEVICEID").to_owned()]; - - if let Err(e) = client.delete_devices(devices, None).await { - if let Some(info) = e.uiaa_response() { - let mut auth_parameters = BTreeMap::new(); - - let identifier = json!({ - "type": "m.id.user", - "user": "example", - }); - auth_parameters.insert("identifier".to_owned(), identifier); - auth_parameters.insert("password".to_owned(), "wordpass".into()); - - let auth_data = uiaa::AuthData::Password(assign!( - uiaa::Password::new( - uiaa::UserIdentifier::UserIdOrLocalpart("example"), - "wordpass", - ), { - session: info.session.as_deref(), - } - )); - - client.delete_devices(devices, Some(auth_data)).await.unwrap(); - } - } - } - #[async_test] async fn retry_limit_http_requests() { - let client = test_client_builder() + let server = MockServer::start().await; + let client = test_client_builder(Some(server.uri())) .request_config(RequestConfig::new().retry_limit(3)) .build() .await @@ -3566,20 +2340,22 @@ pub(crate) mod tests { assert!(client.inner.http_client.request_config.retry_limit.unwrap() == 3); - let m = mock("POST", "/_matrix/client/r0/login").with_status(501).expect(3).create(); + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(501)) + .expect(3) + .mount(&server) + .await; - if client.login("example", "wordpass", None, None).await.is_err() { - m.assert(); - } else { - panic!("this request should return an `Err` variant") - } + client.login_username("example", "wordpass").send().await.unwrap_err(); } #[async_test] async fn retry_timeout_http_requests() { // Keep this timeout small so that the test doesn't take long let retry_timeout = Duration::from_secs(5); - let client = test_client_builder() + let server = MockServer::start().await; + let client = test_client_builder(Some(server.uri())) .request_config(RequestConfig::new().retry_timeout(retry_timeout)) .build() .await @@ -3587,469 +2363,43 @@ pub(crate) mod tests { assert!(client.inner.http_client.request_config.retry_timeout.unwrap() == retry_timeout); - let m = - mock("POST", "/_matrix/client/r0/login").with_status(501).expect_at_least(2).create(); + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(501)) + .expect(2..) + .mount(&server) + .await; - if client.login("example", "wordpass", None, None).await.is_err() { - m.assert(); - } else { - panic!("this request should return an `Err` variant") - } + client.login_username("example", "wordpass").send().await.unwrap_err(); } #[async_test] async fn short_retry_initial_http_requests() { - let client = test_client_builder().build().await.unwrap(); + let server = MockServer::start().await; + let client = test_client_builder(Some(server.uri())).build().await.unwrap(); - let m = - mock("POST", "/_matrix/client/r0/login").with_status(501).expect_at_least(3).create(); + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(501)) + .expect(3..) + .mount(&server) + .await; - if client.login("example", "wordpass", None, None).await.is_err() { - m.assert(); - } else { - panic!("this request should return an `Err` variant") - } + client.login_username("example", "wordpass").send().await.unwrap_err(); } #[async_test] async fn no_retry_http_requests() { - let client = logged_in_client().await; + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; - let m = mock("GET", "/_matrix/client/r0/devices").with_status(501).create(); - - if client.devices().await.is_err() { - m.assert(); - } else { - panic!("this request should return an `Err` variant") - } - } - - #[async_test] - async fn get_media_content() { - let client = logged_in_client().await; - - let request = MediaRequest { - source: MediaSource::Plain(mxc_uri!("mxc://localhost/textfile").to_owned()), - format: MediaFormat::File, - }; - - let m = mock( - "GET", - Matcher::Regex(r"^/_matrix/media/r0/download/localhost/textfile\?.*$".to_owned()), - ) - .with_status(200) - .with_body("Some very interesting text.") - .expect(2) - .create(); - - assert!(client.get_media_content(&request, true).await.is_ok()); - assert!(client.get_media_content(&request, true).await.is_ok()); - assert!(client.get_media_content(&request, false).await.is_ok()); - m.assert(); - } - - #[async_test] - async fn get_media_file() { - let client = logged_in_client().await; - - let event_content = ImageMessageEventContent::plain( - "filename.jpg".into(), - mxc_uri!("mxc://example.org/image").to_owned(), - Some(Box::new(assign!(ImageInfo::new(), { - height: Some(uint!(398)), - width: Some(uint!(394)), - mimetype: Some("image/jpeg".into()), - size: Some(uint!(31037)), - }))), - ); - - let m = mock( - "GET", - Matcher::Regex(r"^/_matrix/media/r0/download/example%2Eorg/image\?.*$".to_owned()), - ) - .with_status(200) - .with_body("binaryjpegdata") - .create(); - - assert!(client.get_file(event_content.clone(), true).await.is_ok()); - assert!(client.get_file(event_content.clone(), true).await.is_ok()); - m.assert(); - - let m = mock( - "GET", - Matcher::Regex(r"^/_matrix/media/r0/thumbnail/example%2Eorg/image\?.*$".to_owned()), - ) - .with_status(200) - .with_body("smallerbinaryjpegdata") - .create(); - - assert!(client - .get_thumbnail( - event_content, - MediaThumbnailSize { method: Method::Scale, width: uint!(100), height: uint!(100) }, - true - ) - .await - .is_ok()); - m.assert(); - } - - #[async_test] - async fn whoami() { - let client = logged_in_client().await; - - let _m = mock("GET", "/_matrix/client/r0/account/whoami") - .with_status(200) - .with_body(test_json::WHOAMI.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let user_id = user_id!("@joe:example.org"); - - assert_eq!(client.whoami().await.unwrap().user_id, user_id); - } - - #[async_test] - async fn test_state_event_getting() { - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - - let session = Session { - access_token: "1234".to_owned(), - user_id: user_id!("@example:localhost").to_owned(), - device_id: device_id!("DEVICEID").to_owned(), - }; - - let sync = json!({ - "next_batch": "1234", - "rooms": { - "join": { - "!SVkFJHzfwvuaIEawgC:localhost": { - "state": { - "events": [ - { - "type": "m.custom.note", - "sender": "@example:localhost", - "content": { - "body": "Note 1", - }, - "state_key": "note.1", - "origin_server_ts": 1611853078727u64, - "unsigned": { - "replaces_state": "$2s9GcbVxbbFS3EZY9vN1zhavaDJnF32cAIGAxi99NuQ", - "age": 15458166523u64 - }, - "event_id": "$NVCTvrlxodf3ZGjJ6foxepEq8ysSkTq8wG0wKeQBVZg" - }, - { - "type": "m.custom.note", - "sender": "@example2:localhost", - "content": { - "body": "Note 2", - }, - "state_key": "note.2", - "origin_server_ts": 1611853078727u64, - "unsigned": { - "replaces_state": "$2s9GcbVxbbFS3EZY9vN1zhavaDJnF32cAIGAxi99NuQ", - "age": 15458166523u64 - }, - "event_id": "$NVCTvrlxodf3ZGjJ6foxepEq8ysSkTq8wG0wKeQBVZg" - }, - { - "type": "m.room.encryption", - "sender": "@example:localhost", - "content": { - "algorithm": "m.megolm.v1.aes-sha2" - }, - "state_key": "", - "origin_server_ts": 1586437448151u64, - "unsigned": { - "age": 40873797099u64 - }, - "event_id": "$vyG3wu1QdJSh5gc-09SwjXBXlXo8gS7s4QV_Yxha0Xw" - }, - ] - } - } - } - } - }); - - let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(sync.to_string()) - .create(); - - let client = test_client_builder() - .request_config(RequestConfig::new().retry_limit(3)) - .build() - .await - .unwrap(); - client.restore_login(session.clone()).await.unwrap(); - - let room = client.get_joined_room(room_id); - assert!(room.is_none()); - - client.sync_once(SyncSettings::default()).await.unwrap(); - - let room = client.get_joined_room(room_id).unwrap(); - - let state_events = room.get_state_events(StateEventType::RoomEncryption).await.unwrap(); - assert_eq!(state_events.len(), 1); - - let state_events = room.get_state_events("m.custom.note".into()).await.unwrap(); - assert_eq!(state_events.len(), 2); - - let encryption_event = room - .get_state_event(StateEventType::RoomEncryption, "") - .await - .unwrap() - .unwrap() - .deserialize() - .unwrap(); - - matches::assert_matches!(encryption_event, AnySyncStateEvent::RoomEncryption(_)); - } - - // FIXME: removing timelines during reading the stream currently leaves to an - // inconsistent undefined state. This tests shows that, but because - // different implementations deal with problem in different, - // inconsistent manners, isn't activated. - //#[async_test] - #[allow(dead_code)] - #[cfg(feature = "experimental-timeline")] - async fn room_timeline_with_remove() { - let client = logged_in_client().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(test_json::SYNC.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _ = client.sync_once(sync_settings).await.unwrap(); - sync.assert(); - drop(sync); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - let (forward_stream, backward_stream) = room.timeline().await.unwrap(); - - // these two syncs lead to the store removing its existing timeline - // and replace them with new ones - let sync_2 = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/sync\?.*since=s526_47314_0_7_1_1_1_11444_1.*".to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::MORE_SYNC.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let sync_3 = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/sync\?.*since=s526_47314_0_7_1_1_1_11444_2.*".to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::MORE_SYNC_2.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let mocked_messages = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/rooms/.*/messages.*from=t392-516_47314_0_7_1_1_1_11444_1.*" - .to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::SYNC_ROOM_MESSAGES_BATCH_1.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let mocked_messages_2 = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/rooms/.*/messages.*from=t47409-4357353_219380_26003_2269.*" - .to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::SYNC_ROOM_MESSAGES_BATCH_2.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - assert_eq!(client.sync_token().await, Some("s526_47314_0_7_1_1_1_11444_1".to_owned())); - let sync_settings = SyncSettings::new() - .timeout(Duration::from_millis(3000)) - .token("s526_47314_0_7_1_1_1_11444_1"); - let _ = client.sync_once(sync_settings).await.unwrap(); - sync_2.assert(); - let sync_settings = SyncSettings::new() - .timeout(Duration::from_millis(3000)) - .token("s526_47314_0_7_1_1_1_11444_2"); - let _ = client.sync_once(sync_settings).await.unwrap(); - sync_3.assert(); - - let expected_forward_events = vec![ - "$152037280074GZeOm:localhost", - "$editevid:localhost", - "$151957878228ssqrJ:localhost", - "$15275046980maRLj:localhost", - "$15275047031IXQRi:localhost", - "$098237280074GZeOm:localhost", - "$152037280074GZeOm2:localhost", - "$editevid2:localhost", - "$151957878228ssqrJ2:localhost", - "$15275046980maRLj2:localhost", - "$15275047031IXQRi2:localhost", - "$098237280074GZeOm2:localhost", - ]; - - use futures_util::StreamExt; - let forward_events = forward_stream - .take(expected_forward_events.len()) - .collect::>() + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/devices")) + .respond_with(ResponseTemplate::new(501)) + .expect(1) + .mount(&server) .await; - for (r, e) in forward_events.into_iter().zip(expected_forward_events.iter()) { - assert_eq!(&r.event_id().unwrap().as_str(), e); - } - - let expected_backwards_events = vec![ - "$152037280074GZeOm:localhost", - "$1444812213350496Caaaf:example.com", - "$1444812213350496Cbbbf:example.com", - "$1444812213350496Ccccf:example.com", - "$1444812213350496Caaak:example.com", - "$1444812213350496Cbbbk:example.com", - "$1444812213350496Cccck:example.com", - ]; - - let backward_events = backward_stream - .take(expected_backwards_events.len()) - .collect::>>() - .await; - - for (r, e) in backward_events.into_iter().zip(expected_backwards_events.iter()) { - assert_eq!(&r.unwrap().event_id().unwrap().as_str(), e); - } - - mocked_messages.assert(); - mocked_messages_2.assert(); - } - - #[async_test] - #[cfg(feature = "experimental-timeline")] - async fn room_timeline() { - let client = logged_in_client().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) - .with_status(200) - .with_body(test_json::MORE_SYNC.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let _ = client.sync_once(sync_settings).await.unwrap(); - sync.assert(); - drop(sync); - let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - let (forward_stream, backward_stream) = room.timeline().await.unwrap(); - - let sync_2 = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/sync\?.*since=s526_47314_0_7_1_1_1_11444_2.*".to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::MORE_SYNC_2.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let mocked_messages = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/rooms/.*/messages.*from=t392-516_47314_0_7_1_1_1_11444_1.*" - .to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::SYNC_ROOM_MESSAGES_BATCH_1.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - let mocked_messages_2 = mock( - "GET", - Matcher::Regex( - r"^/_matrix/client/r0/rooms/.*/messages.*from=t47409-4357353_219380_26003_2269.*" - .to_owned(), - ), - ) - .with_status(200) - .with_body(test_json::SYNC_ROOM_MESSAGES_BATCH_2.to_string()) - .match_header("authorization", "Bearer 1234") - .create(); - - assert_eq!(client.sync_token().await, Some("s526_47314_0_7_1_1_1_11444_2".to_owned())); - let sync_settings = SyncSettings::new() - .timeout(Duration::from_millis(3000)) - .token("s526_47314_0_7_1_1_1_11444_2"); - let _ = client.sync_once(sync_settings).await.unwrap(); - sync_2.assert(); - - let expected_forward_events = vec![ - "$152037280074GZeOm2:localhost", - "$editevid2:localhost", - "$151957878228ssqrJ2:localhost", - "$15275046980maRLj2:localhost", - "$15275047031IXQRi2:localhost", - "$098237280074GZeOm2:localhost", - ]; - - use futures_util::StreamExt; - let forward_events = forward_stream - .take(expected_forward_events.len()) - .collect::>() - .await; - - for (r, e) in forward_events.into_iter().zip(expected_forward_events.iter()) { - assert_eq!(&r.event_id().unwrap().as_str(), e); - } - - let expected_backwards_events = vec![ - "$098237280074GZeOm:localhost", - "$15275047031IXQRi:localhost", - "$15275046980maRLj:localhost", - "$151957878228ssqrJ:localhost", - "$editevid:localhost", - "$152037280074GZeOm:localhost", - // ^^^ These come from the first sync before we asked for the timeline and thus - // where cached - // - // While the following are fetched over the network transparently to us after, - // when scrolling back in time: - "$1444812213350496Caaaf:example.com", - "$1444812213350496Cbbbf:example.com", - "$1444812213350496Ccccf:example.com", - "$1444812213350496Caaak:example.com", - "$1444812213350496Cbbbk:example.com", - "$1444812213350496Cccck:example.com", - ]; - - let backward_events = backward_stream - .take(expected_backwards_events.len()) - .collect::>>() - .await; - - for (r, e) in backward_events.into_iter().zip(expected_backwards_events.iter()) { - assert_eq!(&r.unwrap().event_id().unwrap().as_str(), e); - } - - mocked_messages.assert(); - mocked_messages_2.assert(); + client.devices().await.unwrap_err(); } } diff --git a/crates/matrix-sdk/src/docs/encryption.md b/crates/matrix-sdk/src/docs/encryption.md index 992f39618..2010ee0a5 100644 --- a/crates/matrix-sdk/src/docs/encryption.md +++ b/crates/matrix-sdk/src/docs/encryption.md @@ -210,7 +210,7 @@ step. This will replace the access token from the previous login call but won't create a new device. **Note** that the default store supports only a single device, logging in -with a different device id (either `None` or a device ID of another client) +with a different device ID (either `None` or a device ID of another client) is **not** supported using the default store. ## Common pitfalls diff --git a/crates/matrix-sdk/src/encryption/identities/devices.rs b/crates/matrix-sdk/src/encryption/identities/devices.rs index 11c770985..e03427b5b 100644 --- a/crates/matrix-sdk/src/encryption/identities/devices.rs +++ b/crates/matrix-sdk/src/encryption/identities/devices.rs @@ -404,7 +404,7 @@ pub struct UserDevices { } impl UserDevices { - /// Get the specific device with the given device id. + /// Get the specific device with the given device ID. pub fn get(&self, device_id: &DeviceId) -> Option { self.inner.get(device_id).map(|d| Device { inner: d, client: self.client.clone() }) } diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index b952ad279..a76ec8d19 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -126,7 +126,7 @@ impl Client { let keys = reader.finish(); ruma::events::room::EncryptedFileInit { url: response.content_uri, - key: keys.web_key, + key: keys.key, iv: keys.iv, hashes: keys.hashes, v: keys.version, @@ -155,7 +155,7 @@ impl Client { let keys = reader.finish(); ruma::events::room::EncryptedFileInit { url: response.content_uri, - key: keys.web_key, + key: keys.key, iv: keys.iv, hashes: keys.hashes, v: keys.version, @@ -873,35 +873,34 @@ impl Encryption { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use matrix_sdk_test::{async_test, EventBuilder, EventsJson}; - use mockito::{mock, Matcher}; use ruma::{ event_id, events::reaction::{ReactionEventContent, Relation}, room_id, }; use serde_json::json; + use wiremock::{ + matchers::{method, path_regex}, + Mock, MockServer, ResponseTemplate, + }; use crate::client::tests::logged_in_client; #[async_test] async fn test_reaction_sending() { - let client = logged_in_client().await; + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; let event_id = event_id!("$2:example.org"); let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - let _m = mock( - "PUT", - Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/m%2Ereaction/.*".to_owned()), - ) - .with_status(200) - .with_body( - json!({ + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m%2Ereaction/.*".to_owned())) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id, - }) - .to_string(), - ) - .create(); + }))) + .mount(&server) + .await; let response = EventBuilder::default() .add_state_event(EventsJson::Member) diff --git a/crates/matrix-sdk/src/event_handler.rs b/crates/matrix-sdk/src/event_handler.rs index 444a18bbd..49718a089 100644 --- a/crates/matrix-sdk/src/event_handler.rs +++ b/crates/matrix-sdk/src/event_handler.rs @@ -576,7 +576,7 @@ mod tests { async fn event_handler() -> crate::Result<()> { use std::sync::atomic::{AtomicU8, Ordering::SeqCst}; - let client = crate::client::tests::logged_in_client().await; + let client = crate::client::tests::logged_in_client(None).await; let member_count = Arc::new(AtomicU8::new(0)); let typing_count = Arc::new(AtomicU8::new(0)); diff --git a/crates/matrix-sdk/src/http_client.rs b/crates/matrix-sdk/src/http_client.rs index 9c1136b25..0d70eeb26 100644 --- a/crates/matrix-sdk/src/http_client.rs +++ b/crates/matrix-sdk/src/http_client.rs @@ -125,7 +125,7 @@ impl HttpClient { } trace!("Serializing request"); - let request = if !self.request_config.assert_identity { + let request = if !config.assert_identity { let send_access_token = if auth_scheme == AuthScheme::None && !config.force_auth { // Small optimization: Don't take the session lock if we know the auth token // isn't going to be used anyways. @@ -169,7 +169,7 @@ impl HttpClient { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct HttpSettings { #[cfg(not(target_arch = "wasm32"))] pub(crate) disable_ssl_verification: bool, diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 20545e835..5303dfe01 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -58,7 +58,9 @@ mod sync; pub mod encryption; pub use account::Account; -pub use client::{Client, ClientBuildError, ClientBuilder, LoopCtrl}; +#[cfg(all(feature = "sso-login", not(target_arch = "wasm32")))] +pub use client::SsoLoginBuilder; +pub use client::{Client, ClientBuildError, ClientBuilder, LoginBuilder, LoopCtrl}; #[cfg(feature = "image-proc")] pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, Result, RumaApiError}; diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 31c56a3a3..d637ccb49 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -28,7 +28,10 @@ use ruma::{ assign, events::{ direct::DirectEvent, - room::{history_visibility::HistoryVisibility, MediaSource}, + room::{ + history_visibility::HistoryVisibility, server_acl::RoomServerAclEventContent, + MediaSource, + }, tag::{TagInfo, TagName}, AnyRoomAccountDataEvent, AnyStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, RedactContent, RedactedEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, @@ -36,7 +39,7 @@ use ruma::{ SyncStateEvent, }, serde::Raw, - uint, EventId, RoomId, UInt, UserId, + uint, EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedServerName, RoomId, UInt, UserId, }; use crate::{ @@ -931,6 +934,142 @@ impl Common { Err(Error::NoOlmMachine) } } + + /// Get a list of servers that should know this room. + /// + /// Uses the synced members of the room and the suggested [routing + /// algorithm] from the Matrix spec. + /// + /// Returns at most three servers. + /// + /// [routing algorithm]: https://spec.matrix.org/v1.3/appendices/#routing + pub async fn route(&self) -> Result> { + let acl_ev = self + .get_state_event_static::("") + .await? + .and_then(|ev| ev.deserialize().ok()); + let acl = acl_ev.as_ref().and_then(|ev| ev.as_original()).map(|ev| &ev.content); + + // Filter out server names that: + // - Are blocked due to server ACLs + // - Are IP addresses + let members: Vec<_> = self + .joined_members_no_sync() + .await? + .into_iter() + .filter(|member| { + let server = member.user_id().server_name(); + acl.filter(|acl| !acl.is_allowed(server)).is_none() && !server.is_ip_literal() + }) + .collect(); + + // Get the server of the highest power level user in the room, provided + // they are at least power level 50. + let max = members + .iter() + .max_by_key(|member| member.power_level()) + .filter(|max| max.power_level() >= 50) + .map(|member| member.user_id().server_name()); + + // Sort the servers by population. + let servers = members + .iter() + .map(|member| member.user_id().server_name()) + .filter(|server| max.filter(|max| max == server).is_none()) + .fold(BTreeMap::<_, u32>::new(), |mut servers, server| { + *servers.entry(server).or_default() += 1; + servers + }); + let mut servers: Vec<_> = servers.into_iter().collect(); + servers.sort_unstable_by(|(_, count_a), (_, count_b)| count_b.cmp(count_a)); + + Ok(max + .into_iter() + .chain(servers.into_iter().map(|(name, _)| name)) + .take(3) + .map(ToOwned::to_owned) + .collect()) + } + + /// Get a `matrix.to` permalink to this room. + /// + /// If this room has an alias, we use it. Otherwise, we try to use the + /// synced members in the room for [routing] the room ID. + /// + /// [routing]: https://spec.matrix.org/v1.3/appendices/#routing + pub async fn matrix_to_permalink(&self) -> Result { + if let Some(alias) = self.canonical_alias().or_else(|| self.alt_aliases().pop()) { + return Ok(alias.matrix_to_uri()); + } + + let via = self.route().await?; + Ok(self.room_id().matrix_to_uri(via)) + } + + /// Get a `matrix:` permalink to this room. + /// + /// If this room has an alias, we use it. Otherwise, we try to use the + /// synced members in the room for [routing] the room ID. + /// + /// # Arguments + /// + /// * `join` - Whether the user should join the room. + /// + /// [routing]: https://spec.matrix.org/v1.3/appendices/#routing + pub async fn matrix_permalink(&self, join: bool) -> Result { + if let Some(alias) = self.canonical_alias().or_else(|| self.alt_aliases().pop()) { + return Ok(alias.matrix_uri(join)); + } + + let via = self.route().await?; + Ok(self.room_id().matrix_uri(via, join)) + } + + /// Get a `matrix.to` permalink to an event in this room. + /// + /// We try to use the synced members in the room for [routing] the room ID. + /// + /// *Note*: This method does not check if the given event ID is actually + /// part of this room. It needs to be checked before calling this method + /// otherwise the permalink won't work. + /// + /// # Arguments + /// + /// * `event_id` - The ID of the event. + /// + /// [routing]: https://spec.matrix.org/v1.3/appendices/#routing + pub async fn matrix_to_event_permalink( + &self, + event_id: impl Into, + ) -> Result { + // Don't use the alias because an event is tied to a room ID, but an + // alias might point to another room, e.g. after a room upgrade. + let via = self.route().await?; + Ok(self.room_id().matrix_to_event_uri(event_id, via)) + } + + /// Get a `matrix:` permalink to an event in this room. + /// + /// We try to use the synced members in the room for [routing] the room ID. + /// + /// *Note*: This method does not check if the given event ID is actually + /// part of this room. It needs to be checked before calling this method + /// otherwise the permalink won't work. + /// + /// # Arguments + /// + /// * `event_id` - The ID of the event. + /// + /// [routing]: https://spec.matrix.org/v1.3/appendices/#routing + pub async fn matrix_event_permalink( + &self, + event_id: impl Into, + ) -> Result { + // Don't use the alias because an event is tied to a room ID, but an + // alias might point to another room, e.g. after a room upgrade. + let via = self.route().await?; + Ok(self.room_id().matrix_event_uri(event_id, via)) + } } /// Options for [`messages`][Common::messages]. diff --git a/crates/matrix-sdk/src/room/invited.rs b/crates/matrix-sdk/src/room/invited.rs index 1d809271b..face9e554 100644 --- a/crates/matrix-sdk/src/room/invited.rs +++ b/crates/matrix-sdk/src/room/invited.rs @@ -1,7 +1,8 @@ use std::ops::Deref; -use crate::{room::Common, BaseRoom, Client, Result, RoomType}; +use thiserror::Error; +use crate::{room::Common, BaseRoom, Client, Error, Result, RoomMember, RoomType}; /// A room in the invited state. /// /// This struct contains all methods specific to a `Room` with type @@ -12,6 +13,24 @@ pub struct Invited { pub(crate) inner: Common, } +/// Details of the (latest) invite. +#[derive(Debug, Clone)] +pub struct Invite { + /// Who has been invited. + pub invitee: RoomMember, + /// Who sent the invite. + pub inviter: Option, +} + +#[derive(Error, Debug)] +pub enum InvitationError { + /// The client isn't logged in. + #[error("The client isn't authenticated")] + NotAuthenticated, + #[error("No membership event found")] + EventMissing, +} + impl Invited { /// Create a new `room::Invited` if the underlying `Room` has type /// `RoomType::Invited`. @@ -38,6 +57,24 @@ impl Invited { pub async fn accept_invitation(&self) -> Result<()> { self.inner.join().await } + + /// The membership details of the (latest) invite for this room. + pub async fn invite_details(&self) -> Result { + let user_id = self + .inner + .client + .user_id() + .ok_or_else(|| Error::UnknownError(Box::new(InvitationError::NotAuthenticated)))?; + let invitee = self + .inner + .get_member(user_id) + .await? + .ok_or_else(|| Error::UnknownError(Box::new(InvitationError::EventMissing)))?; + let event = invitee.event(); + let inviter_id = event.sender(); + let inviter = self.inner.get_member(inviter_id).await?; + Ok(Invite { invitee, inviter }) + } } impl Deref for Invited { diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs new file mode 100644 index 000000000..f82e9ea52 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -0,0 +1,588 @@ +use std::{collections::BTreeMap, str::FromStr, time::Duration}; + +use matrix_sdk::{ + config::SyncSettings, + media::{MediaFormat, MediaRequest, MediaThumbnailSize}, + Error, HttpError, RumaApiError, +}; +use matrix_sdk_test::{async_test, test_json}; +use ruma::{ + api::{ + client::{ + self as client_api, + account::register::{v3::Request as RegistrationRequest, RegistrationKind}, + directory::{ + get_public_rooms, + get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest}, + }, + media::get_content_thumbnail::v3::Method, + session::get_login_types::v3::LoginType, + uiaa::{self, UiaaResponse}, + }, + error::{FromHttpResponseError, ServerError}, + }, + assign, device_id, + directory::Filter, + events::room::{message::ImageMessageEventContent, ImageInfo, MediaSource}, + mxc_uri, room_id, uint, user_id, +}; +use serde_json::json; +use url::Url; +use wiremock::{ + matchers::{header, method, path, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client, mock_sync, no_retry_test_client}; + +#[async_test] +async fn set_homeserver() { + let (client, _) = no_retry_test_client().await; + let homeserver = Url::from_str("http://example.com/").unwrap(); + client.set_homeserver(homeserver.clone()).await; + + assert_eq!(client.homeserver().await, homeserver); +} + +#[async_test] +async fn login() { + let (client, server) = no_retry_test_client().await; + let homeserver = Url::from_str(&server.uri()).unwrap(); + + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES)) + .mount(&server) + .await; + + let can_password = client + .get_login_types() + .await + .unwrap() + .flows + .iter() + .any(|flow| matches!(flow, LoginType::Password(_))); + assert!(can_password); + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN)) + .mount(&server) + .await; + + client.login_username("example", "wordpass").send().await.unwrap(); + + let logged_in = client.logged_in(); + assert!(logged_in, "Client should be logged in"); + + assert_eq!(client.homeserver().await, homeserver); +} + +#[async_test] +async fn login_with_discovery() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_WITH_DISCOVERY)) + .mount(&server) + .await; + + client.login_username("example", "wordpass").send().await.unwrap(); + + let logged_in = client.logged_in(); + assert!(logged_in, "Client should be logged in"); + + assert_eq!(client.homeserver().await.as_str(), "https://example.org/"); +} + +#[async_test] +async fn login_no_discovery() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN)) + .mount(&server) + .await; + + client.login_username("example", "wordpass").send().await.unwrap(); + + let logged_in = client.logged_in(); + assert!(logged_in, "Client should be logged in"); + + assert_eq!(client.homeserver().await, Url::parse(&server.uri()).unwrap()); +} + +#[async_test] +#[cfg(feature = "sso-login")] +async fn login_with_sso() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN)) + .mount(&server) + .await; + + let idp = ruma::api::client::session::get_login_types::v3::IdentityProvider::new( + "some-id".to_owned(), + "idp-name".to_owned(), + ); + client + .login_sso(|sso_url| async move { + let sso_url = Url::parse(&sso_url).unwrap(); + + let (_, redirect) = + sso_url.query_pairs().find(|(key, _)| key == "redirectUrl").unwrap(); + + let mut redirect_url = Url::parse(&redirect).unwrap(); + redirect_url.set_query(Some("loginToken=tinytoken")); + + reqwest::get(redirect_url.to_string()).await.unwrap(); + + Ok(()) + }) + .identity_provider_id(&idp.id) + .send() + .await + .unwrap(); + + let logged_in = client.logged_in(); + assert!(logged_in, "Client should be logged in"); +} + +#[async_test] +async fn login_with_sso_token() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES)) + .mount(&server) + .await; + + let can_sso = client + .get_login_types() + .await + .unwrap() + .flows + .iter() + .any(|flow| matches!(flow, LoginType::Sso(_))); + assert!(can_sso); + + let sso_url = client.get_sso_login_url("http://127.0.0.1:3030", None).await; + assert!(sso_url.is_ok()); + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN)) + .mount(&server) + .await; + + client.login_token("averysmalltoken").send().await.unwrap(); + + let logged_in = client.logged_in(); + assert!(logged_in, "Client should be logged in"); +} + +#[async_test] +async fn login_error() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/login")) + .respond_with(ResponseTemplate::new(403).set_body_json(&*test_json::LOGIN_RESPONSE_ERR)) + .mount(&server) + .await; + + if let Err(err) = client.login_username("example", "wordpass").send().await { + if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( + RumaApiError::ClientApi(client_api::Error { kind, message, status_code }), + )))) = err + { + if let client_api::error::ErrorKind::Forbidden = kind { + } else { + panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); + } + assert_eq!(message, "Invalid password".to_owned()); + assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); + } else { + panic!("found the wrong `Error` type {:?}, expected `Error::RumaResponse", err); + } + } else { + panic!("this request should return an `Err` variant") + } +} + +#[async_test] +async fn register_error() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/register")) + .respond_with( + ResponseTemplate::new(403).set_body_json(&*test_json::REGISTRATION_RESPONSE_ERR), + ) + .mount(&server) + .await; + + let user = assign!(RegistrationRequest::new(), { + username: Some("user"), + password: Some("password"), + auth: Some(uiaa::AuthData::FallbackAcknowledgement( + uiaa::FallbackAcknowledgement::new("foobar"), + )), + kind: RegistrationKind::User, + }); + + if let Err(err) = client.register(user).await { + if let HttpError::UiaaError(FromHttpResponseError::Server(ServerError::Known( + UiaaResponse::MatrixError(client_api::Error { kind, message, status_code }), + ))) = err + { + if let client_api::error::ErrorKind::Forbidden = kind { + } else { + panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); + } + assert_eq!(message, "Invalid password".to_owned()); + assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); + } else { + panic!("found the wrong `Error` type {:#?}, expected `UiaaResponse`", err); + } + } else { + panic!("this request should return an `Err` variant") + } +} + +#[async_test] +async fn sync() { + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let response = client.sync_once(sync_settings).await.unwrap(); + + assert_ne!(response.next_batch, ""); + + assert!(client.sync_token().await.is_some()); +} + +#[async_test] +async fn devices() { + let (client, server) = logged_in_client().await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/devices")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::DEVICES)) + .mount(&server) + .await; + + assert!(client.devices().await.is_ok()); +} + +#[async_test] +async fn delete_devices() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/delete_devices")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(401).set_body_json(json!({ + "flows": [ + { + "stages": [ + "m.login.password" + ] + } + ], + "params": {}, + "session": "vBslorikviAjxzYBASOBGfPp" + }))) + .up_to_n_times(1) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/delete_devices")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(401).set_body_json(json!({ + "flows": [ + { + "stages": [ + "m.login.password" + ] + } + ], + "params": {}, + "session": "vBslorikviAjxzYBASOBGfPp" + }))) + .mount(&server) + .await; + + let devices = &[device_id!("DEVICEID").to_owned()]; + + if let Err(e) = client.delete_devices(devices, None).await { + if let Some(info) = e.uiaa_response() { + let mut auth_parameters = BTreeMap::new(); + + let identifier = json!({ + "type": "m.id.user", + "user": "example", + }); + auth_parameters.insert("identifier".to_owned(), identifier); + auth_parameters.insert("password".to_owned(), "wordpass".into()); + + let auth_data = uiaa::AuthData::Password(assign!( + uiaa::Password::new( + uiaa::UserIdentifier::UserIdOrLocalpart("example"), + "wordpass", + ), { + session: info.session.as_deref(), + } + )); + + client.delete_devices(devices, Some(auth_data)).await.unwrap(); + } + } +} + +#[async_test] +async fn resolve_room_alias() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/directory/room/%23alias%3Aexample%2Eorg")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::GET_ALIAS)) + .mount(&server) + .await; + + let alias = ruma::room_alias_id!("#alias:example.org"); + assert!(client.resolve_room_alias(alias).await.is_ok()); +} + +#[async_test] +async fn join_leave_room() { + let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let room = client.get_joined_room(room_id); + assert!(room.is_none()); + + client.sync_once(SyncSettings::default()).await.unwrap(); + + let room = client.get_left_room(room_id); + assert!(room.is_none()); + + let room = client.get_joined_room(room_id); + assert!(room.is_some()); + + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, &*test_json::LEAVE_SYNC_EVENT, Some(sync_token.clone())).await; + + client.sync_once(SyncSettings::default().token(sync_token)).await.unwrap(); + + let room = client.get_joined_room(room_id); + assert!(room.is_none()); + + let room = client.get_left_room(room_id); + assert!(room.is_some()); +} + +#[async_test] +async fn join_room_by_id() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/join")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_ID)) + .mount(&server) + .await; + + let room_id = room_id!("!testroom:example.org"); + + assert_eq!( + // this is the `join_by_room_id::Response` but since no PartialEq we check the RoomId + // field + client.join_room_by_id(room_id).await.unwrap().room_id, + room_id + ); +} + +#[async_test] +async fn join_room_by_id_or_alias() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/join/")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_ID)) + .mount(&server) + .await; + + let room_id = room_id!("!testroom:example.org").into(); + + assert_eq!( + // this is the `join_by_room_id::Response` but since no PartialEq we check the RoomId + // field + client + .join_room_by_id_or_alias(room_id, &["server.com".try_into().unwrap()]) + .await + .unwrap() + .room_id, + room_id!("!testroom:example.org") + ); +} + +#[async_test] +async fn room_search_all() { + let (client, server) = no_retry_test_client().await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/publicRooms")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS)) + .mount(&server) + .await; + + let get_public_rooms::v3::Response { chunk, .. } = + client.public_rooms(Some(10), None, None).await.unwrap(); + assert_eq!(chunk.len(), 1); +} + +#[async_test] +async fn room_search_filtered() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/publicRooms")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS)) + .mount(&server) + .await; + + let generic_search_term = Some("cheese"); + let filter = assign!(Filter::new(), { generic_search_term }); + let request = assign!(PublicRoomsFilterRequest::new(), { filter }); + + let get_public_rooms_filtered::v3::Response { chunk, .. } = + client.public_rooms_filtered(request).await.unwrap(); + assert_eq!(chunk.len(), 1); +} + +#[async_test] +async fn invited_rooms() { + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::INVITE_SYNC, None).await; + + let _response = client.sync_once(SyncSettings::default()).await.unwrap(); + + assert!(client.joined_rooms().is_empty()); + assert!(client.left_rooms().is_empty()); + assert!(!client.invited_rooms().is_empty()); + + assert!(client.get_invited_room(room_id!("!696r7674:example.com")).is_some()); +} + +#[async_test] +async fn left_rooms() { + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::LEAVE_SYNC, None).await; + + let _response = client.sync_once(SyncSettings::default()).await.unwrap(); + + assert!(client.joined_rooms().is_empty()); + assert!(!client.left_rooms().is_empty()); + assert!(client.invited_rooms().is_empty()); + + assert!(client.get_left_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).is_some()) +} + +#[async_test] +async fn get_media_content() { + let (client, server) = logged_in_client().await; + + let request = MediaRequest { + source: MediaSource::Plain(mxc_uri!("mxc://localhost/textfile").to_owned()), + format: MediaFormat::File, + }; + + Mock::given(method("GET")) + .and(path("/_matrix/media/r0/download/localhost/textfile")) + .respond_with(ResponseTemplate::new(200).set_body_string("Some very interesting text.")) + .expect(2) + .mount(&server) + .await; + + assert!(client.get_media_content(&request, true).await.is_ok()); + assert!(client.get_media_content(&request, true).await.is_ok()); + assert!(client.get_media_content(&request, false).await.is_ok()); +} + +#[async_test] +async fn get_media_file() { + let (client, server) = logged_in_client().await; + + let event_content = ImageMessageEventContent::plain( + "filename.jpg".into(), + mxc_uri!("mxc://example.org/image").to_owned(), + Some(Box::new(assign!(ImageInfo::new(), { + height: Some(uint!(398)), + width: Some(uint!(394)), + mimetype: Some("image/jpeg".into()), + size: Some(uint!(31037)), + }))), + ); + + Mock::given(method("GET")) + .and(path("/_matrix/media/r0/download/example%2Eorg/image")) + .respond_with(ResponseTemplate::new(200).set_body_raw("binaryjpegdata", "image/jpeg")) + .expect(1) + .named("get_file") + .mount(&server) + .await; + + assert!(client.get_file(event_content.clone(), true).await.is_ok()); + assert!(client.get_file(event_content.clone(), true).await.is_ok()); + + Mock::given(method("GET")) + .and(path("/_matrix/media/r0/thumbnail/example%2Eorg/image")) + .respond_with( + ResponseTemplate::new(200).set_body_raw("smallerbinaryjpegdata", "image/jpeg"), + ) + .expect(1) + .named("get_thumbnail") + .mount(&server) + .await; + + assert!(client + .get_thumbnail( + event_content, + MediaThumbnailSize { method: Method::Scale, width: uint!(100), height: uint!(100) }, + true + ) + .await + .is_ok()); +} + +#[async_test] +async fn whoami() { + let (client, server) = logged_in_client().await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/r0/account/whoami")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::WHOAMI)) + .mount(&server) + .await; + + let user_id = user_id!("@joe:example.org"); + + assert_eq!(client.whoami().await.unwrap().user_id, user_id); +} diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs new file mode 100644 index 000000000..9ddca69e3 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -0,0 +1,59 @@ +// The http mocking library is not supported for wasm32 +#![cfg(not(target_arch = "wasm32"))] + +use matrix_sdk::{config::RequestConfig, Client, ClientBuilder, Session}; +use ruma::{api::MatrixVersion, device_id, user_id}; +use serde::Serialize; +use wiremock::{ + matchers::{header, method, path, query_param, query_param_is_missing}, + Mock, MockServer, ResponseTemplate, +}; + +mod client; +mod room; + +async fn test_client_builder() -> (ClientBuilder, MockServer) { + let server = MockServer::start().await; + let builder = + Client::builder().homeserver_url(server.uri()).server_versions([MatrixVersion::V1_0]); + (builder, server) +} + +async fn no_retry_test_client() -> (Client, MockServer) { + let (builder, server) = test_client_builder().await; + let client = + builder.request_config(RequestConfig::new().disable_retry()).build().await.unwrap(); + (client, server) +} + +async fn logged_in_client() -> (Client, MockServer) { + let session = Session { + access_token: "1234".to_owned(), + user_id: user_id!("@example:localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }; + let (client, server) = no_retry_test_client().await; + client.restore_login(session).await.unwrap(); + + (client, server) +} + +/// Mount a Mock on the given server to handle the `GET /sync` endpoint with +/// an optional `since` param that returns a 200 status code with the given +/// response body. +async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option) { + let mut builder = Mock::given(method("GET")) + .and(path("/_matrix/client/r0/sync")) + .and(header("authorization", "Bearer 1234")); + + if let Some(since) = since { + builder = builder.and(query_param("since", since)); + } else { + builder = builder.and(query_param_is_missing("since")); + } + + builder + .respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + .mount(server) + .await; +} diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs new file mode 100644 index 000000000..c9c9bd177 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -0,0 +1,720 @@ +use std::time::Duration; + +use matrix_sdk::{config::SyncSettings, DisplayName, RoomMember}; +use matrix_sdk_test::{async_test, test_json}; +use ruma::{ + event_id, + events::{AnySyncStateEvent, StateEventType}, + room_id, +}; +use serde_json::{json, Value as JsonValue}; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client, mock_sync}; + +#[async_test] +async fn user_presence() { + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/members")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::MEMBERS)) + .mount(&server) + .await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let members: Vec = room.active_members().await.unwrap(); + + assert_eq!(2, members.len()); + // assert!(room.power_levels.is_some()) +} + +#[async_test] +async fn calculate_room_names_from_summary() { + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::DEFAULT_SYNC_SUMMARY, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + assert_eq!(DisplayName::Calculated("example2".to_owned()), room.display_name().await.unwrap()); +} + +#[async_test] +async fn room_names() { + let (client, server) = logged_in_client().await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + assert_eq!(client.rooms().len(), 1); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + assert_eq!(DisplayName::Aliased("tutorial".to_owned()), room.display_name().await.unwrap()); + + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, &*test_json::INVITE_SYNC, Some(sync_token.clone())).await; + + let _response = client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!(client.rooms().len(), 1); + let invited_room = client.get_invited_room(room_id!("!696r7674:example.com")).unwrap(); + + assert_eq!( + DisplayName::Named("My Room Name".to_owned()), + invited_room.display_name().await.unwrap() + ); +} + +#[async_test] +async fn test_state_event_getting() { + let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + + let (client, server) = logged_in_client().await; + + let sync = json!({ + "next_batch": "1234", + "rooms": { + "join": { + "!SVkFJHzfwvuaIEawgC:localhost": { + "state": { + "events": [ + { + "type": "m.custom.note", + "sender": "@example:localhost", + "content": { + "body": "Note 1", + }, + "state_key": "note.1", + "origin_server_ts": 1611853078727u64, + "unsigned": { + "replaces_state": "$2s9GcbVxbbFS3EZY9vN1zhavaDJnF32cAIGAxi99NuQ", + "age": 15458166523u64 + }, + "event_id": "$NVCTvrlxodf3ZGjJ6foxepEq8ysSkTq8wG0wKeQBVZg" + }, + { + "type": "m.custom.note", + "sender": "@example2:localhost", + "content": { + "body": "Note 2", + }, + "state_key": "note.2", + "origin_server_ts": 1611853078727u64, + "unsigned": { + "replaces_state": "$2s9GcbVxbbFS3EZY9vN1zhavaDJnF32cAIGAxi99NuQ", + "age": 15458166523u64 + }, + "event_id": "$NVCTvrlxodf3ZGjJ6foxepEq8ysSkTq8wG0wKeQBVZg" + }, + { + "type": "m.room.encryption", + "sender": "@example:localhost", + "content": { + "algorithm": "m.megolm.v1.aes-sha2" + }, + "state_key": "", + "origin_server_ts": 1586437448151u64, + "unsigned": { + "age": 40873797099u64 + }, + "event_id": "$vyG3wu1QdJSh5gc-09SwjXBXlXo8gS7s4QV_Yxha0Xw" + }, + ] + } + } + } + } + }); + + mock_sync(&server, sync, None).await; + + let room = client.get_joined_room(room_id); + assert!(room.is_none()); + + client.sync_once(SyncSettings::default()).await.unwrap(); + + let room = client.get_joined_room(room_id).unwrap(); + + let state_events = room.get_state_events(StateEventType::RoomEncryption).await.unwrap(); + assert_eq!(state_events.len(), 1); + + let state_events = room.get_state_events("m.custom.note".into()).await.unwrap(); + assert_eq!(state_events.len(), 2); + + let encryption_event = room + .get_state_event(StateEventType::RoomEncryption, "") + .await + .unwrap() + .unwrap() + .deserialize() + .unwrap(); + + matches::assert_matches!(encryption_event, AnySyncStateEvent::RoomEncryption(_)); +} + +// FIXME: removing timelines during reading the stream currently leaves to an +// inconsistent undefined state. This tests shows that, but because +// different implementations deal with problem in different, +// inconsistent manners, isn't activated. +//#[async_test] +#[allow(dead_code)] +#[cfg(feature = "experimental-timeline")] +async fn room_timeline_with_remove() { + use futures_util::StreamExt; + use matrix_sdk::deserialized_responses::SyncRoomEvent; + use wiremock::matchers::query_param; + + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + mock_sync(&server, &*test_json::SYNC, None).await; + + let _ = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let (forward_stream, backward_stream) = room.timeline().await.unwrap(); + + // these two syncs lead to the store removing its existing timeline + // and replace them with new ones + mock_sync(&server, &*test_json::MORE_SYNC, Some("s526_47314_0_7_1_1_1_11444_1".to_owned())) + .await; + mock_sync(&server, &*test_json::MORE_SYNC_2, Some("s526_47314_0_7_1_1_1_11444_2".to_owned())) + .await; + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .and(query_param("from", "t392-516_47314_0_7_1_1_1_11444_1")) + .respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_1), + ) + .expect(1) + .named("messages_batch_1") + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .and(query_param("from", "t47409-4357353_219380_26003_2269")) + .respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_2), + ) + .expect(1) + .named("messages_batch_2") + .mount(&server) + .await; + + assert_eq!(client.sync_token().await, Some("s526_47314_0_7_1_1_1_11444_1".to_owned())); + let sync_settings = SyncSettings::new() + .timeout(Duration::from_millis(3000)) + .token("s526_47314_0_7_1_1_1_11444_1"); + let _ = client.sync_once(sync_settings).await.unwrap(); + + let sync_settings = SyncSettings::new() + .timeout(Duration::from_millis(3000)) + .token("s526_47314_0_7_1_1_1_11444_2"); + let _ = client.sync_once(sync_settings).await.unwrap(); + + let expected_forward_events = vec![ + "$152037280074GZeOm:localhost", + "$editevid:localhost", + "$151957878228ssqrJ:localhost", + "$15275046980maRLj:localhost", + "$15275047031IXQRi:localhost", + "$098237280074GZeOm:localhost", + "$152037280074GZeOm2:localhost", + "$editevid2:localhost", + "$151957878228ssqrJ2:localhost", + "$15275046980maRLj2:localhost", + "$15275047031IXQRi2:localhost", + "$098237280074GZeOm2:localhost", + ]; + + let forward_events = + forward_stream.take(expected_forward_events.len()).collect::>().await; + + for (r, e) in forward_events.into_iter().zip(expected_forward_events.iter()) { + assert_eq!(&r.event_id().unwrap().as_str(), e); + } + + let expected_backwards_events = vec![ + "$152037280074GZeOm:localhost", + "$1444812213350496Caaaf:example.com", + "$1444812213350496Cbbbf:example.com", + "$1444812213350496Ccccf:example.com", + "$1444812213350496Caaak:example.com", + "$1444812213350496Cbbbk:example.com", + "$1444812213350496Cccck:example.com", + ]; + + let backward_events = backward_stream + .take(expected_backwards_events.len()) + .collect::>>() + .await; + + for (r, e) in backward_events.into_iter().zip(expected_backwards_events.iter()) { + assert_eq!(&r.unwrap().event_id().unwrap().as_str(), e); + } +} + +#[async_test] +#[cfg(feature = "experimental-timeline")] +async fn room_timeline() { + use futures_util::StreamExt; + use matrix_sdk::deserialized_responses::SyncRoomEvent; + use wiremock::matchers::query_param; + + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + mock_sync(&server, &*test_json::MORE_SYNC, None).await; + + let _ = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + let (forward_stream, backward_stream) = room.timeline().await.unwrap(); + + let sync_token = client.sync_token().await.unwrap(); + assert_eq!(sync_token, "s526_47314_0_7_1_1_1_11444_2"); + mock_sync(&server, &*test_json::MORE_SYNC_2, Some(sync_token.clone())).await; + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .and(query_param("from", "t392-516_47314_0_7_1_1_1_11444_1")) + .respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_1), + ) + .expect(1) + .named("messages_batch_1") + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .and(query_param("from", "t47409-4357353_219380_26003_2269")) + .respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::SYNC_ROOM_MESSAGES_BATCH_2), + ) + .expect(1) + .named("messages_batch_2") + .mount(&server) + .await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)).token(sync_token); + let _ = client.sync_once(sync_settings).await.unwrap(); + + let expected_forward_events = vec![ + "$152037280074GZeOm2:localhost", + "$editevid2:localhost", + "$151957878228ssqrJ2:localhost", + "$15275046980maRLj2:localhost", + "$15275047031IXQRi2:localhost", + "$098237280074GZeOm2:localhost", + ]; + + let forward_events = + forward_stream.take(expected_forward_events.len()).collect::>().await; + + for (r, e) in forward_events.into_iter().zip(expected_forward_events.iter()) { + assert_eq!(&r.event_id().unwrap().as_str(), e); + } + + let expected_backwards_events = vec![ + "$098237280074GZeOm:localhost", + "$15275047031IXQRi:localhost", + "$15275046980maRLj:localhost", + "$151957878228ssqrJ:localhost", + "$editevid:localhost", + "$152037280074GZeOm:localhost", + // ^^^ These come from the first sync before we asked for the timeline and thus + // where cached + // + // While the following are fetched over the network transparently to us after, + // when scrolling back in time: + "$1444812213350496Caaaf:example.com", + "$1444812213350496Cbbbf:example.com", + "$1444812213350496Ccccf:example.com", + "$1444812213350496Caaak:example.com", + "$1444812213350496Cbbbk:example.com", + "$1444812213350496Cccck:example.com", + ]; + + let backward_events = backward_stream + .take(expected_backwards_events.len()) + .collect::>>() + .await; + + for (r, e) in backward_events.into_iter().zip(expected_backwards_events.iter()) { + assert_eq!(&r.unwrap().event_id().unwrap().as_str(), e); + } +} + +#[async_test] +async fn room_permalink() { + fn sync_response(index: u8, room_timeline_events: &[JsonValue]) -> JsonValue { + json!({ + "device_one_time_keys_count": {}, + "next_batch": format!("s526_47314_0_7_1_1_1_11444_{}", index + 1), + "device_lists": { + "changed": [], + "left": [] + }, + "account_data": { + "events": [] + }, + "rooms": { + "invite": {}, + "join": { + "!test_room:127.0.0.1": { + "summary": {}, + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [] + }, + "state": { + "events": [] + }, + "timeline": { + "events": room_timeline_events, + "limited": false, + "prev_batch": format!("s526_47314_0_7_1_1_1_11444_{}", index - 1), + }, + "unread_notifications": { + "highlight_count": 0, + "notification_count": 0, + } + } + }, + "leave": {} + }, + "to_device": { + "events": [] + }, + "presence": { + "events": [] + } + }) + } + + fn room_member_events(nb: usize, server: &str) -> Vec { + let mut events = Vec::with_capacity(nb); + for i in 0..nb { + let id = format!("${server}{i}"); + let user = format!("@user{i}:{server}"); + events.push(json!({ + "content": { + "membership": "join", + }, + "event_id": id, + "origin_server_ts": 151800140, + "sender": user, + "state_key": user, + "type": "m.room.member", + })) + } + events + } + + let (client, server) = logged_in_client().await; + + // Without elligible server + let mut sync_index = 1; + let res = sync_response( + sync_index, + &[ + json!({ + "content": { + "creator": "@creator:127.0.0.1", + "room_version": "6", + }, + "event_id": "$151957878228ekrDs", + "origin_server_ts": 15195787, + "sender": "@creator:localhost", + "state_key": "", + "type": "m.room.create", + }), + json!({ + "content": { + "membership": "join", + }, + "event_id": "$151800140517rfvjc", + "origin_server_ts": 151800140, + "sender": "@creator:127.0.0.1", + "state_key": "@creator:127.0.0.1", + "type": "m.room.member", + }), + ], + ); + mock_sync(&server, res, None).await; + client.sync_once(SyncSettings::new()).await.unwrap(); + let room = client.get_room(room_id!("!test_room:127.0.0.1")).unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1" + ); + assert_eq!( + room.matrix_permalink(true).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?action=join" + ); + + // With a single elligible server + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "membership": "join", + }, + "event_id": "$151800140517rfvjc", + "origin_server_ts": 151800140, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + })], + ); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=localhost" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=localhost" + ); + + // With two elligible servers + sync_index += 1; + let res = sync_response(sync_index, &room_member_events(15, "notarealhs")); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=localhost" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=localhost" + ); + + // With three elligible servers + sync_index += 1; + let res = sync_response(sync_index, &room_member_events(5, "mymatrix")); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=mymatrix&via=localhost" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=mymatrix&via=localhost" + ); + + // With four elligible servers + sync_index += 1; + let res = sync_response(sync_index, &room_member_events(10, "yourmatrix")); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=yourmatrix&via=mymatrix" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=notarealhs&via=yourmatrix&via=mymatrix" + ); + + // With power levels + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "users": { + "@example:localhost": 50, + }, + }, + "event_id": "$15139375512JaHAW", + "origin_server_ts": 151393755, + "sender": "@creator:127.0.0.1", + "state_key": "", + "type": "m.room.power_levels", + })], + ); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=localhost&via=notarealhs&via=yourmatrix" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=localhost&via=notarealhs&via=yourmatrix" + ); + + // With higher power levels + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "users": { + "@example:localhost": 50, + "@user0:mymatrix": 70, + }, + }, + "event_id": "$15139375512JaHAZ", + "origin_server_ts": 151393755, + "sender": "@creator:127.0.0.1", + "state_key": "", + "type": "m.room.power_levels", + })], + ); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=mymatrix&via=notarealhs&via=yourmatrix" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=mymatrix&via=notarealhs&via=yourmatrix" + ); + + // With server ACLs + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "allow": ["*"], + "allow_ip_literals": true, + "deny": ["notarealhs"], + }, + "event_id": "$143273582443PhrSn", + "origin_server_ts": 1432735824, + "sender": "@creator:127.0.0.1", + "state_key": "", + "type": "m.room.server_acl", + })], + ); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=mymatrix&via=yourmatrix&via=localhost" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1?via=mymatrix&via=yourmatrix&via=localhost" + ); + + // With an alternative alias + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "alt_aliases": ["#alias:localhost"], + }, + "event_id": "$15139375513VdeRF", + "origin_server_ts": 151393755, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + })], + ); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%23alias%3Alocalhost" + ); + assert_eq!(room.matrix_permalink(false).await.unwrap().to_string(), "matrix:r/alias:localhost"); + + // With a canonical alias + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "alias": "#canonical:localhost", + "alt_aliases": ["#alias:localhost"], + }, + "event_id": "$15139375513VdeRF", + "origin_server_ts": 151393755, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + })], + ); + let sync_token = client.sync_token().await.unwrap(); + mock_sync(&server, res, Some(sync_token.clone())).await; + client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap(); + + assert_eq!( + room.matrix_to_permalink().await.unwrap().to_string(), + "https://matrix.to/#/%23canonical%3Alocalhost" + ); + assert_eq!( + room.matrix_permalink(false).await.unwrap().to_string(), + "matrix:r/canonical:localhost" + ); + assert_eq!( + room.matrix_permalink(true).await.unwrap().to_string(), + "matrix:r/canonical:localhost?action=join" + ); + + let event_id = event_id!("$15139375512JaHAW"); + assert_eq!( + room.matrix_to_event_permalink(event_id).await.unwrap().to_string(), + "https://matrix.to/#/%21test_room%3A127.0.0.1/%2415139375512JaHAW?via=mymatrix&via=yourmatrix&via=localhost" + ); + assert_eq!( + room.matrix_event_permalink(event_id).await.unwrap().to_string(), + "matrix:roomid/test_room:127.0.0.1/e/15139375512JaHAW?via=mymatrix&via=yourmatrix&via=localhost" + ); +} diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs new file mode 100644 index 000000000..06b46aaf8 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -0,0 +1,514 @@ +use std::{io::Cursor, time::Duration}; + +use matrix_sdk::{ + attachment::{ + AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, + Thumbnail, + }, + config::SyncSettings, +}; +use matrix_sdk_test::{async_test, test_json}; +use ruma::{ + api::client::membership::Invite3pidInit, assign, event_id, + events::room::message::RoomMessageEventContent, mxc_uri, room_id, thirdparty, uint, user_id, + TransactionId, +}; +use serde_json::json; +use wiremock::{ + matchers::{body_partial_json, header, method, path, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client, mock_sync}; + +#[async_test] +async fn invite_user_by_id() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/invite$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let user = user_id!("@example:localhost"); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.invite_user_by_id(user).await.unwrap(); +} + +#[async_test] +async fn invite_user_by_3pid() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/invite$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.invite_user_by_3pid( + Invite3pidInit { + id_server: "example.org", + id_access_token: "IdToken", + medium: thirdparty::Medium::Email, + address: "address", + } + .into(), + ) + .await + .unwrap(); +} + +#[async_test] +async fn leave_room() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/leave$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.leave().await.unwrap(); +} + +#[async_test] +async fn ban_user() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/ban$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let user = user_id!("@example:localhost"); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.ban_user(user, None).await.unwrap(); +} + +#[async_test] +async fn kick_user() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/kick$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let user = user_id!("@example:localhost"); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.kick_user(user, None).await.unwrap(); +} + +#[async_test] +async fn read_receipt() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/receipt")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let event_id = event_id!("$xxxxxx:example.org"); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.read_receipt(event_id).await.unwrap(); +} + +#[async_test] +async fn read_marker() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/read_markers$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let event_id = event_id!("$xxxxxx:example.org"); + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.read_marker(event_id, None).await.unwrap(); +} + +#[async_test] +async fn typing_notice() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/typing")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.typing_notice(true).await.unwrap(); +} + +#[async_test] +async fn room_state_event_send() { + use ruma::events::room::member::{MembershipState, RoomMemberEventContent}; + + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + + let room = client.get_joined_room(room_id).unwrap(); + + let avatar_url = mxc_uri!("mxc://example.org/avA7ar"); + let member_event = assign!(RoomMemberEventContent::new(MembershipState::Join), { + avatar_url: Some(avatar_url.to_owned()) + }); + let response = room.send_state_event(member_event, "").await.unwrap(); + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); +} + +#[async_test] +async fn room_message_send() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let content = RoomMessageEventContent::text_plain("Hello world"); + let txn_id = TransactionId::new(); + let response = room.send(content, Some(&txn_id)).await.unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn room_attachment_send() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, &mut media, AttachmentConfig::new()) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn room_attachment_send_info() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + })); + + let response = + room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn room_attachment_send_wrong_info() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let config = AttachmentConfig::new().info(AttachmentInfo::Video(BaseVideoInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + duration: Some(Duration::from_millis(3600)), + size: None, + blurhash: None, + })); + + let response = room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await; + + assert!(response.is_err()) +} + +#[async_test] +async fn room_attachment_send_info_thumbnail() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + "thumbnail_info": { + "h": 360, + "w": 480, + "mimetype":"image/jpeg", + "size": 3600, + }, + "thumbnail_url": "mxc://example.com/AQwafuaFswefuhsfAFAgsw", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .expect(2) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let mut thumbnail_reader = Cursor::new("Thumbnail"); + + let config = AttachmentConfig::with_thumbnail(Thumbnail { + reader: &mut thumbnail_reader, + content_type: &mime::IMAGE_JPEG, + info: Some(BaseThumbnailInfo { + height: Some(uint!(360)), + width: Some(uint!(480)), + size: Some(uint!(3600)), + }), + }) + .info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + })); + + let response = + room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn room_redact() { + let (client, server) = logged_in_client().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let event_id = event_id!("$xxxxxxxx:example.com"); + + let txn_id = TransactionId::new(); + let reason = Some("Indecent material"); + let response = room.redact(event_id, reason, Some(txn_id)).await.unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs new file mode 100644 index 000000000..2614db150 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -0,0 +1,33 @@ +use std::time::Duration; + +use matrix_sdk::config::SyncSettings; +use matrix_sdk_test::{async_test, test_json}; +use ruma::room_id; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client, mock_sync}; + +#[async_test] +async fn forget_room() { + let (client, server) = logged_in_client().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/forget$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::LEAVE_SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_left_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + room.forget().await.unwrap(); +} diff --git a/crates/matrix-sdk/tests/integration/room/mod.rs b/crates/matrix-sdk/tests/integration/room/mod.rs new file mode 100644 index 000000000..b9e0e2c78 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/mod.rs @@ -0,0 +1,3 @@ +mod common; +mod joined; +mod left; diff --git a/labs/sled-state-inspector/Cargo.toml b/labs/sled-state-inspector/Cargo.toml index 6c743e37a..d4996bb38 100644 --- a/labs/sled-state-inspector/Cargo.toml +++ b/labs/sled-state-inspector/Cargo.toml @@ -10,7 +10,7 @@ clap = "3.2.4" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matrix-sdk-base = { path = "../../crates/matrix-sdk-base", version = "0.5.0" } matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", version = "0.1.0" } -ruma = "0.6.1" +ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f" } rustyline = "9.1.2" rustyline-derive = "0.6.0" serde = "1.0.136" diff --git a/labs/sled-state-inspector/src/main.rs b/labs/sled-state-inspector/src/main.rs index 766afb2ee..5c088d7c7 100644 --- a/labs/sled-state-inspector/src/main.rs +++ b/labs/sled-state-inspector/src/main.rs @@ -200,7 +200,7 @@ impl Printer { impl Inspector { fn new(database_path: &str, json: bool, color: bool) -> Self { let printer = Printer::new(json, color); - let store = Store::new(Box::new( + let store = Store::new(Arc::new( StateStore::open_with_path(database_path).expect("Can't open sled database"), )); diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index d613d2118..0b34f0858 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -22,8 +22,6 @@ enum CiCommand { Clippy, /// Check documentation Docs, - /// Run default tests - Test, /// Run tests with a specific feature set TestFeatures { #[clap(subcommand)] @@ -85,7 +83,6 @@ impl CiArgs { CiCommand::Typos => check_typos(), CiCommand::Clippy => check_clippy(), CiCommand::Docs => check_docs(), - CiCommand::Test => run_tests(), CiCommand::TestFeatures { cmd } => run_feature_tests(cmd), CiCommand::TestAppservice => run_appservice_tests(), CiCommand::Wasm { cmd } => run_wasm_checks(cmd), @@ -97,7 +94,6 @@ impl CiArgs { check_clippy()?; check_typos()?; check_docs()?; - run_tests()?; run_feature_tests(None)?; run_appservice_tests()?; run_wasm_checks(None)?; @@ -142,12 +138,6 @@ fn check_docs() -> Result<()> { build_docs([], DenyWarnings::Yes) } -fn run_tests() -> Result<()> { - cmd!("rustup run stable cargo test").run()?; - cmd!("rustup run beta cargo test").run()?; - Ok(()) -} - fn run_feature_tests(cmd: Option) -> Result<()> { let args = BTreeMap::from([ (FeatureSet::NoEncryption, "--no-default-features --features sled,native-tls"), @@ -165,7 +155,12 @@ fn run_feature_tests(cmd: Option) -> Result<()> { ]); let run = |arg_set: &str| { - cmd!("rustup run stable cargo test -p matrix-sdk").args(arg_set.split_whitespace()).run() + cmd!("rustup run stable cargo nextest run -p matrix-sdk") + .args(arg_set.split_whitespace()) + .run()?; + cmd!("rustup run stable cargo test --doc -p matrix-sdk") + .args(arg_set.split_whitespace()) + .run() }; match cmd { @@ -187,15 +182,17 @@ fn run_crypto_tests() -> Result<()> { "rustup run stable cargo clippy -p matrix-sdk-crypto --features=backups_v1 -- -D warnings" ) .run()?; - cmd!("rustup run stable cargo test -p matrix-sdk-crypto --features=backups_v1").run()?; - cmd!("rustup run stable cargo test -p matrix-sdk-crypto-ffi").run()?; + cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --features=backups_v1").run()?; + cmd!("rustup run stable cargo test --doc -p matrix-sdk-crypto --features=backups_v1").run()?; + cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto-ffi").run()?; Ok(()) } fn run_appservice_tests() -> Result<()> { cmd!("rustup run stable cargo clippy -p matrix-sdk-appservice -- -D warnings").run()?; - cmd!("rustup run stable cargo test -p matrix-sdk-appservice").run()?; + cmd!("rustup run stable cargo nextest run -p matrix-sdk-appservice").run()?; + cmd!("rustup run stable cargo test --doc -p matrix-sdk-appservice").run()?; Ok(()) }