Compare commits

..

160 Commits

Author SHA1 Message Date
Benjamin Kampmann 402d061d42 ci(crypto-nodejs): fixing path to npm package for publishing 2022-07-12 18:02:59 +02:00
Benjamin Kampmann 2a1bc372fc chore(crypto-nodejs): setting version 2022-07-12 17:35:46 +02:00
Benjamin Kampmann bcab2a6d8c ci(crypto-nodejs): setting base branch 2022-07-12 17:24:34 +02:00
Benjamin Kampmann 703b3a3561 ci(crypto-nodejs): Fixing typo in workflow 2022-07-12 17:18:54 +02:00
Benjamin Kampmann 36f62ce67f ci(crypto-nodejs): Use regular npm version to set version 2022-07-12 17:14:15 +02:00
Benjamin Kampmann 9bc605a76d ci(crypto-nodejs): FIxing name and directory for action 2022-07-12 17:11:20 +02:00
Benjamin Kampmann 13a6825af7 ci(crypto-nodejs): Fix workflow syntax error 2022-07-12 17:07:49 +02:00
Benjamin Kampmann c5796991e8 chore(crypto-nodejs): Adding changelog 2022-07-12 17:05:40 +02:00
Benjamin Kampmann 9a45325683 ci(crypto-nodejs): use org-wide secrets (#835) 2022-07-12 17:00:44 +02:00
Ivan Enderlin 0bde5ccf38 feat(bindings/crypto-nodejs): Add #[napi(strict)] to force type checking from JavaScript (#829)
* feat(bindings/crypto-nodejs): Add `#[napi(strict)]` to force type checking from JavaScript.

* chore(bindings/crypto-nodejs): Use our own fork of `napi-rs` for the moment.
2022-07-12 16:24:24 +02:00
Benjamin Kampmann 94b635c074 build(crypto-nodejs): Crypto Node.js release infrastructure (#763)
* feat(crypto-nodejs): Download lib binary in postinstall

* build(crypto-nodejs): Workflow to prebuild napi bindings

* ci(crypto-nodejs): Disable broken target, install without download

* ci(apple-ffi): Don't run for drafts

* ci(coverage): Don't run for draft PRs

* fix(crypto-nodejs): bind to current version for download

* fix(crypto-nodejs): Ignore libs and package

* ci(crypto-nodejs): Build and upload NPM package

* fix(crypto-nodejs): Set proper target list

* ci(crypto-nodejs): Remove FreeBSD from build pipeline

* ci(crypto-nodejs): Linkers for linux cross compile

* ci(crypto-nodejs): Add arm64 build for windows

* ci(crypto-nodejs): Proper linkers for arm and musl

* ci(crypto-nodejs): Correct apt command for musl

* fix(crypto-nodejs): Drop arm64 linux musl support

* ci(crypto-nodejs): Manual Workflow trigger process

* chore(crypto-nodejs): Get Github to pickup our action

* ci(crypto-nodejs): Add i686 Linux built

* ci(crypto-nodejs): Configure cliff for nodejs changelogs

* ci(crypto-nodejs): Proper gcc for i868 targets

* docs(crypto-nodejs): Add supported targets for npm install

* ci(crypto-nodejs): Limit building of binaries to tags

* style: consol.log -> console.info; Improve docs

Co-authored-by: Ivan Enderlin <ivan@mnt.io>

* activate for testing

* fix broken merge

* 0.1.0

* fix(js): put in the proper package name

* activate for PR for testing

* fix(nodejs): getting ready for publishing

* ci(crypto-nodejs): Adding docs and fixing naming for workflows

* typo: missed one

* fixing package name

Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2022-07-12 16:05:57 +02:00
Benjamin Kampmann 15be2dc45e Merge pull request #832 from johannescpk/sdk/identity-assertion-session
fix(sdk): Can't assert identity without session
2022-07-12 12:25:46 +02:00
Johannes Becker 420ca26bf5 fix(sdk): Can't assert identity without session 2022-07-12 10:44:56 +02:00
Kévin Commaille 2d0653894c refactor(test): Rename LOGOUT to EMPTY
This name is more accurate for its uses.
2022-07-09 17:59:45 +02:00
Damir Jelić 47cfac7f4c test: Optimize sha2 even in debug builds
This makes the tests finish on my machine twice as fast. This works
mainly because some tests utilize pbkdf2 to derive a key from a
passphrase.
2022-07-08 18:47:36 +02:00
Kévin Commaille 9539cbcfb9 test(appservice): Replace mockito with wiremock 2022-07-08 16:33:29 +02:00
Kévin Commaille 9778518347 test(sdk): Replace mockito with wiremock 2022-07-08 16:33:29 +02:00
Ivan Enderlin dc2276cd8a feat(bindings/crypto-nodejs): Implement an Attachment API.
feat(bindings/crypto-nodejs): Implement an `Attachment` API.
2022-07-08 16:26:19 +02:00
Damir Jelić f1c880ff5f feat(bindings/ffi): Add an authentication service
This adds a basic authentication service to the bindings that abstracts away the Client until a login has been completed successfully.
2022-07-08 12:24:15 +02:00
Damir Jelić 93e5728d65 test(sdk): Move the integration tests
This moves the bulk of the Client tests into integration tests.
2022-07-08 12:08:27 +02:00
Damir Jelić a7af96d081 feat(crypto): Customized event types
This patch adds customized event types, currently only for the
m.room_key and m.secret.send to-device events.

This allows us to:
    a) Deserialize the session_key field into a vodozemac type
    b) Control when we zeroize secrets better
2022-07-07 19:20:42 +02:00
Doug 0dee880cd0 Address PR comments. 2022-07-07 17:15:12 +01:00
Ivan Enderlin f0190b4601 feat(bindings/crypto-nodejs): Transform timeout into milliseconds
feat(bindings/crypto-nodejs): Transform `timeout` into milliseconds
2022-07-07 14:54:18 +02:00
Ivan Enderlin 6d83f01e73 fix(sdk): THe MediaEncryptionInfo.web_key has been renamed. 2022-07-07 13:59:36 +02:00
Ivan Enderlin 2eb5fc77f5 feat(bindings/crypto-nodejs): Remove Clone impl for MediaEncryptionInfo.
We don't want to clone a struct that contains a secret.

However, on the Node.js side, we can only receive arguments by
references. The problem we have is that we cannot transfer the
ownership of `MediaEncryptionInfo` to `AttachmentDecryptor` because we
don't own it. To simulate this behavior, we use `Option.take`.

A new method then appears:
`EncryptedAttachment.hasMediaEncryptionInfoBeenConsumed` to know if
the media encryption info has been consumed by `Attachment.decrypt`
already or not. That way, we can decrypt only once. It is possible to
do a JSON-encoded backup of the media encryption info by calling
`EncryptedAttachment.mediaEncryptionInfo` though.
2022-07-07 13:53:46 +02:00
Kévin Commaille 5ab8bd0885 Fix missing import 2022-07-07 13:24:54 +02:00
Kévin Commaille ee69863912 Move event permalink test 2022-07-07 13:19:31 +02:00
Kévin Commaille e87d599f84 Merge remote-tracking branch 'upstream/main' into integration-tests 2022-07-07 13:16:09 +02:00
Ivan Enderlin 0b011d9097 doc(bindings/crypto-nodejs): Add link to the specification. 2022-07-07 13:15:14 +02:00
Ivan Enderlin 0f5851cc01 chore(crypto): Rename MediaEncryptionInfo.web_key to .key. 2022-07-07 13:14:05 +02:00
Kévin Commaille d6a2f15c68 Simplify use of via
Due to a ruma upgrade
2022-07-07 12:30:15 +02:00
Kévin Commaille 36a47c28ed Add note that the event should be part of the room 2022-07-07 12:30:15 +02:00
Kévin Commaille 900016b249 feat(sdk): Get a permalink for an event 2022-07-07 12:30:15 +02:00
Kévin Commaille de60a24602 Remove __test feature 2022-07-07 11:26:49 +02:00
Ivan Enderlin 29c10b8424 feat(bindings/crypto-nodejs): Convert timeout from u128 to u64.
First, u128 has a bug in `serde`,
cf. https://github.com/serde-rs/json/issues/625.

Second, we don't need to represent the timeout as a u128, it's clearly
too large. This patch tries to convert it to u64. It should never
fail, but we propagate the error anyway.
2022-07-07 11:12:12 +02:00
Johannes Becker 4b856ce9d6 fix(sdk): Use the local config variable to decide identity assertion 2022-07-07 10:16:23 +02:00
Ivan Enderlin c043daede0 test(crypto): Fix a test. 2022-07-07 10:15:24 +02:00
Ivan Enderlin 9f6988f766 Merge branch 'main' into fix-issue-796 2022-07-07 10:11:43 +02:00
Ivan Enderlin ed0709373d fix(crypto): Rename web_key to key for MediaEncryptionInfo.
Based on the [Section 11.11.1.6.1 Extensions to `m.room.message`
msgtypes](https://spec.matrix.org/v1.2/client-server-api/#extensions-to-mroommessage-msgtypes),
the parameter for the JSON Web Key is named `key`, not `web_key`. This
patch fixes that by renaming the field when serializing and
deserializing.
2022-07-07 10:04:34 +02:00
Ivan Enderlin acf9b15571 feat(bindings/crypto-nodejs): Use latest napi-rs version to avoid cloning Uint8Array.
The new `napi-rs` release includes a patch that avoids cloning and
copying data inside a `Uint8Array`
(https://github.com/napi-rs/napi-rs/pull/1224), it now returns a
“Node.js reference” of it.

This new `napi-rs` release also includes one of our patch,
https://github.com/napi-rs/napi-rs/pull/1200, which means we no longer
need to depend on our fork.
2022-07-07 09:49:34 +02:00
Charles Wright ba39185679 Fix build errors 2022-07-06 18:08:02 +02:00
Doug da277c4978 Create a new client on login.
More clippy errors.
2022-07-06 12:43:02 +01:00
Doug 9925d73e7b Fix typos and clippy errors. 2022-07-06 11:52:33 +01:00
Doug fec879f0f3 Simplify AuthenticationError for now. 2022-07-06 10:29:13 +01:00
Doug 91427b82a5 Use an Optional client instead of failable init. 2022-07-05 18:14:25 +01:00
Ivan Enderlin d7739369ae chore(bindings/crypto-nodejs): Remove useless napi::Result. 2022-07-05 17:45:09 +02:00
Ivan Enderlin 4fd24eebea feat(bindings/crypto-nodejs): Implement an Attachment API.
This patch provides a new API to encrypt and decrypt attachment,
i.e. big buffer of type `Uint8Array`.

It's based on `matrix_sdk_crypto::AttachmentEncryptor` and `AttachmentDecryptor`.
2022-07-05 17:31:52 +02:00
Benjamin Kampmann 73daec3757 Merge pull request #810 from gnunicorn/expose-invite-details
feat(sdk): Expose details of invite for invited room
2022-07-05 14:56:05 +02:00
Benjamin Kampmann d9f3b257b4 Apply suggestions from code review
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2022-07-05 14:38:53 +02:00
Damir Jelić 771c33d710 chore(crypto): Bump vodozemac
Vodozemac used to accept and return strings when encrypting and
decrypting. This is quite unusual for a pure cryptographic library so we
switched towards the usual setup where we encrypt/decrypt raw bytes.

Since we do encrypt/decrypt JSON strings in Matrix land, we do the
string conversions over here.
2022-07-05 13:23:50 +02:00
Doug 56adf6a89b Add a client_container with locks. 2022-07-05 11:43:10 +01:00
Ivan Enderlin e5a7a975a3 feat(bindings/crypto-nodejs): Transform timeout into milliseconds. 2022-07-05 12:05:53 +02:00
Ivan Enderlin f3e69a2352 fix(bindings/cryto-nodejs): Fix memory corruption in async functions
fix(bindings/cryto-nodejs): Fix memory corruption in async functions
2022-07-05 10:25:16 +02:00
Ivan Enderlin 607d7ebc22 fix(bindings/cryto-nodejs): Fix memory corruption in async functions.
In async functions, the Node.js GC may or may not (that's a random
behavior) collect the arguments passed to the function as soon as it
returns. The function may not be executed yet, since it's async. Thus,
it leads to memory corruption: The function tries to read later on the
value inside an argument and… it crashes at best.

To avoid this bug, there is no other choice than cloning the values
before the function returns, in its “sync path” (so before any
transformation of an `.await` point into an “async block”).

The performance impact is not “massive”, I'm not sure it could be
noticeable easily since it is most of the time related to identifiers
(e.g. `UserId`), which are cheap to clone. I have to find the balance
here, and cloning offers the best trade off from my point of view.
2022-07-05 09:07:20 +02:00
Doug 0178b71437 Add basic AuthenticationService to the FFI. 2022-07-04 16:55:50 +01:00
Kévin Commaille dd6a902240 test(sdk): Move integration tests 2022-07-04 16:22:20 +02:00
Kévin Commaille 4eb1337dc8 ci: Remove whitespaces in config file 2022-07-04 16:22:19 +02:00
Benjamin Kampmann 81f02f0d0b Merge pull request #804 from gnunicorn/ben-hunting-the-nodjs-segfault
Hunting the nodejs segfault bug, long-term
2022-07-04 15:25:09 +02:00
Ivan Enderlin 76fe6d54ac feat(bindings/crypto-*): Add fallback_keys field to KeysUploadRequest
feat(bindings/crypto-*): Add `fallback_keys` field to `KeysUploadRequest`
2022-07-04 15:02:38 +02:00
Ivan Enderlin eb358889e9 Merge branch 'main' into fix-issue-800 2022-07-04 14:31:28 +02:00
Ivan Enderlin c82631c414 feat(bindings/crypto-js): Implement OlmMachine.sign
feat(bindings/crypto-js): Implement `OlmMachine.sign`
2022-07-04 14:16:27 +02:00
Ivan Enderlin fb4a940a26 chore: Make Clippy happy… 2022-07-04 13:50:28 +02:00
Ivan Enderlin 05561a8777 chore(crypto): Make Clippy happy. 2022-07-04 13:06:14 +02:00
Ivan Enderlin 909ada43d7 chore(base): Make Clippy happy.
So, Clippy suggests to change `(&member).into()` to `member.into()`
but it's the same, and `From<T>` is not implemented for this `T`, only
`From<&T>` is present. Thus, to deceive Clippy, I'm using
`std::borrow::Borrow` here. Not super happy with that though…
2022-07-04 12:00:42 +02:00
Ivan Enderlin 6176b3b658 feat(bindings/crypto-ffi): Add fallback_keys field to KeysUpload. 2022-07-04 11:49:45 +02:00
Ivan Enderlin 566227576e feat(bindings/crypto-js): Add fallback_keys field to KeysUploadRequest. 2022-07-04 11:47:27 +02:00
Ivan Enderlin d6c0ef1497 feat(bindings/crypto-nodejs): Add fallback_keys field to KeysUploadRequest. 2022-07-04 11:47:15 +02:00
Ivan Enderlin f72a14890d chore(crypto) Make Clippy happy. 2022-07-04 11:42:50 +02:00
Ivan Enderlin 62378b4abc Merge branch 'main' into fix-issue-797 2022-07-04 11:24:33 +02:00
Ivan Enderlin f96069f591 chore(store-encryption): Call DerefMut manually.
Clippy on nigtly is raising a warning, which is turned into an error
on the CI. It's the [`explicit_auto_deref`
lint](https://rust-lang.github.io/rust-clippy/master/index.html#explicit_auto_deref). I
suspect it's a false-positive but I'm not sure. Anyway, to workaround
this and unblock our CI, let's call `DerefMut::deref_mut` manually:
it's clearer anyway.
2022-07-04 11:00:23 +02:00
Ivan Enderlin 59615d4ae3 chore(bindings/crypto-nodejs): Clean up based on feedback. 2022-07-04 10:17:11 +02:00
Benjamin Kampmann fd38c757e4 feat(sdk): Expose details of invite for invited room 2022-07-01 19:44:27 +02:00
Jonas Platte 861d899541 refactor(base): Remove an unnecessary allocation 2022-07-01 16:16:29 +02:00
Jonas Platte fd08c9e7da refactor(base): Remove check for own user in notification handling
This is now done in Ruma.
Reverts commit bc78095611.
2022-07-01 16:16:29 +02:00
Jonas Platte cffb565a5f chore: Allow some usage of deprecated fields
… to allow CI to succeed. They should be removed soon.
2022-07-01 16:16:29 +02:00
Jonas Platte f20d1c3d76 chore: Upgrade ruma 2022-07-01 16:16:29 +02:00
Jonas Platte e4f6c0cc58 chore(sdk): Remove feature ruma/appservice-api-helper
No longer used as of https://github.com/matrix-org/matrix-rust-sdk/pull/710
2022-07-01 16:16:29 +02:00
Jonas Platte d3ae99eb22 chore: Silence new clippy lint 2022-07-01 12:39:46 +02:00
Jonas Platte bc47caa356 chore: Remove unnecessary map_err's 2022-07-01 11:55:28 +02:00
Ivan Enderlin afa96f1bf4 test(bindings/crypto-nodejs): Add more signing test cases. 2022-06-30 16:58:39 +02:00
Ivan Enderlin c99f42347c chore(bindings/crypto-nodejs): Simplify code by removing matches!. 2022-06-30 16:52:08 +02:00
Ivan Enderlin 51cb35502d doc(bindings/crypto-nodejs): Add missing documentation. 2022-06-30 16:50:33 +02:00
Ivan Enderlin b59077e83d chore(bindings/crypto-nodejs): Replacing into_iter by iter on &BTreeMap.
Calling `into_iter` on `&BTreeMap` will not consume it. It has the
same effect as calling `iter`. So let's do it.
2022-06-30 16:48:44 +02:00
Ivan Enderlin 3f197734d9 feat(bindings/crypto-nodejs) Implement OlmMachine.sign.
This patch first implements the new `Signatures`, `Signature` and `MaybeSignature` types.

Then, it moves some Vodozemac types into their own module, and
implements the new `Ed25519Signature` type.

Finally, it implements `OlmMachine.sign`.
2022-06-30 16:44:07 +02:00
Benjamin Kampmann 1961403512 ci(crypto-nodejs): Only build non-release version on failure, improve CI build time 2022-06-30 12:53:51 +02:00
Benjamin Kampmann f1ebbfd245 ci(crypto-nodejs): Create non-release build of version and upload everything as artifacts upon failure 2022-06-30 12:24:21 +02:00
Ivan Enderlin 0458ed9be1 feat(bindings/crypto-js): Implement DeviceKeyId, DeviceKeyAlgorithm and DeviceKeyAlgorithmName. 2022-06-30 08:51:36 +02:00
Ivan Enderlin 12c7b76fea feat(bindings/crypto-js): Implement `OlmMachine.crossSigningStatus. 2022-06-30 08:33:28 +02:00
Marcel a8601e186a fix(appservice): Don't process the same transaction twice 2022-06-29 16:17:25 +00:00
Stefan Ceriu 8a2d13feea feat(bindings): Session verification through FFI 2022-06-29 13:59:52 +02:00
Benjamin Kampmann e2ca56114e Merge pull request #785 from zecakeh/room-permalink
feat(sdk): Add method to get a room permalink
2022-06-29 13:58:25 +02:00
Anderas 3c6d159a04 refactor: Use ClientBuilder pattern in SDK FFI
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2022-06-29 13:13:31 +02:00
Benjamin Kampmann 464bc43290 Merge pull request #793 from Hywan/test-crypto-nodejs-timeout
test(crypto-nodejs): Increase timeout
2022-06-29 12:58:11 +02:00
Kévin Commaille 297861e186 Fix docs styling 2022-06-29 12:20:57 +02:00
Kévin Commaille 8313029e33 Split into methods for both Matrix URI formats 2022-06-29 12:02:26 +02:00
Ivan Enderlin 913bdd683e feat(crypto-js): Change the package name
feat(crypto-js): Change the package name
2022-06-28 19:28:58 +02:00
Ivan Enderlin 041b9bc405 feat(crypto-js): Change the package name. 2022-06-28 17:05:21 +02:00
Ivan Enderlin 1526f76686 test(crypto-nodejs): Increase timeout.
For some unknown reasons, sometimes, randomly, one test (initializing
an `OlmMachine` with a local store with a passphrase) can take more
than 5s, only on Github Actions. Let's increase the test timeout value
so that the entire test suite doesn't fail.
2022-06-28 16:47:46 +02:00
Kévin Commaille f0e0194ff2 feat(sdk): Add method to get a room permalink
Include routing for room IDs
2022-06-26 13:38:01 +02:00
Kévin Commaille ebc7177438 feat(base): Add method to get Room alt aliases 2022-06-26 12:01:51 +02:00
Ivan Enderlin 091fab8a2a chore(bindings): Move matrix-sdk-ffi and matrix-sdk-crypto-ffi into the bindings/ directory
chore(bindings): Move `matrix-sdk-ffi` and `matrix-sdk-crypto-ffi` into the `bindings/` directory
2022-06-23 15:54:32 +02:00
Ivan Enderlin 818d715395 chore: Implement feedback. 2022-06-23 15:53:19 +02:00
Ivan Enderlin 5a0089da52 doc(bindings): Mention bindings in the top README.md file. 2022-06-23 15:12:51 +02:00
Ivan Enderlin 68b6c19dd4 test: Ensure all crates members of the workspace are compiled & tested. 2022-06-23 14:10:31 +02:00
Ivan Enderlin c29e2b9563 !fixup 2022-06-23 11:35:42 +02:00
Ivan Enderlin ecc28efd53 chore(bindings): Move matrix-sdk-ffi and matrix-sdk-crypto-ffi into the bindings/ directory. 2022-06-23 11:31:59 +02:00
Jonas Platte f3a61020e7 refactor(sdk): Rewrite sso login to be easier to read 2022-06-22 17:17:52 +02:00
Jonas Platte a423e92246 chore: Consistently capitalize 'device ID' 2022-06-22 17:17:52 +02:00
Jonas Platte b5d7f10c6b feature: Introduce a login builder API
This improves the readability of login calls.
The old login API is kept, but deprecated.
2022-06-22 17:17:52 +02:00
Ivan Enderlin 931eabf55c chore(bindings): Move crypto-nodejs and crypto-js into the bindings/ directory
chore(bindings): Move `crypto-nodejs` and `crypto-js` into the `bindings/` directory
2022-06-22 16:33:20 +02:00
Ivan Enderlin 8cd7fa9fb0 chore: Implement feedback. 2022-06-22 16:03:37 +02:00
Ivan Enderlin 1604f24136 chore(test): Fix YAML. 2022-06-22 16:03:37 +02:00
Ivan Enderlin 3da737b9e2 chore(test): Shorten job name for test-matrix-sdk-crypto-nodejs.
In the Github UI, we can only see:

    🐧 [m]-crypto-nodejs, Node.js…

What interests us is the Node.js version number.
2022-06-22 16:03:37 +02:00
Ivan Enderlin 2ffcc1a415 chore: Use [m] as an alias for matrix-sdk. 2022-06-22 16:03:37 +02:00
Ivan Enderlin 829dab42c5 chore(test): Rename the test-matrix-sdk-crypto-js job. 2022-06-22 16:03:37 +02:00
Ivan Enderlin 54acd314cc chore(test): Move the wasm workflow inside the ci workflow. 2022-06-22 16:03:37 +02:00
Ivan Enderlin 74953031ee chore(test): Use os-name in step name for test-appservice. 2022-06-22 16:03:37 +02:00
Ivan Enderlin 0436eb9349 chore(ci): Rephrase a little bit the Github Actions steps. 2022-06-22 16:03:37 +02:00
Ivan Enderlin a23bb8f5a0 chore(docs): Rephrase a little bit the Github Actions steps. 2022-06-22 11:57:41 +02:00
Ivan Enderlin 8db58986fb chore(bindings): Move crypto-nodejs and crypto-js into the bindings/ directory.
`matrix-sdk-crypto-nodejs` and `matrix-sdk-crypto-js` are no longer
default members of the Cargo virtual workspace. The Github Actions
workflows for the bindings now live in a `bindings_ci.yml` files
(ideally, it should be in a subdirectory,
`.github/workflows/bindings/ci.yml` but it doesn't work).
2022-06-22 11:54:49 +02:00
Ivan Enderlin 6ad323bc4e test: Run tests faster with nextest
test: Run tests faster with `nextest`
2022-06-22 09:56:02 +02:00
Ivan Enderlin b0d51fdfa5 test: There is no doctest for matrix-sdk-crypto-ffi. 2022-06-22 09:26:51 +02:00
Ivan Enderlin 3bfc68d476 test: Add missing cargo-nextest installation.
This patch also changes the step's name from Clippy to Test.
2022-06-22 09:26:51 +02:00
Ivan Enderlin eb33333925 test: Run doctests manually.
`cargo-nextest` doesn't support doctests for now, so we must run them
“manually” by running a separate `cargo test --doc` command.
2022-06-22 09:26:51 +02:00
Ivan Enderlin d9475c131a test(xtask): Remove xtask -- ci test as it is unused. 2022-06-22 09:26:48 +02:00
Ivan Enderlin 399862d955 test: Run tests faster with nextest.
> [`cargo-nextest`](https://nexte.st/index.html) is a next-generation
> test runner for Rust projects.

This patch installs and uses `nextest` to run our own tests.

Comparing `cargo test` and `cargo nextest` with hyperfine provides the
following results:

```sh
$ hyperfine 'cargo test --workspace' 'cargo nextest run --workspace && cargo test --doc'
Benchmark 1: cargo test --workspace
  Time (mean ± σ):     51.785 s ±  2.066 s    [User: 183.471 s, System: 10.563 s]
  Range (min … max):   49.151 s … 56.641 s    10 runs

Benchmark 2: cargo nextest run --workspace && cargo test --doc
  Time (mean ± σ):     44.556 s ±  0.894 s    [User: 192.213 s, System: 11.441 s]
  Range (min … max):   43.170 s … 45.762 s    10 runs
```

Benchmark 2 is 1.16 times faster than Benchmark 1.
2022-06-22 09:26:07 +02:00
Ivan Enderlin 2c1f5fed8d feat(crypto-js): Migrate tests and polish the API
feat(crypto-js): Migrate tests and polish the API
2022-06-21 12:04:45 +02:00
Ivan Enderlin 8b2237fa7a Merge branch 'main' into feat-crypto-js-next 2022-06-21 11:38:48 +02:00
Ivan Enderlin e5ea2a770b chore(crypto-js): Implement feedback from PR. 2022-06-21 11:25:58 +02:00
Jonas Platte 5f31e9d131 chore: Add missing json language specification to docs 2022-06-20 22:33:35 +02:00
Jonas Platte 6cb9c11b88 chore: Remove unnecessary pub visibility from OnceCell imports 2022-06-20 22:33:35 +02:00
Jonas Platte a00c130fc3 feature: Allow passing already-Arc'ed stores to StoreConfig methods 2022-06-20 18:00:33 +02:00
Jonas Platte 4971802e75 chore: Replace usage of Store with Arc<dyn StateStore> 2022-06-17 17:35:33 +02:00
Jonas Platte 8690addfd5 chore: Add Clone impl for ClientBuilder 2022-06-17 14:59:22 +02:00
Jonas Platte 00a20f325b chore: Add Clone impl for StoreConfig
… by storing the stores inside Arc's instead of Box'es.
2022-06-17 14:59:22 +02:00
Jonas Platte a4e4bfe833 refactor(sdk)!: Change store builder methods from Box<dyn Trait> to impl Trait
To migrate, don't box the store before passing it to `builder.state_store` or
`builder.crypto_store` (remove `Box::new`).
2022-06-17 14:59:22 +02:00
Jonas Platte 02aa537f2a chore: Keep uniffi version in sync across deps, CI 2022-06-17 13:37:07 +02:00
Anderas c755e19fb3 Merge pull request #765 from matrix-org/andy/crypto_ffi
Build Crypto iOS framework
2022-06-16 11:25:39 +01:00
Jonas Platte 8250c24525 chore: Undo pinning of clap 2022-06-15 20:47:55 +02:00
Andy Uhnak fe29fa57eb Build Crypto iOS framework 2022-06-15 13:52:21 +01:00
Ivan Enderlin c564b1a5e1 feat(crypto-nodejs): Add store_path and store_passphrase to the OlmMachine constructor
feat(crypto-nodejs): Add `store_path` and `store_passphrase` to the `OlmMachine` constructor
2022-06-15 07:28:29 +02:00
Benjamin Kampmann 9d691e238a Merge pull request #759 from Hywan/fix-codecov-labs
chore(test): Remove `labs` from the projects + exclude `matrix-sdk-indexeddb` from code coverage report
2022-06-14 20:29:07 +02:00
Ivan Enderlin 520e2f30f7 doc(crypto-js): Add missing module documentation. 2022-06-14 16:54:22 +02:00
Ivan Enderlin c56ab5928c test(crypto-js): Add a workflow to test matrix-sdk-crypto-js. 2022-06-14 16:34:08 +02:00
Ivan Enderlin f8cd2310be feat(crypto-js): Implement OlmMachine.decryptRoomEvent & siblings. 2022-06-14 16:16:54 +02:00
Ivan Enderlin 073fb45580 feat(crypto-nodejs): Define Node.js versions policy.
We now support only “current”, “active” or “maintenance” versions
according to https://nodejs.org/en/about/releases/, which are
compatible with NAPI v6.
2022-06-14 16:05:01 +02:00
Ivan Enderlin 3833d35348 chore(crypto-nodejs): Drop Node.js v12.17.
There is a segfault with `napi-rs` and Node.js in v12.17. It's an old
version, it may be fair to drop its support for now. Let's see if
people would need it in the future, we may work on `napi-rs` to fix
this bug in case it's really necessary.
2022-06-14 16:05:01 +02:00
Ivan Enderlin 83b730d1c8 chore(crypto-nodejs): Make the code compatible with Node.js < 18. 2022-06-14 16:05:01 +02:00
Ivan Enderlin 6477cc5072 feat(crypto-nodejs): Add store_path and store_passphrase to the OlmMachine constructor.
This patch adds the `store_path` and the `store_passphrase` arguments
to the `OlmMachine` constructor to use a `CryptoStore` instead of
having an in-memory Olm machine.
2022-06-14 16:05:01 +02:00
Ivan Enderlin 2117b36a75 chore(test): Exclude matrix-sdk-indexeddb from code coverage report. 2022-06-14 16:04:41 +02:00
Ivan Enderlin 5e8ed3bcbf chore(test): Remove labs from the projects.
It's already part of the `ignore` section.
2022-06-14 16:04:41 +02:00
Jonas Platte 87639a4c4c fix(appservice): Remove erroneous ? operator 2022-06-14 15:20:24 +02:00
Amanda Graven de04aba5b3 fix(appservice): Remove erroneous ? operator 2022-06-14 15:02:55 +02:00
Amanda Graven 5243091bec test(appservice): Virtual client membership
Test that virtual clients get assigned the correct membership to rooms
when processing received transactions.
2022-06-14 14:39:54 +02:00
Amanda Graven 1d746f1ef1 fix(appservice): Virtual client non-membership
Don't assume virtual client membership is join if none is stored, since
that leads to a client being told it's joined in rooms it has no
membership of. The main appservice client still assumes it's joined
every room it receives transaction events about.
2022-06-14 14:39:54 +02:00
Ivan Enderlin 56d74e25b8 test(crypto-js): Finish to migrate the test suites + code clean up. 2022-06-14 14:09:52 +02:00
Ivan Enderlin a758d98f84 feat(crypto-nodejs): Update license. 2022-06-14 12:12:27 +02:00
Ivan Enderlin 5adae6fd41 feat(crypto-js): Migrate tests and polish the API. 2022-06-14 12:12:27 +02:00
Damir Jelić 38d771cca6 ci(coverage): Set the correct out format for CI coverage reports 2022-06-14 12:01:03 +02:00
Jonas Platte dd4c329f57 chore: Prevent clap upgrades beyond 3.2 2022-06-14 11:02:19 +02:00
199 changed files with 7940 additions and 3363 deletions
+12 -4
View File
@@ -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:
+173
View File
@@ -0,0 +1,173 @@
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
if: github.event_name == 'push' || !github.event.pull_request.draft
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
View File
@@ -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: [12.17, 14.0, 15.0, 16.0]
include:
- node-version: 16.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 }}
+2 -1
View File
@@ -13,6 +13,7 @@ jobs:
code_coverage:
name: Code Coverage
runs-on: "ubuntu-latest"
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout repository
@@ -38,7 +39,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: --workspace
args: --out Xml
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
@@ -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:
-58
View File
@@ -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'
@@ -0,0 +1,109 @@
name: Prepare Crypto-Node.js Release
#
# This is a helper workflow to craft a new Node.js release, trigger this via
# the Github Workflow UI by dispatching it manually. Provide the version, the
# matrix-sdk-crypto-nodejs npm package should be set to, and a optionally the
# old version (as used in the git tag) this release should be compared to.
#
# This will then:
# 1. bump the npm version to the one you specified
# 2. commit that change together with the changelog (if it changed, see below)
# 3. create the appropriate tag on that commit
# 4. create the Github draft release, including the changes (if given, see below)
# 5. push these to a new branch, including tag, triggering the `release-crypto-nodejs` workflow
# 6. create a PR to merge these back into `main`
#
# Additionally, if you provide a tag to comapare this tag to, this will:
# 1. create a changelog between the two releases, used for the github release
# 2. update the Changelog.md and include it in the commit
#
# The remaining tasks are done by the release-crypto-nodejs workflow.
on:
workflow_dispatch:
inputs:
version:
description: 'New Node.js SemVer version to create'
required: true
type: string
previous_version:
description: 'Create the changelog by comparing to this old SemVer Version (as used in the tag) '
type: string
env:
PKG_PATH: "bindings/matrix-sdk-crypto-nodejs"
TAG_PREFIX: "matrix-sdk-crypto-nodejs-v"
jobs:
prepare-release:
name: "Preparing crypto-nodejs release tag"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
# Generate changelog since last tag, if given
- name: Generate a changelog for upload
if: inputs.previous_version
uses: orhun/git-cliff-action@v1
with:
config: "${{ env.PKG_PATH }}/cliff.toml"
args: --strip header "${{env.TAG_PREFIX}}${{ inputs.previous_version }}..HEAD"
env:
GIT_CLIFF_TAG: "Changes ${{ inputs.previous_version }} -> ${{ inputs.version }}"
GIT_CLIFF_OUTPUT: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md"
# Update changelog since last tag, if given
- name: Update existing Changelog
if: inputs.previous_version
uses: orhun/git-cliff-action@v1
with:
config: "${{ env.PKG_PATH }}/cliff.toml"
args: "${{ inputs.previous_version }}..HEAD"
env:
GIT_CLIFF_TAG: "${{ inputs.version }}"
GIT_CLIFF_PREPEND: "${{ env.PKG_PATH }}/CHANGELOG.md"
- name: Set version
id: package_version
working-directory: ${{ env.PKG_PATH }}
run: npm version ${{ inputs.version }}
- uses: EndBug/add-and-commit@v9
with:
default_author: github_actions
message: "Tagging Crypto-Node.js for release"
tag: "${{env.TAG_PREFIX}}${{ inputs.version }}"
new_branch: "gh-action/release-${{ env.TAG_PREFIX }}${{ inputs.version }}"
push: true
add: |
${{ env.PKG_PATH }}/package.json
${{ env.PKG_PATH }}/CHANGELOG.md
# if we have generated changes
- name: Update Github Release notes
if: inputs.previous_version
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }}
body_path: "${{ env.PKG_PATH }}/CHANGES-${{ inputs.version }}.md"
# no changes, use the default changelog for the body
- name: Update Github Release notes
if: ${{!inputs.previous_version}}
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ env.TAG_PREFIX }}${{ inputs.version }}
body_path: "${{ env.PKG_PATH }}/CHANGELOG.md"
# finally, let's create a PR for all this, too
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
branch: "gh-action/release-${{ env.TAG_PREFIX }}${{ inputs.version }}"
base: "main"
title: "Preparing Release ${{ env.TAG_PREFIX }}${{ inputs.version }}"
body: |
Automatic Pull-Request to merge release ${{ env.TAG_PREFIX }}${{ inputs.version }} back into main
+117
View File
@@ -0,0 +1,117 @@
name: Release Crypto-Node.js
#
# This workflow releases the crypto-bindings for nodejs
#
# It is triggered when seeing a tag prefixed matching `matrix-sdk-crypto-nodejs-v[0-9]+.*`,
# which then build the native bindings for linux, mac and windows via the CI and uploads
# them to the corresponding Github Release tag. Once they are finished, this workflow will
# package the npm tar.gz and uploads that to the Github Release tag as well, before publishing
# it to npmjs.com automatically.
#
# The usual way to trigger this is by manually triggering the `prep-crypto-nodejs-release`
# workflow. See its documentation for instructions how to use it.
env:
PKG_PATH: "bindings/matrix-sdk-crypto-nodejs"
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: 'aarch64-linux-gnu-gcc'
CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER: 'i686-linux-gnu-gcc'
CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: 'arm-linux-gnueabihf-gcc'
on:
push:
tags:
- matrix-sdk-crypto-nodejs-v[0-9]+.*
jobs:
upload-assets:
name: "Upload prebuilt libraries"
strategy:
fail-fast: false
matrix:
include:
# ----------------------------------- Linux
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: i686-unknown-linux-gnu
apt_install: gcc-i686-linux-gnu g++-i686-linux-gnu
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
apt_install: gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
- target: arm-unknown-linux-gnueabihf
os: ubuntu-latest
apt_install: gcc-arm-linux-gnueabihf
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
# ----------------------------------- macOS
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-apple-darwin
os: macos-latest
# ----------------------------------- Windows
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: i686-pc-windows-msvc
os: windows-latest
- target: aarch64-pc-windows-msvc
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
target: ${{ matrix.target }}
override: true
- name: Install Node.js
uses: actions/setup-node@v3
- name: Load cache
uses: Swatinem/rust-cache@v1
- if: ${{ matrix.apt_install }}
run: |
sudo apt-get update
sudo apt-get install -y ${{ matrix.apt_install }}
- name: Build lib
working-directory: ${{env.PKG_PATH}}
run: |
npm install --ignore-scripts
npx napi build --platform --release --strip --target ${{ matrix.target }}
- name: Upload artifacts to release
uses: softprops/action-gh-release@v1
with:
draft: true
files: ${{env.PKG_PATH}}/*.node
publish-nodejs-package:
name: "Package nodejs package"
runs-on: ubuntu-latest
needs:
- upload-assets
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- name: Install Node.js
uses: actions/setup-node@v3
- name: Build lib
working-directory: ${{env.PKG_PATH}}
run: |
npm install --ignore-scripts
npm run build
npm pack
- name: Upload npm package to release
uses: softprops/action-gh-release@v1
with:
draft: true
files: ${{env.PKG_PATH}}/*tgz
- name: Publish to npmjs.com
uses: JS-DevTools/npm-publish@v1
with:
package: ${{env.PKG_PATH}}/package.json
token: ${{ secrets.NPM_TOKEN }}
-75
View File
@@ -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 }}
+3
View File
@@ -8,3 +8,6 @@ emsdk-*
## User settings
xcuserdata/
.vscode/
## OS garbage
.DS_Store
+12 -2
View File
@@ -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 }
+6
View File
@@ -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)
+1 -1
View File
@@ -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"] }
+22
View File
@@ -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 {
+17
View File
@@ -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
View File
@@ -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.
+73
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
@@ -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();
@@ -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
@@ -15,7 +15,7 @@ interface MigrationError {
};
callback interface Logger {
void log(string log_line);
void log(string logLine);
};
callback interface ProgressListener {
@@ -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"
+3
View File
@@ -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"
+55
View File
@@ -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-org/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,
}
}
}
@@ -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;
@@ -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(),
}
}
}
@@ -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);
@@ -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,
}
}
}
@@ -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);
});
});
@@ -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);
});
@@ -1,12 +1,8 @@
const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../');
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);
});
const user = new UserId('@alice:example.org');
@@ -14,7 +10,7 @@ describe(OlmMachine.name, () => {
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 () => {
@@ -32,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));
@@ -48,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));
@@ -90,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();
@@ -325,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",
}
}
@@ -4,3 +4,4 @@
/index.d.ts
/matrix-sdk-crypto.*.node
/docs/*
*.tgz
@@ -0,0 +1,8 @@
src/
tests/
Cargo.toml
build.rs
*.node
*.tgz
tsconfig.json
cliff.toml
@@ -0,0 +1,28 @@
# Matrix-Rust-SDK Node.js Bindings
## 0.1.0-beta.0 - 2022-07-21
Welcome to the first release of `matrix-sdk-crypto-nodejs`. This is a
Node.js binding for the Rust `matrix-sdk-crypto` library. This is a
no-network-IO implementation of a state machine, named `OlmMachine`,
that handles E2EE (End-to-End Encryption) for Matrix clients.
The goal of this binding is _not_ to cover the entirety of the
`matrix-sdk-crypto` API, but only what's required to build Matrix bots
or Matrix bridges (i.e. to connect different networks together via the
Matrix protocol).
This project replaces and deprecates a previous project, with the same
name and same goals, inside [the `matrix-rust-sdk-bindings`
repository](https://github.com/matrix-org/matrix-rust-sdk-bindings),
with the NPM package name `@turt2live/matrix-sdk-crypto-nodejs`. The
The new official package name is
`@matrix-org/matrix-sdk-crypto-nodejs`.
Note: All bindings are now part of [the `matrix-rust-sdk`
repository](https://github.com/matrix-org/matrix-rust-sdk) (see the
`bindings/` root directory).
[A documentation is available inside the new
`matrix-sdk-crypto-nodejs`
project](https://github.com/matrix-org/matrix-rust-sdk/tree/0bde5ccf38f8cda3865297a2d12ddcdaf4b80ca7/bindings/matrix-sdk-crypto-nodejs).
@@ -25,14 +25,16 @@ 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" }
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 = { git = "https://github.com/Hywan/napi-rs", branch = "fix-napi-strict-on-t-and-ref-t", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = { git = "https://github.com/Hywan/napi-rs", branch = "fix-napi-strict-on-t-and-ref-t" }
serde_json = "1.0.79"
http = "0.2.6"
zeroize = "1.3.0"
tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true }
[build-dependencies]
@@ -11,6 +11,77 @@ Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for
## Usage
Just add the latest release to your `package.json`:
```sh
$ npm install --save matrix-sdk-crypto
```
When installing, NPM will download the corresponding prebuilt Rust library for your current host system. The following are supported:
<table>
<thead>
<tr>
<th>Platform</th>
<th>Architecture</th>
<th>Triple</th>
<th>Prebuilt available</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="5">Linux</td>
<td rowspan="2"><code>aarch</code></td>
<td><code>aarch64-unknown-linux-gnu</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>arm-unknown-linux-gnueabihf</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="3"><code>amd</code></td>
<td><code>x86_64-unknown-linux-gnu</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>x86_64-unknown-linux-musl</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>i686-unknown-linux-gnu</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="2">macOS</td>
<td><code>aarch</code></td>
<td><code>arch64-apple-darwin</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>amd</code></td>
<td><code>x86_64-apple-darwin</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="3">Windows</td>
<td><code>aarch</code></td>
<td><code>aarch64-pc-windows-msvc</code></td>
<td>✅</td>
</tr>
<tr>
<td rowspan="2"><code>amd</code></td>
<td><code>x86_64-pc-windows-msvc</code></td>
<td>✅</td>
</tr>
<tr>
<td><code>i686-pc-windows-msvc</code></td>
<td>✅</td>
</tr>
</tbody>
</table>
## Development
This Node.js binding is written in [Rust]. To build this binding, you
need to install the Rust compiler, see [the Install Rust
Page](https://www.rust-lang.org/tools/install). Then, the workflow is
@@ -18,17 +89,20 @@ pretty classical by using [npm], see [the Downloading and installing
Node.js and npm
Page](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
The binding is using NAPI 6 (Node API version 6), which means that is
compatible with Node.js versions 12.17.0, 14.0.0, 15.0.0 and 16.0.0
(see [the full Node API version
matrix](https://nodejs.org/api/n-api.html#node-api-version-matrix))
(we do not support the version 10.20).
The binding is compatible with, and tested against, the Node.js
versions that are in “current”, “active” or “maintenance” states,
according to [the Node.js Releases
Page](https://nodejs.org/en/about/releases/), _and_ which are
compatible with [NAPI v6 (Node.js
API)](https://nodejs.org/api/n-api.html#node-api-version-matrix). It
means that this binding will work with the following versions: 14.0.0,
16.0.0 and 18.0.0.
Once the Rust compiler, Node.js and npm are installed, you can run the
following commands:
```sh
$ npm install
$ npm install --ignore-scripts
$ npm run build
$ npm run test
```
@@ -0,0 +1,59 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Matrix SDK Crypto Node.js Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | filter(attribute="scope", value="crypto-nodejs") | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.id | truncate(length=7, end="") }}{% if commit.breaking %} [**breaking**] {% endif %}: {{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^test", group = "Testing"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
@@ -0,0 +1,113 @@
const { DownloaderHelper } = require('node-downloader-helper');
const { version } = require("./package.json");
const { platform, arch } = process
const DOWNLOADS_BASE_URL = "https://github.com/matrix-org/matrix-rust-sdk/releases/download";
const CURRENT_VERSION = `matrix-sdk-crypto-nodejs-${version}`;
const byteHelper = function (value) {
if (value === 0) {
return '0 b';
}
const units = ['b', 'kB', 'MB', 'GB', 'TB'];
const number = Math.floor(Math.log(value) / Math.log(1024));
return (value / Math.pow(1024, Math.floor(number))).toFixed(1) + ' ' +
units[number];
};
function download_lib(libname) {
let startTime = new Date();
const url = `${DOWNLOADS_BASE_URL}/${CURRENT_VERSION}/${libname}`;
console.info(`Downloading lib ${libname} from ${url}`);
const dl = new DownloaderHelper(url, __dirname, {
override: true,
});
dl.on('end', () => console.info('Download Completed'));
dl.on('error', (err) => console.info('Download Failed', err));
dl.on('progress', stats => {
const progress = stats.progress.toFixed(1);
const speed = byteHelper(stats.speed);
const downloaded = byteHelper(stats.downloaded);
const total = byteHelper(stats.total);
// print every one second (`progress.throttled` can be used instead)
const currentTime = new Date();
const elaspsedTime = currentTime - startTime;
if (elaspsedTime > 1000) {
startTime = currentTime;
console.info(`${speed}/s - ${progress}% [${downloaded}/${total}]`);
}
});
dl.start().catch(err => console.error(err));
}
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
download_lib('matrix-sdk-crypto.win32-x64-msvc.node')
break
case 'ia32':
download_lib('matrix-sdk-crypto.win32-ia32-msvc.node')
break
case 'arm64':
download_lib('matrix-sdk-crypto.win32-arm64-msvc.node')
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
switch (arch) {
case 'x64':
download_lib('matrix-sdk-crypto.darwin-x64.node')
break
case 'arm64':
download_lib('matrix-sdk-crypto.darwin-arm64.node')
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
download_lib('matrix-sdk-crypto.linux-x64-musl.node')
} else {
download_lib('matrix-sdk-crypto.linux-x64-gnu.node')
}
break
case 'arm64':
if (isMusl()) {
throw new Error('Linux for arm64 musl isn\'t support at the moment')
} else {
download_lib('matrix-sdk-crypto.linux-arm64-gnu.node')
}
break
case 'arm':
download_lib('matrix-sdk-crypto.linux-arm-gnueabihf.node')
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
@@ -1,6 +1,6 @@
{
"name": "matrix-sdk-crypto",
"version": "0.5.0",
"name": "@matrix-org/matrix-sdk-crypto-nodejs",
"version": "0.1.0-beta.0",
"main": "index.js",
"types": "index.d.ts",
"napi": {
@@ -11,18 +11,23 @@
]
}
},
"license": "MIT",
"license": "Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.9.0",
"jest": "^28.1.0",
"typedoc": "^0.22.17"
},
"engines": {
"node": ">= 10"
"node": ">= 14"
},
"scripts": {
"build": "napi build --platform --release --strip",
"test": "jest --verbose",
"release-build": "napi build --platform --release --strip",
"build": "napi build --platform",
"postinstall": "node download-lib.js",
"test": "jest --verbose --testTimeout 10000",
"doc": "typedoc --tsconfig ."
},
"dependencies": {
"node-downloader-helper": "^2.1.1"
}
}
@@ -0,0 +1,126 @@
use std::{
io::{Cursor, Read},
ops::Deref,
};
use napi::bindgen_prelude::Uint8Array;
use napi_derive::*;
use crate::into_err;
/// A type to encrypt and to decrypt anything that can fit in an
/// `Uint8Array`, usually big buffer.
#[napi]
pub struct Attachment;
#[napi]
impl Attachment {
/// Encrypt the content of the `Uint8Array`.
///
/// It produces an `EncryptedAttachment`, 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()
}
}
@@ -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;
@@ -23,7 +24,7 @@ impl From<ruma::OwnedUserId> for UserId {
#[napi]
impl UserId {
/// Parse/validate and create a new `UserId`.
#[napi(constructor)]
#[napi(constructor, strict)]
pub fn new(id: String) -> napi::Result<Self> {
Ok(Self::from(ruma::UserId::parse(id.as_str()).map_err(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.
@@ -81,7 +78,7 @@ impl From<ruma::OwnedDeviceId> for DeviceId {
#[napi]
impl DeviceId {
/// Create a new `DeviceId`.
#[napi(constructor)]
#[napi(constructor, strict)]
pub fn new(id: String) -> Self {
Self::from(Into::<ruma::OwnedDeviceId>::into(id))
}
@@ -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, strict)]
pub fn new(id: String) -> napi::Result<Self> {
Ok(Self::from(ruma::DeviceKeyId::parse(id.as_str()).map_err(into_err)?))
}
/// Returns key algorithm of the device key ID.
#[napi(getter)]
pub fn algorithm(&self) -> DeviceKeyAlgorithm {
self.inner.algorithm().into()
}
/// Returns device ID of the device key ID.
#[napi(getter)]
pub fn device_id(&self) -> DeviceId {
self.inner.device_id().to_owned().into()
}
/// Return the device key ID as a string.
#[napi]
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
self.inner.to_string()
}
}
/// The basic key algorithms in the specification.
#[napi]
pub struct DeviceKeyAlgorithm {
inner: ruma::DeviceKeyAlgorithm,
}
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithm {
fn from(inner: ruma::DeviceKeyAlgorithm) -> Self {
Self { inner }
}
}
#[napi]
impl DeviceKeyAlgorithm {
/// Read the device key algorithm's name. If the name is
/// `Unknown`, one may be interested by the `to_string` method to
/// read the original name.
#[napi(getter)]
pub fn name(&self) -> DeviceKeyAlgorithmName {
self.inner.clone().into()
}
/// Return the device key algorithm as a string.
#[napi]
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
self.inner.to_string()
}
}
/// The basic key algorithm names in the specification.
#[napi]
pub enum DeviceKeyAlgorithmName {
/// The Ed25519 signature algorithm.
Ed25519,
/// The Curve25519 ECDH algorithm.
Curve25519,
/// The Curve25519 ECDH algorithm, but the key also contains
/// signatures.
SignedCurve25519,
/// An unknown device key algorithm.
Unknown,
}
impl From<ruma::DeviceKeyAlgorithm> for DeviceKeyAlgorithmName {
fn from(value: ruma::DeviceKeyAlgorithm) -> Self {
use ruma::DeviceKeyAlgorithm::*;
match value {
Ed25519 => Self::Ed25519,
Curve25519 => Self::Curve25519,
SignedCurve25519 => Self::SignedCurve25519,
_ => Self::Unknown,
}
}
}
/// A Matrix [room ID].
///
/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
@@ -112,7 +212,7 @@ impl From<ruma::OwnedRoomId> for RoomId {
#[napi]
impl RoomId {
/// Parse/validate and create a new `RoomId`.
#[napi(constructor)]
#[napi(constructor, strict)]
pub fn new(id: String) -> napi::Result<Self> {
Ok(Self::from(ruma::RoomId::parse(id).map_err(into_err)?))
}
@@ -152,7 +252,7 @@ pub struct ServerName {
#[napi]
impl ServerName {
/// Parse/validate and create a new `ServerName`.
#[napi(constructor)]
#[napi(constructor, strict)]
pub fn new(name: String) -> napi::Result<Self> {
Ok(Self { inner: ruma::ServerName::parse(name).map_err(into_err)? })
}
@@ -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;
@@ -1,6 +1,9 @@
//! The crypto specific Olm objects.
use std::collections::{BTreeMap, HashMap};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use napi::bindgen_prelude::Either7;
use napi_derive::*;
@@ -9,10 +12,11 @@ use ruma::{
OwnedTransactionId, UInt,
};
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
@@ -37,25 +41,62 @@ impl OlmMachine {
/// Create a new memory-based `OlmMachine` asynchronously.
///
/// The created machine will keep the encryption keys only in
/// memory and once the object is dropped the keys will be lost.
/// The persistence of the encryption keys and all the inner
/// objects are controlled by the `store_path` argument.
///
/// # Arguments
///
/// * `user_id`, the unique ID of the user that owns this machine.
/// * `device_id`, the unique id of the device that owns this machine.
#[napi]
/// * `store_path`, the path to a directory where the state of the machine
/// should be persisted; if not set, the created machine will keep the
/// encryption keys only in memory, and once the object is dropped, the
/// keys will be lost.
/// * `store_passphrase`, the passphrase that should be used to encrypt the
/// data at rest in the store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted. This argument is
/// ignored if `store_path` is not set.
#[napi(strict)]
pub async fn initialize(
user_id: &identifiers::UserId,
device_id: &identifiers::DeviceId,
) -> Self {
OlmMachine {
inner: matrix_sdk_crypto::OlmMachine::new(
user_id.inner.as_ref(),
device_id.inner.as_ref(),
)
.await,
}
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(
store_path,
store_passphrase.as_deref(),
)
.map(Arc::new)
.map_err(into_err)
})
.transpose()?;
store_passphrase.zeroize();
Ok(OlmMachine {
inner: match store {
Some(store) => matrix_sdk_crypto::OlmMachine::with_store(
user_id.inner.as_ref(),
device_id.inner.as_ref(),
store,
)
.await
.map_err(into_err)?,
None => {
matrix_sdk_crypto::OlmMachine::new(
user_id.inner.as_ref(),
device_id.inner.as_ref(),
)
.await
}
},
})
}
/// It's not possible to construct an `OlmMachine` with its
@@ -83,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()
}
@@ -103,7 +144,7 @@ impl OlmMachine {
/// response.
/// * `one_time_keys_count`, the current one-time keys counts that the sync
/// response returned.
#[napi]
#[napi(strict)]
pub async fn receive_sync_changes(
&self,
to_device_events: String,
@@ -112,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)))
@@ -129,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(),
)
@@ -174,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.
@@ -187,7 +227,7 @@ impl OlmMachine {
/// * `request_type`, the request type associated to the request ID.
/// * `response`, the response that was received from the server after the
/// outgoing request was sent out.
#[napi]
#[napi(strict)]
pub async fn mark_request_as_sent(
&self,
request_id: String,
@@ -232,14 +272,20 @@ impl OlmMachine {
/// * `users`, the list of users that we should check if we lack a session
/// with one of their devices. This can be an empty array or `null` when
/// calling this method between sync requests.
#[napi]
#[napi(strict)]
pub async fn get_missing_sessions(
&self,
users: Option<Vec<&identifiers::UserId>>,
) -> napi::Result<Option<requests::KeysClaimRequest>> {
let users = users
.unwrap_or_default()
.into_iter()
.map(|user| user.inner.clone())
.collect::<Vec<_>>();
match self
.inner
.get_missing_sessions(identifiers::lower_user_ids_to_ruma(users.unwrap_or_default()))
.get_missing_sessions(users.iter().map(AsRef::as_ref))
.await
.map_err(into_err)?
{
@@ -266,9 +312,11 @@ impl OlmMachine {
/// # Arguments
///
/// * `users`, an array over user IDs that should be marked for tracking.
#[napi]
#[napi(strict)]
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.
@@ -278,22 +326,22 @@ impl OlmMachine {
/// * `room_id`, the room ID of the room where the room key will be used.
/// * `users`, the list of users that should receive the room key.
/// * `encryption_settings`, the encryption settings.
#[napi]
#[napi(strict)]
pub async fn share_room_key(
&self,
room_id: &identifiers::RoomId,
users: Vec<&identifiers::UserId>,
encryption_settings: &encryption::EncryptionSettings,
) -> napi::Result<String> {
let room_id = room_id.inner.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)?,
)
@@ -309,20 +357,20 @@ impl OlmMachine {
/// * `event_type`, the plaintext type of the event.
/// * `content`, the JSON-encoded content of the message that should be
/// encrypted.
#[napi]
#[napi(strict)]
pub async fn encrypt_room_event(
&self,
room_id: &identifiers::RoomId,
event_type: String,
content: String,
) -> napi::Result<String> {
let room_id = room_id.inner.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)?,
)
@@ -335,7 +383,7 @@ impl OlmMachine {
///
/// * `event`, the event that should be decrypted.
/// * `room_id`, the ID of the room where the event was sent to.
#[napi]
#[napi(strict)]
pub async fn decrypt_room_event(
&self,
event: String,
@@ -343,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.
#[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 },
}
/// Sign the given message using our device key and if available
/// cross-signing master key.
#[napi(strict)]
pub async fn sign(&self, message: String) -> types::Signatures {
self.inner.sign(message.as_str()).await.into()
}
}
@@ -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
}
}
@@ -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();
@@ -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())
}
}
@@ -13,7 +13,7 @@ pub struct DeviceLists {
#[napi]
impl DeviceLists {
/// Create an empty `DeviceLists`.
#[napi(constructor)]
#[napi(constructor, strict)]
pub fn new(
changed: Option<Vec<&identifiers::UserId>>,
left: Option<Vec<&identifiers::UserId>>,
@@ -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(strict)]
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(strict)]
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(strict)]
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, strict)]
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();
}
});
});
@@ -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())
}
+57
View File
@@ -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()
}
}
}
}
}
}

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