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