Compare commits

..

801 Commits

Author SHA1 Message Date
Damir Jelić 4f2cad8f62 matrix-sdk: Bump our versions
CI / Check style (push) Failing after 38s
CI / Run clippy (push) Has been skipped
CI / linux / beta (push) Has been skipped
CI / linux / stable (push) Has been skipped
CI / macOS / stable (push) Has been skipped
CI / windows / stable-x86_64-msvc (push) Has been skipped
2021-01-05 11:23:18 +01:00
Damir Jelić 8924865c9c crypto: Fix a couple of new clippy warnings. 2021-01-04 17:39:40 +01:00
Damir Jelić 60950044f2 matrix-sdk: Bump our deps. 2021-01-04 17:22:09 +01:00
Damir Jelić 4c6c1d2107 matrix-sdk: Get rid of the common macros crate
This crate was used to support different trait bounds on WASM vs other
targets, since we only define async traits in a couple of places having
a whole crate to support this feels a bit excessive.

This patch defines a target specific super trait instead, this lowers
the compile time a couple of seconds.
2021-01-04 16:34:14 +01:00
Damir Jelić 2e3b6fba7d common: Use the re-exported versions of js_int and assign 2021-01-04 15:29:49 +01:00
Damir Jelić de51291166 common: Remove the direct dep to js_int now that Ruma re-exports it 2021-01-04 15:13:48 +01:00
Damir Jelić d84a852ae9 matrix-sdk: Bump ruma to a released version. 2021-01-04 14:06:07 +01:00
Damir Jelić 9245b2a89a crypto: Properly canonicalize the json when verifying signatures as well. 2020-12-22 15:45:42 +01:00
Damir Jelić d39e3141fc crypto: Use CanonicalJsonValue for all the signature calculations. 2020-12-22 14:12:57 +01:00
Alex Black d4327d4cfc EventEmitter: add VoIP event support (m.call.* event types)
Signed-off-by: Alex Black <blacka101@gmail.com>
2020-12-17 00:19:37 +11:00
Damir Jelić d9e5a17ab0 crypto: Use a native Rust sha2 implementation to calculate the commitment 2020-12-08 16:21:29 +01:00
Damir Jelić b5c61af472 crypto: Move the base64 helpers into a common module. 2020-12-08 16:21:29 +01:00
Damir Jelić fd705b7d5e crypto: Canonicalize the start event content before calculating the commitment
This fixes: #117.
2020-12-08 16:02:51 +01:00
Damir Jelić 8e53982bcd Merge branch 'master' into crypto-improvements 2020-12-08 15:06:14 +01:00
Damir Jelić ca4e738fff Merge branch 'master' into user-avatar-ci 2020-12-08 14:43:46 +01:00
Damir Jelić 594e9b9e2d README: Swap out the CI badge. 2020-12-08 14:31:14 +01:00
Damir Jelić 15b87e9dc1 CI: Restrict code coverage to the master branch. 2020-12-08 14:30:49 +01:00
Damir Jelić fa3583234c CI: Fix the coverage yml. 2020-12-08 14:09:51 +01:00
Damir Jelić 9679db6ddc CI: Enable code coverage again. 2020-12-08 14:05:25 +01:00
Damir Jelić fdf48d1f30 CI: Rename the workflow file. 2020-12-08 13:58:26 +01:00
Damir Jelić 40d13d9b59 cyrpto: Another timing based test that only works on Linux. 2020-12-08 13:37:55 +01:00
Damir Jelić 4ab6ae7f30 crypto: Fix an os_target definition. 2020-12-08 13:15:19 +01:00
Damir Jelić c8dd6bfd26 crypto: Scope the imports for the unwedging test into the test. 2020-12-08 12:56:16 +01:00
Damir Jelić 0a76f75c22 Revert "CI: Use the cargo-clippy workflow."
This reverts commit 795c1225dd.
2020-12-08 12:45:16 +01:00
Damir Jelić 795c1225dd CI: Use the cargo-clippy workflow. 2020-12-08 12:37:39 +01:00
Damir Jelić b982d36303 crypto: Run the time sensitive tests only on linux. 2020-12-08 12:34:59 +01:00
Damir Jelić a80aa4c2ad base: Fix some lint issues. 2020-12-08 12:11:55 +01:00
Damir Jelić 27d9cf04de base: Remove a flaky state store test.
The state store is undergoing a rewrite and this test fails more often
than i would like making our CI seem flaky.

Remove the test since it's going to become obsolete anyways.
2020-12-08 11:52:21 +01:00
Damir Jelić e4779163b8 CI: Run the tests on the CI. 2020-12-08 11:36:01 +01:00
Damir Jelić 2cc899338a CI: Split out the fmt and clippy runs into two jobs. 2020-12-08 11:21:09 +01:00
Amanda Graven 8dc56ec332 Add methods for setting, getting and uploading avatar 2020-12-08 11:18:00 +01:00
Damir Jelić 1d5ee22dd2 CI: Name our lint stages. 2020-12-08 11:08:41 +01:00
Damir Jelić 59917f45e3 matrix-sdk: Fix a clippy lint. 2020-12-08 11:01:20 +01:00
Damir Jelić 35247fac2a crypto: Fix a lint issue. 2020-12-08 10:50:58 +01:00
Damir Jelić e24fb8b471 CI: Fix the cargo fmt invocation. 2020-12-08 10:44:19 +01:00
Damir Jelić 795900cf39 CI: Add a lint github workflow. 2020-12-08 10:42:05 +01:00
Amanda Graven bca7f41ca9 Fix error in example 2020-12-07 13:14:23 +01:00
Amanda Graven 7f503eb71c Add examples, remove user from method names 2020-12-07 12:59:10 +01:00
Amanda Graven a26dc3179a Add methods for getting and setting display name 2020-12-07 11:17:26 +01:00
Damir Jelić aa1a64628f crypto: Remove a bunch of unneeded whitespace in a log line. 2020-12-05 14:59:40 +01:00
Damir Jelić 0e66640b9f crypto: Log both user id versions when the device keys mismatch. 2020-12-05 14:59:40 +01:00
Damir Jelić 3f41e5071b crypto: Preserve the relationship info while decrypting events. 2020-12-05 14:59:40 +01:00
Damir Jelić 9eb17e757c matrix-sdk: Update ruma. 2020-12-05 14:59:40 +01:00
Damir Jelić 804bd221b2 crypto: Improve key imports.
This patch changes so key imports load all existing sessions at once
instead loading a single session for each session we are importing. It
removes the need to lock the session when we check the first known index
and exposes the total number of sessions the key export contained.
2020-12-02 11:12:46 +01:00
Damir Jelić e20b1efae9 crypto: Store private identities and accounts with the Changes struct as well. 2020-12-01 17:14:32 +01:00
Damir Jelić e65915e159 Merge branch 'crypto-improvements' 2020-12-01 15:10:58 +01:00
Damir Jelić 4800e80492 matrix-sdk: Remove an unused import. 2020-12-01 15:08:53 +01:00
Damir Jelić 5d0ff961b2 crypto: Check the Olm message hash if we fail to decrypt an Olm message.
Wether by accident (the next_batch token doesn't get stored properly) or
by malicious intent (the server replays a message) an Olm encrypted to-device
message may appear multiple times.

This is usually fine since nothing bad happens, we don't decrypt the message
and the message gets thrown away.

Since the introduction of Olm session unwedging an undecryptable message
leads to the creation of a new fresh Olm session. To avoid this we
remember which Olm messages we already decrypted so they don't trigger
an unwedging dance.
2020-12-01 14:50:04 +01:00
Damir Jelić 270350cd34 crypto: Save the olm message hash. 2020-12-01 14:38:03 +01:00
Damir Jelić ae2391791d crypto: Use a released sqlx version. 2020-12-01 13:25:51 +01:00
Damir Jelić 24592adbba crypto: Return a higher level struct when decrypting olm messages instead of tuples 2020-12-01 12:41:11 +01:00
Damir Jelić efe659910f crypto: Remove some stale TODOs. 2020-12-01 11:20:55 +01:00
Damir Jelić 08babb6d6c crypto: Document the new cross signing methods in the store. 2020-12-01 10:54:41 +01:00
Damir Jelić 50bd408d48 matrix-sdk: Don't use try_from for the u32 -> UInt conversion. 2020-12-01 10:34:10 +01:00
Damir Jelić ce4d53a88c examples: Feature gate the cross signing bootstrap example. 2020-11-26 14:27:11 +01:00
Damir Jelić 7e9baf2707 crypto: Remove some dead code definitions. 2020-11-26 14:15:52 +01:00
Damir Jelić 3073883076 crypto: Fix a clippy warning. 2020-11-26 14:02:35 +01:00
Damir Jelić 7ec5a5ad1a Merge branch 'master' into crypto-improvements 2020-11-26 13:24:57 +01:00
Jonas Platte 0422bae924 Fix clippy lint rc_buffer 2020-11-25 19:01:28 +01:00
Jonas Platte 27ecab8574 Update ruma 2020-11-25 19:01:28 +01:00
Jonas Platte 5ca66a6985 Upgrade ruma dependency 2020-11-23 15:34:38 +01:00
Jonas Platte 2e387436cf Remove unstable-synapse-quirks from default feature set for ruma
otherwise there is no point in exposing that feature
2020-11-23 15:27:43 +01:00
Jonas Platte 591f031246 Don't disable default-features on ruma
there are no features that are active by default, this is a no-op.
2020-11-23 14:58:48 +01:00
Damir Jelić dedb1eb745 Merge branch 'update-ruma' 2020-11-20 21:21:24 +01:00
Damir Jelić c40edcf2fc matrix-sdk: Try to lower our compile times, at least in the crypto part for now. 2020-11-20 20:35:48 +01:00
Damir Jelić 6509e72a74 Revert "base: Don't handle the wildcard case for member events anymore."
Using the exhaustive feature in ruma enables the appservice/federation
apis, adding some 10 more crates to our dependencies. Disable that
feature for now.

This reverts commit 41529a6bff.
2020-11-20 20:35:48 +01:00
Jonas Platte 38fec7f2b3 Upgrade ruma 2020-11-20 20:35:48 +01:00
Alejandro Domínguez 95243003c4 Update ruma 2020-11-20 20:14:18 +01:00
Damir Jelić 11fcf5c42f rust-sdk: Document the cross signing bootstrap method. 2020-11-05 14:33:45 +01:00
Damir Jelić b27f1b0e34 crypto: Fix some clippy warnings. 2020-10-30 14:38:29 +01:00
Damir Jelić b67cd4ddd2 crypto: Create a trusted public cross signing identity when we create a private one. 2020-10-30 13:21:14 +01:00
Damir Jelić 44cc1cef71 crypto: Let devices hold on to the private identity. 2020-10-30 11:41:48 +01:00
Damir Jelić 34bec59389 crypto: Hold on to the private identity in the store. 2020-10-30 11:34:55 +01:00
Damir Jelić cb95f576a5 crypto: Clear out the signatures when signing a device.
This avoids re-uploading all the existing signatures.
2020-10-29 15:37:29 +01:00
Damir Jelić 5c530cf9ee crypto: Upload signatures after verification is done. 2020-10-27 16:39:23 +01:00
Damir Jelić 30a78bb1d6 crypto: Add the private identity to the Sas object. 2020-10-27 14:21:22 +01:00
Damir Jelić 2077ea0ddf crypto: Split out the device_key signing method. 2020-10-27 13:48:51 +01:00
Damir Jelić e757d605f5 crypto: Allow users to be signed as well. 2020-10-27 13:29:19 +01:00
Damir Jelić 61a5293af5 cyrpto: Document the signing module. 2020-10-26 16:03:59 +01:00
Damir Jelić 6e83a4bbca crypto: Split out the signing module into two files. 2020-10-26 16:03:59 +01:00
Damir Jelić 5c14910126 crypto: WIP cross signing bootstrap. 2020-10-26 16:03:59 +01:00
Damir Jelić 8ed1e37cef crypto: Save the account if we create a new one. 2020-10-23 11:17:37 +02:00
Damir Jelić 5fd004bae5 crypto: Connect the private identity to the verification machine. 2020-10-23 11:17:13 +02:00
Damir Jelić 7de002b128 crypto: Fix some lint issues. 2020-10-22 16:40:05 +02:00
Damir Jelić f60dc7ed78 crypto: Allow cross signing identities to be stored/restored. 2020-10-22 16:25:25 +02:00
Damir Jelić 78d7f6c10b crypto: Fix a clippy issue. 2020-10-21 17:05:36 +02:00
Damir Jelić fa25ca4475 crypto: Make the pickle key encryption future proof. 2020-10-21 16:52:40 +02:00
Damir Jelić c9db63509f crypto: Add error handling to the signing module. 2020-10-21 16:24:10 +02:00
Damir Jelić ac0df5dea9 crypto: Properly handle errors in the pickle key decryption. 2020-10-21 15:28:43 +02:00
Damir Jelić d175c47a05 crypto: Use a random pickle key in the sqlite store. 2020-10-21 15:13:21 +02:00
Damir Jelić 959e8450af crypto: Use a transaction to create sqlite tables. 2020-10-21 14:01:27 +02:00
Damir Jelić dd0642cd59 crypto: Add a pickle key struct. 2020-10-21 13:21:22 +02:00
Damir Jelić 6a7da5a8b6 crypto: Correctly generate a random nonce for pickling of the signing objects. 2020-10-21 12:55:45 +02:00
Damir Jelić 7cab7cadc9 crypto: Rework the cryptostore.
This modifies the cryptostore and storage logic in two ways:
    * The cryptostore trait has only one main save method.
    * The receive_sync method tries to save all the objects in one
    `save_changes()` call.

This means that all the changes a sync makes get commited to the store
in one transaction, leaving us in a consistent state.

This also means that we can pass the Changes struct the receive sync
method collects to our caller if the caller wishes to store the room
state and crypto state changes in a single transaction.
2020-10-20 17:19:37 +02:00
Damir Jelić 728d80ed06 crypto: Connect the cross signing to the main state machine. 2020-10-19 16:03:01 +02:00
Damir Jelić 0c1d33d43f Merge branch 'master' into crypto-improvements 2020-10-18 10:21:52 +02:00
Damir Jelić 8f99180c99 Merge branch 'up-ruma' into master 2020-10-18 10:17:43 +02:00
Jonas Platte 0682292b91 Upgrade ruma 2020-10-18 02:01:39 +02:00
Damir Jelić 404cc410cc crypto: Fix the docs and return value of the import_keys method. 2020-10-17 14:39:19 +02:00
Damir Jelić 17cc4fcb81 matrix-sdk: Fix an import for the non-crypto case. 2020-10-16 19:45:38 +02:00
Damir Jelić 93f49265a6 crypto: Use a git version of sqlx.
The beta release has a nasty bug where one thread would consume 100% of
CPU.
2020-10-16 19:42:41 +02:00
Damir Jelić b1c8c64205 matrix-sdk: Add support to delete devices. 2020-10-16 17:27:00 +02:00
Damir Jelić 425a07d670 crypto: Don't load all the devices in the sqlite store. 2020-10-16 16:57:26 +02:00
Damir Jelić 4262f1d3b0 crypto: Don't cache inbound group sessions in the sqlite store. 2020-10-16 15:54:50 +02:00
Damir Jelić b5560d3cb6 crypto: More transactions in the sqlite store. 2020-10-16 15:23:34 +02:00
Damir Jelić fc54c63a4c crypto: Upgrade sqlx to the beta release.
This change is much needed to enable transactions in our sqlite store,
before this release creating a transaction would take ownership of the
connection, now it just mutably borrows it.
2020-10-16 15:05:53 +02:00
Damir Jelić e7a24d5e68 crypto: Move the session managers under a common module. 2020-10-16 11:09:55 +02:00
Damir Jelić b5c9473424 crypto: Test the session unwedging logic. 2020-10-15 15:03:22 +02:00
Damir Jelić 59d7b53242 crypto: Add an user for a key request if the device was marked as wedged. 2020-10-15 15:02:02 +02:00
Damir Jelić 59a7199202 crypto: Initial test for the session manager. 2020-10-15 13:58:35 +02:00
Damir Jelić d1313b8614 crypto: Fix another clippy warning. 2020-10-14 16:15:26 +02:00
Damir Jelić 4e8ce4cb5d crypto: Fix clippy warnings and don't use the PickleMode for signing pickling. 2020-10-14 16:01:52 +02:00
Damir Jelić c85fe6bc21 crypto: Initial support for private cross signing identities. 2020-10-14 15:35:06 +02:00
Damir Jelić 3338ecf62a Merge branch 'master' into crypto-improvements 2020-10-13 13:02:02 +02:00
Damir Jelić 1c6a67d864 matrix-sdk: Bump our deps. 2020-10-13 13:01:18 +02:00
Damir Jelić e737200fbe matrix-sdk: Remove an useless import. 2020-10-13 11:55:18 +02:00
Damir Jelić 41b3b0651f matrix-sdk: Don't use strings for the content type in the upload methods. 2020-10-13 11:00:52 +02:00
Damir Jelić 1cabc0cac9 crypto: Correctly store the uploaded key count when saving the account.
This fixes: #101.
2020-10-13 09:47:49 +02:00
Denis Kasak e0ee03fa6f Update room_messages docstring to reflect actual state of the API. 2020-10-12 21:18:51 +02:00
Denis Kasak 4ec7aff301 Add documentation on enabling logging. 2020-10-12 20:45:47 +02:00
Denis Kasak f5ff91935f Add missing apostrophe. 2020-10-12 20:45:47 +02:00
Denis Kasak 7519bec9a3 Tweak descriptions given by Describe impls. 2020-10-12 20:45:47 +02:00
Damir Jelić bf7070b8f2 Merge branch 'client-get-session' into master 2020-10-12 15:45:55 +02:00
Damir Jelić 39fc33d37a Merge branch 'ser-deser-session' into master 2020-10-12 15:36:32 +02:00
Denis Kasak d81a6e6872 cargo fmt 2020-10-12 15:17:46 +02:00
Denis Kasak 2afc0c7661 Implement BaseClient::get_session to retrieve the login session.
Closes #100.
2020-10-12 15:12:23 +02:00
Denis Kasak f349811020 Add Serialize/Deserialize impls for matrix_sdk::Session. 2020-10-12 14:56:08 +02:00
Damir Jelić 0ab12fe969 examples: Add some whitespace to the autojoin example. 2020-10-12 12:50:03 +02:00
strct c4ac0d0570 matrix-sdk: example: retry autojoin 2020-10-10 16:17:08 +02:00
Damir Jelić d039a39d84 common: Expose the lock guards publicly. 2020-10-10 14:13:28 +02:00
Damir Jelić 1c008f549c common: Expose the lock guards publicly. 2020-10-10 12:08:58 +02:00
Damir Jelić bd0ac703a0 crypto: Initial logic for session unwedging. 2020-10-09 15:39:35 +02:00
Damir Jelić 6d2e9cfc02 crypto: Share the users_for_key_claim map between modules. 2020-10-09 11:36:31 +02:00
Damir Jelić 661f182382 Merge branch 'master' into crypto-improvements 2020-10-08 18:28:04 +02:00
Dominique Martinet 9ea4835e3e Revert "matrix_sdk examples: set #![type_length_limit = "1075569"]"
This reverts commit 7d023ebdb9.

Rust 1.47.0 got released and the tuning is no longer necessary.
2020-10-08 17:57:23 +02:00
Dominique Martinet 2602c36ad0 matrix_sdk_base: save room states after successfully parsed account events 2020-10-08 16:10:58 +02:00
Dominique Martinet d858940342 matrix_sdk_base: handle response.account_data events
"m.direct" events are not in room account data events but in main one
2020-10-08 16:10:58 +02:00
Dominique Martinet 883183324f matrix_sdk_base: room: add direct_target field
Rooms marked as "direct" are associated a user_id in "m.direct" events.
Clients could want to handle these separately
2020-10-08 16:10:58 +02:00
Dominique Martinet 7d023ebdb9 matrix_sdk examples: set #![type_length_limit = "1075569"]
Some examples no longer build after the following commits, set a
bigger-than-default type_length_limit to let tests pass.

The exceptions are not necessary on nightly and can be removed again
after https://github.com/rust-lang/rust/issues/54540 is fixed.
2020-10-08 16:10:48 +02:00
Damir Jelić 473e49252e crytpo: Get the session from the list of sessions in a safe manner. 2020-10-08 15:56:17 +02:00
Damir Jelić d96c9f85a1 crypto: Add doces for the get_missing_sessions method. 2020-10-08 14:50:35 +02:00
Damir Jelić 279ce0bba0 crypto: Split out the Olm session handling logic into a separate module. 2020-10-08 14:41:34 +02:00
Damir Jelić da5ef42719 crypto: Log when we invalidate a group session. 2020-10-08 14:03:01 +02:00
Dominique Martinet a4eae1053c matrix_sdk: expose RoomMember 2020-10-08 13:16:33 +02:00
Dominique Martinet f7039d9a8d matrix_sdk_base: expose RoomMember 2020-10-08 13:16:33 +02:00
Damir Jelić 723fdeaa06 crypto: Fix a clippy warning. 2020-10-08 12:59:10 +02:00
Damir Jelić 19d513e3c0 crypto: Simplify and test the group session invalidation logic. 2020-10-08 12:40:42 +02:00
Damir Jelić 23ac00c8ec crypto: Initial support for group session invalidation. 2020-10-08 11:16:02 +02:00
Damir Jelić 4019ebf121 crypto: Fix some clippy warnings. 2020-10-07 17:56:29 +02:00
Damir Jelić 220ccfb52b matrix-sdk: Fix the arguments docs for sync_with_callback. 2020-10-07 15:26:44 +02:00
Damir Jelić 9a838abd67 crypto: Log when we're not serving a key request because of a missing session. 2020-10-07 14:22:13 +02:00
Damir Jelić 17d23eb9e5 matrix-sdk: Add automatic key claiming support. 2020-10-07 14:07:47 +02:00
Damir Jelić 8ea0035cd0 crypto: Add the automatic key claim users to the key claim request. 2020-10-07 14:02:50 +02:00
Damir Jelić 06b9c71dbc crypto: Refactor out the key share wait queue. 2020-10-07 12:42:39 +02:00
Damir Jelić 6a8ac62a51 crypto: Remove an unwrap. 2020-10-07 11:57:46 +02:00
Damir Jelić 1e894269c8 crypto: Correctly handle the key share without a session and test it. 2020-10-07 11:57:09 +02:00
Damir Jelić 27c6f30e0f Merge branch 'master' into crypto-improvements 2020-10-06 16:44:11 +02:00
Damir Jelić bc48674f9f Merge branch 'new-sync-methods' into master 2020-10-06 16:43:41 +02:00
Damir Jelić e5f0f64405 crypto: Initial scaffolding for key shares for devices that are missing a session. 2020-10-06 16:38:42 +02:00
Damir Jelić 09f4b07fb7 matrix-sdk: Use a specific version for async-std. 2020-10-06 15:17:45 +02:00
Damir Jelić 2ffac286ed matrix-sdk: Switch to using an enum for the sync loop callback return value. 2020-10-06 15:04:43 +02:00
Damir Jelić 83b48fb53c matrix-sdk: Fix the login example. 2020-10-06 12:43:59 +02:00
Damir Jelić f4137c6bba Merge branch 'master' into crypto-improvements 2020-10-06 12:23:04 +02:00
Damir Jelić e16b7f9c44 matrix-sdk: Add an example for the login method. 2020-10-06 12:01:47 +02:00
Damir Jelić 45953a268c matrix-sdk: Mention that the key import/export methods don't work on WASM. 2020-10-06 11:41:18 +02:00
Damir Jelić 84039ad7aa matrix-sdk: Add links from the login method docs to the restore_login ones. 2020-10-06 11:40:32 +02:00
Damir Jelić 137fa9619f matrix-sdk: Add the ability to stop the sync loop and rename the sync methods.
This renames our sync methods so it's clearer which one the main one is.
Syncing should be done with the sync method, if one wishes to sync only
once the sync_method is provided.

If one wishes to have a callback called with every sync the
sync_with_callback method exists, the callback now returns a boolean
that signals if the loop should be aborted. This does not mean that the
current sync request will abort, a cancelable future is still needed for
this.
2020-10-06 11:37:29 +02:00
Damir Jelić e3d24f5c31 crypto: Fix some clippy warnings. 2020-10-01 16:45:13 +02:00
Damir Jelić 02c765f903 crypto: Don't mark outbound group sessions automatically as shared. 2020-10-01 16:31:24 +02:00
Damir Jelić fc6ff2c78a crytpo: Remove an unneeded map/clone. 2020-10-01 12:46:09 +02:00
Damir Jelić bcdcdeb259 Merge branch 'master' into crypto-improvements 2020-10-01 12:21:45 +02:00
Damir Jelić 1d8f01ef11 crypto: Remove the third Device variant. 2020-10-01 12:15:13 +02:00
Alejandro Domínguez b58d88e0c3 Upgrade ruma 2020-10-01 11:23:26 +02:00
Damir Jelić c8ca93c924 crytpo: Let the verification machine hold on to a raw CryptoStore.
This will later be useful when our higher level store wrapper holds on
to a verification machine to return higher level Device objects.
2020-10-01 11:17:27 +02:00
Damir Jelić d644af7be9 crypto: Remove an unneeded clone. 2020-10-01 09:56:22 +02:00
Damir Jelić ff2079da91 crypto: Move the group session handling logic into separate module. 2020-09-30 15:43:25 +02:00
Damir Jelić 646f18ae18 crypto: Remove an unused import. 2020-09-29 17:53:11 +02:00
Damir Jelić 2b8d4a21a4 crypto: Connect the key request handling to the main state machine. 2020-09-29 17:40:06 +02:00
Damir Jelić 78badd9af8 crypto: Use the correct event type when sending out forwarded room keys. 2020-09-29 17:36:56 +02:00
Damir Jelić 58aef51770 crypto: Remove an unneeded mutable borrow. 2020-09-29 14:44:18 +02:00
Damir Jelić 8fe1eda169 crypto: Test the full key share flow. 2020-09-29 14:18:03 +02:00
Damir Jelić 84066d4a76 crypto: Split out the Account into a read only portion and one with effects. 2020-09-29 12:03:41 +02:00
Damir Jelić e1c220e2f7 crypto: Test a key share cycle. 2020-09-29 10:24:54 +02:00
Damir Jelić 798656dac5 crypto: Allow the key request machine to access the outbound group sessions. 2020-09-29 10:09:47 +02:00
Damir Jelić 721c459577 crypto: Collapse an if tree. 2020-09-28 15:07:57 +02:00
Damir Jelić 23173c4a1e crypto: Test our key sharing decision logic. 2020-09-28 14:51:57 +02:00
Damir Jelić 4a8c5ebab0 crypto: Return an enum that describes why we won't serve a key share request. 2020-09-28 14:12:08 +02:00
Damir Jelić e29508938b crypto: More work on the incoming key request handling. 2020-09-28 13:32:30 +02:00
Damir Jelić a357536ade crypto: Initial scaffolding for incoming key share handling. 2020-09-28 09:27:16 +02:00
Damir Jelić f3be27921c crypto: Move the device trust state logic into the read only device. 2020-09-24 12:45:23 +02:00
Damir Jelić 42c4cf2a30 crypto: Test the outgoing requests method instead of accessing the field. 2020-09-24 12:00:22 +02:00
Damir Jelić c5bece2d58 crypto: Zeroize and remove the session key copies for forwarded room keys. 2020-09-24 11:18:01 +02:00
Damir Jelić 4662ca2e32 crypto: Refactor the one-time key count update logic. 2020-09-24 11:16:15 +02:00
Damir Jelić 5a86b067e4 crypto: Add tests for the identity manager. 2020-09-23 15:45:25 +02:00
Damir Jelić 9a5345ec77 Merge branch 'aledomu-master' into master 2020-09-23 13:45:44 +02:00
Damir Jelić 7c3e751d6e Merge branch 'crypto-improvements' into master 2020-09-23 11:07:49 +02:00
Alejandro Domínguez 3070c98d26 Export "unstable-synapse-quirks" feature from ruma 2020-09-22 21:03:12 +02:00
Damir Jelić 95e906e0dc crypto: Save the account if the one-time key count updates. 2020-09-18 20:50:32 +02:00
Damir Jelić 2e3d30d7b4 crypto: Move the identity/device management logic into a separate struct. 2020-09-18 20:50:32 +02:00
Damir Jelić 5b0457dad0 crypto: Remember the users that received the outbound group session. 2020-09-18 18:55:17 +02:00
Damir Jelić a183584541 crypto: Test that we correctly check the hash when decrypting attachments. 2020-09-18 17:49:44 +02:00
Damir Jelić 562bb5aee3 crypto: Remove some dead key requests code for now. 2020-09-18 17:26:56 +02:00
Damir Jelić dea3e4adf4 crypto: Document when a key export may panic. 2020-09-18 14:04:39 +02:00
Damir Jelić 5d5d5bb141 crypto: Hook up the key requesting to the main state machine. 2020-09-18 13:50:13 +02:00
Damir Jelić c58cf71be1 crypto: Send out key request cancellations once we receive a key. 2020-09-18 13:49:46 +02:00
Damir Jelić af4b00195b crypto: Implement the key/value store for the sqlite store. 2020-09-18 13:42:51 +02:00
Damir Jelić 41529a6bff base: Don't handle the wildcard case for member events anymore. 2020-09-17 17:31:17 +02:00
Damir Jelić 300b03bd9e crypto: Add more test for the outgoing key requests. 2020-09-17 17:13:42 +02:00
Damir Jelić a5b195efc7 crypto: Initial tests for the key requests state machine. 2020-09-17 16:55:33 +02:00
Damir Jelić 692f9baa0e crypto: Add logic to handle outgoing key requests. 2020-09-17 16:09:08 +02:00
Damir Jelić 6b24d91ed9 crypto: Add an initial version of our key request state machine. 2020-09-17 14:16:43 +02:00
Damir Jelić 24ce4881c7 crypto: Add a method to save/load arbitrary objects from a CryptoStore.
This actually adds trait methods that save/load strings from the
CryptoStore. We add a wrapper for the CryptoStore since we can't mix
trait objects and generics, so we add generic methods to save/load
anything that implements Serialize/Deserialize.
2020-09-16 16:03:19 +02:00
Damir Jelić 849934b180 crypto: Use a constant for the attachment encryption version. 2020-09-16 12:39:23 +02:00
Damir Jelić 428b28a985 matrix-sdk: Increase the type length limit for the wasm example. 2020-09-16 12:28:42 +02:00
Damir Jelić 95145fae8f matrix-sdk: Remove the example with encrypted uploads.
The example fail to build on platforms where we don't support encryption. So
remove the example for now.
2020-09-16 12:09:30 +02:00
Damir Jelić ae894e0ff6 crypto: Finish up the attachment encryption.
This adds docs and proper error handling to the attachment encryption.
Zeroing out the key buffers is added as well.
2020-09-16 12:05:44 +02:00
Damir Jelić 86d95518be matrix-sdk: Fix the case where the encryption feature is disabled. 2020-09-15 19:10:26 +02:00
Damir Jelić c8e459bc55 matrix-sdk: Fix the encryption feature. 2020-09-15 18:07:00 +02:00
Damir Jelić 4d431b7c9e matrix-sdk: Test the attachment sending paths. 2020-09-15 18:06:32 +02:00
Damir Jelić 890e6cbc73 crypto: Turn an unwrap into a except. 2020-09-15 17:18:31 +02:00
Damir Jelić e98960f30b matrix-sdk: Add an image uploading bot to the examples. 2020-09-15 17:17:28 +02:00
Damir Jelić c500c06e4b matrix-sdk: Add docs and cleanup the media upload methods. 2020-09-15 17:16:16 +02:00
Damir Jelić 3ac3be501f matrix-sdk: Refactor out the check if a room is encrypted. 2020-09-15 15:02:59 +02:00
Damir Jelić 3573614640 crypto: Add some TODOs for the key query handling. 2020-09-15 12:13:35 +02:00
Damir Jelić a60f60bd7d Merge branch 'master' into encrypted_attachments 2020-09-15 12:04:37 +02:00
Damir Jelić a4980e8a04 matrix-sdk: Remove an unneeded lifetime. 2020-09-14 20:38:53 +02:00
Damir Jelić b628e6286a crypto: Remove an unused import. 2020-09-14 20:27:30 +02:00
Jonas Platte fb47abcc17 Update ruma 2020-09-14 20:26:52 +02:00
Damir Jelić c2756a9a92 matrix-sdk: First draft for our upload method. 2020-09-14 20:07:55 +02:00
Damir Jelić 2d6882c495 crypto: Use a Read implementation for the attachment encryption as well. 2020-09-14 20:06:44 +02:00
Damir Jelić 51f3d90224 crypto: Move the file encryption modules under a submodule. 2020-09-14 17:14:18 +02:00
Damir Jelić 1a140ecc2f crypto: Initial support for attachment encryption. 2020-09-14 16:38:52 +02:00
Damir Jelić f603696ff4 crypto: Expose the olm machine only if the encryption feature is enabled. 2020-09-11 17:06:45 +02:00
Damir Jelić ffd2843b0a matrix-sdk: Expose the import/export keys methods. 2020-09-11 16:34:39 +02:00
Damir Jelić 618a58ba34 crypto: Add error handling to the key exports. 2020-09-10 17:02:36 +02:00
Damir Jelić 126ac3059b Merge branch 'key_export' into master 2020-09-10 16:32:41 +02:00
Damir Jelić 8af18a4df7 crypto: Test the EncryptionSettings conversion. 2020-09-10 16:21:23 +02:00
Damir Jelić 7790c3db8f crypto: Fix a bunch of clippy warnings. 2020-09-10 16:07:28 +02:00
Damir Jelić e3f4c1849c crypto: Finish up the key export feature. 2020-09-10 15:54:41 +02:00
Damir Jelić 848156213b crypto: Add a PartialEq derive for the exported key struct. 2020-09-10 15:51:39 +02:00
Damir Jelić 23e953d9cf crypto: Hide some methods that shouldn't be public. 2020-09-10 15:49:34 +02:00
Damir Jelić 464e181f66 crypto: Add a method to get all group sessions from the store. 2020-09-10 14:59:20 +02:00
Damir Jelić 7bd0e4975b crypto: Store the forwarding chains for group sessions. 2020-09-09 17:27:10 +02:00
Damir Jelić 127d4c225b crypto: Change the crypto store so we can save multiple group sessions at once. 2020-09-09 16:34:18 +02:00
Damir Jelić 9617d9aac9 crypto: Test the import/export of group sessions. 2020-09-09 16:10:16 +02:00
Damir Jelić e828828ace crypto: Document the exported key -> forwarded room key conversion methods. 2020-09-09 15:11:25 +02:00
Damir Jelić 3e9b0a8e7f crypto: Correctly store the ed25519 key map for inbound group sessions. 2020-09-09 15:03:19 +02:00
Damir Jelić aff1e1d0a8 crypto: Add key export methods for inbound group sessions. 2020-09-09 12:47:28 +02:00
Damir Jelić 98f69aed41 crypto: Remove some duplicated types after the group session split. 2020-09-09 11:52:10 +02:00
Damir Jelić acfd0cdb07 crypto: Split out the group session module into multiple files. 2020-09-09 11:07:49 +02:00
Damir Jelić fc60593801 crypto: Remove some unused into implementation. 2020-09-08 17:34:34 +02:00
Damir Jelić 14226c0778 crypto: Refactor some tests. 2020-09-08 16:17:17 +02:00
Damir Jelić 70ffc43ce0 crypto: Store the trust state of our own identities as well. 2020-09-08 16:07:37 +02:00
Damir Jelić 9810a2f630 crypto: Finish up the cross signing storing for the sqlite store. 2020-09-08 15:24:23 +02:00
stoically 35f5117800 matrix-sdk-common: Switch futures-locks to crates.io version 2020-09-08 07:28:32 +02:00
Damir Jelić d35cf56dc8 crypto: Disable the real life key export test since it take a lot of time. 2020-09-07 16:59:30 +02:00
Damir Jelić 083cebe735 crypto: Initial WIP user identity storing logic. 2020-09-07 16:57:58 +02:00
Damir Jelić faaf3f7a29 crypto: Identities add some methods to get the keys/signatures of the keys. 2020-09-07 16:57:17 +02:00
Damir Jelić 34cdf31cc5 matrix-sdk: Don't require the user id to be passed to set a typing notice. 2020-09-05 20:32:16 +02:00
Damir Jelić 6c7dbb814b matrix-sdk: Add a convenience method to get our own devices. 2020-09-05 18:04:15 +02:00
Damir Jelić 217543ef38 matrix-sdk: Bump the versions of our deps. 2020-09-05 18:03:47 +02:00
Damir Jelić f57447527d crypto: Initial logic for encrypting key exports. 2020-09-04 17:59:56 +02:00
Damir Jelić 8dbc7c38e5 crypto: Correctly split the 2 keys in the key export logic. 2020-09-04 16:34:19 +02:00
Damir Jelić 5a069a8721 Merge branch 'master' into key_export 2020-09-04 14:48:56 +02:00
Damir Jelić 89efcee337 crypto: Move the signature verification method under an Utility struct. 2020-09-04 13:18:31 +02:00
Damir Jelić 22daf0d81e Merge branch 'to-device-txn-uuid' into crypto-improvements 2020-09-04 12:54:40 +02:00
Damir Jelić 53fec7a87e crypto: Don't ignore store errors when fetching the identities. 2020-09-04 12:44:03 +02:00
Damir Jelić adf8905d9f crypto: Rename the memory stores into caches and reorder the store module. 2020-09-04 12:42:40 +02:00
Damir Jelić 7b3dfe2f27 crypto: Move the device and user identities under one module. 2020-09-04 10:51:46 +02:00
Jonas Platte 73c104cac1 Replace IncomingToDeviceRequest with customized request type 2020-09-03 20:02:55 +02:00
Damir Jelić d86c05efb3 crypto: Add a fixme to the sqlite store since it's not storing forwarding chains. 2020-09-02 15:08:24 +02:00
Damir Jelić cc236a8765 examples: Fix the wasm bot example. 2020-09-02 14:23:00 +02:00
Damir Jelić 8b5bb7d8c5 crypto: Remove the deserialize implementations for our user identity.
Deriving Serialize/Deserialize for an AtomicBool doesn't seem to be
implemented under WASM. So remove the derives for now.
2020-09-02 13:54:04 +02:00
Damir Jelić 2195da1cd8 crypto: Fix some docs. 2020-09-02 12:28:18 +02:00
Damir Jelić 65843f89dc crypto: Simplify the signature loading in the sqlite cryptostore. 2020-09-02 12:24:46 +02:00
Damir Jelić 8b56546565 crypto: Remove an unwrap from the sqlite cryptostore. 2020-09-02 12:17:38 +02:00
Damir Jelić 8c4acf54e0 crypto: Reorder the errors so unpickling now returns the timestamp error. 2020-09-02 12:11:06 +02:00
Damir Jelić c652762255 crypto: Allow user identities to be seralized/deserialized. 2020-09-02 11:54:04 +02:00
Damir Jelić 4bab678e46 crypto: Allow most of the ReadOnlyDevice to be serialized. 2020-09-02 11:49:49 +02:00
Damir Jelić 81b127b6e7 crypto: Modify all the pickling logic so we return serializeable structs. 2020-09-02 11:45:35 +02:00
Damir Jelić 269cfc3d34 crypto: Add a pickled account struct making account storing easier. 2020-09-02 09:37:10 +02:00
Damir Jelić 987d87cd5d crypto: Use the correct async-trait macro for the CryptoStores. 2020-09-01 17:41:30 +02:00
Damir Jelić 0de4a21320 crypto: Expose some missing structs that are needed to implement a cryptostore. 2020-09-01 17:39:51 +02:00
Devin Ragotzy 6872cc717b matrix_sdk: fix Client docs for methods that used request builders 2020-08-26 16:30:29 -04:00
Devin Ragotzy 5c4e46e908 matrix_sdk_common: Bump ruma 2020-08-26 16:26:40 -04:00
Damir Jelić a2bfa08e09 crypto: Initial decryption method for key exports. 2020-08-26 19:14:24 +02:00
Damir Jelić 977e29c3af matrix-sdk: Fix the wasm bot example. 2020-08-26 16:19:39 +02:00
Damir Jelić a2f7297941 Merge branch 'reexport-reqwest' into master 2020-08-26 16:07:05 +02:00
Alejandro Domínguez 6fa365935f Add "socks" feature from reqwest 2020-08-26 16:01:50 +02:00
Damir Jelić 39628a308b matrix-sdk: Allow any event content to be sent out with room_send(). 2020-08-26 15:41:27 +02:00
Damir Jelić 54391040a4 matrix-sdk: Re-export reqwest. 2020-08-26 14:47:43 +02:00
Damir Jelić 7a418ae09e matrix-sdk: Implement the HttpSend trait directly on the reqwest client. 2020-08-26 14:37:48 +02:00
Damir Jelić deff66ac42 matrix-sdk: Simplify the registration example. 2020-08-26 14:16:31 +02:00
Damir Jelić 2995cebd57 matrix-sdk: Fix some clippy issues. 2020-08-26 13:50:28 +02:00
Damir Jelić ea4befabd9 matrix-sdk: Fix the incorrect return value of the HttpSend trait.
The HttpSend trait incorrectly returns a reqwest::Response, we already
have logic to return the response into a http::Response and we need to
do the conversion since there is no other way to build Ruma responses.
2020-08-26 13:41:15 +02:00
Damir Jelić 6760f81498 matrix-sdk: Update Ruma. 2020-08-26 13:40:38 +02:00
Damir Jelić b3d1e8687e matrix-sdk: Fix to a released version of reqwest. 2020-08-26 10:26:05 +02:00
Damir Jelić 95c8708995 crypto: Document and rename the mark_requests_as_sent() method. 2020-08-24 14:49:57 +02:00
Damir Jelić 8d39821a1f crypto: Remove some unused imports from the top level module. 2020-08-24 14:34:22 +02:00
Damir Jelić 2bcbf1eca4 Merge branch 'power-ev-overflow' into master 2020-08-24 14:27:02 +02:00
Damir Jelić a5f06f772f Merge branch 'rustls' into master 2020-08-24 10:00:21 +02:00
Devin Ragotzy 2b389b920d matrix_sdk_base: Add test for update_member_power overflow 2020-08-23 20:57:59 -04:00
Damir Jelić 298c260c5f crypto: Document the outgoing request types. 2020-08-23 17:03:04 +02:00
Devin Ragotzy 72614e4252 matrix_sdk_crypto: Appease clippy 2020-08-22 08:00:32 -04:00
Devin Ragotzy 8a71cec81a matrix_sdk_base: Member power level math from Int -> i64 2020-08-22 07:52:12 -04:00
Tilo Spannagel a57c6159bd Fix travis ci errors
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-21 19:11:10 +02:00
Tilo Spannagel 5f10f4301c Add feature flag for rustls
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-21 18:36:42 +02:00
Damir Jelić 176181bdcf Merge branch 'crypto-improvements' into master 2020-08-21 18:16:48 +02:00
Damir Jelić edea5e1c51 crypto: Fix a clippy warning. 2020-08-21 16:46:28 +02:00
Damir Jelić b3941ca254 crypto: Verify user identities when we're the first one to confirm as well. 2020-08-21 16:39:15 +02:00
Damir Jelić c3c6428717 crypto: Remove some clippy warnings. 2020-08-21 16:31:02 +02:00
Damir Jelić de90da4adc crypto: Make the verification machine compatible with how we queue up requests. 2020-08-21 16:26:34 +02:00
Damir Jelić 002531349e crypto: Decluter the main doc page a bit. 2020-08-21 15:06:54 +02:00
Damir Jelić e38bfc64f4 crypto: Streamline the key claiming so we use the new mark request as sent method. 2020-08-21 14:40:49 +02:00
Damir Jelić 93e1967119 crypto: Initial refactor to switch to the outgoing_requests queue. 2020-08-21 13:35:01 +02:00
Damir Jelić aee40977a3 crypto: Clamp the rotation period ms so users can't wedge E2E.
Users may set a very small rotation period this might mean that a
session might expire by the time it's shared ending up in a loop where
we constantly need to share a group session yet never manage to send a
message.
2020-08-21 12:50:16 +02:00
Damir Jelić 9fe23227af base: Fix the encryption settings Into implementation. 2020-08-21 12:44:14 +02:00
Damir Jelić ce93869915 crypto: Return an Option instead of an empty result for the key uploads. 2020-08-21 09:50:01 +02:00
Damir Jelić 202c20feda crypto: Rename the method to set the local trust of a device. 2020-08-20 18:01:34 +02:00
Damir Jelić c307690c2e crypto: Fix a clippy warning and some spelling. 2020-08-20 16:06:06 +02:00
Damir Jelić 552a12eeed crypto: More docs for the user identities. 2020-08-20 15:52:40 +02:00
Damir Jelić c2ad298963 crypto: Check that the user ids match for the cross signing keys. 2020-08-20 15:40:49 +02:00
Damir Jelić d908d0f817 crypto: Don't allow user identities to verify devices of other users. 2020-08-20 15:17:19 +02:00
Damir Jelić 9edc876160 crypto: Check that the master key and subkeys have the same user id. 2020-08-20 15:14:58 +02:00
Damir Jelić 398edbbe0c crypto: Reset the verification state of our identity if the master keys change. 2020-08-20 15:13:55 +02:00
Damir Jelić 89b56b5af8 crypto: Don't expose the btree map of the master key dirrectly.
This implements PartialEq for the master key so we can check if they
have changed when doing SAS.
2020-08-20 15:06:49 +02:00
Damir Jelić a57f63d614 crypto: Document the user identities. 2020-08-20 14:44:16 +02:00
Damir Jelić 74dd0a00d3 crypto: Simplify the default hashmaps in the memory stores. 2020-08-20 12:23:18 +02:00
Damir Jelić b97e3d7bae crypto: Fix a clippy warning. 2020-08-20 10:49:14 +02:00
Damir Jelić c3eb4d8106 crypto: Simplify some more function definitions. 2020-08-20 10:36:58 +02:00
Damir Jelić ea49a35b43 crypto: Simplify the function signature of share_group_session. 2020-08-20 10:25:05 +02:00
Damir Jelić a99e47c310 crypto: Shorten some log lines. 2020-08-20 10:23:16 +02:00
Damir Jelić 69fbe65ac4 crypto: Add some docs for the cross signing keys handling method. 2020-08-20 10:21:00 +02:00
Damir Jelić aaa15c768c crypto: Simplify the Olm message map construction. 2020-08-20 10:19:55 +02:00
Damir Jelić 58185e08e8 crypto: Move the olm_encrypt() method into the higher level Device. 2020-08-20 10:18:36 +02:00
Nym Seddon 89c9e31140 doc: Add UIAA auth data to registration example
Add direct request authentication data to registration example
2020-08-20 01:42:01 +00:00
Damir Jelić 1bd15b9fdd crypto: Remove some unneeded clones. 2020-08-19 18:04:06 +02:00
Damir Jelić 23126c4e48 crypto: Disable the sqlite store test if the feature is disabled. 2020-08-19 17:55:28 +02:00
Damir Jelić 6f5352b9a9 crypto: Test the signature checking of user identities. 2020-08-19 17:52:38 +02:00
Damir Jelić eb16737d3b crypto: Add some comments about the order of signature checks. 2020-08-19 15:35:34 +02:00
Damir Jelić 56309ae12c matrix-sdk: Bump the versions of our deps. 2020-08-19 14:52:11 +02:00
Damir Jelić 9fe0717cee examples: Update the emoji verification example tho show a list of devices.
This may showcase that cross signing verification works if the other
device uploads valid signatures.
2020-08-19 14:50:35 +02:00
Damir Jelić 7f23cbbeb5 crypto: Add a TODO about cross signing signatures. 2020-08-19 14:49:40 +02:00
Damir Jelić 3153a81cd2 crypto: Add support to check the cross signing verification state of a device. 2020-08-19 14:47:22 +02:00
Damir Jelić c3e593d998 crypto: The device identity can be our own, so store the identity enum instead. 2020-08-19 14:43:49 +02:00
Damir Jelić c2a386b889 crypto: Fix a clippy warning. 2020-08-19 14:40:04 +02:00
Damir Jelić 317a141e07 crypto: If our own identity passed a SAS flow, mark it as verified. 2020-08-19 14:34:18 +02:00
Damir Jelić 3990e50ca6 crypto: Store the verified identities in the SAS states. 2020-08-19 14:28:16 +02:00
Damir Jelić 90ea0229f2 crypto: Rename TrustState to LocalTrust since.
We might still trust the device event if our local trust isn't set, so
rename the enum to better reflect that meaning.
2020-08-19 11:20:08 +02:00
Damir Jelić a42af5da69 crypto: Let the device hold on to identities.
This makes it possible to check the verification state of the device
directly.
2020-08-19 10:58:14 +02:00
Damir Jelić f63a01a85b crypto: Remove a stale TODO. 2020-08-18 15:36:04 +02:00
Damir Jelić 27e1fb9a35 crypto: Pass the user identity to the SAS object when a start event is received. 2020-08-18 15:25:00 +02:00
Damir Jelić c21517c61e crypto: Store the changed user identities. 2020-08-18 15:23:37 +02:00
Damir Jelić f626f2b24e crypto: Add some logging for the user identity update logic. 2020-08-18 15:22:30 +02:00
Damir Jelić 37a7f69e03 crypto: Implement storage for the user identities in the memory store. 2020-08-18 15:13:56 +02:00
Damir Jelić 38cf771f1f crypto: Pass the identity further through the SAS layer and try to verify it. 2020-08-18 14:24:27 +02:00
Damir Jelić 6d0b73cb3d crypto: Pass the user identity to the SAS object when doing verifications. 2020-08-18 13:37:02 +02:00
Damir Jelić f96437a242 crypto: Initial scaffolding for handling user identities in key queries. 2020-08-18 12:50:03 +02:00
Damir Jelić 150862ec0c matrix-sdk: Remove an useless into(). 2020-08-17 17:47:29 +02:00
Damir Jelić 6db7eb0694 crypto: Add a method to directly verify a device. 2020-08-17 17:36:07 +02:00
Damir Jelić 84c0311d80 crypto: Rename the UserDevicesWrap struct. 2020-08-17 17:12:39 +02:00
Damir Jelić de097d3ca0 crypto: Rename UserDevices to ReadOnlyUserDevices. 2020-08-17 17:01:38 +02:00
Damir Jelić 8aedc3077d matrix-sdk: Add an example to the start verification method of the device. 2020-08-17 16:47:24 +02:00
Damir Jelić 0f26e7e3bc crypto: Fix the doc for the read-only device. 2020-08-17 16:40:37 +02:00
Damir Jelić 91db502cfe crypto: Rename DeviceWrap to Device. 2020-08-17 16:36:50 +02:00
Damir Jelić 43aea6e482 crypto: Rename Device to ReadOnlyDevice. 2020-08-17 16:17:28 +02:00
Damir Jelić e778f7d72d matrix-sdk: Remove an unneeded clone. 2020-08-17 15:56:19 +02:00
Damir Jelić 94248523b3 matrix-sdk: Implement deref for our device wrapper. 2020-08-17 15:54:54 +02:00
Damir Jelić fd8377bce2 crypto: Add device wrappers so that the verification can be started with a device. 2020-08-17 15:36:45 +02:00
Damir Jelić 9e609a0fdf matrix-sdk: Move the session into the http client wrapper. 2020-08-17 15:29:07 +02:00
Damir Jelić 16a115d27e Merge branch 'up-ruma' into master 2020-08-17 11:17:02 +02:00
Damir Jelić 8167f5e9de crypto: Simplify the function signature of the share group session method. 2020-08-16 16:25:48 +02:00
Damir Jelić 5876c89858 crypto: The mark_user_as_changed method doesn't need to be public. 2020-08-15 15:51:04 +02:00
Jonas Platte 5040be042f Update ruma 2020-08-15 15:17:27 +02:00
Jonas Platte ad2d3d2037 Simplify tests in matrix_sdk::client 2020-08-15 03:05:22 +02:00
Damir Jelić 09f009ebd7 matrix-sdk: Bump our deps. 2020-08-14 17:11:54 +02:00
Damir Jelić 664d8c239c crypto: Don't share group sessions with blacklisted devices. 2020-08-14 16:20:49 +02:00
Damir Jelić 97ad060d4b crypto: Test that we can create other users identities. 2020-08-14 16:18:18 +02:00
Damir Jelić f4de3580b6 crypto: Expose the device/identity verification methods through the identities. 2020-08-14 15:32:44 +02:00
Damir Jelić 0fc5134563 crypto: Add methods to check if a cross signing key signed a device. 2020-08-14 15:06:24 +02:00
Damir Jelić b0de9d1809 crypto: Allow some test methods to be dead code since macOS can't use them. 2020-08-14 15:04:59 +02:00
Damir Jelić 75fa7e97f9 crypto: Remove some unneeded clones. 2020-08-14 14:29:53 +02:00
Damir Jelić d21e8213b5 crypto: Don't panic if the key id can't be parsed. 2020-08-14 14:25:51 +02:00
Damir Jelić 181c2a92de crypto: Initial scaffolding for the public cross signing keys. 2020-08-14 14:10:29 +02:00
Damir Jelić 08d76f2ff4 crypto: Pass the device key id to the verify signature method. 2020-08-14 14:08:53 +02:00
Damir Jelić 5b758b8344 crypto: Don't allow dead code in the SAS layer anymore. 2020-08-14 11:09:50 +02:00
Damir Jelić 499f2796ba crypto: Add some logging to the MAC calculation for SAS. 2020-08-14 10:57:17 +02:00
Damir Jelić df0444faa5 crypto: Test the full SAS flow from the Olm machine. 2020-08-13 16:46:11 +02:00
Damir Jelić b4c1b26f96 crytpo: Store the SAS object in the machine if we're starting it. 2020-08-13 16:45:12 +02:00
Damir Jelić 0245782cf4 crypto: Better grammar for a panic message. 2020-08-13 15:59:17 +02:00
Damir Jelić 87d0102663 crypto: Test the Olm machine with the default store. 2020-08-13 15:57:31 +02:00
Damir Jelić 6ee8b07cfe crypto: Test that session expiration works correctly. 2020-08-13 15:03:28 +02:00
Damir Jelić 344631b4ee crypto: Respect the encryption settings of a room when creating sessions. 2020-08-13 14:41:59 +02:00
Damir Jelić f3e03c66a5 travis: Don't clippy check all features. 2020-08-13 12:31:41 +02:00
Damir Jelić d4e31f07a1 matrix-sdk: Fix the docs for our feature flags. 2020-08-13 12:18:24 +02:00
Damir Jelić d4de877e09 base: Fix the docs for our feature flags. 2020-08-13 12:17:30 +02:00
Damir Jelić 9b8e11aab9 crypto: Fix the docs for our features. 2020-08-13 11:06:26 +02:00
Damir Jelić a0abffd026 crypto: Fix the link to the share group session method. 2020-08-13 11:04:37 +02:00
Damir Jelić 4e99278eac matrix-sdk: Expose the device methods in the Client. 2020-08-13 10:49:38 +02:00
Damir Jelić cdb8b5c1e9 matrix-sdk: Fix a couple of typoes. 2020-08-13 10:28:40 +02:00
Damir Jelić bf42e1a39f matrix-sdk: Put the send_to_device method behind the encryption feature for now. 2020-08-12 19:18:30 +02:00
Damir Jelić 5883396106 base: Hide the user devices method behind the encryption feature. 2020-08-12 17:49:08 +02:00
Damir Jelić c6b0a19171 base: Fix a stale docstring. 2020-08-12 17:17:56 +02:00
Damir Jelić 7ee0430054 base: Add methods to fetch user devices. 2020-08-12 17:17:22 +02:00
Damir Jelić 36ca784690 crypto: Expose a method to get all devices of an user. 2020-08-12 17:16:27 +02:00
Damir Jelić 2449bd27c1 matrix-sdk: Make sure our doctests don't make HTTP requests. 2020-08-12 17:15:18 +02:00
Damir Jelić 29bd38734f matrix-sdk: Remove an unused import. 2020-08-12 17:10:31 +02:00
Damir Jelić 6c07620a26 matrix-sdk: Fix the to-device imports for the non-crypto case. 2020-08-12 16:52:50 +02:00
Damir Jelić 3e3894b573 matrix-sdk: Fix for the non-encryption enabled use-case. 2020-08-12 16:19:41 +02:00
Damir Jelić 0a26195472 matrix-sdk: Clean up the client tests. 2020-08-12 15:53:42 +02:00
Damir Jelić 0dc232b268 base: Fix a clippy warning. 2020-08-12 15:39:38 +02:00
Damir Jelić c4465e7979 matrix-sdk: Rename cli to client in the doc examples. 2020-08-12 15:23:44 +02:00
Damir Jelić 41f04d4f5d client: Refactor out the group session sharing logic. 2020-08-12 15:22:17 +02:00
Damir Jelić 15d7deddb8 matrix-sdk: Only claim one-time keys if we're also going to share group sessions. 2020-08-12 15:15:50 +02:00
Damir Jelić 18e597aa79 crypto: More doc fixes. 2020-08-12 15:14:16 +02:00
Damir Jelić 407f9a3da8 matrix-sdk: Make sure to not send out multiple group share requests at once. 2020-08-12 15:12:51 +02:00
Damir Jelić 82c3a795ff crypto: More doc improvements. 2020-08-12 13:28:16 +02:00
Damir Jelić ccda5c7260 crypto: Small doc improvements to the OlmMachine. 2020-08-12 13:11:51 +02:00
Damir Jelić d706140a8f crypto: Fix a SAS docstring. 2020-08-12 12:49:29 +02:00
Damir Jelić 8351858be7 crypto: Expose a method to get a users device. 2020-08-12 12:48:22 +02:00
Damir Jelić 7cb25361b2 matrix-sdk: Expose an API to start SAS verifications. 2020-08-12 11:39:47 +02:00
Damir Jelić 42a4ad60e8 Merge branch 'lockless-cryptostore' into master 2020-08-11 17:37:38 +02:00
Damir Jelić 9a325a4505 matrix-sdk: Move the HttpSend trait into the http_client file. 2020-08-11 17:25:33 +02:00
Damir Jelić fe572017b1 Merge branch 'http-send-trait' into master 2020-08-11 17:07:34 +02:00
Damir Jelić c4ed5b6cda matrix-sdk: Upgrade our deps. 2020-08-11 16:54:58 +02:00
Damir Jelić 0d2f8c6d0f crypto: Fix some clippy warnings. 2020-08-11 16:01:48 +02:00
Damir Jelić fa1a40543c crypto: Add a missing license header to the sas helpers file. 2020-08-11 15:55:13 +02:00
Damir Jelić 7637e79f2c matrix-sdk: Fix the tarpaulin skip directives. 2020-08-11 15:49:04 +02:00
Damir Jelić d0a5b86ff3 crypto: Remove our lock around the cryptostore. 2020-08-11 15:39:50 +02:00
Damir Jelić 707b4c1185 crypto: Put a bunch of crypto store stuff behind atomic references. 2020-08-11 15:17:33 +02:00
Devin Ragotzy 9234ac96e1 matrix_sdk: Use our version of the async_trait macro 2020-08-11 09:17:18 -04:00
Damir Jelić 2437a92998 crypto: Don't require the account loading method to borrow self mutably. 2020-08-11 15:12:15 +02:00
Damir Jelić 947fa08dae crypto: Don't require the load_account to mutably borrow self. 2020-08-11 15:08:07 +02:00
Damir Jelić 8f4ac3da7f crypto: Change the way we load the devices/sessions in the SqliteStore. 2020-08-11 14:43:18 +02:00
Damir Jelić 01bcbaf063 crypto: Remove most mutable self borrows from the crypto-store trait. 2020-08-11 14:34:42 +02:00
Devin Ragotzy 4770dc636a matrix_sdk_common_macros: Bump syn fixing conflicting deps 2020-08-11 08:08:17 -04:00
Devin Ragotzy 9294280dc1 matrix_sdk: Add DefaultHttpClient and impl HttpSend 2020-08-11 08:07:45 -04:00
Devin Ragotzy fba3298162 matrix_sdk: Create HttpSend trait to abstract sending requests 2020-08-11 08:06:43 -04:00
Damir Jelić ac2469d270 crypto: Change the way we check if an user is already tracked. 2020-08-11 13:45:32 +02:00
Damir Jelić db553b2040 crypto: Fix some clippy warnings. 2020-08-11 13:38:20 +02:00
Damir Jelić eeb6a811c0 crypto: Make the in-memory stores threadsafe and cloneable. 2020-08-11 13:18:58 +02:00
Damir Jelić 528483ef0e crypto: Remove the last mutable self borrows in the Olm machine methods. 2020-08-11 12:22:14 +02:00
Damir Jelić 72168ce084 crypto: Fix the unknown method tests fot the SAS state transitions. 2020-08-11 11:51:34 +02:00
Damir Jelić 6c85d3e28f crypto: Use TryFrom to check the accepted SAS protocols. 2020-08-11 11:24:29 +02:00
Damir Jelić d5a853f3da crypto: More SAS tests for all the unknown SAS methods. 2020-08-11 11:05:22 +02:00
Damir Jelić 8a2d6a4450 tarpaulin: Disable tarpaulin debugging. 2020-08-11 10:55:10 +02:00
Damir Jelić c15ffb989a crypto: Remove an unused import. 2020-08-11 09:48:01 +02:00
Damir Jelić 2b78f05aad crypto: More SAS tests. 2020-08-11 09:28:28 +02:00
Damir Jelić 1f0a96e31d crypto: Disable the SAS timeout test on macOS. 2020-08-10 17:26:15 +02:00
Damir Jelić 6593cce778 crypto: Simplify the Instant substraction. 2020-08-10 16:53:15 +02:00
Damir Jelić d7bcf42a2b crypto: False alarm with the deadlock we just didn't use the right method. 2020-08-10 16:18:20 +02:00
Damir Jelić 18b655f829 crypto: Test the cancellation of timed out verifications. 2020-08-10 15:55:08 +02:00
Damir Jelić e2e70d6583 crypto: Cancel timed out verifications. 2020-08-10 15:24:22 +02:00
Damir Jelić c305b5052b matrix-sdk: Don't allow dead code anymore. 2020-08-10 15:23:49 +02:00
Damir Jelić 6f4d2022fd Merge branch 'master' into sas-timeout 2020-08-10 15:00:08 +02:00
Damir Jelić ef5201cf35 Merge branch 'up-ruma' into master 2020-08-10 14:58:47 +02:00
Damir Jelić 7bcdc2a3b6 Merge branch 'master' into sas-timeout 2020-08-10 14:57:32 +02:00
Damir Jelić 7eeff64059 crypto: Cancel timed out events on the state transitions. 2020-08-10 14:29:38 +02:00
Matthew Hodgson 9c4229dc57 typoes 2020-08-10 13:15:58 +01:00
Damir Jelić 6c4e2fa508 crypto: Remove mutable borrows in the tests. 2020-08-10 14:15:47 +02:00
Damir Jelić d5cd608045 base: Remove some unnecessary mutable borrows of the olm machine. 2020-08-10 14:11:55 +02:00
Jonas Platte d83fc971ce Update ruma 2020-08-10 13:58:39 +02:00
Damir Jelić d96142b8cb Merge branch 'master' into sas-timeout 2020-08-10 13:48:02 +02:00
Damir Jelić cd5d5da06a matrix-sdk: Use the upstream git repo for olm-rs. 2020-08-10 13:43:18 +02:00
Damir Jelić 87bcba3561 crypto: Add timestamps to the SAS struct so we can check if it timed out. 2020-08-10 13:30:12 +02:00
Damir Jelić 81e9a7cefc crypto: Pass a String when setting the other SAS pubkey. 2020-08-10 10:18:57 +02:00
Damir Jelić 3ddb2199d2 Merge branch 'fix-http-headers' into master 2020-08-09 10:25:15 +02:00
Jonas Platte 4abab73462 Update reqwest to a git dependency 2020-08-09 00:57:58 +02:00
Damir Jelić 17fd85d687 matrix-sdk: Test that we're passing the auth token in the headers. 2020-08-08 15:00:28 +02:00
Jonas Platte 279e88d9f9 Fix handling of headers in HttpClient 2020-08-08 12:23:43 +02:00
Jonas Platte d016ce1848 Use identifier macros in tests 2020-08-06 13:03:32 +02:00
Jonas Platte 591388d13e Upgrade ruma 2020-08-05 18:00:45 +02:00
Devin Ragotzy a3b4cab22e matrix_sdk_crypto: Fix clippy warnings add wasm emscripten to .gitignore 2020-08-04 20:02:09 -04:00
Devin Ragotzy ffdb9c4a79 Fix failing wasm test and clippy warnings for wasm 2020-08-04 17:39:25 -04:00
Devin Ragotzy cb8d5ce8fb Rename CustomOrRawEvent -> CustomEvent and use raw json when failed
When deserialization fails we fallback to providing the user with a
serde_json::RawValue, basically the json string. Ruma should handle all
events that conform to a matrix event shape correctly by either
converting them to their type or returning a custom event.
2020-08-04 17:27:57 -04:00
Devin Ragotzy c10120602a Add test actually testing a correct message edit event 2020-08-04 17:22:54 -04:00
Devin Ragotzy 47690bd268 Bump ruma and fix failing unrecognized_event test
The test was broken because the JSON being fed into it was bad.
2020-08-04 17:22:54 -04:00
Damir Jelić 807432b31f crypto: Calculate the correct extra info when generating emojis. 2020-08-04 13:54:00 +02:00
Damir Jelić 69d2a00759 crypto: Add a TODO about SAS timing out. 2020-08-04 12:56:55 +02:00
Damir Jelić be01ee2de0 crypto: Cancel the verification if we find a MAC mismatch. 2020-08-04 12:31:56 +02:00
Damir Jelić 408fe5da4b crypto: Check that the other device had a valid MAC. 2020-08-04 12:14:19 +02:00
Damir Jelić 28a7831ffd matrix-sdk: Fix the import for the no-encryption case. 2020-08-04 11:41:20 +02:00
Damir Jelić 2bf8c99dfe Merge branch 'master' into sas-verification 2020-08-04 11:23:24 +02:00
Damir Jelić 77f0676a58 matrix-sdk: The emoji example requires the encryption feature. 2020-08-04 10:49:08 +02:00
Damir Jelić e7b2a54e46 matrix-sdk: Add a tarpaulin config. 2020-08-04 10:40:13 +02:00
Devin Ragotzy 33e1601004 matrix_sdk: Fix import error 2020-08-03 20:13:58 -04:00
Damir Jelić 26ec0c6368 crypto: Proptest the emoji/decimal calculation. 2020-08-03 17:22:44 +02:00
Damir Jelić 9f0fbcccf6 crypto: Remove verification objects that are done or canceled. 2020-08-03 16:18:35 +02:00
Damir Jelić 01ba94c670 matrix-sdk: Hide the cryptostore error behind a feature flag. 2020-08-03 15:40:39 +02:00
Damir Jelić e431ba0bf5 crypto: Fix some clippy warnings. 2020-08-03 15:05:19 +02:00
Damir Jelić a3bb8a0d74 examples: Don't use a proxy for the emoji example. 2020-08-03 14:59:03 +02:00
Damir Jelić 3245fbb1c9 examples: Clean up the emoji example a bit. 2020-08-03 14:51:45 +02:00
Damir Jelić f4517c150c crypto: Add more log lines to the SAS code. 2020-08-03 14:51:04 +02:00
Damir Jelić e37229554b crypto: Make sure that we don't hold on to a mutex guard over an await. 2020-08-03 14:49:33 +02:00
Damir Jelić df9da7539a crypto: Expose some more SAS info publicly. 2020-08-03 14:33:15 +02:00
Damir Jelić 1787d2ebe6 crypto: Hook up marking the device as verified. 2020-08-03 12:38:43 +02:00
Damir Jelić faadb4953b Revert "common: Switch to the ruma git repo."
This reverts commit 021193087d.
2020-08-03 10:22:17 +02:00
Damir Jelić 021193087d common: Switch to the ruma git repo. 2020-08-03 10:04:09 +02:00
Devin Ragotzy 0ac2b84c02 Unify import style across workspace 2020-08-02 08:05:43 -04:00
Devin Ragotzy 230b2a229f matrix_sdk: Remove clippy allows 2020-08-02 07:46:02 -04:00
Devin Ragotzy ed1f12ce37 Run cargo fmt with merge-imports true 2020-08-02 07:46:02 -04:00
Damir Jelić 3f83941d57 Merge branch 'master' into sas-verification 2020-07-31 16:27:52 +02:00
Damir Jelić 91d7a8329e matrix-sdk: Add an example that does SAS verification. 2020-07-31 15:34:46 +02:00
Damir Jelić 1a40491c0b matrix-sdk: Fix some clippy warnings. 2020-07-31 15:18:03 +02:00
Devin Ragotzy 79e661d1d9 sdk-base: Remove MessageWrapper and do not order messages in queue 2020-07-31 09:16:41 -04:00
Damir Jelić dce06d31aa Merge branch 'master' into sas-verification 2020-07-31 15:14:05 +02:00
Damir Jelić 3472614649 Merge branch 'remove-possibly-redacted-type-alias' into master 2020-07-31 15:13:23 +02:00
Damir Jelić 7ecd4a035f crypto: Split out the Sas logic into different files. 2020-07-31 14:54:08 +02:00
Denis Kasak 2ce0765206 Remove confusing type alias for AnyPossiblyRedactedSyncMessageEvent.
There's already a SyncMessageEvent in Ruma which is something else.
Let's prefer the full, unambiguous type.
2020-07-31 14:36:58 +02:00
Damir Jelić 108f6d90c9 matrix-sdk-common: Depend on our local Ruma branch. 2020-07-31 12:29:08 +02:00
Damir Jelić 7ceda2f39c crypto: Update to the latest Ruma changes. 2020-07-31 12:05:07 +02:00
Denis Kasak e00e94c6c3 Fix input order. 2020-07-31 11:46:52 +02:00
Damir Jelić a71c7b2964 crypto: Add a method to set the verification state of devices. 2020-07-30 15:54:56 +02:00
Damir Jelić 30c07b4e08 matrix-sdk: Send out to-device events in the sync_forever() loop. 2020-07-30 15:53:55 +02:00
Damir Jelić d9fbc18777 crypto: Update to the latest Ruma changes. 2020-07-30 15:48:13 +02:00
Damir Jelić a58ace70a7 crypto: Fix the SAS MAC calculation KEYIDS -> KEY_IDS. 2020-07-30 15:45:19 +02:00
Denis Kasak 359c5280d7 Expose sender in PossiblyRedactedExt.
Also add a few missing apostrophes.
2020-07-30 14:40:27 +02:00
Damir Jelić a07767d417 base: Hide the SAS getting method behind the encryption feature. 2020-07-30 11:50:42 +02:00
Damir Jelić 5a58fdff98 cyrpto: Fix a clippy warning. 2020-07-29 14:50:39 +02:00
Damir Jelić 5058f09111 matrix-sdk: Remove an incorrect copyright line. 2020-07-29 14:40:05 +02:00
Damir Jelić 21b0afe72c matrix-sdk: Add a Sas wrapper. 2020-07-29 14:19:47 +02:00
Damir Jelić a726ebab39 crypto: Allow Sas objects to be canceled. 2020-07-29 13:53:33 +02:00
Damir Jelić 2b124d98bc matrix-sdk: Pass the rwlock to the http client when doing requests. 2020-07-29 13:50:01 +02:00
Damir Jelić 4634efc092 crypto: More SAS content to to-device request logic. 2020-07-29 13:23:03 +02:00
Damir Jelić 117ebeaf4b crypto: Return requests when you want to accept a verification. 2020-07-29 12:47:36 +02:00
Damir Jelić 27f918e52d matris-sdk: Move the http request sending logic into a separate struct. 2020-07-29 10:56:18 +02:00
Damir Jelić 9facd86d81 base: Expose the verification methods in the base client. 2020-07-28 16:44:06 +02:00
Damir Jelić 7f2df68d62 crypto: Expose some SAS methods publicly. 2020-07-28 16:24:45 +02:00
Damir Jelić a6fa9f99fd crypto: Hook up the verification machine. 2020-07-28 15:37:20 +02:00
Damir Jelić 7e95d85f17 crypto: Move the cryptostore behind a lock. 2020-07-28 15:03:44 +02:00
Damir Jelić 57b65ec8c4 crypto: Add a verification machine. 2020-07-28 14:45:53 +02:00
Damir Jelić 2d6fff7927 crypto: A bit of cleanup and docs. 2020-07-28 11:29:13 +02:00
Damir Jelić 792623f53d crypto: Fix a clippy warning. 2020-07-27 15:57:30 +02:00
Damir Jelić 6e67585bf6 crypto: Handle all the cancel states. 2020-07-27 15:56:28 +02:00
Damir Jelić 5471c07244 crypto: More canceling. 2020-07-27 15:34:18 +02:00
Damir Jelić 0b04f7960b crypto: Add more checks and cancels in the SAS state machine. 2020-07-27 15:28:14 +02:00
Damir Jelić 623f91733e crypto: More verification canceling. 2020-07-27 13:18:00 +02:00
Damir Jelić da3734ffc7 crypto: Add initial SAS canceling. 2020-07-27 13:16:56 +02:00
Damir Jelić 7128505768 Merge branch 'master' into sas-verification 2020-07-26 21:20:53 +02:00
Jonas Platte 6a96368048 Upgrade ruma 2020-07-26 16:58:27 +02:00
Damir Jelić 8c9c843bfc crypto: Fix a comment in the sas file. 2020-07-25 10:59:20 +02:00
Damir Jelić 094b2f90d6 Merge branch 'master' into sas-verification 2020-07-25 10:31:20 +02:00
Damir Jelić 2cbdca1f58 crypto: Make it easier to create canceled SasState. 2020-07-25 10:24:44 +02:00
Jonas Platte d4fe2fe0a2 Remove redundant braces 2020-07-25 02:32:50 +02:00
Jonas Platte 14db34beee Use Option::and_then over manual match 2020-07-25 02:32:18 +02:00
Jonas Platte 7aea6160c3 Flatten nested match for less indentation 2020-07-25 02:31:52 +02:00
Jonas Platte ca88539ec4 Upgrade ruma 2020-07-25 02:23:10 +02:00
Damir Jelić 670755bfce crypto: Start checking and cancelling our SAS flows. 2020-07-24 17:51:20 +02:00
Damir Jelić 46c1657643 crypto: Fix some clippy warnings. 2020-07-24 16:04:47 +02:00
Damir Jelić 9ac1417292 crypto: Add a higher level simple and threadsafe SAS object. 2020-07-24 15:49:00 +02:00
Damir Jelić de94b903d6 crypto: Rename the Sas struct. 2020-07-24 11:32:38 +02:00
Damir Jelić 2f28976694 crypto: Make the Sas struct thread safe. 2020-07-24 11:26:45 +02:00
Damir Jelić 8ff8ea1342 crypto: Add docs for the SAS structs and methods. 2020-07-23 17:25:57 +02:00
Damir Jelić a1edef0ed5 crypto: Fix some clippy warnings. 2020-07-23 14:47:47 +02:00
Damir Jelić ee51ed78be crypto: Allow users to check the SAS even after a mac event was received. 2020-07-23 14:35:29 +02:00
Damir Jelić 2729f01e0f crypto: Move the emoji/decimal sas calculation out of the Sas object. 2020-07-23 14:26:50 +02:00
Damir Jelić e6730a7007 crypto: More SAS refactoring. 2020-07-23 14:14:29 +02:00
Damir Jelić 6fd852d573 crypto: Refactor out some common SAS methods. 2020-07-23 14:02:07 +02:00
Damir Jelić 7f2b268a59 Merge branch 'master' into sas-verification 2020-07-23 13:43:01 +02:00
Damir Jelić bb9adea5de crypto: Implement the whole SAS flow. 2020-07-23 13:41:57 +02:00
Damir Jelić b1ae5534a1 crypto: Hold a copy of the account to get the ed25519 key when doing SAS. 2020-07-23 11:19:19 +02:00
Damir Jelić 9214f01185 cyrpto: Fill out the method to get the MacEventContent. 2020-07-23 11:08:09 +02:00
Damir Jelić c35f73473e crypto: Add a copyright header to the sas file. 2020-07-23 09:21:11 +02:00
Jonas Platte bf54b17a2f Upgrade ruma 2020-07-22 22:31:42 +02:00
Damir Jelić 4ce26f4fa0 crypto: Add support to get the SAS emoji out of a verification. 2020-07-22 16:41:16 +02:00
Damir Jelić cdcbcdfab3 crypto: Add support to display the decimal SAS value. 2020-07-22 15:11:34 +02:00
Damir Jelić 7a2d5c30db crypto: More Sas states and add an initial test. 2020-07-22 13:43:11 +02:00
Damir Jelić a7bc1a95d3 device: Add a method to create a Device from an Account. 2020-07-22 13:41:49 +02:00
Damir Jelić 4fa58bfaac crypto: Add getters for the user and device id in the account. 2020-07-22 13:40:47 +02:00
Damir Jelić 7c92d91c04 crypto: Use a patched olm-rs for now. 2020-07-22 11:39:30 +02:00
Damir Jelić e612326714 Merge branch 'master' into sas-verification 2020-07-22 11:30:58 +02:00
Damir Jelić 9ef784d665 crypto: Simplify the OlmMachine -> Device conversion. 2020-07-22 09:27:43 +02:00
Damir Jelić 2481fbbd27 crypto: Store the device signatures with the devices as well. 2020-07-21 17:33:47 +02:00
Damir Jelić a9d645cbcd crypto: Rewrite the device keys fetching in the SQLiteStore using filter_map. 2020-07-21 16:46:11 +02:00
Damir Jelić 578c927e58 crypto: Simplify the share_group_session method. 2020-07-21 14:13:10 +02:00
Damir Jelić 24baf1fe0f crypto: More doc fixes. 2020-07-21 13:04:51 +02:00
Damir Jelić 861c07d5ce cyrpto: Fix the docs for the Session encrypt method. 2020-07-21 12:59:15 +02:00
Damir Jelić 451d902604 crypto: Allow that many arguments on the from_pickle session method. 2020-07-21 12:57:31 +02:00
Damir Jelić c3f00c96f8 crypto: Don't require the account to be passed when encrypting. 2020-07-21 12:46:06 +02:00
Damir Jelić e50cf39a17 crypto: Store a copy of the user_id/device_id and identity keys in sessions. 2020-07-21 12:40:23 +02:00
Damir Jelić 3f1439fe28 crypto: Move the olm encryption logic into the Session struct. 2020-07-21 12:03:05 +02:00
Damir Jelić 3d6872607e crypto: Move the m.room_key content creation into the outbound group session. 2020-07-21 11:12:20 +02:00
Damir Jelić fe33430e9b crypto: Use DeviceId instead of str everywhere. 2020-07-21 10:48:15 +02:00
Damir Jelić b22324b305 crypto: Split out the olm module into separate files. 2020-07-21 10:38:14 +02:00
Devin R 037d62b165 matrix-sdk-crypto: Remove map clone from user_devices 2020-07-20 08:10:42 -04:00
Devin R 8c39db002b Remove inaccurate comment about DeviceId 2020-07-18 08:52:51 -04:00
Devin R e27b6fb51e matrix-sdk-crypto: Fix map_clone clippy warning 2020-07-18 08:52:51 -04:00
Devin R e4f94cbfec Remove FullOrRedacted use ruma::AnyPossiblyRedacted event enum 2020-07-18 08:52:51 -04:00
Devin R 807435c043 Updates DeviceId to be Box<DeviceId> 2020-07-18 08:51:19 -04:00
Devin R 71f2a042c2 Rename Stub -> Sync for all ruma events 2020-07-18 08:37:43 -04:00
Devin R 2e8fc3e232 matrix-sdk-base: Integrate redacted events into message queue
Redact message events according to spec and ruma types. Remove content
using events redact() method and insert the redacting event into the
event being redacted.
2020-07-17 13:41:55 -04:00
Damir Jelić d273786d83 matrix-sdk: Bump our dependencies. 2020-07-17 10:01:22 +02:00
Damir Jelić 7ddc785a9a Merge branch 'timeout' 2020-07-17 09:59:21 +02:00
Damir Jelić 3e23affc9e Merge branch 'encombhat-master' 2020-07-17 09:41:29 +02:00
Stephen f2163164bf Wasm fix 2020-07-16 20:53:09 -03:00
Stephen 44dfbd2fa6 Fix 2020-07-16 20:21:34 -03:00
Stephen 2f99d0de59 Bugfix 2020-07-16 20:06:26 -03:00
Black Hat 7a72949613 Client::sync_forever(): Add filter in next iteration. 2020-07-16 15:55:55 -07:00
Stephen b0241e51a3 Fixed formatting 2020-07-16 18:40:52 -03:00
Stephen c5ea4fde35 HTTP timeout 2020-07-16 18:16:30 -03:00
Black Hat cc4ae3db1e Client::SyncSettings: Include sync filter 2020-07-16 06:13:35 -07:00
Black Hat a5c5f5a7b1 Revert "Client::sync(): expose sync filter"
This reverts commit 0542e3d83d.
2020-07-16 06:04:26 -07:00
Damir Jelić 7c46953805 travis: Don't allow failures for Windows. 2020-07-16 14:23:36 +02:00
Damir Jelić 2d83c40626 travis: Test the base client on Windows as well. 2020-07-16 14:09:40 +02:00
Damir Jelić d75101d042 travis: Run the tests on Windows. 2020-07-16 14:09:40 +02:00
Damir Jelić 04bb65f43e travis: Don't test encyrption support on Windows for now. 2020-07-16 14:09:40 +02:00
Damir Jelić c1ffed4fc9 base: Sanitize the room id for the path of the state store.
This closes: #71.
2020-07-16 14:08:56 +02:00
Black Hat 0542e3d83d Client::sync(): expose sync filter 2020-07-16 03:49:46 -07:00
Damir Jelić 2d955027e1 travis: Don't set the linux version. 2020-07-15 16:11:08 +02:00
Damir Jelić 38166135dc travis: Add a clippy stage. 2020-07-15 15:59:04 +02:00
Damir Jelić 5bebe1d434 crypto: Clippy fixes for our tests. 2020-07-15 15:58:36 +02:00
Damir Jelić a2a87b9fff matrix-sdk: Fix a bunch of clippy warnings. 2020-07-15 15:53:17 +02:00
Damir Jelić 497b973eb5 travis: Test newer macOS versions. 2020-07-15 15:46:17 +02:00
Damir Jelić de1988265d crypto: Move the outbound session creation logic into the account. 2020-07-15 15:39:56 +02:00
Damir Jelić 6cced25ae1 travis: Don't allow failures on the wasm target. 2020-07-15 14:13:43 +02:00
Damir Jelić 4e40c13b81 travis: Fix the minimal build. 2020-07-15 14:05:52 +02:00
Damir Jelić bf152df322 matrix-sdk: Hide some tracing imports behind the encryption flag. 2020-07-15 14:05:22 +02:00
Damir Jelić 3315cf5bc6 travis: Add a build that doesn't use any of the features. 2020-07-15 13:23:28 +02:00
Damir Jelić 204279c575 matrix-sdk: Don't hide the tracing import behind the encryption feature. 2020-07-15 13:19:56 +02:00
Damir Jelić 83806b42e9 crypto: Remove a stale comment about clearing private keys from events. 2020-07-15 13:07:48 +02:00
Damir Jelić fa0a22b090 Merge branch 'dkasak-master'
High-level summary of changes:

- Rewrite the disambiguation algorithm to simplify it.
- Fixes to state tracking, e.g. use `state_key` instead of `user_id` when
  determining which member an event is acting on.
- Changes to `RoomMember`:
  * Make `RoomMember` "dumber" and don't let it mutate itself. This came about
    primarily because `update_profile` cannot live on `RoomMember` because it
    needs some information from `Room`. The other few mutating methods then
    looked odd so it seemed best to move them to `Room` so that the room takes
    care of updating its members.
  * Each `RoomMember` now contains all information to calculate its set of
    names:
      + `.name()` (short/ergonomic but potentially ambiguous),
      + `.unique_name()` (unique but may be contain MXID when not necessary),
      + `.disambiguated_name()` (shortest possible while being unique).
- Add some logging using the `tracing` crate.
- Improvements to `EventBuilder`:
  * Add a docstring.
  * Make it clear itself when building a sync response so the same builder can
    be reused for later sync responses.
- A few tests.
2020-07-15 12:52:25 +02:00
Denis Kasak bce7fe0217 Equivalence class -> equivalence set. 2020-07-15 11:58:52 +02:00
Denis Kasak 1fd21ee206 Fix docstrings regarding return value related to disambiguation. 2020-07-15 11:54:30 +02:00
Denis Kasak 8a4a4140b3 Remove stale comment. 2020-07-15 11:22:47 +02:00
Denis Kasak 62943f055d Rewrap docstrings and comments to 80 chars. 2020-07-15 11:21:01 +02:00
Denis Kasak ea149ebd8e Update docstring for disambiguation_updates. 2020-07-15 11:16:13 +02:00
Denis Kasak 32737a5517 Use match instead of if-let in sync_forever. 2020-07-15 09:43:58 +02:00
Damir Jelić 1691a26163 crypto: Add initial Sas scaffolding. 2020-07-14 17:04:08 +02:00
Damir Jelić 51012e632e crypto: Rename the StoreError to StoreResult. 2020-07-14 13:11:44 +02:00
Damir Jelić 5d76fd9aac crypto: Refactor the key query handling logic a bit. 2020-07-14 13:08:57 +02:00
Damir Jelić c25f4c0642 crypto: Verify one-time keys using the device. 2020-07-14 12:49:40 +02:00
Denis Kasak 9e48b7172b cargo fmt 2020-07-14 12:38:55 +02:00
Damir Jelić 68125f5de6 crypto: Refactor out the json verification method. 2020-07-14 12:23:42 +02:00
Damir Jelić b602d3007d crypto: Remove some useless mem::replace calls. 2020-07-14 12:03:27 +02:00
Damir Jelić 41cfbaf520 device: Store the device keys with the algorithm and device id.
This will ensure that we can check the signature of the device later on.
2020-07-14 12:00:29 +02:00
Damir Jelić 8206394918 crypto: Use AlgorithmAndDeviceId to get the device signature. 2020-07-14 11:27:50 +02:00
Damir Jelić ca85564a9f crypto: Move the device keys verificatin logic into the device. 2020-07-14 11:17:09 +02:00
Denis Kasak 7d9a699d62 Fix EventBuilder docstring example. 2020-07-14 11:08:16 +02:00
Damir Jelić a38efc0f29 room: Fix a clippy warning, use unwrap_or_else for the member counts. 2020-07-14 10:56:06 +02:00
Denis Kasak 048a2000e7 Merge 2020-07-13 17:10:13 +02:00
Damir Jelić 18b444aac5 crypto: Move the uploaded key count handing into the account. 2020-07-13 16:46:51 +02:00
Damir Jelić a7a9ac24ed crypto: Move the key count field into the account. 2020-07-13 15:49:16 +02:00
Damir Jelić b2ccb61864 crypto: Add the device id and identity keys to the megolm session.
This way we don't need to pass in the account to encrypt events.
2020-07-13 14:32:59 +02:00
Damir Jelić ac264918b8 crypto: Move the megolm decryption logic into the session. 2020-07-13 14:00:42 +02:00
Damir Jelić 8e19c583c6 crypto: Move the megolm encryption logic into the outbound group session. 2020-07-13 13:19:25 +02:00
Damir Jelić 740a5af068 Merge branch 'dan/bugfix/implable-StateStore' 2020-07-13 10:19:37 +02:00
Dan Enman 8c3855221c mark state::AllRooms and state::ClientState as public 2020-07-11 19:14:55 -03:00
Damir Jelić c2f1e4de64 crypto: Disable a clippy warning. 2020-07-11 23:15:10 +02:00
Damir Jelić 8a7c53c00d matrix-sdk: Remove the wrongly committed olm-sys/olm-rs patch defintions. 2020-07-11 22:18:01 +02:00
Damir Jelić c1ae183795 Merge branch 'deps-and-stuff' 2020-07-11 22:13:35 +02:00
Jonas Platte eea00301ff Remove immediately-deref'ed double references 2020-07-11 21:20:02 +02:00
Jonas Platte 85522ac35a Slightly simplify RoomName::calculate_name 2020-07-11 21:14:32 +02:00
Jonas Platte 9b5f95672b Use js_int macros to improve readability 2020-07-11 21:06:21 +02:00
Jonas Platte ffc5204109 Fix two pattern matching related warnings 2020-07-11 20:57:01 +02:00
Jonas Platte a607d70371 Upgrade mockito in matrix-sdk-base 2020-07-11 20:55:19 +02:00
Jonas Platte 1fcb68c59f Remove unused dependencies 2020-07-11 20:55:05 +02:00
Damir Jelić 9bceb2f539 crypto: Add the user id and device id to the account. 2020-07-11 17:23:50 +02:00
Damir Jelić 7003ea2d23 matrix-sdk-common: Depend on a working revision of the ruma mono repo. 2020-07-11 16:24:36 +02:00
Damir Jelić 18ccd30c8c crypto: Add a bunch of TODO lines documenting how to refactor stuff further. 2020-07-11 12:05:52 +02:00
Damir Jelić eb19c19e36 Merge branch 'perf-1' 2020-07-11 10:33:30 +02:00
Damir Jelić df2bcf6f1f crypto: Style fix for a doc comment. 2020-07-11 09:45:52 +02:00
Jonas Platte 3ee06be87b Rewrite MessageQueue deserialization to reduce allocations 2020-07-10 21:41:46 +02:00
Jonas Platte 3a07a17e9d Remove unnecessary calls to clone() 2020-07-10 21:24:01 +02:00
Damir Jelić 27eeeb8db6 crypto: Move the one-time key signing into the accoung. 2020-07-10 17:53:04 +02:00
Damir Jelić 6ded76a5a7 crypto: Move the device_keys() method into the account. 2020-07-10 17:10:34 +02:00
Denis Kasak 05a41d3b4d Move and rename member_display_name to RoomMember::disambiguated_name.
This makes more sense as all the required information is now available
on `RoomMember`. We also don't have to handle the case of the missing
member since now you have to actually get a `RoomMember` before you can
ask for their name.
2020-07-10 15:47:11 +02:00
Damir Jelić 58d79ca9c6 crypto: Put the user id and device id into the account. 2020-07-10 15:43:32 +02:00
Damir Jelić 4ee245dcce examples: Updat the autojoin example to use the ruma mono repo. 2020-07-10 15:30:17 +02:00
Denis Kasak 8daa12ac56 Print error when receiving invalid response in sync_forever. 2020-07-10 15:11:03 +02:00
Denis Kasak 4134ba969a DRY the membership logging a bit. 2020-07-10 15:11:03 +02:00
Denis Kasak a8f24da3ba cargo fmt 2020-07-10 15:11:03 +02:00
Denis Kasak 390a1aa12c Clarify member_display_name docstring. 2020-07-10 15:11:03 +02:00
Denis Kasak b16724841d Correct state tracking of room members.
- use `state_key` instead of `user_id` to determine which member is
  affected by the event
- assign state directly from the event in `add_member` instead of using
  `membership_change`
- expand/fix docstrings
- add some logging
2020-07-10 15:11:03 +02:00
Denis Kasak ec81a5e539 Implement Room::member_is_tracked. 2020-07-10 15:11:03 +02:00
Denis Kasak 949305da72 Clarify comment. 2020-07-10 15:11:03 +02:00
Denis Kasak 559306a33c Rewrite disambiguation algorithm to handle profile changes.
The new algorithm is simpler. Instead of tracking a list of
disambiguated display names in `Room`, we instead track the display name
ambiguity status in `RoomMember`. This allows a client to generate the
correct name for a member using solely the information available in
`RoomMember`.

The disambiguation algorithm itself now only calculates the set of members
whose ambiguity status had changed instead of producing disambiguated
display names for everyone affected. This is called on each room entry
(join or invite), room entry and profile change, and the updates are
propagated to the affected `RoomMember`s.
2020-07-10 15:11:01 +02:00
Denis Kasak 24d2aa8078 Style (cargo fmt, reordering import). 2020-07-10 15:07:21 +02:00
Denis Kasak e70929317a Revert "add_member provably always returns true."
This reverts commit 7943baee49.
2020-07-10 15:07:17 +02:00
Devin R 62eeb3707f Fix wasm test failure gate unknown import 2020-07-10 08:59:02 -04:00
Devin R 3fa06eeb99 matrix-sdk-base: Add test for MessageQueue/JsonStore interaction
Ruma can't currently handle an event with the wrong event content type.
When replacing the MessageEventStub's content it automatically
serializes as "m.room.redaction".
2020-07-10 08:59:02 -04:00
Devin R c0e6279837 matrix-sdk: Update request_builder to use new constructors
The create_room::Request and get_message_events::Request now have
constructors that we use in our builder structs.
2020-07-10 08:59:02 -04:00
Devin R e7c70854ab sdk_base: message events in message queue have content redacted
The MessageQueue holds MessageEventStub<AnyMessageEventContent> so when
a redaction event is encountered the redacted event's content can be
replaced. The Unsigned field redacted_because is also populated with the
redaction event itself with the addition of a room_id Stub -> full
event.
2020-07-10 08:59:02 -04:00
Devin R dcc3d6e755 sdk_base: Remove room_id as argument from all Room methods
Remove room_id paramater from some client methods. Make CreationContent
two methods of RoomBuilder. Add docs for MessageWrapper.
2020-07-10 08:59:02 -04:00
Devin R 2338d3e8fd matrix-sdk-base: clean up recv/iter joined post rebase
The types for account data in a sync response have changed, no longer
Option. Re word comment in hoist prev_content test.
2020-07-10 08:59:02 -04:00
Devin R b83b9dc59d matrix-sdk-base: Use new accessor methods for models/message.rs
ruma now has field access methods for all of the Any*Event enums use
them for MessageWrapper's AnyMessageEventStub contents.
2020-07-10 08:59:02 -04:00
Devin R 68822861d5 Rebase upstream/master into ruma-mono branch 2020-07-10 08:59:02 -04:00
Devin R b1e7bc77a4 Use ruma/ruma master, address review issues 2020-07-10 08:59:02 -04:00
Devin R eb5949dbc2 Move matrix-sdk to ruma monorepo 2020-07-10 08:59:00 -04:00
Denis Kasak 7943baee49 add_member provably always returns true. 2020-07-10 11:08:40 +02:00
Denis Kasak 7abdeed449 fix: Don't issue a disambiguation in case of a unique display name. 2020-07-10 11:08:40 +02:00
Denis Kasak eeebb43e32 Move mutating methods from RoomMember to Room.
The `update_profile` method cannot live in `RoomMember` since that
operation needs information which only exists in `Room` (for instance,
it needs other members in order to perform display name disambiguation).

Leaving other mutating methods on `RoomMember` (like `update_power` and
`update_presence`) then seemed illogical so they were all moved into
`Room`.

In addition, a small refactoring was done to remove
`did_update_presence` and `update_presence` since their existence
doesn't make much sense anymore and it saves us from repeating work.
Their function is now done in `receive_presence_event`.

Also, several docstrings were corrected and reworded.
2020-07-10 11:08:38 +02:00
Denis Kasak 5f49dab1fa Correct docstring. 2020-07-10 11:05:51 +02:00
Denis Kasak 6cacf83661 Add (failing) test for displayname disambiguation on profile updates. 2020-07-10 11:05:51 +02:00
Denis Kasak 599c1ba98f Add test to ensure member is only treated as joined or invited, not both. 2020-07-10 11:05:51 +02:00
Denis Kasak c2ec69cf44 Style fixes (comment grammar and correctness, whitespace). 2020-07-10 11:05:51 +02:00
Denis Kasak 32bdcede0c Small refactoring to simplify member_disambiguations. 2020-07-10 11:05:51 +02:00
Denis Kasak 9af48920f6 Add some TODOs and FIXMEs. 2020-07-10 11:05:51 +02:00
Damir Jelić a3441429da matrix-sdk: Add an autojoin example. 2020-07-08 20:22:50 +02:00
Damir Jelić 9f957655d0 travis: Allow the wasm target to fail. 2020-07-07 17:40:00 +02:00
Damir Jelić 2663a17065 travis: Allow windows to fail for now. 2020-07-07 17:19:26 +02:00
Damir Jelić 7124235d1b travis: Add a windows test target. 2020-07-07 16:57:20 +02:00
Damir Jelić babbdb4b90 travis: Remove redundant targets. 2020-07-07 16:52:17 +02:00
Damir Jelić 18f6cbc23a travis: Add a lint stage. 2020-07-07 16:48:45 +02:00
Damir Jelić 583dbb07a5 Merge branch 'deps-bump' 2020-07-07 16:13:25 +02:00
Damir Jelić 25207a1586 matrix-sdk: Make the wasm feature for future-timers target specific. 2020-07-07 16:11:33 +02:00
Damir Jelić 283cf0d782 matrix-sdk: Bump all our deps. 2020-07-07 15:52:08 +02:00
Damir Jelić 98d36d0ef0 base: Only update the tracked users when we're done with the state and timeline. 2020-07-07 15:48:28 +02:00
Damir Jelić f33298b1a6 matrix-sdk: Explain what needs to be done to restore a client. 2020-07-07 15:47:34 +02:00
Damir Jelić 11aa306de2 base: Swap around the store creation.
The state store creates directory structure but the crypto store does
not.

This can lead to confusing errors where we require a directory to be
created by the library user but it gets created by the library.
2020-07-06 10:54:01 +02:00
Damir Jelić 669a3f22d2 matrix-sdk: Allow getting the user id from the client. 2020-07-05 16:47:38 +02:00
Denis Kasak ff5f638b60 Remove member from invited_members when he joins. 2020-07-03 15:35:54 +02:00
Denis Kasak 2a0c6c6474 Add test and example event to ensure display name changes work correctly. 2020-07-03 15:35:54 +02:00
Denis Kasak f447c55fcb Move prev_content in test data to top level for now.
Until Ruma fixes it upstream, see hoist_room_event_prev_content for more
information.
2020-07-03 15:35:54 +02:00
Denis Kasak 84fc662614 Document and improve EventBuilder.
EventBuilder now clears itself between `build_sync_response` calls so
that each subsequent call will return an empty response if nothing was
added.

This allows reuse of a single EventBuilder instance which is important
for correct sync token rotation.
2020-07-03 15:35:54 +02:00
Denis Kasak c57f076375 Remove unused import. 2020-07-03 15:35:54 +02:00
Denis Kasak 4561b94f33 Remove outdated TODO. 2020-07-03 15:35:54 +02:00
Denis Kasak 9bd8699e18 Get rid of match on membership change in RoomMember::update_profile.
The calling method already did this when it determined that
update_profile should be called so we don't need to repeat it.
2020-07-03 15:35:54 +02:00
Denis Kasak 5ef9a7b924 tests: Rename get_room_id to test_room_id.
To make it more obvious it's a special room ID value used in tests.
2020-07-03 15:35:54 +02:00
Damir Jelić bd56c52b37 base: Don't double borrow the response in one iter rooms method. 2020-07-03 12:30:57 +02:00
Denis Kasak 1f25c4cf4b Fix and test hoisting of prev_content for timeline events.
The previous test only tested using the `EventEmitter`, which missed the
fact that the client was receiving unhoisted events. The test now also
tests the client state to detect this.
2020-07-03 11:54:08 +02:00
Denis Kasak 3f1a40a7d1 Add a bunch of FIXMEs to have receive_* methods do the emitting. 2020-07-03 10:31:47 +02:00
Damir Jelić b092ed0a82 base: Put the decrypted event replacing in the correct place. 2020-07-02 23:16:56 +02:00
Damir Jelić cd9252cc3d matrix-sdk: Remove an unused import. 2020-06-26 18:21:44 +02:00
Damir Jelić 8b13602b3b Merge branch 'room-search' 2020-06-26 10:15:31 +02:00
Devin R 92a43e7685 Move test data to test crate, fix docs 2020-06-25 08:31:51 -04:00
Damir Jelić 262a61afc9 crypto: Simplify the group session pair creation. 2020-06-25 13:31:30 +02:00
Devin R 1016519bb6 matrix_sdk: Rename public room builder and client methods
Remove 'get' from get_public_rooms* methods.
Rename RoomSearchBuilder -> RoomListFilterBuilder.
Use u32 over UInt in builders and Into<String> for String.
Fix docs of public room methods and builders.
2020-06-24 07:46:40 -04:00
Devin R 4dbe785bd7 matrix_sdk: Add get_public_rooms* methods to Client
This also adds a RoomSearchBuilder for making get_public_rooms_filtered
requests and a test for each method.
2020-06-24 06:54:45 -04:00
Damir Jelić 676d547161 matrix-sdk: Disable the tarpaulin skip lines since it fails to run with them. 2020-06-24 11:25:31 +02:00
Damir Jelić 6a670163d3 Merge branch 'feature/display-name' 2020-06-24 10:42:58 +02:00
Damir Jelić b8c4d1d5fa matrix-sdk: Remove the last test_data folder and fix the remaining tests. 2020-06-24 10:07:03 +02:00
Devin R 9e738f45ef crypto/base: Finish moving to using static json values for test data 2020-06-22 16:18:12 -04:00
Devin R 8e8ac8c5ac matrix_sdk_base: Use test_json values for tests in base 2020-06-21 14:22:28 -04:00
Devin R 4a7b3a103c matrix_sdk_test: Use static JSON values instead of reading files 2020-06-21 14:13:26 -04:00
Devin R fc077bcd6b matrix-sdk-test: Remove duplicate test_data folder, leave top-level 2020-06-21 14:13:03 -04:00
Denis Kasak c0c02baffc Run cargo fmt and apply clippy lints. 2020-06-20 13:05:16 +02:00
Denis Kasak 1174ccfc89 Merge branch 'master' into feature/display-name 2020-06-20 12:54:46 +02:00
Denis Kasak 733689870e Fix compilation error and remaining test.
Ref. for compilation error:
https://github.com/rust-lang/rust/issues/64552
2020-06-20 12:51:02 +02:00
Marcel 255451b8c7 Add missing dependency matrix-sdk-common-macros to matrix-sdk 2020-06-17 19:42:07 +02:00
Marcel d4087a1aae Fix cargo fmt issues that the local version didn't auto fix 2020-06-17 19:16:04 +02:00
Marcel f07ac5d679 Commit missing matrix_sdk_common_macros folder 2020-06-17 19:08:26 +02:00
Marcel 8b77b4171a Do wasm sepcific changes:
- Only use send+sync when not using wasm
- Use wasm capabale async_trait wrapper macro
- Make room and room_member specific structs always clonable
2020-06-17 18:57:39 +02:00
Damir Jelić ea427cf366 Merge branch 'upload-keys' 2020-06-17 09:33:09 +02:00
Devin R 15191d0230 crypto: Fix overflow in should_upload_keys, bail out if uploaded keys > max uploaded 2020-06-16 18:07:13 -04:00
Denis Kasak 5bd3c49afc Correctly handle disambiguation for exiting members, refactor and test. 2020-06-15 17:29:38 +02:00
Denis Kasak 765487dd9f Fix comment style. 2020-06-15 17:29:38 +02:00
Denis Kasak 03e53e991b Hoist prev_content to top-level in both timeline and state events.
Also refactor and document why this hoisting is needed.

This change makes the user_presence test fail because the hoisting
exposes an error encoded into the test's expected result.

Previously, the test expected 2 members in the room at the end. This is
incorrect since one of the members in the test data leaves the room.
However, since the prev_content of state events was previously not
hoisted to the top level, the `membership_change` method would not
notice it and thus not realize the member had left the room. The test
was corrected to expect only a single member in the room.

Another test change was made due to a limitation of EventBuilder: due to
the fact that it makes the test data go through a de/ser cycle, it
cannot easily hoist prev_content to the top level. Because of this, the
tests were change to put prev_content into the top level from the
outset.
2020-06-15 17:21:26 +02:00
Damir Jelić c3373f796b Merge branch 'export-base-error' 2020-06-15 09:47:51 +02:00
Damir Jelić 311e41ee0d matrix-sdk: Fix the author field in the cargo files. 2020-06-15 09:47:13 +02:00
Devin R f8b5fceeb1 matrix-sdk: Export matrix-sdk-base Error type as BaseError 2020-06-14 20:00:41 -04:00
Denis Kasak 97b1bb6004 Must not take our user into account when calculating room name. 2020-06-10 22:53:31 +02:00
Denis Kasak 331cb02266 Split joined/invited users and handle removing users. 2020-06-10 18:12:27 +02:00
Denis Kasak 7751605e37 Nix RoomMember::update_member and tracking membership.
After discussing with poljar, we concluded we don't actually need to
tracking membership state, since we won't be tracking users that
left (banned, kicked, disinvited).

The only thing we need to keep track of is the difference between joined
and invited users which will be dealt with in a separate commit.
2020-06-10 16:36:51 +02:00
Denis Kasak a0eaa9c364 Implement RoomMember::unique_name.
This gives us a name that is as ergonomic as possible while guaranteeing
it is unique.
2020-06-10 14:44:41 +02:00
Denis Kasak 241d456a81 Add RoomMember::name.
Returns the most ergonomic name for the member (either the display name
(if set) or the MXID).
2020-06-10 14:39:12 +02:00
Denis Kasak 3e5b6bb460 Style fixes. 2020-06-10 12:04:58 +02:00
Denis Kasak 5868c72662 Small refactor so we don't duplicate user_id creation. 2020-06-10 12:01:01 +02:00
Denis Kasak 4c184a30a2 Add doc comment to RoomName::calculate_name. 2020-06-10 00:28:56 +02:00
Denis Kasak e4977d1d2a Refactor member_display_name.
Make it more readable, add comments.
2020-06-09 23:02:01 +02:00
Denis Kasak ac069152b9 Retrieve user id from RoomMember instead of reconstructing. 2020-06-09 22:19:51 +02:00
Denis Kasak 82827542b7 fixup: explicit type annotations 2020-06-09 19:31:01 +02:00
Denis Kasak 20a8e8e49b Fix comment styling. 2020-06-09 19:24:00 +02:00
Denis Kasak 098cc1f9f8 Add explicit type annotation. 2020-06-09 19:08:14 +02:00
Denis Kasak a3c46c6144 Run cargo fmt. 2020-06-09 16:41:26 +02:00
Damir Jelić f35fbdf8b0 Merge branch 'register' 2020-06-09 16:30:01 +02:00
Damir Jelić 442464add6 matrix-sdk: Implement sending of Http DELETE requests. 2020-06-09 16:29:17 +02:00
Damir Jelić abe40dff11 matrix_sdk: Remove code duplication in our send methods. 2020-06-09 16:28:54 +02:00
Denis Kasak b93eb0e318 Make Room::member_display_name return MXID as fallback.
If there is no display name set. This means the method can now always
return something so there is no need to wrap in an `Option`.
2020-06-09 16:16:21 +02:00
Denis Kasak e6b67e5fa7 Add short explanation to Room::member_display_name. 2020-06-09 15:35:43 +02:00
Denis Kasak 22ba253103 Use "disambiguated" instead of "resolved" display name in the doc comment.
To match how the C2S spec calls it.
2020-06-09 15:29:37 +02:00
Denis Kasak a9fd63fd4b Fix display name disambiguation so it passes the test. 2020-06-09 15:20:21 +02:00
Denis Kasak 60a43439e5 Properly test for display name disambiguation. 2020-06-09 15:20:21 +02:00
Denis Kasak b6d7939685 matrix-sdk: Vary sync token with each EventBuilder::build_sync_response call.
This allows us to hold onto an EventBuilder object and use it to build
multiple sync responses. Previously this would have resulted in each
of the responses having the same next_batch sync token. This would make
clients ignore the latter responses if they have already received any of
the previous ones.
2020-06-09 15:20:21 +02:00
Denis Kasak 4df0a839aa Fix Markdown in doc comment. 2020-06-09 15:20:21 +02:00
Denis Kasak e3cb3566bf Rename display_names -> disambiguated_display_names. 2020-06-09 15:20:21 +02:00
Valentin Brandl 9f34615869 Add first test for display names 2020-06-09 12:33:24 +02:00
Valentin Brandl 05503b28b7 Only add name duplicates to the display name map 2020-06-09 12:33:24 +02:00
Valentin Brandl 49e913865d Fix failing test 2020-06-09 12:33:06 +02:00
Valentin Brandl 4675a72e6b Rename accessor for display name 2020-06-09 12:30:12 +02:00
Valentin Brandl d5f66631c1 Implement display name resolving 2020-06-09 12:30:12 +02:00
Devin R 81baca2f92 base_client: emit typing events and test using EventEmitter 2020-06-06 17:00:29 -04:00
Damir Jelić 6e5870bd2b crypto: Simplify the max keys calculation for one-time key uploads. 2020-06-04 17:36:33 +02:00
Devin R 6df1f12b45 async_client: add docs/test for register_user, send_uiaa and RegistrationBuilder 2020-06-02 17:13:29 -04:00
Devin R 5abac19b72 request_builder/async_client: add register endpoint and RegistrationBuilder for making the request 2020-06-02 17:13:01 -04:00
Damir Jelić 62e959a94d Merge branch 'expose-send' 2020-06-02 11:20:47 +02:00
Damir Jelić 54871f2af9 matrix-sdk: Make the example for the send method comiple. 2020-06-02 11:15:04 +02:00
Marcel 6a323525b5 Add example to the Client::send() doccomment 2020-06-02 10:40:50 +02:00
Marcel 1d00f79675 Run cargo fmt for the get_profiles example 2020-06-02 10:40:32 +02:00
Marcel 7201749280 Add small example on how to use Client::send 2020-06-02 10:39:50 +02:00
Damir Jelić 5175cd8ddb crypto: Remove some unnecessary mem::replace calls. 2020-06-02 10:36:51 +02:00
Damir Jelić 21b33f4e61 Merge branch 'doc-fix' 2020-06-02 10:31:09 +02:00
Damir Jelić 9f34b371be Merge branch 'unify-ee-methods' 2020-06-02 10:30:20 +02:00
Damir Jelić 587614cdd7 Merge branch 'unrecognized' 2020-06-02 10:28:57 +02:00
Devin R db38bf1276 event_emitter: use enum to represent custom events and raw json 2020-06-01 17:02:12 -04:00
Devin R 761071dac5 base_client: fix doc grammer and consistency, group request methods together 2020-06-01 07:50:45 -04:00
Devin R 8f017e7b27 event-emitter: rename on_account_data_* -> on_non_room_* 2020-06-01 07:13:57 -04:00
Devin R b1864887aa matrix-sdk: enable messages feature by default 2020-06-01 06:45:38 -04:00
Devin R 9cb86596d8 add support for custom events and unrecognized by ruma events, test new code 2020-05-29 17:36:58 -04:00
Damir Jelić 8ee6c3bdc8 matrix-sdk: Don't require Send for the sync callback. 2020-05-29 09:39:17 +02:00
Damir Jelić 16f4021800 common: Depend on the git version of futures-locks again. 2020-05-26 22:21:03 +02:00
Emi Simpson 53876ea6e8 Make Client::send a public method, add a short doccomment 2020-05-20 14:24:35 -04:00
174 changed files with 24401 additions and 9169 deletions
+103
View File
@@ -0,0 +1,103 @@
name: CI
on:
push:
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
style:
name: Check style
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: rustfmt
profile: minimal
override: true
- name: Cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Run clippy
needs: [style]
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
profile: minimal
override: true
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets -- -D warnings
test:
name: ${{ matrix.name }}
needs: [clippy]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
name:
- linux / stable
- linux / beta
- macOS / stable
- windows / stable-x86_64-msvc
include:
- name: linux / stable
- name: linux / beta
rust: beta
- name: macOS / stable
os: macOS-latest
- name: windows / stable-x86_64-msvc
os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust || 'stable' }}
target: ${{ matrix.target }}
profile: minimal
override: true
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
+39
View File
@@ -0,0 +1,39 @@
name: Code coverage
on:
push:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
code_coverage:
name: Code Coverage
runs-on: "ubuntu-latest"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Install tarpaulin
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-tarpaulin -f
- name: Run tarpaulin
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
- name: Upload to codecov.io
uses: codecov/codecov-action@v1
+2
View File
@@ -1,2 +1,4 @@
Cargo.lock
target
master.zip
emsdk-*
+45 -9
View File
@@ -6,17 +6,59 @@ addons:
- libssl-dev
jobs:
allow_failures:
- os: osx
name: macOS 10.15
include:
- os: linux
dist: bionic
- stage: Format
os: linux
before_script:
- rustup component add rustfmt
script:
- cargo fmt --all -- --check
- stage: Clippy
os: linux
before_script:
- rustup component add clippy
script:
- cargo clippy --all-targets -- -D warnings
- stage: Test
os: linux
- os: windows
script:
- cd matrix_sdk
- cargo test --no-default-features --features "messages, native-tls"
- cd ../matrix_sdk_base
- cargo test --no-default-features --features "messages"
- os: osx
- os: linux
name: native-tls build
script:
- cd matrix_sdk
- cargo build --no-default-features --features "native-tls"
- os: linux
name: rustls-tls build
script:
- cd matrix_sdk
- cargo build --no-default-features --features "rustls-tls"
- os: osx
name: macOS 10.15
osx_image: xcode12
- os: linux
name: Coverage
before_script:
- cargo install cargo-tarpaulin
script:
- cargo tarpaulin --out Xml
- cargo tarpaulin --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
after_success:
- bash <(curl -s https://codecov.io/bash)
@@ -42,12 +84,6 @@ jobs:
cd matrix_sdk_base
cargo test --target wasm32-unknown-unknown --no-default-features
before_script:
- rustup component add rustfmt
script:
- cargo fmt --all -- --check
- cargo build
- cargo test
+1 -1
View File
@@ -1,4 +1,4 @@
[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk)
![Build Status](https://img.shields.io/github/workflow/status/matrix-org/matrix-rust-sdk/CI?style=flat-square)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/master.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
+54 -24
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "A high level Matrix client-server library."
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,45 +8,75 @@ license = "Apache-2.0"
name = "matrix-sdk"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.2.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = ["encryption", "sqlite-cryptostore"]
default = ["encryption", "sqlite_cryptostore", "messages", "native-tls"]
messages = ["matrix-sdk-base/messages"]
encryption = ["matrix-sdk-base/encryption"]
sqlite-cryptostore = ["matrix-sdk-base/sqlite-cryptostore"]
encryption = ["matrix-sdk-base/encryption", "dashmap"]
sqlite_cryptostore = ["matrix-sdk-base/sqlite_cryptostore"]
unstable-synapse-quirks = ["matrix-sdk-base/unstable-synapse-quirks"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
socks = ["reqwest/socks"]
docs = ["encryption", "sqlite_cryptostore", "messages"]
[dependencies]
http = "0.2.1"
reqwest = "0.10.4"
serde_json = "1.0.53"
thiserror = "1.0.19"
tracing = "0.1.14"
url = "2.1.1"
futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] }
dashmap = { version = "4.0.1", optional = true }
http = "0.2.2"
serde_json = "1.0.61"
thiserror = "1.0.23"
tracing = "0.1.22"
url = "2.2.0"
zeroize = "1.2.0"
mime = "0.3.16"
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
[dependencies.matrix-sdk-base]
version = "0.1.0"
version = "0.2.0"
path = "../matrix_sdk_base"
default_features = false
[dependencies.reqwest]
version = "0.10.10"
default_features = false
[dependencies.tracing-futures]
version = "0.2.4"
default-features = false
features = ["std", "std-future"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
futures-timer = "3.0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.24"
default-features = false
features = ["fs", "blocking"]
[target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer]
version = "3.0.2"
features = ["wasm-bindgen"]
[dev-dependencies]
async-trait = "0.1.31"
dirs = "2.0.2"
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
serde_json = "1.0.53"
tracing-subscriber = "0.2.5"
async-std = { version = "1.8.0", features = ["unstable"] }
dirs = "3.0.1"
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
serde_json = "1.0.61"
tracing-subscriber = "0.2.15"
tempfile = "3.1.0"
mockito = "0.25.1"
mockito = "0.28.0"
lazy_static = "1.4.0"
futures = "0.3.5"
futures = "0.3.8"
[[example]]
name = "emoji_verification"
required-features = ["encryption"]
+106
View File
@@ -0,0 +1,106 @@
use std::{env, process::exit};
use tokio::time::{delay_for, Duration};
use matrix_sdk::{
self, async_trait,
events::{room::member::MemberEventContent, StrippedStateEvent},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
};
use url::Url;
struct AutoJoinBot {
client: Client,
}
impl AutoJoinBot {
pub fn new(client: Client) -> Self {
Self { client }
}
}
#[async_trait]
impl EventEmitter for AutoJoinBot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
if room_member.state_key != self.client.user_id().await.unwrap() {
return;
}
if let SyncRoom::Invited(room) = room {
let room = room.read().await;
println!("Autojoining room {}", room.room_id);
let mut delay = 2;
while let Err(err) = self.client.join_room_by_id(&room.room_id).await {
// retry autojoin due to synapse sending invites, before the
// invited user can join for more information see
// https://github.com/matrix-org/synapse/issues/4345
eprintln!(
"Failed to join room {} ({:?}), retrying in {}s",
room.room_id, err, delay
);
delay_for(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
eprintln!("Can't join room {} ({:?})", room.room_id, err);
break;
}
}
println!("Successfully joined room {}", room.room_id);
}
}
}
async fn login_and_sync(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let mut home = dirs::home_dir().expect("no home directory found");
home.push("autojoin_bot");
let client_config = ClientConfig::new().store_path(home);
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(username, password, None, Some("autojoin bot"))
.await?;
println!("logged in as {}", username);
client
.add_event_emitter(Box::new(AutoJoinBot::new(client.clone())))
.await;
client.sync(SyncSettings::default()).await;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login_and_sync(homeserver_url, &username, &password).await?;
Ok(())
}
+17 -22
View File
@@ -1,15 +1,18 @@
use std::{env, process::exit};
use matrix_sdk::{
self,
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
self, async_trait,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
AnyMessageEventContent, SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
};
use url::Url;
struct CommandBot {
/// This clone of the `Client` will send requests to the server,
/// while the other keeps us in sync with the server using `sync_forever`.
/// while the other keeps us in sync with the server using `sync`.
client: Client,
}
@@ -19,11 +22,11 @@ impl CommandBot {
}
}
#[async_trait::async_trait]
#[async_trait]
impl EventEmitter for CommandBot {
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
let msg_body = if let MessageEvent {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
..
} = event
@@ -34,12 +37,9 @@ impl EventEmitter for CommandBot {
};
if msg_body.contains("!party") {
let content = MessageEventContent::Text(TextMessageEventContent {
body: "🎉🎊🥳 let's PARTY!! 🥳🎊🎉".to_string(),
format: None,
formatted_body: None,
relates_to: None,
});
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
));
// we clone here to hold the lock for as little time as possible.
let room_id = room.read().await.room_id.clone();
@@ -78,12 +78,7 @@ async fn login_and_sync(
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(
username.clone(),
password,
None,
Some("command bot".to_string()),
)
.login(&username, &password, None, Some("command bot"))
.await?;
println!("logged in as {}", username);
@@ -91,18 +86,18 @@ async fn login_and_sync(
// An initial sync to set up state and so our bot doesn't respond to old messages.
// If the `StateStore` finds saved state in the location given the initial sync will
// be skipped in favor of loading state from the store
client.sync(SyncSettings::default()).await.unwrap();
client.sync_once(SyncSettings::default()).await.unwrap();
// add our CommandBot to be notified of incoming messages, we do this after the initial
// sync to avoid responding to messages before the bot was running.
client
.add_event_emitter(Box::new(CommandBot::new(client.clone())))
.await;
// since we called sync before we `sync_forever` we must pass that sync token to
// `sync_forever`
// since we called `sync_once` before we entered our sync loop we must pass
// that sync token to `sync`
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
// this keeps state from the server streaming in to CommandBot via the EventEmitter trait
client.sync_forever(settings, |_| async {}).await;
client.sync(settings).await;
Ok(())
}
@@ -0,0 +1,124 @@
use std::{
collections::BTreeMap,
env, io,
process::exit,
sync::atomic::{AtomicBool, Ordering},
};
use serde_json::json;
use url::Url;
use matrix_sdk::{
self, api::r0::uiaa::AuthData, identifiers::UserId, Client, ClientConfig, LoopCtrl,
SyncSettings,
};
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
let mut auth_parameters = BTreeMap::new();
let identifier = json!({
"type": "m.id.user",
"user": user,
});
auth_parameters.insert("identifier".to_owned(), identifier);
auth_parameters.insert("password".to_owned(), password.to_owned().into());
// This is needed because of https://github.com/matrix-org/synapse/issues/5665
auth_parameters.insert("user".to_owned(), user.as_str().into());
AuthData::DirectRequest {
kind: "m.login.password",
auth_parameters,
session,
}
}
async fn bootstrap(client: Client, user_id: UserId, password: String) {
println!("Bootstrapping a new cross signing identity, press enter to continue.");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("error: unable to read user input");
#[cfg(feature = "encryption")]
if let Err(e) = client.bootstrap_cross_signing(None).await {
if let Some(response) = e.uiaa_response() {
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
client
.bootstrap_cross_signing(Some(auth_data))
.await
.expect("Couldn't bootstrap cross signing")
} else {
panic!("Error durign cross signing bootstrap {:#?}", e);
}
}
#[cfg(not(feature = "encryption"))]
panic!("Cross signing requires the encryption feature to be enabled");
}
async fn login(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.disable_ssl_verification()
.proxy("http://localhost:8080")
.unwrap();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
let response = client
.login(username, password, None, Some("rust-sdk"))
.await?;
let user_id = &response.user_id;
let client_ref = &client;
let asked = AtomicBool::new(false);
let asked_ref = &asked;
client
.sync_with_callback(SyncSettings::new(), |_| async move {
let asked = asked_ref;
let client = &client_ref;
let user_id = &user_id;
let password = &password;
// Wait for sync to be done then ask the user to bootstrap.
if !asked.load(Ordering::SeqCst) {
tokio::spawn(bootstrap(
(*client).clone(),
(*user_id).clone(),
password.to_string(),
));
}
asked.store(true, Ordering::SeqCst);
LoopCtrl::Continue
})
.await;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login(homeserver_url, &username, &password).await
}
+145
View File
@@ -0,0 +1,145 @@
use std::{env, io, process::exit};
use url::Url;
use matrix_sdk::{
self, events::AnyToDeviceEvent, identifiers::UserId, Client, ClientConfig, LoopCtrl, Sas,
SyncSettings,
};
async fn wait_for_confirmation(client: Client, sas: Sas) {
println!("Does the emoji match: {:?}", sas.emoji());
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("error: unable to read user input");
match input.trim().to_lowercase().as_ref() {
"yes" | "true" | "ok" => {
sas.confirm().await.unwrap();
if sas.is_done() {
print_result(&sas);
print_devices(sas.other_device().user_id(), &client).await;
}
}
_ => sas.cancel().await.unwrap(),
}
}
fn print_result(sas: &Sas) {
let device = sas.other_device();
println!(
"Successfully verified device {} {} {:?}",
device.user_id(),
device.device_id(),
device.local_trust_state()
);
}
async fn print_devices(user_id: &UserId, client: &Client) {
println!("Devices of user {}", user_id);
for device in client.get_user_devices(user_id).await.unwrap().devices() {
println!(
" {:<10} {:<30} {:<}",
device.device_id(),
device.display_name().as_deref().unwrap_or_default(),
device.is_trusted()
);
}
}
async fn login(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.disable_ssl_verification()
.proxy("http://localhost:8080")
.unwrap();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(username, password, None, Some("rust-sdk"))
.await?;
let client_ref = &client;
client
.sync_with_callback(SyncSettings::new(), |response| async move {
let client = &client_ref;
for event in &response.to_device.events {
let e = event
.deserialize()
.expect("Can't deserialize to-device event");
match e {
AnyToDeviceEvent::KeyVerificationStart(e) => {
let sas = client
.get_verification(&e.content.transaction_id)
.await
.expect("Sas object wasn't created");
println!(
"Starting verification with {} {}",
&sas.other_device().user_id(),
&sas.other_device().device_id()
);
print_devices(&e.sender, &client).await;
sas.accept().await.unwrap();
}
AnyToDeviceEvent::KeyVerificationKey(e) => {
let sas = client
.get_verification(&e.content.transaction_id)
.await
.expect("Sas object wasn't created");
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
}
AnyToDeviceEvent::KeyVerificationMac(e) => {
let sas = client
.get_verification(&e.content.transaction_id)
.await
.expect("Sas object wasn't created");
if sas.is_done() {
print_result(&sas);
print_devices(&e.sender, &client).await;
}
}
_ => (),
}
}
LoopCtrl::Continue
})
.await;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login(homeserver_url, &username, &password).await
}
+76
View File
@@ -0,0 +1,76 @@
use std::{convert::TryFrom, env, process::exit};
use url::Url;
use matrix_sdk::{
self, api::r0::profile, identifiers::UserId, Client, ClientConfig, Result as MatrixResult,
};
#[derive(Debug)]
struct UserProfile {
avatar_url: Option<String>,
displayname: Option<String>,
}
/// This function calls the GET profile endpoint
/// Spec: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-profile-userid
/// Ruma: https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/index.html
async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult<UserProfile> {
// First construct the request you want to make
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/index.html for all available Endpoints
let request = profile::get_profile::Request::new(mxid);
// Start the request using matrix_sdk::Client::send
let resp = client.send(request).await?;
// Use the response and construct a UserProfile struct.
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/struct.Response.html
// for details on the Response for this Request
let user_profile = UserProfile {
avatar_url: resp.avatar_url,
displayname: resp.displayname,
};
Ok(user_profile)
}
async fn login(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<Client, matrix_sdk::Error> {
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(username, password, None, Some("rust-sdk"))
.await?;
Ok(client)
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <mxid> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
let client = login(homeserver_url, &username, &password).await?;
let user_id = UserId::try_from(username).expect("Couldn't parse the MXID");
let profile = get_profile(client, &user_id).await?;
println!("{:#?}", profile);
Ok(())
}
+122
View File
@@ -0,0 +1,122 @@
use std::{
env,
fs::File,
io::{Seek, SeekFrom},
path::PathBuf,
process::exit,
sync::Arc,
};
use tokio::sync::Mutex;
use matrix_sdk::{
self, async_trait,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
};
use url::Url;
struct ImageBot {
client: Client,
image: Arc<Mutex<File>>,
}
impl ImageBot {
pub fn new(client: Client, image: File) -> Self {
let image = Arc::new(Mutex::new(image));
Self { client, image }
}
}
#[async_trait]
impl EventEmitter for ImageBot {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
..
} = event
{
msg_body
} else {
return;
};
if msg_body.contains("!image") {
let room_id = room.read().await.room_id.clone();
println!("sending image");
let mut image = self.image.lock().await;
self.client
.room_send_attachment(&room_id, "cat", &mime::IMAGE_JPEG, &mut *image, None)
.await
.unwrap();
image.seek(SeekFrom::Start(0)).unwrap();
println!("message sent");
}
}
}
}
async fn login_and_sync(
homeserver_url: String,
username: String,
password: String,
image: File,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(&username, &password, None, Some("command bot"))
.await?;
client.sync_once(SyncSettings::default()).await.unwrap();
client
.add_event_emitter(Box::new(ImageBot::new(client.clone(), image)))
.await;
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client.sync(settings).await;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password, image_path) = match (
env::args().nth(1),
env::args().nth(2),
env::args().nth(3),
env::args().nth(4),
) {
(Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password> <image>",
env::args().next().unwrap()
);
exit(1)
}
};
println!(
"helloooo {} {} {} {:#?}",
homeserver_url, username, password, image_path
);
let path = PathBuf::from(image_path);
let image = File::open(path).expect("Can't open image file.");
login_and_sync(homeserver_url, username, password, image).await?;
Ok(())
}
+15 -16
View File
@@ -2,18 +2,21 @@ use std::{env, process::exit};
use url::Url;
use matrix_sdk::{
self,
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
self, async_trait,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
};
struct EventCallback;
#[async_trait::async_trait]
#[async_trait]
impl EventEmitter for EventCallback {
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
if let MessageEvent {
if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
sender,
..
@@ -23,12 +26,8 @@ impl EventEmitter for EventCallback {
// any reads should be held for the shortest time possible to
// avoid dead locks
let room = room.read().await;
let member = room.members.get(&sender).unwrap();
member
.display_name
.as_ref()
.map(ToString::to_string)
.unwrap_or(sender.to_string())
let member = room.joined_members.get(&sender).unwrap();
member.name()
};
println!("{}: {}", name, msg_body);
}
@@ -38,8 +37,8 @@ impl EventEmitter for EventCallback {
async fn login(
homeserver_url: String,
username: String,
password: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
@@ -50,9 +49,9 @@ async fn login(
client.add_event_emitter(Box::new(EventCallback)).await;
client
.login(username, password, None, Some("rust-sdk".to_string()))
.login(username, password, None, Some("rust-sdk"))
.await?;
client.sync_forever(SyncSettings::new(), |_| async {}).await;
client.sync(SyncSettings::new()).await;
Ok(())
}
@@ -73,5 +72,5 @@ async fn main() -> Result<(), matrix_sdk::Error> {
}
};
login(homeserver_url, username, password).await
login(homeserver_url, &username, &password).await
}
@@ -10,7 +10,7 @@ edition = "2018"
crate-type = ["cdylib"]
[dependencies]
matrix-sdk = { path = "../..", default-features = false }
matrix-sdk = { path = "../..", default-features = false, features = ["native-tls"] }
url = "2.1.1"
wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.12"
+25 -13
View File
@@ -1,9 +1,13 @@
#![type_length_limit = "1702124"]
use matrix_sdk::{
api::r0::sync::sync_events::Response as SyncResponse,
events::collections::all::RoomEvent,
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
events::{
room::message::{MessageEventContent, TextMessageEventContent},
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
},
identifiers::RoomId,
Client, ClientConfig, SyncSettings,
Client, ClientConfig, LoopCtrl, SyncSettings,
};
use url::Url;
use wasm_bindgen::prelude::*;
@@ -12,11 +16,15 @@ use web_sys::console;
struct WasmBot(Client);
impl WasmBot {
async fn on_room_message(&self, room_id: &RoomId, event: RoomEvent) {
let msg_body = if let RoomEvent::RoomMessage(MessageEvent {
async fn on_room_message(
&self,
room_id: &RoomId,
event: SyncMessageEvent<MessageEventContent>,
) {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
..
}) = event
} = event
{
msg_body.clone()
} else {
@@ -26,23 +34,27 @@ impl WasmBot {
console::log_1(&format!("Received message event {:?}", &msg_body).into());
if msg_body.starts_with("!party") {
let content = MessageEventContent::Text(TextMessageEventContent::new_plain(
"🎉🎊🥳 let's PARTY with wasm!! 🥳🎊🎉".to_string(),
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Text(
TextMessageEventContent::plain("🎉🎊🥳 let's PARTY with wasm!! 🥳🎊🎉".to_string()),
));
self.0.room_send(&room_id, content, None).await.unwrap();
}
}
async fn on_sync_response(&self, response: SyncResponse) {
console::log_1(&format!("Synced").into());
async fn on_sync_response(&self, response: SyncResponse) -> LoopCtrl {
console::log_1(&"Synced".to_string().into());
for (room_id, room) in response.rooms.join {
for event in room.timeline.events {
if let Ok(event) = event.deserialize() {
self.on_room_message(&room_id, event).await
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev)) = event {
self.on_room_message(&room_id, ev).await
}
}
}
}
LoopCtrl::Continue
}
}
@@ -63,11 +75,11 @@ pub async fn run() -> Result<JsValue, JsValue> {
let bot = WasmBot(client.clone());
client.sync(SyncSettings::default()).await.unwrap();
client.sync_once(SyncSettings::default()).await.unwrap();
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client
.sync_forever(settings, |response| bot.on_sync_response(response))
.sync_with_callback(settings, |response| bot.on_sync_response(response))
.await;
Ok(JsValue::NULL)
+1831 -883
View File
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{ops::Deref, result::Result as StdResult};
use matrix_sdk_base::crypto::{
store::CryptoStoreError, Device as BaseDevice, LocalTrust, ReadOnlyDevice,
UserDevices as BaseUserDevices,
};
use matrix_sdk_common::{
api::r0::to_device::send_event_to_device::Request as ToDeviceRequest,
identifiers::{DeviceId, DeviceIdBox},
};
use crate::{error::Result, http_client::HttpClient, Sas};
#[derive(Clone, Debug)]
/// A device represents a E2EE capable client of an user.
pub struct Device {
pub(crate) inner: BaseDevice,
pub(crate) http_client: HttpClient,
}
impl Deref for Device {
type Target = ReadOnlyDevice;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Device {
/// Start a interactive verification with this `Device`
///
/// Returns a `Sas` object that represents the interactive verification flow.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, identifiers::UserId};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # let alice = UserId::try_from("@alice:example.org").unwrap();
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let client = Client::new(homeserver).unwrap();
/// # block_on(async {
/// let device = client.get_device(&alice, "DEVICEID".into())
/// .await
/// .unwrap()
/// .unwrap();
///
/// let verification = device.start_verification().await.unwrap();
/// # });
/// ```
pub async fn start_verification(&self) -> Result<Sas> {
let (sas, request) = self.inner.start_verification().await?;
let txn_id_string = request.txn_id_string();
let request = ToDeviceRequest::new(request.event_type, &txn_id_string, request.messages);
self.http_client.send(request).await?;
Ok(Sas {
inner: sas,
http_client: self.http_client.clone(),
})
}
/// Is the device trusted.
pub fn is_trusted(&self) -> bool {
self.inner.trust_state()
}
/// Set the local trust state of the device to the given state.
///
/// This won't affect any cross signing trust state, this only sets a flag
/// marking to have the given trust state.
///
/// # Arguments
///
/// * `trust_state` - The new trust state that should be set for the device.
pub async fn set_local_trust(
&self,
trust_state: LocalTrust,
) -> StdResult<(), CryptoStoreError> {
self.inner.set_local_trust(trust_state).await
}
}
/// A read only view over all devices belonging to a user.
#[derive(Debug)]
pub struct UserDevices {
pub(crate) inner: BaseUserDevices,
pub(crate) http_client: HttpClient,
}
impl UserDevices {
/// Get the specific device with the given device id.
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
self.inner.get(device_id).map(|d| Device {
inner: d,
http_client: self.http_client.clone(),
})
}
/// Iterator over all the device ids of the user devices.
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
self.inner.keys()
}
/// Iterator over all the devices of the user devices.
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
let client = self.http_client.clone();
self.inner.devices().map(move |d| Device {
inner: d,
http_client: client.clone(),
})
}
}
+63 -6
View File
@@ -14,15 +14,21 @@
//! Error conditions.
use matrix_sdk_base::Error as MatrixError;
use matrix_sdk_common::{
api::{
r0::uiaa::{UiaaInfo, UiaaResponse as UiaaError},
Error as RumaClientError,
},
FromHttpResponseError as RumaResponseError, IntoHttpError as RumaIntoHttpError, ServerError,
};
use reqwest::Error as ReqwestError;
use serde_json::Error as JsonError;
use std::io::Error as IoError;
use thiserror::Error;
use matrix_sdk_base::Error as MatrixError;
use crate::api::Error as RumaClientError;
use crate::FromHttpResponseError as RumaResponseError;
use crate::IntoHttpError as RumaIntoHttpError;
#[cfg(feature = "encryption")]
use matrix_sdk_base::crypto::store::CryptoStoreError;
/// Result type of the rust-sdk.
pub type Result<T> = std::result::Result<T, Error>;
@@ -34,6 +40,10 @@ pub enum Error {
#[error("the queried endpoint requires authentication but was called before logging in")]
AuthenticationRequired,
/// Queried endpoint is not meant for clients.
#[error("the queried endpoint is not meant for clients")]
NotClientRequest,
/// An error at the HTTP layer.
#[error(transparent)]
Reqwest(#[from] ReqwestError),
@@ -42,6 +52,10 @@ pub enum Error {
#[error(transparent)]
SerdeJson(#[from] JsonError),
/// An IO error happened.
#[error(transparent)]
IO(#[from] IoError),
/// An error converting between ruma_client_api types and Hyper types.
#[error("can't parse the JSON response as a Matrix response")]
RumaResponse(RumaResponseError<RumaClientError>),
@@ -50,9 +64,52 @@ pub enum Error {
#[error("can't convert between ruma_client_api and hyper types.")]
IntoHttp(RumaIntoHttpError),
/// An error occured in the Matrix client library.
/// An error occurred in the Matrix client library.
#[error(transparent)]
MatrixError(#[from] MatrixError),
/// An error occurred in the crypto store.
#[cfg(feature = "encryption")]
#[error(transparent)]
CryptoStoreError(#[from] CryptoStoreError),
/// An error occurred while authenticating.
///
/// When registering or authenticating the Matrix server can send a `UiaaResponse`
/// as the error type, this is a User-Interactive Authentication API response. This
/// represents an error with information about how to authenticate the user.
#[error("User-Interactive Authentication required.")]
UiaaError(RumaResponseError<UiaaError>),
}
impl Error {
/// Try to destructure the error into an universal interactive auth info.
///
/// Some requests require universal interactive auth, doing such a request
/// will always fail the first time with a 401 status code, the response
/// body will contain info how the client can authenticate.
///
/// The request will need to be retried, this time containing additional
/// authentication data.
///
/// This method is an convenience method to get to the info the server
/// returned on the first, failed request.
pub fn uiaa_response(&self) -> Option<&UiaaInfo> {
if let Error::UiaaError(RumaResponseError::Http(ServerError::Known(
UiaaError::AuthResponse(i),
))) = self
{
Some(i)
} else {
None
}
}
}
impl From<RumaResponseError<UiaaError>> for Error {
fn from(error: RumaResponseError<UiaaError>) -> Self {
Self::UiaaError(error)
}
}
impl From<RumaResponseError<RumaClientError>> for Error {
+217
View File
@@ -0,0 +1,217 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
use http::{HeaderValue, Method as HttpMethod, Response as HttpResponse};
use reqwest::{Client, Response};
use tracing::trace;
use url::Url;
use matrix_sdk_common::{
api::r0::media::create_content, async_trait, locks::RwLock, AuthScheme, FromHttpResponseError,
};
use crate::{ClientConfig, Error, OutgoingRequest, Result, Session};
/// Abstraction around the http layer. The allows implementors to use different
/// http libraries.
#[async_trait]
pub trait HttpSend: Sync + Send + Debug {
/// The method abstracting sending request types and receiving response types.
///
/// This is called by the client every time it wants to send anything to a homeserver.
///
/// # Arguments
///
/// * `request` - The http request that has been converted from a ruma `Request`.
///
/// # Examples
///
/// ```
/// use std::convert::TryFrom;
/// use matrix_sdk::{HttpSend, Result, async_trait};
///
/// #[derive(Debug)]
/// struct Client(reqwest::Client);
///
/// impl Client {
/// async fn response_to_http_response(
/// &self,
/// mut response: reqwest::Response,
/// ) -> Result<http::Response<Vec<u8>>> {
/// // Convert the reqwest response to a http one.
/// todo!()
/// }
/// }
///
/// #[async_trait]
/// impl HttpSend for Client {
/// async fn send_request(&self, request: http::Request<Vec<u8>>) -> Result<http::Response<Vec<u8>>> {
/// Ok(self
/// .response_to_http_response(
/// self.0
/// .execute(reqwest::Request::try_from(request)?)
/// .await?,
/// )
/// .await?)
/// }
/// }
/// ```
async fn send_request(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>>;
}
#[derive(Clone, Debug)]
pub(crate) struct HttpClient {
pub(crate) inner: Arc<dyn HttpSend>,
pub(crate) homeserver: Arc<Url>,
pub(crate) session: Arc<RwLock<Option<Session>>>,
}
impl HttpClient {
async fn send_request<Request: OutgoingRequest>(
&self,
request: Request,
session: Arc<RwLock<Option<Session>>>,
content_type: Option<HeaderValue>,
) -> Result<http::Response<Vec<u8>>> {
let mut request = {
let read_guard;
let access_token = match Request::METADATA.authentication {
AuthScheme::AccessToken => {
read_guard = session.read().await;
if let Some(session) = read_guard.as_ref() {
Some(session.access_token.as_str())
} else {
return Err(Error::AuthenticationRequired);
}
}
AuthScheme::None => None,
_ => return Err(Error::NotClientRequest),
};
request.try_into_http_request(&self.homeserver.to_string(), access_token)?
};
if let HttpMethod::POST | HttpMethod::PUT | HttpMethod::DELETE = *request.method() {
if let Some(content_type) = content_type {
request
.headers_mut()
.append(http::header::CONTENT_TYPE, content_type);
}
}
self.inner.send_request(request).await
}
pub async fn upload(
&self,
request: create_content::Request<'_>,
) -> Result<create_content::Response> {
let response = self
.send_request(request, self.session.clone(), None)
.await?;
Ok(create_content::Response::try_from(response)?)
}
pub async fn send<Request>(&self, request: Request) -> Result<Request::IncomingResponse>
where
Request: OutgoingRequest,
Error: From<FromHttpResponseError<Request::EndpointError>>,
{
let content_type = HeaderValue::from_static("application/json");
let response = self
.send_request(request, self.session.clone(), Some(content_type))
.await?;
trace!("Got response: {:?}", response);
Ok(Request::IncomingResponse::try_from(response)?)
}
}
/// Build a client with the specified configuration.
pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client> {
let http_client = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let http_client = {
let http_client = match config.timeout {
Some(x) => http_client.timeout(x),
None => http_client,
};
let http_client = if config.disable_ssl_verification {
http_client.danger_accept_invalid_certs(true)
} else {
http_client
};
let http_client = match &config.proxy {
Some(p) => http_client.proxy(p.clone()),
None => http_client,
};
let mut headers = reqwest::header::HeaderMap::new();
let user_agent = match &config.user_agent {
Some(a) => a.clone(),
None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION)).unwrap(),
};
headers.insert(reqwest::header::USER_AGENT, user_agent);
http_client.default_headers(headers)
};
#[cfg(target_arch = "wasm32")]
#[allow(unused)]
let _ = config;
Ok(http_client.build()?)
}
async fn response_to_http_response(mut response: Response) -> Result<http::Response<Vec<u8>>> {
let status = response.status();
let mut http_builder = HttpResponse::builder().status(status);
let headers = http_builder.headers_mut().unwrap();
for (k, v) in response.headers_mut().drain() {
if let Some(key) = k {
headers.insert(key, v);
}
}
let body = response.bytes().await?.as_ref().to_owned();
Ok(http_builder.body(body).unwrap())
}
#[async_trait]
impl HttpSend for Client {
async fn send_request(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>> {
Ok(
response_to_http_response(self.execute(reqwest::Request::try_from(request)?).await?)
.await?,
)
}
}
+58 -12
View File
@@ -15,15 +15,35 @@
//! This crate implements a [Matrix](https://matrix.org/) client library.
//!
//! ## Crate Feature Flags
//! # Enabling logging
//!
//! Users of the matrix-sdk crate can enable log output by depending on the `tracing-subscriber`
//! crate and including the following line in their application (e.g. at the start of `main`):
//!
//! ```rust
//! tracing_subscriber::fmt::init();
//! ```
//!
//! The log output is controlled via the `RUST_LOG` environment variable by setting it to one of
//! the `error`, `warn`, `info`, `debug` or `trace` levels. The output is printed to stdout.
//!
//! The `RUST_LOG` variable also supports a more advanced syntax for filtering log output more
//! precisely, for instance with crate-level granularity. For more information on this, check out
//! the [tracing_subscriber
//! documentation](https://tracing.rs/tracing_subscriber/filter/struct.envfilter).
//!
//! # Crate Feature Flags
//!
//! The following crate feature flags are available:
//!
//! * `encryption`: Enables end-to-end encryption support in the library.
//! * `sqlite-cryptostore`: Enables a SQLite based store for the encryption
//! * `sqlite_cryptostore`: Enables a SQLite based store for the encryption
//! keys. If this is disabled and `encryption` support is enabled the keys will
//! by default be stored only in memory and thus lost after the client is
//! destroyed.
//! * `unstable-synapse-quirks`: Enables support to deal with inconsistencies
//! of Synapse in compliance with the Matrix API specification.
//! * `socks`: Enables SOCKS support in reqwest, the default HTTP client.
#![deny(
missing_debug_implementations,
@@ -35,23 +55,49 @@
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
#[cfg(not(target_arch = "wasm32"))]
pub use matrix_sdk_base::JsonStore;
pub use matrix_sdk_base::{EventEmitter, Room, Session, SyncRoom};
pub use matrix_sdk_base::{RoomState, StateStore};
pub use matrix_sdk_common::*;
pub use reqwest::header::InvalidHeaderValue;
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls",)))]
compile_error!("one of 'native-tls' or 'rustls-tls' features must be enabled");
#[cfg(all(feature = "native-tls", feature = "rustls-tls",))]
compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled");
#[cfg(feature = "encryption")]
pub use matrix_sdk_base::{Device, TrustState};
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use matrix_sdk_base::crypto::LocalTrust;
#[cfg(not(target_arch = "wasm32"))]
pub use matrix_sdk_base::JsonStore;
pub use matrix_sdk_base::{
CustomEvent, Error as BaseError, EventEmitter, Room, RoomMember, RoomState, Session,
StateStore, SyncRoom,
};
#[cfg(feature = "messages")]
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
pub use matrix_sdk_base::{MessageQueue, PossiblyRedactedExt};
pub use matrix_sdk_common::*;
pub use reqwest;
mod client;
mod error;
mod request_builder;
pub use client::{Client, ClientConfig, SyncSettings};
mod http_client;
#[cfg(feature = "encryption")]
mod device;
#[cfg(feature = "encryption")]
mod sas;
pub use client::{Client, ClientConfig, LoopCtrl, SyncSettings};
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use device::Device;
pub use error::{Error, Result};
pub use request_builder::{MessagesRequestBuilder, RoomBuilder};
pub use http_client::HttpSend;
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use sas::Sas;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
-386
View File
@@ -1,386 +0,0 @@
use crate::api;
use crate::events::room::power_levels::PowerLevelsEventContent;
use crate::events::EventJson;
use crate::identifiers::{RoomId, UserId};
use api::r0::filter::RoomEventFilter;
use api::r0::membership::Invite3pid;
use api::r0::message::get_message_events::{self, Direction};
use api::r0::room::{
create_room::{self, CreationContent, InitialStateEvent, RoomPreset},
Visibility,
};
use crate::js_int::UInt;
/// A builder used to create rooms.
///
/// # Examples
/// ```
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, RoomBuilder};
/// # use matrix_sdk::api::r0::room::Visibility;
/// # use matrix_sdk::identifiers::UserId;
/// # use url::Url;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async {
/// let mut builder = RoomBuilder::default();
/// builder.creation_content(false)
/// .initial_state(vec![])
/// .visibility(Visibility::Public)
/// .name("name")
/// .room_version("v1.0");
/// let mut client = Client::new(homeserver).unwrap();
/// client.create_room(builder).await;
/// # })
/// ```
#[derive(Clone, Debug, Default)]
pub struct RoomBuilder {
/// Extra keys to be added to the content of the `m.room.create`.
creation_content: Option<CreationContent>,
/// List of state events to send to the new room.
///
/// Takes precedence over events set by preset, but gets overriden by
/// name and topic keys.
initial_state: Vec<InitialStateEvent>,
/// A list of user IDs to invite to the room.
///
/// This will tell the server to invite everyone in the list to the newly created room.
invite: Vec<UserId>,
/// List of third party IDs of users to invite.
invite_3pid: Vec<Invite3pid>,
/// If set, this sets the `is_direct` flag on room invites.
is_direct: Option<bool>,
/// If this is included, an `m.room.name` event will be sent into the room to indicate
/// the name of the room.
name: Option<String>,
/// Power level content to override in the default power level event.
power_level_content_override: Option<PowerLevelsEventContent>,
/// Convenience parameter for setting various default state events based on a preset.
preset: Option<RoomPreset>,
/// The desired room alias local part.
room_alias_name: Option<String>,
/// Room version to set for the room. Defaults to homeserver's default if not specified.
room_version: Option<String>,
/// If this is included, an `m.room.topic` event will be sent into the room to indicate
/// the topic for the room.
topic: Option<String>,
/// A public visibility indicates that the room will be shown in the published room
/// list. A private visibility will hide the room from the published room list. Rooms
/// default to private visibility if this key is not included.
visibility: Option<Visibility>,
}
impl RoomBuilder {
/// Returns an empty `RoomBuilder` for creating rooms.
pub fn new() -> Self {
Self::default()
}
/// Set the `CreationContent`.
///
/// Weather users on other servers can join this room.
pub fn creation_content(&mut self, federate: bool) -> &mut Self {
let federate = Some(federate);
self.creation_content = Some(CreationContent { federate });
self
}
/// Set the `InitialStateEvent` vector.
pub fn initial_state(&mut self, state: Vec<InitialStateEvent>) -> &mut Self {
self.initial_state = state;
self
}
/// Set the vec of `UserId`s.
pub fn invite(&mut self, invite: Vec<UserId>) -> &mut Self {
self.invite = invite;
self
}
/// Set the vec of `Invite3pid`s.
pub fn invite_3pid(&mut self, invite: Vec<Invite3pid>) -> &mut Self {
self.invite_3pid = invite;
self
}
/// Set the vec of `Invite3pid`s.
pub fn is_direct(&mut self, direct: bool) -> &mut Self {
self.is_direct = Some(direct);
self
}
/// Set the room name. A `m.room.name` event will be sent to the room.
pub fn name<S: Into<String>>(&mut self, name: S) -> &mut Self {
self.name = Some(name.into());
self
}
/// Set the room's power levels.
pub fn power_level_override(&mut self, power: PowerLevelsEventContent) -> &mut Self {
self.power_level_content_override = Some(power);
self
}
/// Convenience for setting various default state events based on a preset.
pub fn preset(&mut self, preset: RoomPreset) -> &mut Self {
self.preset = Some(preset);
self
}
/// The local part of a room alias.
pub fn room_alias_name<S: Into<String>>(&mut self, alias: S) -> &mut Self {
self.room_alias_name = Some(alias.into());
self
}
/// Room version, defaults to homeserver's version if left unspecified.
pub fn room_version<S: Into<String>>(&mut self, version: S) -> &mut Self {
self.room_version = Some(version.into());
self
}
/// If included, a `m.room.topic` event will be sent to the room.
pub fn topic<S: Into<String>>(&mut self, topic: S) -> &mut Self {
self.topic = Some(topic.into());
self
}
/// A public visibility indicates that the room will be shown in the published
/// room list. A private visibility will hide the room from the published room list.
/// Rooms default to private visibility if this key is not included.
pub fn visibility(&mut self, vis: Visibility) -> &mut Self {
self.visibility = Some(vis);
self
}
}
impl Into<create_room::Request> for RoomBuilder {
fn into(self) -> create_room::Request {
create_room::Request {
creation_content: self.creation_content,
initial_state: self.initial_state,
invite: self.invite,
invite_3pid: self.invite_3pid,
is_direct: self.is_direct,
name: self.name,
power_level_content_override: self.power_level_content_override.map(EventJson::from),
preset: self.preset,
room_alias_name: self.room_alias_name,
room_version: self.room_version,
topic: self.topic,
visibility: self.visibility,
}
}
}
/// Create a builder for making get_message_event requests.
///
/// # Examples
/// ```
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, MessagesRequestBuilder};
/// # use matrix_sdk::api::r0::message::get_message_events::{self, Direction};
/// # use matrix_sdk::identifiers::RoomId;
/// # use url::Url;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async {
/// # let room_id = RoomId::try_from("!test:localhost").unwrap();
/// # let last_sync_token = "".to_string();
/// let mut client = Client::new(homeserver).unwrap();
///
/// let mut builder = MessagesRequestBuilder::new();
/// builder.room_id(room_id)
/// .from(last_sync_token)
/// .direction(Direction::Forward);
///
/// client.room_messages(builder).await.is_err();
/// # })
/// ```
#[derive(Clone, Debug, Default)]
pub struct MessagesRequestBuilder {
/// The room to get events from.
room_id: Option<RoomId>,
/// The token to start returning events from.
///
/// This token can be obtained from a
/// prev_batch token returned for each room by the sync API, or from a start or end token
/// returned by a previous request to this endpoint.
from: Option<String>,
/// The token to stop returning events at.
///
/// This token can be obtained from a prev_batch
/// token returned for each room by the sync endpoint, or from a start or end token returned
/// by a previous request to this endpoint.
to: Option<String>,
/// The direction to return events from.
direction: Option<Direction>,
/// The maximum number of events to return.
///
/// Default: 10.
limit: Option<UInt>,
/// A filter of the returned events with.
filter: Option<RoomEventFilter>,
}
impl MessagesRequestBuilder {
/// Create a `MessagesRequestBuilder` builder to make a `get_message_events::Request`.
///
/// The `room_id` and `from`` fields **need to be set** to create the request.
pub fn new() -> Self {
Self::default()
}
/// RoomId is required to create a `get_message_events::Request`.
pub fn room_id(&mut self, room_id: RoomId) -> &mut Self {
self.room_id = Some(room_id);
self
}
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
///
/// This is required to create a `get_message_events::Request`.
pub fn from(&mut self, from: String) -> &mut Self {
self.from = Some(from);
self
}
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
///
/// This token signals when to stop receiving events.
pub fn to(&mut self, to: String) -> &mut Self {
self.to = Some(to);
self
}
/// The direction to return events from.
///
/// If not specified `Direction::Backward` is used.
pub fn direction(&mut self, direction: Direction) -> &mut Self {
self.direction = Some(direction);
self
}
/// The maximum number of events to return.
pub fn limit(&mut self, limit: UInt) -> &mut Self {
self.limit = Some(limit);
self
}
/// Filter events by the given `RoomEventFilter`.
pub fn filter(&mut self, filter: RoomEventFilter) -> &mut Self {
self.filter = Some(filter);
self
}
}
impl Into<get_message_events::Request> for MessagesRequestBuilder {
fn into(self) -> get_message_events::Request {
get_message_events::Request {
room_id: self.room_id.expect("`room_id` and `from` need to be set"),
from: self.from.expect("`room_id` and `from` need to be set"),
to: self.to,
dir: self.direction.unwrap_or(Direction::Backward),
limit: self.limit,
filter: self.filter,
}
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use super::*;
use crate::api::r0::filter::{LazyLoadOptions, RoomEventFilter};
use crate::events::room::power_levels::NotificationPowerLevels;
use crate::js_int::Int;
use crate::{identifiers::RoomId, Client, Session};
use mockito::{mock, Matcher};
use std::convert::TryFrom;
use url::Url;
#[tokio::test]
async fn create_room_builder() {
let homeserver = Url::parse(&mockito::server_url()).unwrap();
let _m = mock("POST", "/_matrix/client/r0/createRoom")
.with_status(200)
.with_body_from_file("../test_data/room_id.json")
.create();
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let mut builder = RoomBuilder::new();
builder
.creation_content(false)
.initial_state(vec![])
.visibility(Visibility::Public)
.name("room_name")
.room_version("v1.0")
.invite_3pid(vec![])
.is_direct(true)
.power_level_override(PowerLevelsEventContent {
ban: Int::MAX,
events: BTreeMap::default(),
events_default: Int::MIN,
invite: Int::MIN,
kick: Int::MIN,
redact: Int::MAX,
state_default: Int::MIN,
users_default: Int::MIN,
notifications: NotificationPowerLevels { room: Int::MIN },
users: BTreeMap::default(),
})
.preset(RoomPreset::PrivateChat)
.room_alias_name("room_alias")
.topic("room topic")
.visibility(Visibility::Private);
let cli = Client::new(homeserver).unwrap();
cli.restore_login(session).await.unwrap();
assert!(cli.create_room(builder).await.is_ok());
}
#[tokio::test]
async fn get_message_events() {
let homeserver = Url::parse(&mockito::server_url()).unwrap();
let _m = mock(
"GET",
Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/messages".to_string()),
)
.with_status(200)
.with_body_from_file("../test_data/room_messages.json")
.create();
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let mut builder = MessagesRequestBuilder::new();
builder
.room_id(RoomId::try_from("!roomid:example.com").unwrap())
.from("t47429-4392820_219380_26003_2265".to_string())
.to("t4357353_219380_26003_2265".to_string())
.direction(Direction::Backward)
.limit(UInt::new(10).unwrap())
.filter(RoomEventFilter {
lazy_load_options: LazyLoadOptions::Enabled {
include_redundant_members: false,
},
..Default::default()
});
let cli = Client::new(homeserver).unwrap();
cli.restore_login(session).await.unwrap();
assert!(cli.room_messages(builder).await.is_ok());
}
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_base::crypto::{ReadOnlyDevice, Sas as BaseSas};
use matrix_sdk_common::api::r0::to_device::send_event_to_device::Request as ToDeviceRequest;
use crate::{error::Result, http_client::HttpClient};
/// An object controling the interactive verification flow.
#[derive(Debug, Clone)]
pub struct Sas {
pub(crate) inner: BaseSas,
pub(crate) http_client: HttpClient,
}
impl Sas {
/// Accept the interactive verification flow.
pub async fn accept(&self) -> Result<()> {
if let Some(req) = self.inner.accept() {
let txn_id_string = req.txn_id_string();
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
self.http_client.send(request).await?;
}
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn confirm(&self) -> Result<()> {
let (to_device, signature) = self.inner.confirm().await?;
if let Some(req) = to_device {
let txn_id_string = req.txn_id_string();
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
self.http_client.send(request).await?;
}
if let Some(s) = signature {
self.http_client.send(s).await?;
}
Ok(())
}
/// Cancel the interactive verification flow.
pub async fn cancel(&self) -> Result<()> {
if let Some(req) = self.inner.cancel() {
let txn_id_string = req.txn_id_string();
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
self.http_client.send(request).await?;
}
Ok(())
}
/// Get the emoji version of the short auth string.
pub fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
self.inner.emoji()
}
/// Get the decimal version of the short auth string.
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
self.inner.decimals()
}
/// Is the verification process done.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Is the verification process canceled.
pub fn is_canceled(&self) -> bool {
self.inner.is_canceled()
}
/// Get the other users device that we're veryfying.
pub fn other_device(&self) -> ReadOnlyDevice {
self.inner.other_device()
}
}
+26 -17
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "The base component to build a Matrix client library."
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,39 +8,48 @@ license = "Apache-2.0"
name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.2.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = ["encryption", "sqlite-cryptostore", "messages"]
default = ["encryption", "sqlite_cryptostore", "messages"]
messages = []
encryption = ["matrix-sdk-crypto"]
sqlite-cryptostore = ["matrix-sdk-crypto/sqlite-cryptostore"]
sqlite_cryptostore = ["matrix-sdk-crypto/sqlite_cryptostore"]
unstable-synapse-quirks = ["matrix-sdk-common/unstable-synapse-quirks"]
docs = ["encryption", "sqlite_cryptostore", "messages"]
[dependencies]
async-trait = "0.1.31"
serde = "1.0.110"
serde_json = "1.0.53"
zeroize = "1.1.0"
serde = "1.0.118"
serde_json = "1.0.61"
zeroize = "1.2.0"
tracing = "0.1.22"
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
matrix-sdk-crypto = { version = "0.1.0", path = "../matrix_sdk_crypto", optional = true }
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
matrix-sdk-crypto = { version = "0.2.0", path = "../matrix_sdk_crypto", optional = true }
# Misc dependencies
thiserror = "1.0.19"
thiserror = "1.0.23"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.21"
version = "0.2.24"
default-features = false
features = ["sync", "fs"]
[dev-dependencies]
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
http = "0.2.1"
tracing-subscriber = "0.2.5"
futures = "0.3.8"
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
http = "0.2.2"
tracing-subscriber = "0.2.15"
tempfile = "3.1.0"
mockito = "0.28.0"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.12"
wasm-bindgen-test = "0.3.19"
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -47,13 +47,13 @@ pub enum Error {
/// An error occurred during a E2EE operation.
#[cfg(feature = "encryption")]
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[error(transparent)]
OlmError(#[from] OlmError),
/// An error occurred during a E2EE group operation.
#[cfg(feature = "encryption")]
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[error(transparent)]
MegolmError(#[from] MegolmError),
}
+364 -100
View File
@@ -15,36 +15,57 @@
use std::sync::Arc;
use matrix_sdk_common::locks::RwLock;
use serde_json::value::RawValue as RawJsonValue;
use crate::events::{
fully_read::FullyReadEvent,
ignored_user_list::IgnoredUserListEvent,
presence::PresenceEvent,
push_rules::PushRulesEvent,
receipt::ReceiptEvent,
room::{
aliases::AliasesEvent,
avatar::AvatarEvent,
canonical_alias::CanonicalAliasEvent,
join_rules::JoinRulesEvent,
member::{MemberEvent, MemberEventContent},
message::{feedback::FeedbackEvent, MessageEvent},
name::NameEvent,
power_levels::PowerLevelsEvent,
redaction::RedactionEvent,
tombstone::TombstoneEvent,
use crate::{
events::{
call::{
answer::AnswerEventContent, candidates::CandidatesEventContent,
hangup::HangupEventContent, invite::InviteEventContent,
},
custom::CustomEventContent,
fully_read::FullyReadEventContent,
ignored_user_list::IgnoredUserListEventContent,
presence::PresenceEvent,
push_rules::PushRulesEventContent,
receipt::ReceiptEventContent,
room::{
aliases::AliasesEventContent,
avatar::AvatarEventContent,
canonical_alias::CanonicalAliasEventContent,
join_rules::JoinRulesEventContent,
member::MemberEventContent,
message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent},
name::NameEventContent,
power_levels::PowerLevelsEventContent,
redaction::SyncRedactionEvent,
tombstone::TombstoneEventContent,
},
typing::TypingEventContent,
BasicEvent, StrippedStateEvent, SyncEphemeralRoomEvent, SyncMessageEvent, SyncStateEvent,
},
stripped::{
StrippedRoomAliases, StrippedRoomAvatar, StrippedRoomCanonicalAlias, StrippedRoomJoinRules,
StrippedRoomMember, StrippedRoomName, StrippedRoomPowerLevels,
},
typing::TypingEvent,
Room, RoomState,
};
use crate::{Room, RoomState};
use matrix_sdk_common::async_trait;
/// Type alias for `RoomState` enum when passed to `EventEmitter` methods.
pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
/// This represents the various "unrecognized" events.
#[derive(Clone, Copy, Debug)]
pub enum CustomEvent<'c> {
/// A custom basic event.
Basic(&'c BasicEvent<CustomEventContent>),
/// A custom basic event.
EphemeralRoom(&'c SyncEphemeralRoomEvent<CustomEventContent>),
/// A custom room event.
Message(&'c SyncMessageEvent<CustomEventContent>),
/// A custom state event.
State(&'c SyncStateEvent<CustomEventContent>),
/// A custom stripped state event.
StrippedState(&'c StrippedStateEvent<CustomEventContent>),
}
/// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event.
/// The `Client` calls each method when the corresponding event is received.
///
@@ -56,19 +77,20 @@ pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
/// # use matrix_sdk_base::{
/// # self,
/// # events::{
/// # room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
/// # room::message::{MessageEventContent, TextMessageEventContent},
/// # SyncMessageEvent
/// # },
/// # EventEmitter, SyncRoom
/// # };
/// # use matrix_sdk_common::locks::RwLock;
/// # use matrix_sdk_common::{async_trait, locks::RwLock};
///
/// struct EventCallback;
///
/// #[async_trait::async_trait]
/// #[async_trait]
/// impl EventEmitter for EventCallback {
/// async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
/// async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
/// if let SyncRoom::Joined(room) = room {
/// if let MessageEvent {
/// if let SyncMessageEvent {
/// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
/// sender,
/// ..
@@ -76,7 +98,7 @@ pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
/// {
/// let name = {
/// let room = room.read().await;
/// let member = room.members.get(&sender).unwrap();
/// let member = room.joined_members.get(&sender).unwrap();
/// member
/// .display_name
/// .as_ref()
@@ -89,94 +111,188 @@ pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
/// }
/// }
/// ```
#[async_trait::async_trait]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventEmitter: Send + Sync {
// ROOM EVENTS from `IncomingTimeline`
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {}
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {}
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
async fn on_room_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {}
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {}
async fn on_room_message_feedback(
&self,
_: SyncRoom,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::CallInvite` event
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallAnswer` event
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallCandidates` event
async fn on_room_call_candidates(
&self,
_: SyncRoom,
_: &SyncMessageEvent<CandidatesEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::CallHangup` event
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {}
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
async fn on_room_power_levels(&self, _: SyncRoom, _: &SyncStateEvent<PowerLevelsEventContent>) {
}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {}
async fn on_room_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {}
// `RoomEvent`s from `IncomingState`
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {}
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomName` event.
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {}
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
async fn on_state_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
async fn on_state_power_levels(
&self,
_: SyncRoom,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {}
async fn on_state_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedRoomMember,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {}
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(&self, _: SyncRoom, _: &StrippedRoomCanonicalAlias) {
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {}
async fn on_stripped_state_aliases(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AliasesEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {}
async fn on_stripped_state_avatar(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AvatarEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {}
async fn on_stripped_state_power_levels(
&self,
_: SyncRoom,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {}
async fn on_stripped_state_join_rules(
&self,
_: SyncRoom,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
}
// `NonRoomEvent` (this is a type alias from ruma_events)
/// Fires when `Client` receives a `NonRoomEvent::RoomMember` event.
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomPresence` event.
async fn on_non_room_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {}
async fn on_non_room_ignored_users(
&self,
_: SyncRoom,
_: &BasicEvent<IgnoredUserListEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {}
async fn on_non_room_push_rules(&self, _: SyncRoom, _: &BasicEvent<PushRulesEventContent>) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {}
async fn on_non_room_fully_read(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<FullyReadEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
async fn on_account_data_typing(&self, _: SyncRoom, _: &TypingEvent) {}
async fn on_non_room_typing(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<TypingEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
///
/// This is always a read receipt.
async fn on_account_data_receipt(&self, _: SyncRoom, _: &ReceiptEvent) {}
async fn on_non_room_receipt(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
) {
}
// `PresenceEvent` is a struct so there is only the one method
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {}
/// Fires when `Client` receives a `Event::Custom` event or if deserialization fails
/// because the event was unknown to ruma.
///
/// The only guarantee this method can give about the event is that it is valid JSON.
async fn on_unrecognized_event(&self, _: SyncRoom, _: &RawJsonValue) {}
/// Fires when `Client` receives a `Event::Custom` event or if deserialization fails
/// because the event was unknown to ruma.
///
/// The only guarantee this method can give about the event is that it is in the
/// shape of a valid matrix event.
async fn on_custom_event(&self, _: SyncRoom, _: &CustomEvent<'_>) {}
}
#[cfg(test)]
mod test {
use super::*;
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_common::{async_trait, locks::Mutex};
use matrix_sdk_test::{async_test, sync_response, SyncResponseFile};
use std::sync::Arc;
@@ -186,65 +302,108 @@ mod test {
#[derive(Clone)]
pub struct EvEmitterTest(Arc<Mutex<Vec<String>>>);
#[async_trait::async_trait]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventEmitter for EvEmitterTest {
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("member".to_string())
}
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("name".to_string())
}
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
async fn on_room_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("canonical".to_string())
}
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("aliases".to_string())
}
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("avatar".to_string())
}
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {
self.0.lock().await.push("message".to_string())
}
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {
async fn on_room_message_feedback(
&self,
_: SyncRoom,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
self.0.lock().await.push("feedback".to_string())
}
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {
self.0.lock().await.push("call invite".to_string())
}
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {
self.0.lock().await.push("call answer".to_string())
}
async fn on_room_call_candidates(
&self,
_: SyncRoom,
_: &SyncMessageEvent<CandidatesEventContent>,
) {
self.0.lock().await.push("call candidates".to_string())
}
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {
self.0.lock().await.push("call hangup".to_string())
}
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {
self.0.lock().await.push("redaction".to_string())
}
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
async fn on_room_power_levels(
&self,
_: SyncRoom,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("power".to_string())
}
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {
self.0.lock().await.push("tombstone".to_string())
}
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("state member".to_string())
}
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("state name".to_string())
}
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
async fn on_state_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("state canonical".to_string())
}
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("state aliases".to_string())
}
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("state avatar".to_string())
}
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
async fn on_state_power_levels(
&self,
_: SyncRoom,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("state power".to_string())
}
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {
async fn on_state_join_rules(
&self,
_: SyncRoom,
_: &SyncStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("state rules".to_string())
}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedRoomMember,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
self.0
@@ -252,65 +411,116 @@ mod test {
.await
.push("stripped state member".to_string())
}
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(
&self,
_: SyncRoom,
_: &StrippedStateEvent<NameEventContent>,
) {
self.0.lock().await.push("stripped state name".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedRoomCanonicalAlias,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
self.0
.lock()
.await
.push("stripped state canonical".to_string())
}
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AliasesEventContent>,
) {
self.0
.lock()
.await
.push("stripped state aliases".to_string())
}
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AvatarEventContent>,
) {
self.0
.lock()
.await
.push("stripped state avatar".to_string())
}
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(
&self,
_: SyncRoom,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("stripped state power".to_string())
}
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(
&self,
_: SyncRoom,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("stripped state rules".to_string())
}
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("account presence".to_string())
async fn on_non_room_presence(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("presence".to_string())
}
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {
async fn on_non_room_ignored_users(
&self,
_: SyncRoom,
_: &BasicEvent<IgnoredUserListEventContent>,
) {
self.0.lock().await.push("account ignore".to_string())
}
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {
async fn on_non_room_push_rules(&self, _: SyncRoom, _: &BasicEvent<PushRulesEventContent>) {
self.0.lock().await.push("account push rules".to_string())
}
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {
async fn on_non_room_fully_read(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<FullyReadEventContent>,
) {
self.0.lock().await.push("account read".to_string())
}
async fn on_non_room_typing(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<TypingEventContent>,
) {
self.0.lock().await.push("typing event".to_string())
}
async fn on_non_room_receipt(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
) {
self.0.lock().await.push("receipt event".to_string())
}
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("presence event".to_string())
}
async fn on_unrecognized_event(&self, _: SyncRoom, _: &RawJsonValue) {
self.0.lock().await.push("unrecognized event".to_string())
}
async fn on_custom_event(&self, _: SyncRoom, _: &CustomEvent<'_>) {
self.0.lock().await.push("custom event".to_string())
}
}
use crate::identifiers::UserId;
use crate::{BaseClient, Session};
use std::convert::TryFrom;
use crate::{identifiers::user_id, BaseClient, Session};
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:example.com").unwrap(),
device_id: "DEVICEID".to_owned(),
user_id: user_id!("@example:example.com"),
device_id: "DEVICEID".into(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
@@ -341,9 +551,10 @@ mod test {
"state member",
"state member",
"message",
"account read",
"account ignore",
"presence event"
"presence event",
"receipt event",
"account read",
],
)
}
@@ -394,4 +605,57 @@ mod test {
],
)
}
#[async_test]
async fn event_emitter_more_events() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::All);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"message",
"message", // this is a message edit event
"redaction",
"unrecognized event",
// "unrecognized event", this is actually a redacted "m.room.messages" event
// the ephemeral room events are looped over after the room events
"receipt event",
"typing event"
],
)
}
#[async_test]
async fn event_emitter_voip() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::Voip);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"call invite",
"call answer",
"call candidates",
"call hangup",
],
)
}
}
+19 -5
View File
@@ -20,10 +20,12 @@
//! The following crate feature flags are available:
//!
//! * `encryption`: Enables end-to-end encryption support in the library.
//! * `sqlite-cryptostore`: Enables a SQLite based store for the encryption
//! * `sqlite_cryptostore`: Enables a SQLite based store for the encryption
//! keys. If this is disabled and `encryption` support is enabled the keys will
//! by default be stored only in memory and thus lost after the client is
//! destroyed.
//! * `unstable-synapse-quirks`: Enables support to deal with inconsistencies
//! of Synapse in compliance with the Matrix API specification.
#![deny(
missing_debug_implementations,
dead_code,
@@ -34,8 +36,12 @@
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
pub use crate::{error::Error, error::Result, session::Session};
pub use crate::{
error::{Error, Result},
session::Session,
};
pub use matrix_sdk_common::*;
mod client;
@@ -46,10 +52,18 @@ mod session;
mod state;
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
pub use event_emitter::{EventEmitter, SyncRoom};
pub use event_emitter::{CustomEvent, EventEmitter, SyncRoom};
pub use models::{Room, RoomMember};
pub use state::{AllRooms, ClientState};
#[cfg(feature = "encryption")]
pub use matrix_sdk_crypto::{Device, TrustState};
pub use models::Room;
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use matrix_sdk_crypto as crypto;
#[cfg(feature = "messages")]
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
pub use models::{MessageQueue, PossiblyRedactedExt};
#[cfg(not(target_arch = "wasm32"))]
pub use state::JsonStore;
pub use state::StateStore;
-56
View File
@@ -1,56 +0,0 @@
//! De-/serialization functions to and from json strings, allows the type to be used as a query string.
use serde::de::{Deserialize, Deserializer, Error as _};
use crate::events::collections::all::Event;
use crate::events::presence::PresenceEvent;
use crate::events::EventJson;
pub fn deserialize_events<'de, D>(deserializer: D) -> Result<Vec<Event>, D::Error>
where
D: Deserializer<'de>,
{
let mut events = vec![];
let ev = Vec::<EventJson<Event>>::deserialize(deserializer)?;
for event in ev {
events.push(event.deserialize().map_err(D::Error::custom)?);
}
Ok(events)
}
pub fn deserialize_presence<'de, D>(deserializer: D) -> Result<Vec<PresenceEvent>, D::Error>
where
D: Deserializer<'de>,
{
let mut events = vec![];
let ev = Vec::<EventJson<PresenceEvent>>::deserialize(deserializer)?;
for event in ev {
events.push(event.deserialize().map_err(D::Error::custom)?);
}
Ok(events)
}
#[cfg(test)]
mod test {
use std::fs;
use crate::events::room::member::MemberEvent;
use crate::events::EventJson;
use crate::models::RoomMember;
#[test]
fn events_and_presence_deserialization() {
let ev_json = fs::read_to_string("../test_data/events/member.json").unwrap();
let ev = serde_json::from_str::<EventJson<MemberEvent>>(&ev_json)
.unwrap()
.deserialize()
.unwrap();
let member = RoomMember::new(&ev);
let member_json = serde_json::to_string(&member).unwrap();
let mem = serde_json::from_str::<RoomMember>(&member_json).unwrap();
assert_eq!(member, mem);
}
}
+129 -108
View File
@@ -3,104 +3,112 @@
//! The `Room` struct optionally holds a `MessageQueue` if the "messages"
//! feature is enabled.
use std::cmp::Ordering;
use std::ops::Deref;
use std::vec::IntoIter;
use crate::events::room::message::MessageEvent;
use crate::events::EventJson;
use std::{time::SystemTime, vec::IntoIter};
use matrix_sdk_common::{
events::AnyPossiblyRedactedSyncMessageEvent,
identifiers::{EventId, UserId},
};
use serde::{de, ser, Serialize};
/// A queue that holds the 10 most recent messages received from the server.
/// Exposes some of the field access methods found in the event held by
/// `AnyPossiblyRedacted*` enums.
///
/// This is just an extension trait to ease the use of certain event enums.
pub trait PossiblyRedactedExt {
/// Access the redacted or full event's `event_id` field.
fn event_id(&self) -> &EventId;
/// Access the redacted or full event's `origin_server_ts` field.
fn origin_server_ts(&self) -> &SystemTime;
/// Access the redacted or full event's `sender` field.
fn sender(&self) -> &UserId;
}
impl PossiblyRedactedExt for AnyPossiblyRedactedSyncMessageEvent {
/// Access the underlying event's `event_id`.
fn event_id(&self) -> &EventId {
match self {
Self::Regular(e) => e.event_id(),
Self::Redacted(e) => e.event_id(),
}
}
/// Access the underlying event's `origin_server_ts`.
fn origin_server_ts(&self) -> &SystemTime {
match self {
Self::Regular(e) => e.origin_server_ts(),
Self::Redacted(e) => e.origin_server_ts(),
}
}
/// Access the underlying event's `sender`.
fn sender(&self) -> &UserId {
match self {
Self::Regular(e) => e.sender(),
Self::Redacted(e) => e.sender(),
}
}
}
const MESSAGE_QUEUE_CAP: usize = 35;
/// A queue that holds the 35 most recent messages received from the server.
#[derive(Clone, Debug, Default)]
pub struct MessageQueue {
msgs: Vec<MessageWrapper>,
}
#[derive(Clone, Debug, Serialize)]
pub struct MessageWrapper(MessageEvent);
impl Deref for MessageWrapper {
type Target = MessageEvent;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialEq for MessageWrapper {
fn eq(&self, other: &MessageWrapper) -> bool {
self.0.event_id == other.0.event_id
}
}
impl Eq for MessageWrapper {}
impl PartialOrd for MessageWrapper {
fn partial_cmp(&self, other: &MessageWrapper) -> Option<Ordering> {
Some(self.0.origin_server_ts.cmp(&other.0.origin_server_ts))
}
}
impl Ord for MessageWrapper {
fn cmp(&self, other: &MessageWrapper) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}
impl PartialEq for MessageQueue {
fn eq(&self, other: &MessageQueue) -> bool {
self.msgs.len() == other.msgs.len()
&& self
.msgs
.iter()
.zip(other.msgs.iter())
.all(|(msg_a, msg_b)| msg_a.event_id == msg_b.event_id)
}
pub(crate) msgs: Vec<AnyPossiblyRedactedSyncMessageEvent>,
}
impl MessageQueue {
/// Create a new empty `MessageQueue`.
pub fn new() -> Self {
Self {
msgs: Vec::with_capacity(20),
msgs: Vec::with_capacity(45),
}
}
/// Inserts a `MessageEvent` into `MessageQueue`, sorted by by `origin_server_ts`.
///
/// Removes the oldest element in the queue if there are more than 10 elements.
pub fn push(&mut self, msg: MessageEvent) -> bool {
pub fn push(&mut self, msg: AnyPossiblyRedactedSyncMessageEvent) -> bool {
// only push new messages into the queue
if let Some(latest) = self.msgs.last() {
if msg.origin_server_ts < latest.origin_server_ts && self.msgs.len() >= 10 {
if msg.origin_server_ts() < latest.origin_server_ts() && self.msgs.len() >= 10 {
return false;
}
}
let message = MessageWrapper(msg);
match self.msgs.binary_search_by(|m| m.cmp(&message)) {
Ok(pos) => {
if self.msgs[pos] != message {
self.msgs.insert(pos, message)
}
}
Err(pos) => self.msgs.insert(pos, message),
if self.msgs.iter().all(|old| old.event_id() != msg.event_id()) {
self.msgs.push(msg)
}
if self.msgs.len() > 10 {
self.msgs.remove(0);
if self.msgs.len() > MESSAGE_QUEUE_CAP {
self.msgs.pop();
}
true
}
pub fn iter(&self) -> impl Iterator<Item = &MessageWrapper> {
/// Iterate over the messages in the queue.
pub fn iter(&self) -> impl Iterator<Item = &AnyPossiblyRedactedSyncMessageEvent> {
self.msgs.iter()
}
/// Iterate over each message mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AnyPossiblyRedactedSyncMessageEvent> {
self.msgs.iter_mut()
}
}
impl PartialEq for MessageQueue {
fn eq(&self, other: &MessageQueue) -> bool {
self.msgs
.iter()
.zip(other.msgs.iter())
.all(|(a, b)| a.event_id() == b.event_id())
}
}
impl IntoIterator for MessageQueue {
type Item = MessageWrapper;
type Item = AnyPossiblyRedactedSyncMessageEvent;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
@@ -109,23 +117,38 @@ impl IntoIterator for MessageQueue {
}
pub(crate) mod ser_deser {
use std::fmt;
use super::*;
struct MessageQueueDeserializer;
impl<'de> de::Visitor<'de> for MessageQueueDeserializer {
type Value = MessageQueue;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an array of message events")
}
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut msgs = Vec::with_capacity(access.size_hint().unwrap_or(0));
while let Some(msg) = access.next_element::<AnyPossiblyRedactedSyncMessageEvent>()? {
msgs.push(msg);
}
Ok(MessageQueue { msgs })
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<MessageQueue, D::Error>
where
D: de::Deserializer<'de>,
{
use serde::de::Error;
let messages: Vec<EventJson<MessageEvent>> = de::Deserialize::deserialize(deserializer)?;
let mut msgs = vec![];
for json in messages {
let msg = json.deserialize().map_err(D::Error::custom)?;
msgs.push(MessageWrapper(msg));
}
Ok(MessageQueue { msgs })
deserializer.deserialize_seq(MessageQueueDeserializer)
}
pub fn serialize<S>(msgs: &MessageQueue, serializer: S) -> Result<S::Ok, S::Error>
@@ -138,37 +161,34 @@ pub(crate) mod ser_deser {
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
use std::convert::TryFrom;
use matrix_sdk_common::{
events::{AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent},
identifiers::{room_id, user_id, RoomId},
};
use matrix_sdk_test::test_json;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use crate::events::{collections::all::RoomEvent, EventJson};
use crate::identifiers::{RoomId, UserId};
use super::*;
use crate::Room;
#[test]
fn serialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let mut room = Room::new(&id, &user);
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
);
let mut msgs = MessageQueue::new();
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
msgs.push(msg.clone());
msg
} else {
panic!("this should always be a RoomMessage")
};
room.messages = msgs.clone();
msgs.push(msg.clone());
room.messages = msgs;
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room);
@@ -186,8 +206,10 @@ mod test {
},
"own_user_id": "@example:example.com",
"creator": null,
"members": {},
"messages": [ message ],
"direct_target": null,
"joined_members": {},
"invited_members": {},
"messages": [ msg ],
"typing_users": [],
"power_levels": null,
"encrypted": null,
@@ -202,25 +224,22 @@ mod test {
#[test]
fn deserialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let mut room = Room::new(&id, &user);
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
);
let mut msgs = MessageQueue::new();
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
msgs.push(msg.clone());
msg
} else {
panic!("this should always be a RoomMessage")
};
msgs.push(msg.clone());
room.messages = msgs;
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room.clone());
joined_rooms.insert(id, room);
let json = serde_json::json!({
"!roomid:example.com": {
@@ -235,8 +254,10 @@ mod test {
},
"own_user_id": "@example:example.com",
"creator": null,
"members": {},
"messages": [ message ],
"direct_target": null,
"joined_members": {},
"invited_members": {},
"messages": [ msg ],
"typing_users": [],
"power_levels": null,
"encrypted": null,
+3 -2
View File
@@ -1,9 +1,10 @@
mod event_deser;
#[cfg(feature = "messages")]
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
mod message;
mod room;
mod room_member;
#[cfg(feature = "messages")]
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
pub use message::{MessageQueue, PossiblyRedactedExt};
pub use room::{Room, RoomName};
pub use room_member::RoomMember;
File diff suppressed because it is too large Load Diff
+148 -141
View File
@@ -15,27 +15,26 @@
use std::convert::TryFrom;
use crate::events::collections::all::Event;
use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState};
use crate::events::room::{
member::{MemberEvent, MembershipChange, MembershipState},
power_levels::PowerLevelsEvent,
use matrix_sdk_common::{
events::{presence::PresenceEvent, room::member::MemberEventContent, SyncStateEvent},
identifiers::{RoomId, UserId},
presence::PresenceState,
Int, UInt,
};
use crate::identifiers::UserId;
use crate::js_int::{Int, UInt};
use serde::{Deserialize, Serialize};
// Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob.
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(Clone))]
#[derive(Debug, Serialize, Deserialize, Clone)]
/// A Matrix room member.
///
pub struct RoomMember {
/// The unique mxid of the user.
/// The unique MXID of the user.
pub user_id: UserId,
/// The human readable name of the user.
pub display_name: Option<String>,
/// Whether the member's display name is ambiguous due to being shared with
/// other members.
pub display_name_ambiguous: bool,
/// The matrix url of the users avatar.
pub avatar_url: Option<String>,
/// The time, in ms, since the user interacted with the server.
@@ -43,7 +42,7 @@ pub struct RoomMember {
/// If the user should be considered active.
pub currently_active: Option<bool>,
/// The unique id of the room.
pub room_id: Option<String>,
pub room_id: RoomId,
/// If the member is typing.
pub typing: Option<bool>,
/// The presence of the user, if found.
@@ -54,15 +53,19 @@ pub struct RoomMember {
pub power_level: Option<Int>,
/// The normalized power level of this `RoomMember` (0-100).
pub power_level_norm: Option<Int>,
/// The `MembershipState` of this `RoomMember`.
pub membership: MembershipState,
/// The human readable name of this room member.
pub name: String,
// FIXME: The docstring below is currently a lie since we only store the initial event that
// creates the member (the one we pass to RoomMember::new).
//
// The intent of this field is to keep the last (or last few?) state events related to the room
// member cached so we can quickly go back to the previous one in case some of them get
// redacted. Keeping all state for each room member is probably too much.
//
// Needs design.
/// The events that created the state of this room member.
#[serde(deserialize_with = "super::event_deser::deserialize_events")]
pub events: Vec<Event>,
pub events: Vec<SyncStateEvent<MemberEventContent>>,
/// The `PresenceEvent`s connected to this user.
#[serde(deserialize_with = "super::event_deser::deserialize_presence")]
pub presence_events: Vec<PresenceEvent>,
}
@@ -73,19 +76,28 @@ impl PartialEq for RoomMember {
&& self.user_id == other.user_id
&& self.name == other.name
&& self.display_name == other.display_name
&& self.display_name_ambiguous == other.display_name_ambiguous
&& self.avatar_url == other.avatar_url
&& self.last_active_ago == other.last_active_ago
&& self.membership == other.membership
}
}
impl RoomMember {
pub fn new(event: &MemberEvent) -> Self {
/// Create a new room member.
///
/// # Arguments
///
/// * `event` - event associated with a member joining, leaving or getting
/// invited to a room.
///
/// * `room_id` - The unique id of the room this member is part of.
pub fn new(event: &SyncStateEvent<MemberEventContent>, room_id: &RoomId) -> Self {
Self {
name: event.state_key.clone(),
room_id: event.room_id.as_ref().map(|id| id.to_string()),
room_id: room_id.clone(),
user_id: UserId::try_from(event.state_key.as_str()).unwrap(),
display_name: event.content.displayname.clone(),
display_name_ambiguous: false,
avatar_url: event.content.avatar_url.clone(),
presence: None,
status_msg: None,
@@ -94,149 +106,96 @@ impl RoomMember {
typing: None,
power_level: None,
power_level_norm: None,
membership: event.content.membership,
presence_events: Vec::default(),
events: vec![Event::RoomMember(event.clone())],
events: vec![event.clone()],
}
}
pub fn update_member(&mut self, event: &MemberEvent) -> bool {
use MembershipChange::*;
match event.membership_change() {
ProfileChanged => {
self.display_name = event.content.displayname.clone();
self.avatar_url = event.content.avatar_url.clone();
true
}
Banned | Kicked | KickedAndBanned | InvitationRejected | InvitationRevoked | Left
| Unbanned | Joined | Invited => {
self.membership = event.content.membership;
true
}
NotImplemented => false,
None => false,
// we ignore the error here as only a buggy or malicious server would send this
Error => false,
}
/// Returns the most ergonomic (but potentially ambiguous/non-unique) name
/// available for the member.
///
/// This is the member's display name if it is set, otherwise their MXID.
pub fn name(&self) -> String {
self.display_name
.clone()
.unwrap_or_else(|| format!("{}", self.user_id))
}
pub fn update_power(&mut self, event: &PowerLevelsEvent, max_power: Int) -> bool {
let changed;
if let Some(user_power) = event.content.users.get(&self.user_id) {
changed = self.power_level != Some(*user_power);
self.power_level = Some(*user_power);
/// Returns a name for the member which is guaranteed to be unique, but not
/// necessarily the most ergonomic.
///
/// This is either a name in the format "DISPLAY_NAME (MXID)" if the
/// member's display name is set, or simply "MXID" if not.
pub fn unique_name(&self) -> String {
self.display_name
.clone()
.map(|d| format!("{} ({})", d, self.user_id))
.unwrap_or_else(|| format!("{}", self.user_id))
}
/// Get the disambiguated display name for the member which is as ergonomic
/// as possible while still guaranteeing it is unique.
///
/// If the member's display name is currently ambiguous (i.e. shared by
/// other room members), this method will return the same result as
/// `RoomMember::unique_name`. Otherwise, this method will return the same
/// result as `RoomMember::name`.
///
/// This is usually the name you want when showing room messages from the
/// member or when showing the member in the member list.
///
/// **Warning**: When displaying a room member's display name, clients
/// *must* use a disambiguated name, so they *must not* use
/// `RoomMember::display_name` directly. Clients *should* use this method to
/// obtain the name, but an acceptable alternative is to use
/// `RoomMember::unique_name` in certain situations.
pub fn disambiguated_name(&self) -> String {
if self.display_name_ambiguous {
self.unique_name()
} else {
changed = self.power_level != Some(event.content.users_default);
self.power_level = Some(event.content.users_default);
self.name()
}
if max_power > Int::from(0) {
self.power_level_norm = Some((self.power_level.unwrap() * Int::from(100)) / max_power);
}
changed
}
/// If the current `PresenceEvent` updated the state of this `User`.
///
/// Returns true if the specific users presence has changed, false otherwise.
///
/// # Arguments
///
/// * `presence` - The presence event for a this room member.
pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool {
let PresenceEvent {
content:
PresenceEventContent {
avatar_url,
currently_active,
displayname,
last_active_ago,
presence,
status_msg,
},
..
} = presence;
self.display_name == *displayname
&& self.avatar_url == *avatar_url
&& self.presence.as_ref() == Some(presence)
&& self.status_msg == *status_msg
&& self.last_active_ago == *last_active_ago
&& self.currently_active == *currently_active
}
/// Updates the `User`s presence.
///
/// This should only be used if `did_update_presence` was true.
///
/// # Arguments
///
/// * `presence` - The presence event for a this room member.
pub fn update_presence(&mut self, presence_ev: &PresenceEvent) {
let PresenceEvent {
content:
PresenceEventContent {
avatar_url,
currently_active,
displayname,
last_active_ago,
presence,
status_msg,
},
..
} = presence_ev;
self.presence_events.push(presence_ev.clone());
self.avatar_url = avatar_url.clone();
self.currently_active = *currently_active;
self.display_name = displayname.clone();
self.last_active_ago = *last_active_ago;
self.presence = Some(*presence);
self.status_msg = status_msg.clone();
}
}
#[cfg(test)]
mod test {
use matrix_sdk_test::{async_test, EventBuilder, EventsFile};
use matrix_sdk_test::{async_test, EventBuilder, EventsJson};
use crate::events::collections::all::RoomEvent;
use crate::events::room::member::MembershipState;
use crate::identifiers::{RoomId, UserId};
use crate::{BaseClient, Session};
use crate::js_int::Int;
use crate::{
identifiers::{room_id, user_id, RoomId},
int, BaseClient, Session,
};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use std::convert::TryFrom;
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
user_id: user_id!("@example:localhost"),
device_id: "DEVICEID".into(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client
}
fn get_room_id() -> RoomId {
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
// TODO: Move this to EventBuilder since it's a magic room ID used in EventBuilder's example
// events.
fn test_room_id() -> RoomId {
room_id!("!SVkFJHzfwvuaIEawgC:localhost")
}
#[async_test]
async fn room_member_events() {
let client = get_client().await;
let room_id = get_room_id();
let room_id = test_room_id();
let mut response = EventBuilder::default()
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
.add_room_event(EventsJson::Member)
.add_room_event(EventsJson::PowerLevels)
.build_sync_response();
client.receive_sync_response(&mut response).await.unwrap();
@@ -245,23 +204,72 @@ mod test {
let room = room.read().await;
let member = room
.members
.get(&UserId::try_from("@example:localhost").unwrap())
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.membership, MembershipState::Join);
assert_eq!(member.power_level, Int::new(100));
assert_eq!(member.power_level, Some(int!(100)));
}
#[async_test]
async fn room_member_display_name_change() {
let client = get_client().await;
let room_id = test_room_id();
let mut builder = EventBuilder::default();
let mut initial_response = builder
.add_room_event(EventsJson::Member)
.build_sync_response();
let mut name_change_response = builder
.add_room_event(EventsJson::MemberNameChange)
.build_sync_response();
client
.receive_sync_response(&mut initial_response)
.await
.unwrap();
let room = client.get_joined_room(&room_id).await.unwrap();
// Initially, the display name is "example".
{
let room = room.read().await;
let member = room
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.display_name.as_ref().unwrap(), "example");
}
client
.receive_sync_response(&mut name_change_response)
.await
.unwrap();
// Afterwards, the display name is "changed".
{
let room = room.read().await;
let member = room
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.display_name.as_ref().unwrap(), "changed");
}
}
#[async_test]
async fn member_presence_events() {
let client = get_client().await;
let room_id = get_room_id();
let room_id = test_room_id();
let mut response = EventBuilder::default()
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
.add_presence_event(EventsFile::Presence)
.add_room_event(EventsJson::Member)
.add_room_event(EventsJson::PowerLevels)
.add_presence_event(EventsJson::Presence)
.build_sync_response();
client.receive_sync_response(&mut response).await.unwrap();
@@ -270,12 +278,11 @@ mod test {
let room = room.read().await;
let member = room
.members
.get(&UserId::try_from("@example:localhost").unwrap())
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.membership, MembershipState::Join);
assert_eq!(member.power_level, Int::new(100));
assert_eq!(member.power_level, Some(int!(100)));
assert!(member.avatar_url.is_none());
assert_eq!(member.last_active_ago, None);
+6 -3
View File
@@ -15,15 +15,18 @@
//! User sessions.
use crate::identifiers::UserId;
use serde::{Deserialize, Serialize};
use matrix_sdk_common::identifiers::{DeviceId, UserId};
/// A user session, containing an access token and information about the
/// associated user account.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Session {
/// The access token used for this session.
pub access_token: String,
/// The user the access token was issued for.
pub user_id: UserId,
/// The ID of the client device
pub device_id: String,
pub device_id: Box<DeviceId>,
}
+67 -95
View File
@@ -1,16 +1,15 @@
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
use std::{
collections::HashMap,
fmt, fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use matrix_sdk_common::identifiers::RoomId;
use matrix_sdk_common::locks::RwLock;
use tokio::fs as async_fs;
use tokio::io::AsyncWriteExt;
use matrix_sdk_common::{async_trait, identifiers::RoomId, locks::RwLock};
use tokio::{fs as async_fs, io::AsyncWriteExt};
use super::{AllRooms, ClientState, StateStore};
use crate::{Error, Result, Room, RoomState, Session};
@@ -18,7 +17,7 @@ use crate::{Error, Result, Room, RoomState, Session};
/// A default `StateStore` implementation that serializes state as json
/// and saves it to disk.
///
/// When logged in the `JsonStore` appends the user_id to it's folder path,
/// When logged in the `JsonStore` appends the user_id to its folder path,
/// so all files are saved in `my_client/user_id_localpart/*`.
pub struct JsonStore {
path: Arc<RwLock<PathBuf>>,
@@ -39,6 +38,36 @@ impl JsonStore {
user_path_set: AtomicBool::new(false),
})
}
/// Build a path for a file where the Room state to be stored in.
async fn build_room_path(&self, room_state: &str, room_id: &RoomId) -> PathBuf {
let mut path = self.path.read().await.clone();
path.push("rooms");
path.push(room_state);
path.push(JsonStore::sanitize_room_id(room_id));
path.set_extension("json");
path
}
/// Build a path for the file where the Client state to be stored in.
async fn build_client_path(&self) -> PathBuf {
let mut path = self.path.read().await.clone();
path.push("client");
path.set_extension("json");
path
}
/// Replace common characters that can't be used in a file name with an
/// underscore.
fn sanitize_room_id(room_id: &RoomId) -> String {
room_id.as_str().replace(
&['.', ':', '<', '>', '"', '/', '\\', '|', '?', '*'][..],
"_",
)
}
}
impl fmt::Debug for JsonStore {
@@ -49,7 +78,7 @@ impl fmt::Debug for JsonStore {
}
}
#[async_trait::async_trait]
#[async_trait]
impl StateStore for JsonStore {
async fn load_client_state(&self, sess: &Session) -> Result<Option<ClientState>> {
if !self.user_path_set.load(Ordering::SeqCst) {
@@ -57,8 +86,7 @@ impl StateStore for JsonStore {
self.path.write().await.push(sess.user_id.localpart())
}
let mut path = self.path.read().await.clone();
path.push("client.json");
let path = self.build_client_path().await;
let json = async_fs::read_to_string(path)
.await
@@ -93,7 +121,6 @@ impl StateStore for JsonStore {
}
let json = async_fs::read_to_string(&file).await?;
let room = serde_json::from_str::<Room>(&json).map_err(Error::from)?;
let room_id = room.room_id.clone();
@@ -115,8 +142,7 @@ impl StateStore for JsonStore {
}
async fn store_client_state(&self, state: ClientState) -> Result<()> {
let mut path = self.path.read().await.clone();
path.push("client.json");
let path = self.build_client_path().await;
if !path.exists() {
let mut dir = path.clone();
@@ -147,9 +173,7 @@ impl StateStore for JsonStore {
self.path.write().await.push(room.own_user_id.localpart())
}
let mut path = self.path.read().await.clone();
path.push("rooms");
path.push(&format!("{}/{}.json", room_state, room.room_id));
let path = self.build_room_path(room_state, &room.room_id).await;
if !path.exists() {
let mut dir = path.clone();
@@ -179,15 +203,13 @@ impl StateStore for JsonStore {
return Err(Error::StateStore("path for JsonStore not set".into()));
}
let mut to_del = self.path.read().await.clone();
to_del.push("rooms");
to_del.push(&format!("{}/{}.json", room_state, room_id));
let path = self.build_room_path(room_state, room_id).await;
if !to_del.exists() {
return Err(Error::StateStore(format!("file {:?} not found", to_del)));
if !path.exists() {
return Err(Error::StateStore(format!("file {:?} not found", path)));
}
tokio::fs::remove_file(to_del).await.map_err(Error::from)
tokio::fs::remove_file(path).await.map_err(Error::from)
}
}
@@ -195,43 +217,33 @@ impl StateStore for JsonStore {
mod test {
use super::*;
use http::Response;
use std::convert::TryFrom;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use tempfile::tempdir;
use crate::api::r0::sync::sync_events::Response as SyncResponse;
use crate::identifiers::{RoomId, UserId};
use crate::{BaseClient, BaseClientConfig, Session};
fn sync_response(file: &str) -> SyncResponse {
let mut file = File::open(file).unwrap();
let mut data = vec![];
file.read_to_end(&mut data).unwrap();
let response = Response::builder().body(data).unwrap();
SyncResponse::try_from(response).unwrap()
}
use crate::{
identifiers::{room_id, user_id},
push::Ruleset,
Session,
};
#[tokio::test]
async fn test_store_client_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let user = UserId::try_from("@example:example.com").unwrap();
let user = user_id!("@example:example.com");
let sess = Session {
access_token: "32nj9zu034btz90".to_string(),
user_id: user.clone(),
device_id: "Tester".to_string(),
device_id: "Tester".into(),
};
let state = ClientState {
sync_token: Some("hello".into()),
ignored_users: vec![user],
push_ruleset: None,
push_ruleset: None::<Ruleset>,
};
let mut path_with_user = PathBuf::from(path);
@@ -252,8 +264,8 @@ mod test {
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
@@ -270,8 +282,8 @@ mod test {
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
@@ -288,8 +300,8 @@ mod test {
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
@@ -306,8 +318,8 @@ mod test {
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
@@ -330,8 +342,8 @@ mod test {
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
@@ -346,44 +358,4 @@ mod test {
// test that we have removed the correct room
assert!(invited.is_empty());
}
#[tokio::test]
async fn test_client_sync_store() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@cheeky_monkey:matrix.org").unwrap(),
device_id: "DEVICEID".to_owned(),
};
// a sync response to populate our JSON store
let store = Box::new(JsonStore::open(path).unwrap());
let client =
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
client.restore_login(session.clone()).await.unwrap();
let mut response = sync_response("../test_data/sync.json");
// gather state to save to the db, the first time through loading will be skipped
client.receive_sync_response(&mut response).await.unwrap();
// now syncing the client will update from the state store
let store = Box::new(JsonStore::open(path).unwrap());
let client =
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
client.restore_login(session.clone()).await.unwrap();
// assert the synced client and the logged in client are equal
assert_eq!(*client.session().read().await, Some(session));
assert_eq!(
client.sync_token().await,
Some("s526_47314_0_7_1_1_1_11444_1".to_string())
);
assert_eq!(
*client.ignored_users.read().await,
vec![UserId::try_from("@someone:example.org").unwrap()]
);
}
}
+69 -59
View File
@@ -15,6 +15,12 @@
use std::collections::HashMap;
use matrix_sdk_common::{
async_trait,
identifiers::{RoomId, UserId},
push::Ruleset,
AsyncTraitDeps,
};
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
@@ -22,10 +28,10 @@ mod json_store;
#[cfg(not(target_arch = "wasm32"))]
pub use json_store::JsonStore;
use crate::client::{BaseClient, Token};
use crate::events::push_rules::Ruleset;
use crate::identifiers::{RoomId, UserId};
use crate::{Result, Room, RoomState, Session};
use crate::{
client::{BaseClient, Token},
Result, Room, RoomState, Session,
};
/// `ClientState` holds all the information to restore a `BaseClient`
/// except the `access_token` as the default store is not secure.
@@ -84,8 +90,9 @@ pub struct AllRooms {
}
/// Abstraction around the data store to avoid unnecessary request on client initialization.
#[async_trait::async_trait]
pub trait StateStore: Send + Sync {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStore: AsyncTraitDeps {
/// Loads the state of `BaseClient` through `ClientState` type.
///
/// An `Option::None` should be returned only if the `StateStore` tries to
@@ -114,14 +121,13 @@ mod test {
use super::*;
use std::collections::HashMap;
use std::convert::TryFrom;
use crate::identifiers::RoomId;
use crate::identifiers::{room_id, user_id};
#[test]
fn serialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
@@ -140,64 +146,68 @@ mod test {
#[cfg(not(feature = "messages"))]
assert_eq!(
r#"{
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"members": {},
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}"#,
serde_json::to_string_pretty(&joined_rooms).unwrap()
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"direct_target": null,
"joined_members": {},
"invited_members": {},
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}),
serde_json::to_value(&joined_rooms).unwrap()
);
#[cfg(feature = "messages")]
assert_eq!(
r#"{
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"members": {},
"messages": [],
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}"#,
serde_json::to_string_pretty(&joined_rooms).unwrap()
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"direct_target": null,
"joined_members": {},
"invited_members": {},
"messages": [],
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}),
serde_json::to_value(&joined_rooms).unwrap()
);
}
#[test]
fn deserialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
+17 -14
View File
@@ -1,6 +1,6 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
description = "Collection of common types used in the matrix-sdk"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "Collection of common types and imports used in the matrix-sdk"
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
@@ -8,24 +8,27 @@ license = "Apache-2.0"
name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.2.0"
[features]
unstable-synapse-quirks = ["ruma/unstable-synapse-quirks"]
[dependencies]
js_int = "0.1.5"
ruma-api = "0.16.1"
ruma-client-api = "0.9.0"
ruma-events = "0.21.2"
ruma-identifiers = "0.16.1"
instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] }
instant = { version = "0.1.9", features = ["wasm-bindgen", "now"] }
async-trait = "0.1.42"
[dependencies.ruma]
version = "0.0.2"
features = ["client-api", "unstable-pre-spec"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
uuid = { version = "0.8.1", features = ["v4"] }
uuid = { version = "0.8.1", default-features = false, features = ["v4", "serde"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.21"
version = "0.2.24"
default-features = false
features = ["sync", "time", "fs"]
features = ["sync"]
[target.'cfg(target_arch = "wasm32")'.dependencies]
futures-locks = { version = "0.5.0", default-features = false }
uuid = { version = "0.8.1", features = ["v4", "wasm-bindgen"] }
futures-locks = { version = "0.6.0", default-features = false }
uuid = { version = "0.8.1", default-features = false, features = ["v4", "wasm-bindgen"] }
+26 -7
View File
@@ -1,13 +1,32 @@
pub use async_trait::async_trait;
pub use instant;
pub use js_int;
pub use ruma_api::{
error::{FromHttpResponseError, IntoHttpError, ServerError},
Endpoint,
pub use ruma::{
api::{
client as api,
error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError, ServerError},
AuthScheme, EndpointError, OutgoingRequest,
},
assign, directory, encryption, events, identifiers, int, presence, push,
serde::{CanonicalJsonValue, Raw},
thirdparty, uint, Int, Outgoing, UInt,
};
pub use ruma_client_api as api;
pub use ruma_events as events;
pub use ruma_identifiers as identifiers;
pub use uuid;
pub mod locks;
/// Super trait that is used for our store traits, this trait will differ if
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
/// implemented, while other targets will.
#[cfg(not(target_arch = "wasm32"))]
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
/// Super trait that is used for our store traits, this trait will differ if
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
/// implemented, while other targets will.
#[cfg(target_arch = "wasm32")]
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
#[cfg(target_arch = "wasm32")]
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
+2 -6
View File
@@ -3,11 +3,7 @@
// https://www.reddit.com/r/rust/comments/f4zldz/i_audited_3_different_implementation_of_async/
#[cfg(target_arch = "wasm32")]
pub use futures_locks::Mutex;
#[cfg(target_arch = "wasm32")]
pub use futures_locks::RwLock;
pub use futures_locks::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::sync::Mutex;
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::sync::RwLock;
pub use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
+35 -27
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "Matrix encryption library"
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,44 +8,52 @@ license = "Apache-2.0"
name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.2.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = []
sqlite-cryptostore = ["sqlx"]
sqlite_cryptostore = ["sqlx"]
docs = ["sqlite_cryptostore"]
[dependencies]
async-trait = "0.1.31"
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
olm-rs = { version = "0.5.0", features = ["serde"] }
serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.53"
cjson = "0.1.0"
zeroize = { version = "1.1.0", features = ["zeroize_derive"] }
url = "2.1.1"
olm-rs = { version = "1.0.0", features = ["serde"] }
getrandom = "0.2.1"
serde = { version = "1.0.118", features = ["derive", "rc"] }
serde_json = "1.0.61"
zeroize = { version = "1.2.0", features = ["zeroize_derive"] }
url = "2.2.0"
# Misc dependencies
thiserror = "1.0.19"
tracing = "0.1.14"
atomic = "0.4.5"
dashmap = "3.11.2"
[dependencies.tracing-futures]
version = "0.2.4"
default-features = false
features = ["std", "std-future"]
thiserror = "1.0.23"
tracing = "0.1.22"
atomic = "0.5.0"
dashmap = "4.0.1"
sha2 = "0.9.2"
aes-gcm = "0.8.0"
aes-ctr = "0.6.0"
pbkdf2 = { version = "0.6.0", default-features = false }
hmac = "0.10.1"
base64 = "0.13.0"
byteorder = "1.3.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.sqlx]
version = "0.3.5"
version = "0.4.2"
optional = true
default-features = false
features = ["runtime-tokio", "sqlite"]
features = ["runtime-tokio-native-tls", "sqlite", "macros"]
[dev-dependencies]
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
serde_json = "1.0.53"
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
futures = "0.3.8"
proptest = "0.10.1"
serde_json = "1.0.61"
tempfile = "3.1.0"
http = "0.2.1"
http = "0.2.2"
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
indoc = "1.0.3"
-319
View File
@@ -1,319 +0,0 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
#[cfg(test)]
use std::convert::TryFrom;
use std::mem;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use atomic::Atomic;
#[cfg(test)]
use super::OlmMachine;
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
use matrix_sdk_common::events::Algorithm;
use matrix_sdk_common::identifiers::{DeviceId, UserId};
/// A device represents a E2EE capable client of an user.
#[derive(Debug, Clone)]
pub struct Device {
user_id: Arc<UserId>,
device_id: Arc<DeviceId>,
algorithms: Arc<Vec<Algorithm>>,
keys: Arc<BTreeMap<KeyAlgorithm, String>>,
display_name: Arc<Option<String>>,
deleted: Arc<AtomicBool>,
trust_state: Arc<Atomic<TrustState>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
/// The trust state of a device.
pub enum TrustState {
/// The device has been verified and is trusted.
Verified = 0,
/// The device been blacklisted from communicating.
BlackListed = 1,
/// The trust state of the device is being ignored.
Ignored = 2,
/// The trust state is unset.
Unset = 3,
}
impl From<i64> for TrustState {
fn from(state: i64) -> Self {
match state {
0 => TrustState::Verified,
1 => TrustState::BlackListed,
2 => TrustState::Ignored,
3 => TrustState::Unset,
_ => TrustState::Unset,
}
}
}
impl Device {
/// Create a new Device.
pub fn new(
user_id: UserId,
device_id: DeviceId,
display_name: Option<String>,
trust_state: TrustState,
algorithms: Vec<Algorithm>,
keys: BTreeMap<KeyAlgorithm, String>,
) -> Self {
Device {
user_id: Arc::new(user_id),
device_id: Arc::new(device_id),
display_name: Arc::new(display_name),
trust_state: Arc::new(Atomic::new(trust_state)),
algorithms: Arc::new(algorithms),
keys: Arc::new(keys),
deleted: Arc::new(AtomicBool::new(false)),
}
}
/// The user id of the device owner.
pub fn user_id(&self) -> &UserId {
&self.user_id
}
/// The unique ID of the device.
pub fn device_id(&self) -> &DeviceId {
&self.device_id
}
/// Get the human readable name of the device.
pub fn display_name(&self) -> &Option<String> {
&self.display_name
}
/// Get the key of the given key algorithm belonging to this device.
pub fn get_key(&self, algorithm: KeyAlgorithm) -> Option<&String> {
self.keys.get(&algorithm)
}
/// Get a map containing all the device keys.
pub fn keys(&self) -> &BTreeMap<KeyAlgorithm, String> {
&self.keys
}
/// Get the trust state of the device.
pub fn trust_state(&self) -> TrustState {
self.trust_state.load(Ordering::Relaxed)
}
/// Get the list of algorithms this device supports.
pub fn algorithms(&self) -> &[Algorithm] {
&self.algorithms
}
/// Is the device deleted.
pub fn deleted(&self) -> bool {
self.deleted.load(Ordering::Relaxed)
}
/// Update a device with a new device keys struct.
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) {
let mut keys = BTreeMap::new();
for (key_id, key) in device_keys.keys.iter() {
let key_id = key_id.0;
let _ = keys.insert(key_id, key.clone());
}
let display_name = Arc::new(
device_keys
.unsigned
.as_ref()
.map(|d| d.device_display_name.clone())
.flatten(),
);
let _ = mem::replace(
&mut self.algorithms,
Arc::new(device_keys.algorithms.clone()),
);
let _ = mem::replace(&mut self.keys, Arc::new(keys));
let _ = mem::replace(&mut self.display_name, display_name);
}
/// Mark the device as deleted.
pub(crate) fn mark_as_deleted(&self) {
self.deleted.store(true, Ordering::Relaxed);
}
}
#[cfg(test)]
impl From<&OlmMachine> for Device {
fn from(machine: &OlmMachine) -> Self {
Device {
user_id: Arc::new(machine.user_id().clone()),
device_id: Arc::new(machine.device_id().clone()),
algorithms: Arc::new(vec![
Algorithm::MegolmV1AesSha2,
Algorithm::OlmV1Curve25519AesSha2,
]),
keys: Arc::new(
machine
.identity_keys()
.iter()
.map(|(key, value)| {
(
KeyAlgorithm::try_from(key.as_ref()).unwrap(),
value.to_owned(),
)
})
.collect(),
),
display_name: Arc::new(None),
deleted: Arc::new(AtomicBool::new(false)),
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
}
}
}
impl From<&DeviceKeys> for Device {
fn from(device_keys: &DeviceKeys) -> Self {
let mut keys = BTreeMap::new();
for (key_id, key) in device_keys.keys.iter() {
let key_id = key_id.0;
let _ = keys.insert(key_id, key.clone());
}
Device {
user_id: Arc::new(device_keys.user_id.clone()),
device_id: Arc::new(device_keys.device_id.clone()),
algorithms: Arc::new(device_keys.algorithms.clone()),
keys: Arc::new(keys),
display_name: Arc::new(
device_keys
.unsigned
.as_ref()
.map(|d| d.device_display_name.clone())
.flatten(),
),
deleted: Arc::new(AtomicBool::new(false)),
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
}
}
}
impl PartialEq for Device {
fn eq(&self, other: &Self) -> bool {
self.user_id() == other.user_id() && self.device_id() == other.device_id()
}
}
#[cfg(test)]
pub(crate) mod test {
use serde_json::json;
use std::convert::{From, TryFrom};
use crate::device::{Device, TrustState};
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
use matrix_sdk_common::identifiers::UserId;
fn device_keys() -> DeviceKeys {
let user_id = UserId::try_from("@alice:example.org").unwrap();
let device_id = "DEVICEID";
let device_keys = json!({
"algorithms": vec![
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": device_id,
"user_id": user_id.to_string(),
"keys": {
"curve25519:DEVICEID": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
"ed25519:DEVICEID": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
},
"signatures": {
user_id.to_string(): {
"ed25519:DEVICEID": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA"
}
},
"unsigned": {
"device_display_name": "Alice's mobile phone"
}
});
serde_json::from_value(device_keys).unwrap()
}
pub(crate) fn get_device() -> Device {
let device_keys = device_keys();
Device::from(&device_keys)
}
#[test]
fn create_a_device() {
let user_id = UserId::try_from("@alice:example.org").unwrap();
let device_id = "DEVICEID";
let device = get_device();
assert_eq!(&user_id, device.user_id());
assert_eq!(device_id, device.device_id());
assert_eq!(device.algorithms.len(), 2);
assert_eq!(TrustState::Unset, device.trust_state());
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
assert_eq!(
device.get_key(KeyAlgorithm::Curve25519).unwrap(),
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
);
assert_eq!(
device.get_key(KeyAlgorithm::Ed25519).unwrap(),
"nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
);
}
#[test]
fn update_a_device() {
let mut device = get_device();
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
let mut device_keys = device_keys();
device_keys.unsigned.as_mut().unwrap().device_display_name =
Some("Alice's work computer".to_owned());
device.update_device(&device_keys);
assert_eq!(
"Alice's work computer",
device.display_name().as_ref().unwrap()
);
}
#[test]
fn delete_a_device() {
let device = get_device();
assert!(!device.deleted());
let device_clone = device.clone();
device.mark_as_deleted();
assert!(device.deleted());
assert!(device_clone.deleted());
}
}
+68 -11
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use cjson::Error as CjsonError;
use matrix_sdk_common::identifiers::{DeviceId, Error as IdentifierError, UserId};
use olm_rs::errors::{OlmGroupSessionError, OlmSessionError};
use serde_json::Error as SerdeError;
use thiserror::Error;
@@ -47,8 +47,23 @@ pub enum OlmError {
Store(#[from] CryptoStoreError),
/// The session with a device has become corrupted.
#[error("decryption failed likely because a Olm session was wedged")]
SessionWedged,
#[error(
"decryption failed likely because an Olm session from {0} with sender key {1} was wedged"
)]
SessionWedged(UserId, String),
/// An Olm message got replayed while the Olm ratchet has already moved
/// forward.
#[error("decryption failed because an Olm message from {0} with sender key {1} was replayed")]
ReplayedMessage(UserId, String),
/// Encryption failed because the device does not have a valid Olm session
/// with us.
#[error(
"encryption failed because the device does not \
have a valid Olm session with us"
)]
MissingSession,
}
/// Error representing a failure during a group encryption operation.
@@ -93,6 +108,9 @@ pub enum EventError {
#[error("the Encrypted message is missing the signing key of the sender")]
MissingSigningKey,
#[error("the Encrypted message is missing the sender key")]
MissingSenderKey,
#[error("the Encrypted message is missing the field {0}")]
MissingField(String),
@@ -104,22 +122,61 @@ pub enum EventError {
}
#[derive(Error, Debug)]
pub(crate) enum SignatureError {
pub enum SessionUnpicklingError {
/// The underlying Olm session operation returned an error.
#[error("can't finish Olm Session operation {0}")]
OlmSession(#[from] OlmSessionError),
/// The Session timestamp was invalid.
#[error("can't load session timestamps")]
SessionTimestampError,
}
#[derive(Error, Debug)]
pub enum SignatureError {
#[error("the signature used a unsupported algorithm")]
UnsupportedAlgorithm,
#[error("the key id of the signing key is invalid")]
InvalidKeyId(#[from] IdentifierError),
#[error("the signing key is missing from the object that signed the message")]
MissingSigningKey,
#[error("the user id of the signing differs from the subkey user id")]
UserIdMissmatch,
#[error("the provided JSON value isn't an object")]
NotAnObject,
#[error("the provided JSON object doesn't contain a signatures field")]
NoSignatureFound,
#[error("the provided JSON object can't be converted to a canonical representation")]
CanonicalJsonError(CjsonError),
#[error("the signature didn't match the provided key")]
VerificationError,
#[error(transparent)]
JsonError(#[from] SerdeError),
}
impl From<CjsonError> for SignatureError {
fn from(error: CjsonError) -> Self {
Self::CanonicalJsonError(error)
}
#[derive(Error, Debug)]
pub(crate) enum SessionCreationError {
#[error(
"Failed to create a new Olm session for {0} {1}, the requested \
one-time key isn't a signed curve key"
)]
OneTimeKeyNotSigned(UserId, Box<DeviceId>),
#[error(
"Tried to create a new Olm session for {0} {1}, but the signed \
one-time key is missing"
)]
OneTimeKeyMissing(UserId, Box<DeviceId>),
#[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")]
InvalidSignature(UserId, Box<DeviceId>, SignatureError),
#[error(
"Tried to create an Olm session for {0} {1}, but the device is missing \
a curve25519 key"
)]
DeviceMissingCurveKey(UserId, Box<DeviceId>),
#[error("Error creating new Olm session for {0} {1}: {2:?}")]
OlmError(UserId, Box<DeviceId>, OlmSessionError),
}
@@ -0,0 +1,347 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::BTreeMap,
io::{Error as IoError, ErrorKind, Read},
};
use thiserror::Error;
use zeroize::Zeroizing;
use serde::{Deserialize, Serialize};
use matrix_sdk_common::events::room::JsonWebKey;
use getrandom::getrandom;
use aes_ctr::{
cipher::{NewStreamCipher, SyncStreamCipher},
Aes256Ctr,
};
use base64::DecodeError;
use sha2::{Digest, Sha256};
use crate::utilities::{decode, decode_url_safe, encode, encode_url_safe};
const IV_SIZE: usize = 16;
const KEY_SIZE: usize = 32;
const VERSION: &str = "v2";
/// A wrapper that transparently encrypts anything that implements `Read` as an
/// Matrix attachment.
#[derive(Debug)]
pub struct AttachmentDecryptor<'a, R: 'a + Read> {
inner_reader: &'a mut R,
expected_hash: Vec<u8>,
sha: Sha256,
aes: Aes256Ctr,
}
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read_bytes = self.inner_reader.read(buf)?;
if read_bytes == 0 {
let hash = self.sha.finalize_reset();
if hash.as_slice() == self.expected_hash.as_slice() {
Ok(0)
} else {
Err(IoError::new(
ErrorKind::Other,
"Hash missmatch while decrypting",
))
}
} else {
self.sha.update(&buf[0..read_bytes]);
self.aes.apply_keystream(&mut buf[0..read_bytes]);
Ok(read_bytes)
}
}
}
/// Error type for attachment decryption.
#[derive(Error, Debug)]
pub enum DecryptorError {
/// Some data in the encrypted attachment coldn't be decoded, this may be a
/// hash, the secret key, or the initialization vector.
#[error(transparent)]
Decode(#[from] DecodeError),
/// A hash is missing from the encryption info.
#[error("The encryption info is missing a hash")]
MissingHash,
/// The supplied key or IV has an invalid length.
#[error("The supplied key or IV has an invalid length.")]
KeyNonceLength,
/// The supplied data was encrypted with an unknown version of the
/// attachment encryption spec.
#[error("Unknown version for the encrypted attachment.")]
UnknownVersion,
}
impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> {
/// Wrap the given reader decrypting all the data we read from it.
///
/// # Arguments
///
/// * `reader` - The `Reader` that should be wrapped and decrypted.
///
/// * `info` - The encryption info that is necessary to decrypt data from
/// the reader.
///
/// # Examples
/// ```
/// # use std::io::{Cursor, Read};
/// # use matrix_sdk_crypto::{AttachmentEncryptor, AttachmentDecryptor};
/// let data = "Hello world".to_owned();
/// let mut cursor = Cursor::new(data.clone());
///
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
///
/// let mut encrypted = Vec::new();
/// encryptor.read_to_end(&mut encrypted).unwrap();
/// let info = encryptor.finish();
///
/// let mut cursor = Cursor::new(encrypted);
/// let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).unwrap();
/// let mut decrypted_data = Vec::new();
/// decryptor.read_to_end(&mut decrypted_data).unwrap();
///
/// let decrypted = String::from_utf8(decrypted_data).unwrap();
/// ```
pub fn new(
input: &'a mut R,
info: EncryptionInfo,
) -> Result<AttachmentDecryptor<'a, R>, DecryptorError> {
if info.version != VERSION {
return Err(DecryptorError::UnknownVersion);
}
let hash = decode(
info.hashes
.get("sha256")
.ok_or(DecryptorError::MissingHash)?,
)?;
let key = Zeroizing::from(decode_url_safe(info.web_key.k)?);
let iv = decode(info.iv)?;
let sha = Sha256::default();
let aes = Aes256Ctr::new_var(&key, &iv).map_err(|_| DecryptorError::KeyNonceLength)?;
Ok(AttachmentDecryptor {
inner_reader: input,
expected_hash: hash,
sha,
aes,
})
}
}
/// A wrapper that transparently encrypts anything that implements `Read`.
#[derive(Debug)]
pub struct AttachmentEncryptor<'a, R: Read + 'a> {
finished: bool,
inner_reader: &'a mut R,
web_key: JsonWebKey,
iv: String,
hashes: BTreeMap<String, String>,
aes: Aes256Ctr,
sha: Sha256,
}
impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read_bytes = self.inner_reader.read(buf)?;
if read_bytes == 0 {
let hash = self.sha.finalize_reset();
self.hashes
.entry("sha256".to_owned())
.or_insert_with(|| encode(hash));
Ok(0)
} else {
self.aes.apply_keystream(&mut buf[0..read_bytes]);
self.sha.update(&buf[0..read_bytes]);
Ok(read_bytes)
}
}
}
impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
/// Wrap the given reader encrypting all the data we read from it.
///
/// After all the reads are done, and all the data is encrypted that we wish
/// to encrypt a call to [`finish()`](#method.finish) is necessary to get
/// the decryption key for the data.
///
/// # Arguments
///
/// * `reader` - The `Reader` that should be wrapped and enrypted.
///
/// # Panics
///
/// Panics if we can't generate enough random data to create a fresh
/// encryption key.
///
/// # Examples
/// ```
/// # use std::io::{Cursor, Read};
/// # use matrix_sdk_crypto::AttachmentEncryptor;
/// let data = "Hello world".to_owned();
/// let mut cursor = Cursor::new(data.clone());
///
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
///
/// let mut encrypted = Vec::new();
/// encryptor.read_to_end(&mut encrypted).unwrap();
/// let key = encryptor.finish();
/// ```
pub fn new(reader: &'a mut R) -> Self {
let mut key = Zeroizing::new([0u8; KEY_SIZE]);
let mut iv = Zeroizing::new([0u8; IV_SIZE]);
getrandom(&mut *key).expect("Can't generate randomness");
// Only populate the the first 8 bits with randomness, the rest is 0
// initialized.
getrandom(&mut iv[0..8]).expect("Can't generate randomness");
let web_key = JsonWebKey {
kty: "oct".to_owned(),
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
alg: "A256CTR".to_owned(),
k: encode_url_safe(&*key),
ext: true,
};
let encoded_iv = encode(&*iv);
let aes = Aes256Ctr::new_var(&*key, &*iv).expect("Cannot create AES encryption object.");
AttachmentEncryptor {
finished: false,
inner_reader: reader,
iv: encoded_iv,
web_key,
hashes: BTreeMap::new(),
aes,
sha: Sha256::default(),
}
}
/// Consume the encryptor and get the encryption key.
pub fn finish(mut self) -> EncryptionInfo {
let hash = self.sha.finalize();
self.hashes
.entry("sha256".to_owned())
.or_insert_with(|| encode(hash));
EncryptionInfo {
version: VERSION.to_string(),
hashes: self.hashes,
iv: self.iv,
web_key: self.web_key,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptionInfo {
#[serde(rename = "v")]
pub version: String,
pub web_key: JsonWebKey,
pub iv: String,
pub hashes: BTreeMap<String, String>,
}
#[cfg(test)]
mod test {
use super::{AttachmentDecryptor, AttachmentEncryptor, EncryptionInfo};
use serde_json::json;
use std::io::{Cursor, Read};
const EXAMPLE_DATA: &[u8] = &[
179, 154, 118, 127, 186, 127, 110, 33, 203, 33, 33, 134, 67, 100, 173, 46, 235, 27, 215,
172, 36, 26, 75, 47, 33, 160,
];
fn example_key() -> EncryptionInfo {
let info = json!({
"v": "v2",
"web_key": {
"kty": "oct",
"alg": "A256CTR",
"ext": true,
"k": "Voq2nkPme_x8no5-Tjq_laDAdxE6iDbxnlQXxwFPgE4",
"key_ops": ["encrypt", "decrypt"]
},
"iv": "i0DovxYdJEcAAAAAAAAAAA",
"hashes": {
"sha256": "ANdt819a8bZl4jKy3Z+jcqtiNICa2y0AW4BBJ/iQRAU"
}
});
serde_json::from_value(info).unwrap()
}
#[test]
fn encrypt_decrypt_cycle() {
let data = "Hello world".to_owned();
let mut cursor = Cursor::new(data.clone());
let mut encryptor = AttachmentEncryptor::new(&mut cursor);
let mut encrypted = Vec::new();
encryptor.read_to_end(&mut encrypted).unwrap();
let key = encryptor.finish();
assert_ne!(encrypted.as_slice(), data.as_bytes());
let mut cursor = Cursor::new(encrypted);
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
decryptor.read_to_end(&mut decrypted_data).unwrap();
let decrypted = String::from_utf8(decrypted_data).unwrap();
assert_eq!(data, decrypted);
}
#[test]
fn real_decrypt() {
let mut cursor = Cursor::new(EXAMPLE_DATA.to_vec());
let key = example_key();
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
decryptor.read_to_end(&mut decrypted_data).unwrap();
let decrypted = String::from_utf8(decrypted_data).unwrap();
assert_eq!("It's a secret to everybody", decrypted);
}
#[test]
fn decrypt_invalid_hash() {
let mut cursor = Cursor::new("fake message");
let key = example_key();
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
assert!(decryptor.read_to_end(&mut decrypted_data).is_err())
}
}
@@ -0,0 +1,328 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use serde_json::Error as SerdeError;
use std::io::{Cursor, Read, Seek, SeekFrom};
use thiserror::Error;
use byteorder::{BigEndian, ReadBytesExt};
use getrandom::getrandom;
use aes_ctr::{
cipher::{NewStreamCipher, SyncStreamCipher},
Aes256Ctr,
};
use hmac::{Hmac, Mac, NewMac};
use pbkdf2::pbkdf2;
use sha2::{Sha256, Sha512};
use crate::{
olm::ExportedRoomKey,
utilities::{decode, encode, DecodeError},
};
const SALT_SIZE: usize = 16;
const IV_SIZE: usize = 16;
const MAC_SIZE: usize = 32;
const KEY_SIZE: usize = 32;
const VERSION: u8 = 1;
const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----";
const FOOTER: &str = "-----END MEGOLM SESSION DATA-----";
/// Error representing a failure during key export or import.
#[derive(Error, Debug)]
pub enum KeyExportError {
/// The key export doesn't contain valid headers.
#[error("Invalid or missing key export headers.")]
InvalidHeaders,
/// The key export has been encrypted with an unsupported version.
#[error("The key export has been encrypted with an unsupported version.")]
UnsupportedVersion,
/// The MAC of the encrypted payload is invalid.
#[error("The MAC of the encrypted payload is invalid.")]
InvalidMAC,
/// The decrypted key export isn't valid UTF-8.
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
/// The decrypted key export doesn't contain valid JSON.
#[error(transparent)]
Json(#[from] SerdeError),
/// The key export string isn't valid base64.
#[error(transparent)]
Decode(#[from] DecodeError),
/// The key export doesn't all the required fields.
#[error(transparent)]
IO(#[from] std::io::Error),
}
/// Try to decrypt a reader into a list of exported room keys.
///
/// # Arguments
///
/// * `passphrase` - The passphrase that was used to encrypt the exported keys.
///
/// # Examples
/// ```no_run
/// # use std::io::Cursor;
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export};
/// # use matrix_sdk_common::identifiers::user_id;
/// # use futures::executor::block_on;
/// # let alice = user_id!("@alice:example.org");
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
/// # block_on(async {
/// # let export = Cursor::new("".to_owned());
/// let exported_keys = decrypt_key_export(export, "1234").unwrap();
/// machine.import_keys(exported_keys).await.unwrap();
/// # });
/// ```
pub fn decrypt_key_export(
mut input: impl Read,
passphrase: &str,
) -> Result<Vec<ExportedRoomKey>, KeyExportError> {
let mut x: String = String::new();
input.read_to_string(&mut x)?;
if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) {
return Err(KeyExportError::InvalidHeaders);
}
let payload: String = x
.lines()
.filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER)))
.collect();
Ok(serde_json::from_str(&decrypt_helper(
&payload, passphrase,
)?)?)
}
/// Encrypt the list of exported room keys using the given passphrase.
///
/// # Arguments
///
/// * `keys` - A list of sessions that should be encrypted.
///
/// * `passphrase` - The passphrase that will be used to encrypt the exported
/// room keys.
///
/// * `rounds` - The number of rounds that should be used for the key
/// derivation when the passphrase gets turned into an AES key. More rounds are
/// increasingly computationally intensive and as such help against bruteforce
/// attacks. Should be at least `10000`, while values in the `100000` ranges
/// should be preferred.
///
/// # Panics
///
/// This method will panic if it can't get enough randomness from the OS to
/// encrypt the exported keys securely.
///
/// # Examples
/// ```no_run
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export};
/// # use matrix_sdk_common::identifiers::{user_id, room_id};
/// # use futures::executor::block_on;
/// # let alice = user_id!("@alice:example.org");
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
/// # block_on(async {
/// let room_id = room_id!("!test:localhost");
/// let exported_keys = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap();
/// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1);
/// # });
/// ```
pub fn encrypt_key_export(
keys: &[ExportedRoomKey],
passphrase: &str,
rounds: u32,
) -> Result<String, SerdeError> {
let mut plaintext = serde_json::to_string(keys)?.into_bytes();
let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds);
Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
}
fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String {
let mut salt = [0u8; SALT_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut derived_keys = [0u8; KEY_SIZE * 2];
getrandom(&mut salt).expect("Can't generate randomness");
getrandom(&mut iv).expect("Can't generate randomness");
let mut iv = u128::from_be_bytes(iv);
iv &= !(1 << 63);
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
let mut aes = Aes256Ctr::new_var(&key, &iv.to_be_bytes()).expect("Can't create AES object");
aes.apply_keystream(&mut plaintext);
let mut payload: Vec<u8> = vec![];
payload.extend(&VERSION.to_be_bytes());
payload.extend(&salt);
payload.extend(&iv.to_be_bytes());
payload.extend(&rounds.to_be_bytes());
payload.extend_from_slice(&plaintext);
let mut hmac = Hmac::<Sha256>::new_varkey(hmac_key).expect("Can't create HMAC object");
hmac.update(&payload);
let mac = hmac.finalize();
payload.extend(mac.into_bytes());
encode(payload)
}
fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
let decoded = decode(ciphertext)?;
let mut decoded = Cursor::new(decoded);
let mut salt = [0u8; SALT_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut mac = [0u8; MAC_SIZE];
let mut derived_keys = [0u8; KEY_SIZE * 2];
let version = decoded.read_u8()?;
decoded.read_exact(&mut salt)?;
decoded.read_exact(&mut iv)?;
let rounds = decoded.read_u32::<BigEndian>()?;
let ciphertext_start = decoded.position() as usize;
decoded.seek(SeekFrom::End(-32))?;
let ciphertext_end = decoded.position() as usize;
decoded.read_exact(&mut mac)?;
let mut decoded = decoded.into_inner();
if version != VERSION {
return Err(KeyExportError::UnsupportedVersion);
}
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
let mut hmac = Hmac::<Sha256>::new_varkey(hmac_key).expect("Can't create an HMAC object");
hmac.update(&decoded[0..ciphertext_end]);
hmac.verify(&mac).map_err(|_| KeyExportError::InvalidMAC)?;
let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
let mut aes = Aes256Ctr::new_var(&key, &iv).expect("Can't create an AES object");
aes.apply_keystream(&mut ciphertext);
Ok(String::from_utf8(ciphertext.to_owned())?)
}
#[cfg(test)]
mod test {
use indoc::indoc;
use proptest::prelude::*;
use std::io::Cursor;
use matrix_sdk_common::identifiers::room_id;
use matrix_sdk_test::async_test;
use super::{decode, decrypt_helper, decrypt_key_export, encrypt_helper, encrypt_key_export};
use crate::machine::test::get_prepared_machine;
const PASSPHRASE: &str = "1234";
const TEST_EXPORT: &str = indoc! {"
-----BEGIN MEGOLM SESSION DATA-----
Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo
Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI
CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU
bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD
ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9
41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U
XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53
uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR
RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo
SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9
MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ
GYW19pdjg0qdXNk/eqZsQTsNWVo6A
-----END MEGOLM SESSION DATA-----
"};
fn export_wihtout_headers() -> String {
TEST_EXPORT
.lines()
.filter(|l| !l.starts_with("-----"))
.collect()
}
#[test]
fn test_decode() {
let export = export_wihtout_headers();
assert!(decode(export).is_ok());
}
proptest! {
#[test]
fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
let mut plaintext_bytes = plaintext.clone().into_bytes();
let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1);
let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
prop_assert!(plaintext == decrypted);
}
}
#[test]
fn test_encrypt_decrypt() {
let data = "It's a secret to everybody";
let mut bytes = data.to_owned().into_bytes();
let encrypted = encrypt_helper(&mut bytes, PASSPHRASE, 10);
let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
assert_eq!(data, decrypted);
}
#[async_test]
async fn test_session_encrypt() {
let (machine, _) = get_prepared_machine().await;
let room_id = room_id!("!test:localhost");
machine
.create_outbound_group_session_with_defaults(&room_id)
.await
.unwrap();
let export = machine
.export_keys(|s| s.room_id() == &room_id)
.await
.unwrap();
assert!(!export.is_empty());
let encrypted = encrypt_key_export(&export, "1234", 1).unwrap();
let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap();
assert_eq!(export, decrypted);
assert_eq!(machine.import_keys(decrypted).await.unwrap(), (0, 1));
}
#[test]
fn test_real_decrypt() {
let reader = Cursor::new(TEST_EXPORT);
let imported = decrypt_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
assert!(!imported.is_empty())
}
}
@@ -0,0 +1,5 @@
mod attachments;
mod key_export;
pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError};
pub use key_export::{decrypt_key_export, encrypt_key_export};
+618
View File
@@ -0,0 +1,618 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashMap},
convert::{TryFrom, TryInto},
ops::Deref,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use atomic::Atomic;
use matrix_sdk_common::{
api::r0::keys::SignedKey,
encryption::DeviceKeys,
events::{
forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
room::encrypted::EncryptedEventContent, EventType,
},
identifiers::{
DeviceId, DeviceIdBox, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, UserId,
},
locks::Mutex,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tracing::warn;
use crate::{
olm::{InboundGroupSession, PrivateCrossSigningIdentity, Session},
store::{Changes, DeviceChanges},
};
#[cfg(test)]
use crate::{OlmMachine, ReadOnlyAccount};
use crate::{
error::{EventError, OlmError, OlmResult, SignatureError},
identities::{OwnUserIdentity, UserIdentities},
olm::Utility,
store::{CryptoStore, Result as StoreResult},
verification::VerificationMachine,
Sas, ToDeviceRequest,
};
/// A read-only version of a `Device`.
#[derive(Debug, Clone)]
pub struct ReadOnlyDevice {
user_id: Arc<UserId>,
device_id: Arc<Box<DeviceId>>,
algorithms: Arc<[EventEncryptionAlgorithm]>,
keys: Arc<BTreeMap<DeviceKeyId, String>>,
pub(crate) signatures: Arc<BTreeMap<UserId, BTreeMap<DeviceKeyId, String>>>,
display_name: Arc<Option<String>>,
deleted: Arc<AtomicBool>,
trust_state: Arc<Atomic<LocalTrust>>,
}
#[derive(Debug, Clone)]
/// A device represents a E2EE capable client of an user.
pub struct Device {
pub(crate) inner: ReadOnlyDevice,
pub(crate) private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
pub(crate) verification_machine: VerificationMachine,
pub(crate) own_identity: Option<OwnUserIdentity>,
pub(crate) device_owner_identity: Option<UserIdentities>,
}
impl Deref for Device {
type Target = ReadOnlyDevice;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Device {
/// Start a interactive verification with this `Device`
///
/// Returns a `Sas` object and to-device request that needs to be sent out.
pub async fn start_verification(&self) -> StoreResult<(Sas, ToDeviceRequest)> {
self.verification_machine
.start_sas(self.inner.clone())
.await
}
/// Get the Olm sessions that belong to this device.
pub(crate) async fn get_sessions(&self) -> StoreResult<Option<Arc<Mutex<Vec<Session>>>>> {
if let Some(k) = self.get_key(DeviceKeyAlgorithm::Curve25519) {
self.verification_machine.store.get_sessions(k).await
} else {
Ok(None)
}
}
/// Get the trust state of the device.
pub fn trust_state(&self) -> bool {
self.inner
.trust_state(&self.own_identity, &self.device_owner_identity)
}
/// Set the local trust state of the device to the given state.
///
/// This won't affect any cross signing trust state, this only sets a flag
/// marking to have the given trust state.
///
/// # Arguments
///
/// * `trust_state` - The new trust state that should be set for the device.
pub async fn set_local_trust(&self, trust_state: LocalTrust) -> StoreResult<()> {
self.inner.set_trust_state(trust_state);
let changes = Changes {
devices: DeviceChanges {
changed: vec![self.inner.clone()],
..Default::default()
},
..Default::default()
};
self.verification_machine.store.save_changes(changes).await
}
/// Encrypt the given content for this `Device`.
///
/// # Arguments
///
/// * `event_type` - The type of the event.
///
/// * `content` - The content of the event that should be encrypted.
pub(crate) async fn encrypt(
&self,
event_type: EventType,
content: Value,
) -> OlmResult<(Session, EncryptedEventContent)> {
self.inner
.encrypt(&**self.verification_machine.store, event_type, content)
.await
}
/// Encrypt the given inbound group session as a forwarded room key for this
/// device.
pub async fn encrypt_session(
&self,
session: InboundGroupSession,
) -> OlmResult<(Session, EncryptedEventContent)> {
let export = session.export().await;
let content: ForwardedRoomKeyToDeviceEventContent = if let Ok(c) = export.try_into() {
c
} else {
// TODO remove this panic.
panic!(
"Can't share session {} with device {} {}, key export can't \
be converted to a forwarded room key content",
session.session_id(),
self.user_id(),
self.device_id()
);
};
let content = serde_json::to_value(content)?;
self.encrypt(EventType::ForwardedRoomKey, content).await
}
}
/// A read only view over all devices belonging to a user.
#[derive(Debug)]
pub struct UserDevices {
pub(crate) inner: HashMap<DeviceIdBox, ReadOnlyDevice>,
pub(crate) private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
pub(crate) verification_machine: VerificationMachine,
pub(crate) own_identity: Option<OwnUserIdentity>,
pub(crate) device_owner_identity: Option<UserIdentities>,
}
impl UserDevices {
/// Get the specific device with the given device id.
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
self.inner.get(device_id).map(|d| Device {
inner: d.clone(),
private_identity: self.private_identity.clone(),
verification_machine: self.verification_machine.clone(),
own_identity: self.own_identity.clone(),
device_owner_identity: self.device_owner_identity.clone(),
})
}
/// Iterator over all the device ids of the user devices.
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
self.inner.keys()
}
/// Iterator over all the devices of the user devices.
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
self.inner.values().map(move |d| Device {
inner: d.clone(),
private_identity: self.private_identity.clone(),
verification_machine: self.verification_machine.clone(),
own_identity: self.own_identity.clone(),
device_owner_identity: self.device_owner_identity.clone(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
/// The local trust state of a device.
pub enum LocalTrust {
/// The device has been verified and is trusted.
Verified = 0,
/// The device been blacklisted from communicating.
BlackListed = 1,
/// The trust state of the device is being ignored.
Ignored = 2,
/// The trust state is unset.
Unset = 3,
}
impl From<i64> for LocalTrust {
fn from(state: i64) -> Self {
match state {
0 => LocalTrust::Verified,
1 => LocalTrust::BlackListed,
2 => LocalTrust::Ignored,
3 => LocalTrust::Unset,
_ => LocalTrust::Unset,
}
}
}
impl ReadOnlyDevice {
/// Create a new Device.
pub fn new(
user_id: UserId,
device_id: Box<DeviceId>,
display_name: Option<String>,
trust_state: LocalTrust,
algorithms: Vec<EventEncryptionAlgorithm>,
keys: BTreeMap<DeviceKeyId, String>,
signatures: BTreeMap<UserId, BTreeMap<DeviceKeyId, String>>,
) -> Self {
Self {
user_id: Arc::new(user_id),
device_id: Arc::new(device_id),
display_name: Arc::new(display_name),
trust_state: Arc::new(Atomic::new(trust_state)),
signatures: Arc::new(signatures),
algorithms: algorithms.into(),
keys: Arc::new(keys),
deleted: Arc::new(AtomicBool::new(false)),
}
}
/// The user id of the device owner.
pub fn user_id(&self) -> &UserId {
&self.user_id
}
/// The unique ID of the device.
pub fn device_id(&self) -> &DeviceId {
&self.device_id
}
/// Get the human readable name of the device.
pub fn display_name(&self) -> &Option<String> {
&self.display_name
}
/// Get the key of the given key algorithm belonging to this device.
pub fn get_key(&self, algorithm: DeviceKeyAlgorithm) -> Option<&String> {
self.keys
.get(&DeviceKeyId::from_parts(algorithm, &self.device_id))
}
/// Get a map containing all the device keys.
pub fn keys(&self) -> &BTreeMap<DeviceKeyId, String> {
&self.keys
}
/// Get a map containing all the device signatures.
pub fn signatures(&self) -> &BTreeMap<UserId, BTreeMap<DeviceKeyId, String>> {
&self.signatures
}
/// Get the trust state of the device.
pub fn local_trust_state(&self) -> LocalTrust {
self.trust_state.load(Ordering::Relaxed)
}
/// Is the device locally marked as trusted.
pub fn is_trusted(&self) -> bool {
self.local_trust_state() == LocalTrust::Verified
}
/// Is the device locally marked as blacklisted.
///
/// Blacklisted devices won't receive any group sessions.
pub fn is_blacklisted(&self) -> bool {
self.local_trust_state() == LocalTrust::BlackListed
}
/// Set the trust state of the device to the given state.
///
/// Note: This should only done in the cryptostore where the trust state can
/// be stored.
pub(crate) fn set_trust_state(&self, state: LocalTrust) {
self.trust_state.store(state, Ordering::Relaxed)
}
/// Get the list of algorithms this device supports.
pub fn algorithms(&self) -> &[EventEncryptionAlgorithm] {
&self.algorithms
}
/// Is the device deleted.
pub fn deleted(&self) -> bool {
self.deleted.load(Ordering::Relaxed)
}
pub(crate) fn trust_state(
&self,
own_identity: &Option<OwnUserIdentity>,
device_owner: &Option<UserIdentities>,
) -> bool {
// TODO we want to return an enum mentioning if the trust is local, if
// only the identity is trusted, if the identity and the device are
// trusted.
if self.is_trusted() {
// If the device is localy marked as verified just return so, no
// need to check signatures.
true
} else {
own_identity.as_ref().map_or(false, |own_identity| {
// Our own identity needs to be marked as verified.
own_identity.is_verified()
&& device_owner
.as_ref()
.map(|device_identity| match device_identity {
// If it's one of our own devices, just check that
// we signed the device.
UserIdentities::Own(_) => {
own_identity.is_device_signed(&self).map_or(false, |_| true)
}
// If it's a device from someone else, first check
// that our user has signed the other user and then
// check if the other user has signed this device.
UserIdentities::Other(device_identity) => {
own_identity
.is_identity_signed(&device_identity)
.map_or(false, |_| true)
&& device_identity
.is_device_signed(&self)
.map_or(false, |_| true)
}
})
.unwrap_or(false)
})
}
}
pub(crate) async fn encrypt(
&self,
store: &dyn CryptoStore,
event_type: EventType,
content: Value,
) -> OlmResult<(Session, EncryptedEventContent)> {
let sender_key = if let Some(k) = self.get_key(DeviceKeyAlgorithm::Curve25519) {
k
} else {
warn!(
"Trying to encrypt a Megolm session for user {} on device {}, \
but the device doesn't have a curve25519 key",
self.user_id(),
self.device_id()
);
return Err(EventError::MissingSenderKey.into());
};
let session = if let Some(s) = store.get_sessions(sender_key).await? {
let sessions = s.lock().await;
sessions.get(0).cloned()
} else {
None
};
let mut session = if let Some(s) = session {
s
} else {
warn!(
"Trying to encrypt a Megolm session for user {} on device {}, \
but no Olm session is found",
self.user_id(),
self.device_id()
);
return Err(OlmError::MissingSession);
};
let message = session.encrypt(&self, event_type, content).await?;
Ok((session, message))
}
/// Update a device with a new device keys struct.
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) -> Result<(), SignatureError> {
self.verify_device_keys(device_keys)?;
let display_name = Arc::new(device_keys.unsigned.device_display_name.clone());
self.algorithms = device_keys.algorithms.as_slice().into();
self.keys = Arc::new(device_keys.keys.clone());
self.signatures = Arc::new(device_keys.signatures.clone());
self.display_name = display_name;
Ok(())
}
fn is_signed_by_device(&self, json: &mut Value) -> Result<(), SignatureError> {
let signing_key = self
.get_key(DeviceKeyAlgorithm::Ed25519)
.ok_or(SignatureError::MissingSigningKey)?;
let utility = Utility::new();
utility.verify_json(
&self.user_id,
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, self.device_id()),
signing_key,
json,
)
}
pub(crate) fn as_device_keys(&self) -> DeviceKeys {
DeviceKeys::new(
self.user_id().clone(),
self.device_id().into(),
self.algorithms().to_vec(),
self.keys().clone(),
self.signatures().to_owned(),
)
}
pub(crate) fn as_signature_message(&self) -> Value {
json!({
"user_id": &*self.user_id,
"device_id": &*self.device_id,
"keys": &*self.keys,
"algorithms": &*self.algorithms,
"signatures": &*self.signatures,
})
}
pub(crate) fn verify_device_keys(
&self,
device_keys: &DeviceKeys,
) -> Result<(), SignatureError> {
let mut device_keys = serde_json::to_value(device_keys).unwrap();
self.is_signed_by_device(&mut device_keys)
}
pub(crate) fn verify_one_time_key(
&self,
one_time_key: &SignedKey,
) -> Result<(), SignatureError> {
self.is_signed_by_device(&mut json!(&one_time_key))
}
/// Mark the device as deleted.
pub(crate) fn mark_as_deleted(&self) {
self.deleted.store(true, Ordering::Relaxed);
}
#[cfg(test)]
pub async fn from_machine(machine: &OlmMachine) -> ReadOnlyDevice {
ReadOnlyDevice::from_account(machine.account()).await
}
#[cfg(test)]
pub async fn from_account(account: &ReadOnlyAccount) -> ReadOnlyDevice {
let device_keys = account.device_keys().await;
ReadOnlyDevice::try_from(&device_keys).unwrap()
}
}
impl TryFrom<&DeviceKeys> for ReadOnlyDevice {
type Error = SignatureError;
fn try_from(device_keys: &DeviceKeys) -> Result<Self, Self::Error> {
let device = Self {
user_id: Arc::new(device_keys.user_id.clone()),
device_id: Arc::new(device_keys.device_id.clone()),
algorithms: device_keys.algorithms.as_slice().into(),
signatures: Arc::new(device_keys.signatures.clone()),
keys: Arc::new(device_keys.keys.clone()),
display_name: Arc::new(device_keys.unsigned.device_display_name.clone()),
deleted: Arc::new(AtomicBool::new(false)),
trust_state: Arc::new(Atomic::new(LocalTrust::Unset)),
};
device.verify_device_keys(device_keys)?;
Ok(device)
}
}
impl PartialEq for ReadOnlyDevice {
fn eq(&self, other: &Self) -> bool {
self.user_id() == other.user_id() && self.device_id() == other.device_id()
}
}
#[cfg(test)]
pub(crate) mod test {
use serde_json::json;
use std::convert::TryFrom;
use crate::identities::{LocalTrust, ReadOnlyDevice};
use matrix_sdk_common::{
encryption::DeviceKeys,
identifiers::{user_id, DeviceKeyAlgorithm},
};
fn device_keys() -> DeviceKeys {
let device_keys = json!({
"algorithms": vec![
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "BNYQQWUMXO",
"user_id": "@example:localhost",
"keys": {
"curve25519:BNYQQWUMXO": "xfgbLIC5WAl1OIkpOzoxpCe8FsRDT6nch7NQsOb15nc",
"ed25519:BNYQQWUMXO": "2/5LWJMow5zhJqakV88SIc7q/1pa8fmkfgAzx72w9G4"
},
"signatures": {
"@example:localhost": {
"ed25519:BNYQQWUMXO": "kTwMrbsLJJM/uFGOj/oqlCaRuw7i9p/6eGrTlXjo8UJMCFAetoyWzoMcF35vSe4S6FTx8RJmqX6rM7ep53MHDQ"
}
},
"unsigned": {
"device_display_name": "Alice's mobile phone"
}
});
serde_json::from_value(device_keys).unwrap()
}
pub(crate) fn get_device() -> ReadOnlyDevice {
let device_keys = device_keys();
ReadOnlyDevice::try_from(&device_keys).unwrap()
}
#[test]
fn create_a_device() {
let user_id = user_id!("@example:localhost");
let device_id = "BNYQQWUMXO";
let device = get_device();
assert_eq!(&user_id, device.user_id());
assert_eq!(device_id, device.device_id());
assert_eq!(device.algorithms.len(), 2);
assert_eq!(LocalTrust::Unset, device.local_trust_state());
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
assert_eq!(
device.get_key(DeviceKeyAlgorithm::Curve25519).unwrap(),
"xfgbLIC5WAl1OIkpOzoxpCe8FsRDT6nch7NQsOb15nc"
);
assert_eq!(
device.get_key(DeviceKeyAlgorithm::Ed25519).unwrap(),
"2/5LWJMow5zhJqakV88SIc7q/1pa8fmkfgAzx72w9G4"
);
}
#[test]
fn update_a_device() {
let mut device = get_device();
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
let display_name = "Alice's work computer".to_owned();
let mut device_keys = device_keys();
device_keys.unsigned.device_display_name = Some(display_name.clone());
device.update_device(&device_keys).unwrap();
assert_eq!(&display_name, device.display_name().as_ref().unwrap());
}
#[test]
fn delete_a_device() {
let device = get_device();
assert!(!device.deleted());
let device_clone = device.clone();
device.mark_as_deleted();
assert!(device.deleted());
assert!(device_clone.deleted());
}
}
+734
View File
@@ -0,0 +1,734 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashSet},
convert::TryFrom,
sync::Arc,
};
use tracing::{info, trace, warn};
use matrix_sdk_common::{
api::r0::keys::get_keys::Response as KeysQueryResponse,
encryption::DeviceKeys,
identifiers::{DeviceId, DeviceIdBox, UserId},
};
use crate::{
error::OlmResult,
identities::{
MasterPubkey, OwnUserIdentity, ReadOnlyDevice, SelfSigningPubkey, UserIdentities,
UserIdentity, UserSigningPubkey,
},
requests::KeysQueryRequest,
session_manager::GroupSessionManager,
store::{Changes, DeviceChanges, IdentityChanges, Result as StoreResult, Store},
};
#[derive(Debug, Clone)]
pub(crate) struct IdentityManager {
user_id: Arc<UserId>,
device_id: Arc<DeviceIdBox>,
group_manager: GroupSessionManager,
store: Store,
}
impl IdentityManager {
pub fn new(
user_id: Arc<UserId>,
device_id: Arc<DeviceIdBox>,
store: Store,
group_manager: GroupSessionManager,
) -> Self {
IdentityManager {
user_id,
device_id,
store,
group_manager,
}
}
fn user_id(&self) -> &UserId {
&self.user_id
}
fn device_id(&self) -> &DeviceId {
&self.device_id
}
/// Receive a successful keys query response.
///
/// Returns a list of devices newly discovered devices and devices that
/// changed.
///
/// # Arguments
///
/// * `response` - The keys query response of the request that the client
/// performed.
pub async fn receive_keys_query_response(
&self,
response: &KeysQueryResponse,
) -> OlmResult<(DeviceChanges, IdentityChanges)> {
// TODO create a enum that tells us how the device/identity changed,
// e.g. new/deleted/display name change.
//
// TODO create a struct that will hold the device/identity and the
// change enum and return the struct.
//
// TODO once outbound group sessions hold on to the set of users that
// received the session, invalidate the session if a user device
// got added/deleted.
let changed_devices = self
.handle_devices_from_key_query(&response.device_keys)
.await?;
let changed_identities = self.handle_cross_singing_keys(response).await?;
let changes = Changes {
identities: changed_identities.clone(),
devices: changed_devices.clone(),
..Default::default()
};
self.store.save_changes(changes).await?;
Ok((changed_devices, changed_identities))
}
/// Handle the device keys part of a key query response.
///
/// # Arguments
///
/// * `device_keys_map` - A map holding the device keys of the users for
/// which the key query was done.
///
/// Returns a list of devices that changed. Changed here means either
/// they are new, one of their properties has changed or they got deleted.
async fn handle_devices_from_key_query(
&self,
device_keys_map: &BTreeMap<UserId, BTreeMap<DeviceIdBox, DeviceKeys>>,
) -> StoreResult<DeviceChanges> {
let mut users_with_new_or_deleted_devices = HashSet::new();
let mut changes = DeviceChanges::default();
for (user_id, device_map) in device_keys_map {
// TODO move this out into the handle keys query response method
// since we might fail handle the new device at any point here or
// when updating the user identities.
self.store.update_tracked_user(user_id, false).await?;
for (device_id, device_keys) in device_map.iter() {
// We don't need our own device in the device store.
if user_id == self.user_id() && &**device_id == self.device_id() {
continue;
}
if user_id != &device_keys.user_id || device_id != &device_keys.device_id {
warn!(
"Mismatch in device keys payload of device {}|{} from user {}|{}",
device_id, device_keys.device_id, user_id, device_keys.user_id
);
continue;
}
let device = self.store.get_readonly_device(&user_id, device_id).await?;
if let Some(mut device) = device {
if let Err(e) = device.update_device(device_keys) {
warn!(
"Failed to update the device keys for {} {}: {:?}",
user_id, device_id, e
);
continue;
}
changes.changed.push(device);
} else {
let device = match ReadOnlyDevice::try_from(device_keys) {
Ok(d) => d,
Err(e) => {
warn!(
"Failed to create a new device for {} {}: {:?}",
user_id, device_id, e
);
continue;
}
};
info!("Adding a new device to the device store {:?}", device);
users_with_new_or_deleted_devices.insert(user_id);
changes.new.push(device);
}
}
let current_devices: HashSet<&DeviceIdBox> = device_map.keys().collect();
let stored_devices = self.store.get_readonly_devices(&user_id).await?;
let stored_devices_set: HashSet<&DeviceIdBox> = stored_devices.keys().collect();
let deleted_devices_set = stored_devices_set.difference(&current_devices);
for device_id in deleted_devices_set {
users_with_new_or_deleted_devices.insert(user_id);
if let Some(device) = stored_devices.get(*device_id) {
device.mark_as_deleted();
changes.deleted.push(device.clone());
}
}
}
self.group_manager
.invalidate_sessions_new_devices(&users_with_new_or_deleted_devices);
Ok(changes)
}
/// Handle the device keys part of a key query response.
///
/// # Arguments
///
/// * `response` - The keys query response.
///
/// Returns a list of identities that changed. Changed here means either
/// they are new, one of their properties has changed or they got deleted.
async fn handle_cross_singing_keys(
&self,
response: &KeysQueryResponse,
) -> StoreResult<IdentityChanges> {
let mut changes = IdentityChanges::default();
for (user_id, master_key) in &response.master_keys {
let master_key = MasterPubkey::from(master_key);
let self_signing = if let Some(s) = response.self_signing_keys.get(user_id) {
SelfSigningPubkey::from(s)
} else {
warn!(
"User identity for user {} didn't contain a self signing pubkey",
user_id
);
continue;
};
let result = if let Some(mut i) = self.store.get_user_identity(user_id).await? {
match &mut i {
UserIdentities::Own(ref mut identity) => {
let user_signing = if let Some(s) = response.user_signing_keys.get(user_id)
{
UserSigningPubkey::from(s)
} else {
warn!(
"User identity for our own user {} didn't \
contain a user signing pubkey",
user_id
);
continue;
};
identity
.update(master_key, self_signing, user_signing)
.map(|_| (i, false))
}
UserIdentities::Other(ref mut identity) => identity
.update(master_key, self_signing)
.map(|_| (i, false)),
}
} else if user_id == self.user_id() {
if let Some(s) = response.user_signing_keys.get(user_id) {
let user_signing = UserSigningPubkey::from(s);
if master_key.user_id() != user_id
|| self_signing.user_id() != user_id
|| user_signing.user_id() != user_id
{
warn!(
"User id mismatch in one of the cross signing keys for user {}",
user_id
);
continue;
}
OwnUserIdentity::new(master_key, self_signing, user_signing)
.map(|i| (UserIdentities::Own(i), true))
} else {
warn!(
"User identity for our own user {} didn't contain a \
user signing pubkey",
user_id
);
continue;
}
} else if master_key.user_id() != user_id || self_signing.user_id() != user_id {
warn!(
"User id mismatch in one of the cross signing keys for user {}",
user_id
);
continue;
} else {
UserIdentity::new(master_key, self_signing)
.map(|i| (UserIdentities::Other(i), true))
};
match result {
Ok((i, new)) => {
trace!(
"Updated or created new user identity for {}: {:?}",
user_id,
i
);
if new {
changes.new.push(i);
} else {
changes.changed.push(i);
}
}
Err(e) => {
warn!(
"Couldn't update or create new user identity for {}: {:?}",
user_id, e
);
continue;
}
}
}
Ok(changes)
}
/// Get a key query request if one is needed.
///
/// Returns a key query reqeust if the client should query E2E keys,
/// otherwise None.
///
/// The response of a successful key query requests needs to be passed to
/// the [`OlmMachine`] with the [`receive_keys_query_response`].
///
/// [`OlmMachine`]: struct.OlmMachine.html
/// [`receive_keys_query_response`]: #method.receive_keys_query_response
pub async fn users_for_key_query(&self) -> Option<KeysQueryRequest> {
let mut users = self.store.users_for_key_query();
if users.is_empty() {
None
} else {
let mut device_keys: BTreeMap<UserId, Vec<Box<DeviceId>>> = BTreeMap::new();
for user in users.drain() {
device_keys.insert(user, Vec::new());
}
Some(KeysQueryRequest::new(device_keys))
}
}
/// Mark that the given user has changed his devices.
///
/// This will queue up the given user for a key query.
///
/// Note: The user already needs to be tracked for it to be queued up for a
/// key query.
///
/// Returns true if the user was queued up for a key query, false otherwise.
pub async fn mark_user_as_changed(&self, user_id: &UserId) -> StoreResult<bool> {
if self.store.is_user_tracked(user_id) {
self.store.update_tracked_user(user_id, true).await?;
Ok(true)
} else {
Ok(false)
}
}
/// Update the tracked users.
///
/// # Arguments
///
/// * `users` - An iterator over user ids that should be marked for
/// tracking.
///
/// This will mark users that weren't seen before for a key query and
/// tracking.
///
/// If the user is already known to the Olm machine it will not be
/// considered for a key query.
pub async fn update_tracked_users(&self, users: impl IntoIterator<Item = &UserId>) {
for user in users {
if self.store.is_user_tracked(user) {
continue;
}
if let Err(e) = self.store.update_tracked_user(user, true).await {
warn!("Error storing users for tracking {}", e);
}
}
}
}
#[cfg(test)]
pub(crate) mod test {
use std::{convert::TryFrom, sync::Arc};
use matrix_sdk_common::{
api::r0::keys::get_keys::Response as KeyQueryResponse,
identifiers::{room_id, user_id, DeviceIdBox, RoomId, UserId},
locks::Mutex,
};
use matrix_sdk_test::async_test;
use serde_json::json;
use crate::{
identities::IdentityManager,
machine::test::response_from_file,
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
session_manager::GroupSessionManager,
store::{CryptoStore, MemoryStore, Store},
verification::VerificationMachine,
};
fn user_id() -> UserId {
user_id!("@example:localhost")
}
fn other_user_id() -> UserId {
user_id!("@example2:localhost")
}
fn device_id() -> DeviceIdBox {
"WSKKLTJZCL".into()
}
fn room_id() -> RoomId {
room_id!("!test:localhost")
}
fn manager() -> IdentityManager {
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(user_id())));
let user_id = Arc::new(user_id());
let account = ReadOnlyAccount::new(&user_id, &device_id());
let store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
let verification = VerificationMachine::new(account.clone(), identity.clone(), store);
let store = Store::new(
user_id.clone(),
identity,
Arc::new(Box::new(MemoryStore::new())),
verification,
);
let account = Account {
inner: account,
store: store.clone(),
};
let group = GroupSessionManager::new(account, store.clone());
IdentityManager::new(user_id, Arc::new(device_id()), store, group)
}
pub(crate) fn other_key_query() -> KeyQueryResponse {
let data = response_from_file(&json!({
"device_keys": {
"@example2:localhost": {
"SKISMLNIMH": {
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
"device_id": "SKISMLNIMH",
"keys": {
"curve25519:SKISMLNIMH": "qO9xFazIcW8dE0oqHGMojGgJwbBpMOhGnIfJy2pzvmI",
"ed25519:SKISMLNIMH": "y3wV3AoyIGREqrJJVH8DkQtlwHBUxoZ9ApP76kFgXQ8"
},
"signatures": {
"@example2:localhost": {
"ed25519:SKISMLNIMH": "YwbT35rbjKoYFZVU1tQP8MsL06+znVNhNzUMPt6jTEYRBFoC4GDq9hQEJBiFSq37r1jvLMteggVAWw37fs1yBA",
"ed25519:ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc": "PWuuTE/aTkp1EJQkPHhRx2BxbF+wjMIDFxDRp7JAerlMkDsNFUTfRRusl6vqROPU36cl+yY8oeJTZGFkU6+pBQ"
}
},
"user_id": "@example2:localhost",
"unsigned": {
"device_display_name": "Riot Desktop (Linux)"
}
}
}
},
"failures": {},
"master_keys": {
"@example2:localhost": {
"user_id": "@example2:localhost",
"usage": ["master"],
"keys": {
"ed25519:kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do": "kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do"
},
"signatures": {
"@example2:localhost": {
"ed25519:SKISMLNIMH": "KdUZqzt8VScGNtufuQ8lOf25byYLWIhmUYpPENdmM8nsldexD7vj+Sxoo7PknnTX/BL9h2N7uBq0JuykjunCAw"
}
}
}
},
"self_signing_keys": {
"@example2:localhost": {
"user_id": "@example2:localhost",
"usage": ["self_signing"],
"keys": {
"ed25519:ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc": "ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc"
},
"signatures": {
"@example2:localhost": {
"ed25519:kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do": "W/O8BnmiUETPpH02mwYaBgvvgF/atXnusmpSTJZeUSH/vHg66xiZOhveQDG4cwaW8iMa+t9N4h1DWnRoHB4mCQ"
}
}
}
},
"user_signing_keys": {}
}));
KeyQueryResponse::try_from(data).expect("Can't parse the keys upload response")
}
pub(crate) fn own_key_query() -> KeyQueryResponse {
let data = response_from_file(&json!({
"device_keys": {
"@example:localhost": {
"WSKKLTJZCL": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "WSKKLTJZCL",
"keys": {
"curve25519:WSKKLTJZCL": "wnip2tbJBJxrFayC88NNJpm61TeSNgYcqBH4T9yEDhU",
"ed25519:WSKKLTJZCL": "lQ+eshkhgKoo+qp9Qgnj3OX5PBoWMU5M9zbuEevwYqE"
},
"signatures": {
"@example:localhost": {
"ed25519:WSKKLTJZCL": "SKpIUnq7QK0xleav0PrIQyKjVm+TgZr7Yi8cKjLeZDtkgyToE2d4/e3Aj79dqOlLB92jFVE4d1cM/Ry04wFwCA",
"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210": "9UGu1iC5YhFCdELGfB29YaV+QE0t/X5UDSsPf4QcdZyXIwyp9zBbHX2lh9vWudNQ+akZpaq7ZRaaM+4TCnw/Ag"
}
},
"user_id": "@example:localhost",
"unsigned": {
"device_display_name": "Cross signing capable"
}
},
"LVWOVGOXME": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "LVWOVGOXME",
"keys": {
"curve25519:LVWOVGOXME": "KMfWKUhnDW1D11hNzATs/Ax1FQRsJxKCWzq0NyGtIiI",
"ed25519:LVWOVGOXME": "k+NC3L7CBD6fBClcHBrKLOkqCyGNSKhWXiH5Q2STRnA"
},
"signatures": {
"@example:localhost": {
"ed25519:LVWOVGOXME": "39Ir5Bttpc5+bQwzLj7rkjm5E5/cp/JTbMJ/t0enj6J5w9MXVBFOUqqM2hpaRaRwILMMpwYbJ8IOGjl0Y/MGAw"
}
},
"user_id": "@example:localhost",
"unsigned": {
"device_display_name": "Non-cross signing"
}
}
}
},
"failures": {},
"master_keys": {
"@example:localhost": {
"user_id": "@example:localhost",
"usage": [
"master"
],
"keys": {
"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0": "rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"
},
"signatures": {
"@example:localhost": {
"ed25519:WSKKLTJZCL": "ZzJp1wtmRdykXAUEItEjNiFlBrxx8L6/Vaen9am8AuGwlxxJtOkuY4m+4MPLvDPOgavKHLsrRuNLAfCeakMlCQ"
}
}
}
},
"self_signing_keys": {
"@example:localhost": {
"user_id": "@example:localhost",
"usage": [
"self_signing"
],
"keys": {
"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210": "0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"
},
"signatures": {
"@example:localhost": {
"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0": "AC7oDUW4rUhtInwb4lAoBJ0wAuu4a5k+8e34B5+NKsDB8HXRwgVwUWN/MRWc/sJgtSbVlhzqS9THEmQQ1C51Bw"
}
}
}
},
"user_signing_keys": {
"@example:localhost": {
"user_id": "@example:localhost",
"usage": [
"user_signing"
],
"keys": {
"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo": "DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"
},
"signatures": {
"@example:localhost": {
"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0": "C4L2sx9frGqj8w41KyynHGqwUbbwBYRZpYCB+6QWnvQFA5Oi/1PJj8w5anwzEsoO0TWmLYmf7FXuAGewanOWDg"
}
}
}
}
}));
KeyQueryResponse::try_from(data).expect("Can't parse the keys upload response")
}
#[async_test]
async fn test_manager_creation() {
let manager = manager();
assert!(manager.users_for_key_query().await.is_none())
}
#[async_test]
async fn test_manager_key_query_response() {
let manager = manager();
let other_user = other_user_id();
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 0);
manager
.receive_keys_query_response(&other_key_query())
.await
.unwrap();
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 1);
let device = manager
.store
.get_readonly_device(&other_user, "SKISMLNIMH".into())
.await
.unwrap()
.unwrap();
let identity = manager
.store
.get_user_identity(&other_user)
.await
.unwrap()
.unwrap();
let identity = identity.other().unwrap();
assert!(identity.is_device_signed(&device).is_ok())
}
#[async_test]
async fn test_manager_own_key_query_response() {
let manager = manager();
let other_user = other_user_id();
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 0);
manager
.receive_keys_query_response(&other_key_query())
.await
.unwrap();
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 1);
let device = manager
.store
.get_readonly_device(&other_user, "SKISMLNIMH".into())
.await
.unwrap()
.unwrap();
let identity = manager
.store
.get_user_identity(&other_user)
.await
.unwrap()
.unwrap();
let identity = identity.other().unwrap();
assert!(identity.is_device_signed(&device).is_ok())
}
#[async_test]
async fn test_session_invalidation() {
let manager = manager();
let room_id = room_id();
let user_id = other_user_id();
let device_id: DeviceIdBox = "SKISMLNIMH".into();
manager
.group_manager
.create_outbound_group_session(&room_id, Default::default())
.await
.unwrap();
let session = manager
.group_manager
.get_outbound_group_session(&room_id)
.unwrap();
session.add_recipient(&user_id);
session.mark_as_shared();
assert!(!session.invalidated());
assert!(!session.expired());
// Receiving a new device invalidates the session.
manager
.receive_keys_query_response(&other_key_query())
.await
.unwrap();
assert!(session.invalidated());
manager
.group_manager
.create_outbound_group_session(&room_id, Default::default())
.await
.unwrap();
let session = manager
.group_manager
.get_outbound_group_session(&room_id)
.unwrap();
session.add_recipient(&user_id);
session.mark_as_shared();
assert!(!session.invalidated());
assert!(!session.expired());
let device = manager
.store
.get_device(&user_id, &device_id)
.await
.unwrap()
.unwrap();
assert!(!device.deleted());
let response = KeyQueryResponse::try_from(response_from_file(&json!({
"device_keys": {
user_id: {}
},
"failures": {},
})))
.unwrap();
// Noticing that a device got deleted invalidates the session as well
manager
.receive_keys_query_response(&response)
.await
.unwrap();
assert!(device.deleted());
assert!(session.invalidated());
}
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Collection of public identities used in Matrix.
//!
//! Matrix supports two main types of identities, a per-device identity and a
//! per-user identity.
//!
//! ## Device
//!
//! Every E2EE capable Matrix client will create a new Olm account and upload
//! the public keys of the Olm account to the server. This is represented as a
//! `ReadOnlyDevice`.
//!
//! Devices can have a local trust state which is needs to be saved in our
//! `CryptoStore`, to avoid reference cycles a wrapper for the `ReadOnlyDevice`
//! exists which adds methods to manipulate the local trust state.
//!
//! ## User
//!
//! Cross-signing capable devices will upload 3 additional (master, self-signing,
//! user-signing) public keys which represent the user identity owning all the
//! devices. This is represented in two ways, as a `UserIdentity` for other
//! users and as `OwnUserIdentity` for our own user.
//!
//! This is done because the server will only give us access to 2 of the 3
//! additional public keys for other users, while it will give us access to all
//! 3 for our own user.
//!
//! Both identity sets need to reqularly fetched from the server using the
//! `/keys/query` API call.
pub(crate) mod device;
mod manager;
pub(crate) mod user;
pub use device::{Device, LocalTrust, ReadOnlyDevice, UserDevices};
pub(crate) use manager::IdentityManager;
pub use user::{
MasterPubkey, OwnUserIdentity, SelfSigningPubkey, UserIdentities, UserIdentity,
UserSigningPubkey,
};
+857
View File
@@ -0,0 +1,857 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{btree_map::Iter, BTreeMap},
convert::TryFrom,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use serde::{Deserialize, Serialize};
use serde_json::to_value;
use matrix_sdk_common::{
api::r0::keys::{CrossSigningKey, KeyUsage},
identifiers::{DeviceKeyId, UserId},
};
#[cfg(test)]
use crate::olm::PrivateCrossSigningIdentity;
use crate::{error::SignatureError, olm::Utility, ReadOnlyDevice};
/// Wrapper for a cross signing key marking it as the master key.
///
/// Master keys are used to sign other cross signing keys, the self signing and
/// user signing keys of an user will be signed by their master key.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MasterPubkey(Arc<CrossSigningKey>);
/// Wrapper for a cross signing key marking it as a self signing key.
///
/// Self signing keys are used to sign the user's own devices.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelfSigningPubkey(Arc<CrossSigningKey>);
/// Wrapper for a cross signing key marking it as a user signing key.
///
/// User signing keys are used to sign the master keys of other users.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSigningPubkey(Arc<CrossSigningKey>);
impl PartialEq for MasterPubkey {
fn eq(&self, other: &MasterPubkey) -> bool {
self.0.user_id == other.0.user_id && self.0.keys == other.0.keys
}
}
impl PartialEq for SelfSigningPubkey {
fn eq(&self, other: &SelfSigningPubkey) -> bool {
self.0.user_id == other.0.user_id && self.0.keys == other.0.keys
}
}
impl PartialEq for UserSigningPubkey {
fn eq(&self, other: &UserSigningPubkey) -> bool {
self.0.user_id == other.0.user_id && self.0.keys == other.0.keys
}
}
impl From<CrossSigningKey> for MasterPubkey {
fn from(key: CrossSigningKey) -> Self {
Self(Arc::new(key))
}
}
impl From<CrossSigningKey> for SelfSigningPubkey {
fn from(key: CrossSigningKey) -> Self {
Self(Arc::new(key))
}
}
impl From<CrossSigningKey> for UserSigningPubkey {
fn from(key: CrossSigningKey) -> Self {
Self(Arc::new(key))
}
}
impl Into<CrossSigningKey> for MasterPubkey {
fn into(self) -> CrossSigningKey {
self.0.as_ref().clone()
}
}
impl Into<CrossSigningKey> for UserSigningPubkey {
fn into(self) -> CrossSigningKey {
self.0.as_ref().clone()
}
}
impl Into<CrossSigningKey> for SelfSigningPubkey {
fn into(self) -> CrossSigningKey {
self.0.as_ref().clone()
}
}
impl AsRef<CrossSigningKey> for MasterPubkey {
fn as_ref(&self) -> &CrossSigningKey {
&self.0
}
}
impl AsRef<CrossSigningKey> for SelfSigningPubkey {
fn as_ref(&self) -> &CrossSigningKey {
&self.0
}
}
impl AsRef<CrossSigningKey> for UserSigningPubkey {
fn as_ref(&self) -> &CrossSigningKey {
&self.0
}
}
impl From<&CrossSigningKey> for MasterPubkey {
fn from(key: &CrossSigningKey) -> Self {
Self(Arc::new(key.clone()))
}
}
impl From<&CrossSigningKey> for SelfSigningPubkey {
fn from(key: &CrossSigningKey) -> Self {
Self(Arc::new(key.clone()))
}
}
impl From<&CrossSigningKey> for UserSigningPubkey {
fn from(key: &CrossSigningKey) -> Self {
Self(Arc::new(key.clone()))
}
}
impl<'a> From<&'a SelfSigningPubkey> for CrossSigningSubKeys<'a> {
fn from(key: &'a SelfSigningPubkey) -> Self {
CrossSigningSubKeys::SelfSigning(key)
}
}
impl<'a> From<&'a UserSigningPubkey> for CrossSigningSubKeys<'a> {
fn from(key: &'a UserSigningPubkey) -> Self {
CrossSigningSubKeys::UserSigning(key)
}
}
/// Enum over the cross signing sub-keys.
pub(crate) enum CrossSigningSubKeys<'a> {
/// The self signing subkey.
SelfSigning(&'a SelfSigningPubkey),
/// The user signing subkey.
UserSigning(&'a UserSigningPubkey),
}
impl<'a> CrossSigningSubKeys<'a> {
/// Get the id of the user that owns this cross signing subkey.
fn user_id(&self) -> &UserId {
match self {
CrossSigningSubKeys::SelfSigning(key) => &key.0.user_id,
CrossSigningSubKeys::UserSigning(key) => &key.0.user_id,
}
}
/// Get the `CrossSigningKey` from an sub-keys enum
pub(crate) fn cross_signing_key(&self) -> &CrossSigningKey {
match self {
CrossSigningSubKeys::SelfSigning(key) => &key.0,
CrossSigningSubKeys::UserSigning(key) => &key.0,
}
}
}
impl MasterPubkey {
/// Get the user id of the master key's owner.
pub fn user_id(&self) -> &UserId {
&self.0.user_id
}
/// Get the keys map of containing the master keys.
pub fn keys(&self) -> &BTreeMap<String, String> {
&self.0.keys
}
/// Get the list of `KeyUsage` that is set for this key.
pub fn usage(&self) -> &[KeyUsage] {
&self.0.usage
}
/// Get the signatures map of this cross signing key.
pub fn signatures(&self) -> &BTreeMap<UserId, BTreeMap<String, String>> {
&self.0.signatures
}
/// Get the master key with the given key id.
///
/// # Arguments
///
/// * `key_id` - The id of the key that should be fetched.
pub fn get_key(&self, key_id: &DeviceKeyId) -> Option<&str> {
self.0.keys.get(key_id.as_str()).map(|k| k.as_str())
}
/// Check if the given cross signing sub-key is signed by the master key.
///
/// # Arguments
///
/// * `subkey` - The subkey that should be checked for a valid signature.
///
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub(crate) fn verify_subkey<'a>(
&self,
subkey: impl Into<CrossSigningSubKeys<'a>>,
) -> Result<(), SignatureError> {
let (key_id, key) = self
.0
.keys
.iter()
.next()
.ok_or(SignatureError::MissingSigningKey)?;
let key_id = DeviceKeyId::try_from(key_id.as_str())?;
// FIXME `KeyUsage is missing PartialEq.
// if self.0.usage.contains(&KeyUsage::Master) {
// return Err(SignatureError::MissingSigningKey);
// }
let subkey: CrossSigningSubKeys = subkey.into();
if &self.0.user_id != subkey.user_id() {
return Err(SignatureError::UserIdMissmatch);
}
let utility = Utility::new();
utility.verify_json(
&self.0.user_id,
&key_id,
key,
&mut to_value(subkey.cross_signing_key()).map_err(|_| SignatureError::NotAnObject)?,
)
}
}
impl<'a> IntoIterator for &'a MasterPubkey {
type Item = (&'a String, &'a String);
type IntoIter = Iter<'a, String, String>;
fn into_iter(self) -> Self::IntoIter {
self.keys().iter()
}
}
impl UserSigningPubkey {
/// Get the user id of the user signing key's owner.
pub fn user_id(&self) -> &UserId {
&self.0.user_id
}
/// Get the keys map of containing the user signing keys.
pub fn keys(&self) -> &BTreeMap<String, String> {
&self.0.keys
}
/// Check if the given master key is signed by this user signing key.
///
/// # Arguments
///
/// * `master_key` - The master key that should be checked for a valid
/// signature.
///
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub(crate) fn verify_master_key(
&self,
master_key: &MasterPubkey,
) -> Result<(), SignatureError> {
let (key_id, key) = self
.0
.keys
.iter()
.next()
.ok_or(SignatureError::MissingSigningKey)?;
// TODO check that the usage is OK.
let utility = Utility::new();
utility.verify_json(
&self.0.user_id,
&DeviceKeyId::try_from(key_id.as_str())?,
key,
&mut to_value(&*master_key.0).map_err(|_| SignatureError::NotAnObject)?,
)
}
}
impl<'a> IntoIterator for &'a UserSigningPubkey {
type Item = (&'a String, &'a String);
type IntoIter = Iter<'a, String, String>;
fn into_iter(self) -> Self::IntoIter {
self.keys().iter()
}
}
impl SelfSigningPubkey {
/// Get the user id of the self signing key's owner.
pub fn user_id(&self) -> &UserId {
&self.0.user_id
}
/// Get the keys map of containing the self signing keys.
pub fn keys(&self) -> &BTreeMap<String, String> {
&self.0.keys
}
/// Check if the given device is signed by this self signing key.
///
/// # Arguments
///
/// * `device` - The device that should be checked for a valid signature.
///
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub(crate) fn verify_device(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
let (key_id, key) = self
.0
.keys
.iter()
.next()
.ok_or(SignatureError::MissingSigningKey)?;
// TODO check that the usage is OK.
let utility = Utility::new();
utility.verify_json(
&self.0.user_id,
&DeviceKeyId::try_from(key_id.as_str())?,
key,
&mut device.as_signature_message(),
)
}
}
impl<'a> IntoIterator for &'a SelfSigningPubkey {
type Item = (&'a String, &'a String);
type IntoIter = Iter<'a, String, String>;
fn into_iter(self) -> Self::IntoIter {
self.keys().iter()
}
}
/// Enum over the different user identity types we can have.
#[derive(Debug, Clone)]
pub enum UserIdentities {
/// Our own user identity.
Own(OwnUserIdentity),
/// Identities of other users.
Other(UserIdentity),
}
impl From<OwnUserIdentity> for UserIdentities {
fn from(identity: OwnUserIdentity) -> Self {
UserIdentities::Own(identity)
}
}
impl From<UserIdentity> for UserIdentities {
fn from(identity: UserIdentity) -> Self {
UserIdentities::Other(identity)
}
}
impl UserIdentities {
/// The unique user id of this identity.
pub fn user_id(&self) -> &UserId {
match self {
UserIdentities::Own(i) => i.user_id(),
UserIdentities::Other(i) => i.user_id(),
}
}
/// Get the master key of the identity.
pub fn master_key(&self) -> &MasterPubkey {
match self {
UserIdentities::Own(i) => i.master_key(),
UserIdentities::Other(i) => i.master_key(),
}
}
/// Get the self-signing key of the identity.
pub fn self_signing_key(&self) -> &SelfSigningPubkey {
match self {
UserIdentities::Own(i) => &i.self_signing_key,
UserIdentities::Other(i) => &i.self_signing_key,
}
}
/// Get the user-signing key of the identity, this is only present for our
/// own user identity..
pub fn user_signing_key(&self) -> Option<&UserSigningPubkey> {
match self {
UserIdentities::Own(i) => Some(&i.user_signing_key),
UserIdentities::Other(_) => None,
}
}
/// Destructure the enum into an `OwnUserIdentity` if it's of the correct
/// type.
pub fn own(&self) -> Option<&OwnUserIdentity> {
match self {
UserIdentities::Own(i) => Some(i),
_ => None,
}
}
/// Destructure the enum into an `UserIdentity` if it's of the correct
/// type.
pub fn other(&self) -> Option<&UserIdentity> {
match self {
UserIdentities::Other(i) => Some(i),
_ => None,
}
}
}
impl PartialEq for UserIdentities {
fn eq(&self, other: &UserIdentities) -> bool {
self.user_id() == other.user_id()
}
}
/// Struct representing a cross signing identity of a user.
///
/// This is the user identity of a user that isn't our own. Other users will
/// only contain a master key and a self signing key, meaning that only device
/// signatures can be checked with this identity.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserIdentity {
user_id: Arc<UserId>,
pub(crate) master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
}
impl UserIdentity {
/// Create a new user identity with the given master and self signing key.
///
/// # Arguments
///
/// * `master_key` - The master key of the user identity.
///
/// * `self signing key` - The self signing key of user identity.
///
/// Returns a `SignatureError` if the self signing key fails to be correctly
/// verified by the given master key.
pub fn new(
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
) -> Result<Self, SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
Ok(Self {
user_id: Arc::new(master_key.0.user_id.clone()),
master_key,
self_signing_key,
})
}
#[cfg(test)]
pub async fn from_private(identity: &PrivateCrossSigningIdentity) -> Self {
let master_key = identity
.master_key
.lock()
.await
.as_ref()
.unwrap()
.public_key
.clone();
let self_signing_key = identity
.self_signing_key
.lock()
.await
.as_ref()
.unwrap()
.public_key
.clone();
Self {
user_id: Arc::new(identity.user_id().clone()),
master_key,
self_signing_key,
}
}
/// Get the user id of this identity.
pub fn user_id(&self) -> &UserId {
&self.user_id
}
/// Get the public master key of the identity.
pub fn master_key(&self) -> &MasterPubkey {
&self.master_key
}
/// Get the public self-signing key of the identity.
pub fn self_signing_key(&self) -> &SelfSigningPubkey {
&self.self_signing_key
}
/// Update the identity with a new master key and self signing key.
///
/// # Arguments
///
/// * `master_key` - The new master key of the user identity.
///
/// * `self_signing_key` - The new self signing key of user identity.
///
/// Returns a `SignatureError` if we failed to update the identity.
pub fn update(
&mut self,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
) -> Result<(), SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
self.master_key = master_key;
self.self_signing_key = self_signing_key;
Ok(())
}
/// Check if the given device has been signed by this identity.
///
/// The user_id of the user identity and the user_id of the device need to
/// match for the signature check to succeed as we don't trust users to sign
/// devices of other users.
///
/// # Arguments
///
/// * `device` - The device that should be checked for a valid signature.
///
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub fn is_device_signed(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
if self.user_id() != device.user_id() {
return Err(SignatureError::UserIdMissmatch);
}
self.self_signing_key.verify_device(device)
}
}
/// Struct representing a cross signing identity of our own user.
///
/// This is the user identity of our own user. This user identity will contain a
/// master key, self signing key as well as a user signing key.
///
/// This identity can verify other identities as well as devices belonging to
/// the identity.
#[derive(Debug, Clone)]
pub struct OwnUserIdentity {
user_id: Arc<UserId>,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
user_signing_key: UserSigningPubkey,
verified: Arc<AtomicBool>,
}
impl OwnUserIdentity {
/// Create a new own user identity with the given master, self signing, and
/// user signing key.
///
/// # Arguments
///
/// * `master_key` - The master key of the user identity.
///
/// * `self_signing_key` - The self signing key of user identity.
///
/// * `user_signing_key` - The user signing key of user identity.
///
/// Returns a `SignatureError` if the self signing key fails to be correctly
/// verified by the given master key.
pub fn new(
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
user_signing_key: UserSigningPubkey,
) -> Result<Self, SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
master_key.verify_subkey(&user_signing_key)?;
Ok(Self {
user_id: Arc::new(master_key.0.user_id.clone()),
master_key,
self_signing_key,
user_signing_key,
verified: Arc::new(AtomicBool::new(false)),
})
}
/// Get the user id of this identity.
pub fn user_id(&self) -> &UserId {
&self.user_id
}
/// Get the public master key of the identity.
pub fn master_key(&self) -> &MasterPubkey {
&self.master_key
}
/// Get the public self-signing key of the identity.
pub fn self_signing_key(&self) -> &SelfSigningPubkey {
&self.self_signing_key
}
/// Get the public user-signing key of the identity.
pub fn user_signing_key(&self) -> &UserSigningPubkey {
&self.user_signing_key
}
/// Check if the given identity has been signed by this identity.
///
/// # Arguments
///
/// * `identity` - The identity of another user that we want to check if
/// it's has been signed.
///
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub fn is_identity_signed(&self, identity: &UserIdentity) -> Result<(), SignatureError> {
self.user_signing_key
.verify_master_key(&identity.master_key)
}
/// Check if the given device has been signed by this identity.
///
/// Only devices of our own user should be checked with this method, if a
/// device of a different user is given the signature check will always fail
/// even if a valid signature exists.
///
/// # Arguments
///
/// * `device` - The device that should be checked for a valid signature.
///
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub fn is_device_signed(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
if self.user_id() != device.user_id() {
return Err(SignatureError::UserIdMissmatch);
}
self.self_signing_key.verify_device(device)
}
/// Mark our identity as verified.
pub fn mark_as_verified(&self) {
self.verified.store(true, Ordering::SeqCst)
}
/// Check if our identity is verified.
pub fn is_verified(&self) -> bool {
self.verified.load(Ordering::SeqCst)
}
/// Update the identity with a new master key and self signing key.
///
/// Note: This will reset the verification state if the master keys differ.
///
/// # Arguments
///
/// * `master_key` - The new master key of the user identity.
///
/// * `self_signing_key` - The new self signing key of user identity.
///
/// * `user_signing_key` - The new user signing key of user identity.
///
/// Returns a `SignatureError` if we failed to update the identity.
pub fn update(
&mut self,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
user_signing_key: UserSigningPubkey,
) -> Result<(), SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
master_key.verify_subkey(&user_signing_key)?;
self.self_signing_key = self_signing_key;
self.user_signing_key = user_signing_key;
if self.master_key != master_key {
self.verified.store(false, Ordering::SeqCst)
}
self.master_key = master_key;
Ok(())
}
}
#[cfg(test)]
pub(crate) mod test {
use std::{convert::TryFrom, sync::Arc};
use crate::{
identities::{
manager::test::{other_key_query, own_key_query},
Device, ReadOnlyDevice,
},
olm::{PrivateCrossSigningIdentity, ReadOnlyAccount},
store::MemoryStore,
verification::VerificationMachine,
};
use matrix_sdk_common::{
api::r0::keys::get_keys::Response as KeyQueryResponse, identifiers::user_id, locks::Mutex,
};
use matrix_sdk_test::async_test;
use super::{OwnUserIdentity, UserIdentities, UserIdentity};
fn device(response: &KeyQueryResponse) -> (ReadOnlyDevice, ReadOnlyDevice) {
let mut devices = response.device_keys.values().next().unwrap().values();
let first = ReadOnlyDevice::try_from(devices.next().unwrap()).unwrap();
let second = ReadOnlyDevice::try_from(devices.next().unwrap()).unwrap();
(first, second)
}
fn own_identity(response: &KeyQueryResponse) -> OwnUserIdentity {
let user_id = user_id!("@example:localhost");
let master_key = response.master_keys.get(&user_id).unwrap();
let user_signing = response.user_signing_keys.get(&user_id).unwrap();
let self_signing = response.self_signing_keys.get(&user_id).unwrap();
OwnUserIdentity::new(master_key.into(), self_signing.into(), user_signing.into()).unwrap()
}
pub(crate) fn get_own_identity() -> OwnUserIdentity {
own_identity(&own_key_query())
}
pub(crate) fn get_other_identity() -> UserIdentity {
let user_id = user_id!("@example2:localhost");
let response = other_key_query();
let master_key = response.master_keys.get(&user_id).unwrap();
let self_signing = response.self_signing_keys.get(&user_id).unwrap();
UserIdentity::new(master_key.into(), self_signing.into()).unwrap()
}
#[test]
fn own_identity_create() {
let user_id = user_id!("@example:localhost");
let response = own_key_query();
let master_key = response.master_keys.get(&user_id).unwrap();
let user_signing = response.user_signing_keys.get(&user_id).unwrap();
let self_signing = response.self_signing_keys.get(&user_id).unwrap();
OwnUserIdentity::new(master_key.into(), self_signing.into(), user_signing.into()).unwrap();
}
#[test]
fn other_identity_create() {
get_other_identity();
}
#[test]
fn own_identity_check_signatures() {
let response = own_key_query();
let identity = get_own_identity();
let (first, second) = device(&response);
assert!(identity.is_device_signed(&first).is_err());
assert!(identity.is_device_signed(&second).is_ok());
let private_identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(
second.user_id().clone(),
)));
let verification_machine = VerificationMachine::new(
ReadOnlyAccount::new(second.user_id(), second.device_id()),
private_identity.clone(),
Arc::new(Box::new(MemoryStore::new())),
);
let first = Device {
inner: first,
verification_machine: verification_machine.clone(),
private_identity: private_identity.clone(),
own_identity: Some(identity.clone()),
device_owner_identity: Some(UserIdentities::Own(identity.clone())),
};
let second = Device {
inner: second,
verification_machine,
private_identity,
own_identity: Some(identity.clone()),
device_owner_identity: Some(UserIdentities::Own(identity.clone())),
};
assert!(!second.trust_state());
assert!(!second.is_trusted());
assert!(!first.trust_state());
assert!(!first.is_trusted());
identity.mark_as_verified();
assert!(second.trust_state());
assert!(!first.trust_state());
}
#[async_test]
async fn own_device_with_private_identity() {
let response = own_key_query();
let (_, device) = device(&response);
let account = ReadOnlyAccount::new(device.user_id(), device.device_id());
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let id = Arc::new(Mutex::new(identity.clone()));
let verification_machine = VerificationMachine::new(
ReadOnlyAccount::new(device.user_id(), device.device_id()),
id.clone(),
Arc::new(Box::new(MemoryStore::new())),
);
let public_identity = identity.as_public_identity().await.unwrap();
let mut device = Device {
inner: device,
verification_machine: verification_machine.clone(),
private_identity: id.clone(),
own_identity: Some(public_identity.clone()),
device_owner_identity: Some(public_identity.clone().into()),
};
assert!(!device.trust_state());
let mut device_keys = device.as_device_keys();
identity.sign_device_keys(&mut device_keys).await.unwrap();
device.inner.signatures = Arc::new(device_keys.signatures);
assert!(device.trust_state());
}
}
File diff suppressed because it is too large Load Diff
+24 -11
View File
@@ -25,19 +25,32 @@
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
mod device;
mod error;
mod file_encryption;
mod identities;
mod key_request;
mod machine;
mod memory_stores;
mod olm;
mod store;
pub mod olm;
mod requests;
mod session_manager;
pub mod store;
mod utilities;
mod verification;
pub use device::{Device, TrustState};
pub use error::{MegolmError, OlmError};
pub use machine::{OlmMachine, OneTimeKeys};
pub use memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices};
pub use olm::{Account, InboundGroupSession, OutboundGroupSession, Session};
#[cfg(feature = "sqlite-cryptostore")]
pub use store::sqlite::SqliteStore;
pub use store::{CryptoStore, CryptoStoreError};
pub use file_encryption::{
decrypt_key_export, encrypt_key_export, AttachmentDecryptor, AttachmentEncryptor,
DecryptorError,
};
pub use identities::{
Device, LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserDevices, UserIdentities, UserIdentity,
};
pub use machine::OlmMachine;
pub use olm::EncryptionSettings;
pub(crate) use olm::ReadOnlyAccount;
pub use requests::{
IncomingResponse, KeysQueryRequest, OutgoingRequest, OutgoingRequests, ToDeviceRequest,
};
pub use verification::Sas;
File diff suppressed because it is too large Load Diff
-793
View File
@@ -1,793 +0,0 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::instant::Instant;
use std::fmt;
use std::mem;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use matrix_sdk_common::locks::Mutex;
use serde::Serialize;
use zeroize::Zeroize;
pub use olm_rs::account::IdentityKeys;
use olm_rs::account::{OlmAccount, OneTimeKeys};
use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError};
use olm_rs::inbound_group_session::OlmInboundGroupSession;
use olm_rs::outbound_group_session::OlmOutboundGroupSession;
use olm_rs::session::OlmSession;
use olm_rs::PicklingMode;
pub use olm_rs::{
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
use matrix_sdk_common::api::r0::keys::SignedKey;
use matrix_sdk_common::identifiers::RoomId;
/// Account holding identity keys for which sessions can be created.
///
/// An account is the central identity for encrypted communication between two
/// devices.
#[derive(Clone)]
pub struct Account {
inner: Arc<Mutex<OlmAccount>>,
identity_keys: Arc<IdentityKeys>,
shared: Arc<AtomicBool>,
}
#[cfg_attr(tarpaulin, skip)]
impl fmt::Debug for Account {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Account")
.field("identity_keys", self.identity_keys())
.field("shared", &self.shared())
.finish()
}
}
#[cfg_attr(tarpaulin, skip)]
impl Default for Account {
fn default() -> Self {
Self::new()
}
}
impl Account {
/// Create a fresh new account, this will generate the identity key-pair.
pub fn new() -> Self {
let account = OlmAccount::new();
let identity_keys = account.parsed_identity_keys();
Account {
inner: Arc::new(Mutex::new(account)),
identity_keys: Arc::new(identity_keys),
shared: Arc::new(AtomicBool::new(false)),
}
}
/// Get the public parts of the identity keys for the account.
pub fn identity_keys(&self) -> &IdentityKeys {
&self.identity_keys
}
/// Has the account been shared with the server.
pub fn shared(&self) -> bool {
self.shared.load(Ordering::Relaxed)
}
/// Mark the account as shared.
///
/// Messages shouldn't be encrypted with the session before it has been
/// shared.
pub fn mark_as_shared(&self) {
self.shared.store(true, Ordering::Relaxed);
}
/// Get the one-time keys of the account.
///
/// This can be empty, keys need to be generated first.
pub async fn one_time_keys(&self) -> OneTimeKeys {
self.inner.lock().await.parsed_one_time_keys()
}
/// Generate count number of one-time keys.
pub async fn generate_one_time_keys(&self, count: usize) {
self.inner.lock().await.generate_one_time_keys(count);
}
/// Get the maximum number of one-time keys the account can hold.
pub async fn max_one_time_keys(&self) -> usize {
self.inner.lock().await.max_number_of_one_time_keys()
}
/// Mark the current set of one-time keys as being published.
pub async fn mark_keys_as_published(&self) {
self.inner.lock().await.mark_keys_as_published();
}
/// Sign the given string using the accounts signing key.
///
/// Returns the signature as a base64 encoded string.
pub async fn sign(&self, string: &str) -> String {
self.inner.lock().await.sign(string)
}
/// Store the account as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the account, either an
/// unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
self.inner.lock().await.pickle(pickle_mode)
}
/// Restore an account from a previously pickled string.
///
/// # Arguments
///
/// * `pickle` - The pickled string of the account.
///
/// * `pickle_mode` - The mode that was used to pickle the account, either an
/// unencrypted mode or an encrypted using passphrase.
///
/// * `shared` - Boolean determining if the account was uploaded to the
/// server.
pub fn from_pickle(
pickle: String,
pickle_mode: PicklingMode,
shared: bool,
) -> Result<Self, OlmAccountError> {
let account = OlmAccount::unpickle(pickle, pickle_mode)?;
let identity_keys = account.parsed_identity_keys();
Ok(Account {
inner: Arc::new(Mutex::new(account)),
identity_keys: Arc::new(identity_keys),
shared: Arc::new(AtomicBool::from(shared)),
})
}
/// Create a new session with another account given a one-time key.
///
/// Returns the newly created session or a `OlmSessionError` if creating a
/// session failed.
///
/// # Arguments
/// * `their_identity_key` - The other account's identity/curve25519 key.
///
/// * `their_one_time_key` - A signed one-time key that the other account
/// created and shared with us.
pub async fn create_outbound_session(
&self,
their_identity_key: &str,
their_one_time_key: &SignedKey,
) -> Result<Session, OlmSessionError> {
let session = self
.inner
.lock()
.await
.create_outbound_session(their_identity_key, &their_one_time_key.key)?;
let now = Instant::now();
let session_id = session.session_id();
Ok(Session {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(their_identity_key.to_owned()),
creation_time: Arc::new(now),
last_use_time: Arc::new(now),
})
}
/// Create a new session with another account given a pre-key Olm message.
///
/// Returns the newly created session or a `OlmSessionError` if creating a
/// session failed.
///
/// # Arguments
/// * `their_identity_key` - The other account's identitiy/curve25519 key.
///
/// * `message` - A pre-key Olm message that was sent to us by the other
/// account.
pub async fn create_inbound_session(
&self,
their_identity_key: &str,
message: PreKeyMessage,
) -> Result<Session, OlmSessionError> {
let session = self
.inner
.lock()
.await
.create_inbound_session_from(their_identity_key, message)?;
self.inner
.lock()
.await
.remove_one_time_keys(&session)
.expect(
"Session was successfully created but the account doesn't hold a matching one-time key",
);
let now = Instant::now();
let session_id = session.session_id();
Ok(Session {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(their_identity_key.to_owned()),
creation_time: Arc::new(now),
last_use_time: Arc::new(now),
})
}
}
impl PartialEq for Account {
fn eq(&self, other: &Self) -> bool {
self.identity_keys() == other.identity_keys() && self.shared() == other.shared()
}
}
/// Cryptographic session that enables secure communication between two
/// `Account`s
#[derive(Clone)]
pub struct Session {
inner: Arc<Mutex<OlmSession>>,
session_id: Arc<String>,
pub(crate) sender_key: Arc<String>,
pub(crate) creation_time: Arc<Instant>,
pub(crate) last_use_time: Arc<Instant>,
}
#[cfg_attr(tarpaulin, skip)]
impl fmt::Debug for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Session")
.field("session_id", &self.session_id())
.field("sender_key", &self.sender_key)
.finish()
}
}
impl Session {
/// Decrypt the given Olm message.
///
/// Returns the decrypted plaintext or an `OlmSessionError` if decryption
/// failed.
///
/// # Arguments
///
/// * `message` - The Olm message that should be decrypted.
pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
let plaintext = self.inner.lock().await.decrypt(message)?;
mem::replace(&mut self.last_use_time, Arc::new(Instant::now()));
Ok(plaintext)
}
/// Encrypt the given plaintext as a OlmMessage.
///
/// Returns the encrypted Olm message.
///
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub async fn encrypt(&mut self, plaintext: &str) -> OlmMessage {
let message = self.inner.lock().await.encrypt(plaintext);
mem::replace(&mut self.last_use_time, Arc::new(Instant::now()));
message
}
/// Check if a pre-key Olm message was encrypted for this session.
///
/// Returns true if it matches, false if not and a OlmSessionError if there
/// was an error checking if it matches.
///
/// # Arguments
///
/// * `their_identity_key` - The identity/curve25519 key of the account
/// that encrypted this Olm message.
///
/// * `message` - The pre-key Olm message that should be checked.
pub async fn matches(
&self,
their_identity_key: &str,
message: PreKeyMessage,
) -> Result<bool, OlmSessionError> {
self.inner
.lock()
.await
.matches_inbound_session_from(their_identity_key, message)
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Store the session as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
self.inner.lock().await.pickle(pickle_mode)
}
/// Restore a Session from a previously pickled string.
///
/// Returns the restored Olm Session or a `OlmSessionError` if there was an
/// error.
///
/// # Arguments
///
/// * `pickle` - The pickled string of the session.
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
///
/// * `sender_key` - The public curve25519 key of the account that
/// established the session with us.
///
/// * `creation_time` - The timestamp that marks when the session was
/// created.
///
/// * `last_use_time` - The timestamp that marks when the session was
/// last used to encrypt or decrypt an Olm message.
pub fn from_pickle(
pickle: String,
pickle_mode: PicklingMode,
sender_key: String,
creation_time: Instant,
last_use_time: Instant,
) -> Result<Self, OlmSessionError> {
let session = OlmSession::unpickle(pickle, pickle_mode)?;
let session_id = session.session_id();
Ok(Session {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(sender_key),
creation_time: Arc::new(creation_time),
last_use_time: Arc::new(last_use_time),
})
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}
/// The private session key of a group session.
/// Can be used to create a new inbound group session.
#[derive(Clone, Debug, Serialize, Zeroize)]
#[zeroize(drop)]
pub struct GroupSessionKey(pub String);
/// Inbound group session.
///
/// Inbound group sessions are used to exchange room messages between a group of
/// participants. Inbound group sessions are used to decrypt the room messages.
#[derive(Clone)]
pub struct InboundGroupSession {
inner: Arc<Mutex<OlmInboundGroupSession>>,
session_id: Arc<String>,
pub(crate) sender_key: Arc<String>,
pub(crate) signing_key: Arc<String>,
pub(crate) room_id: Arc<RoomId>,
forwarding_chains: Arc<Mutex<Option<Vec<String>>>>,
}
impl InboundGroupSession {
/// Create a new inbound group session for the given room.
///
/// These sessions are used to decrypt room messages.
///
/// # Arguments
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `signing_key` - The public ed25519 key of the account that
/// sent us the session.
///
/// * `room_id` - The id of the room that the session is used in.
///
/// * `session_key` - The private session key that is used to decrypt
/// messages.
pub fn new(
sender_key: &str,
signing_key: &str,
room_id: &RoomId,
session_key: GroupSessionKey,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::new(&session_key.0)?;
let session_id = session.session_id();
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(sender_key.to_owned()),
signing_key: Arc::new(signing_key.to_owned()),
room_id: Arc::new(room_id.clone()),
forwarding_chains: Arc::new(Mutex::new(None)),
})
}
/// Store the group session as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the group session,
/// either an unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
self.inner.lock().await.pickle(pickle_mode)
}
/// Restore a Session from a previously pickled string.
///
/// Returns the restored group session or a `OlmGroupSessionError` if there
/// was an error.
///
/// # Arguments
///
/// * `pickle` - The pickled string of the group session session.
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `signing_key` - The public ed25519 key of the account that
/// sent us the session.
///
/// * `room_id` - The id of the room that the session is used in.
pub fn from_pickle(
pickle: String,
pickle_mode: PicklingMode,
sender_key: String,
signing_key: String,
room_id: RoomId,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::unpickle(pickle, pickle_mode)?;
let session_id = session.session_id();
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(sender_key),
signing_key: Arc::new(signing_key),
room_id: Arc::new(room_id),
forwarding_chains: Arc::new(Mutex::new(None)),
})
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Get the first message index we know how to decrypt.
pub async fn first_known_index(&self) -> u32 {
self.inner.lock().await.first_known_index()
}
/// Decrypt the given ciphertext.
///
/// Returns the decrypted plaintext or an `OlmGroupSessionError` if
/// decryption failed.
///
/// # Arguments
///
/// * `message` - The message that should be decrypted.
pub async fn decrypt(&self, message: String) -> Result<(String, u32), OlmGroupSessionError> {
self.inner.lock().await.decrypt(message)
}
}
#[cfg_attr(tarpaulin, skip)]
impl fmt::Debug for InboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InboundGroupSession")
.field("session_id", &self.session_id())
.finish()
}
}
impl PartialEq for InboundGroupSession {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}
/// Outbound group session.
///
/// Outbound group sessions are used to exchange room messages between a group
/// of participants. Outbound group sessions are used to encrypt the room
/// messages.
#[derive(Clone)]
pub struct OutboundGroupSession {
inner: Arc<Mutex<OlmOutboundGroupSession>>,
session_id: Arc<String>,
room_id: Arc<RoomId>,
creation_time: Arc<Instant>,
message_count: Arc<AtomicUsize>,
shared: Arc<AtomicBool>,
}
impl OutboundGroupSession {
/// Create a new outbound group session for the given room.
///
/// Outbound group sessions are used to encrypt room messages.
///
/// # Arguments
///
/// * `room_id` - The id of the room that the session is used in.
pub fn new(room_id: &RoomId) -> Self {
let session = OlmOutboundGroupSession::new();
let session_id = session.session_id();
OutboundGroupSession {
inner: Arc::new(Mutex::new(session)),
room_id: Arc::new(room_id.to_owned()),
session_id: Arc::new(session_id),
creation_time: Arc::new(Instant::now()),
message_count: Arc::new(AtomicUsize::new(0)),
shared: Arc::new(AtomicBool::new(false)),
}
}
/// Encrypt the given plaintext using this session.
///
/// Returns the encrypted ciphertext.
///
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub async fn encrypt(&self, plaintext: String) -> String {
let session = self.inner.lock().await;
session.encrypt(plaintext)
}
/// Check if the session has expired and if it should be rotated.
///
/// A session will expire after some time or if enough messages have been
/// encrypted using it.
pub fn expired(&self) -> bool {
// TODO implement this.
false
}
/// Mark the session as shared.
///
/// Messages shouldn't be encrypted with the session before it has been
/// shared.
pub fn mark_as_shared(&self) {
self.shared.store(true, Ordering::Relaxed);
}
/// Check if the session has been marked as shared.
pub fn shared(&self) -> bool {
self.shared.load(Ordering::Relaxed)
}
/// Get the session key of this session.
///
/// A session key can be used to to create an `InboundGroupSession`.
pub async fn session_key(&self) -> GroupSessionKey {
let session = self.inner.lock().await;
GroupSessionKey(session.session_key())
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Get the current message index for this session.
///
/// Each message is sent with an increasing index. This returns the
/// message index that will be used for the next encrypted message.
pub async fn message_index(&self) -> u32 {
let session = self.inner.lock().await;
session.session_message_index()
}
}
#[cfg_attr(tarpaulin, skip)]
impl std::fmt::Debug for OutboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OutboundGroupSession")
.field("session_id", &self.session_id)
.field("room_id", &self.room_id)
.field("creation_time", &self.creation_time)
.field("message_count", &self.message_count)
.finish()
}
}
#[cfg(test)]
pub(crate) mod test {
use crate::olm::{Account, InboundGroupSession, OutboundGroupSession, Session};
use matrix_sdk_common::api::r0::keys::SignedKey;
use matrix_sdk_common::identifiers::RoomId;
use olm_rs::session::OlmMessage;
use std::collections::BTreeMap;
use std::convert::TryFrom;
pub(crate) async fn get_account_and_session() -> (Account, Session) {
let alice = Account::new();
let bob = Account::new();
bob.generate_one_time_keys(1).await;
let one_time_key = bob
.one_time_keys()
.await
.curve25519()
.iter()
.nth(0)
.unwrap()
.1
.to_owned();
let one_time_key = SignedKey {
key: one_time_key,
signatures: BTreeMap::new(),
};
let sender_key = bob.identity_keys().curve25519().to_owned();
let session = alice
.create_outbound_session(&sender_key, &one_time_key)
.await
.unwrap();
(alice, session)
}
#[test]
fn account_creation() {
let account = Account::new();
let identyty_keys = account.identity_keys();
assert!(!account.shared());
assert!(!identyty_keys.ed25519().is_empty());
assert_ne!(identyty_keys.values().len(), 0);
assert_ne!(identyty_keys.keys().len(), 0);
assert_ne!(identyty_keys.iter().len(), 0);
assert!(identyty_keys.contains_key("ed25519"));
assert_eq!(
identyty_keys.ed25519(),
identyty_keys.get("ed25519").unwrap()
);
assert!(!identyty_keys.curve25519().is_empty());
account.mark_as_shared();
assert!(account.shared());
}
#[tokio::test]
async fn one_time_keys_creation() {
let account = Account::new();
let one_time_keys = account.one_time_keys().await;
assert!(one_time_keys.curve25519().is_empty());
assert_ne!(account.max_one_time_keys().await, 0);
account.generate_one_time_keys(10).await;
let one_time_keys = account.one_time_keys().await;
assert!(!one_time_keys.curve25519().is_empty());
assert_ne!(one_time_keys.values().len(), 0);
assert_ne!(one_time_keys.keys().len(), 0);
assert_ne!(one_time_keys.iter().len(), 0);
assert!(one_time_keys.contains_key("curve25519"));
assert_eq!(one_time_keys.curve25519().keys().len(), 10);
assert_eq!(
one_time_keys.curve25519(),
one_time_keys.get("curve25519").unwrap()
);
account.mark_keys_as_published().await;
let one_time_keys = account.one_time_keys().await;
assert!(one_time_keys.curve25519().is_empty());
}
#[tokio::test]
async fn session_creation() {
let alice = Account::new();
let bob = Account::new();
let alice_keys = alice.identity_keys();
alice.generate_one_time_keys(1).await;
let one_time_keys = alice.one_time_keys().await;
alice.mark_keys_as_published().await;
let one_time_key = one_time_keys
.curve25519()
.iter()
.nth(0)
.unwrap()
.1
.to_owned();
let one_time_key = SignedKey {
key: one_time_key,
signatures: BTreeMap::new(),
};
let mut bob_session = bob
.create_outbound_session(alice_keys.curve25519(), &one_time_key)
.await
.unwrap();
let plaintext = "Hello world";
let message = bob_session.encrypt(plaintext).await;
let prekey_message = match message.clone() {
OlmMessage::PreKey(m) => m,
OlmMessage::Message(_) => panic!("Incorrect message type"),
};
let bob_keys = bob.identity_keys();
let mut alice_session = alice
.create_inbound_session(bob_keys.curve25519(), prekey_message.clone())
.await
.unwrap();
assert!(alice_session
.matches(bob_keys.curve25519(), prekey_message)
.await
.unwrap());
assert_eq!(bob_session.session_id(), alice_session.session_id());
let decyrpted = alice_session.decrypt(message).await.unwrap();
assert_eq!(plaintext, decyrpted);
}
#[tokio::test]
async fn group_session_creation() {
let room_id = RoomId::try_from("!test:localhost").unwrap();
let outbound = OutboundGroupSession::new(&room_id);
assert_eq!(0, outbound.message_index().await);
assert!(!outbound.shared());
outbound.mark_as_shared();
assert!(outbound.shared());
let inbound = InboundGroupSession::new(
"test_key",
"test_key",
&room_id,
outbound.session_key().await,
)
.unwrap();
assert_eq!(0, inbound.first_known_index().await);
assert_eq!(outbound.session_id(), inbound.session_id());
let plaintext = "This is a secret to everybody".to_owned();
let ciphertext = outbound.encrypt(plaintext.clone()).await;
assert_eq!(plaintext, inbound.decrypt(ciphertext).await.unwrap().0);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
fmt, mem,
sync::Arc,
};
use olm_rs::{
errors::OlmGroupSessionError, inbound_group_session::OlmInboundGroupSession, PicklingMode,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use zeroize::Zeroizing;
pub use olm_rs::{
account::IdentityKeys,
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
use matrix_sdk_common::{
events::{
forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
room::encrypted::EncryptedEventContent, AnySyncRoomEvent, SyncMessageEvent,
},
identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId},
locks::Mutex,
Raw,
};
use super::{ExportedGroupSessionKey, ExportedRoomKey, GroupSessionKey};
use crate::error::{EventError, MegolmResult};
// TODO add creation times to the inbound grop sessions so we can export
// sessions that were created between some time period, this should only be set
// for non-imported sessoins.
/// Inbound group session.
///
/// Inbound group sessions are used to exchange room messages between a group of
/// participants. Inbound group sessions are used to decrypt the room messages.
#[derive(Clone)]
pub struct InboundGroupSession {
inner: Arc<Mutex<OlmInboundGroupSession>>,
session_id: Arc<str>,
first_known_index: u32,
pub(crate) sender_key: Arc<str>,
pub(crate) signing_key: Arc<BTreeMap<DeviceKeyAlgorithm, String>>,
pub(crate) room_id: Arc<RoomId>,
forwarding_chains: Arc<Mutex<Option<Vec<String>>>>,
imported: Arc<bool>,
}
impl InboundGroupSession {
/// Create a new inbound group session for the given room.
///
/// These sessions are used to decrypt room messages.
///
/// # Arguments
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `signing_key` - The public ed25519 key of the account that
/// sent us the session.
///
/// * `room_id` - The id of the room that the session is used in.
///
/// * `session_key` - The private session key that is used to decrypt
/// messages.
pub(crate) fn new(
sender_key: &str,
signing_key: &str,
room_id: &RoomId,
session_key: GroupSessionKey,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::new(&session_key.0)?;
let session_id = session.session_id();
let first_known_index = session.first_known_index();
let mut keys: BTreeMap<DeviceKeyAlgorithm, String> = BTreeMap::new();
keys.insert(DeviceKeyAlgorithm::Ed25519, signing_key.to_owned());
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: session_id.into(),
sender_key: sender_key.to_owned().into(),
first_known_index,
signing_key: Arc::new(keys),
room_id: Arc::new(room_id.clone()),
forwarding_chains: Arc::new(Mutex::new(None)),
imported: Arc::new(false),
})
}
/// Create a InboundGroupSession from an exported version of the group
/// session.
///
/// Most notably this can be called with an `ExportedRoomKey` from a
/// previous [`export()`] call.
///
///
/// [`export()`]: #method.export
pub fn from_export(
exported_session: impl Into<ExportedRoomKey>,
) -> Result<Self, OlmGroupSessionError> {
Self::try_from(exported_session.into())
}
/// Create a new inbound group session from a forwarded room key content.
///
/// # Arguments
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `content` - A forwarded room key content that contains the session key
/// to create the `InboundGroupSession`.
pub(crate) fn from_forwarded_key(
sender_key: &str,
content: &mut ForwardedRoomKeyToDeviceEventContent,
) -> Result<Self, OlmGroupSessionError> {
let key = Zeroizing::from(mem::take(&mut content.session_key));
let session = OlmInboundGroupSession::import(&key)?;
let first_known_index = session.first_known_index();
let mut forwarding_chains = content.forwarding_curve25519_key_chain.clone();
forwarding_chains.push(sender_key.to_owned());
let mut sender_claimed_key = BTreeMap::new();
sender_claimed_key.insert(
DeviceKeyAlgorithm::Ed25519,
content.sender_claimed_ed25519_key.to_owned(),
);
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: content.session_id.as_str().into(),
sender_key: content.sender_key.as_str().into(),
first_known_index,
signing_key: Arc::new(sender_claimed_key),
room_id: Arc::new(content.room_id.clone()),
forwarding_chains: Arc::new(Mutex::new(Some(forwarding_chains))),
imported: Arc::new(true),
})
}
/// Store the group session as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the group session,
/// either an unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> PickledInboundGroupSession {
let pickle = self.inner.lock().await.pickle(pickle_mode);
PickledInboundGroupSession {
pickle: InboundGroupSessionPickle::from(pickle),
sender_key: self.sender_key.to_string(),
signing_key: (&*self.signing_key).clone(),
room_id: (&*self.room_id).clone(),
forwarding_chains: self.forwarding_chains.lock().await.clone(),
imported: *self.imported,
}
}
/// Export this session at the first known message index.
///
/// If only a limited part of this session should be exported use
/// [`export_at_index()`](#method.export_at_index).
pub async fn export(&self) -> ExportedRoomKey {
self.export_at_index(self.first_known_index())
.await
.expect("Can't export at the first known index")
}
/// Export this session at the given message index.
pub async fn export_at_index(&self, message_index: u32) -> Option<ExportedRoomKey> {
let session_key =
ExportedGroupSessionKey(self.inner.lock().await.export(message_index).ok()?);
Some(ExportedRoomKey {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
room_id: (&*self.room_id).clone(),
sender_key: (&*self.sender_key).to_owned(),
session_id: self.session_id().to_owned(),
forwarding_curve25519_key_chain: self
.forwarding_chains
.lock()
.await
.as_ref()
.cloned()
.unwrap_or_default(),
sender_claimed_keys: (&*self.signing_key).clone(),
session_key,
})
}
/// Restore a Session from a previously pickled string.
///
/// Returns the restored group session or a `OlmGroupSessionError` if there
/// was an error.
///
/// # Arguments
///
/// * `pickle` - The pickled version of the `InboundGroupSession`.
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
pub fn from_pickle(
pickle: PickledInboundGroupSession,
pickle_mode: PicklingMode,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::unpickle(pickle.pickle.0, pickle_mode)?;
let first_known_index = session.first_known_index();
let session_id = session.session_id();
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: session_id.into(),
sender_key: pickle.sender_key.into(),
first_known_index,
signing_key: Arc::new(pickle.signing_key),
room_id: Arc::new(pickle.room_id),
forwarding_chains: Arc::new(Mutex::new(pickle.forwarding_chains)),
imported: Arc::new(pickle.imported),
})
}
/// The room where this session is used in.
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Get the first message index we know how to decrypt.
pub fn first_known_index(&self) -> u32 {
self.first_known_index
}
/// Decrypt the given ciphertext.
///
/// Returns the decrypted plaintext or an `OlmGroupSessionError` if
/// decryption failed.
///
/// # Arguments
///
/// * `message` - The message that should be decrypted.
pub(crate) async fn decrypt_helper(
&self,
message: String,
) -> Result<(String, u32), OlmGroupSessionError> {
self.inner.lock().await.decrypt(message)
}
/// Decrypt an event from a room timeline.
///
/// # Arguments
///
/// * `event` - The event that should be decrypted.
pub(crate) async fn decrypt(
&self,
event: &SyncMessageEvent<EncryptedEventContent>,
) -> MegolmResult<(Raw<AnySyncRoomEvent>, u32)> {
let content = match &event.content {
EncryptedEventContent::MegolmV1AesSha2(c) => c,
_ => return Err(EventError::UnsupportedAlgorithm.into()),
};
let (plaintext, message_index) = self.decrypt_helper(content.ciphertext.clone()).await?;
let mut decrypted_value = serde_json::from_str::<Value>(&plaintext)?;
let decrypted_object = decrypted_value
.as_object_mut()
.ok_or(EventError::NotAnObject)?;
// TODO better number conversion here.
let server_ts = event
.origin_server_ts
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let server_ts: i64 = server_ts.try_into().unwrap_or_default();
decrypted_object.insert("sender".to_owned(), event.sender.to_string().into());
decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into());
decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into());
decrypted_object.insert(
"unsigned".to_owned(),
serde_json::to_value(&event.unsigned).unwrap_or_default(),
);
if let Some(decrypted_content) = decrypted_object
.get_mut("content")
.map(|c| c.as_object_mut())
.flatten()
{
if !decrypted_content.contains_key("m.relates_to") {
if let Some(relation) = &content.relates_to {
decrypted_content.insert(
"m.relates_to".to_owned(),
serde_json::to_value(relation).unwrap_or_default(),
);
}
}
}
Ok((
serde_json::from_value::<Raw<AnySyncRoomEvent>>(decrypted_value)?,
message_index,
))
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for InboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InboundGroupSession")
.field("session_id", &self.session_id())
.finish()
}
}
impl PartialEq for InboundGroupSession {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}
/// A pickled version of an `InboundGroupSession`.
///
/// Holds all the information that needs to be stored in a database to restore
/// an InboundGroupSession.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledInboundGroupSession {
/// The pickle string holding the InboundGroupSession.
pub pickle: InboundGroupSessionPickle,
/// The public curve25519 key of the account that sent us the session
pub sender_key: String,
/// The public ed25519 key of the account that sent us the session.
pub signing_key: BTreeMap<DeviceKeyAlgorithm, String>,
/// The id of the room that the session is used in.
pub room_id: RoomId,
/// The list of claimed ed25519 that forwarded us this key. Will be None if
/// we dirrectly received this session.
pub forwarding_chains: Option<Vec<String>>,
/// Flag remembering if the session was dirrectly sent to us by the sender
/// or if it was imported.
pub imported: bool,
}
/// The typed representation of a base64 encoded string of the GroupSession pickle.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InboundGroupSessionPickle(String);
impl From<String> for InboundGroupSessionPickle {
fn from(pickle_string: String) -> Self {
InboundGroupSessionPickle(pickle_string)
}
}
impl InboundGroupSessionPickle {
/// Get the string representation of the pickle.
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<ExportedRoomKey> for InboundGroupSession {
type Error = OlmGroupSessionError;
fn try_from(key: ExportedRoomKey) -> Result<Self, Self::Error> {
let session = OlmInboundGroupSession::import(&key.session_key.0)?;
let first_known_index = session.first_known_index();
let forwarding_chains = if key.forwarding_curve25519_key_chain.is_empty() {
None
} else {
Some(key.forwarding_curve25519_key_chain)
};
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: key.session_id.into(),
sender_key: key.sender_key.into(),
first_known_index,
signing_key: Arc::new(key.sender_claimed_keys),
room_id: Arc::new(key.room_id),
forwarding_chains: Arc::new(Mutex::new(forwarding_chains)),
imported: Arc::new(true),
})
}
}
@@ -0,0 +1,176 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::{
events::forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId},
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, convert::TryInto};
use zeroize::Zeroize;
mod inbound;
mod outbound;
pub use inbound::{InboundGroupSession, InboundGroupSessionPickle, PickledInboundGroupSession};
pub use outbound::{EncryptionSettings, OutboundGroupSession};
/// The private session key of a group session.
/// Can be used to create a new inbound group session.
#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)]
#[zeroize(drop)]
pub struct GroupSessionKey(pub String);
/// The exported version of an private session key of a group session.
/// Can be used to create a new inbound group session.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Zeroize)]
#[zeroize(drop)]
pub struct ExportedGroupSessionKey(pub String);
/// An exported version of a `InboundGroupSession`
///
/// This can be used to share the `InboundGroupSession` in an exported file.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ExportedRoomKey {
/// The encryption algorithm that the session uses.
pub algorithm: EventEncryptionAlgorithm,
/// The room where the session is used.
pub room_id: RoomId,
/// The Curve25519 key of the device which initiated the session originally.
pub sender_key: String,
/// The ID of the session that the key is for.
pub session_id: String,
/// The key for the session.
pub session_key: ExportedGroupSessionKey,
/// The Ed25519 key of the device which initiated the session originally.
pub sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String>,
/// Chain of Curve25519 keys through which this session was forwarded, via
/// m.forwarded_room_key events.
pub forwarding_curve25519_key_chain: Vec<String>,
}
impl TryInto<ForwardedRoomKeyToDeviceEventContent> for ExportedRoomKey {
type Error = ();
/// Convert an exported room key into a content for a forwarded room key
/// event.
///
/// This will fail if the exported room key has multiple sender claimed keys
/// or if the algorithm of the claimed sender key isn't
/// `DeviceKeyAlgorithm::Ed25519`.
fn try_into(self) -> Result<ForwardedRoomKeyToDeviceEventContent, Self::Error> {
if self.sender_claimed_keys.len() != 1 {
Err(())
} else {
let (algorithm, claimed_key) = self.sender_claimed_keys.iter().next().ok_or(())?;
if algorithm != &DeviceKeyAlgorithm::Ed25519 {
return Err(());
}
Ok(ForwardedRoomKeyToDeviceEventContent {
algorithm: self.algorithm,
room_id: self.room_id,
sender_key: self.sender_key,
session_id: self.session_id,
session_key: self.session_key.0.clone(),
sender_claimed_ed25519_key: claimed_key.to_owned(),
forwarding_curve25519_key_chain: self.forwarding_curve25519_key_chain,
})
}
}
}
impl From<ForwardedRoomKeyToDeviceEventContent> for ExportedRoomKey {
/// Convert the content of a forwarded room key into a exported room key.
fn from(forwarded_key: ForwardedRoomKeyToDeviceEventContent) -> Self {
let mut sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String> = BTreeMap::new();
sender_claimed_keys.insert(
DeviceKeyAlgorithm::Ed25519,
forwarded_key.sender_claimed_ed25519_key,
);
Self {
algorithm: forwarded_key.algorithm,
room_id: forwarded_key.room_id,
session_id: forwarded_key.session_id,
forwarding_curve25519_key_chain: forwarded_key.forwarding_curve25519_key_chain,
sender_claimed_keys,
sender_key: forwarded_key.sender_key,
session_key: ExportedGroupSessionKey(forwarded_key.session_key),
}
}
}
#[cfg(test)]
mod test {
use std::{
sync::Arc,
time::{Duration, Instant},
};
use matrix_sdk_common::{
events::{
room::message::{MessageEventContent, TextMessageEventContent},
AnyMessageEventContent,
},
identifiers::{room_id, user_id},
};
use super::EncryptionSettings;
use crate::ReadOnlyAccount;
#[tokio::test]
#[cfg(target_os = "linux")]
async fn expiration() {
let settings = EncryptionSettings {
rotation_period_msgs: 1,
..Default::default()
};
let account = ReadOnlyAccount::new(&user_id!("@alice:example.org"), "DEVICEID".into());
let (session, _) = account
.create_group_session_pair(&room_id!("!test_room:example.org"), settings)
.await
.unwrap();
assert!(!session.expired());
let _ = session
.encrypt(AnyMessageEventContent::RoomMessage(
MessageEventContent::Text(TextMessageEventContent::plain("Test message")),
))
.await;
assert!(session.expired());
let settings = EncryptionSettings {
rotation_period: Duration::from_millis(100),
..Default::default()
};
let (mut session, _) = account
.create_group_session_pair(&room_id!("!test_room:example.org"), settings)
.await
.unwrap();
assert!(!session.expired());
session.creation_time = Arc::new(Instant::now() - Duration::from_secs(60 * 60));
assert!(session.expired());
}
}
@@ -0,0 +1,423 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use dashmap::{DashMap, DashSet};
use matrix_sdk_common::{api::r0::to_device::DeviceIdOrAllDevices, uuid::Uuid};
use std::{
cmp::min,
fmt,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use tracing::debug;
use matrix_sdk_common::{
events::{
room::{encrypted::EncryptedEventContent, encryption::EncryptionEventContent},
AnyMessageEventContent, EventContent,
},
identifiers::{DeviceId, DeviceIdBox, EventEncryptionAlgorithm, RoomId, UserId},
instant::Instant,
locks::Mutex,
};
use serde_json::{json, Value};
use olm_rs::outbound_group_session::OlmOutboundGroupSession;
pub use olm_rs::{
account::IdentityKeys,
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
use crate::ToDeviceRequest;
use super::GroupSessionKey;
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
const ROTATION_MESSAGES: u64 = 100;
/// Settings for an encrypted room.
///
/// This determines the algorithm and rotation periods of a group session.
#[derive(Debug)]
pub struct EncryptionSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
/// How long the session should be used before changing it.
pub rotation_period: Duration,
/// How many messages should be sent before changing the session.
pub rotation_period_msgs: u64,
}
impl Default for EncryptionSettings {
fn default() -> Self {
Self {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
rotation_period: ROTATION_PERIOD,
rotation_period_msgs: ROTATION_MESSAGES,
}
}
}
impl From<&EncryptionEventContent> for EncryptionSettings {
fn from(content: &EncryptionEventContent) -> Self {
let rotation_period: Duration = content
.rotation_period_ms
.map_or(ROTATION_PERIOD, |r| Duration::from_millis(r.into()));
let rotation_period_msgs: u64 = content
.rotation_period_msgs
.map_or(ROTATION_MESSAGES, Into::into);
Self {
algorithm: content.algorithm.clone(),
rotation_period,
rotation_period_msgs,
}
}
}
/// Outbound group session.
///
/// Outbound group sessions are used to exchange room messages between a group
/// of participants. Outbound group sessions are used to encrypt the room
/// messages.
#[derive(Clone)]
pub struct OutboundGroupSession {
inner: Arc<Mutex<OlmOutboundGroupSession>>,
device_id: Arc<DeviceIdBox>,
account_identity_keys: Arc<IdentityKeys>,
session_id: Arc<str>,
room_id: Arc<RoomId>,
pub(crate) creation_time: Arc<Instant>,
message_count: Arc<AtomicU64>,
shared: Arc<AtomicBool>,
invalidated: Arc<AtomicBool>,
settings: Arc<EncryptionSettings>,
shared_with_set: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
to_share_with_set: Arc<DashMap<Uuid, Arc<ToDeviceRequest>>>,
}
impl OutboundGroupSession {
/// Create a new outbound group session for the given room.
///
/// Outbound group sessions are used to encrypt room messages.
///
/// # Arguments
///
/// * `device_id` - The id of the device that created this session.
///
/// * `identity_keys` - The identity keys of the account that created this
/// session.
///
/// * `room_id` - The id of the room that the session is used in.
///
/// * `settings` - Settings determining the algorithm and rotation period of
/// the outbound group session.
pub fn new(
device_id: Arc<DeviceIdBox>,
identity_keys: Arc<IdentityKeys>,
room_id: &RoomId,
settings: EncryptionSettings,
) -> Self {
let session = OlmOutboundGroupSession::new();
let session_id = session.session_id();
OutboundGroupSession {
inner: Arc::new(Mutex::new(session)),
room_id: Arc::new(room_id.to_owned()),
device_id,
account_identity_keys: identity_keys,
session_id: session_id.into(),
creation_time: Arc::new(Instant::now()),
message_count: Arc::new(AtomicU64::new(0)),
shared: Arc::new(AtomicBool::new(false)),
invalidated: Arc::new(AtomicBool::new(false)),
settings: Arc::new(settings),
shared_with_set: Arc::new(DashMap::new()),
to_share_with_set: Arc::new(DashMap::new()),
}
}
pub fn add_request(&self, request_id: Uuid, request: Arc<ToDeviceRequest>) {
self.to_share_with_set.insert(request_id, request);
}
pub fn add_recipient(&self, user_id: &UserId) {
self.shared_with_set
.entry(user_id.to_owned())
.or_insert_with(DashSet::new);
}
pub fn contains_recipient(&self, user_id: &UserId) -> bool {
self.shared_with_set.contains_key(user_id)
}
/// Mark the request with the given request id as sent.
///
/// This removes the request from the queue and marks the set of
/// users/devices that received the session.
pub fn mark_request_as_sent(&self, request_id: &Uuid) {
if let Some((_, r)) = self.to_share_with_set.remove(request_id) {
let user_pairs = r.messages.iter().map(|(u, v)| {
(
u.clone(),
v.keys().filter_map(|d| {
if let DeviceIdOrAllDevices::DeviceId(d) = d {
Some(d.clone())
} else {
None
}
}),
)
});
user_pairs.for_each(|(u, d)| {
self.shared_with_set
.entry(u)
.or_insert_with(DashSet::new)
.extend(d);
});
if self.to_share_with_set.is_empty() {
debug!(
"Marking session {} for room {} as shared.",
self.session_id(),
self.room_id
);
self.mark_as_shared();
}
}
}
/// Encrypt the given plaintext using this session.
///
/// Returns the encrypted ciphertext.
///
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub(crate) async fn encrypt_helper(&self, plaintext: String) -> String {
let session = self.inner.lock().await;
self.message_count.fetch_add(1, Ordering::SeqCst);
session.encrypt(plaintext)
}
/// Encrypt a room message for the given room.
///
/// Beware that a group session needs to be shared before this method can be
/// called using the `share_group_session()` method.
///
/// Since group sessions can expire or become invalid if the room membership
/// changes client authors should check with the
/// `should_share_group_session()` method if a new group session needs to
/// be shared.
///
/// # Arguments
///
/// * `content` - The plaintext content of the message that should be
/// encrypted.
///
/// # Panics
///
/// Panics if the content can't be serialized.
pub async fn encrypt(&self, content: AnyMessageEventContent) -> EncryptedEventContent {
let json_content = json!({
"content": content,
"room_id": &*self.room_id,
"type": content.event_type(),
});
let plaintext = json_content.to_string();
let ciphertext = self.encrypt_helper(plaintext).await;
EncryptedEventContent::MegolmV1AesSha2(
matrix_sdk_common::events::room::encrypted::MegolmV1AesSha2ContentInit {
ciphertext,
sender_key: self.account_identity_keys.curve25519().to_owned(),
session_id: self.session_id().to_owned(),
device_id: (&*self.device_id).to_owned(),
}
.into(),
)
}
/// Check if the session has expired and if it should be rotated.
///
/// A session will expire after some time or if enough messages have been
/// encrypted using it.
pub fn expired(&self) -> bool {
let count = self.message_count.load(Ordering::SeqCst);
count >= self.settings.rotation_period_msgs
|| self.creation_time.elapsed()
// Since the encryption settings are provided by users and not
// checked someone could set a really low rotation perdiod so
// clamp it at a minute.
>= min(self.settings.rotation_period, Duration::from_secs(3600))
}
/// Has the session been invalidated.
pub fn invalidated(&self) -> bool {
self.invalidated.load(Ordering::Relaxed)
}
/// Mark the session as shared.
///
/// Messages shouldn't be encrypted with the session before it has been
/// shared.
pub fn mark_as_shared(&self) {
self.shared.store(true, Ordering::Relaxed);
}
/// Check if the session has been marked as shared.
pub fn shared(&self) -> bool {
self.shared.load(Ordering::Relaxed)
}
/// Get the session key of this session.
///
/// A session key can be used to to create an `InboundGroupSession`.
pub async fn session_key(&self) -> GroupSessionKey {
let session = self.inner.lock().await;
GroupSessionKey(session.session_key())
}
/// Get the room id of the room this session belongs to.
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Get the current message index for this session.
///
/// Each message is sent with an increasing index. This returns the
/// message index that will be used for the next encrypted message.
pub async fn message_index(&self) -> u32 {
let session = self.inner.lock().await;
session.session_message_index()
}
/// Get the outbound group session key as a json value that can be sent as a
/// m.room_key.
pub async fn as_json(&self) -> Value {
json!({
"algorithm": EventEncryptionAlgorithm::MegolmV1AesSha2,
"room_id": &*self.room_id,
"session_id": &*self.session_id,
"session_key": self.session_key().await,
"chain_index": self.message_index().await,
})
}
/// Mark the session as invalid.
///
/// This should be called if an user/device deletes a device that received
/// this session.
pub fn invalidate_session(&self) {
self.invalidated.store(true, Ordering::Relaxed)
}
/// Clear out the requests returning the request ids.
pub fn clear_requests(&self) -> Vec<Uuid> {
let request_ids = self
.to_share_with_set
.iter()
.map(|item| *item.key())
.collect();
self.to_share_with_set.clear();
request_ids
}
/// Has or will the session be shared with the given user/device pair.
pub(crate) fn is_shared_with(&self, user_id: &UserId, device_id: &DeviceId) -> bool {
let shared_with = self
.shared_with_set
.get(user_id)
.map(|d| d.contains(device_id))
.unwrap_or(false);
let should_be_shared_with = if self.shared() {
false
} else {
let device_id = DeviceIdOrAllDevices::DeviceId(device_id.into());
self.to_share_with_set.iter().any(|item| {
if let Some(e) = item.value().messages.get(user_id) {
e.contains_key(&device_id)
} else {
false
}
})
};
shared_with || should_be_shared_with
}
/// Mark that the session was shared with the given user/device pair.
#[cfg(test)]
pub fn mark_shared_with(&self, user_id: &UserId, device_id: &DeviceId) {
self.shared_with_set
.entry(user_id.to_owned())
.or_insert_with(DashSet::new)
.insert(device_id.to_owned());
}
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for OutboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OutboundGroupSession")
.field("session_id", &self.session_id)
.field("room_id", &self.room_id)
.field("creation_time", &self.creation_time)
.field("message_count", &self.message_count)
.finish()
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use matrix_sdk_common::{
events::room::encryption::EncryptionEventContent, identifiers::EventEncryptionAlgorithm,
uint,
};
use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD};
#[test]
fn encryption_settings_conversion() {
let mut content = EncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
let settings = EncryptionSettings::from(&content);
assert_eq!(settings.rotation_period, ROTATION_PERIOD);
assert_eq!(settings.rotation_period_msgs, ROTATION_MESSAGES);
content.rotation_period_ms = Some(uint!(3600));
content.rotation_period_msgs = Some(uint!(500));
let settings = EncryptionSettings::from(&content);
assert_eq!(settings.rotation_period, Duration::from_millis(3600));
assert_eq!(settings.rotation_period_msgs, 500);
}
}
+246
View File
@@ -0,0 +1,246 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The crypto specific Olm objects.
//!
//! Note: You'll only be interested in these if you are implementing a custom
//! `CryptoStore`.
mod account;
mod group_sessions;
mod session;
mod signing;
mod utility;
pub(crate) use account::{Account, OlmDecryptionInfo, SessionType};
pub use account::{AccountPickle, OlmMessageHash, PickledAccount, ReadOnlyAccount};
pub use group_sessions::{
EncryptionSettings, ExportedRoomKey, InboundGroupSession, InboundGroupSessionPickle,
PickledInboundGroupSession,
};
pub(crate) use group_sessions::{GroupSessionKey, OutboundGroupSession};
pub use olm_rs::{account::IdentityKeys, PicklingMode};
pub use session::{PickledSession, Session, SessionPickle};
pub use signing::{PickledCrossSigningIdentity, PrivateCrossSigningIdentity};
pub(crate) use utility::Utility;
#[cfg(test)]
pub(crate) mod test {
use crate::olm::{InboundGroupSession, ReadOnlyAccount, Session};
use matrix_sdk_common::{
api::r0::keys::SignedKey,
events::forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
identifiers::{room_id, user_id, DeviceId, UserId},
};
use olm_rs::session::OlmMessage;
use std::{collections::BTreeMap, convert::TryInto};
fn alice_id() -> UserId {
user_id!("@alice:example.org")
}
fn alice_device_id() -> Box<DeviceId> {
"ALICEDEVICE".into()
}
fn bob_id() -> UserId {
user_id!("@bob:example.org")
}
fn bob_device_id() -> Box<DeviceId> {
"BOBDEVICE".into()
}
pub(crate) async fn get_account_and_session() -> (ReadOnlyAccount, Session) {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
bob.generate_one_time_keys_helper(1).await;
let one_time_key = bob
.one_time_keys()
.await
.curve25519()
.iter()
.next()
.unwrap()
.1
.to_owned();
let one_time_key = SignedKey {
key: one_time_key,
signatures: BTreeMap::new(),
};
let sender_key = bob.identity_keys().curve25519().to_owned();
let session = alice
.create_outbound_session_helper(&sender_key, &one_time_key)
.await
.unwrap();
(alice, session)
}
#[test]
fn account_creation() {
let account = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let identyty_keys = account.identity_keys();
assert!(!account.shared());
assert!(!identyty_keys.ed25519().is_empty());
assert_ne!(identyty_keys.values().len(), 0);
assert_ne!(identyty_keys.keys().len(), 0);
assert_ne!(identyty_keys.iter().len(), 0);
assert!(identyty_keys.contains_key("ed25519"));
assert_eq!(
identyty_keys.ed25519(),
identyty_keys.get("ed25519").unwrap()
);
assert!(!identyty_keys.curve25519().is_empty());
account.mark_as_shared();
assert!(account.shared());
}
#[tokio::test]
async fn one_time_keys_creation() {
let account = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let one_time_keys = account.one_time_keys().await;
assert!(one_time_keys.curve25519().is_empty());
assert_ne!(account.max_one_time_keys().await, 0);
account.generate_one_time_keys_helper(10).await;
let one_time_keys = account.one_time_keys().await;
assert!(!one_time_keys.curve25519().is_empty());
assert_ne!(one_time_keys.values().len(), 0);
assert_ne!(one_time_keys.keys().len(), 0);
assert_ne!(one_time_keys.iter().len(), 0);
assert!(one_time_keys.contains_key("curve25519"));
assert_eq!(one_time_keys.curve25519().keys().len(), 10);
assert_eq!(
one_time_keys.curve25519(),
one_time_keys.get("curve25519").unwrap()
);
account.mark_keys_as_published().await;
let one_time_keys = account.one_time_keys().await;
assert!(one_time_keys.curve25519().is_empty());
}
#[tokio::test]
async fn session_creation() {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
let alice_keys = alice.identity_keys();
alice.generate_one_time_keys_helper(1).await;
let one_time_keys = alice.one_time_keys().await;
alice.mark_keys_as_published().await;
let one_time_key = one_time_keys
.curve25519()
.iter()
.next()
.unwrap()
.1
.to_owned();
let one_time_key = SignedKey {
key: one_time_key,
signatures: BTreeMap::new(),
};
let mut bob_session = bob
.create_outbound_session_helper(alice_keys.curve25519(), &one_time_key)
.await
.unwrap();
let plaintext = "Hello world";
let message = bob_session.encrypt_helper(plaintext).await;
let prekey_message = match message.clone() {
OlmMessage::PreKey(m) => m,
OlmMessage::Message(_) => panic!("Incorrect message type"),
};
let bob_keys = bob.identity_keys();
let mut alice_session = alice
.create_inbound_session(bob_keys.curve25519(), prekey_message.clone())
.await
.unwrap();
assert!(alice_session
.matches(bob_keys.curve25519(), prekey_message)
.await
.unwrap());
assert_eq!(bob_session.session_id(), alice_session.session_id());
let decyrpted = alice_session.decrypt(message).await.unwrap();
assert_eq!(plaintext, decyrpted);
}
#[tokio::test]
async fn group_session_creation() {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let room_id = room_id!("!test:localhost");
let (outbound, _) = alice
.create_group_session_pair_with_defaults(&room_id)
.await
.unwrap();
assert_eq!(0, outbound.message_index().await);
assert!(!outbound.shared());
outbound.mark_as_shared();
assert!(outbound.shared());
let inbound = InboundGroupSession::new(
"test_key",
"test_key",
&room_id,
outbound.session_key().await,
)
.unwrap();
assert_eq!(0, inbound.first_known_index());
assert_eq!(outbound.session_id(), inbound.session_id());
let plaintext = "This is a secret to everybody".to_owned();
let ciphertext = outbound.encrypt_helper(plaintext.clone()).await;
assert_eq!(
plaintext,
inbound.decrypt_helper(ciphertext).await.unwrap().0
);
}
#[tokio::test]
async fn group_session_export() {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let room_id = room_id!("!test:localhost");
let (_, inbound) = alice
.create_group_session_pair_with_defaults(&room_id)
.await
.unwrap();
let export = inbound.export().await;
let export: ForwardedRoomKeyToDeviceEventContent = export.try_into().unwrap();
let imported = InboundGroupSession::from_export(export).unwrap();
assert_eq!(inbound.session_id(), imported.session_id());
}
}
+278
View File
@@ -0,0 +1,278 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, fmt, sync::Arc};
use matrix_sdk_common::{
events::{
room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content},
EventType,
},
identifiers::{DeviceId, DeviceKeyAlgorithm, UserId},
instant::{Duration, Instant},
locks::Mutex,
};
use olm_rs::{errors::OlmSessionError, session::OlmSession, PicklingMode};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use super::IdentityKeys;
use crate::{
error::{EventError, OlmResult, SessionUnpicklingError},
ReadOnlyDevice,
};
pub use olm_rs::{
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
/// Cryptographic session that enables secure communication between two
/// `Account`s
#[derive(Clone)]
pub struct Session {
pub(crate) user_id: Arc<UserId>,
pub(crate) device_id: Arc<Box<DeviceId>>,
pub(crate) our_identity_keys: Arc<IdentityKeys>,
pub(crate) inner: Arc<Mutex<OlmSession>>,
pub(crate) session_id: Arc<str>,
pub(crate) sender_key: Arc<str>,
pub(crate) creation_time: Arc<Instant>,
pub(crate) last_use_time: Arc<Instant>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Session")
.field("session_id", &self.session_id())
.field("sender_key", &self.sender_key)
.finish()
}
}
impl Session {
/// Decrypt the given Olm message.
///
/// Returns the decrypted plaintext or an `OlmSessionError` if decryption
/// failed.
///
/// # Arguments
///
/// * `message` - The Olm message that should be decrypted.
pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
let plaintext = self.inner.lock().await.decrypt(message)?;
self.last_use_time = Arc::new(Instant::now());
Ok(plaintext)
}
/// Encrypt the given plaintext as a OlmMessage.
///
/// Returns the encrypted Olm message.
///
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
let message = self.inner.lock().await.encrypt(plaintext);
self.last_use_time = Arc::new(Instant::now());
message
}
/// Encrypt the given event content content as an m.room.encrypted event
/// content.
///
/// # Arguments
///
/// * `recipient_device` - The device for which this message is going to be
/// encrypted, this needs to be the device that was used to create this
/// session with.
///
/// * `event_type` - The type of the event.
///
/// * `content` - The content of the event.
pub async fn encrypt(
&mut self,
recipient_device: &ReadOnlyDevice,
event_type: EventType,
content: Value,
) -> OlmResult<EncryptedEventContent> {
let recipient_signing_key = recipient_device
.get_key(DeviceKeyAlgorithm::Ed25519)
.ok_or(EventError::MissingSigningKey)?;
let payload = json!({
"sender": self.user_id.as_str(),
"sender_device": self.device_id.as_ref(),
"keys": {
"ed25519": self.our_identity_keys.ed25519(),
},
"recipient": recipient_device.user_id(),
"recipient_keys": {
"ed25519": recipient_signing_key,
},
"type": event_type,
"content": content,
});
let plaintext = serde_json::to_string(&payload)?;
let ciphertext = self.encrypt_helper(&plaintext).await.to_tuple();
let message_type = ciphertext.0;
let ciphertext = CiphertextInfo::new(ciphertext.1, (message_type as u32).into());
let mut content = BTreeMap::new();
content.insert((&*self.sender_key).to_owned(), ciphertext);
Ok(EncryptedEventContent::OlmV1Curve25519AesSha2(
OlmV1Curve25519AesSha2Content::new(
content,
self.our_identity_keys.curve25519().to_string(),
),
))
}
/// Check if a pre-key Olm message was encrypted for this session.
///
/// Returns true if it matches, false if not and a OlmSessionError if there
/// was an error checking if it matches.
///
/// # Arguments
///
/// * `their_identity_key` - The identity/curve25519 key of the account
/// that encrypted this Olm message.
///
/// * `message` - The pre-key Olm message that should be checked.
pub async fn matches(
&self,
their_identity_key: &str,
message: PreKeyMessage,
) -> Result<bool, OlmSessionError> {
self.inner
.lock()
.await
.matches_inbound_session_from(their_identity_key, message)
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Store the session as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> PickledSession {
let pickle = self.inner.lock().await.pickle(pickle_mode);
PickledSession {
pickle: SessionPickle::from(pickle),
sender_key: self.sender_key.to_string(),
// FIXME this should use the duration from the unix epoch.
creation_time: self.creation_time.elapsed(),
last_use_time: self.last_use_time.elapsed(),
}
}
/// Restore a Session from a previously pickled string.
///
/// Returns the restored Olm Session or a `SessionUnpicklingError` if there
/// was an error.
///
/// # Arguments
///
/// * `user_id` - Our own user id that the session belongs to.
///
/// * `device_id` - Our own device id that the session belongs to.
///
/// * `our_idenity_keys` - An clone of the Arc to our own identity keys.
///
/// * `pickle` - The pickled version of the `Session`.
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
pub fn from_pickle(
user_id: Arc<UserId>,
device_id: Arc<Box<DeviceId>>,
our_identity_keys: Arc<IdentityKeys>,
pickle: PickledSession,
pickle_mode: PicklingMode,
) -> Result<Self, SessionUnpicklingError> {
let session = OlmSession::unpickle(pickle.pickle.0, pickle_mode)?;
let session_id = session.session_id();
// FIXME this should use the UNIX epoch.
let now = Instant::now();
let creation_time = now
.checked_sub(pickle.creation_time)
.ok_or(SessionUnpicklingError::SessionTimestampError)?;
let last_use_time = now
.checked_sub(pickle.last_use_time)
.ok_or(SessionUnpicklingError::SessionTimestampError)?;
Ok(Session {
user_id,
device_id,
our_identity_keys,
inner: Arc::new(Mutex::new(session)),
session_id: session_id.into(),
sender_key: pickle.sender_key.into(),
creation_time: Arc::new(creation_time),
last_use_time: Arc::new(last_use_time),
})
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}
/// A pickled version of a `Session`.
///
/// Holds all the information that needs to be stored in a database to restore
/// a Session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledSession {
/// The pickle string holding the Olm Session.
pub pickle: SessionPickle,
/// The curve25519 key of the other user that we share this session with.
pub sender_key: String,
/// The relative time elapsed since the session was created.
pub creation_time: Duration,
/// The relative time elapsed since the session was last used.
pub last_use_time: Duration,
}
/// The typed representation of a base64 encoded string of the Olm Session pickle.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionPickle(String);
impl From<String> for SessionPickle {
fn from(picle_string: String) -> Self {
SessionPickle(picle_string)
}
}
impl SessionPickle {
/// Get the string representation of the pickle.
pub fn as_str(&self) -> &str {
&self.0
}
}
+599
View File
@@ -0,0 +1,599 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod pk_signing;
use serde::{Deserialize, Serialize};
use serde_json::Error as JsonError;
use std::{
collections::BTreeMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use matrix_sdk_common::{
api::r0::keys::{upload_signatures::Request as SignatureUploadRequest, KeyUsage},
encryption::DeviceKeys,
identifiers::{DeviceKeyAlgorithm, DeviceKeyId, UserId},
locks::Mutex,
};
use crate::{
error::SignatureError, requests::UploadSigningKeysRequest, OwnUserIdentity, ReadOnlyAccount,
ReadOnlyDevice, UserIdentity,
};
use pk_signing::{MasterSigning, PickledSignings, SelfSigning, Signing, SigningError, UserSigning};
/// Private cross signing identity.
///
/// This object holds the private and public ed25519 key triplet that is used
/// for cross signing.
///
/// The object might be comletely empty or have only some of the key pairs
/// available.
///
/// It can be used to sign devices or other identities.
#[derive(Clone, Debug)]
pub struct PrivateCrossSigningIdentity {
user_id: Arc<UserId>,
shared: Arc<AtomicBool>,
pub(crate) master_key: Arc<Mutex<Option<MasterSigning>>>,
pub(crate) user_signing_key: Arc<Mutex<Option<UserSigning>>>,
pub(crate) self_signing_key: Arc<Mutex<Option<SelfSigning>>>,
}
/// The pickled version of a `PrivateCrossSigningIdentity`.
///
/// Can be used to store the identity.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledCrossSigningIdentity {
/// The user id of the identity owner.
pub user_id: UserId,
/// Have the public keys of the identity been shared.
pub shared: bool,
/// The encrypted pickle of the identity.
pub pickle: String,
}
impl PrivateCrossSigningIdentity {
/// Get the user id that this identity belongs to.
pub fn user_id(&self) -> &UserId {
&self.user_id
}
/// Is the identity empty.
///
/// An empty identity doesn't contain any private keys.
///
/// It is usual for the identity not to contain the master key since the
/// master key is only needed to sign the subkeys.
///
/// An empty identity indicates that either no identity was created for this
/// use or that another device created it and hasn't shared it yet with us.
pub async fn is_empty(&self) -> bool {
let has_master = self.master_key.lock().await.is_some();
let has_user = self.user_signing_key.lock().await.is_some();
let has_self = self.self_signing_key.lock().await.is_some();
!(has_master && has_user && has_self)
}
/// Create a new empty identity.
pub(crate) fn empty(user_id: UserId) -> Self {
Self {
user_id: Arc::new(user_id),
shared: Arc::new(AtomicBool::new(false)),
master_key: Arc::new(Mutex::new(None)),
self_signing_key: Arc::new(Mutex::new(None)),
user_signing_key: Arc::new(Mutex::new(None)),
}
}
pub(crate) async fn as_public_identity(&self) -> Result<OwnUserIdentity, SignatureError> {
let master = self
.master_key
.lock()
.await
.as_ref()
.ok_or(SignatureError::MissingSigningKey)?
.public_key
.clone();
let self_signing = self
.self_signing_key
.lock()
.await
.as_ref()
.ok_or(SignatureError::MissingSigningKey)?
.public_key
.clone();
let user_signing = self
.user_signing_key
.lock()
.await
.as_ref()
.ok_or(SignatureError::MissingSigningKey)?
.public_key
.clone();
let identity = OwnUserIdentity::new(master, self_signing, user_signing)?;
identity.mark_as_verified();
Ok(identity)
}
/// Sign the given public user identity with this private identity.
pub(crate) async fn sign_user(
&self,
user_identity: &UserIdentity,
) -> Result<SignatureUploadRequest, SignatureError> {
let signed_keys = self
.user_signing_key
.lock()
.await
.as_ref()
.ok_or(SignatureError::MissingSigningKey)?
.sign_user(&user_identity)
.await?;
Ok(SignatureUploadRequest::new(signed_keys))
}
/// Sign the given device keys with this identity.
pub(crate) async fn sign_device(
&self,
device: &ReadOnlyDevice,
) -> Result<SignatureUploadRequest, SignatureError> {
let mut device_keys = device.as_device_keys();
device_keys.signatures.clear();
self.sign_device_keys(&mut device_keys).await
}
/// Sign an Olm account with this private identity.
pub(crate) async fn sign_account(
&self,
account: &ReadOnlyAccount,
) -> Result<SignatureUploadRequest, SignatureError> {
let mut device_keys = account.unsigned_device_keys();
self.sign_device_keys(&mut device_keys).await
}
pub(crate) async fn sign_device_keys(
&self,
mut device_keys: &mut DeviceKeys,
) -> Result<SignatureUploadRequest, SignatureError> {
self.self_signing_key
.lock()
.await
.as_ref()
.ok_or(SignatureError::MissingSigningKey)?
.sign_device(&mut device_keys)
.await?;
let mut signed_keys = BTreeMap::new();
signed_keys
.entry((&*self.user_id).to_owned())
.or_insert_with(BTreeMap::new)
.insert(
device_keys.device_id.to_string(),
serde_json::to_value(device_keys)?,
);
Ok(SignatureUploadRequest::new(signed_keys))
}
/// Create a new identity for the given Olm Account.
///
/// Returns the new identity, the upload signing keys request and a
/// signature upload request that contains the signature of the account
/// signed by the self signing key.
///
/// # Arguments
///
/// * `account` - The Olm account that is creating the new identity. The
/// account will sign the master key and the self signing key will sign the
/// account.
pub(crate) async fn new_with_account(
account: &ReadOnlyAccount,
) -> (Self, UploadSigningKeysRequest, SignatureUploadRequest) {
let master = Signing::new();
let mut public_key =
master.cross_signing_key(account.user_id().to_owned(), KeyUsage::Master);
let signature = account
.sign_json(
serde_json::to_value(&public_key)
.expect("Can't convert own public master key to json"),
)
.await;
public_key
.signatures
.entry(account.user_id().to_owned())
.or_insert_with(BTreeMap::new)
.insert(
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, account.device_id())
.to_string(),
signature,
);
let master = MasterSigning {
inner: master,
public_key: public_key.into(),
};
let identity = Self::new_helper(account.user_id(), master).await;
let signature_request = identity
.sign_account(account)
.await
.expect("Can't sign own device with new cross signign keys");
let request = identity.as_upload_request().await;
(identity, request, signature_request)
}
async fn new_helper(user_id: &UserId, master: MasterSigning) -> Self {
let user = Signing::new();
let mut public_key = user.cross_signing_key(user_id.to_owned(), KeyUsage::UserSigning);
master.sign_subkey(&mut public_key).await;
let user = UserSigning {
inner: user,
public_key: public_key.into(),
};
let self_signing = Signing::new();
let mut public_key =
self_signing.cross_signing_key(user_id.to_owned(), KeyUsage::SelfSigning);
master.sign_subkey(&mut public_key).await;
let self_signing = SelfSigning {
inner: self_signing,
public_key: public_key.into(),
};
Self {
user_id: Arc::new(user_id.to_owned()),
shared: Arc::new(AtomicBool::new(false)),
master_key: Arc::new(Mutex::new(Some(master))),
self_signing_key: Arc::new(Mutex::new(Some(self_signing))),
user_signing_key: Arc::new(Mutex::new(Some(user))),
}
}
/// Create a new cross signing identity without signing the device that
/// created it.
#[cfg(test)]
pub(crate) async fn new(user_id: UserId) -> Self {
let master = Signing::new();
let public_key = master.cross_signing_key(user_id.clone(), KeyUsage::Master);
let master = MasterSigning {
inner: master,
public_key: public_key.into(),
};
Self::new_helper(&user_id, master).await
}
/// Mark the identity as shared.
pub fn mark_as_shared(&self) {
self.shared.store(true, Ordering::SeqCst)
}
/// Has the identity been shared.
///
/// A shared identity here means that the public keys of the identity have
/// been uploaded to the server.
pub fn shared(&self) -> bool {
self.shared.load(Ordering::SeqCst)
}
/// Store the cross signing identity as a pickle.
///
/// # Arguments
///
/// * `pickle_key` - The key that should be used to encrypt the signing
/// object, must be 32 bytes long.
///
/// # Panics
///
/// This will panic if the provided pickle key isn't 32 bytes long.
pub async fn pickle(
&self,
pickle_key: &[u8],
) -> Result<PickledCrossSigningIdentity, JsonError> {
let master_key = if let Some(m) = self.master_key.lock().await.as_ref() {
Some(m.pickle(pickle_key).await)
} else {
None
};
let self_signing_key = if let Some(m) = self.self_signing_key.lock().await.as_ref() {
Some(m.pickle(pickle_key).await)
} else {
None
};
let user_signing_key = if let Some(m) = self.user_signing_key.lock().await.as_ref() {
Some(m.pickle(pickle_key).await)
} else {
None
};
let pickle = PickledSignings {
master_key,
user_signing_key,
self_signing_key,
};
let pickle = serde_json::to_string(&pickle)?;
Ok(PickledCrossSigningIdentity {
user_id: self.user_id.as_ref().to_owned(),
shared: self.shared(),
pickle,
})
}
/// Restore the private cross signing identity from a pickle.
///
/// # Panic
///
/// Panics if the pickle_key isn't 32 bytes long.
pub async fn from_pickle(
pickle: PickledCrossSigningIdentity,
pickle_key: &[u8],
) -> Result<Self, SigningError> {
let signings: PickledSignings = serde_json::from_str(&pickle.pickle)?;
let master = if let Some(m) = signings.master_key {
Some(MasterSigning::from_pickle(m, pickle_key)?)
} else {
None
};
let self_signing = if let Some(s) = signings.self_signing_key {
Some(SelfSigning::from_pickle(s, pickle_key)?)
} else {
None
};
let user_signing = if let Some(u) = signings.user_signing_key {
Some(UserSigning::from_pickle(u, pickle_key)?)
} else {
None
};
Ok(Self {
user_id: Arc::new(pickle.user_id),
shared: Arc::new(AtomicBool::from(pickle.shared)),
master_key: Arc::new(Mutex::new(master)),
self_signing_key: Arc::new(Mutex::new(self_signing)),
user_signing_key: Arc::new(Mutex::new(user_signing)),
})
}
/// Get the upload request that is needed to share the public keys of this
/// identity.
pub(crate) async fn as_upload_request(&self) -> UploadSigningKeysRequest {
let master_key = self
.master_key
.lock()
.await
.as_ref()
.cloned()
.map(|k| k.public_key.into());
let user_signing_key = self
.user_signing_key
.lock()
.await
.as_ref()
.cloned()
.map(|k| k.public_key.into());
let self_signing_key = self
.self_signing_key
.lock()
.await
.as_ref()
.cloned()
.map(|k| k.public_key.into());
UploadSigningKeysRequest {
master_key,
user_signing_key,
self_signing_key,
}
}
}
#[cfg(test)]
mod test {
use crate::{
identities::{ReadOnlyDevice, UserIdentity},
olm::ReadOnlyAccount,
};
use std::{collections::BTreeMap, sync::Arc};
use super::{PrivateCrossSigningIdentity, Signing};
use matrix_sdk_common::{
api::r0::keys::CrossSigningKey,
identifiers::{user_id, UserId},
};
use matrix_sdk_test::async_test;
fn user_id() -> UserId {
user_id!("@example:localhost")
}
fn pickle_key() -> &'static [u8] {
&[0u8; 32]
}
#[test]
fn signing_creation() {
let signing = Signing::new();
assert!(!signing.public_key().as_str().is_empty());
}
#[async_test]
async fn signature_verification() {
let signing = Signing::new();
let message = "Hello world";
let signature = signing.sign(message).await;
assert!(signing.verify(message, &signature).await.is_ok());
}
#[async_test]
async fn pickling_signing() {
let signing = Signing::new();
let pickled = signing.pickle(pickle_key()).await;
let unpickled = Signing::from_pickle(pickled, pickle_key()).unwrap();
assert_eq!(signing.public_key(), unpickled.public_key());
}
#[async_test]
async fn private_identity_creation() {
let identity = PrivateCrossSigningIdentity::new(user_id()).await;
let master_key = identity.master_key.lock().await;
let master_key = master_key.as_ref().unwrap();
assert!(master_key
.public_key
.verify_subkey(
&identity
.self_signing_key
.lock()
.await
.as_ref()
.unwrap()
.public_key,
)
.is_ok());
assert!(master_key
.public_key
.verify_subkey(
&identity
.user_signing_key
.lock()
.await
.as_ref()
.unwrap()
.public_key,
)
.is_ok());
}
#[async_test]
async fn identity_pickling() {
let identity = PrivateCrossSigningIdentity::new(user_id()).await;
let pickled = identity.pickle(pickle_key()).await.unwrap();
let unpickled = PrivateCrossSigningIdentity::from_pickle(pickled, pickle_key())
.await
.unwrap();
assert_eq!(identity.user_id, unpickled.user_id);
assert_eq!(
&*identity.master_key.lock().await,
&*unpickled.master_key.lock().await
);
assert_eq!(
&*identity.user_signing_key.lock().await,
&*unpickled.user_signing_key.lock().await
);
assert_eq!(
&*identity.self_signing_key.lock().await,
&*unpickled.self_signing_key.lock().await
);
}
#[async_test]
async fn private_identity_signed_by_accound() {
let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into());
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let master = identity.master_key.lock().await;
let master = master.as_ref().unwrap();
assert!(!master.public_key.signatures().is_empty());
}
#[async_test]
async fn sign_device() {
let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into());
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let mut device = ReadOnlyDevice::from_account(&account).await;
let self_signing = identity.self_signing_key.lock().await;
let self_signing = self_signing.as_ref().unwrap();
let mut device_keys = device.as_device_keys();
self_signing.sign_device(&mut device_keys).await.unwrap();
device.signatures = Arc::new(device_keys.signatures);
let public_key = &self_signing.public_key;
public_key.verify_device(&device).unwrap()
}
#[async_test]
async fn sign_user_identity() {
let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into());
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let bob_account = ReadOnlyAccount::new(&user_id!("@bob:localhost"), "DEVICEID".into());
let (bob_private, _, _) = PrivateCrossSigningIdentity::new_with_account(&bob_account).await;
let mut bob_public = UserIdentity::from_private(&bob_private).await;
let user_signing = identity.user_signing_key.lock().await;
let user_signing = user_signing.as_ref().unwrap();
let signatures = user_signing.sign_user(&bob_public).await.unwrap();
let (key_id, signature) = signatures
.iter()
.next()
.unwrap()
.1
.iter()
.next()
.map(|(k, s)| (k.to_string(), serde_json::from_value(s.to_owned()).unwrap()))
.unwrap();
let mut master: CrossSigningKey = bob_public.master_key.as_ref().clone();
master
.signatures
.entry(identity.user_id().to_owned())
.or_insert_with(BTreeMap::new)
.insert(key_id, signature);
bob_public.master_key = master.into();
user_signing
.public_key
.verify_master_key(bob_public.master_key())
.unwrap();
}
}
@@ -0,0 +1,416 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use aes_gcm::{
aead::{generic_array::GenericArray, Aead, NewAead},
Aes256Gcm,
};
use getrandom::getrandom;
use matrix_sdk_common::{
encryption::DeviceKeys,
identifiers::{DeviceKeyAlgorithm, DeviceKeyId},
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Error as JsonError, Value};
use std::{collections::BTreeMap, convert::TryInto, sync::Arc};
use thiserror::Error;
use zeroize::Zeroizing;
use olm_rs::pk::OlmPkSigning;
#[cfg(test)]
use olm_rs::{errors::OlmUtilityError, utility::OlmUtility};
use matrix_sdk_common::{
api::r0::keys::{CrossSigningKey, KeyUsage},
identifiers::UserId,
locks::Mutex,
CanonicalJsonValue,
};
use crate::{
error::SignatureError,
identities::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
utilities::{decode_url_safe as decode, encode_url_safe as encode, DecodeError},
UserIdentity,
};
const NONCE_SIZE: usize = 12;
/// Error type reporting failures in the Signign operations.
#[derive(Debug, Error)]
pub enum SigningError {
/// Error decoding the base64 encoded pickle data.
#[error(transparent)]
Decode(#[from] DecodeError),
/// Error decrypting the pickled signing seed
#[error("Error decrypting the pickled signign seed")]
Decryption(String),
/// Error deserializing the pickle data.
#[error(transparent)]
Json(#[from] JsonError),
}
#[derive(Clone)]
pub struct Signing {
inner: Arc<Mutex<OlmPkSigning>>,
seed: Arc<Zeroizing<Vec<u8>>>,
public_key: PublicSigningKey,
}
impl std::fmt::Debug for Signing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Signing")
.field("public_key", &self.public_key.as_str())
.finish()
}
}
impl PartialEq for Signing {
fn eq(&self, other: &Signing) -> bool {
self.seed == other.seed
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InnerPickle {
version: u8,
nonce: String,
ciphertext: String,
}
#[derive(Clone, PartialEq, Debug)]
pub struct MasterSigning {
pub inner: Signing,
pub public_key: MasterPubkey,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PickledMasterSigning {
pickle: PickledSigning,
public_key: CrossSigningKey,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PickledUserSigning {
pickle: PickledSigning,
public_key: CrossSigningKey,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PickledSelfSigning {
pickle: PickledSigning,
public_key: CrossSigningKey,
}
impl Signature {
#[cfg(test)]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl PickledSigning {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublicSigningKey(Arc<str>);
impl PublicSigningKey {
pub fn as_str(&self) -> &str {
&self.0
}
#[allow(clippy::inherent_to_string)]
fn to_string(&self) -> String {
self.0.to_string()
}
}
impl MasterSigning {
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledMasterSigning {
let pickle = self.inner.pickle(pickle_key).await;
let public_key = self.public_key.clone().into();
PickledMasterSigning { pickle, public_key }
}
pub fn from_pickle(
pickle: PickledMasterSigning,
pickle_key: &[u8],
) -> Result<Self, SigningError> {
let inner = Signing::from_pickle(pickle.pickle, pickle_key)?;
Ok(Self {
inner,
public_key: pickle.public_key.into(),
})
}
pub async fn sign_subkey<'a>(&self, subkey: &mut CrossSigningKey) {
let subkey_wihtout_signatures = json!({
"user_id": subkey.user_id.clone(),
"keys": subkey.keys.clone(),
"usage": subkey.usage.clone(),
});
let message = serde_json::to_string(&subkey_wihtout_signatures)
.expect("Can't serialize cross signing subkey");
let signature = self.inner.sign(&message).await;
subkey
.signatures
.entry(self.public_key.user_id().to_owned())
.or_insert_with(BTreeMap::new)
.insert(
DeviceKeyId::from_parts(
DeviceKeyAlgorithm::Ed25519,
self.inner.public_key().as_str().into(),
)
.to_string(),
signature.0,
);
}
}
impl UserSigning {
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledUserSigning {
let pickle = self.inner.pickle(pickle_key).await;
let public_key = self.public_key.clone().into();
PickledUserSigning { pickle, public_key }
}
pub async fn sign_user(
&self,
user: &UserIdentity,
) -> Result<BTreeMap<UserId, BTreeMap<String, Value>>, SignatureError> {
let user_master: &CrossSigningKey = user.master_key().as_ref();
let signature = self
.inner
.sign_json(serde_json::to_value(user_master)?)
.await?;
let mut signatures = BTreeMap::new();
signatures
.entry(self.public_key.user_id().to_owned())
.or_insert_with(BTreeMap::new)
.insert(
DeviceKeyId::from_parts(
DeviceKeyAlgorithm::Ed25519,
self.inner.public_key.as_str().into(),
)
.to_string(),
serde_json::to_value(signature.0)?,
);
Ok(signatures)
}
pub fn from_pickle(
pickle: PickledUserSigning,
pickle_key: &[u8],
) -> Result<Self, SigningError> {
let inner = Signing::from_pickle(pickle.pickle, pickle_key)?;
Ok(Self {
inner,
public_key: pickle.public_key.into(),
})
}
}
impl SelfSigning {
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledSelfSigning {
let pickle = self.inner.pickle(pickle_key).await;
let public_key = self.public_key.clone().into();
PickledSelfSigning { pickle, public_key }
}
pub async fn sign_device_helper(&self, value: Value) -> Result<Signature, SignatureError> {
self.inner.sign_json(value).await
}
pub async fn sign_device(&self, device_keys: &mut DeviceKeys) -> Result<(), SignatureError> {
// Create a copy of the device keys containing only fields that will
// get signed.
let json_device = json!({
"user_id": device_keys.user_id,
"device_id": device_keys.device_id,
"algorithms": device_keys.algorithms,
"keys": device_keys.keys,
});
let signature = self.sign_device_helper(json_device).await?;
device_keys
.signatures
.entry(self.public_key.user_id().to_owned())
.or_insert_with(BTreeMap::new)
.insert(
DeviceKeyId::from_parts(
DeviceKeyAlgorithm::Ed25519,
self.inner.public_key.as_str().into(),
),
signature.0,
);
Ok(())
}
pub fn from_pickle(
pickle: PickledSelfSigning,
pickle_key: &[u8],
) -> Result<Self, SigningError> {
let inner = Signing::from_pickle(pickle.pickle, pickle_key)?;
Ok(Self {
inner,
public_key: pickle.public_key.into(),
})
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct SelfSigning {
pub inner: Signing,
pub public_key: SelfSigningPubkey,
}
#[derive(Clone, PartialEq, Debug)]
pub struct UserSigning {
pub inner: Signing,
pub public_key: UserSigningPubkey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledSignings {
pub master_key: Option<PickledMasterSigning>,
pub user_signing_key: Option<PickledUserSigning>,
pub self_signing_key: Option<PickledSelfSigning>,
}
#[derive(Debug, Clone)]
pub struct Signature(String);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledSigning(String);
impl Signing {
pub fn new() -> Self {
let seed = OlmPkSigning::generate_seed();
Self::from_seed(seed)
}
pub fn from_seed(seed: Vec<u8>) -> Self {
let inner = OlmPkSigning::new(seed.clone()).expect("Unable to create pk signing object");
let public_key = PublicSigningKey(inner.public_key().into());
Signing {
inner: Arc::new(Mutex::new(inner)),
seed: Arc::new(Zeroizing::from(seed)),
public_key,
}
}
pub fn from_pickle(pickle: PickledSigning, pickle_key: &[u8]) -> Result<Self, SigningError> {
let pickled: InnerPickle = serde_json::from_str(pickle.as_str())?;
let key = GenericArray::from_slice(pickle_key);
let cipher = Aes256Gcm::new(key);
let nonce = decode(pickled.nonce)?;
let nonce = GenericArray::from_slice(&nonce);
let ciphertext = &decode(pickled.ciphertext)?;
let seed = cipher
.decrypt(&nonce, ciphertext.as_slice())
.map_err(|e| SigningError::Decryption(e.to_string()))?;
Ok(Self::from_seed(seed))
}
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledSigning {
let key = GenericArray::from_slice(pickle_key);
let cipher = Aes256Gcm::new(key);
let mut nonce = vec![0u8; NONCE_SIZE];
getrandom(&mut nonce).expect("Can't generate nonce to pickle the signing object");
let nonce = GenericArray::from_slice(nonce.as_slice());
let ciphertext = cipher
.encrypt(nonce, self.seed.as_slice())
.expect("Can't encrypt signing pickle");
let ciphertext = encode(ciphertext);
let pickle = InnerPickle {
version: 1,
nonce: encode(nonce.as_slice()),
ciphertext,
};
PickledSigning(serde_json::to_string(&pickle).expect("Can't encode pickled signing"))
}
pub fn public_key(&self) -> &PublicSigningKey {
&self.public_key
}
pub fn cross_signing_key(&self, user_id: UserId, usage: KeyUsage) -> CrossSigningKey {
let mut keys = BTreeMap::new();
keys.insert(
DeviceKeyId::from_parts(
DeviceKeyAlgorithm::Ed25519,
self.public_key().as_str().into(),
)
.to_string(),
self.public_key().to_string(),
);
CrossSigningKey {
user_id,
usage: vec![usage],
keys,
signatures: BTreeMap::new(),
}
}
#[cfg(test)]
pub async fn verify(
&self,
message: &str,
signature: &Signature,
) -> Result<bool, OlmUtilityError> {
let utility = OlmUtility::new();
utility.ed25519_verify(self.public_key.as_str(), message, signature.as_str())
}
pub async fn sign_json(&self, mut json: Value) -> Result<Signature, SignatureError> {
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
let _ = json_object.remove("signatures");
let canonical_json: CanonicalJsonValue =
json.try_into().expect("Can't canonicalize the json value");
Ok(self.sign(&canonical_json.to_string()).await)
}
pub async fn sign(&self, message: &str) -> Signature {
Signature(self.inner.lock().await.sign(message))
}
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use olm_rs::utility::OlmUtility;
use serde_json::Value;
use std::convert::TryInto;
use matrix_sdk_common::{
identifiers::{DeviceKeyAlgorithm, DeviceKeyId, UserId},
CanonicalJsonValue,
};
use crate::error::SignatureError;
pub(crate) struct Utility {
inner: OlmUtility,
}
impl Utility {
pub fn new() -> Self {
Self {
inner: OlmUtility::new(),
}
}
/// Verify a signed JSON object.
///
/// The object must have a signatures key associated with an object of the
/// form `user_id: {key_id: signature}`.
///
/// Returns Ok if the signature was successfully verified, otherwise an
/// SignatureError.
///
/// # Arguments
///
/// * `user_id` - The user who signed the JSON object.
///
/// * `key_id` - The id of the key that signed the JSON object.
///
/// * `signing_key` - The public ed25519 key which was used to sign the JSON
/// object.
///
/// * `json` - The JSON object that should be verified.
pub(crate) fn verify_json(
&self,
user_id: &UserId,
key_id: &DeviceKeyId,
signing_key: &str,
json: &mut Value,
) -> Result<(), SignatureError> {
if key_id.algorithm() != DeviceKeyAlgorithm::Ed25519 {
return Err(SignatureError::UnsupportedAlgorithm);
}
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
let unsigned = json_object.remove("unsigned");
let signatures = json_object.remove("signatures");
let canonical_json: CanonicalJsonValue = json
.clone()
.try_into()
.map_err(|_| SignatureError::NotAnObject)?;
let canonical_json: String = canonical_json.to_string();
let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?;
let signature_object = signatures
.as_object()
.ok_or(SignatureError::NoSignatureFound)?;
let signature = signature_object
.get(user_id.as_str())
.ok_or(SignatureError::NoSignatureFound)?;
let signature = signature
.get(key_id.to_string())
.ok_or(SignatureError::NoSignatureFound)?;
let signature = signature.as_str().ok_or(SignatureError::NoSignatureFound)?;
let ret = match self
.inner
.ed25519_verify(signing_key, &canonical_json, signature)
{
Ok(_) => Ok(()),
Err(_) => Err(SignatureError::VerificationError),
};
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
if let Some(u) = unsigned {
json_object.insert("unsigned".to_string(), u);
}
json_object.insert("signatures".to_string(), signatures);
ret
}
}
#[cfg(test)]
mod test {
use super::Utility;
use matrix_sdk_common::identifiers::{user_id, DeviceKeyAlgorithm, DeviceKeyId};
use serde_json::json;
#[test]
fn signature_test() {
let mut device_keys = json!({
"device_id": "GBEWHQOYGS",
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"keys": {
"curve25519:GBEWHQOYGS": "F8QhZ0Z1rjtWrQOblMDgZtEX5x1UrG7sZ2Kk3xliNAU",
"ed25519:GBEWHQOYGS": "n469gw7zm+KW+JsFIJKnFVvCKU14HwQyocggcCIQgZY"
},
"signatures": {
"@example:localhost": {
"ed25519:GBEWHQOYGS": "OlF2REsqjYdAfr04ONx8VS/5cB7KjrWYRlLF4eUm2foAiQL/RAfsjsa2JXZeoOHh6vEualZHbWlod49OewVqBg"
}
},
"unsigned": {
"device_display_name": "Weechat-Matrix-rs"
},
"user_id": "@example:localhost"
});
let signing_key = "n469gw7zm+KW+JsFIJKnFVvCKU14HwQyocggcCIQgZY";
let utility = Utility::new();
utility
.verify_json(
&user_id!("@example:localhost"),
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, "GBEWHQOYGS".into()),
&signing_key,
&mut device_keys,
)
.expect("Can't verify device keys");
}
}
+232
View File
@@ -0,0 +1,232 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use matrix_sdk_common::{
api::r0::{
keys::{
claim_keys::Response as KeysClaimResponse,
get_keys::Response as KeysQueryResponse,
upload_keys::{Request as KeysUploadRequest, Response as KeysUploadResponse},
upload_signatures::{
Request as SignatureUploadRequest, Response as SignatureUploadResponse,
},
upload_signing_keys::Response as SigningKeysUploadResponse,
CrossSigningKey,
},
to_device::{send_event_to_device::Response as ToDeviceResponse, DeviceIdOrAllDevices},
},
events::EventType,
identifiers::{DeviceIdBox, UserId},
uuid::Uuid,
};
use serde_json::value::RawValue as RawJsonValue;
/// Customized version of `ruma_client_api::r0::to_device::send_event_to_device::Request`, using a
/// UUID for the transaction ID.
#[derive(Clone, Debug)]
pub struct ToDeviceRequest {
/// Type of event being sent to each device.
pub event_type: EventType,
/// A request identifier unique to the access token used to send the request.
pub txn_id: Uuid,
/// A map of users to devices to a content for a message event to be
/// sent to the user's device. Individual message events can be sent
/// to devices, but all events must be of the same type.
/// The content's type for this field will be updated in a future
/// release, until then you can create a value using
/// `serde_json::value::to_raw_value`.
pub messages: BTreeMap<UserId, BTreeMap<DeviceIdOrAllDevices, Box<RawJsonValue>>>,
}
impl ToDeviceRequest {
/// Gets the transaction ID as a string.
pub fn txn_id_string(&self) -> String {
self.txn_id.to_string()
}
}
/// Request that will publish a cross signing identity.
///
/// This uploads the public cross signing key triplet.
#[derive(Debug, Clone)]
pub struct UploadSigningKeysRequest {
/// The user's master key.
pub master_key: Option<CrossSigningKey>,
/// The user's self-signing key. Must be signed with the accompanied master, or by the
/// user's most recently uploaded master key if no master key is included in the request.
pub self_signing_key: Option<CrossSigningKey>,
/// The user's user-signing key. Must be signed with the accompanied master, or by the
/// user's most recently uploaded master key if no master key is included in the request.
pub user_signing_key: Option<CrossSigningKey>,
}
/// Customized version of `ruma_client_api::r0::keys::get_keys::Request`, without any references.
#[derive(Clone, Debug)]
pub struct KeysQueryRequest {
/// The time (in milliseconds) to wait when downloading keys from remote
/// servers. 10 seconds is the recommended default.
pub timeout: Option<Duration>,
/// The keys to be downloaded. An empty list indicates all devices for
/// the corresponding user.
pub device_keys: BTreeMap<UserId, Vec<DeviceIdBox>>,
/// If the client is fetching keys as a result of a device update
/// received in a sync request, this should be the 'since' token of that
/// sync request, or any later sync token. This allows the server to
/// ensure its response contains the keys advertised by the notification
/// in that sync.
pub token: Option<String>,
}
impl KeysQueryRequest {
pub(crate) fn new(device_keys: BTreeMap<UserId, Vec<DeviceIdBox>>) -> Self {
Self {
timeout: None,
device_keys,
token: None,
}
}
}
/// Enum over the different outgoing requests we can have.
#[derive(Debug)]
pub enum OutgoingRequests {
/// The keys upload request, uploading device and one-time keys.
KeysUpload(KeysUploadRequest),
/// The keys query request, fetching the device and cross singing keys of
/// other users.
KeysQuery(KeysQueryRequest),
/// The to-device requests, this request is used for a couple of different
/// things, the main use is key requests/forwards and interactive device
/// verification.
ToDeviceRequest(ToDeviceRequest),
/// Signature upload request, this request is used after a successful device
/// or user verification is done.
SignatureUpload(SignatureUploadRequest),
}
#[cfg(test)]
impl OutgoingRequests {
pub fn to_device(&self) -> Option<&ToDeviceRequest> {
match self {
OutgoingRequests::ToDeviceRequest(r) => Some(r),
_ => None,
}
}
}
impl From<KeysQueryRequest> for OutgoingRequests {
fn from(request: KeysQueryRequest) -> Self {
OutgoingRequests::KeysQuery(request)
}
}
impl From<KeysUploadRequest> for OutgoingRequests {
fn from(request: KeysUploadRequest) -> Self {
OutgoingRequests::KeysUpload(request)
}
}
impl From<ToDeviceRequest> for OutgoingRequests {
fn from(request: ToDeviceRequest) -> Self {
OutgoingRequests::ToDeviceRequest(request)
}
}
impl From<SignatureUploadRequest> for OutgoingRequests {
fn from(request: SignatureUploadRequest) -> Self {
OutgoingRequests::SignatureUpload(request)
}
}
/// Enum over all the incoming responses we need to receive.
#[derive(Debug)]
pub enum IncomingResponse<'a> {
/// The keys upload response, notifying us about the amount of uploaded
/// one-time keys.
KeysUpload(&'a KeysUploadResponse),
/// The keys query response, giving us the device and cross singing keys of
/// other users.
KeysQuery(&'a KeysQueryResponse),
/// The to-device response, an empty response.
ToDevice(&'a ToDeviceResponse),
/// The key claiming requests, giving us new one-time keys of other users so
/// new Olm sessions can be created.
KeysClaim(&'a KeysClaimResponse),
/// The cross signing keys upload response, marking our private cross
/// signing identity as shared.
SigningKeysUpload(&'a SigningKeysUploadResponse),
/// The cross signing keys upload response, marking our private cross
/// signing identity as shared.
SignatureUpload(&'a SignatureUploadResponse),
}
impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysUploadResponse) -> Self {
IncomingResponse::KeysUpload(response)
}
}
impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysQueryResponse) -> Self {
IncomingResponse::KeysQuery(response)
}
}
impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> {
fn from(response: &'a ToDeviceResponse) -> Self {
IncomingResponse::ToDevice(response)
}
}
impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysClaimResponse) -> Self {
IncomingResponse::KeysClaim(response)
}
}
impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> {
fn from(response: &'a SignatureUploadResponse) -> Self {
IncomingResponse::SignatureUpload(response)
}
}
/// Outgoing request type, holds the unique ID of the request and the actual
/// request.
#[derive(Debug, Clone)]
pub struct OutgoingRequest {
/// The unique id of a request, needs to be passed when receiving a
/// response.
pub(crate) request_id: Uuid,
/// The underlying outgoing request.
pub(crate) request: Arc<OutgoingRequests>,
}
impl OutgoingRequest {
/// Get the unique id of this request.
pub fn request_id(&self) -> &Uuid {
&self.request_id
}
/// Get the underlying outgoing request.
pub fn request(&self) -> &OutgoingRequests {
&self.request
}
}
@@ -0,0 +1,243 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
use dashmap::DashMap;
use matrix_sdk_common::{
api::r0::to_device::DeviceIdOrAllDevices,
events::{room::encrypted::EncryptedEventContent, AnyMessageEventContent, EventType},
identifiers::{RoomId, UserId},
uuid::Uuid,
};
use tracing::{debug, info};
use crate::{
error::{EventError, MegolmResult, OlmResult},
olm::{Account, InboundGroupSession, OutboundGroupSession},
store::{Changes, Store},
Device, EncryptionSettings, OlmError, ToDeviceRequest,
};
#[derive(Debug, Clone)]
pub struct GroupSessionManager {
account: Account,
/// Store for the encryption keys.
/// Persists all the encryption keys so a client can resume the session
/// without the need to create new keys.
store: Store,
/// The currently active outbound group sessions.
outbound_group_sessions: Arc<DashMap<RoomId, OutboundGroupSession>>,
outbound_sessions_being_shared: Arc<DashMap<Uuid, OutboundGroupSession>>,
}
impl GroupSessionManager {
const MAX_TO_DEVICE_MESSAGES: usize = 20;
pub(crate) fn new(account: Account, store: Store) -> Self {
Self {
account,
store,
outbound_group_sessions: Arc::new(DashMap::new()),
outbound_sessions_being_shared: Arc::new(DashMap::new()),
}
}
pub fn invalidate_group_session(&self, room_id: &RoomId) -> bool {
self.outbound_group_sessions.remove(room_id).is_some()
}
pub fn mark_request_as_sent(&self, request_id: &Uuid) {
if let Some((_, s)) = self.outbound_sessions_being_shared.remove(request_id) {
s.mark_request_as_sent(request_id);
}
}
pub fn invalidate_sessions_new_devices(&self, users: &HashSet<&UserId>) {
for session in self.outbound_group_sessions.iter() {
if users.iter().any(|u| session.contains_recipient(u)) {
info!(
"Invalidating outobund session {} for room {}",
session.session_id(),
session.room_id()
);
session.invalidate_session();
if !session.shared() {
for request_id in session.clear_requests() {
self.outbound_sessions_being_shared.remove(&request_id);
}
}
}
}
}
/// Get an outbound group session for a room, if one exists.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which we should get the outbound
/// group session.
pub fn get_outbound_group_session(&self, room_id: &RoomId) -> Option<OutboundGroupSession> {
#[allow(clippy::map_clone)]
self.outbound_group_sessions.get(room_id).map(|s| s.clone())
}
pub async fn encrypt(
&self,
room_id: &RoomId,
content: AnyMessageEventContent,
) -> MegolmResult<EncryptedEventContent> {
let session = if let Some(s) = self.get_outbound_group_session(room_id) {
s
} else {
panic!("Session wasn't created nor shared");
};
if session.expired() {
panic!("Session expired");
}
Ok(session.encrypt(content).await)
}
/// Should the client share a group session for the given room.
///
/// Returns true if a session needs to be shared before room messages can be
/// encrypted, false if one is already shared and ready to encrypt room
/// messages.
///
/// This should be called every time a new room message wants to be sent out
/// since group sessions can expire at any time.
pub fn should_share_group_session(&self, room_id: &RoomId) -> bool {
let session = self.outbound_group_sessions.get(room_id);
match session {
Some(s) => !s.shared() || s.expired() || s.invalidated(),
None => true,
}
}
/// Create a new outbound group session.
///
/// This also creates a matching inbound group session and saves that one in
/// the store.
pub async fn create_outbound_group_session(
&self,
room_id: &RoomId,
settings: EncryptionSettings,
) -> OlmResult<(OutboundGroupSession, InboundGroupSession)> {
let (outbound, inbound) = self
.account
.create_group_session_pair(room_id, settings)
.await
.map_err(|_| EventError::UnsupportedAlgorithm)?;
let _ = self
.outbound_group_sessions
.insert(room_id.to_owned(), outbound.clone());
Ok((outbound, inbound))
}
/// Get to-device requests to share a group session with users in a room.
///
/// # Arguments
///
/// `room_id` - The room id of the room where the group session will be
/// used.
///
/// `users` - The list of users that should receive the group session.
pub async fn share_group_session(
&self,
room_id: &RoomId,
users: impl Iterator<Item = &UserId>,
encryption_settings: impl Into<EncryptionSettings>,
) -> OlmResult<Vec<Arc<ToDeviceRequest>>> {
let mut changes = Changes::default();
let (session, inbound_session) = self
.create_outbound_group_session(room_id, encryption_settings.into())
.await?;
changes.inbound_group_sessions.push(inbound_session);
let mut devices: Vec<Device> = Vec::new();
for user_id in users {
session.add_recipient(user_id);
let user_devices = self.store.get_user_devices(&user_id).await?;
devices.extend(user_devices.devices().filter(|d| !d.is_blacklisted()));
}
let mut requests = Vec::new();
let key_content = session.as_json().await;
for device_map_chunk in devices.chunks(Self::MAX_TO_DEVICE_MESSAGES) {
let mut messages = BTreeMap::new();
for device in device_map_chunk {
let encrypted = device
.encrypt(EventType::RoomKey, key_content.clone())
.await;
let (used_session, encrypted) = match encrypted {
Ok(c) => c,
Err(OlmError::MissingSession)
| Err(OlmError::EventError(EventError::MissingSenderKey)) => {
continue;
}
Err(e) => return Err(e),
};
changes.sessions.push(used_session);
messages
.entry(device.user_id().clone())
.or_insert_with(BTreeMap::new)
.insert(
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
serde_json::value::to_raw_value(&encrypted)?,
);
}
let id = Uuid::new_v4();
let request = Arc::new(ToDeviceRequest {
event_type: EventType::RoomEncrypted,
txn_id: id,
messages,
});
session.add_request(id, request.clone());
self.outbound_sessions_being_shared
.insert(id, session.clone());
requests.push(request);
}
if requests.is_empty() {
debug!(
"Session {} for room {} doesn't need to be shared with anyone, marking as shared",
session.session_id(),
session.room_id()
);
session.mark_as_shared();
}
self.store.save_changes(changes).await?;
Ok(requests)
}
}
@@ -0,0 +1,19 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod group_sessions;
mod sessions;
pub(crate) use group_sessions::GroupSessionManager;
pub(crate) use sessions::SessionManager;
@@ -0,0 +1,497 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use dashmap::{DashMap, DashSet};
use matrix_sdk_common::{
api::r0::{
keys::claim_keys::{Request as KeysClaimRequest, Response as KeysClaimResponse},
to_device::DeviceIdOrAllDevices,
},
assign,
events::EventType,
identifiers::{DeviceId, DeviceIdBox, DeviceKeyAlgorithm, UserId},
uuid::Uuid,
};
use serde_json::{json, value::to_raw_value};
use tracing::{error, info, warn};
use crate::{
error::OlmResult,
key_request::KeyRequestMachine,
olm::Account,
requests::{OutgoingRequest, ToDeviceRequest},
store::{Changes, Result as StoreResult, Store},
ReadOnlyDevice,
};
#[derive(Debug, Clone)]
pub(crate) struct SessionManager {
account: Account,
store: Store,
/// A map of user/devices that we need to automatically claim keys for.
/// Submodules can insert user/device pairs into this map and the
/// user/device paris will be added to the list of users when
/// [`get_missing_sessions`](#method.get_missing_sessions) is called.
users_for_key_claim: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
wedged_devices: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
key_request_machine: KeyRequestMachine,
outgoing_to_device_requests: Arc<DashMap<Uuid, OutgoingRequest>>,
}
impl SessionManager {
const KEY_CLAIM_TIMEOUT: Duration = Duration::from_secs(10);
const UNWEDGING_INTERVAL: Duration = Duration::from_secs(60 * 60);
pub fn new(
account: Account,
users_for_key_claim: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
key_request_machine: KeyRequestMachine,
store: Store,
) -> Self {
Self {
account,
store,
key_request_machine,
users_for_key_claim,
wedged_devices: Arc::new(DashMap::new()),
outgoing_to_device_requests: Arc::new(DashMap::new()),
}
}
/// Mark the outgoing request as sent.
pub fn mark_outgoing_request_as_sent(&self, id: &Uuid) {
self.outgoing_to_device_requests.remove(id);
}
pub async fn mark_device_as_wedged(&self, sender: &UserId, curve_key: &str) -> StoreResult<()> {
if let Some(device) = self
.store
.get_device_from_curve_key(sender, curve_key)
.await?
{
let sessions = device.get_sessions().await?;
if let Some(sessions) = sessions {
let mut sessions = sessions.lock().await;
sessions.sort_by_key(|s| s.creation_time.clone());
let session = sessions.get(0);
if let Some(session) = session {
if session.creation_time.elapsed() > Self::UNWEDGING_INTERVAL {
self.users_for_key_claim
.entry(device.user_id().clone())
.or_insert_with(DashSet::new)
.insert(device.device_id().into());
self.wedged_devices
.entry(device.user_id().to_owned())
.or_insert_with(DashSet::new)
.insert(device.device_id().into());
}
}
}
}
Ok(())
}
#[allow(dead_code)]
pub fn is_device_wedged(&self, device: &ReadOnlyDevice) -> bool {
self.wedged_devices
.get(device.user_id())
.map(|d| d.contains(device.device_id()))
.unwrap_or(false)
}
/// Check if the session was created to unwedge a Device.
///
/// If the device was wedged this will queue up a dummy to-device message.
async fn check_if_unwedged(&self, user_id: &UserId, device_id: &DeviceId) -> OlmResult<()> {
if self
.wedged_devices
.get(user_id)
.map(|d| d.remove(device_id))
.flatten()
.is_some()
{
if let Some(device) = self.store.get_device(user_id, device_id).await? {
let (_, content) = device.encrypt(EventType::Dummy, json!({})).await?;
let id = Uuid::new_v4();
let mut messages = BTreeMap::new();
messages
.entry(device.user_id().to_owned())
.or_insert_with(BTreeMap::new)
.insert(
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
to_raw_value(&content)?,
);
let request = OutgoingRequest {
request_id: id,
request: Arc::new(
ToDeviceRequest {
event_type: EventType::RoomEncrypted,
txn_id: id,
messages,
}
.into(),
),
};
self.outgoing_to_device_requests.insert(id, request);
}
}
Ok(())
}
/// Get the a key claiming request for the user/device pairs that we are
/// missing Olm sessions for.
///
/// Returns None if no key claiming request needs to be sent out.
///
/// Sessions need to be established between devices so group sessions for a
/// room can be shared with them.
///
/// This should be called every time a group session needs to be shared as
/// well as between sync calls. After a sync some devices may request room
/// keys without us having a valid Olm session with them, making it
/// impossible to server the room key request, thus it's necessary to check
/// for missing sessions between sync as well.
///
/// **Note**: Care should be taken that only one such request at a time is
/// in flight, e.g. using a lock.
///
/// The response of a successful key claiming requests needs to be passed to
/// the `OlmMachine` with the [`receive_keys_claim_response`].
///
/// # Arguments
///
/// `users` - The list of users that we should check if we lack a session
/// with one of their devices. This can be an empty iterator when calling
/// this method between sync requests.
///
/// [`receive_keys_claim_response`]: #method.receive_keys_claim_response
pub async fn get_missing_sessions(
&self,
users: &mut impl Iterator<Item = &UserId>,
) -> OlmResult<Option<(Uuid, KeysClaimRequest)>> {
let mut missing = BTreeMap::new();
// Add the list of devices that the user wishes to establish sessions
// right now.
for user_id in users {
let user_devices = self.store.get_user_devices(user_id).await?;
for device in user_devices.devices() {
let sender_key = if let Some(k) = device.get_key(DeviceKeyAlgorithm::Curve25519) {
k
} else {
continue;
};
let sessions = self.store.get_sessions(sender_key).await?;
let is_missing = if let Some(sessions) = sessions {
sessions.lock().await.is_empty()
} else {
true
};
if is_missing {
missing
.entry(user_id.to_owned())
.or_insert_with(BTreeMap::new)
.insert(
device.device_id().into(),
DeviceKeyAlgorithm::SignedCurve25519,
);
}
}
}
// Add the list of sessions that for some reason automatically need to
// create an Olm session.
for item in self.users_for_key_claim.iter() {
let user = item.key();
for device_id in item.value().iter() {
missing
.entry(user.to_owned())
.or_insert_with(BTreeMap::new)
.insert(device_id.to_owned(), DeviceKeyAlgorithm::SignedCurve25519);
}
}
if missing.is_empty() {
Ok(None)
} else {
Ok(Some((
Uuid::new_v4(),
assign!(KeysClaimRequest::new(missing), {
timeout: Some(Self::KEY_CLAIM_TIMEOUT),
}),
)))
}
}
/// Receive a successful key claim response and create new Olm sessions with
/// the claimed keys.
///
/// # Arguments
///
/// * `response` - The response containing the claimed one-time keys.
pub async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> {
// TODO log the failures here
let mut changes = Changes::default();
for (user_id, user_devices) in &response.one_time_keys {
for (device_id, key_map) in user_devices {
let device = match self.store.get_readonly_device(&user_id, device_id).await {
Ok(Some(d)) => d,
Ok(None) => {
warn!(
"Tried to create an Olm session for {} {}, but the device is unknown",
user_id, device_id
);
continue;
}
Err(e) => {
warn!(
"Tried to create an Olm session for {} {}, but \
can't fetch the device from the store {:?}",
user_id, device_id, e
);
continue;
}
};
info!("Creating outbound Session for {} {}", user_id, device_id);
let session = match self.account.create_outbound_session(device, &key_map).await {
Ok(s) => s,
Err(e) => {
warn!("Error creating new outbound session {:?}", e);
continue;
}
};
changes.sessions.push(session);
self.key_request_machine.retry_keyshare(&user_id, device_id);
if let Err(e) = self.check_if_unwedged(&user_id, device_id).await {
error!(
"Error while treating an unwedged device {} {} {:?}",
user_id, device_id, e
);
}
}
}
Ok(self.store.save_changes(changes).await?)
}
}
#[cfg(test)]
mod test {
use dashmap::DashMap;
use matrix_sdk_common::locks::Mutex;
use std::{collections::BTreeMap, sync::Arc};
use matrix_sdk_common::{
api::r0::keys::claim_keys::Response as KeyClaimResponse,
identifiers::{user_id, DeviceIdBox, UserId},
};
use matrix_sdk_test::async_test;
use super::SessionManager;
use crate::{
identities::ReadOnlyDevice,
key_request::KeyRequestMachine,
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
store::{CryptoStore, MemoryStore, Store},
verification::VerificationMachine,
};
fn user_id() -> UserId {
user_id!("@example:localhost")
}
fn device_id() -> DeviceIdBox {
"DEVICEID".into()
}
fn bob_account() -> ReadOnlyAccount {
ReadOnlyAccount::new(&user_id!("@bob:localhost"), "BOBDEVICE".into())
}
async fn session_manager() -> SessionManager {
let user_id = user_id();
let device_id = device_id();
let outbound_sessions = Arc::new(DashMap::new());
let users_for_key_claim = Arc::new(DashMap::new());
let account = ReadOnlyAccount::new(&user_id, &device_id);
let store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
store.save_account(account.clone()).await.unwrap();
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(
user_id.clone(),
)));
let verification =
VerificationMachine::new(account.clone(), identity.clone(), store.clone());
let user_id = Arc::new(user_id);
let device_id = Arc::new(device_id);
let store = Store::new(user_id.clone(), identity, store, verification);
let account = Account {
inner: account,
store: store.clone(),
};
let key_request = KeyRequestMachine::new(
user_id,
device_id,
store.clone(),
outbound_sessions,
users_for_key_claim.clone(),
);
SessionManager::new(account, users_for_key_claim, key_request, store)
}
#[async_test]
async fn session_creation() {
let manager = session_manager().await;
let bob = bob_account();
let bob_device = ReadOnlyDevice::from_account(&bob).await;
manager.store.save_devices(&[bob_device]).await.unwrap();
let (_, request) = manager
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
.await
.unwrap()
.unwrap();
assert!(request.one_time_keys.contains_key(bob.user_id()));
bob.generate_one_time_keys_helper(1).await;
let one_time = bob.signed_one_time_keys_helper().await.unwrap();
bob.mark_keys_as_published().await;
let mut one_time_keys = BTreeMap::new();
one_time_keys
.entry(bob.user_id().clone())
.or_insert_with(BTreeMap::new)
.insert(bob.device_id().into(), one_time);
let response = KeyClaimResponse::new(one_time_keys);
manager
.receive_keys_claim_response(&response)
.await
.unwrap();
assert!(manager
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
.await
.unwrap()
.is_none());
}
// This test doesn't run on macos because we're modifying the session
// creation time so we can get around the UNWEDGING_INTERVAL.
#[async_test]
#[cfg(target_os = "linux")]
async fn session_unwedging() {
use matrix_sdk_common::{
identifiers::DeviceKeyAlgorithm,
instant::{Duration, Instant},
};
let manager = session_manager().await;
let bob = bob_account();
let (_, mut session) = bob.create_session_for(&manager.account).await;
let bob_device = ReadOnlyDevice::from_account(&bob).await;
session.creation_time = Arc::new(Instant::now() - Duration::from_secs(3601));
manager
.store
.save_devices(&[bob_device.clone()])
.await
.unwrap();
manager.store.save_sessions(&[session]).await.unwrap();
assert!(manager
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
.await
.unwrap()
.is_none());
let curve_key = bob_device.get_key(DeviceKeyAlgorithm::Curve25519).unwrap();
assert!(!manager.users_for_key_claim.contains_key(bob.user_id()));
assert!(!manager.is_device_wedged(&bob_device));
manager
.mark_device_as_wedged(bob_device.user_id(), &curve_key)
.await
.unwrap();
assert!(manager.is_device_wedged(&bob_device));
assert!(manager.users_for_key_claim.contains_key(bob.user_id()));
let (_, request) = manager
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
.await
.unwrap()
.unwrap();
assert!(request.one_time_keys.contains_key(bob.user_id()));
bob.generate_one_time_keys_helper(1).await;
let one_time = bob.signed_one_time_keys_helper().await.unwrap();
bob.mark_keys_as_published().await;
let mut one_time_keys = BTreeMap::new();
one_time_keys
.entry(bob.user_id().clone())
.or_insert_with(BTreeMap::new)
.insert(bob.device_id().into(), one_time);
let response = KeyClaimResponse::new(one_time_keys);
assert!(manager.outgoing_to_device_requests.is_empty());
manager
.receive_keys_claim_response(&response)
.await
.unwrap();
assert!(!manager.is_device_wedged(&bob_device));
assert!(manager
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
.await
.unwrap()
.is_none());
assert!(!manager.outgoing_to_device_requests.is_empty())
}
}
@@ -12,27 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use std::sync::Arc;
//! Collection of small in-memory stores that can be used to cache Olm objects.
//!
//! Note: You'll only be interested in these if you are implementing a custom
//! `CryptoStore`.
use dashmap::{DashMap, ReadOnlyView};
use matrix_sdk_common::locks::Mutex;
use std::{collections::HashMap, sync::Arc};
use super::device::Device;
use super::olm::{InboundGroupSession, Session};
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
use dashmap::DashMap;
use matrix_sdk_common::{
identifiers::{DeviceId, DeviceIdBox, RoomId, UserId},
locks::Mutex,
};
use crate::{
identities::ReadOnlyDevice,
olm::{InboundGroupSession, Session},
};
/// In-memory store for Olm Sessions.
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub struct SessionStore {
entries: HashMap<String, Arc<Mutex<Vec<Session>>>>,
entries: Arc<DashMap<String, Arc<Mutex<Vec<Session>>>>>,
}
impl SessionStore {
/// Create a new empty Session store.
pub fn new() -> Self {
SessionStore {
entries: HashMap::new(),
entries: Arc::new(DashMap::new()),
}
}
@@ -40,17 +48,16 @@ impl SessionStore {
///
/// Returns true if the the session was added, false if the session was
/// already in the store.
pub async fn add(&mut self, session: Session) -> bool {
if !self.entries.contains_key(&*session.sender_key) {
self.entries.insert(
session.sender_key.to_string(),
Arc::new(Mutex::new(Vec::new())),
);
}
let sessions = self.entries.get_mut(&*session.sender_key).unwrap();
pub async fn add(&self, session: Session) -> bool {
let sessions_lock = self
.entries
.entry(session.sender_key.to_string())
.or_insert_with(|| Arc::new(Mutex::new(Vec::new())));
if !sessions.lock().await.contains(&session) {
sessions.lock().await.push(session);
let mut sessions = sessions_lock.lock().await;
if !sessions.contains(&session) {
sessions.push(session);
true
} else {
false
@@ -59,51 +66,57 @@ impl SessionStore {
/// Get all the sessions that belong to the given sender key.
pub fn get(&self, sender_key: &str) -> Option<Arc<Mutex<Vec<Session>>>> {
self.entries.get(sender_key).cloned()
#[allow(clippy::map_clone)]
self.entries.get(sender_key).map(|s| s.clone())
}
/// Add a list of sessions belonging to the sender key.
pub fn set_for_sender(&mut self, sender_key: &str, sessions: Vec<Session>) {
pub fn set_for_sender(&self, sender_key: &str, sessions: Vec<Session>) {
self.entries
.insert(sender_key.to_owned(), Arc::new(Mutex::new(sessions)));
}
}
#[derive(Debug, Default)]
/// In-memory store that houlds inbound group sessions.
#[derive(Debug, Default, Clone)]
/// In-memory store that holds inbound group sessions.
pub struct GroupSessionStore {
entries: HashMap<RoomId, HashMap<String, HashMap<String, InboundGroupSession>>>,
#[allow(clippy::type_complexity)]
entries: Arc<DashMap<RoomId, HashMap<String, HashMap<String, InboundGroupSession>>>>,
}
impl GroupSessionStore {
/// Create a new empty store.
pub fn new() -> Self {
GroupSessionStore {
entries: HashMap::new(),
entries: Arc::new(DashMap::new()),
}
}
/// Add a inbound group session to the store.
/// Add an inbound group session to the store.
///
/// Returns true if the the session was added, false if the session was
/// already in the store.
pub fn add(&mut self, session: InboundGroupSession) -> bool {
if !self.entries.contains_key(&session.room_id) {
let room_id = &*session.room_id;
self.entries.insert(room_id.clone(), HashMap::new());
}
pub fn add(&self, session: InboundGroupSession) -> bool {
self.entries
.entry((&*session.room_id).clone())
.or_insert_with(HashMap::new)
.entry(session.sender_key.to_string())
.or_insert_with(HashMap::new)
.insert(session.session_id().to_owned(), session)
.is_none()
}
let room_map = self.entries.get_mut(&session.room_id).unwrap();
if !room_map.contains_key(&*session.sender_key) {
let sender_key = &*session.sender_key;
room_map.insert(sender_key.to_owned(), HashMap::new());
}
let sender_map = room_map.get_mut(&*session.sender_key).unwrap();
let ret = sender_map.insert(session.session_id().to_owned(), session);
ret.is_none()
/// Get all the group sessions the store knows about.
pub fn get_all(&self) -> Vec<InboundGroupSession> {
self.entries
.iter()
.flat_map(|d| {
d.value()
.values()
.flat_map(|t| t.values().cloned().collect::<Vec<InboundGroupSession>>())
.collect::<Vec<InboundGroupSession>>()
})
.collect()
}
/// Get a inbound group session from our store.
@@ -129,30 +142,7 @@ impl GroupSessionStore {
/// In-memory store holding the devices of users.
#[derive(Clone, Debug, Default)]
pub struct DeviceStore {
entries: Arc<DashMap<UserId, DashMap<String, Device>>>,
}
/// A read only view over all devices belonging to a user.
#[derive(Debug)]
pub struct UserDevices {
entries: ReadOnlyView<DeviceId, Device>,
}
impl UserDevices {
/// Get the specific device with the given device id.
pub fn get(&self, device_id: &str) -> Option<Device> {
self.entries.get(device_id).cloned()
}
/// Iterator over all the device ids of the user devices.
pub fn keys(&self) -> impl Iterator<Item = &DeviceId> {
self.entries.keys()
}
/// Iterator over all the devices of the user devices.
pub fn devices(&self) -> impl Iterator<Item = &Device> {
self.entries.values()
}
entries: Arc<DashMap<UserId, DashMap<Box<DeviceId>, ReadOnlyDevice>>>,
}
impl DeviceStore {
@@ -166,21 +156,17 @@ impl DeviceStore {
/// Add a device to the store.
///
/// Returns true if the device was already in the store, false otherwise.
pub fn add(&self, device: Device) -> bool {
pub fn add(&self, device: ReadOnlyDevice) -> bool {
let user_id = device.user_id();
if !self.entries.contains_key(&user_id) {
self.entries.insert(user_id.clone(), DashMap::new());
}
let device_map = self.entries.get_mut(&user_id).unwrap();
device_map
.insert(device.device_id().to_owned(), device)
self.entries
.entry(user_id.to_owned())
.or_insert_with(DashMap::new)
.insert(device.device_id().into(), device)
.is_none()
}
/// Get the device with the given device_id and belonging to the given user.
pub fn get(&self, user_id: &UserId, device_id: &str) -> Option<Device> {
pub fn get(&self, user_id: &UserId, device_id: &DeviceId) -> Option<ReadOnlyDevice> {
self.entries
.get(user_id)
.and_then(|m| m.get(device_id).map(|d| d.value().clone()))
@@ -189,7 +175,7 @@ impl DeviceStore {
/// Remove the device with the given device_id and belonging to the given user.
///
/// Returns the device if it was removed, None if it wasn't in the store.
pub fn remove(&self, user_id: &UserId, device_id: &str) -> Option<Device> {
pub fn remove(&self, user_id: &UserId, device_id: &DeviceId) -> Option<ReadOnlyDevice> {
self.entries
.get(user_id)
.and_then(|m| m.remove(device_id))
@@ -197,31 +183,30 @@ impl DeviceStore {
}
/// Get a read-only view over all devices of the given user.
pub fn user_devices(&self, user_id: &UserId) -> UserDevices {
if !self.entries.contains_key(user_id) {
self.entries.insert(user_id.clone(), DashMap::new());
}
UserDevices {
entries: self.entries.get(user_id).unwrap().clone().into_read_only(),
}
pub fn user_devices(&self, user_id: &UserId) -> HashMap<DeviceIdBox, ReadOnlyDevice> {
self.entries
.entry(user_id.clone())
.or_insert_with(DashMap::new)
.iter()
.map(|i| (i.key().to_owned(), i.value().clone()))
.collect()
}
}
#[cfg(test)]
mod test {
use std::convert::TryFrom;
use crate::device::test::get_device;
use crate::memory_stores::{DeviceStore, GroupSessionStore, SessionStore};
use crate::olm::test::get_account_and_session;
use crate::olm::{InboundGroupSession, OutboundGroupSession};
use matrix_sdk_common::identifiers::RoomId;
use crate::{
identities::device::test::get_device,
olm::{test::get_account_and_session, InboundGroupSession},
store::caches::{DeviceStore, GroupSessionStore, SessionStore},
};
use matrix_sdk_common::identifiers::room_id;
#[tokio::test]
async fn test_session_store() {
let (_, session) = get_account_and_session().await;
let mut store = SessionStore::new();
let store = SessionStore::new();
assert!(store.add(session.clone()).await);
assert!(!store.add(session.clone()).await);
@@ -238,7 +223,7 @@ mod test {
async fn test_session_store_bulk_storing() {
let (_, session) = get_account_and_session().await;
let mut store = SessionStore::new();
let store = SessionStore::new();
store.set_for_sender(&session.sender_key, vec![session.clone()]);
let sessions = store.get(&session.sender_key).unwrap();
@@ -251,9 +236,13 @@ mod test {
#[tokio::test]
async fn test_group_session_store() {
let room_id = RoomId::try_from("!test:localhost").unwrap();
let (account, _) = get_account_and_session().await;
let room_id = room_id!("!test:localhost");
let outbound = OutboundGroupSession::new(&room_id);
let (outbound, _) = account
.create_group_session_pair_with_defaults(&room_id)
.await
.unwrap();
assert_eq!(0, outbound.message_index().await);
assert!(!outbound.shared());
@@ -268,7 +257,7 @@ mod test {
)
.unwrap();
let mut store = GroupSessionStore::new();
let store = GroupSessionStore::new();
store.add(inbound.clone());
let loaded_session = store
@@ -291,12 +280,12 @@ mod test {
let user_devices = store.user_devices(device.user_id());
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
assert_eq!(&**user_devices.keys().next().unwrap(), device.device_id());
assert_eq!(user_devices.values().next().unwrap(), &device);
let loaded_device = user_devices.get(device.device_id()).unwrap();
assert_eq!(device, loaded_device);
assert_eq!(&device, loaded_device);
store.remove(device.user_id(), device.device_id());
+196 -68
View File
@@ -12,66 +12,133 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashSet;
use std::sync::Arc;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use async_trait::async_trait;
use matrix_sdk_common::locks::Mutex;
use dashmap::{DashMap, DashSet};
use matrix_sdk_common::{
async_trait,
identifiers::{DeviceId, DeviceIdBox, RoomId, UserId},
locks::Mutex,
};
use super::{Account, CryptoStore, InboundGroupSession, Result, Session};
use crate::device::Device;
use crate::memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices};
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
use super::{
caches::{DeviceStore, GroupSessionStore, SessionStore},
Changes, CryptoStore, InboundGroupSession, ReadOnlyAccount, Result, Session,
};
use crate::{
identities::{ReadOnlyDevice, UserIdentities},
olm::PrivateCrossSigningIdentity,
};
#[derive(Debug)]
/// An in-memory only store that will forget all the E2EE key once it's dropped.
#[derive(Debug, Clone)]
pub struct MemoryStore {
sessions: SessionStore,
inbound_group_sessions: GroupSessionStore,
tracked_users: HashSet<UserId>,
users_for_key_query: HashSet<UserId>,
tracked_users: Arc<DashSet<UserId>>,
users_for_key_query: Arc<DashSet<UserId>>,
olm_hashes: Arc<DashMap<String, DashSet<String>>>,
devices: DeviceStore,
identities: Arc<DashMap<UserId, UserIdentities>>,
values: Arc<DashMap<String, String>>,
}
impl MemoryStore {
pub fn new() -> Self {
impl Default for MemoryStore {
fn default() -> Self {
MemoryStore {
sessions: SessionStore::new(),
inbound_group_sessions: GroupSessionStore::new(),
tracked_users: HashSet::new(),
users_for_key_query: HashSet::new(),
tracked_users: Arc::new(DashSet::new()),
users_for_key_query: Arc::new(DashSet::new()),
olm_hashes: Arc::new(DashMap::new()),
devices: DeviceStore::new(),
identities: Arc::new(DashMap::new()),
values: Arc::new(DashMap::new()),
}
}
}
#[async_trait]
impl MemoryStore {
/// Create a new empty `MemoryStore`.
pub fn new() -> Self {
Self::default()
}
pub(crate) async fn save_devices(&self, mut devices: Vec<ReadOnlyDevice>) {
for device in devices.drain(..) {
let _ = self.devices.add(device);
}
}
async fn delete_devices(&self, mut devices: Vec<ReadOnlyDevice>) {
for device in devices.drain(..) {
let _ = self.devices.remove(device.user_id(), device.device_id());
}
}
async fn save_sessions(&self, mut sessions: Vec<Session>) {
for session in sessions.drain(..) {
let _ = self.sessions.add(session.clone()).await;
}
}
async fn save_inbound_group_sessions(&self, mut sessions: Vec<InboundGroupSession>) {
for session in sessions.drain(..) {
self.inbound_group_sessions.add(session);
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl CryptoStore for MemoryStore {
async fn load_account(&mut self) -> Result<Option<Account>> {
async fn load_account(&self) -> Result<Option<ReadOnlyAccount>> {
Ok(None)
}
async fn save_account(&mut self, _: Account) -> Result<()> {
async fn save_account(&self, _: ReadOnlyAccount) -> Result<()> {
Ok(())
}
async fn save_sessions(&mut self, sessions: &[Session]) -> Result<()> {
for session in sessions {
let _ = self.sessions.add(session.clone()).await;
async fn save_changes(&self, mut changes: Changes) -> Result<()> {
self.save_sessions(changes.sessions).await;
self.save_inbound_group_sessions(changes.inbound_group_sessions)
.await;
self.save_devices(changes.devices.new).await;
self.save_devices(changes.devices.changed).await;
self.delete_devices(changes.devices.deleted).await;
for identity in changes
.identities
.new
.drain(..)
.chain(changes.identities.changed)
{
let _ = self
.identities
.insert(identity.user_id().to_owned(), identity.clone());
}
for hash in changes.message_hashes {
self.olm_hashes
.entry(hash.sender_key.to_owned())
.or_insert_with(DashSet::new)
.insert(hash.hash.clone());
}
Ok(())
}
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
async fn get_sessions(&self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
Ok(self.sessions.get(sender_key))
}
async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<bool> {
Ok(self.inbound_group_sessions.add(session))
}
async fn get_inbound_group_session(
&mut self,
&self,
room_id: &RoomId,
sender_key: &str,
session_id: &str,
@@ -81,15 +148,36 @@ impl CryptoStore for MemoryStore {
.get(room_id, sender_key, session_id))
}
fn tracked_users(&self) -> &HashSet<UserId> {
&self.tracked_users
async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
Ok(self.inbound_group_sessions.get_all())
}
fn users_for_key_query(&self) -> &HashSet<UserId> {
&self.users_for_key_query
fn users_for_key_query(&self) -> HashSet<UserId> {
#[allow(clippy::map_clone)]
self.users_for_key_query.iter().map(|u| u.clone()).collect()
}
async fn update_tracked_user(&mut self, user: &UserId, dirty: bool) -> Result<bool> {
fn is_user_tracked(&self, user_id: &UserId) -> bool {
self.tracked_users.contains(user_id)
}
fn has_users_for_key_query(&self) -> bool {
!self.users_for_key_query.is_empty()
}
async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result<bool> {
// TODO to prevent a race between the sync and a key query in flight we
// need to have an additional state to mention that the user changed.
//
// A simple counter could be used for this or enum with two states, e.g.
// The counter would work as follows:
// * 0 -> User is synced, no need for a key query.
// * 1 -> A sync has marked the user as dirty.
// * 2 -> A sync has marked the user again as dirty, before we got a
// successful key query response.
//
// The counter would top out at 2 since there won't be a race between 3
// different key queries syncs.
if dirty {
self.users_for_key_query.insert(user.clone());
} else {
@@ -99,49 +187,71 @@ impl CryptoStore for MemoryStore {
Ok(self.tracked_users.insert(user.clone()))
}
#[allow(clippy::ptr_arg)]
async fn get_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<Option<Device>> {
async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<ReadOnlyDevice>> {
Ok(self.devices.get(user_id, device_id))
}
async fn delete_device(&self, device: Device) -> Result<()> {
let _ = self.devices.remove(device.user_id(), device.device_id());
Ok(())
}
async fn get_user_devices(&self, user_id: &UserId) -> Result<UserDevices> {
async fn get_user_devices(
&self,
user_id: &UserId,
) -> Result<HashMap<DeviceIdBox, ReadOnlyDevice>> {
Ok(self.devices.user_devices(user_id))
}
async fn save_devices(&self, devices: &[Device]) -> Result<()> {
for device in devices {
let _ = self.devices.add(device.clone());
}
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentities>> {
#[allow(clippy::map_clone)]
Ok(self.identities.get(user_id).map(|i| i.clone()))
}
async fn save_value(&self, key: String, value: String) -> Result<()> {
self.values.insert(key, value);
Ok(())
}
async fn remove_value(&self, key: &str) -> Result<()> {
self.values.remove(key);
Ok(())
}
async fn get_value(&self, key: &str) -> Result<Option<String>> {
Ok(self.values.get(key).map(|v| v.to_owned()))
}
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
Ok(None)
}
async fn is_message_known(&self, message_hash: &crate::olm::OlmMessageHash) -> Result<bool> {
Ok(self
.olm_hashes
.entry(message_hash.sender_key.to_owned())
.or_insert_with(DashSet::new)
.contains(&message_hash.hash))
}
}
#[cfg(test)]
mod test {
use std::convert::TryFrom;
use crate::device::test::get_device;
use crate::olm::test::get_account_and_session;
use crate::olm::{InboundGroupSession, OutboundGroupSession};
use crate::store::memorystore::MemoryStore;
use crate::store::CryptoStore;
use matrix_sdk_common::identifiers::RoomId;
use crate::{
identities::device::test::get_device,
olm::{test::get_account_and_session, InboundGroupSession, OlmMessageHash},
store::{memorystore::MemoryStore, Changes, CryptoStore},
};
use matrix_sdk_common::identifiers::room_id;
#[tokio::test]
async fn test_session_store() {
let (account, session) = get_account_and_session().await;
let mut store = MemoryStore::new();
let store = MemoryStore::new();
assert!(store.load_account().await.unwrap().is_none());
store.save_account(account).await.unwrap();
store.save_sessions(&[session.clone()]).await.unwrap();
store.save_sessions(vec![session.clone()]).await;
let sessions = store
.get_sessions(&session.sender_key)
@@ -157,9 +267,13 @@ mod test {
#[tokio::test]
async fn test_group_session_store() {
let room_id = RoomId::try_from("!test:localhost").unwrap();
let (account, _) = get_account_and_session().await;
let room_id = room_id!("!test:localhost");
let outbound = OutboundGroupSession::new(&room_id);
let (outbound, _) = account
.create_group_session_pair_with_defaults(&room_id)
.await
.unwrap();
let inbound = InboundGroupSession::new(
"test_key",
"test_key",
@@ -168,11 +282,10 @@ mod test {
)
.unwrap();
let mut store = MemoryStore::new();
let store = MemoryStore::new();
let _ = store
.save_inbound_group_session(inbound.clone())
.await
.unwrap();
.save_inbound_group_sessions(vec![inbound.clone()])
.await;
let loaded_session = store
.get_inbound_group_session(&room_id, "test_key", outbound.session_id())
@@ -187,7 +300,7 @@ mod test {
let device = get_device();
let store = MemoryStore::new();
store.save_devices(&[device.clone()]).await.unwrap();
store.save_devices(vec![device.clone()]).await;
let loaded_device = store
.get_device(device.user_id(), device.device_id())
@@ -199,14 +312,14 @@ mod test {
let user_devices = store.get_user_devices(device.user_id()).await.unwrap();
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
assert_eq!(&**user_devices.keys().next().unwrap(), device.device_id());
assert_eq!(user_devices.values().next().unwrap(), &device);
let loaded_device = user_devices.get(device.device_id()).unwrap();
assert_eq!(device, loaded_device);
assert_eq!(&device, loaded_device);
store.delete_device(device.clone()).await.unwrap();
store.delete_devices(vec![device.clone()]).await;
assert!(store
.get_device(device.user_id(), device.device_id())
.await
@@ -217,7 +330,7 @@ mod test {
#[tokio::test]
async fn test_tracked_users() {
let device = get_device();
let mut store = MemoryStore::new();
let store = MemoryStore::new();
assert!(store
.update_tracked_user(device.user_id(), false)
@@ -228,8 +341,23 @@ mod test {
.await
.unwrap());
let tracked_users = store.tracked_users();
assert!(store.is_user_tracked(device.user_id()));
}
let _ = tracked_users.contains(device.user_id());
#[tokio::test]
async fn test_message_hash() {
let store = MemoryStore::new();
let hash = OlmMessageHash {
sender_key: "test_sender".to_owned(),
hash: "test_hash".to_owned(),
};
let mut changes = Changes::default();
changes.message_hashes.push(hash.clone());
assert!(!store.is_message_known(&hash).await.unwrap());
store.save_changes(changes).await.unwrap();
assert!(store.is_message_known(&hash).await.unwrap());
}
}
+325 -68
View File
@@ -12,30 +12,275 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use core::fmt::Debug;
use std::collections::HashSet;
use std::io::Error as IoError;
use std::sync::Arc;
use url::ParseError;
//! Types and traits to implement the storage layer for the [`OlmMachine`]
//!
//! The storage layer for the [`OlmMachine`] can be customized using a trait.
//! Implementing your own [`CryptoStore`]
//!
//! An in-memory only store is provided as well as a SQLite based one, depending
//! on your needs and targets a custom store may be implemented, e.g. for
//! `wasm-unknown-unknown` an indexeddb store would be needed
//!
//! ```
//! # use matrix_sdk_crypto::{
//! # OlmMachine,
//! # store::MemoryStore,
//! # };
//! # use matrix_sdk_common::identifiers::{user_id, DeviceIdBox};
//! # let user_id = user_id!("@example:localhost");
//! # let device_id: DeviceIdBox = "TEST".into();
//! let store = Box::new(MemoryStore::new());
//!
//! let machine = OlmMachine::new_with_store(user_id, device_id, store);
//! ```
//!
//! [`OlmMachine`]: /matrix_sdk_crypto/struct.OlmMachine.html
//! [`CryptoStore`]: trait.Cryptostore.html
use async_trait::async_trait;
use matrix_sdk_common::locks::Mutex;
pub mod caches;
mod memorystore;
mod pickle_key;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "sqlite_cryptostore")]
pub(crate) mod sqlite;
pub use memorystore::MemoryStore;
pub use pickle_key::{EncryptedPickleKey, PickleKey};
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "sqlite_cryptostore")]
pub use sqlite::SqliteStore;
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
io::Error as IoError,
ops::Deref,
sync::Arc,
};
use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError};
use serde::{Deserialize, Serialize};
use serde_json::Error as SerdeError;
use thiserror::Error;
use super::device::Device;
use super::memory_stores::UserDevices;
use super::olm::{Account, InboundGroupSession, Session};
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError};
pub mod memorystore;
#[cfg(feature = "sqlite-cryptostore")]
pub mod sqlite;
#[cfg(feature = "sqlite-cryptostore")]
#[cfg_attr(feature = "docs", doc(cfg(r#sqlite_cryptostore)))]
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "sqlite_cryptostore")]
use sqlx::Error as SqlxError;
use matrix_sdk_common::{
async_trait,
identifiers::{
DeviceId, DeviceIdBox, DeviceKeyAlgorithm, Error as IdentifierValidationError, RoomId,
UserId,
},
locks::Mutex,
AsyncTraitDeps,
};
use crate::{
error::SessionUnpicklingError,
identities::{Device, ReadOnlyDevice, UserDevices, UserIdentities},
olm::{
InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity, ReadOnlyAccount, Session,
},
verification::VerificationMachine,
};
/// A `CryptoStore` specific result type.
pub type Result<T> = std::result::Result<T, CryptoStoreError>;
/// A wrapper for our CryptoStore trait object.
///
/// This is needed because we want to have a generic interface so we can
/// store/restore objects that we can serialize. Since trait objects and
/// generics don't mix let the CryptoStore store strings and this wrapper
/// adds the generic interface on top.
#[derive(Debug, Clone)]
pub(crate) struct Store {
user_id: Arc<UserId>,
identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
inner: Arc<Box<dyn CryptoStore>>,
verification_machine: VerificationMachine,
}
#[derive(Debug, Default)]
#[allow(missing_docs)]
pub struct Changes {
pub account: Option<ReadOnlyAccount>,
pub private_identity: Option<PrivateCrossSigningIdentity>,
pub sessions: Vec<Session>,
pub message_hashes: Vec<OlmMessageHash>,
pub inbound_group_sessions: Vec<InboundGroupSession>,
pub identities: IdentityChanges,
pub devices: DeviceChanges,
}
#[derive(Debug, Clone, Default)]
#[allow(missing_docs)]
pub struct IdentityChanges {
pub new: Vec<UserIdentities>,
pub changed: Vec<UserIdentities>,
}
#[derive(Debug, Clone, Default)]
#[allow(missing_docs)]
pub struct DeviceChanges {
pub new: Vec<ReadOnlyDevice>,
pub changed: Vec<ReadOnlyDevice>,
pub deleted: Vec<ReadOnlyDevice>,
}
impl Store {
pub fn new(
user_id: Arc<UserId>,
identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
store: Arc<Box<dyn CryptoStore>>,
verification_machine: VerificationMachine,
) -> Self {
Self {
user_id,
identity,
inner: store,
verification_machine,
}
}
pub async fn get_readonly_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<ReadOnlyDevice>> {
self.inner.get_device(user_id, device_id).await
}
pub async fn save_sessions(&self, sessions: &[Session]) -> Result<()> {
let changes = Changes {
sessions: sessions.to_vec(),
..Default::default()
};
self.save_changes(changes).await
}
#[cfg(test)]
pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> {
let changes = Changes {
devices: DeviceChanges {
changed: devices.to_vec(),
..Default::default()
},
..Default::default()
};
self.save_changes(changes).await
}
#[cfg(test)]
pub async fn save_inbound_group_sessions(
&self,
sessions: &[InboundGroupSession],
) -> Result<()> {
let changes = Changes {
inbound_group_sessions: sessions.to_vec(),
..Default::default()
};
self.save_changes(changes).await
}
pub async fn get_readonly_devices(
&self,
user_id: &UserId,
) -> Result<HashMap<DeviceIdBox, ReadOnlyDevice>> {
self.inner.get_user_devices(user_id).await
}
pub async fn get_device_from_curve_key(
&self,
user_id: &UserId,
curve_key: &str,
) -> Result<Option<Device>> {
self.get_user_devices(user_id).await.map(|d| {
d.devices().find(|d| {
d.get_key(DeviceKeyAlgorithm::Curve25519)
.map_or(false, |k| k == curve_key)
})
})
}
pub async fn get_user_devices(&self, user_id: &UserId) -> Result<UserDevices> {
let devices = self.inner.get_user_devices(user_id).await?;
let own_identity = self
.inner
.get_user_identity(&self.user_id)
.await?
.map(|i| i.own().cloned())
.flatten();
let device_owner_identity = self.inner.get_user_identity(user_id).await.ok().flatten();
Ok(UserDevices {
inner: devices,
private_identity: self.identity.clone(),
verification_machine: self.verification_machine.clone(),
own_identity,
device_owner_identity,
})
}
pub async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<Device>> {
let own_identity = self
.get_user_identity(&self.user_id)
.await?
.map(|i| i.own().cloned())
.flatten();
let device_owner_identity = self.get_user_identity(user_id).await?;
Ok(self
.inner
.get_device(user_id, device_id)
.await?
.map(|d| Device {
inner: d,
private_identity: self.identity.clone(),
verification_machine: self.verification_machine.clone(),
own_identity,
device_owner_identity,
}))
}
pub async fn get_object<V: for<'b> Deserialize<'b>>(&self, key: &str) -> Result<Option<V>> {
if let Some(value) = self.get_value(key).await? {
Ok(Some(serde_json::from_str(&value)?))
} else {
Ok(None)
}
}
pub async fn save_object(&self, key: &str, value: &impl Serialize) -> Result<()> {
let value = serde_json::to_string(value)?;
self.save_value(key.to_owned(), value).await
}
pub async fn delete_object(&self, key: &str) -> Result<()> {
self.inner.remove_value(key).await?;
Ok(())
}
}
impl Deref for Store {
type Target = dyn CryptoStore;
fn deref(&self) -> &Self::Target {
&**self.inner
}
}
#[derive(Error, Debug)]
/// The crypto store's error type.
pub enum CryptoStoreError {
@@ -47,7 +292,7 @@ pub enum CryptoStoreError {
/// SQL error occurred.
// TODO flatten the SqlxError to make it easier for other store
// implementations.
#[cfg(feature = "sqlite-cryptostore")]
#[cfg(feature = "sqlite_cryptostore")]
#[error(transparent)]
DatabaseError(#[from] SqlxError),
@@ -68,57 +313,53 @@ pub enum CryptoStoreError {
OlmGroupSession(#[from] OlmGroupSessionError),
/// A session time-stamp couldn't be loaded.
#[error("can't load session timestamps")]
SessionTimestampError,
#[error(transparent)]
SessionUnpickling(#[from] SessionUnpicklingError),
/// Failed to decrypt an pickled object.
#[error("An object failed to be decrypted while unpickling")]
UnpicklingError,
/// A Matirx identifier failed to be validated.
#[error(transparent)]
IdentifierValidation(#[from] IdentifierValidationError),
/// The store failed to (de)serialize a data type.
#[error(transparent)]
Serialization(#[from] SerdeError),
/// An error occurred while parsing an URL.
#[error(transparent)]
UrlParse(#[from] ParseError),
}
pub type Result<T> = std::result::Result<T, CryptoStoreError>;
#[async_trait]
/// Trait abstracting a store that the `OlmMachine` uses to store cryptographic
/// keys.
pub trait CryptoStore: Debug + Send + Sync {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait CryptoStore: AsyncTraitDeps {
/// Load an account that was previously stored.
async fn load_account(&mut self) -> Result<Option<Account>>;
async fn load_account(&self) -> Result<Option<ReadOnlyAccount>>;
/// Save the given account in the store.
///
/// # Arguments
///
/// * `account` - The account that should be stored.
async fn save_account(&mut self, account: Account) -> Result<()>;
async fn save_account(&self, account: ReadOnlyAccount) -> Result<()>;
/// Save the given sessions in the store.
/// Try to load a private cross signing identity, if one is stored.
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>>;
/// Save the set of changes to the store.
///
/// # Arguments
///
/// * `session` - The sessions that should be stored.
async fn save_sessions(&mut self, session: &[Session]) -> Result<()>;
/// * `changes` - The set of changes that should be stored.
async fn save_changes(&self, changes: Changes) -> Result<()>;
/// Get all the sessions that belong to the given sender key.
///
/// # Arguments
///
/// * `sender_key` - The sender key that was used to establish the sessions.
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>>;
/// Save the given inbound group session in the store.
///
/// If the session wasn't already in the store true is returned, false
/// otherwise.
///
/// # Arguments
///
/// * `session` - The session that should be stored.
async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<bool>;
async fn get_sessions(&self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>>;
/// Get the inbound group session from our store.
///
@@ -129,18 +370,24 @@ pub trait CryptoStore: Debug + Send + Sync {
///
/// * `session_id` - The unique id of the session.
async fn get_inbound_group_session(
&mut self,
&self,
room_id: &RoomId,
sender_key: &str,
session_id: &str,
) -> Result<Option<InboundGroupSession>>;
/// Get the set of tracked users.
fn tracked_users(&self) -> &HashSet<UserId>;
/// Get all the inbound group sessions we have stored.
async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>>;
/// Is the given user already tracked.
fn is_user_tracked(&self, user_id: &UserId) -> bool;
/// Are there any tracked users that are marked as dirty.
fn has_users_for_key_query(&self) -> bool;
/// Set of users that we need to query keys for. This is a subset of
/// the tracked users.
fn users_for_key_query(&self) -> &HashSet<UserId>;
fn users_for_key_query(&self) -> HashSet<UserId>;
/// Add an user for tracking.
///
@@ -151,21 +398,7 @@ pub trait CryptoStore: Debug + Send + Sync {
/// * `user` - The user that should be marked as tracked.
///
/// * `dirty` - Should the user be also marked for a key query.
async fn update_tracked_user(&mut self, user: &UserId, dirty: bool) -> Result<bool>;
/// Save the given devices in the store.
///
/// # Arguments
///
/// * `device` - The device that should be stored.
async fn save_devices(&self, devices: &[Device]) -> Result<()>;
/// Delete the given device from the store.
///
/// # Arguments
///
/// * `device` - The device that should be stored.
async fn delete_device(&self, device: Device) -> Result<()>;
async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result<bool>;
/// Get the device for the given user with the given device id.
///
@@ -174,14 +407,38 @@ pub trait CryptoStore: Debug + Send + Sync {
/// * `user_id` - The user that the device belongs to.
///
/// * `device_id` - The unique id of the device.
#[allow(clippy::ptr_arg)]
async fn get_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<Option<Device>>;
async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<ReadOnlyDevice>>;
/// Get all the devices of the given user.
///
///
/// # Arguments
///
/// * `user_id` - The user for which we should get all the devices.
async fn get_user_devices(&self, user_id: &UserId) -> Result<UserDevices>;
async fn get_user_devices(
&self,
user_id: &UserId,
) -> Result<HashMap<DeviceIdBox, ReadOnlyDevice>>;
/// Get the user identity that is attached to the given user id.
///
/// # Arguments
///
/// * `user_id` - The user for which we should get the identity.
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentities>>;
/// Save a serializeable object in the store.
async fn save_value(&self, key: String, value: String) -> Result<()>;
/// Remove a value from the store.
async fn remove_value(&self, key: &str) -> Result<()>;
/// Load a serializeable object from the store.
async fn get_value(&self, key: &str) -> Result<Option<String>>;
/// Check if a hash for an Olm message stored in the database.
async fn is_message_known(&self, message_hash: &OlmMessageHash) -> Result<bool>;
}
+206
View File
@@ -0,0 +1,206 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryFrom;
use aes_gcm::{
aead::{generic_array::GenericArray, Aead, NewAead},
Aes256Gcm, Error as DecryptionError,
};
use getrandom::getrandom;
use hmac::Hmac;
use olm_rs::PicklingMode;
use pbkdf2::pbkdf2;
use sha2::Sha256;
use zeroize::{Zeroize, Zeroizing};
use serde::{Deserialize, Serialize};
const KEY_SIZE: usize = 32;
const NONCE_SIZE: usize = 12;
const KDF_SALT_SIZE: usize = 32;
const KDF_ROUNDS: u32 = 10000;
/// Version specific info for the key derivation method that is used.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum KdfInfo {
Pbkdf2 {
/// The number of PBKDF rounds that were used when deriving the AES key.
rounds: u32,
},
}
/// Version specific info for encryption method that is used to encrypt our
/// pickle key.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum CipherTextInfo {
Aes256Gcm {
/// The nonce that was used to encrypt the ciphertext.
nonce: Vec<u8>,
/// The encrypted pickle key.
ciphertext: Vec<u8>,
},
}
/// An encrypted version of our pickle key, this can be safely stored in a
/// database.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EncryptedPickleKey {
/// Info about the key derivation method that was used to expand the
/// passphrase into an encryption key.
pub kdf_info: KdfInfo,
/// The ciphertext with it's accompanying additional data that is needed to
/// decrypt the pickle key.
pub ciphertext_info: CipherTextInfo,
/// The salt that was used when the passphrase was expanded into a AES key.
kdf_salt: Vec<u8>,
}
/// A pickle key that will be used to encrypt all the private keys for Olm.
///
/// Olm uses AES256 to encrypt accounts, sessions, inbound group sessions. We
/// also implement our own pickling for the cross-signing types using
/// AES256-GCM so the key sizes match.
#[derive(Debug, Zeroize, PartialEq)]
pub struct PickleKey {
aes256_key: Vec<u8>,
}
impl Default for PickleKey {
fn default() -> Self {
let mut key = vec![0u8; KEY_SIZE];
getrandom(&mut key).expect("Can't generate new pickle key");
Self { aes256_key: key }
}
}
impl TryFrom<Vec<u8>> for PickleKey {
type Error = ();
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() != KEY_SIZE {
Err(())
} else {
Ok(Self { aes256_key: value })
}
}
}
impl PickleKey {
/// Generate a new random pickle key.
pub fn new() -> Self {
Default::default()
}
fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Zeroizing<Vec<u8>> {
let mut key = Zeroizing::from(vec![0u8; KEY_SIZE]);
pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), &salt, rounds, &mut *key);
key
}
/// Get a `PicklingMode` version of this pickle key.
pub fn pickle_mode(&self) -> PicklingMode {
PicklingMode::Encrypted {
key: self.aes256_key.clone(),
}
}
/// Get the raw AES256 key.
pub fn key(&self) -> &[u8] {
&self.aes256_key
}
/// Encrypt and export our pickle key using the given passphrase.
///
/// # Arguments
///
/// * `passphrase` - The passphrase that should be used to encrypt the
/// pickle key.
pub fn encrypt(&self, passphrase: &str) -> EncryptedPickleKey {
let mut salt = vec![0u8; KDF_SALT_SIZE];
getrandom(&mut salt).expect("Can't generate new random pickle key");
let key = PickleKey::expand_key(passphrase, &salt, KDF_ROUNDS);
let key = GenericArray::from_slice(key.as_ref());
let cipher = Aes256Gcm::new(&key);
let mut nonce = vec![0u8; NONCE_SIZE];
getrandom(&mut nonce).expect("Can't generate new random nonce for the pickle key");
let ciphertext = cipher
.encrypt(
&GenericArray::from_slice(nonce.as_ref()),
self.aes256_key.as_slice(),
)
.expect("Can't encrypt pickle key");
EncryptedPickleKey {
kdf_info: KdfInfo::Pbkdf2 { rounds: KDF_ROUNDS },
kdf_salt: salt,
ciphertext_info: CipherTextInfo::Aes256Gcm { nonce, ciphertext },
}
}
/// Restore a pickle key from an encrypted export.
///
/// # Arguments
///
/// * `passphrase` - The passphrase that should be used to encrypt the
/// pickle key.
///
/// * `encrypted` - The exported and encrypted version of the pickle key.
pub fn from_encrypted(
passphrase: &str,
encrypted: EncryptedPickleKey,
) -> Result<Self, DecryptionError> {
let key = match encrypted.kdf_info {
KdfInfo::Pbkdf2 { rounds } => Self::expand_key(passphrase, &encrypted.kdf_salt, rounds),
};
let key = GenericArray::from_slice(key.as_ref());
let decrypted = match encrypted.ciphertext_info {
CipherTextInfo::Aes256Gcm { nonce, ciphertext } => {
let cipher = Aes256Gcm::new(&key);
let nonce = GenericArray::from_slice(&nonce);
cipher.decrypt(nonce, ciphertext.as_ref())?
}
};
Ok(Self {
aes256_key: decrypted,
})
}
}
#[cfg(test)]
mod test {
use super::PickleKey;
#[test]
fn generating() {
PickleKey::new();
}
#[test]
fn encrypting() {
let passphrase = "it's a secret to everybody";
let pickle_key = PickleKey::new();
let encrypted = pickle_key.encrypt(passphrase);
let decrypted = PickleKey::from_encrypted(passphrase, encrypted).unwrap();
assert_eq!(pickle_key, decrypted);
}
}
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub use base64::DecodeError;
use base64::{decode_config, encode_config, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
/// Decode the input as base64 with no padding.
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
decode_config(input, STANDARD_NO_PAD)
}
/// Decode the input as URL safe base64 with no padding.
pub fn decode_url_safe(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
decode_config(input, URL_SAFE_NO_PAD)
}
/// Encode the input as base64 with no padding.
pub fn encode(input: impl AsRef<[u8]>) -> String {
encode_config(input, STANDARD_NO_PAD)
}
/// Encode the input as URL safe base64 with no padding.
pub fn encode_url_safe(input: impl AsRef<[u8]>) -> String {
encode_config(input, URL_SAFE_NO_PAD)
}
@@ -0,0 +1,401 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use dashmap::DashMap;
use matrix_sdk_common::locks::Mutex;
use tracing::{trace, warn};
use matrix_sdk_common::{
events::{AnyToDeviceEvent, AnyToDeviceEventContent},
identifiers::{DeviceId, UserId},
uuid::Uuid,
};
use super::sas::{content_to_request, Sas, VerificationResult};
use crate::{
olm::PrivateCrossSigningIdentity,
requests::{OutgoingRequest, ToDeviceRequest},
store::{CryptoStore, CryptoStoreError},
ReadOnlyAccount, ReadOnlyDevice,
};
#[derive(Clone, Debug)]
pub struct VerificationMachine {
account: ReadOnlyAccount,
private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
pub(crate) store: Arc<Box<dyn CryptoStore>>,
verifications: Arc<DashMap<String, Sas>>,
outgoing_to_device_messages: Arc<DashMap<Uuid, OutgoingRequest>>,
}
impl VerificationMachine {
pub(crate) fn new(
account: ReadOnlyAccount,
identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
store: Arc<Box<dyn CryptoStore>>,
) -> Self {
Self {
account,
private_identity: identity,
store,
verifications: Arc::new(DashMap::new()),
outgoing_to_device_messages: Arc::new(DashMap::new()),
}
}
pub async fn start_sas(
&self,
device: ReadOnlyDevice,
) -> Result<(Sas, ToDeviceRequest), CryptoStoreError> {
let identity = self.store.get_user_identity(device.user_id()).await?;
let private_identity = self.private_identity.lock().await.clone();
let (sas, content) = Sas::start(
self.account.clone(),
private_identity,
device.clone(),
self.store.clone(),
identity,
);
let request = content_to_request(
device.user_id(),
device.device_id(),
AnyToDeviceEventContent::KeyVerificationStart(content),
);
self.verifications
.insert(sas.flow_id().to_owned(), sas.clone());
Ok((sas, request))
}
pub fn get_sas(&self, transaction_id: &str) -> Option<Sas> {
#[allow(clippy::map_clone)]
self.verifications.get(transaction_id).map(|s| s.clone())
}
fn queue_up_content(
&self,
recipient: &UserId,
recipient_device: &DeviceId,
content: AnyToDeviceEventContent,
) {
let request = content_to_request(recipient, recipient_device, content);
let request_id = request.txn_id;
let request = OutgoingRequest {
request_id,
request: Arc::new(request.into()),
};
self.outgoing_to_device_messages.insert(request_id, request);
}
fn receive_event_helper(&self, sas: &Sas, event: &mut AnyToDeviceEvent) {
if let Some(c) = sas.receive_event(event) {
self.queue_up_content(sas.other_user_id(), sas.other_device_id(), c);
}
}
pub fn mark_request_as_sent(&self, uuid: &Uuid) {
self.outgoing_to_device_messages.remove(uuid);
}
pub fn outgoing_to_device_requests(&self) -> Vec<OutgoingRequest> {
#[allow(clippy::map_clone)]
self.outgoing_to_device_messages
.iter()
.map(|r| (*r).clone())
.collect()
}
pub fn garbage_collect(&self) {
self.verifications
.retain(|_, s| !(s.is_done() || s.is_canceled()));
for sas in self.verifications.iter() {
if let Some(r) = sas.cancel_if_timed_out() {
self.outgoing_to_device_messages.insert(
r.txn_id,
OutgoingRequest {
request_id: r.txn_id,
request: Arc::new(r.into()),
},
);
}
}
}
pub async fn receive_event(
&self,
event: &mut AnyToDeviceEvent,
) -> Result<(), CryptoStoreError> {
trace!("Received a key verification event {:?}", event);
match event {
AnyToDeviceEvent::KeyVerificationStart(e) => {
trace!(
"Received a m.key.verification start event from {} {}",
e.sender,
e.content.from_device
);
if let Some(d) = self
.store
.get_device(&e.sender, &e.content.from_device)
.await?
{
let private_identity = self.private_identity.lock().await.clone();
match Sas::from_start_event(
self.account.clone(),
private_identity,
d,
self.store.clone(),
e,
self.store.get_user_identity(&e.sender).await?,
) {
Ok(s) => {
self.verifications
.insert(e.content.transaction_id.clone(), s);
}
Err(c) => {
warn!(
"Can't start key verification with {} {}, canceling: {:?}",
e.sender, e.content.from_device, c
);
self.queue_up_content(&e.sender, &e.content.from_device, c)
}
}
} else {
warn!(
"Received a key verification start event from an unknown device {} {}",
e.sender, e.content.from_device
);
}
}
AnyToDeviceEvent::KeyVerificationCancel(e) => {
self.verifications.remove(&e.content.transaction_id);
}
AnyToDeviceEvent::KeyVerificationAccept(e) => {
if let Some(s) = self.get_sas(&e.content.transaction_id) {
self.receive_event_helper(&s, event)
};
}
AnyToDeviceEvent::KeyVerificationKey(e) => {
if let Some(s) = self.get_sas(&e.content.transaction_id) {
self.receive_event_helper(&s, event)
};
}
AnyToDeviceEvent::KeyVerificationMac(e) => {
if let Some(s) = self.get_sas(&e.content.transaction_id) {
self.receive_event_helper(&s, event);
if s.is_done() {
match s.mark_as_done().await? {
VerificationResult::Ok => (),
VerificationResult::Cancel(r) => {
self.outgoing_to_device_messages.insert(
r.txn_id,
OutgoingRequest {
request_id: r.txn_id,
request: Arc::new(r.into()),
},
);
}
VerificationResult::SignatureUpload(r) => {
let request_id = Uuid::new_v4();
self.outgoing_to_device_messages.insert(
request_id,
OutgoingRequest {
request_id,
request: Arc::new(r.into()),
},
);
}
}
}
};
}
_ => (),
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::{
convert::TryFrom,
sync::Arc,
time::{Duration, Instant},
};
use matrix_sdk_common::{
events::AnyToDeviceEventContent,
identifiers::{DeviceId, UserId},
locks::Mutex,
};
use super::{Sas, VerificationMachine};
use crate::{
olm::PrivateCrossSigningIdentity,
requests::OutgoingRequests,
store::{CryptoStore, MemoryStore},
verification::test::{get_content_from_request, wrap_any_to_device_content},
ReadOnlyAccount, ReadOnlyDevice,
};
fn alice_id() -> UserId {
UserId::try_from("@alice:example.org").unwrap()
}
fn alice_device_id() -> Box<DeviceId> {
"JLAFKJWSCS".into()
}
fn bob_id() -> UserId {
UserId::try_from("@bob:example.org").unwrap()
}
fn bob_device_id() -> Box<DeviceId> {
"BOBDEVCIE".into()
}
async fn setup_verification_machine() -> (VerificationMachine, Sas) {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
let store = MemoryStore::new();
let bob_store = MemoryStore::new();
let bob_device = ReadOnlyDevice::from_account(&bob).await;
let alice_device = ReadOnlyDevice::from_account(&alice).await;
store.save_devices(vec![bob_device]).await;
bob_store.save_devices(vec![alice_device.clone()]).await;
let bob_store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(bob_store));
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(alice_id())));
let machine = VerificationMachine::new(alice, identity, Arc::new(Box::new(store)));
let (bob_sas, start_content) = Sas::start(
bob,
PrivateCrossSigningIdentity::empty(bob_id()),
alice_device,
bob_store,
None,
);
machine
.receive_event(&mut wrap_any_to_device_content(
bob_sas.user_id(),
AnyToDeviceEventContent::KeyVerificationStart(start_content),
))
.await
.unwrap();
(machine, bob_sas)
}
#[test]
fn create() {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(alice_id())));
let store = MemoryStore::new();
let _ = VerificationMachine::new(alice, identity, Arc::new(Box::new(store)));
}
#[tokio::test]
async fn full_flow() {
let (alice_machine, bob) = setup_verification_machine().await;
let alice = alice_machine.get_sas(bob.flow_id()).unwrap();
let mut event = alice
.accept()
.map(|c| wrap_any_to_device_content(alice.user_id(), get_content_from_request(&c)))
.unwrap();
let mut event = bob
.receive_event(&mut event)
.map(|c| wrap_any_to_device_content(bob.user_id(), c))
.unwrap();
assert!(alice_machine.outgoing_to_device_messages.is_empty());
alice_machine.receive_event(&mut event).await.unwrap();
assert!(!alice_machine.outgoing_to_device_messages.is_empty());
let request = alice_machine
.outgoing_to_device_messages
.iter()
.next()
.unwrap();
let txn_id = *request.request_id();
let r = if let OutgoingRequests::ToDeviceRequest(r) = request.request() {
r
} else {
panic!("Invalid request type");
};
let mut event = wrap_any_to_device_content(alice.user_id(), get_content_from_request(r));
drop(request);
alice_machine.mark_request_as_sent(&txn_id);
assert!(bob.receive_event(&mut event).is_none());
assert!(alice.emoji().is_some());
assert!(bob.emoji().is_some());
assert_eq!(alice.emoji(), bob.emoji());
let mut event = wrap_any_to_device_content(
alice.user_id(),
get_content_from_request(&alice.confirm().await.unwrap().0.unwrap()),
);
bob.receive_event(&mut event);
let mut event = wrap_any_to_device_content(
bob.user_id(),
get_content_from_request(&bob.confirm().await.unwrap().0.unwrap()),
);
alice.receive_event(&mut event);
assert!(alice.is_done());
assert!(bob.is_done());
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn timing_out() {
let (alice_machine, bob) = setup_verification_machine().await;
let alice = alice_machine.get_sas(bob.flow_id()).unwrap();
assert!(!alice.timed_out());
assert!(alice_machine.outgoing_to_device_messages.is_empty());
// This line panics on macOS, so we're disabled for now.
alice.set_creation_time(Instant::now() - Duration::from_secs(60 * 15));
assert!(alice.timed_out());
assert!(alice_machine.outgoing_to_device_messages.is_empty());
alice_machine.garbage_collect();
assert!(!alice_machine.outgoing_to_device_messages.is_empty());
alice_machine.garbage_collect();
assert!(alice_machine.verifications.is_empty());
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod machine;
mod sas;
pub use machine::VerificationMachine;
pub use sas::{Sas, VerificationResult};
#[cfg(test)]
pub(crate) mod test {
use crate::requests::{OutgoingRequest, OutgoingRequests, ToDeviceRequest};
use serde_json::Value;
use matrix_sdk_common::{
events::{AnyToDeviceEvent, AnyToDeviceEventContent, EventType, ToDeviceEvent},
identifiers::UserId,
};
pub(crate) fn request_to_event(sender: &UserId, request: &ToDeviceRequest) -> AnyToDeviceEvent {
let content = get_content_from_request(request);
wrap_any_to_device_content(sender, content)
}
pub(crate) fn outgoing_request_to_event(
sender: &UserId,
request: &OutgoingRequest,
) -> AnyToDeviceEvent {
match request.request() {
OutgoingRequests::ToDeviceRequest(r) => request_to_event(sender, r),
_ => panic!("Unsupported outgoing request"),
}
}
pub(crate) fn wrap_any_to_device_content(
sender: &UserId,
content: AnyToDeviceEventContent,
) -> AnyToDeviceEvent {
match content {
AnyToDeviceEventContent::KeyVerificationKey(c) => {
AnyToDeviceEvent::KeyVerificationKey(ToDeviceEvent {
sender: sender.clone(),
content: c,
})
}
AnyToDeviceEventContent::KeyVerificationStart(c) => {
AnyToDeviceEvent::KeyVerificationStart(ToDeviceEvent {
sender: sender.clone(),
content: c,
})
}
AnyToDeviceEventContent::KeyVerificationAccept(c) => {
AnyToDeviceEvent::KeyVerificationAccept(ToDeviceEvent {
sender: sender.clone(),
content: c,
})
}
AnyToDeviceEventContent::KeyVerificationMac(c) => {
AnyToDeviceEvent::KeyVerificationMac(ToDeviceEvent {
sender: sender.clone(),
content: c,
})
}
_ => unreachable!(),
}
}
pub(crate) fn get_content_from_request(request: &ToDeviceRequest) -> AnyToDeviceEventContent {
let json: Value = serde_json::from_str(
request
.messages
.values()
.next()
.unwrap()
.values()
.next()
.unwrap()
.get(),
)
.unwrap();
match request.event_type {
EventType::KeyVerificationStart => {
AnyToDeviceEventContent::KeyVerificationStart(serde_json::from_value(json).unwrap())
}
EventType::KeyVerificationKey => {
AnyToDeviceEventContent::KeyVerificationKey(serde_json::from_value(json).unwrap())
}
EventType::KeyVerificationAccept => AnyToDeviceEventContent::KeyVerificationAccept(
serde_json::from_value(json).unwrap(),
),
EventType::KeyVerificationMac => {
AnyToDeviceEventContent::KeyVerificationMac(serde_json::from_value(json).unwrap())
}
EventType::KeyVerificationCancel => AnyToDeviceEventContent::KeyVerificationCancel(
serde_json::from_value(json).unwrap(),
),
_ => unreachable!(),
}
}
}
@@ -0,0 +1,605 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, convert::TryInto};
use sha2::{Digest, Sha256};
use tracing::{trace, warn};
use olm_rs::sas::OlmSas;
use matrix_sdk_common::{
api::r0::to_device::DeviceIdOrAllDevices,
events::{
key::verification::{
cancel::CancelCode, mac::MacToDeviceEventContent, start::StartToDeviceEventContent,
},
AnyToDeviceEventContent, EventType, ToDeviceEvent,
},
identifiers::{DeviceId, DeviceKeyAlgorithm, DeviceKeyId, UserId},
uuid::Uuid,
CanonicalJsonValue,
};
use crate::{
identities::{ReadOnlyDevice, UserIdentities},
utilities::encode,
ReadOnlyAccount, ToDeviceRequest,
};
#[derive(Clone, Debug)]
pub struct SasIds {
pub account: ReadOnlyAccount,
pub other_device: ReadOnlyDevice,
pub other_identity: Option<UserIdentities>,
}
/// Calculate the commitment for a accept event from the public key and the
/// start event.
///
/// # Arguments
///
/// * `public_key` - Our own ephemeral public key that is used for the
/// interactive verification.
///
/// * `content` - The `m.key.verification.start` event content that started the
/// interactive verification process.
pub fn calculate_commitment(public_key: &str, content: &StartToDeviceEventContent) -> String {
let json_content: CanonicalJsonValue = serde_json::to_value(content)
.expect("Can't serialize content")
.try_into()
.expect("Can't canonicalize content");
encode(
Sha256::new()
.chain(&format!("{}{}", public_key, json_content))
.finalize(),
)
}
/// Get a tuple of an emoji and a description of the emoji using a number.
///
/// This is taken directly from the [spec]
///
/// # Panics
///
/// The spec defines 64 unique emojis, this function panics if the index is
/// bigger than 63.
///
/// [spec]: https://matrix.org/docs/spec/client_server/latest#sas-method-emoji
fn emoji_from_index(index: u8) -> (&'static str, &'static str) {
match index {
0 => ("🐶", "Dog"),
1 => ("🐱", "Cat"),
2 => ("🦁", "Lion"),
3 => ("🐎", "Horse"),
4 => ("🦄", "Unicorn"),
5 => ("🐷", "Pig"),
6 => ("🐘", "Elephant"),
7 => ("🐰", "Rabbit"),
8 => ("🐼", "Panda"),
9 => ("🐓", "Rooster"),
10 => ("🐧", "Penguin"),
11 => ("🐢", "Turtle"),
12 => ("🐟", "Fish"),
13 => ("🐙", "Octopus"),
14 => ("🦋", "Butterfly"),
15 => ("🌷", "Flower"),
16 => ("🌳", "Tree"),
17 => ("🌵", "Cactus"),
18 => ("🍄", "Mushroom"),
19 => ("🌏", "Globe"),
20 => ("🌙", "Moon"),
21 => ("☁️", "Cloud"),
22 => ("🔥", "Fire"),
23 => ("🍌", "Banana"),
24 => ("🍎", "Apple"),
25 => ("🍓", "Strawberry"),
26 => ("🌽", "Corn"),
27 => ("🍕", "Pizza"),
28 => ("🎂", "Cake"),
29 => ("❤️", "Heart"),
30 => ("😀", "Smiley"),
31 => ("🤖", "Robot"),
32 => ("🎩", "Hat"),
33 => ("👓", "Glasses"),
34 => ("🔧", "Spanner"),
35 => ("🎅", "Santa"),
36 => ("👍", "Thumbs up"),
37 => ("☂️", "Umbrella"),
38 => ("", "Hourglass"),
39 => ("", "Clock"),
40 => ("🎁", "Gift"),
41 => ("💡", "Light Bulb"),
42 => ("📕", "Book"),
43 => ("✏️", "Pencil"),
44 => ("📎", "Paperclip"),
45 => ("✂️", "Scissors"),
46 => ("🔒", "Lock"),
47 => ("🔑", "Key"),
48 => ("🔨", "Hammer"),
49 => ("☎️", "Telephone"),
50 => ("🏁", "Flag"),
51 => ("🚂", "Train"),
52 => ("🚲", "Bicycle"),
53 => ("✈️", "Airplane"),
54 => ("🚀", "Rocket"),
55 => ("🏆", "Trophy"),
56 => ("", "Ball"),
57 => ("🎸", "Guitar"),
58 => ("🎺", "Trumpet"),
59 => ("🔔", "Bell"),
60 => ("", "Anchor"),
61 => ("🎧", "Headphones"),
62 => ("📁", "Folder"),
63 => ("📌", "Pin"),
_ => panic!("Trying to fetch an emoji outside the allowed range"),
}
}
/// Get the extra info that will be used when we check the MAC of a
/// m.key.verification.key event.
///
/// # Arguments
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
fn extra_mac_info_receive(ids: &SasIds, flow_id: &str) -> String {
format!(
"MATRIX_KEY_VERIFICATION_MAC{first_user}{first_device}\
{second_user}{second_device}{transaction_id}",
first_user = ids.other_device.user_id(),
first_device = ids.other_device.device_id(),
second_user = ids.account.user_id(),
second_device = ids.account.device_id(),
transaction_id = flow_id,
)
}
/// Get the content for a m.key.verification.mac event.
///
/// Returns a tuple that contains the list of verified devices and the list of
/// verified master keys.
///
/// # Arguments
///
/// * `sas` - The Olm SAS object that can be used to MACs
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
///
/// * `event` - The m.key.verification.mac event that was sent to us by
/// the other side.
pub fn receive_mac_event(
sas: &OlmSas,
ids: &SasIds,
flow_id: &str,
event: &ToDeviceEvent<MacToDeviceEventContent>,
) -> Result<(Vec<ReadOnlyDevice>, Vec<UserIdentities>), CancelCode> {
let mut verified_devices = Vec::new();
let mut verified_identities = Vec::new();
let info = extra_mac_info_receive(&ids, flow_id);
trace!(
"Received a key.verification.mac event from {} {}",
event.sender,
ids.other_device.device_id()
);
let mut keys = event.content.mac.keys().cloned().collect::<Vec<String>>();
keys.sort();
let keys = sas
.calculate_mac(&keys.join(","), &format!("{}KEY_IDS", &info))
.expect("Can't calculate SAS MAC");
if keys != event.content.keys {
return Err(CancelCode::KeyMismatch);
}
for (key_id, key_mac) in &event.content.mac {
trace!(
"Checking MAC for the key id {} from {} {}",
key_id,
event.sender,
ids.other_device.device_id()
);
let key_id: DeviceKeyId = match key_id.as_str().try_into() {
Ok(id) => id,
Err(_) => continue,
};
if let Some(key) = ids.other_device.keys().get(&key_id) {
if key_mac
== &sas
.calculate_mac(key, &format!("{}{}", info, key_id))
.expect("Can't calculate SAS MAC")
{
verified_devices.push(ids.other_device.clone());
} else {
return Err(CancelCode::KeyMismatch);
}
} else if let Some(identity) = &ids.other_identity {
if let Some(key) = identity.master_key().get_key(&key_id) {
// TODO we should check that the master key signs the device,
// this way we know the master key also trusts the device
if key_mac
== &sas
.calculate_mac(key, &format!("{}{}", info, key_id))
.expect("Can't calculate SAS MAC")
{
trace!(
"Successfully verified the master key {} from {}",
key_id,
event.sender
);
verified_identities.push(identity.clone())
} else {
return Err(CancelCode::KeyMismatch);
}
}
} else {
warn!(
"Key ID {} in MAC event from {} {} doesn't belong to any device \
or user identity",
key_id,
event.sender,
ids.other_device.device_id()
);
}
}
Ok((verified_devices, verified_identities))
}
/// Get the extra info that will be used when we generate a MAC and need to send
/// it out
///
/// # Arguments
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
fn extra_mac_info_send(ids: &SasIds, flow_id: &str) -> String {
format!(
"MATRIX_KEY_VERIFICATION_MAC{first_user}{first_device}\
{second_user}{second_device}{transaction_id}",
first_user = ids.account.user_id(),
first_device = ids.account.device_id(),
second_user = ids.other_device.user_id(),
second_device = ids.other_device.device_id(),
transaction_id = flow_id,
)
}
/// Get the content for a m.key.verification.mac event.
///
/// # Arguments
///
/// * `sas` - The Olm SAS object that can be used to generate the MAC
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
///
/// # Panics
///
/// This will panic if the public key of the other side wasn't set.
pub fn get_mac_content(sas: &OlmSas, ids: &SasIds, flow_id: &str) -> MacToDeviceEventContent {
let mut mac: BTreeMap<String, String> = BTreeMap::new();
let key_id = DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, ids.account.device_id());
let key = ids.account.identity_keys().ed25519();
let info = extra_mac_info_send(ids, flow_id);
mac.insert(
key_id.to_string(),
sas.calculate_mac(key, &format!("{}{}", info, key_id))
.expect("Can't calculate SAS MAC"),
);
// TODO Add the cross signing master key here if we trust/have it.
let mut keys = mac.keys().cloned().collect::<Vec<String>>();
keys.sort();
let keys = sas
.calculate_mac(&keys.join(","), &format!("{}KEY_IDS", &info))
.expect("Can't calculate SAS MAC");
MacToDeviceEventContent {
transaction_id: flow_id.to_owned(),
keys,
mac,
}
}
/// Get the extra info that will be used when we generate bytes for the short
/// auth string.
///
/// # Arguments
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
///
/// * `we_started` - Flag signaling if the SAS process was started on our side.
fn extra_info_sas(
ids: &SasIds,
own_pubkey: &str,
their_pubkey: &str,
flow_id: &str,
we_started: bool,
) -> String {
let our_info = format!(
"{}|{}|{}",
ids.account.user_id(),
ids.account.device_id(),
own_pubkey
);
let their_info = format!(
"{}|{}|{}",
ids.other_device.user_id(),
ids.other_device.device_id(),
their_pubkey
);
let (first_info, second_info) = if we_started {
(our_info, their_info)
} else {
(their_info, our_info)
};
let info = format!(
"MATRIX_KEY_VERIFICATION_SAS|{first_info}|{second_info}|{flow_id}",
first_info = first_info,
second_info = second_info,
flow_id = flow_id,
);
trace!("Generated a SAS extra info: {}", info);
info
}
/// Get the emoji version of the short authentication string.
///
/// Returns a vector of tuples where the first element is the emoji and the
/// second element the English description of the emoji.
///
/// # Arguments
///
/// * `sas` - The Olm SAS object that can be used to generate bytes using the
/// shared secret.
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
///
/// * `we_started` - Flag signaling if the SAS process was started on our side.
///
/// # Panics
///
/// This will panic if the public key of the other side wasn't set.
pub fn get_emoji(
sas: &OlmSas,
ids: &SasIds,
their_pubkey: &str,
flow_id: &str,
we_started: bool,
) -> Vec<(&'static str, &'static str)> {
let bytes = sas
.generate_bytes(
&extra_info_sas(&ids, &sas.public_key(), their_pubkey, &flow_id, we_started),
6,
)
.expect("Can't generate bytes");
bytes_to_emoji(bytes)
}
fn bytes_to_emoji_index(bytes: Vec<u8>) -> Vec<u8> {
let bytes: Vec<u64> = bytes.iter().map(|b| *b as u64).collect();
// Join the 6 bytes into one 64 bit unsigned int. This u64 will contain 48
// bits from our 6 bytes.
let mut num: u64 = bytes[0] << 40;
num += bytes[1] << 32;
num += bytes[2] << 24;
num += bytes[3] << 16;
num += bytes[4] << 8;
num += bytes[5];
// Take the top 42 bits of our 48 bits from the u64 and convert each 6 bits
// into a 6 bit number.
vec![
((num >> 42) & 63) as u8,
((num >> 36) & 63) as u8,
((num >> 30) & 63) as u8,
((num >> 24) & 63) as u8,
((num >> 18) & 63) as u8,
((num >> 12) & 63) as u8,
((num >> 6) & 63) as u8,
]
}
fn bytes_to_emoji(bytes: Vec<u8>) -> Vec<(&'static str, &'static str)> {
let numbers = bytes_to_emoji_index(bytes);
// Convert the 6 bit number into a emoji/description tuple.
numbers.into_iter().map(emoji_from_index).collect()
}
/// Get the decimal version of the short authentication string.
///
/// Returns a tuple containing three 4 digit integer numbers that represent
/// the short auth string.
///
/// # Arguments
///
/// * `sas` - The Olm SAS object that can be used to generate bytes using the
/// shared secret.
///
/// * `ids` - The ids that are used for this SAS authentication flow.
///
/// * `flow_id` - The unique id that identifies this SAS verification process.
///
/// * `we_started` - Flag signaling if the SAS process was started on our side.
///
/// # Panics
///
/// This will panic if the public key of the other side wasn't set.
pub fn get_decimal(
sas: &OlmSas,
ids: &SasIds,
their_pubkey: &str,
flow_id: &str,
we_started: bool,
) -> (u16, u16, u16) {
let bytes = sas
.generate_bytes(
&extra_info_sas(&ids, &sas.public_key(), their_pubkey, &flow_id, we_started),
5,
)
.expect("Can't generate bytes");
bytes_to_decimal(bytes)
}
fn bytes_to_decimal(bytes: Vec<u8>) -> (u16, u16, u16) {
let bytes: Vec<u16> = bytes.into_iter().map(|b| b as u16).collect();
// This bitwise operation is taken from the [spec]
// [spec]: https://matrix.org/docs/spec/client_server/latest#sas-method-decimal
let first = bytes[0] << 5 | bytes[1] >> 3;
let second = (bytes[1] & 0x7) << 10 | bytes[2] << 2 | bytes[3] >> 6;
let third = (bytes[3] & 0x3F) << 7 | bytes[4] >> 1;
(first + 1000, second + 1000, third + 1000)
}
pub fn content_to_request(
recipient: &UserId,
recipient_device: &DeviceId,
content: AnyToDeviceEventContent,
) -> ToDeviceRequest {
let mut messages = BTreeMap::new();
let mut user_messages = BTreeMap::new();
user_messages.insert(
DeviceIdOrAllDevices::DeviceId(recipient_device.into()),
serde_json::value::to_raw_value(&content).expect("Can't serialize to-device content"),
);
messages.insert(recipient.clone(), user_messages);
let event_type = match content {
AnyToDeviceEventContent::KeyVerificationAccept(_) => EventType::KeyVerificationAccept,
AnyToDeviceEventContent::KeyVerificationStart(_) => EventType::KeyVerificationStart,
AnyToDeviceEventContent::KeyVerificationKey(_) => EventType::KeyVerificationKey,
AnyToDeviceEventContent::KeyVerificationMac(_) => EventType::KeyVerificationMac,
AnyToDeviceEventContent::KeyVerificationCancel(_) => EventType::KeyVerificationCancel,
_ => unreachable!(),
};
ToDeviceRequest {
txn_id: Uuid::new_v4(),
event_type,
messages,
}
}
#[cfg(test)]
mod test {
use matrix_sdk_common::events::key::verification::start::StartToDeviceEventContent;
use proptest::prelude::*;
use serde_json::json;
use super::{
bytes_to_decimal, bytes_to_emoji, bytes_to_emoji_index, calculate_commitment,
emoji_from_index,
};
#[test]
fn commitment_calculation() {
let commitment = "CCQmB4JCdB0FW21FdAnHj/Hu8+W9+Nb0vgwPEnZZQ4g";
let public_key = "Q/NmNFEUS1fS+YeEmiZkjjblKTitrKOAk7cPEumcMlg";
let content = json!({
"from_device":"XOWLHHFSWM",
"transaction_id":"bYxBsirjUJO9osar6ST4i2M2NjrYLA7l",
"method":"m.sas.v1",
"key_agreement_protocols":["curve25519-hkdf-sha256","curve25519"],
"hashes":["sha256"],
"message_authentication_codes":["hkdf-hmac-sha256","hmac-sha256"],
"short_authentication_string":["decimal","emoji"]
});
let content: StartToDeviceEventContent = serde_json::from_value(content).unwrap();
let calculated_commitment = calculate_commitment(public_key, &content);
assert_eq!(commitment, &calculated_commitment);
}
#[test]
fn emoji_generation() {
let bytes = vec![0, 0, 0, 0, 0, 0];
let index: Vec<(&'static str, &'static str)> = vec![0, 0, 0, 0, 0, 0, 0]
.into_iter()
.map(emoji_from_index)
.collect();
assert_eq!(bytes_to_emoji(bytes), index);
let bytes = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
let index: Vec<(&'static str, &'static str)> = vec![63, 63, 63, 63, 63, 63, 63]
.into_iter()
.map(emoji_from_index)
.collect();
assert_eq!(bytes_to_emoji(bytes), index);
}
#[test]
fn decimal_generation() {
let bytes = vec![0, 0, 0, 0, 0];
let result = bytes_to_decimal(bytes);
assert_eq!(result, (1000, 1000, 1000));
let bytes = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
let result = bytes_to_decimal(bytes);
assert_eq!(result, (9191, 9191, 9191));
}
proptest! {
#[test]
fn proptest_emoji(bytes in prop::array::uniform6(0u8..)) {
let numbers = bytes_to_emoji_index(bytes.to_vec());
for number in numbers {
prop_assert!(number < 64);
}
}
}
proptest! {
#[test]
fn proptest_decimals(bytes in prop::array::uniform5(0u8..)) {
let (first, second, third) = bytes_to_decimal(bytes.to_vec());
prop_assert!((1000..=9191).contains(&first));
prop_assert!((1000..=9191).contains(&second));
prop_assert!((1000..=9191).contains(&third));
}
}
}
@@ -0,0 +1,969 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod helpers;
mod sas_state;
#[cfg(test)]
use std::time::Instant;
use std::sync::{Arc, Mutex};
use tracing::{error, info, trace, warn};
use matrix_sdk_common::{
api::r0::keys::upload_signatures::Request as SignatureUploadRequest,
events::{
key::verification::{
accept::AcceptToDeviceEventContent, cancel::CancelCode, mac::MacToDeviceEventContent,
start::StartToDeviceEventContent,
},
AnyToDeviceEvent, AnyToDeviceEventContent, ToDeviceEvent,
},
identifiers::{DeviceId, UserId},
};
use crate::{
error::SignatureError,
identities::{LocalTrust, ReadOnlyDevice, UserIdentities},
olm::PrivateCrossSigningIdentity,
store::{Changes, CryptoStore, CryptoStoreError, DeviceChanges},
ReadOnlyAccount, ToDeviceRequest,
};
pub use helpers::content_to_request;
use sas_state::{
Accepted, Canceled, Confirmed, Created, Done, KeyReceived, MacReceived, SasState, Started,
};
#[derive(Debug)]
/// A result of a verification flow.
pub enum VerificationResult {
/// The verification succeeded, nothing needs to be done.
Ok,
/// The verification was canceled.
Cancel(ToDeviceRequest),
/// The verification is done and has signatures that need to be uploaded.
SignatureUpload(SignatureUploadRequest),
}
#[derive(Clone, Debug)]
/// Short authentication string object.
pub struct Sas {
inner: Arc<Mutex<InnerSas>>,
store: Arc<Box<dyn CryptoStore>>,
account: ReadOnlyAccount,
private_identity: PrivateCrossSigningIdentity,
other_device: ReadOnlyDevice,
other_identity: Option<UserIdentities>,
flow_id: Arc<str>,
}
impl Sas {
/// Get our own user id.
pub fn user_id(&self) -> &UserId {
self.account.user_id()
}
/// Get our own device id.
pub fn device_id(&self) -> &DeviceId {
self.account.device_id()
}
/// Get the user id of the other side.
pub fn other_user_id(&self) -> &UserId {
self.other_device.user_id()
}
/// Get the device id of the other side.
pub fn other_device_id(&self) -> &DeviceId {
self.other_device.device_id()
}
/// Get the device of the other user.
pub fn other_device(&self) -> ReadOnlyDevice {
self.other_device.clone()
}
/// Get the unique ID that identifies this SAS verification flow.
pub fn flow_id(&self) -> &str {
&self.flow_id
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn set_creation_time(&self, time: Instant) {
self.inner.lock().unwrap().set_creation_time(time)
}
/// Start a new SAS auth flow with the given device.
///
/// # Arguments
///
/// * `account` - Our own account.
///
/// * `other_device` - The other device which we are going to verify.
///
/// Returns the new `Sas` object and a `StartEventContent` that needs to be
/// sent out through the server to the other device.
pub(crate) fn start(
account: ReadOnlyAccount,
private_identity: PrivateCrossSigningIdentity,
other_device: ReadOnlyDevice,
store: Arc<Box<dyn CryptoStore>>,
other_identity: Option<UserIdentities>,
) -> (Sas, StartToDeviceEventContent) {
let (inner, content) = InnerSas::start(
account.clone(),
other_device.clone(),
other_identity.clone(),
);
let flow_id = inner.verification_flow_id();
let sas = Sas {
inner: Arc::new(Mutex::new(inner)),
account,
private_identity,
store,
other_device,
flow_id,
other_identity,
};
(sas, content)
}
/// Create a new Sas object from a m.key.verification.start request.
///
/// # Arguments
///
/// * `account` - Our own account.
///
/// * `other_device` - The other device which we are going to verify.
///
/// * `event` - The m.key.verification.start event that was sent to us by
/// the other side.
pub(crate) fn from_start_event(
account: ReadOnlyAccount,
private_identity: PrivateCrossSigningIdentity,
other_device: ReadOnlyDevice,
store: Arc<Box<dyn CryptoStore>>,
event: &ToDeviceEvent<StartToDeviceEventContent>,
other_identity: Option<UserIdentities>,
) -> Result<Sas, AnyToDeviceEventContent> {
let inner = InnerSas::from_start_event(
account.clone(),
other_device.clone(),
event,
other_identity.clone(),
)?;
let flow_id = inner.verification_flow_id();
Ok(Sas {
inner: Arc::new(Mutex::new(inner)),
account,
private_identity,
other_device,
other_identity,
store,
flow_id,
})
}
/// Accept the SAS verification.
///
/// This does nothing if the verification was already accepted, otherwise it
/// returns an `AcceptEventContent` that needs to be sent out.
pub fn accept(&self) -> Option<ToDeviceRequest> {
self.inner.lock().unwrap().accept().map(|c| {
let content = AnyToDeviceEventContent::KeyVerificationAccept(c);
self.content_to_request(content)
})
}
/// Confirm the Sas verification.
///
/// This confirms that the short auth strings match on both sides.
///
/// Does nothing if we're not in a state where we can confirm the short auth
/// string, otherwise returns a `MacEventContent` that needs to be sent to
/// the server.
pub async fn confirm(
&self,
) -> Result<(Option<ToDeviceRequest>, Option<SignatureUploadRequest>), CryptoStoreError> {
let (content, done) = {
let mut guard = self.inner.lock().unwrap();
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.confirm();
*guard = sas;
(content, guard.is_done())
};
let mac_request = content
.map(|c| self.content_to_request(AnyToDeviceEventContent::KeyVerificationMac(c)));
if done {
match self.mark_as_done().await? {
VerificationResult::Cancel(r) => Ok((Some(r), None)),
VerificationResult::Ok => Ok((mac_request, None)),
VerificationResult::SignatureUpload(r) => Ok((mac_request, Some(r))),
}
} else {
Ok((mac_request, None))
}
}
pub(crate) async fn mark_as_done(&self) -> Result<VerificationResult, CryptoStoreError> {
if let Some(device) = self.mark_device_as_verified().await? {
let identity = self.mark_identity_as_verified().await?;
// We only sign devices of our own user here.
let signature_request = if device.user_id() == self.user_id() {
match self.private_identity.sign_device(&device).await {
Ok(r) => Some(r),
Err(SignatureError::MissingSigningKey) => {
warn!(
"Can't sign the device keys for {} {}, \
no private user signing key found",
device.user_id(),
device.device_id(),
);
None
}
Err(e) => {
error!(
"Error signing device keys for {} {} {:?}",
device.user_id(),
device.device_id(),
e
);
None
}
}
} else {
None
};
let mut changes = Changes {
devices: DeviceChanges {
changed: vec![device],
..Default::default()
},
..Default::default()
};
let identity_signature_request = if let Some(i) = identity {
// We only sign other users here.
let request = if let Some(i) = i.other() {
// Signing can fail if the user signing key is missing.
match self.private_identity.sign_user(&i).await {
Ok(r) => Some(r),
Err(SignatureError::MissingSigningKey) => {
warn!(
"Can't sign the public cross signing keys for {}, \
no private user signing key found",
i.user_id()
);
None
}
Err(e) => {
error!(
"Error signing the public cross signing keys for {} {:?}",
i.user_id(),
e
);
None
}
}
} else {
None
};
changes.identities.changed.push(i);
request
} else {
None
};
// If there are two signature upload requests, merge them. Otherwise
// use the one we have or None.
//
// Realistically at most one reuqest will be used but let's make
// this future proof.
let merged_request = if let Some(mut r) = signature_request {
if let Some(user_request) = identity_signature_request {
r.signed_keys.extend(user_request.signed_keys);
Some(r)
} else {
Some(r)
}
} else if let Some(r) = identity_signature_request {
Some(r)
} else {
None
};
// TODO store the request as well.
self.store.save_changes(changes).await?;
Ok(merged_request
.map(VerificationResult::SignatureUpload)
.unwrap_or(VerificationResult::Ok))
} else {
Ok(self
.cancel()
.map(VerificationResult::Cancel)
.unwrap_or(VerificationResult::Ok))
}
}
pub(crate) async fn mark_identity_as_verified(
&self,
) -> Result<Option<UserIdentities>, CryptoStoreError> {
// If there wasn't an identity available during the verification flow
// return early as there's nothing to do.
if self.other_identity.is_none() {
return Ok(None);
}
// TODO signal an error, e.g. when the identity got deleted so we don't
// verify/save the device either.
let identity = self.store.get_user_identity(self.other_user_id()).await?;
if let Some(identity) = identity {
if self
.other_identity
.as_ref()
.map_or(false, |i| i.master_key() == identity.master_key())
{
if self
.verified_identities()
.map_or(false, |i| i.contains(&identity))
{
trace!(
"Marking user identity of {} as verified.",
identity.user_id(),
);
if let UserIdentities::Own(i) = &identity {
i.mark_as_verified();
}
Ok(Some(identity))
} else {
info!(
"The interactive verification process didn't contain a \
MAC for the user identity of {} {:?}",
identity.user_id(),
self.verified_identities(),
);
Ok(None)
}
} else {
warn!(
"The master keys of {} have changed while an interactive \
verification was going on, not marking the identity as verified.",
identity.user_id(),
);
Ok(None)
}
} else {
info!(
"The identity for {} was deleted while an interactive \
verification was going on.",
self.other_user_id(),
);
Ok(None)
}
}
pub(crate) async fn mark_device_as_verified(
&self,
) -> Result<Option<ReadOnlyDevice>, CryptoStoreError> {
let device = self
.store
.get_device(self.other_user_id(), self.other_device_id())
.await?;
if let Some(device) = device {
if device.keys() == self.other_device.keys() {
if self
.verified_devices()
.map_or(false, |v| v.contains(&device))
{
trace!(
"Marking device {} {} as verified.",
device.user_id(),
device.device_id()
);
device.set_trust_state(LocalTrust::Verified);
Ok(Some(device))
} else {
info!(
"The interactive verification process didn't contain a \
MAC for the device {} {}",
device.user_id(),
device.device_id()
);
Ok(None)
}
} else {
warn!(
"The device keys of {} {} have changed while an interactive \
verification was going on, not marking the device as verified.",
device.user_id(),
device.device_id()
);
Ok(None)
}
} else {
let device = self.other_device();
info!(
"The device {} {} was deleted while an interactive \
verification was going on.",
device.user_id(),
device.device_id()
);
Ok(None)
}
}
/// Cancel the verification.
///
/// This cancels the verification with the `CancelCode::User`.
///
/// Returns None if the `Sas` object is already in a canceled state,
/// otherwise it returns a request that needs to be sent out.
pub fn cancel(&self) -> Option<ToDeviceRequest> {
let mut guard = self.inner.lock().unwrap();
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.cancel(CancelCode::User);
*guard = sas;
content.map(|c| self.content_to_request(c))
}
pub(crate) fn cancel_if_timed_out(&self) -> Option<ToDeviceRequest> {
if self.is_canceled() || self.is_done() {
None
} else if self.timed_out() {
let mut guard = self.inner.lock().unwrap();
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.cancel(CancelCode::Timeout);
*guard = sas;
content.map(|c| self.content_to_request(c))
} else {
None
}
}
/// Has the SAS verification flow timed out.
pub fn timed_out(&self) -> bool {
self.inner.lock().unwrap().timed_out()
}
/// Are we in a state where we can show the short auth string.
pub fn can_be_presented(&self) -> bool {
self.inner.lock().unwrap().can_be_presented()
}
/// Is the SAS flow done.
pub fn is_done(&self) -> bool {
self.inner.lock().unwrap().is_done()
}
/// Is the SAS flow canceled.
pub fn is_canceled(&self) -> bool {
self.inner.lock().unwrap().is_canceled()
}
/// Get the emoji version of the short auth string.
///
/// Returns None if we can't yet present the short auth string, otherwise a
/// Vec of tuples with the emoji and description.
pub fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
self.inner.lock().unwrap().emoji()
}
/// Get the decimal version of the short auth string.
///
/// Returns None if we can't yet present the short auth string, otherwise a
/// tuple containing three 4-digit integers that represent the short auth
/// string.
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
self.inner.lock().unwrap().decimals()
}
pub(crate) fn receive_event(
&self,
event: &mut AnyToDeviceEvent,
) -> Option<AnyToDeviceEventContent> {
let mut guard = self.inner.lock().unwrap();
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.receive_event(event);
*guard = sas;
content
}
pub(crate) fn verified_devices(&self) -> Option<Arc<[ReadOnlyDevice]>> {
self.inner.lock().unwrap().verified_devices()
}
pub(crate) fn verified_identities(&self) -> Option<Arc<[UserIdentities]>> {
self.inner.lock().unwrap().verified_identities()
}
pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest {
content_to_request(self.other_user_id(), self.other_device_id(), content)
}
}
#[derive(Clone, Debug)]
enum InnerSas {
Created(SasState<Created>),
Started(SasState<Started>),
Accepted(SasState<Accepted>),
KeyRecieved(SasState<KeyReceived>),
Confirmed(SasState<Confirmed>),
MacReceived(SasState<MacReceived>),
Done(SasState<Done>),
Canceled(SasState<Canceled>),
}
impl InnerSas {
fn start(
account: ReadOnlyAccount,
other_device: ReadOnlyDevice,
other_identity: Option<UserIdentities>,
) -> (InnerSas, StartToDeviceEventContent) {
let sas = SasState::<Created>::new(account, other_device, other_identity);
let content = sas.as_content();
(InnerSas::Created(sas), content)
}
fn from_start_event(
account: ReadOnlyAccount,
other_device: ReadOnlyDevice,
event: &ToDeviceEvent<StartToDeviceEventContent>,
other_identity: Option<UserIdentities>,
) -> Result<InnerSas, AnyToDeviceEventContent> {
match SasState::<Started>::from_start_event(account, other_device, event, other_identity) {
Ok(s) => Ok(InnerSas::Started(s)),
Err(s) => Err(s.as_content()),
}
}
fn accept(&self) -> Option<AcceptToDeviceEventContent> {
if let InnerSas::Started(s) = self {
Some(s.as_content())
} else {
None
}
}
#[cfg(test)]
#[allow(dead_code)]
fn set_creation_time(&mut self, time: Instant) {
match self {
InnerSas::Created(s) => s.set_creation_time(time),
InnerSas::Started(s) => s.set_creation_time(time),
InnerSas::Canceled(s) => s.set_creation_time(time),
InnerSas::Accepted(s) => s.set_creation_time(time),
InnerSas::KeyRecieved(s) => s.set_creation_time(time),
InnerSas::Confirmed(s) => s.set_creation_time(time),
InnerSas::MacReceived(s) => s.set_creation_time(time),
InnerSas::Done(s) => s.set_creation_time(time),
}
}
fn cancel(self, code: CancelCode) -> (InnerSas, Option<AnyToDeviceEventContent>) {
let sas = match self {
InnerSas::Created(s) => s.cancel(code),
InnerSas::Started(s) => s.cancel(code),
InnerSas::Accepted(s) => s.cancel(code),
InnerSas::KeyRecieved(s) => s.cancel(code),
InnerSas::MacReceived(s) => s.cancel(code),
_ => return (self, None),
};
let content = sas.as_content();
(InnerSas::Canceled(sas), Some(content))
}
fn confirm(self) -> (InnerSas, Option<MacToDeviceEventContent>) {
match self {
InnerSas::KeyRecieved(s) => {
let sas = s.confirm();
let content = sas.as_content();
(InnerSas::Confirmed(sas), Some(content))
}
InnerSas::MacReceived(s) => {
let sas = s.confirm();
let content = sas.as_content();
(InnerSas::Done(sas), Some(content))
}
_ => (self, None),
}
}
fn receive_event(
self,
event: &mut AnyToDeviceEvent,
) -> (InnerSas, Option<AnyToDeviceEventContent>) {
match event {
AnyToDeviceEvent::KeyVerificationAccept(e) => {
if let InnerSas::Created(s) = self {
match s.into_accepted(e) {
Ok(s) => {
let content = s.as_content();
(
InnerSas::Accepted(s),
Some(AnyToDeviceEventContent::KeyVerificationKey(content)),
)
}
Err(s) => {
let content = s.as_content();
(InnerSas::Canceled(s), Some(content))
}
}
} else {
(self, None)
}
}
AnyToDeviceEvent::KeyVerificationKey(e) => match self {
InnerSas::Accepted(s) => match s.into_key_received(e) {
Ok(s) => (InnerSas::KeyRecieved(s), None),
Err(s) => {
let content = s.as_content();
(InnerSas::Canceled(s), Some(content))
}
},
InnerSas::Started(s) => match s.into_key_received(e) {
Ok(s) => {
let content = s.as_content();
(
InnerSas::KeyRecieved(s),
Some(AnyToDeviceEventContent::KeyVerificationKey(content)),
)
}
Err(s) => {
let content = s.as_content();
(InnerSas::Canceled(s), Some(content))
}
},
_ => (self, None),
},
AnyToDeviceEvent::KeyVerificationMac(e) => match self {
InnerSas::KeyRecieved(s) => match s.into_mac_received(e) {
Ok(s) => (InnerSas::MacReceived(s), None),
Err(s) => {
let content = s.as_content();
(InnerSas::Canceled(s), Some(content))
}
},
InnerSas::Confirmed(s) => match s.into_done(e) {
Ok(s) => (InnerSas::Done(s), None),
Err(s) => {
let content = s.as_content();
(InnerSas::Canceled(s), Some(content))
}
},
_ => (self, None),
},
_ => (self, None),
}
}
fn can_be_presented(&self) -> bool {
matches!(self, InnerSas::KeyRecieved(_) | InnerSas::MacReceived(_))
}
fn is_done(&self) -> bool {
matches!(self, InnerSas::Done(_))
}
fn is_canceled(&self) -> bool {
matches!(self, InnerSas::Canceled(_))
}
fn timed_out(&self) -> bool {
match self {
InnerSas::Created(s) => s.timed_out(),
InnerSas::Started(s) => s.timed_out(),
InnerSas::Canceled(s) => s.timed_out(),
InnerSas::Accepted(s) => s.timed_out(),
InnerSas::KeyRecieved(s) => s.timed_out(),
InnerSas::Confirmed(s) => s.timed_out(),
InnerSas::MacReceived(s) => s.timed_out(),
InnerSas::Done(s) => s.timed_out(),
}
}
fn verification_flow_id(&self) -> Arc<str> {
match self {
InnerSas::Created(s) => s.verification_flow_id.clone(),
InnerSas::Started(s) => s.verification_flow_id.clone(),
InnerSas::Canceled(s) => s.verification_flow_id.clone(),
InnerSas::Accepted(s) => s.verification_flow_id.clone(),
InnerSas::KeyRecieved(s) => s.verification_flow_id.clone(),
InnerSas::Confirmed(s) => s.verification_flow_id.clone(),
InnerSas::MacReceived(s) => s.verification_flow_id.clone(),
InnerSas::Done(s) => s.verification_flow_id.clone(),
}
}
fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
match self {
InnerSas::KeyRecieved(s) => Some(s.get_emoji()),
InnerSas::MacReceived(s) => Some(s.get_emoji()),
_ => None,
}
}
fn decimals(&self) -> Option<(u16, u16, u16)> {
match self {
InnerSas::KeyRecieved(s) => Some(s.get_decimal()),
InnerSas::MacReceived(s) => Some(s.get_decimal()),
_ => None,
}
}
fn verified_devices(&self) -> Option<Arc<[ReadOnlyDevice]>> {
if let InnerSas::Done(s) = self {
Some(s.verified_devices())
} else {
None
}
}
fn verified_identities(&self) -> Option<Arc<[UserIdentities]>> {
if let InnerSas::Done(s) = self {
Some(s.verified_identities())
} else {
None
}
}
}
#[cfg(test)]
mod test {
use std::{convert::TryFrom, sync::Arc};
use matrix_sdk_common::{
events::{EventContent, ToDeviceEvent},
identifiers::{DeviceId, UserId},
};
use crate::{
olm::PrivateCrossSigningIdentity,
store::{CryptoStore, MemoryStore},
verification::test::{get_content_from_request, wrap_any_to_device_content},
ReadOnlyAccount, ReadOnlyDevice,
};
use super::{Accepted, Created, Sas, SasState, Started};
fn alice_id() -> UserId {
UserId::try_from("@alice:example.org").unwrap()
}
fn alice_device_id() -> Box<DeviceId> {
"JLAFKJWSCS".into()
}
fn bob_id() -> UserId {
UserId::try_from("@bob:example.org").unwrap()
}
fn bob_device_id() -> Box<DeviceId> {
"BOBDEVCIE".into()
}
fn wrap_to_device_event<C: EventContent>(sender: &UserId, content: C) -> ToDeviceEvent<C> {
ToDeviceEvent {
sender: sender.clone(),
content,
}
}
async fn get_sas_pair() -> (SasState<Created>, SasState<Started>) {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let alice_device = ReadOnlyDevice::from_account(&alice).await;
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
let bob_device = ReadOnlyDevice::from_account(&bob).await;
let alice_sas = SasState::<Created>::new(alice.clone(), bob_device, None);
let start_content = alice_sas.as_content();
let event = wrap_to_device_event(alice_sas.user_id(), start_content);
let bob_sas =
SasState::<Started>::from_start_event(bob.clone(), alice_device, &event, None);
(alice_sas, bob_sas.unwrap())
}
#[tokio::test]
async fn create_sas() {
let (_, _) = get_sas_pair().await;
}
#[tokio::test]
async fn sas_accept() {
let (alice, bob) = get_sas_pair().await;
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
alice.into_accepted(&event).unwrap();
}
#[tokio::test]
async fn sas_key_share() {
let (alice, bob) = get_sas_pair().await;
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
let alice: SasState<Accepted> = alice.into_accepted(&event).unwrap();
let mut event = wrap_to_device_event(alice.user_id(), alice.as_content());
let bob = bob.into_key_received(&mut event).unwrap();
let mut event = wrap_to_device_event(bob.user_id(), bob.as_content());
let alice = alice.into_key_received(&mut event).unwrap();
assert_eq!(alice.get_decimal(), bob.get_decimal());
assert_eq!(alice.get_emoji(), bob.get_emoji());
}
#[tokio::test]
async fn sas_full() {
let (alice, bob) = get_sas_pair().await;
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
let alice: SasState<Accepted> = alice.into_accepted(&event).unwrap();
let mut event = wrap_to_device_event(alice.user_id(), alice.as_content());
let bob = bob.into_key_received(&mut event).unwrap();
let mut event = wrap_to_device_event(bob.user_id(), bob.as_content());
let alice = alice.into_key_received(&mut event).unwrap();
assert_eq!(alice.get_decimal(), bob.get_decimal());
assert_eq!(alice.get_emoji(), bob.get_emoji());
let bob = bob.confirm();
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
let alice = alice.into_mac_received(&event).unwrap();
assert!(!alice.get_emoji().is_empty());
let alice = alice.confirm();
let event = wrap_to_device_event(alice.user_id(), alice.as_content());
let bob = bob.into_done(&event).unwrap();
assert!(bob.verified_devices().contains(&bob.other_device()));
assert!(alice.verified_devices().contains(&alice.other_device()));
}
#[tokio::test]
async fn sas_wrapper_full() {
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
let alice_device = ReadOnlyDevice::from_account(&alice).await;
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
let bob_device = ReadOnlyDevice::from_account(&bob).await;
let alice_store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
let bob_store = MemoryStore::new();
bob_store.save_devices(vec![alice_device.clone()]).await;
let bob_store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(bob_store));
let (alice, content) = Sas::start(
alice,
PrivateCrossSigningIdentity::empty(alice_id()),
bob_device,
alice_store,
None,
);
let event = wrap_to_device_event(alice.user_id(), content);
let bob = Sas::from_start_event(
bob,
PrivateCrossSigningIdentity::empty(bob_id()),
alice_device,
bob_store,
&event,
None,
)
.unwrap();
let mut event = wrap_any_to_device_content(
bob.user_id(),
get_content_from_request(&bob.accept().unwrap()),
);
let content = alice.receive_event(&mut event);
assert!(!alice.can_be_presented());
assert!(!bob.can_be_presented());
let mut event = wrap_any_to_device_content(alice.user_id(), content.unwrap());
let mut event =
wrap_any_to_device_content(bob.user_id(), bob.receive_event(&mut event).unwrap());
assert!(bob.can_be_presented());
alice.receive_event(&mut event);
assert!(alice.can_be_presented());
assert_eq!(alice.emoji().unwrap(), bob.emoji().unwrap());
assert_eq!(alice.decimals().unwrap(), bob.decimals().unwrap());
let mut event = wrap_any_to_device_content(
alice.user_id(),
get_content_from_request(&alice.confirm().await.unwrap().0.unwrap()),
);
bob.receive_event(&mut event);
let mut event = wrap_any_to_device_content(
bob.user_id(),
get_content_from_request(&bob.confirm().await.unwrap().0.unwrap()),
);
alice.receive_event(&mut event);
assert!(alice
.verified_devices()
.unwrap()
.contains(&alice.other_device()));
assert!(bob
.verified_devices()
.unwrap()
.contains(&bob.other_device()));
}
}
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "Helpers to write tests for the Matrix SDK"
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,10 +8,12 @@ license = "Apache-2.0"
name = "matrix-sdk-test"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.2.0"
[dependencies]
serde_json = "1.0.53"
http = "0.2.1"
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
serde_json = "1.0.61"
http = "0.2.2"
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
matrix-sdk-test-macros = { version = "0.1.0", path = "../matrix_sdk_test_macros" }
lazy_static = "1.4.0"
serde = "1.0.118"
+153 -128
View File
@@ -1,26 +1,24 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use std::panic;
use std::{collections::HashMap, convert::TryFrom, panic};
use http::Response;
use matrix_sdk_common::api::r0::sync::sync_events::Response as SyncResponse;
use matrix_sdk_common::events::{
collections::{
all::{RoomEvent, StateEvent},
only::Event,
use matrix_sdk_common::{
api::r0::sync::sync_events::Response as SyncResponse,
events::{
presence::PresenceEvent, AnyBasicEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent,
AnySyncStateEvent,
},
presence::PresenceEvent,
stripped::AnyStrippedStateEvent,
EventJson, TryFromRaw,
identifiers::{room_id, RoomId},
};
use matrix_sdk_common::identifiers::RoomId;
use serde_json::Value as JsonValue;
pub use matrix_sdk_test_macros::async_test;
pub mod test_json;
/// Embedded event files
#[derive(Debug)]
pub enum EventsFile {
pub enum EventsJson {
Alias,
Aliases,
Create,
@@ -28,6 +26,7 @@ pub enum EventsFile {
HistoryVisibility,
JoinRules,
Member,
MemberNameChange,
MessageEmote,
MessageNotice,
MessageText,
@@ -44,181 +43,185 @@ pub enum EventsFile {
Typing,
}
/// Easily create events to stream into either a Client or a `Room` for testing.
/// The `EventBuilder` struct can be used to easily generate valid sync responses for testing.
/// These can be then fed into either `Client` or `Room`.
///
/// It supports generated a number of canned events, such as a member entering a room, his power
/// level and display name changing and similar. It also supports insertion of custom events in the
/// form of `EventsJson` values.
///
/// **Important** You *must* use the *same* builder when sending multiple sync responses to
/// a single client. Otherwise, the subsequent responses will be *ignored* by the client because
/// the `next_batch` sync token will not be rotated properly.
///
/// # Example usage
///
/// ```rust
/// use matrix_sdk_test::{EventBuilder, EventsJson};
///
/// let mut builder = EventBuilder::new();
///
/// // response1 now contains events that add an example member to the room and change their power
/// // level
/// let response1 = builder
/// .add_room_event(EventsJson::Member)
/// .add_room_event(EventsJson::PowerLevels)
/// .build_sync_response();
///
/// // response2 is now empty (nothing changed)
/// let response2 = builder.build_sync_response();
///
/// // response3 contains a display name change for member example
/// let response3 = builder
/// .add_room_event(EventsJson::MemberNameChange)
/// .build_sync_response();
/// ```
#[derive(Default)]
pub struct EventBuilder {
/// The events that determine the state of a `Room`.
joined_room_events: HashMap<RoomId, Vec<RoomEvent>>,
joined_room_events: HashMap<RoomId, Vec<AnySyncRoomEvent>>,
/// The events that determine the state of a `Room`.
invited_room_events: HashMap<RoomId, Vec<AnyStrippedStateEvent>>,
invited_room_events: HashMap<RoomId, Vec<AnySyncStateEvent>>,
/// The events that determine the state of a `Room`.
left_room_events: HashMap<RoomId, Vec<RoomEvent>>,
left_room_events: HashMap<RoomId, Vec<AnySyncRoomEvent>>,
/// The presence events that determine the presence state of a `RoomMember`.
presence_events: Vec<PresenceEvent>,
/// The state events that determine the state of a `Room`.
state_events: Vec<StateEvent>,
state_events: Vec<AnySyncStateEvent>,
/// The ephemeral room events that determine the state of a `Room`.
ephemeral: Vec<Event>,
ephemeral: Vec<AnySyncEphemeralRoomEvent>,
/// The account data events that determine the state of a `Room`.
account_data: Vec<Event>,
account_data: Vec<AnyBasicEvent>,
/// Internal counter to enable the `prev_batch` and `next_batch` of each sync response to vary.
batch_counter: i64,
}
impl EventBuilder {
pub fn new() -> Self {
let builder: EventBuilder = Default::default();
builder
}
/// Add an event to the room events `Vec`.
pub fn add_ephemeral<Ev: TryFromRaw>(
mut self,
file: EventsFile,
variant: fn(Ev) -> Event,
) -> Self {
let val: &str = match file {
EventsFile::Typing => include_str!("../test_data/events/typing.json"),
_ => panic!("unknown ephemeral event file {:?}", file),
pub fn add_ephemeral(&mut self, json: EventsJson) -> &mut Self {
let val: &JsonValue = match json {
EventsJson::Typing => &test_json::TYPING,
_ => panic!("unknown ephemeral event {:?}", json),
};
let event = serde_json::from_str::<EventJson<Ev>>(&val)
.unwrap()
.deserialize()
.unwrap();
self.ephemeral.push(variant(event));
let event = serde_json::from_value::<AnySyncEphemeralRoomEvent>(val.clone()).unwrap();
self.ephemeral.push(event);
self
}
/// Add an event to the room events `Vec`.
#[allow(clippy::match_single_binding, unused)]
pub fn add_account<Ev: TryFromRaw>(
mut self,
file: EventsFile,
variant: fn(Ev) -> Event,
) -> Self {
let val: &str = match file {
_ => panic!("unknown account event file {:?}", file),
pub fn add_account(&mut self, json: EventsJson) -> &mut Self {
let val: &JsonValue = match json {
_ => panic!("unknown account event {:?}", json),
};
let event = serde_json::from_str::<EventJson<Ev>>(&val)
.unwrap()
.deserialize()
.unwrap();
self.account_data.push(variant(event));
let event = serde_json::from_value::<AnyBasicEvent>(val.clone()).unwrap();
self.account_data.push(event);
self
}
/// Add an event to the room events `Vec`.
pub fn add_room_event<Ev: TryFromRaw>(
mut self,
file: EventsFile,
variant: fn(Ev) -> RoomEvent,
) -> Self {
let val = match file {
EventsFile::Member => include_str!("../test_data/events/member.json"),
EventsFile::PowerLevels => include_str!("../test_data/events/power_levels.json"),
_ => panic!("unknown room event file {:?}", file),
pub fn add_room_event(&mut self, json: EventsJson) -> &mut Self {
let val: &JsonValue = match json {
EventsJson::Member => &test_json::MEMBER,
EventsJson::MemberNameChange => &test_json::MEMBER_NAME_CHANGE,
EventsJson::PowerLevels => &test_json::POWER_LEVELS,
_ => panic!("unknown room event json {:?}", json),
};
let event = serde_json::from_str::<EventJson<Ev>>(&val)
.unwrap()
.deserialize()
.unwrap();
self.add_joined_event(
&RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap(),
variant(event),
);
let event = serde_json::from_value::<AnySyncRoomEvent>(val.clone()).unwrap();
self.add_joined_event(&room_id!("!SVkFJHzfwvuaIEawgC:localhost"), event);
self
}
pub fn add_custom_joined_event<Ev: TryFromRaw>(
mut self,
pub fn add_custom_joined_event(
&mut self,
room_id: &RoomId,
event: serde_json::Value,
variant: fn(Ev) -> RoomEvent,
) -> Self {
let event = serde_json::from_value::<EventJson<Ev>>(event)
.unwrap()
.deserialize()
.unwrap();
self.add_joined_event(room_id, variant(event));
) -> &mut Self {
let event = serde_json::from_value::<AnySyncRoomEvent>(event).unwrap();
self.add_joined_event(room_id, event);
self
}
fn add_joined_event(&mut self, room_id: &RoomId, event: RoomEvent) {
fn add_joined_event(&mut self, room_id: &RoomId, event: AnySyncRoomEvent) {
self.joined_room_events
.entry(room_id.clone())
.or_insert_with(Vec::new)
.push(event);
}
pub fn add_custom_invited_event<Ev: TryFromRaw>(
mut self,
pub fn add_custom_invited_event(
&mut self,
room_id: &RoomId,
event: serde_json::Value,
variant: fn(Ev) -> AnyStrippedStateEvent,
) -> Self {
let event = serde_json::from_value::<EventJson<Ev>>(event)
.unwrap()
.deserialize()
.unwrap();
) -> &mut Self {
let event = serde_json::from_value::<AnySyncStateEvent>(event).unwrap();
self.invited_room_events
.entry(room_id.clone())
.or_insert_with(Vec::new)
.push(variant(event));
.push(event);
self
}
pub fn add_custom_left_event<Ev: TryFromRaw>(
mut self,
pub fn add_custom_left_event(
&mut self,
room_id: &RoomId,
event: serde_json::Value,
variant: fn(Ev) -> RoomEvent,
) -> Self {
let event = serde_json::from_value::<EventJson<Ev>>(event)
.unwrap()
.deserialize()
.unwrap();
) -> &mut Self {
let event = serde_json::from_value::<AnySyncRoomEvent>(event).unwrap();
self.left_room_events
.entry(room_id.clone())
.or_insert_with(Vec::new)
.push(variant(event));
.push(event);
self
}
/// Add a state event to the state events `Vec`.
pub fn add_state_event<Ev: TryFromRaw>(
mut self,
file: EventsFile,
variant: fn(Ev) -> StateEvent,
) -> Self {
let val = match file {
EventsFile::Alias => include_str!("../test_data/events/alias.json"),
EventsFile::Aliases => include_str!("../test_data/events/aliases.json"),
EventsFile::Name => include_str!("../test_data/events/name.json"),
_ => panic!("unknown state event file {:?}", file),
pub fn add_state_event(&mut self, json: EventsJson) -> &mut Self {
let val: &JsonValue = match json {
EventsJson::Alias => &test_json::ALIAS,
EventsJson::Aliases => &test_json::ALIASES,
EventsJson::Name => &test_json::NAME,
EventsJson::Member => &test_json::MEMBER,
EventsJson::PowerLevels => &test_json::POWER_LEVELS,
_ => panic!("unknown state event {:?}", json),
};
let event = serde_json::from_str::<EventJson<Ev>>(&val)
.unwrap()
.deserialize()
.unwrap();
self.state_events.push(variant(event));
let event = serde_json::from_value::<AnySyncStateEvent>(val.clone()).unwrap();
self.state_events.push(event);
self
}
/// Add an presence event to the presence events `Vec`.
pub fn add_presence_event(mut self, file: EventsFile) -> Self {
let val = match file {
EventsFile::Presence => include_str!("../test_data/events/presence.json"),
_ => panic!("unknown presence event file {:?}", file),
pub fn add_presence_event(&mut self, json: EventsJson) -> &mut Self {
let val: &JsonValue = match json {
EventsJson::Presence => &test_json::PRESENCE,
_ => panic!("unknown presence event {:?}", json),
};
let event = serde_json::from_str::<EventJson<PresenceEvent>>(&val)
.unwrap()
.deserialize()
.unwrap();
let event = serde_json::from_value::<PresenceEvent>(val.clone()).unwrap();
self.presence_events.push(event);
self
}
/// Consumes `ResponseBuilder and returns SyncResponse.
pub fn build_sync_response(mut self) -> SyncResponse {
let main_room_id = RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap();
/// Builds a `SyncResponse` containing the events we queued so far. The next response returned
/// by `build_sync_response` will then be empty if no further events were queued.
pub fn build_sync_response(&mut self) -> SyncResponse {
let main_room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
// First time building a sync response, so initialize the `prev_batch` to a default one.
let prev_batch = self.generate_sync_token();
self.batch_counter += 1;
let next_batch = self.generate_sync_token();
// TODO generalize this.
let joined_room = serde_json::json!({
@@ -235,7 +238,7 @@ impl EventBuilder {
"timeline": {
"events": self.joined_room_events.remove(&main_room_id).unwrap_or_default(),
"limited": true,
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
"prev_batch": prev_batch
},
"unread_notifications": {
"highlight_count": 0,
@@ -262,7 +265,7 @@ impl EventBuilder {
"timeline": {
"events": events,
"limited": true,
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
"prev_batch": prev_batch
},
"unread_notifications": {
"highlight_count": 0,
@@ -282,7 +285,7 @@ impl EventBuilder {
"timeline": {
"events": events,
"limited": false,
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
"prev_batch": prev_batch
},
});
left_rooms.insert(room_id, room);
@@ -302,7 +305,7 @@ impl EventBuilder {
let body = serde_json::json! {
{
"device_one_time_keys_count": {},
"next_batch": "s526_47314_0_7_1_1_1_11444_1",
"next_batch": next_batch,
"device_lists": {
"changed": [],
"left": []
@@ -323,29 +326,51 @@ impl EventBuilder {
let response = Response::builder()
.body(serde_json::to_vec(&body).unwrap())
.unwrap();
// Clear state so that the next sync response will be empty if nothing was added.
self.clear();
SyncResponse::try_from(response).unwrap()
}
fn generate_sync_token(&self) -> String {
format!("t392-516_47314_0_7_1_1_1_11444_{}", self.batch_counter)
}
pub fn clear(&mut self) {
self.account_data.clear();
self.ephemeral.clear();
self.invited_room_events.clear();
self.joined_room_events.clear();
self.left_room_events.clear();
self.presence_events.clear();
self.state_events.clear();
}
}
/// Embedded sync reponse files
pub enum SyncResponseFile {
All,
Default,
DefaultWithSummary,
Invite,
Leave,
Voip,
}
/// Get specific API responses for testing
pub fn sync_response(kind: SyncResponseFile) -> SyncResponse {
let data = match kind {
SyncResponseFile::Default => include_bytes!("../test_data/sync.json").to_vec(),
SyncResponseFile::DefaultWithSummary => {
include_bytes!("../test_data/sync_with_summary.json").to_vec()
}
SyncResponseFile::Invite => include_bytes!("../test_data/invite_sync.json").to_vec(),
SyncResponseFile::Leave => include_bytes!("../test_data/leave_sync.json").to_vec(),
let data: &JsonValue = match kind {
SyncResponseFile::All => &test_json::MORE_SYNC,
SyncResponseFile::Default => &test_json::SYNC,
SyncResponseFile::DefaultWithSummary => &test_json::DEFAULT_SYNC_SUMMARY,
SyncResponseFile::Invite => &test_json::INVITE_SYNC,
SyncResponseFile::Leave => &test_json::LEAVE_SYNC,
SyncResponseFile::Voip => &test_json::VOIP_SYNC,
};
let response = Response::builder().body(data.to_vec()).unwrap();
let response = Response::builder()
.body(data.to_string().as_bytes().to_vec())
.unwrap();
SyncResponse::try_from(response).unwrap()
}
+615
View File
@@ -0,0 +1,615 @@
use lazy_static::lazy_static;
use serde_json::{json, Value as JsonValue};
lazy_static! {
pub static ref ALIAS: JsonValue = json!({
"content": {
"alias": "#tutorial:localhost"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.canonical_alias",
"unsigned": {
"age": 703422
}
});
}
lazy_static! {
pub static ref ALIASES: JsonValue = json!({
"content": {
"aliases": [
"#tutorial:localhost"
]
},
"event_id": "$15139375516NUgtD:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "localhost",
"type": "m.room.aliases",
"unsigned": {
"age": 703422
}
});
}
lazy_static! {
pub static ref CREATE: JsonValue = json!({
"content": {
"creator": "@example:localhost",
"m.federate": true,
"room_version": "1"
},
"event_id": "$151957878228ekrDs:localhost",
"origin_server_ts": 15195787,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 139298
}
});
}
lazy_static! {
pub static ref FULLY_READ: JsonValue = json!({
"content": {
"event_id": "$someplace:example.org"
},
"room_id": "!somewhere:example.org",
"type": "m.fully_read"
});
}
lazy_static! {
pub static ref HISTORY_VISIBILITY: JsonValue = json!({
"content": {
"history_visibility": "world_readable"
},
"event_id": "$151957878235ricnD:localhost",
"origin_server_ts": 151957878,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.history_visibility",
"unsigned": {
"age": 1392989
}
});
}
lazy_static! {
pub static ref JOIN_RULES: JsonValue = json!({
"content": {
"join_rule": "public"
},
"event_id": "$151957878231iejdB:localhost",
"origin_server_ts": 151957878,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.join_rules",
"unsigned": {
"age": 1392989
}
});
}
lazy_static! {
pub static ref ROOM_MESSAGES: JsonValue = json!({
"chunk": [
{
"age": 1042,
"content": {
"body": "hello world",
"msgtype": "m.text"
},
"event_id": "$1444812213350496Caaaa:example.com",
"origin_server_ts": 1444812213737i64,
"room_id": "!Xq3620DUiqCaoxq:example.com",
"sender": "@alice:example.com",
"type": "m.room.message"
},
{
"age": 20123,
"content": {
"body": "the world is big",
"msgtype": "m.text"
},
"event_id": "$1444812213350496Cbbbb:example.com",
"origin_server_ts": 1444812194656i64,
"room_id": "!Xq3620DUiqCaoxq:example.com",
"sender": "@bob:example.com",
"type": "m.room.message"
},
{
"age": 50789,
"content": {
"name": "New room name"
},
"event_id": "$1444812213350496Ccccc:example.com",
"origin_server_ts": 1444812163990i64,
"prev_content": {
"name": "Old room name"
},
"room_id": "!Xq3620DUiqCaoxq:example.com",
"sender": "@bob:example.com",
"state_key": "",
"type": "m.room.name"
}
],
"end": "t47409-4357353_219380_26003_2265",
"start": "t47429-4392820_219380_26003_2265"
});
}
lazy_static! {
pub static ref KEYS_QUERY: JsonValue = json!({
"device_keys": {
"@alice:example.org": {
"JLAFKJWSCS": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "JLAFKJWSCS",
"user_id": "@alice:example.org",
"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"
}
}
}
},
"failures": {}
});
}
lazy_static! {
pub static ref KEYS_UPLOAD: JsonValue = json!({
"one_time_key_counts": {
"curve25519": 10,
"signed_curve25519": 20
}
});
}
lazy_static! {
pub static ref LOGIN: JsonValue = json!({
"access_token": "abc123",
"device_id": "GHTYAJCE",
"home_server": "matrix.org",
"user_id": "@cheeky_monkey:matrix.org"
});
}
lazy_static! {
pub static ref LOGIN_RESPONSE_ERR: JsonValue = json!({
"errcode": "M_FORBIDDEN",
"error": "Invalid password"
});
}
lazy_static! {
pub static ref LOGOUT: JsonValue = json!({});
}
lazy_static! {
pub static ref EVENT_ID: JsonValue = json!({
"event_id": "$h29iv0s8:example.com"
});
}
// TODO: Move `prev_content` into `unsigned` once ruma supports it
lazy_static! {
pub static ref MEMBER: JsonValue = json!({
"content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"event_id": "$151800140517rfvjc:localhost",
"membership": "join",
"origin_server_ts": 151800140,
"sender": "@example:localhost",
"state_key": "@example:localhost",
"type": "m.room.member",
"prev_content": {
"avatar_url": null,
"displayname": "example",
"membership": "invite"
},
"unsigned": {
"age": 297036,
"replaces_state": "$151800111315tsynI:localhost"
}
});
}
// TODO: Move `prev_content` into `unsigned` once ruma supports it
lazy_static! {
pub static ref MEMBER_NAME_CHANGE: JsonValue = json!({
"content": {
"avatar_url": null,
"displayname": "changed",
"membership": "join"
},
"event_id": "$151800234427abgho:localhost",
"membership": "join",
"origin_server_ts": 151800152,
"sender": "@example:localhost",
"state_key": "@example:localhost",
"type": "m.room.member",
"prev_content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"unsigned": {
"age": 297032,
"replaces_state": "$151800140517rfvjc:localhost"
}
});
}
lazy_static! {
pub static ref MESSAGE_EDIT: JsonValue = json!({
"content": {
"body": " * edited message",
"m.new_content": {
"body": "edited message",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$someeventid:foo",
"rel_type": "m.replace"
},
"msgtype": "m.text"
},
"event_id": "$eventid:foo",
"origin_server_ts": 159026265,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 85
}
});
}
lazy_static! {
pub static ref MESSAGE_EMOTE: JsonValue = json!({
"content": {
"body": "is dancing", "format": "org.matrix.custom.html",
"formatted_body": "<strong>is dancing</strong>",
"msgtype": "m.emote"
},
"event_id": "$152037280074GZeOm:localhost",
"origin_server_ts": 152037280,
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 598971
}
});
}
lazy_static! {
pub static ref MESSAGE_NOTICE: JsonValue = json!({
"origin_server_ts": 153356516,
"sender": "@_neb_github:matrix.org",
"event_id": "$153356516319138IHRIC:matrix.org",
"unsigned": {
"age": 743
},
"content": {
"body": "https://github.com/matrix-org/matrix-python-sdk/issues/266 : Consider allowing MatrixClient.__init__ to take sync_token kwarg",
"format": "org.matrix.custom.html",
"formatted_body": "<a href='https://github.com/matrix-org/matrix-python-sdk/pull/313'>313: nio wins!</a>",
"msgtype": "m.notice"
},
"type": "m.room.message",
"room_id": "!YHhmBTmGBHGQOlGpaZ:matrix.org"
});
}
lazy_static! {
pub static ref MESSAGE_TEXT: JsonValue = json!({
"content": {
"body": "is dancing", "format": "org.matrix.custom.html",
"formatted_body": "<strong>is dancing</strong>",
"msgtype": "m.text"
},
"event_id": "$152037280074GZeOm:localhost",
"origin_server_ts": 152037280,
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 598971
}
});
}
lazy_static! {
pub static ref NAME: JsonValue = json!({
"content": {
"name": "room name"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 703422
}
});
}
lazy_static! {
pub static ref POWER_LEVELS: JsonValue = json!({
"content": {
"ban": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.message": 25
},
"events_default": 0,
"invite": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@example:localhost": 100,
"@bob:localhost": 0
},
"users_default": 0
},
"event_id": "$15139375512JaHAW:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.power_levels",
"unsigned": {
"age": 703422
}
}
);
}
lazy_static! {
pub static ref PRESENCE: JsonValue = json!({
"content": {
"avatar_url": "mxc://localhost:wefuiwegh8742w",
"currently_active": false,
"last_active_ago": 1,
"presence": "online",
"status_msg": "Making cupcakes"
},
"sender": "@example:localhost",
"type": "m.presence"
});
}
lazy_static! {
pub static ref PUBLIC_ROOMS: JsonValue = json!({
"chunk": [
{
"aliases": [
"#murrays:cheese.bar"
],
"avatar_url": "mxc://bleeker.street/CHEDDARandBRIE",
"guest_can_join": false,
"name": "CHEESE",
"num_joined_members": 37,
"room_id": "!ol19s:bleecker.street",
"topic": "Tasty tasty cheese",
"world_readable": true
}
],
"next_batch": "p190q",
"prev_batch": "p1902",
"total_room_count_estimate": 115
});
}
lazy_static! {
pub static ref REGISTRATION_RESPONSE_ERR: JsonValue = json!({
"errcode": "M_FORBIDDEN",
"error": "Invalid password",
"completed": ["example.type.foo"],
"flows": [
{
"stages": ["example.type.foo", "example.type.bar"]
},
{
"stages": ["example.type.foo", "example.type.baz"]
}
],
"params": {
"example.type.baz": {
"example_key": "foobar"
}
},
"session": "xxxxxx"
});
}
lazy_static! {
pub static ref REACTION: JsonValue = json!({
"content": {
"m.relates_to": {
"event_id": "$MDitXXXXXXuBlpP7S6c6XXXXXXXC2HqZ3peV1NrV4PKA",
"key": "👍",
"rel_type": "m.annotation"
}
},
"event_id": "$QZn9xEXXXXXfd2tAGFH-XXgsffZlVMobk47Tl5Lpdtg",
"origin_server_ts": 159027581,
"sender": "@devinr528:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 85
}
});
}
lazy_static! {
pub static ref REDACTED_INVALID: JsonValue = json!({
"content": {},
"event_id": "$15275046980maRLj:localhost",
"origin_server_ts": 1527504698,
"sender": "@example:localhost",
"type": "m.room.message"
});
}
lazy_static! {
pub static ref REDACTED_STATE: JsonValue = json!({
"content": {},
"event_id": "$example_id:example.org",
"origin_server_ts": 153232493,
"sender": "@example:example.org",
"state_key": "test_state_key",
"type": "m.some.state",
"unsigned": {
"age": 3069315,
"redacted_because": {
"content": {},
"event_id": "$redaction_example_id:example.org",
"origin_server_ts": 153232494,
"redacts": "$example_id:example.org",
"sender": "@example:example:org",
"type": "m.room.redaction",
"unsigned": {"age": 30693147}
},
"redacted_by": "$redaction_example_id:example.org"
}
});
}
lazy_static! {
pub static ref REDACTED: JsonValue = json!({
"content": {},
"event_id": "$15275046980maRLj:localhost",
"origin_server_ts": 1527504698,
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 19334,
"redacted_because": {
"content": {},
"event_id": "$15275047031IXQRi:localhost",
"origin_server_ts": 1527504703,
"redacts": "$15275046980maRLj:localhost",
"sender": "@example:localhost",
"type": "m.room.redaction",
"unsigned": {
"age": 14523
}
},
"redacted_by": "$15275047031IXQRi:localhost"
}
});
}
lazy_static! {
pub static ref REDACTION: JsonValue = json!({
"content": {
"reason": "😀"
},
"event_id": "$151957878228ssqrJ:localhost",
"origin_server_ts": 151957878,
"sender": "@example:localhost",
"type": "m.room.redaction",
"redacts": "$151957878228ssqrj:localhost"
});
}
lazy_static! {
pub static ref ROOM_AVATAR: JsonValue = json!({
"content": {
"info": {
"h": 398,
"mimetype": "image/jpeg",
"size": 31037,
"w": 394
},
"url": "mxc://domain.com/JWEIFJgwEIhweiWJE"
},
"event_id": "$143273582443PhrSn:domain.com",
"origin_server_ts": 143273582,
"room_id": "!jEsUZKDJdhlrceRyVU:domain.com",
"sender": "@example:domain.com",
"state_key": "",
"type": "m.room.avatar",
"unsigned": {
"age": 1234
}
});
}
lazy_static! {
pub static ref ROOM_ID: JsonValue = json!({
"room_id": "!testroom:example.org"
});
}
lazy_static! {
pub static ref TAG: JsonValue = json!({
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
});
}
// TODO: Move `prev_content` into `unsigned` once ruma supports it
lazy_static! {
pub static ref TOPIC: JsonValue = json!({
"content": {
"topic": "😀"
},
"event_id": "$151957878228ssqrJ:localhost",
"origin_server_ts": 151957878,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.topic",
"prev_content": {
"topic": "test"
},
"unsigned": {
"age": 1392989,
"prev_sender": "@example:localhost",
"replaces_state": "$151957069225EVYKm:localhost"
}
});
}
lazy_static! {
pub static ref TYPING: JsonValue = json!({
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
});
}
+42
View File
@@ -0,0 +1,42 @@
//! Test data for the matrix-sdk crates.
//!
//! Exporting each const allows all the test data to have a single source of truth.
//! When running `cargo publish` no external folders are allowed so all the
//! test data needs to be contained within this crate.
use lazy_static::lazy_static;
use serde_json::{json, Value as JsonValue};
pub mod events;
pub mod sync;
pub use events::{
ALIAS, ALIASES, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGOUT, MEMBER,
MEMBER_NAME_CHANGE, MESSAGE_EDIT, MESSAGE_TEXT, NAME, POWER_LEVELS, PRESENCE, PUBLIC_ROOMS,
REACTION, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR,
ROOM_ID, ROOM_MESSAGES, TYPING,
};
pub use sync::{
DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, SYNC, VOIP_SYNC,
};
lazy_static! {
pub static ref DEVICES: JsonValue = json!({
"devices": [
{
"device_id": "BNYQQWUMXO",
"display_name": "Client 1",
"last_seen_ip": "-",
"last_seen_ts": 1596117733037u64,
"user_id": "@example:localhost"
},
{
"device_id": "LEBKSEUSNR",
"display_name": "Client 2",
"last_seen_ip": "-",
"last_seen_ts": 1599057006985u64,
"user_id": "@example:localhost"
}
]
});
}
File diff suppressed because it is too large Load Diff
-211
View File
@@ -1,211 +0,0 @@
{
"events_before": [],
"events_after": [
{
"content": {
"body": "yeah, let's do that",
"msgtype": "m.text"
},
"event_id": "$15163623196QOZxj:localhost",
"origin_server_ts": 1516362319505,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 43464955731
},
"user_id": "@example:localhost",
"age": 43464955731
}
],
"start": "t182-189_0_0_0_0_0_0_0_0",
"end": "t184-190_0_0_0_0_0_0_0_0",
"event": {
"content": {
"body": "ok, let's handle invites, joins and parts",
"msgtype": "m.text"
},
"event_id": "$15163622445EBvZJ:localhost",
"origin_server_ts": 1516362244026,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example2:localhost",
"type": "m.room.message",
"unsigned": {
"age": 43465031210
},
"user_id": "@example2:localhost",
"age": 43465031210
},
"state": [
{
"content": {
"topic": "amazing work"
},
"event_id": "$151568196747dxLZM:localhost",
"origin_server_ts": 1515681967443,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.topic",
"unsigned": {
"replaces_state": "$151567214844LzHAk:localhost",
"age": 44145307793
},
"user_id": "@example:localhost",
"age": 44145307793,
"replaces_state": "$151567214844LzHAk:localhost"
},
{
"content": {
"aliases": [
"#tutorial:localhost"
]
},
"event_id": "$15139375516NUgtD:localhost",
"origin_server_ts": 1513937551720,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "localhost",
"type": "m.room.aliases",
"unsigned": {
"age": 45889723516
},
"user_id": "@example:localhost",
"age": 45889723516
},
{
"content": {
"history_visibility": "shared"
},
"event_id": "$15139375515VaJEY:localhost",
"origin_server_ts": 1513937551613,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.history_visibility",
"unsigned": {
"age": 45889723623
},
"user_id": "@example:localhost",
"age": 45889723623
},
{
"content": {
"join_rule": "public"
},
"event_id": "$15139375514WsgmR:localhost",
"origin_server_ts": 1513937551539,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.join_rules",
"unsigned": {
"age": 45889723697
},
"user_id": "@example:localhost",
"age": 45889723697
},
{
"content": {
"alias": "#tutorial:localhost"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 1513937551461,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.canonical_alias",
"unsigned": {
"age": 45889723775
},
"user_id": "@example:localhost",
"age": 45889723775
},
{
"content": {
"ban": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100
},
"events_default": 0,
"invite": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@example:localhost": 100
},
"users_default": 0
},
"event_id": "$15139375512JaHAW:localhost",
"origin_server_ts": 1513937551359,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.power_levels",
"unsigned": {
"age": 45889723877
},
"user_id": "@example:localhost",
"age": 45889723877
},
{
"content": {
"creator": "@example:localhost"
},
"event_id": "$15139375510KUZHi:localhost",
"origin_server_ts": 1513937551203,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 45889724033
},
"user_id": "@example:localhost",
"age": 45889724033
},
{
"content": {
"avatar_url": null,
"displayname": "example2",
"membership": "join"
},
"event_id": "$151396611913abyeC:localhost",
"membership": "join",
"origin_server_ts": 1513966119908,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example2:localhost",
"state_key": "@example2:localhost",
"type": "m.room.member",
"unsigned": {
"age": 45861155328
},
"user_id": "@example2:localhost",
"age": 45861155328
},
{
"content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"event_id": "$15139375511GBYDY:localhost",
"membership": "join",
"origin_server_ts": 1513937551274,
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
"sender": "@example:localhost",
"state_key": "@example:localhost",
"type": "m.room.member",
"unsigned": {
"age": 45889723962
},
"user_id": "@example:localhost",
"age": 45889723962
}
]
}
@@ -1,18 +0,0 @@
{
"completed": [
"example.type.foo"
],
"flows": [
{
"stages": [
"example.type.foo"
]
}
],
"params": {
"example.type.baz": {
"example_key": "foobar"
}
},
"session": "xxxxxxyz"
}
-10
View File
@@ -1,10 +0,0 @@
{
"devices": [
{
"device_id": "QBUAZIFURK",
"display_name": "android",
"last_seen_ip": "1.2.3.4",
"last_seen_ts": 1474491775024
}
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"event_id": "$h29iv0s8:example.com"
}
@@ -1,13 +0,0 @@
{
"content": {
"alias": "#tutorial:localhost"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 1513937551461,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.canonical_alias",
"unsigned": {
"age": 7034220433
}
}
@@ -1,15 +0,0 @@
{
"content": {
"aliases": [
"#tutorial:localhost"
]
},
"event_id": "$15139375516NUgtD:localhost",
"origin_server_ts": 1513937551720,
"sender": "@example:localhost",
"state_key": "localhost",
"type": "m.room.aliases",
"unsigned": {
"age": 7034220174
}
}
@@ -1,15 +0,0 @@
{
"content": {
"creator": "@example:localhost",
"m.federate": true,
"room_version": "1"
},
"event_id": "$151957878228ekrDs:localhost",
"origin_server_ts": 1519578782185,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1392989709
}
}
@@ -1,7 +0,0 @@
{
"content": {
"event_id": "$someplace:example.org"
},
"room_id": "!somewhere:example.org",
"type": "m.fully_read"
}
@@ -1,13 +0,0 @@
{
"content": {
"history_visibility": "world_readable"
},
"event_id": "$151957878235ricnD:localhost",
"origin_server_ts": 1519578782195,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.history_visibility",
"unsigned": {
"age": 1392989715
}
}
@@ -1,13 +0,0 @@
{
"content": {
"join_rule": "public"
},
"event_id": "$151957878231iejdB:localhost",
"origin_server_ts": 1519578782192,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.join_rules",
"unsigned": {
"age": 1392989713
}
}
@@ -1,22 +0,0 @@
{
"content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"event_id": "$151800140517rfvjc:localhost",
"membership": "join",
"origin_server_ts": 1518001405556,
"sender": "@example:localhost",
"state_key": "@example:localhost",
"type": "m.room.member",
"unsigned": {
"age": 2970366338,
"replaces_state": "$151800111315tsynI:localhost",
"prev_content": {
"avatar_url": null,
"displayname": "example",
"membership": "invite"
}
}
}
@@ -1,14 +0,0 @@
{
"content": {
"body": "is dancing", "format": "org.matrix.custom.html",
"formatted_body": "<strong>is dancing</strong>",
"msgtype": "m.emote"
},
"event_id": "$152037280074GZeOm:localhost",
"origin_server_ts": 1520372800469,
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 598971425
}
}
@@ -1,16 +0,0 @@
{
"origin_server_ts": 1533565163841,
"sender": "@_neb_github:matrix.org",
"event_id": "$153356516319138IHRIC:matrix.org",
"unsigned": {
"age": 743
},
"content": {
"body": "https://github.com/matrix-org/matrix-python-sdk/issues/266 : Consider allowing MatrixClient.__init__ to take sync_token kwarg",
"format": "org.matrix.custom.html",
"formatted_body": "<a href='https://github.com/matrix-org/matrix-python-sdk/pull/313'>313: nio wins!</a>",
"msgtype": "m.notice"
},
"type": "m.room.message",
"room_id": "!YHhmBTmGBHGQOlGpaZ:matrix.org"
}
@@ -1,14 +0,0 @@
{
"content": {
"body": "is dancing", "format": "org.matrix.custom.html",
"formatted_body": "<strong>is dancing</strong>",
"msgtype": "m.text"
},
"event_id": "$152037280074GZeOm:localhost",
"origin_server_ts": 1520372800469,
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 598971425
}
}
@@ -1,13 +0,0 @@
{
"content": {
"name": "room name"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 1513937551461,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 7034220433
}
}
@@ -1,31 +0,0 @@
{
"content": {
"ban": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.message": 25
},
"events_default": 0,
"invite": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@example:localhost": 100,
"@bob:localhost": 0
},
"users_default": 0
},
"event_id": "$15139375512JaHAW:localhost",
"origin_server_ts": 1513937551359,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.power_levels",
"unsigned": {
"age": 7034220535
}
}
@@ -1,11 +0,0 @@
{
"content": {
"avatar_url": "mxc://localhost:wefuiwegh8742w",
"currently_active": false,
"last_active_ago": 1,
"presence": "online",
"status_msg": "Making cupcakes"
},
"sender": "@example:localhost",
"type": "m.presence"
}
@@ -1,22 +0,0 @@
{
"content": {},
"event_id": "$15275046980maRLj:localhost",
"origin_server_ts": 1527504698685,
"sender": "@example:localhost",
"type": "m.room.message",
"unsigned": {
"age": 19334,
"redacted_because": {
"content": {},
"event_id": "$15275047031IXQRi:localhost",
"origin_server_ts": 1527504703496,
"redacts": "$15275046980maRLj:localhost",
"sender": "@example:localhost",
"type": "m.room.redaction",
"unsigned": {
"age": 14523
}
},
"redacted_by": "$15275047031IXQRi:localhost"
}
}
@@ -1,7 +0,0 @@
{
"content": {},
"event_id": "$15275046980maRLj:localhost",
"origin_server_ts": 1527504698685,
"sender": "@example:localhost",
"type": "m.room.message"
}

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