Compare commits
379 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a600432946 | |||
| cde77cfc92 | |||
| d1f8cbaade | |||
| 5f29cd2c1c | |||
| 59b80d8fbd | |||
| 68bb8182e4 | |||
| f6c8687dc8 | |||
| 472d8faace | |||
| fc5f3c2fcc | |||
| fb756208d8 | |||
| 6a98e93845 | |||
| 79e155acfb | |||
| 59ae6e3dc0 | |||
| e628ed3ef4 | |||
| 11d40e9daa | |||
| a07f0631b7 | |||
| 790d1dd8f7 | |||
| c92e510a4d | |||
| c48a6c0601 | |||
| 383f3f9834 | |||
| 3c1e9ba6e9 | |||
| 26893b9877 | |||
| 2b734b8e69 | |||
| c5f6f87a6c | |||
| 66cdb62a3d | |||
| f53e33723b | |||
| 06bc6e7568 | |||
| 5e3f42ec5a | |||
| 08b3dfa3b5 | |||
| 6cf9563441 | |||
| fb65c7f4ba | |||
| c4452909e7 | |||
| 2d3669b03b | |||
| 848e6e5897 | |||
| c723b76138 | |||
| 57f6b0af09 | |||
| 1c4082af45 | |||
| eece5d318e | |||
| bb6ade2165 | |||
| 586b010811 | |||
| 84ab0fde51 | |||
| ec18df2c2a | |||
| f50503e7c1 | |||
| c619e5c381 | |||
| e7c4a74ed6 | |||
| f8ea019f02 | |||
| 6db8dd620d | |||
| bdc1fa4c03 | |||
| 4e66a2d436 | |||
| 0fa948448e | |||
| 76c675cd09 | |||
| 85a4a594c5 | |||
| f70746c50f | |||
| 712490b671 | |||
| b580e68469 | |||
| bd2cf18fbc | |||
| 092f4217b0 | |||
| abd2ac7168 | |||
| eeea70640e | |||
| 6047838f53 | |||
| cb51799246 | |||
| 44d99277fe | |||
| 5b8e643541 | |||
| ae85c209ab | |||
| 2306caa62f | |||
| 17c11ae23f | |||
| 5b51096e37 | |||
| ac79d6bcee | |||
| b6e056f832 | |||
| d99a22d68d | |||
| 2602c155d0 | |||
| 80f562643f | |||
| 578cb4e268 | |||
| c53c6a94d7 | |||
| 907cf19f05 | |||
| 88682e1c3b | |||
| 20a4edf899 | |||
| 3222b11346 | |||
| fc9d6a6d47 | |||
| c9917e4079 | |||
| b233ab87bb | |||
| 73c3a709de | |||
| 6ce7b30b72 | |||
| 980d55a2f3 | |||
| 988be62804 | |||
| 23efd0850d | |||
| 17e0f1d9ab | |||
| 3c85bd55d3 | |||
| 2298d72ab9 | |||
| 408407b33d | |||
| ab426384e1 | |||
| 5bc68c0c6d | |||
| ebf20d5b2c | |||
| 93d9c40323 | |||
| c6ea976d7f | |||
| 5f24915300 | |||
| fbe174fb64 | |||
| 977d5331c0 | |||
| d40d7e18f5 | |||
| 11be68ad49 | |||
| b0d0782a72 | |||
| dbb6d8ac71 | |||
| a30845f9ce | |||
| 379f290b8b | |||
| 6c413bba48 | |||
| e17a39d446 | |||
| fcadf6ec4a | |||
| 231fde219c | |||
| 2774bd238b | |||
| fed67192bc | |||
| 16db970558 | |||
| c9a79bf32e | |||
| d74ed508f9 | |||
| eafba9c7ef | |||
| 610923af89 | |||
| 23dfeb13df | |||
| f4abd7d027 | |||
| b716e71784 | |||
| 094598196a | |||
| db1d1c49a0 | |||
| ff4125c11e | |||
| a0d51803ed | |||
| 3aabd63975 | |||
| 394e37f9ea | |||
| 369b88d6f8 | |||
| ec8b3ae515 | |||
| c94382b46c | |||
| 2a6a67c6cc | |||
| 37f0a9ad7b | |||
| 28540ad50a | |||
| 29d92d3e81 | |||
| 0477f354c9 | |||
| c7a0c1402c | |||
| 2af5643243 | |||
| 5e9885946f | |||
| e89879d8a6 | |||
| 2f219f83db | |||
| 63e9f794c7 | |||
| 7c0b910d7a | |||
| c77ecad9a5 | |||
| db2897cf1e | |||
| 5c5ce0dfe3 | |||
| 6de213483c | |||
| f5846b89ea | |||
| c5e7bedb37 | |||
| 2b46c560c7 | |||
| c6ad0665b5 | |||
| 8ab84dee16 | |||
| 62b2c07be2 | |||
| 2fb29ae8fd | |||
| b57e858ad1 | |||
| 054aac17aa | |||
| 68b65dd357 | |||
| f2881126cd | |||
| 11968a5888 | |||
| ad279dc566 | |||
| 2814932845 | |||
| a2430dbc53 | |||
| e51d2dd36a | |||
| 604af1ac8c | |||
| 68c6393eb2 | |||
| 4cbf9c7f47 | |||
| 8bb3b75b1d | |||
| a76f0c7cb4 | |||
| 01e31afcbd | |||
| 3b2f2f922e | |||
| 64b83b3245 | |||
| 563e6b3cdd | |||
| 6518bff2ac | |||
| 0e26247b53 | |||
| e69f7dbc5f | |||
| 4d0f6df89a | |||
| 6b184363a1 | |||
| b519069634 | |||
| 1bd44a7427 | |||
| 568ff5a3f5 | |||
| a6bf40d4e2 | |||
| b3bb99d76a | |||
| 243bab7036 | |||
| 88b39f4b67 | |||
| 5e8061f846 | |||
| 870e96a1df | |||
| 59070c2af6 | |||
| cec8936728 | |||
| 14071b0d31 | |||
| 57173e4385 | |||
| 997caad985 | |||
| 2b752c0c02 | |||
| 2cccb8b450 | |||
| 0c540ac8de | |||
| 6033b7b886 | |||
| b67f8d1389 | |||
| ae645ad9f0 | |||
| 5b72509dac | |||
| 3ce42a096b | |||
| 8331c2f267 | |||
| b3c9570b0f | |||
| 9d5c877df9 | |||
| a8e2727473 | |||
| 4b9c6e6bd2 | |||
| d29ac088c0 | |||
| 3a316de9ef | |||
| 40cb37e824 | |||
| f165b55a1d | |||
| 84b91d4575 | |||
| f5832423f4 | |||
| 73dd07aadf | |||
| 0f39a45734 | |||
| f41060c39a | |||
| bbb8e12bac | |||
| d0e1471c91 | |||
| 322ef1fd63 | |||
| 47cb97bc60 | |||
| 8d35bea830 | |||
| d8bcc4e3f1 | |||
| 434ac86090 | |||
| 1061026f96 | |||
| e638c49160 | |||
| 5d84db9fb7 | |||
| 874bdea634 | |||
| 68497d3a1f | |||
| b9e198c172 | |||
| 40d0a82342 | |||
| d49c0a1bcb | |||
| 91fb7b0a7c | |||
| 9b12c22823 | |||
| 3957006fae | |||
| 874029dff0 | |||
| 6aff3ed407 | |||
| c0ae78ae82 | |||
| 8b22f01ecd | |||
| 2ed694b041 | |||
| a0ef6ab811 | |||
| d098b39024 | |||
| 3cf23f8a5c | |||
| fc59bc2992 | |||
| da65f43983 | |||
| 72e77d237a | |||
| ecc3e18e85 | |||
| dea70af889 | |||
| 30362091e5 | |||
| ada4b6ef16 | |||
| 59e6066579 | |||
| 0aa3362671 | |||
| 5873db7331 | |||
| b3fe05ec81 | |||
| 92fbf58b13 | |||
| a4b2cc84c7 | |||
| 89c3f6fa0e | |||
| 1395da694e | |||
| 264b20535e | |||
| caba350b33 | |||
| d9fe194111 | |||
| 258adda67c | |||
| 40dc13b2e2 | |||
| 4cda54ca1c | |||
| 8116c5b3f7 | |||
| 35d584c67b | |||
| 9504cbcc4f | |||
| 1dcc5127d0 | |||
| 6790699279 | |||
| 85e3d7083c | |||
| 262ace1773 | |||
| 7cd101d8cb | |||
| ce2058aea9 | |||
| e9b0acaa8e | |||
| bd2da08c4e | |||
| 0a88d419c6 | |||
| 80c190db36 | |||
| 550cf00ee7 | |||
| fbca7951fc | |||
| 1e1358fcef | |||
| fd1b3329f5 | |||
| 9c9d8468a5 | |||
| 55ca03f100 | |||
| 83708725b2 | |||
| 6f59d62e5c | |||
| 1c348f0cdb | |||
| 634596257d | |||
| 5e4973a1dc | |||
| 19f023e0ee | |||
| 090c15fe19 | |||
| e8b307dc4f | |||
| 056479d450 | |||
| e5ebe2f888 | |||
| e8e1b431ad | |||
| 847d40e567 | |||
| cf6c555e6a | |||
| b508aa9ebc | |||
| 5e7634506e | |||
| ba39b64ced | |||
| 33ad65a105 | |||
| fcebe89f6c | |||
| 2d5eb920b8 | |||
| 26de2c86ed | |||
| cba1e95d0a | |||
| 78a5a88638 | |||
| b7b9c67259 | |||
| 54bff81470 | |||
| fe21972f4a | |||
| 58e3c72446 | |||
| 6dd5c6c317 | |||
| 4e0af3eafe | |||
| 1d0791142c | |||
| 2560ba2980 | |||
| 19be3dd852 | |||
| 40f2a6978b | |||
| 1fd8c43d94 | |||
| 63cc3fd890 | |||
| c556ca40b1 | |||
| 3e32f47903 | |||
| 8f2824186a | |||
| b0dbb20e22 | |||
| 0519c4c6b1 | |||
| 28184b4a29 | |||
| 76175abea2 | |||
| d28f829b1c | |||
| 184c3dce15 | |||
| a08a3078da | |||
| c2100d7622 | |||
| a91fa59174 | |||
| 574a6b68ae | |||
| 2f4c1dfcc4 | |||
| 1b62a21dbd | |||
| a78825eff9 | |||
| 0bad7b213e | |||
| e4bb37b1a8 | |||
| 54c443ac68 | |||
| 3af9af96ea | |||
| f75d188131 | |||
| fc3a00054f | |||
| 84e41c2ade | |||
| 4630733b55 | |||
| eb690e14e4 | |||
| 009430e829 | |||
| 073fb73ff3 | |||
| e789747834 | |||
| 833002f846 | |||
| 3838fab788 | |||
| 907e9fc476 | |||
| b829a39cd2 | |||
| daa7af0605 | |||
| 47a1adc864 | |||
| 98e448acdd | |||
| 72bd51f26e | |||
| 29db856322 | |||
| cd5cda916f | |||
| b809d1c263 | |||
| 7bb6675abf | |||
| b91bea94f4 | |||
| 9e02049b05 | |||
| ba4dc6c60a | |||
| 1511a27f4c | |||
| 7ae6c147fa | |||
| f51630eb07 | |||
| bf873bde42 | |||
| 017f81e430 | |||
| ec5fff2046 | |||
| 73e294b1bd | |||
| e5ec479923 | |||
| 75107f99b2 | |||
| fb8efe368a | |||
| 1faf477537 | |||
| d55618921b | |||
| b34716f7e9 | |||
| aec7ef6f9c | |||
| 329f09ce0a | |||
| 68c23af5ae | |||
| a54f30c02f | |||
| fde00b1c62 | |||
| 8dd425f8ff | |||
| 69204d4fb3 | |||
| 9cdcbf6bf8 | |||
| 9596087959 | |||
| 6570402b95 | |||
| e0c9b990e7 | |||
| fb1b554b86 | |||
| 81de2b3afc | |||
| 9a53fa3876 |
+7
-2
@@ -1,7 +1,6 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint",
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
ecmaVersion: 2017,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
@@ -67,5 +66,11 @@ module.exports = {
|
||||
"padded-blocks": ["warn"],
|
||||
"no-extend-native": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- node # Latest stable version of nodejs.
|
||||
- "10.11.0"
|
||||
script:
|
||||
- ./travis.sh
|
||||
|
||||
+236
@@ -1,3 +1,239 @@
|
||||
Changes in [0.14.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3) (2019-01-22)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3-rc.1...v0.14.3)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.14.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3-rc.1) (2019-01-17)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2...v0.14.3-rc.1)
|
||||
|
||||
* Merge develop into experimental
|
||||
[\#815](https://github.com/matrix-org/matrix-js-sdk/pull/815)
|
||||
* Add a getAllEndToEndSessions to crypto store
|
||||
[\#812](https://github.com/matrix-org/matrix-js-sdk/pull/812)
|
||||
* T3chguy/fix displayname logic
|
||||
[\#668](https://github.com/matrix-org/matrix-js-sdk/pull/668)
|
||||
* Contributing: Note that rebase lets you mass signoff commits
|
||||
[\#814](https://github.com/matrix-org/matrix-js-sdk/pull/814)
|
||||
* take into account homoglyphs when calculating similar display names
|
||||
[\#672](https://github.com/matrix-org/matrix-js-sdk/pull/672)
|
||||
* Emit for key backup failures
|
||||
[\#809](https://github.com/matrix-org/matrix-js-sdk/pull/809)
|
||||
* emit oldEventId on "updatePendingEvent"
|
||||
[\#646](https://github.com/matrix-org/matrix-js-sdk/pull/646)
|
||||
* Add getThirdpartyUser to base api
|
||||
[\#589](https://github.com/matrix-org/matrix-js-sdk/pull/589)
|
||||
* Support custom status messages
|
||||
[\#805](https://github.com/matrix-org/matrix-js-sdk/pull/805)
|
||||
* Extra checks to avoid release script blowing up mid-process.
|
||||
[\#749](https://github.com/matrix-org/matrix-js-sdk/pull/749)
|
||||
* Move glob regex utilities out of the pushprocessor and into a more generic
|
||||
place
|
||||
[\#800](https://github.com/matrix-org/matrix-js-sdk/pull/800)
|
||||
|
||||
Changes in [0.14.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2) (2018-12-10)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2-rc.1...v0.14.2)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.14.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2-rc.1) (2018-12-06)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.1...v0.14.2-rc.1)
|
||||
|
||||
* fix some assertions in e2e backup unit test
|
||||
[\#794](https://github.com/matrix-org/matrix-js-sdk/pull/794)
|
||||
* Config should be called with auth
|
||||
[\#798](https://github.com/matrix-org/matrix-js-sdk/pull/798)
|
||||
* Don't re-establish sessions with unknown devices
|
||||
[\#792](https://github.com/matrix-org/matrix-js-sdk/pull/792)
|
||||
* e2e key backups
|
||||
[\#684](https://github.com/matrix-org/matrix-js-sdk/pull/684)
|
||||
* WIP: online incremental megolm backups
|
||||
[\#595](https://github.com/matrix-org/matrix-js-sdk/pull/595)
|
||||
* Support for e2e key backups
|
||||
[\#736](https://github.com/matrix-org/matrix-js-sdk/pull/736)
|
||||
* Passphrase Support for e2e backups
|
||||
[\#786](https://github.com/matrix-org/matrix-js-sdk/pull/786)
|
||||
* Add 'getSsoLoginUrl' function
|
||||
[\#783](https://github.com/matrix-org/matrix-js-sdk/pull/783)
|
||||
* Fix: don't set the room name to null when heroes are missing.
|
||||
[\#784](https://github.com/matrix-org/matrix-js-sdk/pull/784)
|
||||
* Handle crypto db version upgrades
|
||||
[\#785](https://github.com/matrix-org/matrix-js-sdk/pull/785)
|
||||
* Restart broken Olm sessions
|
||||
[\#780](https://github.com/matrix-org/matrix-js-sdk/pull/780)
|
||||
* Use the last olm session that got a message
|
||||
[\#776](https://github.com/matrix-org/matrix-js-sdk/pull/776)
|
||||
|
||||
Changes in [0.14.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.1) (2018-11-22)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0...v0.14.1)
|
||||
|
||||
* Warning when crypto DB is too new to use.
|
||||
|
||||
Changes in [0.14.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0) (2018-11-19)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0-rc.1...v0.14.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0-rc.1) (2018-11-15)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.1...v0.14.0-rc.1)
|
||||
|
||||
BREAKING CHANGE
|
||||
----------------
|
||||
|
||||
* js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to
|
||||
continue using Olm with the js-sdk. The js-sdk will call Olm's
|
||||
init() method when the client is started.
|
||||
|
||||
All Changes
|
||||
-----------
|
||||
|
||||
* Prevent messages from being sent if other messages have failed to send
|
||||
[\#781](https://github.com/matrix-org/matrix-js-sdk/pull/781)
|
||||
* A unit test for olm
|
||||
[\#777](https://github.com/matrix-org/matrix-js-sdk/pull/777)
|
||||
* Set access_token and user_id after login in with username and password.
|
||||
[\#778](https://github.com/matrix-org/matrix-js-sdk/pull/778)
|
||||
* Add function to get currently joined rooms.
|
||||
[\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779)
|
||||
* Remove the request-only stuff we don't need anymore
|
||||
[\#775](https://github.com/matrix-org/matrix-js-sdk/pull/775)
|
||||
* Manually construct query strings for browser-request instances
|
||||
[\#770](https://github.com/matrix-org/matrix-js-sdk/pull/770)
|
||||
* Fix: correctly check for crypto being present
|
||||
[\#769](https://github.com/matrix-org/matrix-js-sdk/pull/769)
|
||||
* Update babel-eslint to 8.1.1
|
||||
[\#768](https://github.com/matrix-org/matrix-js-sdk/pull/768)
|
||||
* Support `request` in the browser and support supplying servers to try in
|
||||
joinRoom()
|
||||
[\#764](https://github.com/matrix-org/matrix-js-sdk/pull/764)
|
||||
* loglevel should be a normal dependency
|
||||
[\#767](https://github.com/matrix-org/matrix-js-sdk/pull/767)
|
||||
* Stop devicelist when client is stopped
|
||||
[\#766](https://github.com/matrix-org/matrix-js-sdk/pull/766)
|
||||
* Update to WebAssembly-powered Olm
|
||||
[\#743](https://github.com/matrix-org/matrix-js-sdk/pull/743)
|
||||
* Logging lib. Fixes #332
|
||||
[\#763](https://github.com/matrix-org/matrix-js-sdk/pull/763)
|
||||
* Use new stop() method on matrix-mock-request
|
||||
[\#765](https://github.com/matrix-org/matrix-js-sdk/pull/765)
|
||||
|
||||
Changes in [0.13.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.1) (2018-11-14)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.0...v0.13.1)
|
||||
|
||||
* Add function to get currently joined rooms.
|
||||
[\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779)
|
||||
|
||||
Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0)
|
||||
|
||||
BREAKING CHANGE
|
||||
----------------
|
||||
* `MatrixClient::login` now sets client `access_token` and `user_id` following successful login with username and password.
|
||||
|
||||
Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.12.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1-rc.1) (2018-10-24)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0...v0.12.1-rc.1)
|
||||
|
||||
* Add repository type to package.json to make it valid
|
||||
[\#762](https://github.com/matrix-org/matrix-js-sdk/pull/762)
|
||||
* Add getMediaConfig()
|
||||
[\#761](https://github.com/matrix-org/matrix-js-sdk/pull/761)
|
||||
* add new examples, to be expanded into a post
|
||||
[\#739](https://github.com/matrix-org/matrix-js-sdk/pull/739)
|
||||
|
||||
Changes in [0.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0) (2018-10-16)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0-rc.1...v0.12.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0-rc.1) (2018-10-11)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1...v0.12.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
----------------
|
||||
* If js-sdk finds data in the store that is incompatible with the options currently being used,
|
||||
it will emit sync state ERROR with an error of type InvalidStoreError. It will also stop trying
|
||||
to sync in this situation: the app must stop the client and then either clear the store or
|
||||
change the options (in this case, enable or disable lazy loading of members) and then start
|
||||
the client again.
|
||||
|
||||
All Changes
|
||||
-----------
|
||||
|
||||
* never replace /sync'ed memberships with OOB ones
|
||||
[\#760](https://github.com/matrix-org/matrix-js-sdk/pull/760)
|
||||
* Don't fail to start up if lazy load check fails
|
||||
[\#759](https://github.com/matrix-org/matrix-js-sdk/pull/759)
|
||||
* Make e2e work on Edge
|
||||
[\#754](https://github.com/matrix-org/matrix-js-sdk/pull/754)
|
||||
* throw error with same name and message over idb worker boundary
|
||||
[\#758](https://github.com/matrix-org/matrix-js-sdk/pull/758)
|
||||
* Default to a room version of 1 when there is no room create event
|
||||
[\#755](https://github.com/matrix-org/matrix-js-sdk/pull/755)
|
||||
* Silence bluebird warnings
|
||||
[\#757](https://github.com/matrix-org/matrix-js-sdk/pull/757)
|
||||
* allow non-ff merge from release branch into master
|
||||
[\#750](https://github.com/matrix-org/matrix-js-sdk/pull/750)
|
||||
* Reject with the actual error on indexeddb error
|
||||
[\#751](https://github.com/matrix-org/matrix-js-sdk/pull/751)
|
||||
* Update mocha to v5
|
||||
[\#744](https://github.com/matrix-org/matrix-js-sdk/pull/744)
|
||||
* disable lazy loading for guests as they cant create filters
|
||||
[\#748](https://github.com/matrix-org/matrix-js-sdk/pull/748)
|
||||
* Revert "Add getMediaLimits to client"
|
||||
[\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745)
|
||||
|
||||
Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1-rc.1) (2018-09-27)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0...v0.11.1-rc.1)
|
||||
|
||||
* make usage of hub compatible with latest version (2.5)
|
||||
[\#747](https://github.com/matrix-org/matrix-js-sdk/pull/747)
|
||||
* Detect when lazy loading has been toggled in client.startClient
|
||||
[\#746](https://github.com/matrix-org/matrix-js-sdk/pull/746)
|
||||
* Add getMediaLimits to client
|
||||
[\#644](https://github.com/matrix-org/matrix-js-sdk/pull/644)
|
||||
* Split npm start into an init and watch script
|
||||
[\#742](https://github.com/matrix-org/matrix-js-sdk/pull/742)
|
||||
* Revert "room name should only take canonical alias into account"
|
||||
[\#738](https://github.com/matrix-org/matrix-js-sdk/pull/738)
|
||||
* fix display name disambiguation with LL
|
||||
[\#737](https://github.com/matrix-org/matrix-js-sdk/pull/737)
|
||||
* Introduce Room.myMembership event
|
||||
[\#735](https://github.com/matrix-org/matrix-js-sdk/pull/735)
|
||||
* room name should only take canonical alias into account
|
||||
[\#733](https://github.com/matrix-org/matrix-js-sdk/pull/733)
|
||||
* state events from context response were not wrapped in a MatrixEvent
|
||||
[\#732](https://github.com/matrix-org/matrix-js-sdk/pull/732)
|
||||
* Reduce amount of promises created when inserting members
|
||||
[\#724](https://github.com/matrix-org/matrix-js-sdk/pull/724)
|
||||
* dont wait for LL members to be stored to resolve the members
|
||||
[\#726](https://github.com/matrix-org/matrix-js-sdk/pull/726)
|
||||
* RoomState.members emitted with wrong argument order for OOB members
|
||||
[\#728](https://github.com/matrix-org/matrix-js-sdk/pull/728)
|
||||
|
||||
Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0)
|
||||
|
||||
@@ -113,3 +113,8 @@ include the line in your commit or pull request comment::
|
||||
can't be accepted. Git makes this trivial - just use the -s flag when you do
|
||||
``git commit``, having first set ``user.name`` and ``user.email`` git configs
|
||||
(which you should have done anyway :)
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are on git 2.17+
|
||||
you can mass signoff using rebase::
|
||||
|
||||
git rebase --signoff origin/develop
|
||||
|
||||
@@ -30,9 +30,61 @@ In Node.js
|
||||
console.log("Public Rooms: %s", JSON.stringify(data));
|
||||
});
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
await client.startClient({initialSyncLimit: 10});
|
||||
```
|
||||
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once('sync', function(state, prevState, res) {
|
||||
if(state === 'PREPARED') {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
console.log(state);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To send a message:
|
||||
|
||||
```javascript
|
||||
var content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
};
|
||||
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
console.log(err);
|
||||
});
|
||||
```
|
||||
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
});
|
||||
```
|
||||
|
||||
By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
|
||||
|
||||
```javascript
|
||||
Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
client.getRoom(roomId).timeline.forEach(t => {
|
||||
console.log(t.event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
What does this SDK do?
|
||||
----------------------
|
||||
@@ -267,13 +319,13 @@ To provide the Olm library in a browser application:
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz``
|
||||
* ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://matrix.org/packages/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you
|
||||
can use ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
|
||||
can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz
|
||||
--save-optional`` (if your application also works without e2e crypto enabled)
|
||||
or ``--save`` (if it doesn't) to do so.
|
||||
|
||||
|
||||
+13
-1
@@ -1,5 +1,17 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("browser-request"));
|
||||
const request = require('browser-request');
|
||||
const queryString = require('qs');
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
// we mimic `request`'s query string interface to make it all work
|
||||
// as expected.
|
||||
// browser-request will happily take the constructed string as the
|
||||
// query string without trying to modify it further.
|
||||
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
|
||||
return request(opts, fn);
|
||||
});
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with
|
||||
// indexeddb disabled.
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ set -x
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm use 6 || exit $?
|
||||
nvm use 10 || exit $?
|
||||
npm install || exit $?
|
||||
|
||||
RC=0
|
||||
|
||||
Generated
+6772
File diff suppressed because it is too large
Load Diff
+18
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.11.0",
|
||||
"version": "0.14.3",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -10,15 +10,18 @@
|
||||
"test": "npm run test:build && npm run test:run",
|
||||
"check": "npm run test:build && _mocha --recursive specbuild --colors",
|
||||
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"start": "babel -s -w -d lib src",
|
||||
"start": "npm run start:init && npm run start:watch",
|
||||
"start:watch": "babel -s -w --skip-initial-build -d lib src",
|
||||
"start:init": "babel -s -d lib src",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
|
||||
"dist": "npm run build",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 102 src spec",
|
||||
"lint": "eslint --max-warnings 101 src spec",
|
||||
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -51,34 +54,38 @@
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"base-x": "3.0.4",
|
||||
"bluebird": "^3.5.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"request": "^2.53.0"
|
||||
"loglevel": "1.6.1",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"browserify": "^14.0.0",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^3.13.1",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"exorcist": "^0.4.0",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"jsdoc": "^3.5.5",
|
||||
"lolex": "^1.5.2",
|
||||
"matrix-mock-request": "^1.2.0",
|
||||
"mocha": "^3.2.0",
|
||||
"mocha-jenkins-reporter": "^0.3.6",
|
||||
"matrix-mock-request": "^1.2.2",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-jenkins-reporter": "^0.4.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglify-js": "^2.8.26",
|
||||
"watchify": "^3.2.1"
|
||||
"watchify": "^3.11.0"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
+24
-4
@@ -11,7 +11,17 @@
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
|
||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
|
||||
exit
|
||||
fi
|
||||
else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
fi
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
@@ -45,7 +55,8 @@ fi
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
changelog_file="CHANGELOG.md"
|
||||
while getopts hc:xz f; do
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xz f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -60,6 +71,9 @@ while getopts hc:xz f; do
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
@@ -74,6 +88,12 @@ if [ -z "$skip_changelog" ]; then
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
@@ -245,7 +265,7 @@ release_text=`mktemp`
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -f "${release_text}" "$tag"
|
||||
hub release create $hubflags $assets -F "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
@@ -281,7 +301,7 @@ fi
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
git merge "$rel_branch"
|
||||
|
||||
# push master and docs (if generated) to github
|
||||
git push origin master
|
||||
|
||||
@@ -102,9 +102,11 @@ TestClient.prototype.start = function() {
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
return this.httpBackend.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliceTestClient.stop();
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
|
||||
@@ -410,10 +410,10 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliTestClient.stop();
|
||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
bobTestClient.stop();
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||
});
|
||||
|
||||
it("Bob uploads device keys", function() {
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("MatrixClient events", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
|
||||
@@ -111,6 +111,7 @@ describe("getEventTimeline support", function() {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
|
||||
@@ -41,6 +41,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
|
||||
@@ -64,6 +64,7 @@ describe("MatrixClient opts", function() {
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
@@ -94,7 +95,7 @@ describe("MatrixClient opts", function() {
|
||||
httpBackend.flush("/txn1", 1);
|
||||
});
|
||||
|
||||
it("should be able to sync / get new events", function(done) {
|
||||
it("should be able to sync / get new events", async function() {
|
||||
const expectedEventTypes = [ // from /initialSync
|
||||
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
|
||||
"m.room.create",
|
||||
@@ -110,20 +111,16 @@ describe("MatrixClient opts", function() {
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
client.startClient();
|
||||
httpBackend.flush("/pushrules", 1).then(function() {
|
||||
return httpBackend.flush("/filter", 1);
|
||||
}).then(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).done(function() {
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
);
|
||||
done();
|
||||
});
|
||||
await client.startClient();
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||
|
||||
@@ -130,6 +130,7 @@ describe("MatrixClient room timelines", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("local echo events", function() {
|
||||
|
||||
@@ -38,6 +38,7 @@ describe("MatrixClient syncing", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("startClient", function() {
|
||||
|
||||
@@ -296,7 +296,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliceTestClient.stop();
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function() {
|
||||
@@ -817,8 +817,14 @@ describe("megolm", function() {
|
||||
};
|
||||
});
|
||||
|
||||
// Grab the event that we'll need to resend
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const pendingEvents = room.getPendingEvents();
|
||||
expect(pendingEvents.length).toEqual(1);
|
||||
const unsentEvent = pendingEvents[0];
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.client.resendEvent(unsentEvent, room),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
const AutoDiscovery = sdk.AutoDiscovery;
|
||||
|
||||
import expect from 'expect';
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
httpBackend = new MockHttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
|
||||
it("should throw an error when no domain is specified", function() {
|
||||
return Promise.all([
|
||||
AutoDiscovery.findClientConfig(/* no args */).then(() => {
|
||||
throw new Error("Expected a failure, not success with no args");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig("").then(() => {
|
||||
throw new Error("Expected a failure, not success with an empty string");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig(null).then(() => {
|
||||
throw new Error("Expected a failure, not success with null");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig(true).then(() => {
|
||||
throw new Error("Expected a failure, not success with a non-string");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return PROMPT when .well-known 404s", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (empty string)", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (no property)", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (disallowed scheme)", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "mxc://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 404)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 500)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 200 but wrong content)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
|
||||
"m.homeserver", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS with the right homeserver URL", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(missing base_url)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
not_base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(empty base_url)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(validation error: 404)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(validation error: 500)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " +
|
||||
"verifiably accurate", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS and preserve non-standard keys from the " +
|
||||
".well-known response", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
+110
-8
@@ -1,20 +1,122 @@
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
|
||||
const sdk = require("../..");
|
||||
let Crypto;
|
||||
if (sdk.CRYPTO_ENABLED) {
|
||||
Crypto = require("../../lib/crypto");
|
||||
}
|
||||
import '../olm-loader';
|
||||
|
||||
import Crypto from '../../lib/crypto';
|
||||
import expect from 'expect';
|
||||
|
||||
import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../MockStorageApi';
|
||||
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
const sdk = require("../..");
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
Olm.init().then(done);
|
||||
});
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(2);
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||
});
|
||||
|
||||
|
||||
describe('Session management', function() {
|
||||
const otkResponse = {
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:FLIBBLE': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally a valid signature',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let crypto;
|
||||
let mockBaseApis;
|
||||
let mockRoomList;
|
||||
|
||||
let fakeEmitter;
|
||||
|
||||
beforeEach(async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
cryptoStore.storeEndToEndDeviceData({
|
||||
devices: {
|
||||
'@bob:home.server': {
|
||||
'BOBDEVICE': {
|
||||
keys: {
|
||||
'curve25519:BOBDEVICE': 'this is a key',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
trackingStatus: {},
|
||||
});
|
||||
|
||||
mockBaseApis = {
|
||||
sendToDevice: expect.createSpy(),
|
||||
getKeyBackupVersion: expect.createSpy(),
|
||||
isGuest: expect.createSpy(),
|
||||
};
|
||||
mockRoomList = {};
|
||||
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
crypto = new Crypto(
|
||||
mockBaseApis,
|
||||
sessionStore,
|
||||
"@alice:home.server",
|
||||
"FLIBBLE",
|
||||
sessionStore,
|
||||
cryptoStore,
|
||||
mockRoomList,
|
||||
);
|
||||
crypto.registerEventHandlers(fakeEmitter);
|
||||
await crypto.init();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await crypto.stop();
|
||||
});
|
||||
|
||||
it("restarts wedged Olm sessions", async function() {
|
||||
const prom = new Promise((resolve) => {
|
||||
mockBaseApis.claimOneTimeKeys = function() {
|
||||
resolve();
|
||||
return otkResponse;
|
||||
};
|
||||
});
|
||||
|
||||
fakeEmitter.emit('toDeviceEvent', {
|
||||
getType: expect.createSpy().andReturn('m.room.message'),
|
||||
getContent: expect.createSpy().andReturn({
|
||||
msgtype: 'm.bad.encrypted',
|
||||
}),
|
||||
getWireContent: expect.createSpy().andReturn({
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
sender_key: 'this is a key',
|
||||
}),
|
||||
getSender: expect.createSpy().andReturn('@bob:home.server'),
|
||||
});
|
||||
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,16 +59,25 @@ describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
for (const dl of deviceLists) {
|
||||
dl.stop();
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
@@ -76,7 +85,9 @@ describe('DeviceList', function() {
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
return new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
|
||||
const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
|
||||
it("should successfully download and store device keys", function() {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run megolm tests: libolm not available");
|
||||
}
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
@@ -13,20 +9,17 @@ import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
|
||||
// Crypto and OlmDevice won't import unless we have global.Olm
|
||||
let OlmDevice;
|
||||
let Crypto;
|
||||
if (global.Olm) {
|
||||
OlmDevice = require('../../../../lib/crypto/OlmDevice');
|
||||
Crypto = require('../../../../lib/crypto');
|
||||
}
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../../lib/crypto';
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("MegolmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm unit tests: libolm not present');
|
||||
@@ -38,9 +31,11 @@ describe("MegolmDecryption", function() {
|
||||
let mockCrypto;
|
||||
let mockBaseApis;
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
await Olm.init();
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockBaseApis = {};
|
||||
|
||||
@@ -69,7 +64,7 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
describe('receives some keys:', function() {
|
||||
let groupSession;
|
||||
beforeEach(function() {
|
||||
beforeEach(async function() {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
@@ -98,7 +93,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
};
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
await event.attemptDecryption(mockCrypto).then(() => {
|
||||
megolmDecryption.onRoomKeyEvent(event);
|
||||
});
|
||||
});
|
||||
@@ -266,5 +261,92 @@ describe("MegolmDecryption", function() {
|
||||
// test is successful if no exception is thrown
|
||||
});
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
olmDevice.verifySignature = expect.createSpy();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
|
||||
|
||||
mockCrypto.downloadKeys.andReturn(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: expect.createSpy().andReturn(false),
|
||||
isUnverified: expect.createSpy().andReturn(false),
|
||||
getIdentityKey: expect.createSpy().andReturn(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: expect.createSpy().andReturn(''),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: expect.createSpy().andReturn(
|
||||
[{userId: "@alice:home.server"}],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.reset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
return olmDevice;
|
||||
}
|
||||
|
||||
async function setupSession(initiator, opponent) {
|
||||
await opponent.generateOneTimeKeys(1);
|
||||
const keys = await opponent.getOneTimeKeys();
|
||||
const firstKey = Object.values(keys['curve25519'])[0];
|
||||
|
||||
const sid = await initiator.createOutboundSession(
|
||||
opponent.deviceCurve25519Key, firstKey,
|
||||
);
|
||||
return sid;
|
||||
}
|
||||
|
||||
describe("OlmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let aliceOlmDevice;
|
||||
let bobOlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
await global.Olm.init();
|
||||
|
||||
aliceOlmDevice = makeOlmDevice();
|
||||
bobOlmDevice = makeOlmDevice();
|
||||
await aliceOlmDevice.init();
|
||||
await bobOlmDevice.init();
|
||||
});
|
||||
|
||||
describe('olm', function() {
|
||||
it("can decrypt messages", async function() {
|
||||
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sid,
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
|
||||
const result = await bobOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
expect(result.payload).toEqual(
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../..';
|
||||
import algorithms from '../../../lib/crypto/algorithms';
|
||||
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../MockStorageApi';
|
||||
import testUtils from '../../test-utils';
|
||||
|
||||
import OlmDevice from '../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../lib/crypto';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
|
||||
const ENCRYPTED_EVENT = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: '!ROOM:ID',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: 'SENDER_CURVE25519',
|
||||
session_id: SESSION_ID,
|
||||
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
|
||||
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
|
||||
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
|
||||
},
|
||||
event_id: '$event1',
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const KEY_BACKUP_DATA = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
|
||||
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
|
||||
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
|
||||
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
|
||||
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
|
||||
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
|
||||
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
|
||||
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
|
||||
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
|
||||
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
|
||||
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
|
||||
mac: '5lxYBHQU80M',
|
||||
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
|
||||
},
|
||||
};
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
}
|
||||
|
||||
describe("MegolmBackup", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockStorage;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
});
|
||||
|
||||
describe("backup", function() {
|
||||
let mockBaseApis;
|
||||
let realSetTimeout;
|
||||
|
||||
beforeEach(function() {
|
||||
mockBaseApis = {};
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// to tick the clock between the first try and the retry.
|
||||
realSetTimeout = global.setTimeout;
|
||||
global.setTimeout = function(f, n) {
|
||||
return realSetTimeout(f, n/100);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.setTimeout = realSetTimeout;
|
||||
});
|
||||
|
||||
it('automatically calls the key back up', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: ROOM_ID,
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
},
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
|
||||
mockCrypto.decryptEvent = function() {
|
||||
return Promise.resolve(decryptedData);
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = expect.createSpy();
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "foobar",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
);
|
||||
}
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("restore", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = makeTestClient(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('can restore from backup', function() {
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room', function() {
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[SESSION_ID]: KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -139,6 +139,9 @@ describe("MatrixClient", function() {
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
@@ -182,7 +185,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", function(done) {
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
@@ -191,15 +194,19 @@ describe("MatrixClient", function() {
|
||||
const filter = new sdk.Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
store.getFilter.andReturn(filter);
|
||||
client.startClient();
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener("sync", syncListener);
|
||||
resolve();
|
||||
} else if (state === "ERROR") {
|
||||
reject(new Error("sync error"));
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await syncPromise;
|
||||
});
|
||||
|
||||
describe("getSyncState", function() {
|
||||
@@ -207,15 +214,18 @@ describe("MatrixClient", function() {
|
||||
expect(client.getSyncState()).toBe(null);
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", function(done) {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
it("should return the same sync state as emitted sync events", async function() {
|
||||
const syncingPromise = new Promise((resolve) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
client.removeListener("sync", syncListener);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await syncingPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -258,8 +268,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", function() {
|
||||
client.startClient();
|
||||
it("should return false if there is no request waiting", async function() {
|
||||
await client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -201,22 +201,6 @@ describe("RoomMember", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("supersedesOutOfBand", function() {
|
||||
it("should be set by markSupersedesOutOfBand", function() {
|
||||
const member = new RoomMember();
|
||||
expect(member.supersedesOutOfBand()).toEqual(false);
|
||||
member.markSupersedesOutOfBand();
|
||||
expect(member.supersedesOutOfBand()).toEqual(true);
|
||||
});
|
||||
it("should be cleared by clearSupersedesOutOfBand", function() {
|
||||
const member = new RoomMember();
|
||||
member.markSupersedesOutOfBand();
|
||||
expect(member.supersedesOutOfBand()).toEqual(true);
|
||||
member.clearSupersedesOutOfBand();
|
||||
expect(member.supersedesOutOfBand()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setMembershipEvent", function() {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
@@ -301,5 +285,52 @@ describe("RoomMember", function() {
|
||||
member.setMembershipEvent(joinEvent); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should set 'name' to user_id if it is just whitespace", function() {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: " \u200b ",
|
||||
});
|
||||
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual(userA); // it should fallback because all whitespace
|
||||
});
|
||||
|
||||
it("should disambiguate users on a fuzzy displayname match", function() {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: "Alíce\u200b", // note diacritic and zero width char
|
||||
});
|
||||
|
||||
const roomState = {
|
||||
getStateEvents: function(type) {
|
||||
if (type !== "m.room.member") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userC, name: "Alice",
|
||||
}),
|
||||
joinEvent,
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toNotEqual("Alíce"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,41 +299,25 @@ describe("RoomState", function() {
|
||||
expect(eventReceived).toEqual(true);
|
||||
});
|
||||
|
||||
it("should overwrite existing members", function() {
|
||||
it("should never overwrite existing members", function() {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).toEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(true);
|
||||
});
|
||||
|
||||
it("should allow later state events to overwrite", function() {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
state.setStateEvents([memberEvent]);
|
||||
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).toEqual(memberEvent.getId());
|
||||
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
});
|
||||
|
||||
it("should emit members when updating a member", function() {
|
||||
const doesntExistYetUserId = "@doesntexistyet:hs";
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.members', (_, __, member) => {
|
||||
expect(member.userId).toEqual(userA);
|
||||
expect(member.userId).toEqual(doesntExistYetUserId);
|
||||
eventReceived = true;
|
||||
});
|
||||
|
||||
@@ -341,28 +325,6 @@ describe("RoomState", function() {
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
expect(eventReceived).toEqual(true);
|
||||
});
|
||||
|
||||
|
||||
it("should not overwrite members updated since starting loading oob",
|
||||
function() {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
|
||||
const existingMemberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setStateEvents([existingMemberEvent]);
|
||||
expect(state.getMember(userA).supersedesOutOfBand()).toEqual(true);
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).toEqual(existingMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
expect(memberA.supersedesOutOfBand()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clone", function() {
|
||||
@@ -386,20 +348,6 @@ describe("RoomState", function() {
|
||||
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
|
||||
});
|
||||
|
||||
it("should copy supersedes flag when OOB loading is progress",
|
||||
function() {
|
||||
// include OOB members in copy
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setStateEvents([utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
})]);
|
||||
const copy = state.clone();
|
||||
const memberA = state.getMember(userA);
|
||||
const memberACopy = copy.getMember(userA);
|
||||
expect(memberA.supersedesOutOfBand()).toEqual(true);
|
||||
expect(memberACopy.supersedesOutOfBand()).toEqual(true);
|
||||
});
|
||||
|
||||
it("should mark old copy as not waiting for out of band anymore", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
const copy = state.clone();
|
||||
|
||||
+20
-8
@@ -1402,13 +1402,25 @@ describe("Room", function() {
|
||||
it("should return synced membership if membership isn't available yet",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.setSyncedMembership("invite");
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.getMyMembership()).toEqual("invite");
|
||||
room.addLiveEvents([utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true,
|
||||
})]);
|
||||
});
|
||||
it("should emit a Room.myMembership event on a change",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const events = [];
|
||||
room.on("Room.myMembership", (_room, membership, oldMembership) => {
|
||||
events.push({membership, oldMembership});
|
||||
});
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.getMyMembership()).toEqual("invite");
|
||||
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
|
||||
events.splice(0); //clear
|
||||
room.updateMyMembership("invite");
|
||||
expect(events.length).toEqual(0);
|
||||
room.updateMyMembership("join");
|
||||
expect(room.getMyMembership()).toEqual("join");
|
||||
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1439,11 +1451,11 @@ describe("Room", function() {
|
||||
it("should return false if synced membership not join",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.setSyncedMembership("invite");
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.maySendMessage()).toEqual(false);
|
||||
room.setSyncedMembership("leave");
|
||||
room.updateMyMembership("leave");
|
||||
expect(room.maySendMessage()).toEqual(false);
|
||||
room.setSyncedMembership("join");
|
||||
room.updateMyMembership("join");
|
||||
expect(room.maySendMessage()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const logger = require("./logger");
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
|
||||
/**
|
||||
* Description for what an automatically discovered client configuration
|
||||
* would look like. Although this is a class, it is recommended that it
|
||||
* be treated as an interface definition rather than as a class.
|
||||
*
|
||||
* Additional properties than those defined here may be present, and
|
||||
* should follow the Java package naming convention.
|
||||
*/
|
||||
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
|
||||
// Dev note: this is basically a copy/paste of the .well-known response
|
||||
// object as defined in the spec. It does have additional information,
|
||||
// however. Overall, this exists to serve as a place for documentation
|
||||
// and not functionality.
|
||||
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* The homeserver configuration the client should use. This will
|
||||
* always be present on the object.
|
||||
* @type {{state: string, base_url: string}} The configuration.
|
||||
*/
|
||||
this["m.homeserver"] = {
|
||||
/**
|
||||
* The lookup result state. If this is anything other than
|
||||
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
|
||||
* if this is not AutoDiscovery.SUCCESS then the client should
|
||||
* assume the other properties in the client config (such as
|
||||
* the identity server configuration) are not valid.
|
||||
*/
|
||||
state: AutoDiscovery.PROMPT,
|
||||
|
||||
/**
|
||||
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
|
||||
* then this will contain a human-readable (English) message
|
||||
* for what went wrong. If the state is none of those previously
|
||||
* mentioned, this will be falsey.
|
||||
*/
|
||||
error: "Something went wrong",
|
||||
|
||||
/**
|
||||
* The base URL clients should use to talk to the homeserver,
|
||||
* particularly for the login process. May be falsey if the
|
||||
* state is not AutoDiscovery.SUCCESS.
|
||||
*/
|
||||
base_url: "https://matrix.org",
|
||||
};
|
||||
|
||||
/**
|
||||
* The identity server configuration the client should use. This
|
||||
* will always be present on teh object.
|
||||
* @type {{state: string, base_url: string}} The configuration.
|
||||
*/
|
||||
this["m.identity_server"] = {
|
||||
/**
|
||||
* The lookup result state. If this is anything other than
|
||||
* AutoDiscovery.SUCCESS then base_url may be falsey.
|
||||
*/
|
||||
state: AutoDiscovery.PROMPT,
|
||||
|
||||
/**
|
||||
* The base URL clients should use for interacting with the
|
||||
* identity server. May be falsey if the state is not
|
||||
* AutoDiscovery.SUCCESS.
|
||||
*/
|
||||
base_url: "https://vector.im",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for automatically discovery resources, such as homeservers
|
||||
* for users to log in to.
|
||||
*/
|
||||
export class AutoDiscovery {
|
||||
// Dev note: the constants defined here are related to but not
|
||||
// exactly the same as those in the spec. This is to hopefully
|
||||
// translate the meaning of the states in the spec, but also
|
||||
// support our own if needed.
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
* the error to the user and refuse logging in.
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get FAIL_ERROR() { return "FAIL_ERROR"; }
|
||||
|
||||
/**
|
||||
* The auto discovery failed, however the client may still recover
|
||||
* from the problem. The client is recommended to that the same
|
||||
* action it would for PROMPT while also warning the user about
|
||||
* what went wrong. The client may also treat this the same as
|
||||
* a FAIL_ERROR state.
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
|
||||
|
||||
/**
|
||||
* The auto discovery didn't fail but did not find anything of
|
||||
* interest. The client is expected to prompt the user for more
|
||||
* information, or fail if it prefers.
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get PROMPT() { return "PROMPT"; }
|
||||
|
||||
/**
|
||||
* The auto discovery was successful.
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get SUCCESS() { return "SUCCESS"; }
|
||||
|
||||
/**
|
||||
* Attempts to automatically discover client configuration information
|
||||
* prior to logging in. Such information includes the homeserver URL
|
||||
* and identity server URL the client would want. Additional details
|
||||
* may also be discovered, and will be transparently included in the
|
||||
* response object unaltered.
|
||||
* @param {string} domain The homeserver domain to perform discovery
|
||||
* on. For example, "matrix.org".
|
||||
* @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when discovery fails.
|
||||
*/
|
||||
static async findClientConfig(domain) {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
|
||||
// We use a .well-known lookup for all cases. According to the spec, we
|
||||
// can do other discovery mechanisms if we want such as custom lookups
|
||||
// however we won't bother with that here (mostly because the spec only
|
||||
// supports .well-known right now).
|
||||
//
|
||||
// By using .well-known, we need to ensure we at least pull out a URL
|
||||
// for the homeserver. We don't really need an identity server configuration
|
||||
// but will return one anyways (with state PROMPT) to make development
|
||||
// easier for clients. If we can't get a homeserver URL, all bets are
|
||||
// off on the rest of the config and we'll assume it is invalid too.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
// write. We'll update the properties of this object over the duration
|
||||
// of this function.
|
||||
const clientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: "Invalid homeserver discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
// Technically, we don't have a problem with the identity server
|
||||
// config at this point.
|
||||
state: AutoDiscovery.PROMPT,
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Step 1: Actually request the .well-known JSON file and make sure it
|
||||
// at least has a homeserver definition.
|
||||
const wellknown = await this._fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!wellknown || wellknown.action !== "SUCCESS"
|
||||
|| !wellknown.raw["m.homeserver"]
|
||||
|| !wellknown.raw["m.homeserver"]["base_url"]) {
|
||||
logger.error("No m.homeserver key in well-known response");
|
||||
if (wellknown.reason) logger.error(wellknown.reason);
|
||||
if (wellknown.action === "IGNORE") {
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.PROMPT,
|
||||
error: null,
|
||||
base_url: null,
|
||||
};
|
||||
} else {
|
||||
// this can only ever be FAIL_PROMPT at this point.
|
||||
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
|
||||
}
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
|
||||
// sure it points to a homeserver in Step 3.
|
||||
const hsUrl = this._sanitizeWellKnownUrl(
|
||||
wellknown.raw["m.homeserver"]["base_url"],
|
||||
);
|
||||
if (!hsUrl) {
|
||||
logger.error("Invalid base_url for m.homeserver");
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this._fetchWellKnownObject(
|
||||
`${hsUrl}/_matrix/client/versions`,
|
||||
);
|
||||
if (!hsVersions || !hsVersions.raw["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 4: Now that the homeserver looks valid, update our client config.
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
base_url: hsUrl,
|
||||
};
|
||||
|
||||
// Step 5: Try to pull out the identity server configuration
|
||||
let isUrl = "";
|
||||
if (wellknown.raw["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch. Note that we also fail the homeserver check in the
|
||||
// object because according to the spec we're supposed to FAIL_ERROR
|
||||
// if *anything* goes wrong with the IS validation, including invalid
|
||||
// format. This means we're supposed to stop discovery completely.
|
||||
const failingClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: "Invalid identity server discovery response",
|
||||
|
||||
// We'll provide the base_url that was previously valid for
|
||||
// debugging purposes.
|
||||
base_url: clientConfig["m.homeserver"].base_url,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: "Invalid identity server discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
|
||||
// points to an identity server in Step 5b.
|
||||
isUrl = this._sanitizeWellKnownUrl(
|
||||
wellknown.raw["m.identity_server"]["base_url"],
|
||||
);
|
||||
if (!isUrl) {
|
||||
logger.error("Invalid base_url for m.identity_server");
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
|
||||
// Step 5b: Verify there is an identity server listening on the provided
|
||||
// URL.
|
||||
const isResponse = await this._fetchWellKnownObject(
|
||||
`${isUrl}/_matrix/identity/api/v1`,
|
||||
);
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
|
||||
logger.error("Invalid /api/v1 response");
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Now that the identity server is valid, or never existed,
|
||||
// populate the IS section.
|
||||
if (isUrl && isUrl.length > 0) {
|
||||
clientConfig["m.identity_server"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
base_url: isUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 7: Copy any other keys directly into the clientConfig. This is for
|
||||
// things like custom configuration of services.
|
||||
Object.keys(wellknown.raw)
|
||||
.filter((k) => k !== "m.homeserver" && k !== "m.identity_server")
|
||||
.map((k) => clientConfig[k] = wellknown.raw[k]);
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
|
||||
* is suitable for the requirements laid out by .well-known auto discovery.
|
||||
* If valid, the URL will also be stripped of any trailing slashes.
|
||||
* @param {string} url The potentially invalid URL to sanitize.
|
||||
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
|
||||
* @private
|
||||
*/
|
||||
static _sanitizeWellKnownUrl(url) {
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
// We have to try and parse the URL using the NodeJS URL
|
||||
// library if we're on NodeJS and use the browser's URL
|
||||
// library when we're in a browser. To accomplish this, we
|
||||
// try the NodeJS version first and fall back to the browser.
|
||||
let parsed = null;
|
||||
try {
|
||||
if (NodeURL) parsed = new NodeURL(url);
|
||||
else parsed = new URL(url);
|
||||
} catch (e) {
|
||||
parsed = new URL(url);
|
||||
}
|
||||
|
||||
if (!parsed || !parsed.hostname) return false;
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
||||
|
||||
const port = parsed.port ? `:${parsed.port}` : "";
|
||||
const path = parsed.pathname ? parsed.pathname : "";
|
||||
let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
|
||||
if (saferUrl.endsWith("/")) {
|
||||
saferUrl = saferUrl.substring(0, saferUrl.length - 1);
|
||||
}
|
||||
return saferUrl;
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a JSON object from a given URL, as expected by all .well-known
|
||||
* related lookups. If the server gives a 404 then the `action` will be
|
||||
* IGNORE. If the server returns something that isn't JSON, the `action`
|
||||
* will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
|
||||
*
|
||||
* The returned object will be a result of the call in object form with
|
||||
* the following properties:
|
||||
* raw: The JSON object returned by the server.
|
||||
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
|
||||
* reason: Relatively human readable description of what went wrong.
|
||||
* error: The actual Error, if one exists.
|
||||
* @param {string} url The URL to fetch a JSON object from.
|
||||
* @return {Promise<object>} Resolves to the returned state.
|
||||
* @private
|
||||
*/
|
||||
static async _fetchWellKnownObject(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const request = require("./matrix").getRequest();
|
||||
if (!request) throw new Error("No request library available");
|
||||
request(
|
||||
{ method: "GET", uri: url },
|
||||
(err, response, body) => {
|
||||
if (err || response.statusCode < 200 || response.statusCode >= 300) {
|
||||
let action = "FAIL_PROMPT";
|
||||
let reason = (err ? err.message : null) || "General failure";
|
||||
if (response.statusCode === 404) {
|
||||
action = "IGNORE";
|
||||
reason = "No .well-known JSON file found";
|
||||
}
|
||||
resolve({raw: {}, action: action, reason: reason, error: err});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resolve({raw: JSON.parse(body), action: "SUCCESS"});
|
||||
} catch (e) {
|
||||
let reason = "General failure";
|
||||
if (e.name === "SyntaxError") reason = "Invalid JSON";
|
||||
resolve({
|
||||
raw: {},
|
||||
action: "FAIL_PROMPT",
|
||||
reason: reason, error: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+71
-4
@@ -262,7 +262,19 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
|
||||
utils.extend(login_data, data);
|
||||
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/login", undefined, login_data,
|
||||
(error, response) => {
|
||||
if (loginType === "m.login.password" && response &&
|
||||
response.access_token && response.user_id) {
|
||||
this._http.opts.accessToken = response.access_token;
|
||||
this.credentials = {
|
||||
userId: response.user_id,
|
||||
};
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(error, response);
|
||||
}
|
||||
}, "POST", "/login", undefined, login_data,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -298,9 +310,23 @@ MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
|
||||
* @return {string} The HS URL to hit to begin the CAS login process.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
|
||||
return this._http.getUrl("/login/cas/redirect", {
|
||||
return this.getSsoLoginUrl(redirectUrl, "cas");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} redirectUrl The URL to redirect to after the HS
|
||||
* authenticates with the SSO.
|
||||
* @param {string} loginType The type of SSO login we are doing (sso or cas).
|
||||
* Defaults to 'sso'.
|
||||
* @return {string} The HS URL to hit to begin the SSO login process.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
|
||||
if (loginType === undefined) {
|
||||
loginType = "sso";
|
||||
}
|
||||
return this._http.getUrl("/login/"+loginType+"/redirect", {
|
||||
"redirectUrl": redirectUrl,
|
||||
}, httpApi.PREFIX_UNSTABLE);
|
||||
}, httpApi.PREFIX_R0);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -927,6 +953,28 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {module:client.Promise} Resolves: A list of the user's current rooms
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getJoinedRooms = function() {
|
||||
const path = utils.encodeUri("/joined_rooms");
|
||||
return this._http.authedRequest(undefined, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve membership info. for a room.
|
||||
* @param {string} roomId ID of the room to get membership for
|
||||
* @return {module:client.Promise} Resolves: A list of currently joined users
|
||||
* and their profile data.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) {
|
||||
const path = utils.encodeUri("/rooms/$roomId/joined_members", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
return this._http.authedRequest(undefined, "GET", path);
|
||||
};
|
||||
|
||||
// Room Directory operations
|
||||
// =========================
|
||||
@@ -1785,7 +1833,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
|
||||
* Get information on how a specific place on a third party protocol
|
||||
* may be reached.
|
||||
* @param {string} protocol The protocol given in getThirdpartyProtocols()
|
||||
* @param {object} params Protocol-specific parameters, as given in th
|
||||
* @param {object} params Protocol-specific parameters, as given in the
|
||||
* response to getThirdpartyProtocols()
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
*/
|
||||
@@ -1800,6 +1848,25 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information on how a specific user on a third party protocol
|
||||
* may be reached.
|
||||
* @param {string} protocol The protocol given in getThirdpartyProtocols()
|
||||
* @param {object} params Protocol-specific parameters, as given in the
|
||||
* response to getThirdpartyProtocols()
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
|
||||
const path = utils.encodeUri("/thirdparty/user/$protocol", {
|
||||
$protocol: protocol,
|
||||
});
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "GET", path, params, undefined,
|
||||
httpApi.PREFIX_UNSTABLE,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MatrixBaseApis object
|
||||
*/
|
||||
|
||||
+472
-55
@@ -41,30 +41,46 @@ const SyncApi = require("./sync");
|
||||
const MatrixBaseApis = require("./base-apis");
|
||||
const MatrixError = httpApi.MatrixError;
|
||||
const ContentHelpers = require("./content-helpers");
|
||||
const olmlib = require("./crypto/olmlib");
|
||||
|
||||
import ReEmitter from './ReEmitter';
|
||||
import RoomList from './crypto/RoomList';
|
||||
|
||||
import Crypto from './crypto';
|
||||
import { isCryptoAvailable } from './crypto';
|
||||
import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
|
||||
import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password';
|
||||
import { randomString } from './randomstring';
|
||||
|
||||
const LAZY_LOADING_MESSAGES_FILTER = {
|
||||
lazy_load_members: true,
|
||||
};
|
||||
|
||||
const LAZY_LOADING_SYNC_FILTER = {
|
||||
room: {
|
||||
state: LAZY_LOADING_MESSAGES_FILTER,
|
||||
},
|
||||
};
|
||||
// Disable warnings for now: we use deprecated bluebird functions
|
||||
// and need to migrate, but they spam the console with warnings.
|
||||
Promise.config({warnings: false});
|
||||
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
let CRYPTO_ENABLED = false;
|
||||
const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
|
||||
try {
|
||||
var Crypto = require("./crypto");
|
||||
CRYPTO_ENABLED = true;
|
||||
} catch (e) {
|
||||
console.warn("Unable to load crypto module: crypto will be disabled: " + e);
|
||||
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
|
||||
const keys = [];
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
const decrypted = keyFromRecoverySession(sessionData, decryptionKey);
|
||||
decrypted.session_id = sessionId;
|
||||
decrypted.room_id = roomId;
|
||||
keys.push(decrypted);
|
||||
} catch (e) {
|
||||
console.log("Failed to decrypt session from backup");
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function keyFromRecoverySession(session, decryptionKey) {
|
||||
return JSON.parse(decryptionKey.decrypt(
|
||||
session.session_data.ephemeral,
|
||||
session.session_data.mac,
|
||||
session.session_data.ciphertext,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,6 +156,8 @@ function MatrixClient(opts) {
|
||||
|
||||
MatrixBaseApis.call(this, opts);
|
||||
|
||||
this.olmVersion = null; // Populated after initCrypto is done
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
this.store = opts.store || new StubStore();
|
||||
@@ -192,10 +210,6 @@ function MatrixClient(opts) {
|
||||
|
||||
this._forceTURN = opts.forceTURN || false;
|
||||
|
||||
if (CRYPTO_ENABLED) {
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
}
|
||||
|
||||
// List of which rooms have encryption enabled: separate from crypto because
|
||||
// we still want to know which rooms are encrypted even if crypto is disabled:
|
||||
// we don't want to start sending unencrypted events to them.
|
||||
@@ -385,6 +399,13 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
|
||||
* successfully initialised.
|
||||
*/
|
||||
MatrixClient.prototype.initCrypto = async function() {
|
||||
if (!isCryptoAvailable()) {
|
||||
throw new Error(
|
||||
`End-to-end encryption not supported in this js-sdk build: did ` +
|
||||
`you remember to load the olm library?`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this._crypto) {
|
||||
console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
|
||||
return;
|
||||
@@ -402,13 +423,6 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
// initialise the list of encrypted rooms (whether or not crypto is enabled)
|
||||
await this._roomList.init();
|
||||
|
||||
if (!CRYPTO_ENABLED) {
|
||||
throw new Error(
|
||||
`End-to-end encryption not supported in this js-sdk build: did ` +
|
||||
`you remember to load the olm library?`,
|
||||
);
|
||||
}
|
||||
|
||||
const userId = this.getUserId();
|
||||
if (userId === null) {
|
||||
throw new Error(
|
||||
@@ -433,6 +447,7 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
);
|
||||
|
||||
this.reEmitter.reEmit(crypto, [
|
||||
"crypto.keyBackupFailed",
|
||||
"crypto.roomKeyRequest",
|
||||
"crypto.roomKeyRequestCancellation",
|
||||
"crypto.warning",
|
||||
@@ -440,6 +455,9 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
|
||||
await crypto.init();
|
||||
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
|
||||
|
||||
// if crypto initialisation was successful, tell it to attach its event
|
||||
// handlers.
|
||||
crypto.registerEventHandlers(this);
|
||||
@@ -543,7 +561,15 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified)
|
||||
if (verified === undefined) {
|
||||
verified = true;
|
||||
}
|
||||
return _setDeviceVerification(this, userId, deviceId, verified, null);
|
||||
const prom = _setDeviceVerification(this, userId, deviceId, verified, null);
|
||||
|
||||
// if one of the user's own devices is being marked as verified / unverified,
|
||||
// check the key backup status, since whether or not we use this depends on
|
||||
// whether it has a signature from a verified device
|
||||
if (userId == this.credentials.userId) {
|
||||
this._crypto.checkKeyBackup();
|
||||
}
|
||||
return prom;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -747,6 +773,333 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
|
||||
return this._crypto.importRoomKeys(keys);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information about the current key backup.
|
||||
* @returns {Promise} Information object from API or null
|
||||
*/
|
||||
MatrixClient.prototype.getKeyBackupVersion = function() {
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", "/room_keys/version",
|
||||
).then((res) => {
|
||||
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
|
||||
const err = "Unknown backup algorithm: " + res.algorithm;
|
||||
return Promise.reject(err);
|
||||
} else if (!(typeof res.auth_data === "object")
|
||||
|| !res.auth_data.public_key) {
|
||||
const err = "Invalid backup data returned";
|
||||
return Promise.reject(err);
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (e.errcode === 'M_NOT_FOUND') {
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} info key backup info dict from getKeyBackupVersion()
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool],
|
||||
* device: [DeviceInfo],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
MatrixClient.prototype.isKeyBackupTrusted = function(info) {
|
||||
return this._crypto.isKeyBackupTrusted(info);
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {bool} true if the client is configured to back up keys to
|
||||
* the server, otherwise false.
|
||||
*/
|
||||
MatrixClient.prototype.getKeyBackupEnabled = function() {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return Boolean(this._crypto.backupKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable backing up of keys, using data previously returned from
|
||||
* getKeyBackupVersion.
|
||||
*
|
||||
* @param {object} info Backup information object as returned by getKeyBackupVersion
|
||||
*/
|
||||
MatrixClient.prototype.enableKeyBackup = function(info) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
this._crypto.backupInfo = info;
|
||||
if (this._crypto.backupKey) this._crypto.backupKey.free();
|
||||
this._crypto.backupKey = new global.Olm.PkEncryption();
|
||||
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
|
||||
|
||||
this.emit('crypto.keyBackupStatus', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable backing up of keys.
|
||||
*/
|
||||
MatrixClient.prototype.disableKeyBackup = function() {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
this._crypto.backupInfo = null;
|
||||
if (this._crypto.backupKey) this._crypto.backupKey.free();
|
||||
this._crypto.backupKey = null;
|
||||
|
||||
this.emit('crypto.keyBackupStatus', false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up the data required to create a new backup version. The backup version
|
||||
* will not be created and enabled until createKeyBackupVersion is called.
|
||||
*
|
||||
* @param {string} password Passphrase string that can be entered by the user
|
||||
* when restoring the backup as an alternative to entering the recovery key.
|
||||
* Optional.
|
||||
*
|
||||
* @returns {Promise<object>} Object that can be passed to createKeyBackupVersion and
|
||||
* additionally has a 'recovery_key' member with the user-facing recovery key string.
|
||||
*/
|
||||
MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
let publicKey;
|
||||
const authData = {};
|
||||
if (password) {
|
||||
const keyInfo = await keyForNewBackup(password);
|
||||
publicKey = decryption.init_with_private_key(keyInfo.key);
|
||||
authData.private_key_salt = keyInfo.salt;
|
||||
authData.private_key_iterations = keyInfo.iterations;
|
||||
} else {
|
||||
publicKey = decryption.generate_key();
|
||||
}
|
||||
|
||||
authData.public_key = publicKey;
|
||||
|
||||
return {
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
auth_data: authData,
|
||||
recovery_key: encodeRecoveryKey(decryption.get_private_key()),
|
||||
};
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new key backup version and enable it, using the information return
|
||||
* from prepareKeyBackupVersion.
|
||||
*
|
||||
* @param {object} info Info object from prepareKeyBackupVersion
|
||||
* @returns {Promise<object>} Object with 'version' param indicating the version created
|
||||
*/
|
||||
MatrixClient.prototype.createKeyBackupVersion = function(info) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const data = {
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
};
|
||||
return this._crypto._signObject(data.auth_data).then(() => {
|
||||
return this._http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version", undefined, data,
|
||||
);
|
||||
}).then((res) => {
|
||||
this.enableKeyBackup({
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
version: res.version,
|
||||
});
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
// If we're currently backing up to this backup... stop.
|
||||
// (We start using it automatically in createKeyBackupVersion
|
||||
// so this is symmetrical).
|
||||
if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) {
|
||||
this.disableKeyBackup();
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/room_keys/version/$version", {
|
||||
$version: version,
|
||||
});
|
||||
|
||||
return this._http.authedRequest(
|
||||
undefined, "DELETE", path, undefined, undefined,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) {
|
||||
let path;
|
||||
if (sessionId !== undefined) {
|
||||
path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", {
|
||||
$roomId: roomId,
|
||||
$sessionId: sessionId,
|
||||
});
|
||||
} else if (roomId !== undefined) {
|
||||
path = utils.encodeUri("/room_keys/keys/$roomId", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
} else {
|
||||
path = "/room_keys/keys";
|
||||
}
|
||||
const queryData = version === undefined ? undefined : { version: version };
|
||||
return {
|
||||
path: path,
|
||||
queryData: queryData,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Back up session keys to the homeserver.
|
||||
* @param {string} roomId ID of the room that the keys are for Optional.
|
||||
* @param {string} sessionId ID of the session that the keys are for Optional.
|
||||
* @param {integer} version backup version Optional.
|
||||
* @param {object} data Object keys to send
|
||||
* @return {module:client.Promise} a promise that will resolve when the keys
|
||||
* are uploaded
|
||||
*/
|
||||
MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const path = this._makeKeyBackupPath(roomId, sessionId, version);
|
||||
return this._http.authedRequest(
|
||||
undefined, "PUT", path.path, path.queryData, data,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.backupAllGroupSessions = function(version) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
return this._crypto.backupAllGroupSessions(version);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
try {
|
||||
decodeRecoveryKey(recoveryKey);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
|
||||
password, targetRoomId, targetSessionId, version,
|
||||
) {
|
||||
const backupInfo = await this.getKeyBackupVersion();
|
||||
|
||||
const privKey = await keyForExistingBackup(backupInfo, password);
|
||||
return this._restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
|
||||
recoveryKey, targetRoomId, targetSessionId, version,
|
||||
) {
|
||||
const privKey = decodeRecoveryKey(recoveryKey);
|
||||
return this._restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype._restoreKeyBackup = function(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
let totalKeyCount = 0;
|
||||
let keys = [];
|
||||
|
||||
const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version);
|
||||
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
decryption.init_with_private_key(privKey);
|
||||
} catch(e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", path.path, path.queryData,
|
||||
).then((res) => {
|
||||
if (res.rooms) {
|
||||
for (const [roomId, roomData] of Object.entries(res.rooms)) {
|
||||
if (!roomData.sessions) continue;
|
||||
|
||||
totalKeyCount += Object.keys(roomData.sessions).length;
|
||||
const roomKeys = keysFromRecoverySession(
|
||||
roomData.sessions, decryption, roomId, roomKeys,
|
||||
);
|
||||
for (const k of roomKeys) {
|
||||
k.room_id = roomId;
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
} else if (res.sessions) {
|
||||
totalKeyCount = Object.keys(res.sessions).length;
|
||||
keys = keysFromRecoverySession(
|
||||
res.sessions, decryption, targetRoomId, keys,
|
||||
);
|
||||
} else {
|
||||
totalKeyCount = 1;
|
||||
try {
|
||||
const key = keyFromRecoverySession(res, decryption);
|
||||
key.room_id = targetRoomId;
|
||||
key.session_id = targetSessionId;
|
||||
keys.push(key);
|
||||
} catch (e) {
|
||||
console.log("Failed to decrypt session from backup");
|
||||
}
|
||||
}
|
||||
|
||||
return this.importRoomKeys(keys);
|
||||
}).then(() => {
|
||||
return {total: totalKeyCount, imported: keys.length};
|
||||
}).finally(() => {
|
||||
decryption.free();
|
||||
});
|
||||
};
|
||||
|
||||
MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const path = this._makeKeyBackupPath(roomId, sessionId, version);
|
||||
return this._http.authedRequest(
|
||||
undefined, "DELETE", path.path, path.queryData,
|
||||
);
|
||||
};
|
||||
|
||||
// Group ops
|
||||
// =========
|
||||
// Operations on groups that come down the sync stream (ie. ones the
|
||||
@@ -771,6 +1124,17 @@ MatrixClient.prototype.getGroups = function() {
|
||||
return this.store.getGroups();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the config for the media repository.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves with an object containing the config.
|
||||
*/
|
||||
MatrixClient.prototype.getMediaConfig = function(callback) {
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0,
|
||||
);
|
||||
};
|
||||
|
||||
// Room ops
|
||||
// ========
|
||||
|
||||
@@ -917,6 +1281,8 @@ MatrixClient.prototype.isUserIgnored = function(userId) {
|
||||
* </strong> Default: true.
|
||||
* @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite,
|
||||
* the signing URL is passed in this parameter.
|
||||
* @param {string[]} opts.viaServers The server names to try and join through in
|
||||
* addition to those that are automatically chosen.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Room object.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
@@ -945,6 +1311,13 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
);
|
||||
}
|
||||
|
||||
const queryString = {};
|
||||
if (opts.viaServers) {
|
||||
queryString["server_name"] = opts.viaServers;
|
||||
}
|
||||
|
||||
const reqOpts = {qsStringifyOptions: {arrayFormat: 'repeat'}};
|
||||
|
||||
const defer = Promise.defer();
|
||||
|
||||
const self = this;
|
||||
@@ -955,7 +1328,8 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias});
|
||||
return self._http.authedRequest(undefined, "POST", path, undefined, data);
|
||||
return self._http.authedRequest(
|
||||
undefined, "POST", path, queryString, data, reqOpts);
|
||||
}).then(function(res) {
|
||||
const roomId = res.room_id;
|
||||
const syncApi = new SyncApi(self, self._clientOpts);
|
||||
@@ -1175,6 +1549,13 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
room.addPendingEvent(localEvent, txnId);
|
||||
}
|
||||
|
||||
// addPendingEvent can change the state to NOT_SENT if it believes
|
||||
// that there's other events that have failed. We won't bother to
|
||||
// try sending the event if the state has changed as such.
|
||||
if (localEvent.status === EventStatus.NOT_SENT) {
|
||||
return Promise.reject(new Error("Event blocked by other events not yet sent"));
|
||||
}
|
||||
|
||||
return _sendEvent(this, room, localEvent, callback);
|
||||
};
|
||||
|
||||
@@ -1882,6 +2263,27 @@ MatrixClient.prototype.mxcUrlToHttp =
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a new status message for the user. The message may be null/falsey
|
||||
* to clear the message.
|
||||
* @param {string} newMessage The new message to set.
|
||||
* @return {module:client.Promise} Resolves: to nothing
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
|
||||
return Promise.all(this.getRooms().map((room) => {
|
||||
const isJoined = room.getMyMembership() === "join";
|
||||
const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
|
||||
if (isJoined && looksLikeDm) {
|
||||
return this.sendStateEvent(room.roomId, "im.vector.user_status", {
|
||||
status: newMessage,
|
||||
}, this.getUserId());
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} opts Options to apply
|
||||
* @param {string} opts.presence One of "online", "offline" or "unavailable"
|
||||
@@ -2067,7 +2469,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
|
||||
|
||||
let params = undefined;
|
||||
if (this._clientOpts.lazyLoadMembers) {
|
||||
params = {filter: JSON.stringify(LAZY_LOADING_MESSAGES_FILTER)};
|
||||
params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)};
|
||||
}
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||
@@ -2102,7 +2504,8 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
|
||||
self.getEventMapper()));
|
||||
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
|
||||
} else {
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state);
|
||||
const stateEvents = utils.map(res.state, self.getEventMapper());
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
|
||||
}
|
||||
timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
|
||||
|
||||
@@ -2147,7 +2550,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
|
||||
if (this._clientOpts.lazyLoadMembers) {
|
||||
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
||||
// so the timelineFilter doesn't get written into it below
|
||||
filter = Object.assign({}, LAZY_LOADING_MESSAGES_FILTER);
|
||||
filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
|
||||
}
|
||||
if (timelineFilter) {
|
||||
// XXX: it's horrific that /messages' filter parameter doesn't match
|
||||
@@ -3104,21 +3507,6 @@ MatrixClient.prototype.startClient = async function(opts) {
|
||||
// shallow-copy the opts dict before modifying and storing it
|
||||
opts = Object.assign({}, opts);
|
||||
|
||||
if (opts.lazyLoadMembers) {
|
||||
const supported = await this.doesServerSupportLazyLoading();
|
||||
if (supported) {
|
||||
opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER);
|
||||
} else {
|
||||
console.log("LL: lazy loading requested but not supported " +
|
||||
"by server, so disabling");
|
||||
opts.lazyLoadMembers = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.lazyLoadMembers && this._crypto) {
|
||||
this._crypto.enableLazyLoading();
|
||||
}
|
||||
|
||||
opts.crypto = this._crypto;
|
||||
opts.canResetEntireTimeline = (roomId) => {
|
||||
if (!this._canResetTimelineCallback) {
|
||||
@@ -3127,11 +3515,29 @@ MatrixClient.prototype.startClient = async function(opts) {
|
||||
return this._canResetTimelineCallback(roomId);
|
||||
};
|
||||
this._clientOpts = opts;
|
||||
|
||||
this._syncApi = new SyncApi(this, opts);
|
||||
this._syncApi.sync();
|
||||
};
|
||||
|
||||
/**
|
||||
* store client options with boolean/string/numeric values
|
||||
* to know in the next session what flags the sync data was
|
||||
* created with (e.g. lazy loading)
|
||||
* @param {object} opts the complete set of client options
|
||||
* @return {Promise} for store operation */
|
||||
MatrixClient.prototype._storeClientOptions = function() {
|
||||
const primTypes = ["boolean", "string", "number"];
|
||||
const serializableOpts = Object.entries(this._clientOpts)
|
||||
.filter(([key, value]) => {
|
||||
return primTypes.includes(typeof value);
|
||||
})
|
||||
.reduce((obj, [key, value]) => {
|
||||
obj[key] = value;
|
||||
return obj;
|
||||
}, {});
|
||||
return this.store.storeClientOptions(serializableOpts);
|
||||
};
|
||||
|
||||
/**
|
||||
* High level helper method to stop the client from polling and allow a
|
||||
* clean shutdown.
|
||||
@@ -3479,14 +3885,7 @@ MatrixClient.prototype.getEventMapper = function() {
|
||||
* @return {string} A new client secret
|
||||
*/
|
||||
MatrixClient.prototype.generateClientSecret = function() {
|
||||
let ret = "";
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
return randomString(32);
|
||||
};
|
||||
|
||||
/** */
|
||||
@@ -3730,6 +4129,24 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
|
||||
* @event module:client~MatrixClient#"crypto.keyBackupStatus"
|
||||
* @param {bool} enabled true if key backup has been enabled, otherwise false
|
||||
* @example
|
||||
* matrixClient.on("crypto.keyBackupStatus", function(enabled){
|
||||
* if (enabled) {
|
||||
* [...]
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when we want to suggest to the user that they restore their megolm keys
|
||||
* from backup or by cross-signing the device.
|
||||
*
|
||||
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
|
||||
*/
|
||||
|
||||
// EventEmitter JSDocs
|
||||
|
||||
|
||||
+80
-24
@@ -24,6 +24,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../logger';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import olmlib from './olmlib';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
@@ -71,6 +72,9 @@ export default class DeviceList {
|
||||
// }
|
||||
this._devices = {};
|
||||
|
||||
// map of identity keys to the user who owns it
|
||||
this._userByIdentityKey = {};
|
||||
|
||||
// which users we are tracking device status for.
|
||||
// userId -> TRACKING_STATUS_*
|
||||
this._deviceTrackingStatus = {}; // loaded from storage in load()
|
||||
@@ -110,7 +114,7 @@ export default class DeviceList {
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||
if (deviceData === null) {
|
||||
console.log("Migrating e2e device data...");
|
||||
logger.log("Migrating e2e device data...");
|
||||
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
|
||||
this._deviceTrackingStatus = (
|
||||
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
|
||||
@@ -128,6 +132,16 @@ export default class DeviceList {
|
||||
deviceData.trackingStatus : {};
|
||||
this._syncToken = deviceData ? deviceData.syncToken : null;
|
||||
}
|
||||
this._userByIdentityKey = {};
|
||||
for (const user of Object.keys(this._devices)) {
|
||||
const userDevices = this._devices[user];
|
||||
for (const device of Object.keys(userDevices)) {
|
||||
const idKey = userDevices[device].keys['curve25519:'+device];
|
||||
if (idKey !== undefined) {
|
||||
this._userByIdentityKey[idKey] = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -145,6 +159,12 @@ export default class DeviceList {
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._saveTimer !== null) {
|
||||
clearTimeout(this._saveTimer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the device tracking state to storage, if any changes are
|
||||
* pending other than updating the sync token
|
||||
@@ -190,7 +210,7 @@ export default class DeviceList {
|
||||
const resolveSavePromise = this._resolveSavePromise;
|
||||
this._savePromiseTime = targetTime;
|
||||
this._saveTimer = setTimeout(() => {
|
||||
console.log('Saving device tracking data at token ' + this._syncToken);
|
||||
logger.log('Saving device tracking data at token ' + this._syncToken);
|
||||
// null out savePromise now (after the delay but before the write),
|
||||
// otherwise we could return the existing promise when the save has
|
||||
// actually already happened. Likewise for the dirty flag.
|
||||
@@ -258,7 +278,7 @@ export default class DeviceList {
|
||||
if (this._keyDownloadsInProgressByUser[u]) {
|
||||
// already a key download in progress/queued for this user; its results
|
||||
// will be good enough for us.
|
||||
console.log(
|
||||
logger.log(
|
||||
`downloadKeys: already have a download in progress for ` +
|
||||
`${u}: awaiting its result`,
|
||||
);
|
||||
@@ -269,13 +289,13 @@ export default class DeviceList {
|
||||
});
|
||||
|
||||
if (usersToDownload.length != 0) {
|
||||
console.log("downloadKeys: downloading for", usersToDownload);
|
||||
logger.log("downloadKeys: downloading for", usersToDownload);
|
||||
const downloadPromise = this._doKeyDownload(usersToDownload);
|
||||
promises.push(downloadPromise);
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
console.log("downloadKeys: already have all necessary keys");
|
||||
logger.log("downloadKeys: already have all necessary keys");
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
@@ -357,13 +377,17 @@ export default class DeviceList {
|
||||
/**
|
||||
* Find a device by curve25519 identity key
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} senderKey curve25519 key to match
|
||||
*
|
||||
* @return {module:crypto/deviceinfo?}
|
||||
*/
|
||||
getDeviceByIdentityKey(userId, algorithm, senderKey) {
|
||||
getDeviceByIdentityKey(algorithm, senderKey) {
|
||||
const userId = this._userByIdentityKey[senderKey];
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
algorithm !== olmlib.OLM_ALGORITHM &&
|
||||
algorithm !== olmlib.MEGOLM_ALGORITHM
|
||||
@@ -408,7 +432,23 @@ export default class DeviceList {
|
||||
* @param {Object} devs New device info for user
|
||||
*/
|
||||
storeDevicesForUser(u, devs) {
|
||||
// remove previous devices from _userByIdentityKey
|
||||
if (this._devices[u] !== undefined) {
|
||||
for (const [deviceId, dev] of Object.entries(this._devices[u])) {
|
||||
const identityKey = dev.keys['curve25519:'+deviceId];
|
||||
|
||||
delete this._userByIdentityKey[identityKey];
|
||||
}
|
||||
}
|
||||
|
||||
this._devices[u] = devs;
|
||||
|
||||
// add new ones
|
||||
for (const [deviceId, dev] of Object.entries(devs)) {
|
||||
const identityKey = dev.keys['curve25519:'+deviceId];
|
||||
|
||||
this._userByIdentityKey[identityKey] = u;
|
||||
}
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
@@ -433,7 +473,7 @@ export default class DeviceList {
|
||||
throw new Error('userId must be a string; was '+userId);
|
||||
}
|
||||
if (!this._deviceTrackingStatus[userId]) {
|
||||
console.log('Now tracking device list for ' + userId);
|
||||
logger.log('Now tracking device list for ' + userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
@@ -452,7 +492,7 @@ export default class DeviceList {
|
||||
*/
|
||||
stopTrackingDeviceList(userId) {
|
||||
if (this._deviceTrackingStatus[userId]) {
|
||||
console.log('No longer tracking device list for ' + userId);
|
||||
logger.log('No longer tracking device list for ' + userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
|
||||
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
@@ -487,7 +527,7 @@ export default class DeviceList {
|
||||
*/
|
||||
invalidateUserDeviceList(userId) {
|
||||
if (this._deviceTrackingStatus[userId]) {
|
||||
console.log("Marking device list outdated for", userId);
|
||||
logger.log("Marking device list outdated for", userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
@@ -525,7 +565,23 @@ export default class DeviceList {
|
||||
* @param {Object} devices deviceId->{object} the new devices
|
||||
*/
|
||||
_setRawStoredDevicesForUser(userId, devices) {
|
||||
// remove old devices from _userByIdentityKey
|
||||
if (this._devices[userId] !== undefined) {
|
||||
for (const [deviceId, dev] of Object.entries(this._devices[userId])) {
|
||||
const identityKey = dev.keys['curve25519:'+deviceId];
|
||||
|
||||
delete this._userByIdentityKey[identityKey];
|
||||
}
|
||||
}
|
||||
|
||||
this._devices[userId] = devices;
|
||||
|
||||
// add new devices into _userByIdentityKey
|
||||
for (const [deviceId, dev] of Object.entries(devices)) {
|
||||
const identityKey = dev.keys['curve25519:'+deviceId];
|
||||
|
||||
this._userByIdentityKey[identityKey] = userId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -550,7 +606,7 @@ export default class DeviceList {
|
||||
).then(() => {
|
||||
finished(true);
|
||||
}, (e) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
'Error downloading keys for ' + users + ":", e,
|
||||
);
|
||||
finished(false);
|
||||
@@ -573,7 +629,7 @@ export default class DeviceList {
|
||||
// since we started this request. If that happens, we should
|
||||
// ignore the completion of the first one.
|
||||
if (this._keyDownloadsInProgressByUser[u] !== prom) {
|
||||
console.log('Another update in the queue for', u,
|
||||
logger.log('Another update in the queue for', u,
|
||||
'- not marking up-to-date');
|
||||
return;
|
||||
}
|
||||
@@ -584,7 +640,7 @@ export default class DeviceList {
|
||||
// we didn't get any new invalidations since this download started:
|
||||
// this user's device list is now up to date.
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
|
||||
console.log("Device list for", u, "now up to date");
|
||||
logger.log("Device list for", u, "now up to date");
|
||||
} else {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
@@ -659,7 +715,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
if (this._downloadInProgress) {
|
||||
// just queue up these users
|
||||
console.log('Queued key download for', users);
|
||||
logger.log('Queued key download for', users);
|
||||
return this._queuedQueryDeferred.promise;
|
||||
}
|
||||
|
||||
@@ -679,7 +735,7 @@ class DeviceListUpdateSerialiser {
|
||||
const deferred = this._queuedQueryDeferred;
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
console.log('Starting key download for', downloadUsers);
|
||||
logger.log('Starting key download for', downloadUsers);
|
||||
this._downloadInProgress = true;
|
||||
|
||||
const opts = {};
|
||||
@@ -706,7 +762,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
return prom;
|
||||
}).done(() => {
|
||||
console.log('Completed key download for ' + downloadUsers);
|
||||
logger.log('Completed key download for ' + downloadUsers);
|
||||
|
||||
this._downloadInProgress = false;
|
||||
deferred.resolve();
|
||||
@@ -716,7 +772,7 @@ class DeviceListUpdateSerialiser {
|
||||
this._doQueuedQueries();
|
||||
}
|
||||
}, (e) => {
|
||||
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
||||
logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
||||
this._downloadInProgress = false;
|
||||
deferred.reject(e);
|
||||
});
|
||||
@@ -725,7 +781,7 @@ class DeviceListUpdateSerialiser {
|
||||
}
|
||||
|
||||
async _processQueryResponseForUser(userId, response) {
|
||||
console.log('got keys for ' + userId + ':', response);
|
||||
logger.log('got keys for ' + userId + ':', response);
|
||||
|
||||
// map from deviceid -> deviceinfo for this user
|
||||
const userStore = {};
|
||||
@@ -763,7 +819,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
}
|
||||
|
||||
if (!(deviceId in userResult)) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
logger.log("Device " + userId + ":" + deviceId +
|
||||
" has been removed");
|
||||
delete userStore[deviceId];
|
||||
updated = true;
|
||||
@@ -780,12 +836,12 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
// check that the user_id and device_id in the response object are
|
||||
// correct
|
||||
if (deviceResult.user_id !== userId) {
|
||||
console.warn("Mismatched user_id " + deviceResult.user_id +
|
||||
logger.warn("Mismatched user_id " + deviceResult.user_id +
|
||||
" in keys from " + userId + ":" + deviceId);
|
||||
continue;
|
||||
}
|
||||
if (deviceResult.device_id !== deviceId) {
|
||||
console.warn("Mismatched device_id " + deviceResult.device_id +
|
||||
logger.warn("Mismatched device_id " + deviceResult.device_id +
|
||||
" in keys from " + userId + ":" + deviceId);
|
||||
continue;
|
||||
}
|
||||
@@ -815,7 +871,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
const signKeyId = "ed25519:" + deviceId;
|
||||
const signKey = deviceResult.keys[signKeyId];
|
||||
if (!signKey) {
|
||||
console.warn("Device " + userId + ":" + deviceId +
|
||||
logger.warn("Device " + userId + ":" + deviceId +
|
||||
" has no ed25519 key");
|
||||
return false;
|
||||
}
|
||||
@@ -825,7 +881,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
try {
|
||||
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||
} catch (e) {
|
||||
console.warn("Unable to verify signature on device " +
|
||||
logger.warn("Unable to verify signature on device " +
|
||||
userId + ":" + deviceId + ":" + e);
|
||||
return false;
|
||||
}
|
||||
@@ -842,7 +898,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
// best off sticking with the original keys.
|
||||
//
|
||||
// Should we warn the user about it somehow?
|
||||
console.warn("Ed25519 key for device " + userId + ":" +
|
||||
logger.warn("Ed25519 key for device " + userId + ":" +
|
||||
deviceId + " has changed");
|
||||
return false;
|
||||
}
|
||||
|
||||
+99
-61
@@ -15,19 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../logger';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
/**
|
||||
* olm.js wrapper
|
||||
*
|
||||
* @module crypto/OlmDevice
|
||||
*/
|
||||
const Olm = global.Olm;
|
||||
if (!Olm) {
|
||||
throw new Error("global.Olm is not defined");
|
||||
}
|
||||
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
|
||||
@@ -127,7 +117,7 @@ OlmDevice.prototype.init = async function() {
|
||||
await this._migrateFromSessionStore();
|
||||
|
||||
let e2eKeys;
|
||||
const account = new Olm.Account();
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
await _initialiseAccount(
|
||||
this._sessionStore, this._cryptoStore, this._pickleKey, account,
|
||||
@@ -161,7 +151,7 @@ async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account)
|
||||
* @return {array} The version of Olm.
|
||||
*/
|
||||
OlmDevice.getOlmVersion = function() {
|
||||
return Olm.get_library_version();
|
||||
return global.Olm.get_library_version();
|
||||
};
|
||||
|
||||
OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
@@ -173,7 +163,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
// Migrate from sessionStore
|
||||
pickledAccount = this._sessionStore.getEndToEndAccount();
|
||||
if (pickledAccount !== null) {
|
||||
console.log("Migrating account from session store");
|
||||
logger.log("Migrating account from session store");
|
||||
this._cryptoStore.storeAccount(txn, pickledAccount);
|
||||
}
|
||||
}
|
||||
@@ -195,7 +185,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
// has run against the same localstorage and created some spurious sessions.
|
||||
this._cryptoStore.countEndToEndSessions(txn, (count) => {
|
||||
if (count) {
|
||||
console.log("Crypto store already has sessions: not migrating");
|
||||
logger.log("Crypto store already has sessions: not migrating");
|
||||
return;
|
||||
}
|
||||
let numSessions = 0;
|
||||
@@ -207,7 +197,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
logger.log(
|
||||
"Migrating " + numSessions + " sessions from session store",
|
||||
);
|
||||
});
|
||||
@@ -236,14 +226,14 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
), txn,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Failed to migrate session " + s.senderKey + "/" +
|
||||
s.sessionId + ": " + e.stack || e,
|
||||
);
|
||||
}
|
||||
++numIbSessions;
|
||||
}
|
||||
console.log(
|
||||
logger.log(
|
||||
"Migrated " + numIbSessions +
|
||||
" inbound group sessions from session store",
|
||||
);
|
||||
@@ -268,7 +258,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
*/
|
||||
OlmDevice.prototype._getAccount = function(txn, func) {
|
||||
this._cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
const account = new Olm.Account();
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
account.unpickle(this._pickleKey, pickledAccount);
|
||||
func(account);
|
||||
@@ -305,8 +295,8 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
|
||||
*/
|
||||
OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
|
||||
this._cryptoStore.getEndToEndSession(
|
||||
deviceKey, sessionId, txn, (pickledSession) => {
|
||||
this._unpickleSession(pickledSession, func);
|
||||
deviceKey, sessionId, txn, (sessionInfo) => {
|
||||
this._unpickleSession(sessionInfo, func);
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -316,15 +306,17 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
|
||||
* function with it. The session object is destroyed once the function
|
||||
* returns.
|
||||
*
|
||||
* @param {string} pickledSession
|
||||
* @param {object} sessionInfo
|
||||
* @param {function} func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
|
||||
const session = new Olm.Session();
|
||||
OlmDevice.prototype._unpickleSession = function(sessionInfo, func) {
|
||||
const session = new global.Olm.Session();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, pickledSession);
|
||||
func(session);
|
||||
session.unpickle(this._pickleKey, sessionInfo.session);
|
||||
const unpickledSessInfo = Object.assign({}, sessionInfo, {session});
|
||||
|
||||
func(unpickledSessInfo);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
@@ -334,14 +326,17 @@ OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
|
||||
* store our OlmSession in the session store
|
||||
*
|
||||
* @param {string} deviceKey
|
||||
* @param {OlmSession} session
|
||||
* @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int}
|
||||
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
|
||||
const pickledSession = session.pickle(this._pickleKey);
|
||||
OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) {
|
||||
const sessionId = sessionInfo.session.session_id();
|
||||
const pickledSessionInfo = Object.assign(sessionInfo, {
|
||||
session: sessionInfo.session.pickle(this._pickleKey),
|
||||
});
|
||||
this._cryptoStore.storeEndToEndSession(
|
||||
deviceKey, session.session_id(), pickledSession, txn,
|
||||
deviceKey, sessionId, pickledSessionInfo, txn,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -354,7 +349,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getUtility = function(func) {
|
||||
const utility = new Olm.Utility();
|
||||
const utility = new global.Olm.Utility();
|
||||
try {
|
||||
return func(utility);
|
||||
} finally {
|
||||
@@ -466,12 +461,19 @@ OlmDevice.prototype.createOutboundSession = async function(
|
||||
],
|
||||
(txn) => {
|
||||
this._getAccount(txn, (account) => {
|
||||
const session = new Olm.Session();
|
||||
const session = new global.Olm.Session();
|
||||
try {
|
||||
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
|
||||
newSessionId = session.session_id();
|
||||
this._storeAccount(txn, account);
|
||||
this._saveSession(theirIdentityKey, session, txn);
|
||||
const sessionInfo = {
|
||||
session,
|
||||
// Pretend we've received a message at this point, otherwise
|
||||
// if we try to send a message to the device, it won't use
|
||||
// this session
|
||||
lastReceivedMessageTs: Date.now(),
|
||||
};
|
||||
this._saveSession(theirIdentityKey, sessionInfo, txn);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
@@ -510,7 +512,7 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
],
|
||||
(txn) => {
|
||||
this._getAccount(txn, (account) => {
|
||||
const session = new Olm.Session();
|
||||
const session = new global.Olm.Session();
|
||||
try {
|
||||
session.create_inbound_from(
|
||||
account, theirDeviceIdentityKey, ciphertext,
|
||||
@@ -520,7 +522,13 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
|
||||
const payloadString = session.decrypt(messageType, ciphertext);
|
||||
|
||||
this._saveSession(theirDeviceIdentityKey, session, txn);
|
||||
const sessionInfo = {
|
||||
session,
|
||||
// this counts as a received message: set last received message time
|
||||
// to now
|
||||
lastReceivedMessageTs: Date.now(),
|
||||
};
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
|
||||
result = {
|
||||
payload: payloadString,
|
||||
@@ -568,13 +576,30 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
* @return {Promise<?string>} session id, or null if no established session
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
|
||||
const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
if (sessionIds.length === 0) {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey);
|
||||
if (sessionInfos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// Use the session with the lowest ID.
|
||||
sessionIds.sort();
|
||||
return sessionIds[0];
|
||||
// Use the session that has most recently received a message
|
||||
let idxOfBest = 0;
|
||||
for (let i = 1; i < sessionInfos.length; i++) {
|
||||
const thisSessInfo = sessionInfos[i];
|
||||
const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ?
|
||||
0 : thisSessInfo.lastReceivedMessageTs;
|
||||
|
||||
const bestSessInfo = sessionInfos[idxOfBest];
|
||||
const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ?
|
||||
0 : bestSessInfo.lastReceivedMessageTs;
|
||||
if (
|
||||
thisLastReceived > bestLastReceived || (
|
||||
thisLastReceived === bestLastReceived &&
|
||||
thisSessInfo.sessionId < bestSessInfo.sessionId
|
||||
)
|
||||
) {
|
||||
idxOfBest = i;
|
||||
}
|
||||
}
|
||||
return sessionInfos[idxOfBest].sessionId;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -597,9 +622,10 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey)
|
||||
this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => {
|
||||
const sessionIds = Object.keys(sessions).sort();
|
||||
for (const sessionId of sessionIds) {
|
||||
this._unpickleSession(sessions[sessionId], (session) => {
|
||||
this._unpickleSession(sessions[sessionId], (sessInfo) => {
|
||||
info.push({
|
||||
hasReceivedMessage: session.has_received_message(),
|
||||
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs,
|
||||
hasReceivedMessage: sessInfo.session.has_received_message(),
|
||||
sessionId: sessionId,
|
||||
});
|
||||
});
|
||||
@@ -630,9 +656,9 @@ OlmDevice.prototype.encryptMessage = async function(
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
|
||||
res = session.encrypt(payloadString);
|
||||
this._saveSession(theirDeviceIdentityKey, session, txn);
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
|
||||
res = sessionInfo.session.encrypt(payloadString);
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -657,9 +683,10 @@ OlmDevice.prototype.decryptMessage = async function(
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
|
||||
payloadString = session.decrypt(messageType, ciphertext);
|
||||
this._saveSession(theirDeviceIdentityKey, session, txn);
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
|
||||
payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
|
||||
sessionInfo.lastReceivedMessageTs = Date.now();
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -689,8 +716,8 @@ OlmDevice.prototype.matchesSession = async function(
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
|
||||
matches = session.matches_inbound(ciphertext);
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
|
||||
matches = sessionInfo.session.matches_inbound(ciphertext);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -724,11 +751,11 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
|
||||
*/
|
||||
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
||||
const pickled = this._outboundGroupSessionStore[sessionId];
|
||||
if (pickled === null) {
|
||||
if (pickled === undefined) {
|
||||
throw new Error("Unknown outbound group session " + sessionId);
|
||||
}
|
||||
|
||||
const session = new Olm.OutboundGroupSession();
|
||||
const session = new global.Olm.OutboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, pickled);
|
||||
return func(session);
|
||||
@@ -744,7 +771,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
||||
* @return {string} sessionId for the outbound session.
|
||||
*/
|
||||
OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
const session = new Olm.OutboundGroupSession();
|
||||
const session = new global.Olm.OutboundGroupSession();
|
||||
try {
|
||||
session.create();
|
||||
this._saveOutboundGroupSession(session);
|
||||
@@ -816,7 +843,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
|
||||
* @return {*} result of func
|
||||
*/
|
||||
OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) {
|
||||
const session = new Olm.InboundGroupSession();
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, sessionData.session);
|
||||
return func(session);
|
||||
@@ -889,7 +916,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
(existingSession, existingSessionData) => {
|
||||
if (existingSession) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Update for megolm session " + senderKey + "/" + sessionId,
|
||||
);
|
||||
// for now we just ignore updates. TODO: implement something here
|
||||
@@ -897,7 +924,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
}
|
||||
|
||||
// new session.
|
||||
const session = new Olm.InboundGroupSession();
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
try {
|
||||
if (exportFormat) {
|
||||
session.import_session(sessionKey);
|
||||
@@ -1034,7 +1061,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
}
|
||||
|
||||
if (roomId !== sessionData.room_id) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`requested keys for inbound group session ${senderKey}|` +
|
||||
`${sessionId}, with incorrect room_id ` +
|
||||
`(expected ${sessionData.room_id}, ` +
|
||||
@@ -1058,6 +1085,8 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
* @param {string} roomId room in which the message was received
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {integer} chainIndex The chain index at which to export the session.
|
||||
* If omitted, export at the first index we know about.
|
||||
*
|
||||
* @returns {Promise<{chain_index: number, key: string,
|
||||
* forwarding_curve25519_key_chain: Array<string>,
|
||||
@@ -1065,9 +1094,12 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
* }>}
|
||||
* details of the session key. The key is a base64-encoded megolm key in
|
||||
* export format.
|
||||
*
|
||||
* @throws Error If the given chain index could not be obtained from the known
|
||||
* index (ie. the given chain index is before the first we have).
|
||||
*/
|
||||
OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
roomId, senderKey, sessionId,
|
||||
roomId, senderKey, sessionId, chainIndex,
|
||||
) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
@@ -1078,14 +1110,19 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
const messageIndex = session.first_known_index();
|
||||
|
||||
if (chainIndex === undefined) {
|
||||
chainIndex = session.first_known_index();
|
||||
}
|
||||
|
||||
const exportedSession = session.export_session(chainIndex);
|
||||
|
||||
const claimedKeys = sessionData.keysClaimed || {};
|
||||
const senderEd25519Key = claimedKeys.ed25519 || null;
|
||||
|
||||
result = {
|
||||
"chain_index": messageIndex,
|
||||
"key": session.export_session(messageIndex),
|
||||
"chain_index": chainIndex,
|
||||
"key": exportedSession,
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||
@@ -1119,6 +1156,7 @@ OlmDevice.prototype.exportInboundGroupSession = function(
|
||||
"session_id": sessionId,
|
||||
"session_key": session.export_session(messageIndex),
|
||||
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
||||
"first_known_index": session.first_known_index(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../logger';
|
||||
import utils from '../utils';
|
||||
|
||||
/**
|
||||
@@ -108,7 +109,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
stop() {
|
||||
console.log('stopping OutgoingRoomKeyRequestManager');
|
||||
logger.log('stopping OutgoingRoomKeyRequestManager');
|
||||
// stop the timer on the next run
|
||||
this._clientRunning = false;
|
||||
}
|
||||
@@ -173,7 +174,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
// may have seen it, so we still need to send a cancellation
|
||||
// in that case :/
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
'deleting unnecessary room key request for ' +
|
||||
stringifyRequestBody(requestBody),
|
||||
);
|
||||
@@ -201,7 +202,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
// the request cancelled. There is no point in
|
||||
// sending another cancellation since the other tab
|
||||
// will do it.
|
||||
console.log(
|
||||
logger.log(
|
||||
'Tried to cancel room key request for ' +
|
||||
stringifyRequestBody(requestBody) +
|
||||
' but it was already cancelled in another tab',
|
||||
@@ -222,7 +223,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
updatedReq,
|
||||
andResend,
|
||||
).catch((e) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Error sending room key request cancellation;"
|
||||
+ " will retry later.", e,
|
||||
);
|
||||
@@ -243,6 +244,21 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by target device and state
|
||||
*
|
||||
* @param {string} userId Target user ID
|
||||
* @param {string} deviceId Target device ID
|
||||
*
|
||||
* @return {Promise} resolves to a list of all the
|
||||
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||
*/
|
||||
getOutgoingSentRoomKeyRequest(userId, deviceId) {
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
|
||||
);
|
||||
}
|
||||
|
||||
// start the background timer to send queued requests, if the timer isn't
|
||||
// already running
|
||||
_startTimer() {
|
||||
@@ -261,7 +277,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
}).catch((e) => {
|
||||
// this should only happen if there is an indexeddb error,
|
||||
// in which case we're a bit stuffed anyway.
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`error in OutgoingRoomKeyRequestManager: ${e}`,
|
||||
);
|
||||
}).done();
|
||||
@@ -282,7 +298,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
console.log("Looking for queued outgoing room key requests");
|
||||
logger.log("Looking for queued outgoing room key requests");
|
||||
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||
@@ -290,7 +306,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
]).then((req) => {
|
||||
if (!req) {
|
||||
console.log("No more outgoing room key requests");
|
||||
logger.log("No more outgoing room key requests");
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
return;
|
||||
}
|
||||
@@ -312,7 +328,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
// go around the loop again
|
||||
return this._sendOutgoingRoomKeyRequests();
|
||||
}).catch((e) => {
|
||||
console.error("Error sending room key request; will retry later.", e);
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
this._startTimer();
|
||||
}).done();
|
||||
@@ -321,7 +337,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
|
||||
// given a RoomKeyRequest, send it and update the request record
|
||||
_sendOutgoingRoomKeyRequest(req) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
|
||||
` from ${stringifyRecipientList(req.recipients)}` +
|
||||
`(id ${req.requestId})`,
|
||||
@@ -347,7 +363,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
// Given a RoomKeyRequest, cancel it and delete the request record unless
|
||||
// andResend is set, in which case transition to UNSENT.
|
||||
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Sending cancellation for key request for ` +
|
||||
`${stringifyRequestBody(req.requestBody)} to ` +
|
||||
`${stringifyRecipientList(req.recipients)} ` +
|
||||
|
||||
+156
-18
@@ -24,6 +24,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const logger = require("../../logger");
|
||||
const utils = require("../../utils");
|
||||
const olmlib = require("../olmlib");
|
||||
const base = require("./base");
|
||||
@@ -65,7 +66,7 @@ OutboundSessionInfo.prototype.needsRotation = function(
|
||||
if (this.useCount >= rotationPeriodMsgs ||
|
||||
sessionLifetime >= rotationPeriodMs
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Rotating megolm session after " + this.useCount +
|
||||
" messages, " + sessionLifetime + "ms",
|
||||
);
|
||||
@@ -103,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
}
|
||||
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
console.log("Starting new session because we shared with " + userId);
|
||||
logger.log("Starting new session because we shared with " + userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
}
|
||||
|
||||
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Starting new session because we shared with " +
|
||||
userId + ":" + deviceId,
|
||||
);
|
||||
@@ -143,6 +144,11 @@ function MegolmEncryption(params) {
|
||||
// room).
|
||||
this._setupPromise = Promise.resolve();
|
||||
|
||||
// Map of outbound sessions by sessions ID. Used if we need a particular
|
||||
// session (the session we're currently using to send is always obtained
|
||||
// using _setupPromise).
|
||||
this._outboundSessions = {};
|
||||
|
||||
// default rotation periods
|
||||
this._sessionRotationPeriodMsgs = 100;
|
||||
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||
@@ -182,7 +188,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
console.log("Starting new megolm session because we need to rotate.");
|
||||
logger.log("Starting new megolm session because we need to rotate.");
|
||||
session = null;
|
||||
}
|
||||
|
||||
@@ -192,8 +198,9 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
console.log(`Starting new megolm session for room ${self._roomId}`);
|
||||
logger.log(`Starting new megolm session for room ${self._roomId}`);
|
||||
session = await self._prepareNewSession();
|
||||
self._outboundSessions[session.sessionId] = session;
|
||||
}
|
||||
|
||||
// now check if we need to share with any devices
|
||||
@@ -263,6 +270,18 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
|
||||
);
|
||||
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
||||
sessionId, key.key,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
console.log("Failed to back up group session", e);
|
||||
});
|
||||
}
|
||||
|
||||
return new OutboundSessionInfo(sessionId);
|
||||
};
|
||||
|
||||
@@ -319,7 +338,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
"share keys with device " + userId + ":" + deviceId,
|
||||
);
|
||||
|
||||
@@ -408,8 +427,98 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Re-shares a megolm session key with devices if the key has already been
|
||||
* sent to them.
|
||||
*
|
||||
* @param {string} senderKey The key of the originating device for the session
|
||||
* @param {string} sessionId ID of the outbound session to share
|
||||
* @param {string} userId ID of the user who owns the target device
|
||||
* @param {module:crypto/deviceinfo} device The target device
|
||||
*/
|
||||
MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
senderKey, sessionId, userId, device,
|
||||
) {
|
||||
const obSessionInfo = this._outboundSessions[sessionId];
|
||||
if (!obSessionInfo) {
|
||||
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
|
||||
return;
|
||||
}
|
||||
|
||||
// The chain index of the key we previously sent this device
|
||||
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
|
||||
logger.debug("Session ID " + sessionId + " never shared with user " + userId);
|
||||
return;
|
||||
}
|
||||
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sentChainIndex === undefined) {
|
||||
logger.debug(
|
||||
"Session ID " + sessionId + " never shared with device " +
|
||||
userId + ":" + device.deviceId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the key from the inbound session: the outbound one will already
|
||||
// have been ratcheted to the next chain index.
|
||||
const key = await this._olmDevice.getInboundGroupSessionKey(
|
||||
this._roomId, senderKey, sessionId, sentChainIndex,
|
||||
);
|
||||
|
||||
if (!key) {
|
||||
logger.warn(
|
||||
"No outbound session key found for " + sessionId + ": not re-sharing keys",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, {
|
||||
[userId]: {
|
||||
[device.deviceId]: device,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const payload = {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
},
|
||||
};
|
||||
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._userId,
|
||||
this._deviceId,
|
||||
this._olmDevice,
|
||||
userId,
|
||||
device,
|
||||
payload,
|
||||
),
|
||||
|
||||
await this._baseApis.sendToDevice("m.room.encrypted", {
|
||||
[userId]: {
|
||||
[device.deviceId]: encryptedContent,
|
||||
},
|
||||
});
|
||||
logger.debug(
|
||||
`Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
@@ -441,10 +550,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
|
||||
await this._encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
console.log(`Completed megolm keyshare in ${this._roomId} `
|
||||
logger.log(`Completed megolm keyshare in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
} catch (e) {
|
||||
console.log(`megolm keyshare in ${this._roomId} `
|
||||
logger.log(`megolm keyshare in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
|
||||
|
||||
throw e;
|
||||
@@ -463,7 +572,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
const self = this;
|
||||
console.log(`Starting to encrypt event for ${this._roomId}`);
|
||||
logger.log(`Starting to encrypt event for ${this._roomId}`);
|
||||
|
||||
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
// check if any of these devices are not yet known to the user.
|
||||
@@ -782,12 +891,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
!sessionId ||
|
||||
!content.session_key
|
||||
) {
|
||||
console.error("key event is missing fields");
|
||||
logger.error("key event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!senderKey) {
|
||||
console.error("key event has no sender key (not encrypted?)");
|
||||
logger.error("key event has no sender key (not encrypted?)");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -804,13 +913,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
|
||||
senderKey = content.sender_key;
|
||||
if (!senderKey) {
|
||||
console.error("forwarded_room_key event is missing sender_key field");
|
||||
logger.error("forwarded_room_key event is missing sender_key field");
|
||||
return;
|
||||
}
|
||||
|
||||
const ed25519Key = content.sender_claimed_ed25519_key;
|
||||
if (!ed25519Key) {
|
||||
console.error(
|
||||
logger.error(
|
||||
`forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
|
||||
);
|
||||
return;
|
||||
@@ -823,8 +932,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||
content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
@@ -839,8 +948,21 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId);
|
||||
}).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain,
|
||||
content.session_id, content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
console.log("Failed to back up group session", e);
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.error(`Error handling m.room_key_event: ${e}`);
|
||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -882,7 +1004,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
"sharing keys for session " + body.sender_key + "|"
|
||||
+ body.session_id + " with device "
|
||||
+ userId + ":" + deviceId,
|
||||
@@ -956,6 +1078,22 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
session.forwarding_curve25519_key_chain,
|
||||
session.session_id,
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
console.log("Failed to back up group session", e);
|
||||
});
|
||||
}
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(session.sender_key, session.session_id);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ limitations under the License.
|
||||
*/
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const logger = require("../../logger");
|
||||
const utils = require("../../utils");
|
||||
const olmlib = require("../olmlib");
|
||||
const DeviceInfo = require("../deviceinfo");
|
||||
@@ -273,7 +274,7 @@ OlmDecryption.prototype._decryptMessage = async function(
|
||||
const payload = await this._olmDevice.decryptMessage(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
console.log(
|
||||
logger.log(
|
||||
"Decrypted Olm message from " + theirDeviceIdentityKey +
|
||||
" with session " + sessionId,
|
||||
);
|
||||
@@ -328,7 +329,7 @@ OlmDecryption.prototype._decryptMessage = async function(
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
"created new inbound Olm session ID " +
|
||||
res.session_id + " with " + theirDeviceIdentityKey,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import { randomString } from '../randomstring';
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
export async function keyForExistingBackup(backupData, password) {
|
||||
if (!global.Olm) {
|
||||
throw new Error("Olm is not available");
|
||||
}
|
||||
|
||||
const authData = backupData.auth_data;
|
||||
|
||||
if (!authData.private_key_salt || !authData.private_key_iterations) {
|
||||
throw new Error(
|
||||
"Salt and/or iterations not found: " +
|
||||
"this backup cannot be restored with a passphrase",
|
||||
);
|
||||
}
|
||||
|
||||
return await deriveKey(
|
||||
password, backupData.auth_data.private_key_salt,
|
||||
backupData.auth_data.private_key_iterations,
|
||||
);
|
||||
}
|
||||
|
||||
export async function keyForNewBackup(password) {
|
||||
if (!global.Olm) {
|
||||
throw new Error("Olm is not available");
|
||||
}
|
||||
|
||||
const salt = randomString(32);
|
||||
|
||||
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
|
||||
|
||||
return { key, salt, iterations: DEFAULT_ITERATIONS };
|
||||
}
|
||||
|
||||
async function deriveKey(password, salt, iterations) {
|
||||
const subtleCrypto = global.crypto.subtle;
|
||||
const TextEncoder = global.TextEncoder;
|
||||
if (!subtleCrypto || !TextEncoder) {
|
||||
// TODO: Implement this for node
|
||||
throw new Error("Password-based backup is not avaiable on this platform");
|
||||
}
|
||||
|
||||
const key = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
{name: 'PBKDF2'},
|
||||
false,
|
||||
['deriveBits'],
|
||||
);
|
||||
|
||||
const keybits = await subtleCrypto.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: new TextEncoder().encode(salt),
|
||||
iterations: iterations,
|
||||
hash: 'SHA-512',
|
||||
},
|
||||
key,
|
||||
global.Olm.PRIVATE_KEY_LENGTH * 8,
|
||||
);
|
||||
|
||||
return new Uint8Array(keybits);
|
||||
}
|
||||
+434
-32
@@ -25,6 +25,7 @@ const anotherjson = require('another-json');
|
||||
import Promise from 'bluebird';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
const logger = require("../logger");
|
||||
const utils = require("../utils");
|
||||
const OlmDevice = require("./OlmDevice");
|
||||
const olmlib = require("./olmlib");
|
||||
@@ -36,6 +37,13 @@ const DeviceList = require('./DeviceList').default;
|
||||
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
export function isCryptoAvailable() {
|
||||
return Boolean(global.Olm);
|
||||
}
|
||||
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
*
|
||||
@@ -62,7 +70,7 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
*
|
||||
* @param {RoomList} roomList An initialised RoomList object
|
||||
*/
|
||||
function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
clientStore, cryptoStore, roomList) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
@@ -72,6 +80,14 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._roomList = roomList;
|
||||
|
||||
// track whether this device's megolm keys are being backed up incrementally
|
||||
// to the server or not.
|
||||
// XXX: this should probably have a single source of truth from OlmAccount
|
||||
this.backupInfo = null; // The info dict from /room_keys/version
|
||||
this.backupKey = null; // The encryption key object
|
||||
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
|
||||
this._sendingBackups = false; // Are we currently sending backups?
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
this._deviceList = new DeviceList(
|
||||
baseApis, cryptoStore, sessionStore, this._olmDevice,
|
||||
@@ -115,6 +131,15 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
// has happened for a given room. This is delayed
|
||||
// to avoid loading room members as long as possible.
|
||||
this._roomDeviceTrackingState = {};
|
||||
|
||||
// The timestamp of the last time we forced establishment
|
||||
// of a new session for each device, in milliseconds.
|
||||
// {
|
||||
// userId: {
|
||||
// deviceId: 1234567890000,
|
||||
// },
|
||||
// }
|
||||
this._lastNewSessionForced = {};
|
||||
}
|
||||
utils.inherits(Crypto, EventEmitter);
|
||||
|
||||
@@ -124,6 +149,8 @@ utils.inherits(Crypto, EventEmitter);
|
||||
* Returns a promise which resolves once the crypto module is ready for use.
|
||||
*/
|
||||
Crypto.prototype.init = async function() {
|
||||
await global.Olm.init();
|
||||
|
||||
const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount());
|
||||
let cryptoStoreHasAccount;
|
||||
await this._cryptoStore.doTxn(
|
||||
@@ -174,6 +201,120 @@ Crypto.prototype.init = async function() {
|
||||
);
|
||||
this._deviceList.saveIfDirty();
|
||||
}
|
||||
|
||||
this._checkAndStartKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
* one of the user's verified devices, start backing up
|
||||
* to it.
|
||||
*/
|
||||
Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
console.log("Checking key backup status...");
|
||||
if (this._baseApis.isGuest()) {
|
||||
console.log("Skipping key backup check since user is guest");
|
||||
this._checkedForBackup = true;
|
||||
return;
|
||||
}
|
||||
let backupInfo;
|
||||
try {
|
||||
backupInfo = await this._baseApis.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
console.log("Error checking for active key backup", e);
|
||||
if (e.httpStatus / 100 === 4) {
|
||||
// well that's told us. we won't try again.
|
||||
this._checkedForBackup = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
console.log("Found usable key backup: enabling key backups");
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
} else if (!trustInfo.usable && this.backupInfo) {
|
||||
console.log("No usable key backup: disabling key backup");
|
||||
this._baseApis.disableKeyBackup();
|
||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
||||
console.log("No usable key backup: not enabling key backup");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate
|
||||
*
|
||||
* @param {object} backupInfo Backup info from /room_keys/version endpoint
|
||||
*/
|
||||
Crypto.prototype.checkKeyBackup = async function(backupInfo) {
|
||||
this._checkedForBackup = false;
|
||||
await this._checkAndStartKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool],
|
||||
* device: [DeviceInfo],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
const ret = {
|
||||
usable: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
if (
|
||||
!backupInfo ||
|
||||
!backupInfo.algorithm ||
|
||||
!backupInfo.auth_data ||
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
console.log("Key backup is absent or missing required data");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this._userId];
|
||||
if (!mySigs || mySigs.length === 0) {
|
||||
console.log("Ignoring key backup because it lacks any signatures from this user");
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const device = this._deviceList.getStoredDevice(
|
||||
this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID?
|
||||
);
|
||||
if (!device) {
|
||||
console.log("Ignoring signature from unknown key " + keyId);
|
||||
continue;
|
||||
}
|
||||
const sigInfo = { device };
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this._userId,
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
console.log("Bad signature from device " + device.deviceId, e);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified());
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -196,7 +337,7 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
|
||||
try {
|
||||
crypto._onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
console.error("Error handling membership change:", e);
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -214,6 +355,7 @@ Crypto.prototype.start = function() {
|
||||
/** Stop background processes related to crypto */
|
||||
Crypto.prototype.stop = function() {
|
||||
this._outgoingRoomKeyRequestManager.stop();
|
||||
this._deviceList.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -381,7 +523,7 @@ function _maybeUploadOneTimeKeys(crypto) {
|
||||
// create any more keys.
|
||||
return uploadLoop(keyCount);
|
||||
}).catch((e) => {
|
||||
console.error("Error uploading one-time keys", e.stack || e);
|
||||
logger.error("Error uploading one-time keys", e.stack || e);
|
||||
}).finally(() => {
|
||||
// reset _oneTimeKeyCount to prevent start uploading based on old data.
|
||||
// it will be set again on the next /sync-response
|
||||
@@ -588,7 +730,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
// identity key of the device which set up the Megolm session.
|
||||
|
||||
const device = this._deviceList.getDeviceByIdentityKey(
|
||||
event.getSender(), algorithm, senderKey,
|
||||
algorithm, senderKey,
|
||||
);
|
||||
|
||||
if (device === null) {
|
||||
@@ -606,13 +748,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
|
||||
const claimedKey = event.getClaimedEd25519Key();
|
||||
if (!claimedKey) {
|
||||
console.warn("Event " + event.getId() + " claims no ed25519 key: " +
|
||||
logger.warn("Event " + event.getId() + " claims no ed25519 key: " +
|
||||
"cannot verify sending device");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (claimedKey !== device.getFingerprint()) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
|
||||
"but sender device has key " + device.getFingerprint());
|
||||
return null;
|
||||
@@ -658,7 +800,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
|
||||
const existingConfig = this._roomList.getRoomEncryption(roomId);
|
||||
if (existingConfig) {
|
||||
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
|
||||
console.error("Ignoring m.room.encryption event which requests " +
|
||||
logger.error("Ignoring m.room.encryption event which requests " +
|
||||
"a change of config in " + roomId);
|
||||
return;
|
||||
}
|
||||
@@ -705,7 +847,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
|
||||
}
|
||||
|
||||
if (!this._lazyLoadMembers) {
|
||||
console.log("Enabling encryption in " + roomId + "; " +
|
||||
logger.log("Enabling encryption in " + roomId + "; " +
|
||||
"starting to track device lists for all users therein");
|
||||
|
||||
await this.trackRoomDevices(roomId);
|
||||
@@ -717,7 +859,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
}
|
||||
} else {
|
||||
console.log("Enabling encryption in " + roomId);
|
||||
logger.log("Enabling encryption in " + roomId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -738,7 +880,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) {
|
||||
if (!room) {
|
||||
throw new Error(`Unable to start tracking devices in unknown room ${roomId}`);
|
||||
}
|
||||
console.log(`Starting to track devices for room ${roomId} ...`);
|
||||
logger.log(`Starting to track devices for room ${roomId} ...`);
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
members.forEach((m) => {
|
||||
this._deviceList.startTrackingDeviceList(m.userId);
|
||||
@@ -815,6 +957,7 @@ Crypto.prototype.exportRoomKeys = async function() {
|
||||
const sess = this._olmDevice.exportInboundGroupSession(
|
||||
s.senderKey, s.sessionId, s.sessionData,
|
||||
);
|
||||
delete sess.first_known_index;
|
||||
sess.algorithm = olmlib.MEGOLM_ALGORITHM;
|
||||
exportedSessions.push(sess);
|
||||
});
|
||||
@@ -834,7 +977,7 @@ Crypto.prototype.importRoomKeys = function(keys) {
|
||||
return Promise.map(
|
||||
keys, (key) => {
|
||||
if (!key.room_id || !key.algorithm) {
|
||||
console.warn("ignoring room key entry with missing fields", key);
|
||||
logger.warn("ignoring room key entry with missing fields", key);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -843,6 +986,153 @@ Crypto.prototype.importRoomKeys = function(keys) {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*/
|
||||
Crypto.prototype._scheduleKeyBackupSend = async function() {
|
||||
if (this._sendingBackups) return;
|
||||
|
||||
try {
|
||||
// wait between 0 and 10 seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * 10000;
|
||||
await Promise.delay(delay);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
while (1) {
|
||||
if (!this.backupKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const numBackedUp =
|
||||
await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
return;
|
||||
}
|
||||
numFailures = 0;
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
console.log("Key backup request failed", err);
|
||||
if (err.data) {
|
||||
if (
|
||||
err.data.errcode == 'M_NOT_FOUND' ||
|
||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
||||
) {
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numFailures) {
|
||||
// exponential backoff if we have failures
|
||||
await Promise.delay(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._sendingBackups = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Take some e2e keys waiting to be backed up and send them
|
||||
* to the backup.
|
||||
*
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
Crypto.prototype._backupPendingKeys = async function(limit) {
|
||||
const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const data = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
if (data[roomId] === undefined) {
|
||||
data[roomId] = {sessions: {}};
|
||||
}
|
||||
|
||||
const sessionData = await this._olmDevice.exportInboundGroupSession(
|
||||
session.senderKey, session.sessionId, session.sessionData,
|
||||
);
|
||||
sessionData.algorithm = olmlib.MEGOLM_ALGORITHM;
|
||||
delete sessionData.session_id;
|
||||
delete sessionData.room_id;
|
||||
const firstKnownIndex = sessionData.first_known_index;
|
||||
delete sessionData.first_known_index;
|
||||
const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData));
|
||||
|
||||
const forwardedCount =
|
||||
(sessionData.forwarding_curve25519_key_chain || []).length;
|
||||
|
||||
const device = this._deviceList.getDeviceByIdentityKey(
|
||||
olmlib.MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
|
||||
data[roomId]['sessions'][session.sessionId] = {
|
||||
first_message_index: firstKnownIndex,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: !!(device && device.isVerified()),
|
||||
session_data: encrypted,
|
||||
};
|
||||
}
|
||||
|
||||
await this._baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{rooms: data},
|
||||
);
|
||||
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
|
||||
return sessions.length;
|
||||
};
|
||||
|
||||
Crypto.prototype.backupGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
) {
|
||||
if (!this.backupInfo) {
|
||||
throw new Error("Key backups are not enabled");
|
||||
}
|
||||
|
||||
await this._cryptoStore.markSessionsNeedingBackup([{
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this._scheduleKeyBackupSend();
|
||||
};
|
||||
|
||||
Crypto.prototype.backupAllGroupSessions = async function(version) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_BACKUP,
|
||||
],
|
||||
(txn) => {
|
||||
this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
|
||||
if (session !== null) {
|
||||
this._cryptoStore.markSessionsNeedingBackup([session], txn);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let numKeysBackedUp;
|
||||
do {
|
||||
numKeysBackedUp = await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
} while (numKeysBackedUp > 0);
|
||||
};
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
@@ -963,7 +1253,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
|
||||
requestBody, recipients,
|
||||
).catch((e) => {
|
||||
// this normally means we couldn't talk to the store
|
||||
console.error(
|
||||
logger.error(
|
||||
'Error requesting key for event', e,
|
||||
);
|
||||
}).done();
|
||||
@@ -980,7 +1270,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
|
||||
Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) {
|
||||
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend)
|
||||
.catch((e) => {
|
||||
console.warn("Error clearing pending room key requests", e);
|
||||
logger.warn("Error clearing pending room key requests", e);
|
||||
}).done();
|
||||
};
|
||||
|
||||
@@ -998,7 +1288,7 @@ Crypto.prototype.onCryptoEvent = async function(event) {
|
||||
// finished processing the sync, in onSyncCompleted.
|
||||
await this.setRoomEncryption(roomId, content, true);
|
||||
} catch (e) {
|
||||
console.error("Error configuring encryption in room " + roomId +
|
||||
logger.error("Error configuring encryption in room " + roomId +
|
||||
":", e);
|
||||
}
|
||||
};
|
||||
@@ -1014,7 +1304,7 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) {
|
||||
// scratch, so mark everything as untracked. onCryptoEvent will
|
||||
// be called for all e2e rooms during the processing of the sync,
|
||||
// at which point we'll start tracking all the users of that room.
|
||||
console.log("Initial sync performed - resetting device tracking state");
|
||||
logger.log("Initial sync performed - resetting device tracking state");
|
||||
this._deviceList.stopTrackingAllDeviceLists();
|
||||
this._roomDeviceTrackingState = {};
|
||||
}
|
||||
@@ -1125,6 +1415,8 @@ Crypto.prototype._onToDeviceEvent = function(event) {
|
||||
this._onRoomKeyEvent(event);
|
||||
} else if (event.getType() == "m.room_key_request") {
|
||||
this._onRoomKeyRequestEvent(event);
|
||||
} else if (event.getContent().msgtype === "m.bad.encrypted") {
|
||||
this._onToDeviceBadEncrypted(event);
|
||||
} else if (event.isBeingDecrypted()) {
|
||||
// once the event has been decrypted, try again
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
@@ -1132,7 +1424,7 @@ Crypto.prototype._onToDeviceEvent = function(event) {
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error handling toDeviceEvent:", e);
|
||||
logger.error("Error handling toDeviceEvent:", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1146,14 +1438,109 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
const content = event.getContent();
|
||||
|
||||
if (!content.room_id || !content.algorithm) {
|
||||
console.error("key event is missing fields");
|
||||
logger.error("key event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._checkedForBackup) {
|
||||
// don't bother awaiting on this - the important thing is that we retry if we
|
||||
// haven't managed to check before
|
||||
this._checkAndStartKeyBackup();
|
||||
}
|
||||
|
||||
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||
alg.onRoomKeyEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a toDevice event that couldn't be decrypted
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event undecryptable event
|
||||
*/
|
||||
Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
|
||||
const content = event.getWireContent();
|
||||
const sender = event.getSender();
|
||||
const algorithm = content.algorithm;
|
||||
const deviceKey = content.sender_key;
|
||||
|
||||
if (sender === undefined || deviceKey === undefined || deviceKey === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check when we last forced a new session with this device: if we've already done so
|
||||
// recently, don't do it again.
|
||||
this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {};
|
||||
const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0;
|
||||
if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
|
||||
logger.debug(
|
||||
"New session already forced with device " + sender + ":" + deviceKey +
|
||||
" at " + lastNewSessionForced + ": not forcing another",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// establish a new olm session with this device since we're failing to decrypt messages
|
||||
// on a current session.
|
||||
// Note that an undecryptable message from another device could easily be spoofed -
|
||||
// is there anything we can do to mitigate this?
|
||||
const device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey);
|
||||
if (!device) {
|
||||
logger.info(
|
||||
"Couldn't find device for identity key " + deviceKey +
|
||||
": not re-establishing session",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const devicesByUser = {};
|
||||
devicesByUser[sender] = [device];
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser, true,
|
||||
);
|
||||
|
||||
this._lastNewSessionForced[sender][deviceKey] = Date.now();
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
// We send this first such that, as long as the toDevice messages arrive in the
|
||||
// same order we sent them, the other end will get this first, set up the new session,
|
||||
// then get the keyshare request and send the key over this new session (because it
|
||||
// is the session it has most recently received a message on).
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._userId,
|
||||
this._deviceId,
|
||||
this._olmDevice,
|
||||
sender,
|
||||
device,
|
||||
{type: "m.dummy"},
|
||||
);
|
||||
|
||||
await this._baseApis.sendToDevice("m.room.encrypted", {
|
||||
[sender]: {
|
||||
[device.deviceId]: encryptedContent,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Most of the time this probably won't be necessary since we'll have queued up a key request when
|
||||
// we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
|
||||
// it. This won't always be the case though so we need to re-send any that have already been sent
|
||||
// to avoid races.
|
||||
const requestsToResend =
|
||||
await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(
|
||||
sender, device.deviceId,
|
||||
);
|
||||
for (const keyReq of requestsToResend) {
|
||||
this.cancelRoomKeyRequest(keyReq.requestBody, true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a change in the membership state of a member of a room
|
||||
*
|
||||
@@ -1184,12 +1571,12 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
||||
// by calling _trackRoomDevices
|
||||
if (this._roomDeviceTrackingState[roomId]) {
|
||||
if (member.membership == 'join') {
|
||||
console.log('Join event for ' + member.userId + ' in ' + roomId);
|
||||
logger.log('Join event for ' + member.userId + ' in ' + roomId);
|
||||
// make sure we are tracking the deviceList for this user
|
||||
this._deviceList.startTrackingDeviceList(member.userId);
|
||||
} else if (member.membership == 'invite' &&
|
||||
this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) {
|
||||
console.log('Invite event for ' + member.userId + ' in ' + roomId);
|
||||
logger.log('Invite event for ' + member.userId + ' in ' + roomId);
|
||||
this._deviceList.startTrackingDeviceList(member.userId);
|
||||
}
|
||||
}
|
||||
@@ -1256,7 +1643,7 @@ Crypto.prototype._processReceivedRoomKeyRequests = async function() {
|
||||
this._processReceivedRoomKeyRequestCancellation(cancellation),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`Error processing room key requsts: ${e}`);
|
||||
logger.error(`Error processing room key requsts: ${e}`);
|
||||
} finally {
|
||||
this._processingRoomKeyRequests = false;
|
||||
}
|
||||
@@ -1275,13 +1662,31 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
|
||||
const roomId = body.room_id;
|
||||
const alg = body.algorithm;
|
||||
|
||||
console.log(`m.room_key_request from ${userId}:${deviceId}` +
|
||||
logger.log(`m.room_key_request from ${userId}:${deviceId}` +
|
||||
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
||||
|
||||
if (userId !== this._userId) {
|
||||
// TODO: determine if we sent this device the keys already: in
|
||||
// which case we can do so again.
|
||||
console.log("Ignoring room key request from other user for now");
|
||||
if (!this._roomEncryptors[roomId]) {
|
||||
logger.debug(`room key request for unencrypted room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
const encryptor = this._roomEncryptors[roomId];
|
||||
const device = this._deviceList.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await encryptor.reshareKeyWithDevice(
|
||||
body.sender_key, body.session_id, userId, device,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to re-share keys for session " + body.session_id +
|
||||
" with device " + userId + ":" + device.deviceId, e,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1291,18 +1696,18 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
|
||||
// if we don't have a decryptor for this room/alg, we don't have
|
||||
// the keys for the requested events, and can drop the requests.
|
||||
if (!this._roomDecryptors[roomId]) {
|
||||
console.log(`room key request for unencrypted room ${roomId}`);
|
||||
logger.log(`room key request for unencrypted room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const decryptor = this._roomDecryptors[roomId][alg];
|
||||
if (!decryptor) {
|
||||
console.log(`room key request for unknown alg ${alg} in room ${roomId}`);
|
||||
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await decryptor.hasKeysForKeyRequest(req)) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`room key request for unknown session ${roomId} / ` +
|
||||
body.session_id,
|
||||
);
|
||||
@@ -1316,7 +1721,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
|
||||
// if the device is is verified already, share the keys
|
||||
const device = this._deviceList.getStoredDevice(userId, deviceId);
|
||||
if (device && device.isVerified()) {
|
||||
console.log('device is already verified: sharing keys');
|
||||
logger.log('device is already verified: sharing keys');
|
||||
req.share();
|
||||
return;
|
||||
}
|
||||
@@ -1333,7 +1738,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
|
||||
Crypto.prototype._processReceivedRoomKeyRequestCancellation = async function(
|
||||
cancellation,
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`m.room_key_request cancellation for ${cancellation.userId}:` +
|
||||
`${cancellation.deviceId} (id ${cancellation.requestId})`,
|
||||
);
|
||||
@@ -1518,6 +1923,3 @@ class IncomingRoomKeyRequestCancellation {
|
||||
* @event module:client~MatrixClient#"crypto.warning"
|
||||
* @param {string} type One of the strings listed above
|
||||
*/
|
||||
|
||||
/** */
|
||||
module.exports = Crypto;
|
||||
|
||||
+18
-9
@@ -23,6 +23,7 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
const anotherjson = require('another-json');
|
||||
|
||||
const logger = require("../logger");
|
||||
const utils = require("../utils");
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
*/
|
||||
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for megolm backups
|
||||
*/
|
||||
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event payload for an Olm device
|
||||
@@ -65,7 +71,7 @@ module.exports.encryptMessageForDevice = async function(
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
"Using sessionid " + sessionId + " for device " +
|
||||
recipientUserId + ":" + recipientDevice.deviceId,
|
||||
);
|
||||
@@ -115,14 +121,17 @@ module.exports.encryptMessageForDevice = async function(
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
*
|
||||
* @param {bolean} force If true, establish a new session even if one already exists.
|
||||
* Optional.
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
module.exports.ensureOlmSessionsForDevices = async function(
|
||||
olmDevice, baseApis, devicesByUser,
|
||||
olmDevice, baseApis, devicesByUser, force,
|
||||
) {
|
||||
const devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
@@ -140,7 +149,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key);
|
||||
if (sessionId === null) {
|
||||
if (sessionId === null || force) {
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
@@ -176,7 +185,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
if (result[userId][deviceId].sessionId) {
|
||||
if (result[userId][deviceId].sessionId && !force) {
|
||||
// we already have a result for this device
|
||||
continue;
|
||||
}
|
||||
@@ -190,7 +199,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId,
|
||||
);
|
||||
@@ -219,7 +228,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
deviceInfo.getFingerprint(),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Unable to verify signature on one-time key for device " +
|
||||
userId + ":" + deviceId + ":", e,
|
||||
);
|
||||
@@ -233,12 +242,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
);
|
||||
} catch (e) {
|
||||
// possibly a bad key
|
||||
console.error("Error starting session with device " +
|
||||
logger.error("Error starting session with device " +
|
||||
userId + ":" + deviceId + ": " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Started new sessionid " + sid +
|
||||
logger.log("Started new sessionid " + sid +
|
||||
" for device " + userId + ":" + deviceId);
|
||||
return sid;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import bs58 from 'bs58';
|
||||
|
||||
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
|
||||
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
|
||||
export function encodeRecoveryKey(key) {
|
||||
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
|
||||
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
|
||||
|
||||
let parity = 0;
|
||||
for (let i = 0; i < buf.length - 1; ++i) {
|
||||
parity ^= buf[i];
|
||||
}
|
||||
buf[buf.length - 1] = parity;
|
||||
const base58key = bs58.encode(buf);
|
||||
|
||||
return base58key.match(/.{1,4}/g).join(" ");
|
||||
}
|
||||
|
||||
export function decodeRecoveryKey(recoverykey) {
|
||||
const result = bs58.decode(recoverykey.replace(/ /g, ''));
|
||||
|
||||
let parity = 0;
|
||||
for (const b of result) {
|
||||
parity ^= b;
|
||||
}
|
||||
if (parity !== 0) {
|
||||
throw new Error("Incorrect parity");
|
||||
}
|
||||
|
||||
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
|
||||
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
|
||||
throw new Error("Incorrect prefix");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.length !==
|
||||
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1
|
||||
) {
|
||||
throw new Error("Incorrect length");
|
||||
}
|
||||
|
||||
return result.slice(
|
||||
OLM_RECOVERY_KEY_PREFIX.length,
|
||||
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
|
||||
);
|
||||
}
|
||||
@@ -16,9 +16,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../../logger';
|
||||
import utils from '../../utils';
|
||||
|
||||
export const VERSION = 6;
|
||||
export const VERSION = 7;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -38,7 +40,7 @@ export class Backend {
|
||||
// attempts to delete the database will block (and subsequent
|
||||
// attempts to re-create it will also block).
|
||||
db.onversionchange = (ev) => {
|
||||
console.log(`versionchange for indexeddb ${this._dbName}: closing`);
|
||||
logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
|
||||
db.close();
|
||||
};
|
||||
}
|
||||
@@ -64,7 +66,7 @@ export class Backend {
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
console.log(
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
@@ -75,7 +77,7 @@ export class Backend {
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
console.log(
|
||||
logger.log(
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
@@ -204,6 +206,42 @@ export class Backend {
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
let stateIndex = 0;
|
||||
const results = [];
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (keyReq.recipients.includes({userId, deviceId})) {
|
||||
results.push(keyReq);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
// try the next state in the list
|
||||
stateIndex++;
|
||||
if (stateIndex >= wantedStates.length) {
|
||||
// no matches
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = ev.target.source.openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
}
|
||||
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = store.index("state").openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
|
||||
return promiseifyTxn(txn).then(() => results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
@@ -226,7 +264,7 @@ export class Backend {
|
||||
}
|
||||
const data = cursor.value;
|
||||
if (data.state != expectedState) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Cannot update room key request from ${expectedState} ` +
|
||||
`as it was already updated to ${data.state}`,
|
||||
);
|
||||
@@ -264,7 +302,7 @@ export class Backend {
|
||||
}
|
||||
const data = cursor.value;
|
||||
if (data.state != expectedState) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Cannot delete room key request in state ${data.state} `
|
||||
+ `(expected ${expectedState})`,
|
||||
);
|
||||
@@ -312,7 +350,10 @@ export class Backend {
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
results[cursor.value.sessionId] = cursor.value.session;
|
||||
results[cursor.value.sessionId] = {
|
||||
session: cursor.value.session,
|
||||
lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
|
||||
};
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
@@ -330,7 +371,10 @@ export class Backend {
|
||||
getReq.onsuccess = function() {
|
||||
try {
|
||||
if (getReq.result) {
|
||||
func(getReq.result.session);
|
||||
func({
|
||||
session: getReq.result.session,
|
||||
lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
|
||||
});
|
||||
} else {
|
||||
func(null);
|
||||
}
|
||||
@@ -340,9 +384,32 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, session, txn) {
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
objectStore.put({deviceKey, sessionId, session});
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
func(null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
objectStore.put({
|
||||
deviceKey,
|
||||
sessionId,
|
||||
session: sessionInfo.session,
|
||||
lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
|
||||
});
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -400,7 +467,7 @@ export class Backend {
|
||||
ev.stopPropagation();
|
||||
// ...and this stops it from aborting the transaction
|
||||
ev.preventDefault();
|
||||
console.log(
|
||||
logger.log(
|
||||
"Ignoring duplicate inbound group session: " +
|
||||
senderCurve25519Key + " / " + sessionId,
|
||||
);
|
||||
@@ -460,6 +527,71 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
getSessionsNeedingBackup(limit) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sessions = [];
|
||||
|
||||
const txn = this._db.transaction(
|
||||
["sessions_needing_backup", "inbound_group_sessions"],
|
||||
"readonly",
|
||||
);
|
||||
txn.onerror = reject;
|
||||
txn.oncomplete = function() {
|
||||
resolve(sessions);
|
||||
};
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
const sessionStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
const sessionGetReq = sessionStore.get(cursor.key);
|
||||
sessionGetReq.onsuccess = function() {
|
||||
sessions.push({
|
||||
senderKey: sessionGetReq.result.senderCurve25519Key,
|
||||
sessionId: sessionGetReq.result.sessionId,
|
||||
sessionData: sessionGetReq.result.session,
|
||||
});
|
||||
};
|
||||
if (!limit || sessions.length < limit) {
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
const txn = this._db.transaction("sessions_needing_backup", "readwrite");
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return Promise.all(sessions.map((session) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.delete([session.senderKey, session.sessionId]);
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = reject;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
markSessionsNeedingBackup(sessions, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction("sessions_needing_backup", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return Promise.all(sessions.map((session) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.put({
|
||||
senderCurve25519Key: session.senderKey,
|
||||
sessionId: session.sessionId,
|
||||
});
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = reject;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
const txn = this._db.transaction(stores, mode);
|
||||
const promise = promiseifyTxn(txn);
|
||||
@@ -471,7 +603,7 @@ export class Backend {
|
||||
}
|
||||
|
||||
export function upgradeDatabase(db, oldVersion) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
||||
+ ` to ${VERSION}`,
|
||||
);
|
||||
@@ -498,6 +630,11 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
if (oldVersion < 6) {
|
||||
db.createObjectStore("rooms");
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
db.createObjectStore("sessions_needing_backup", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../../logger';
|
||||
import LocalStorageCryptoStore from './localStorage-crypto-store';
|
||||
import MemoryCryptoStore from './memory-crypto-store';
|
||||
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
|
||||
import {InvalidCryptoStoreError} from '../../errors';
|
||||
|
||||
/**
|
||||
* Internal module. indexeddb storage for e2e.
|
||||
@@ -64,7 +66,7 @@ export default class IndexedDBCryptoStore {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`connecting to indexeddb ${this._dbName}`);
|
||||
logger.log(`connecting to indexeddb ${this._dbName}`);
|
||||
|
||||
const req = this._indexedDB.open(
|
||||
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
|
||||
@@ -77,7 +79,7 @@ export default class IndexedDBCryptoStore {
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
|
||||
);
|
||||
};
|
||||
@@ -89,20 +91,42 @@ export default class IndexedDBCryptoStore {
|
||||
req.onsuccess = (r) => {
|
||||
const db = r.target.result;
|
||||
|
||||
console.log(`connected to indexeddb ${this._dbName}`);
|
||||
logger.log(`connected to indexeddb ${this._dbName}`);
|
||||
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
|
||||
};
|
||||
}).then((backend) => {
|
||||
// Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
|
||||
// Try a dummy query which will fail if the browser doesn't support compund keys, so
|
||||
// we can fall back to a different backend.
|
||||
return backend.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
|
||||
(txn) => {
|
||||
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
|
||||
}).then(() => {
|
||||
return backend;
|
||||
},
|
||||
);
|
||||
}).catch((e) => {
|
||||
console.warn(
|
||||
if (e.name === 'VersionError') {
|
||||
logger.warn("Crypto DB is too new for us to use!", e);
|
||||
// don't fall back to a different store: the user has crypto data
|
||||
// in this db so we should use it or nothing at all.
|
||||
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
|
||||
}
|
||||
logger.warn(
|
||||
`unable to connect to indexeddb ${this._dbName}` +
|
||||
`: falling back to localStorage store: ${e}`,
|
||||
);
|
||||
return new LocalStorageCryptoStore(global.localStorage);
|
||||
}).catch((e) => {
|
||||
console.warn(
|
||||
`unable to open localStorage: falling back to in-memory store: ${e}`,
|
||||
);
|
||||
return new MemoryCryptoStore();
|
||||
|
||||
try {
|
||||
return new LocalStorageCryptoStore(global.localStorage);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`unable to open localStorage: falling back to in-memory store: ${e}`,
|
||||
);
|
||||
return new MemoryCryptoStore();
|
||||
}
|
||||
});
|
||||
|
||||
return this._backendPromise;
|
||||
@@ -120,11 +144,11 @@ export default class IndexedDBCryptoStore {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Removing indexeddb instance: ${this._dbName}`);
|
||||
logger.log(`Removing indexeddb instance: ${this._dbName}`);
|
||||
const req = this._indexedDB.deleteDatabase(this._dbName);
|
||||
|
||||
req.onblocked = () => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
|
||||
);
|
||||
};
|
||||
@@ -134,14 +158,14 @@ export default class IndexedDBCryptoStore {
|
||||
};
|
||||
|
||||
req.onsuccess = () => {
|
||||
console.log(`Removed indexeddb instance: ${this._dbName}`);
|
||||
logger.log(`Removed indexeddb instance: ${this._dbName}`);
|
||||
resolve();
|
||||
};
|
||||
}).catch((e) => {
|
||||
// in firefox, with indexedDB disabled, this fails with a
|
||||
// DOMError. We treat this as non-fatal, so that people can
|
||||
// still use the app.
|
||||
console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
|
||||
logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,6 +217,24 @@ export default class IndexedDBCryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by target device and state
|
||||
*
|
||||
* @param {string} userId Target user ID
|
||||
* @param {string} deviceId Target device ID
|
||||
* @param {Array<Number>} wantedStates list of acceptable states
|
||||
*
|
||||
* @return {Promise} resolves to a list of all the
|
||||
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||
*/
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, wantedStates,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
@@ -270,7 +312,10 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {string} sessionId The ID of the session to retrieve
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(object)} func Called with A map from sessionId
|
||||
* to Base64 end-to-end session.
|
||||
* to session information object with 'session' key being the
|
||||
* Base64 end-to-end session and lastReceivedMessageTs being the
|
||||
* timestamp in milliseconds at which the session last received
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
||||
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
@@ -282,22 +327,36 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {string} deviceKey The public key of the other device.
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(object)} func Called with A map from sessionId
|
||||
* to Base64 end-to-end session.
|
||||
* to session information object with 'session' key being the
|
||||
* Base64 end-to-end session and lastReceivedMessageTs being the
|
||||
* timestamp in milliseconds at which the session last received
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSessions(deviceKey, txn, func) {
|
||||
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all end-to-end sessions
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(object)} func Called one for each session with
|
||||
* an object with, deviceKey, lastReceivedMessageTs, sessionId
|
||||
* and session keys.
|
||||
*/
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
this._backendPromise.value().getAllEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device
|
||||
* @param {string} deviceKey The public key of the other device.
|
||||
* @param {string} sessionId The ID for this end-to-end session.
|
||||
* @param {string} session Base64 encoded end-to-end session.
|
||||
* @param {string} sessionInfo Session information object
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndSession(deviceKey, sessionId, session, txn) {
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
this._backendPromise.value().storeEndToEndSession(
|
||||
deviceKey, sessionId, session, txn,
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -407,6 +466,43 @@ export default class IndexedDBCryptoStore {
|
||||
this._backendPromise.value().getEndToEndRooms(txn, func);
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
/**
|
||||
* Get the inbound group sessions that need to be backed up.
|
||||
* @param {integer} limit The maximum number of sessions to retrieve. 0
|
||||
* for no limit.
|
||||
* @returns {Promise} resolves to an array of inbound group sessions
|
||||
*/
|
||||
getSessionsNeedingBackup(limit) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getSessionsNeedingBackup(limit);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark sessions as needing to be backed up.
|
||||
* @param {Array<object>} sessions The sessions that need to be backed up.
|
||||
* @returns {Promise} resolves when the sessions are unmarked
|
||||
*/
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.unmarkSessionsNeedingBackup(sessions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark sessions as needing to be backed up.
|
||||
* @param {Array<object>} sessions The sessions that need to be backed up.
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} resolves when the sessions are marked
|
||||
*/
|
||||
markSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.markSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a transaction on the crypto store. Any store methods
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
@@ -440,3 +536,4 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
|
||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||
|
||||
@@ -15,6 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../../logger';
|
||||
import MemoryCryptoStore from './memory-crypto-store.js';
|
||||
|
||||
/**
|
||||
@@ -32,6 +34,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
|
||||
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
|
||||
|
||||
function keyEndToEndSessions(deviceKey) {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
@@ -65,7 +68,21 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
}
|
||||
|
||||
_getEndToEndSessions(deviceKey, txn, func) {
|
||||
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
const fixedSessions = {};
|
||||
|
||||
// fix up any old sessions to be objects rather than just the base64 pickle
|
||||
for (const [sid, val] of Object.entries(sessions || {})) {
|
||||
if (typeof val === 'string') {
|
||||
fixedSessions[sid] = {
|
||||
session: val,
|
||||
};
|
||||
} else {
|
||||
fixedSessions[sid] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return fixedSessions;
|
||||
}
|
||||
|
||||
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
||||
@@ -77,9 +94,20 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
func(this._getEndToEndSessions(deviceKey) || {});
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, session, txn) {
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
|
||||
const deviceKey = this.store.key(i).split('/')[1];
|
||||
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
|
||||
func(sess);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
const sessions = this._getEndToEndSessions(deviceKey) || {};
|
||||
sessions[sessionId] = session;
|
||||
sessions[sessionId] = sessionInfo;
|
||||
setJsonItem(
|
||||
this.store, keyEndToEndSessions(deviceKey), sessions,
|
||||
);
|
||||
@@ -165,6 +193,58 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
func(result);
|
||||
}
|
||||
|
||||
getSessionsNeedingBackup(limit) {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const sessions = [];
|
||||
|
||||
for (const session in sessionsNeedingBackup) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
||||
// see getAllEndToEndInboundGroupSessions for the magic number explanations
|
||||
const senderKey = session.substr(0, 43);
|
||||
const sessionId = session.substr(44);
|
||||
this.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, null,
|
||||
(sessionData) => {
|
||||
sessions.push({
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
sessionData: sessionData,
|
||||
});
|
||||
},
|
||||
);
|
||||
if (limit && session.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
for (const session of sessions) {
|
||||
delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
|
||||
}
|
||||
setJsonItem(
|
||||
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
markSessionsNeedingBackup(sessions) {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
for (const session of sessions) {
|
||||
sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
|
||||
}
|
||||
setJsonItem(
|
||||
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all data from this store.
|
||||
*
|
||||
@@ -199,8 +279,8 @@ function getJsonItem(store, key) {
|
||||
// JSON.parse(null) === null, so this returns null.
|
||||
return JSON.parse(store.getItem(key));
|
||||
} catch (e) {
|
||||
console.log("Error: Failed to get key %s: %s", key, e.stack || e);
|
||||
console.log(e.stack);
|
||||
logger.log("Error: Failed to get key %s: %s", key, e.stack || e);
|
||||
logger.log(e.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../../logger';
|
||||
import utils from '../../utils';
|
||||
|
||||
/**
|
||||
@@ -41,6 +42,8 @@ export default class MemoryCryptoStore {
|
||||
this._deviceData = null;
|
||||
// roomId -> Opaque roomInfo object
|
||||
this._rooms = {};
|
||||
// Set of {senderCurve25519Key+'/'+sessionId}
|
||||
this._sessionsNeedingBackup = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +74,7 @@ export default class MemoryCryptoStore {
|
||||
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
console.log(
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
@@ -81,7 +84,7 @@ export default class MemoryCryptoStore {
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
console.log(
|
||||
logger.log(
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
@@ -144,6 +147,19 @@ export default class MemoryCryptoStore {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
const results = [];
|
||||
|
||||
for (const req of this._outgoingRoomKeyRequests) {
|
||||
for (const state of wantedStates) {
|
||||
if (req.state === state && req.recipients.includes({userId, deviceId})) {
|
||||
results.push(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
@@ -163,7 +179,7 @@ export default class MemoryCryptoStore {
|
||||
}
|
||||
|
||||
if (req.state != expectedState) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Cannot update room key request from ${expectedState} ` +
|
||||
`as it was already updated to ${req.state}`,
|
||||
);
|
||||
@@ -194,7 +210,7 @@ export default class MemoryCryptoStore {
|
||||
}
|
||||
|
||||
if (req.state != expectedState) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Cannot delete room key request in state ${req.state} `
|
||||
+ `(expected ${expectedState})`,
|
||||
);
|
||||
@@ -233,13 +249,21 @@ export default class MemoryCryptoStore {
|
||||
func(this._sessions[deviceKey] || {});
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, session, txn) {
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
for (const deviceSessions of Object.values(this._sessions)) {
|
||||
for (const sess of Object.values(deviceSessions)) {
|
||||
func(sess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
let deviceSessions = this._sessions[deviceKey];
|
||||
if (deviceSessions === undefined) {
|
||||
deviceSessions = {};
|
||||
this._sessions[deviceKey] = deviceSessions;
|
||||
}
|
||||
deviceSessions[sessionId] = session;
|
||||
deviceSessions[sessionId] = sessionInfo;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
@@ -295,6 +319,41 @@ export default class MemoryCryptoStore {
|
||||
func(this._rooms);
|
||||
}
|
||||
|
||||
getSessionsNeedingBackup(limit) {
|
||||
const sessions = [];
|
||||
for (const session in this._sessionsNeedingBackup) {
|
||||
if (this._inboundGroupSessions[session]) {
|
||||
sessions.push({
|
||||
senderKey: session.substr(0, 43),
|
||||
sessionId: session.substr(44),
|
||||
sessionData: this._inboundGroupSessions[session],
|
||||
});
|
||||
if (limit && session.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = session.senderKey + '/' + session.sessionId;
|
||||
delete this._sessionsNeedingBackup[sessionKey];
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
markSessionsNeedingBackup(sessions) {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = session.senderKey + '/' + session.sessionId;
|
||||
this._sessionsNeedingBackup[sessionKey] = true;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Session key backups
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
return Promise.resolve(func(null));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// can't just do InvalidStoreError extends Error
|
||||
// because of http://babeljs.io/docs/usage/caveats/#classes
|
||||
export function InvalidStoreError(reason, value) {
|
||||
const message = `Store is invalid because ${reason}, ` +
|
||||
`please stop the client, delete all data and start the client again`;
|
||||
const instance = Reflect.construct(Error, [message]);
|
||||
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
|
||||
instance.reason = reason;
|
||||
instance.value = value;
|
||||
return instance;
|
||||
}
|
||||
|
||||
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
|
||||
|
||||
InvalidStoreError.prototype = Object.create(Error.prototype, {
|
||||
constructor: {
|
||||
value: Error,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
Reflect.setPrototypeOf(InvalidStoreError, Error);
|
||||
|
||||
|
||||
export function InvalidCryptoStoreError(reason) {
|
||||
const message = `Crypto store is invalid because ${reason}, ` +
|
||||
`please stop the client, delete all data and start the client again`;
|
||||
const instance = Reflect.construct(Error, [message]);
|
||||
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
|
||||
instance.reason = reason;
|
||||
instance.name = 'InvalidCryptoStoreError';
|
||||
return instance;
|
||||
}
|
||||
|
||||
InvalidCryptoStoreError.TOO_NEW = "TOO_NEW";
|
||||
|
||||
InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
|
||||
constructor: {
|
||||
value: Error,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
|
||||
@@ -51,6 +51,17 @@ function Filter(userId, filterId) {
|
||||
this.definition = {};
|
||||
}
|
||||
|
||||
Filter.LAZY_LOADING_MESSAGES_FILTER = {
|
||||
lazy_load_members: true,
|
||||
};
|
||||
|
||||
Filter.LAZY_LOADING_SYNC_FILTER = {
|
||||
room: {
|
||||
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the ID of this filter on your homeserver (if known)
|
||||
* @return {?Number} The filter ID
|
||||
|
||||
@@ -752,6 +752,8 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
qs: queryParams,
|
||||
qsStringifyOptions: opts.qsStringifyOptions,
|
||||
useQuerystring: true,
|
||||
body: data,
|
||||
json: false,
|
||||
timeout: localTimeoutMs,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2018 André Jaenisch
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module logger
|
||||
*/
|
||||
const log = require("loglevel");
|
||||
|
||||
// This is to demonstrate, that you can use any namespace you want.
|
||||
// Namespaces allow you to turn on/off the logging for specific parts of the
|
||||
// application.
|
||||
// An idea would be to control this via an environment variable (on Node.js).
|
||||
// See https://www.npmjs.com/package/debug to see how this could be implemented
|
||||
// Part of #332 is introducing a logging library in the first place.
|
||||
const DEFAULT_NAME_SPACE = "matrix";
|
||||
const logger = log.getLogger(DEFAULT_NAME_SPACE);
|
||||
logger.setLevel(log.levels.DEBUG);
|
||||
|
||||
/**
|
||||
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
|
||||
* Can be tailored down to specific use cases if needed.
|
||||
*/
|
||||
module.exports = logger;
|
||||
@@ -34,6 +34,8 @@ module.exports.SyncAccumulator = require("./sync-accumulator");
|
||||
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
|
||||
/** The {@link module:http-api.MatrixError|MatrixError} class. */
|
||||
module.exports.MatrixError = require("./http-api").MatrixError;
|
||||
/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */
|
||||
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
|
||||
/** The {@link module:client.MatrixClient|MatrixClient} class. */
|
||||
module.exports.MatrixClient = require("./client").MatrixClient;
|
||||
/** The {@link module:models/room|Room} class. */
|
||||
@@ -65,6 +67,8 @@ module.exports.Filter = require("./filter");
|
||||
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
||||
/** The {@link module:interactive-auth} class. */
|
||||
module.exports.InteractiveAuth = require("./interactive-auth");
|
||||
/** The {@link module:auto-discovery|AutoDiscovery} class. */
|
||||
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
|
||||
|
||||
|
||||
module.exports.MemoryCryptoStore =
|
||||
|
||||
@@ -59,7 +59,6 @@ function RoomMember(roomId, userId) {
|
||||
member: null,
|
||||
};
|
||||
this._isOutOfBand = false;
|
||||
this._supersedesOutOfBand = false;
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
utils.inherits(RoomMember, EventEmitter);
|
||||
@@ -80,31 +79,6 @@ RoomMember.prototype.isOutOfBand = function() {
|
||||
return this._isOutOfBand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Does the member supersede an incoming out-of-band
|
||||
* member? If so the out-of-band member should be ignored.
|
||||
* @return {bool}
|
||||
*/
|
||||
RoomMember.prototype.supersedesOutOfBand = function() {
|
||||
return this._supersedesOutOfBand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark the member as superseding the future incoming
|
||||
* out-of-band members.
|
||||
*/
|
||||
RoomMember.prototype.markSupersedesOutOfBand = function() {
|
||||
this._supersedesOutOfBand = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the member superseding the future incoming
|
||||
* out-of-band members, as loading finished or failed.
|
||||
*/
|
||||
RoomMember.prototype.clearSupersedesOutOfBand = function() {
|
||||
this._supersedesOutOfBand = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update this room member's membership event. May fire "RoomMember.name" if
|
||||
* this event updates this member's name.
|
||||
@@ -324,26 +298,24 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
|
||||
return selfUserId;
|
||||
}
|
||||
|
||||
if (!roomState) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
// First check if the displayname is something we consider truthy
|
||||
// after stripping it of zero width characters and padding spaces
|
||||
const strippedDisplayName = utils.removeHiddenChars(displayName);
|
||||
if (!strippedDisplayName) {
|
||||
if (!utils.removeHiddenChars(displayName)) {
|
||||
return selfUserId;
|
||||
}
|
||||
|
||||
if (!roomState) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
// Next check if the name contains something that look like a mxid
|
||||
// If it does, it may be someone trying to impersonate someone else
|
||||
// Show full mxid in this case
|
||||
// Also show mxid if there are other people with the same displayname
|
||||
// ignoring any zero width chars (unicode 200B-200D)
|
||||
// if their displayname is made up of just zero width chars, show full mxid
|
||||
// Also show mxid if there are other people with the same or similar
|
||||
// displayname, after hidden character removal.
|
||||
let disambiguate = /@.+:.+/.test(displayName);
|
||||
if (!disambiguate) {
|
||||
const userIds = roomState.getUserIdsWithDisplayName(strippedDisplayName);
|
||||
const userIds = roomState.getUserIdsWithDisplayName(displayName);
|
||||
disambiguate = userIds.some((u) => u !== selfUserId);
|
||||
}
|
||||
|
||||
|
||||
+22
-42
@@ -75,6 +75,8 @@ function RoomState(roomId, oobMemberFlags = undefined) {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
|
||||
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
|
||||
this._displayNameToUserIds = {};
|
||||
this._userIdsToDisplayNames = {};
|
||||
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
|
||||
@@ -154,6 +156,16 @@ RoomState.prototype.getMembers = function() {
|
||||
return utils.values(this.members);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all RoomMembers in this room, excluding the user IDs provided.
|
||||
* @param {Array<string>} excludedIds The user IDs to exclude.
|
||||
* @return {Array<RoomMember>} A list of RoomMembers.
|
||||
*/
|
||||
RoomState.prototype.getMembersExcept = function(excludedIds) {
|
||||
return utils.values(this.members)
|
||||
.filter((m) => !excludedIds.includes(m.userId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a room member by their user ID.
|
||||
* @param {string} userId The room member's user ID.
|
||||
@@ -180,7 +192,7 @@ RoomState.prototype.getSentinelMember = function(userId) {
|
||||
sentinel = new RoomMember(this.roomId, userId);
|
||||
const member = this.members[userId];
|
||||
if (member) {
|
||||
sentinel.setMembershipEvent(member.events.member);
|
||||
sentinel.setMembershipEvent(member.events.member, this);
|
||||
}
|
||||
this._sentinels[userId] = sentinel;
|
||||
}
|
||||
@@ -220,8 +232,7 @@ RoomState.prototype.clone = function() {
|
||||
// if loading is in progress (through _oobMemberFlags)
|
||||
// since these are not new members, we're merely copying them
|
||||
// set the status to not started
|
||||
// after copying, we set back the status and
|
||||
// copy the superseding flag from the current state
|
||||
// after copying, we set back the status
|
||||
const status = this._oobMemberFlags.status;
|
||||
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
||||
|
||||
@@ -249,14 +260,6 @@ RoomState.prototype.clone = function() {
|
||||
copyMember.markOutOfBand();
|
||||
}
|
||||
});
|
||||
} else if (this._oobMemberFlags.status == OOB_STATUS_INPROGRESS) {
|
||||
// copy markSupersedesOutOfBand flags
|
||||
this.getMembers().forEach((member) => {
|
||||
if (member.supersedesOutOfBand()) {
|
||||
const copyMember = copy.getMember(member.userId);
|
||||
copyMember.markSupersedesOutOfBand();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return copy;
|
||||
@@ -341,11 +344,6 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
|
||||
const member = self._getOrCreateMember(userId, event);
|
||||
member.setMembershipEvent(event, self);
|
||||
// if out of band members are loading,
|
||||
// mark the member as more recent
|
||||
if (self._oobMemberFlags.status == OOB_STATUS_INPROGRESS) {
|
||||
member.markSupersedesOutOfBand();
|
||||
}
|
||||
|
||||
self._updateMember(member);
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
@@ -434,12 +432,6 @@ RoomState.prototype.markOutOfBandMembersFailed = function() {
|
||||
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
|
||||
return;
|
||||
}
|
||||
// the request failed, there is nothing to supersede
|
||||
// in case of a retry, these event would not supersede the
|
||||
// retry anymore.
|
||||
this.getMembers().forEach((m) => {
|
||||
m.clearSupersedesOutOfBand();
|
||||
});
|
||||
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
||||
};
|
||||
|
||||
@@ -483,27 +475,15 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
|
||||
}
|
||||
const userId = stateEvent.getStateKey();
|
||||
const existingMember = this.getMember(userId);
|
||||
if (existingMember) {
|
||||
const existingMemberEvent = existingMember.events.member;
|
||||
// ignore out of band members with events we are
|
||||
// already aware of.
|
||||
if (existingMemberEvent.getId() === stateEvent.getId()) {
|
||||
return;
|
||||
}
|
||||
// this member was updated since we started
|
||||
// loading the out of band members.
|
||||
// Ignore the out of band member and clear
|
||||
// the "supersedes" flag as the out of members are now loaded
|
||||
if (existingMember.supersedesOutOfBand()) {
|
||||
existingMember.clearSupersedesOutOfBand();
|
||||
return;
|
||||
}
|
||||
// never replace members received as part of the sync
|
||||
if (existingMember && !existingMember.isOutOfBand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = this._getOrCreateMember(userId, stateEvent);
|
||||
member.setMembershipEvent(stateEvent);
|
||||
member.setMembershipEvent(stateEvent, this);
|
||||
// needed to know which members need to be stored seperately
|
||||
// as the are not part of the sync accumulator
|
||||
// as they are not part of the sync accumulator
|
||||
// this is cleared by setMembershipEvent so when it's updated through /sync
|
||||
member.markOutOfBand();
|
||||
|
||||
@@ -511,7 +491,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
|
||||
|
||||
this._setStateEvent(stateEvent);
|
||||
this._updateMember(member);
|
||||
this.emit("RoomState.members", {}, stateEvent, member);
|
||||
this.emit("RoomState.members", stateEvent, this, member);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -551,12 +531,12 @@ RoomState.prototype.getLastModifiedTime = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user IDs with the specified display name.
|
||||
* Get user IDs with the specified or similar display names.
|
||||
* @param {string} displayName The display name to get user IDs from.
|
||||
* @return {string[]} An array of user IDs or an empty array.
|
||||
*/
|
||||
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
|
||||
return this._displayNameToUserIds[displayName] || [];
|
||||
return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+42
-26
@@ -178,7 +178,7 @@ function Room(roomId, client, myUserId, opts) {
|
||||
|
||||
// read by megolm; boolean value - null indicates "use global value"
|
||||
this._blacklistUnverifiedDevices = null;
|
||||
this._syncedMembership = null;
|
||||
this._selfMembership = null;
|
||||
this._summaryHeroes = null;
|
||||
// awaited by getEncryptionTargetMembers while room members are loading
|
||||
|
||||
@@ -198,7 +198,10 @@ utils.inherits(Room, EventEmitter);
|
||||
*/
|
||||
Room.prototype.getVersion = function() {
|
||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent) return null;
|
||||
if (!createEvent) {
|
||||
console.warn("Room " + this.room_id + " does not have an m.room.create event");
|
||||
return '1';
|
||||
}
|
||||
const ver = createEvent.getContent()['room_version'];
|
||||
if (ver === undefined) return '1';
|
||||
return ver;
|
||||
@@ -257,13 +260,7 @@ Room.prototype.getLiveTimeline = function() {
|
||||
* @return {string} the membership type (join | leave | invite) for the logged in user
|
||||
*/
|
||||
Room.prototype.getMyMembership = function() {
|
||||
if (this.myUserId) {
|
||||
const me = this.getMember(this.myUserId);
|
||||
if (me) {
|
||||
return me.membership;
|
||||
}
|
||||
}
|
||||
return this._syncedMembership;
|
||||
return this._selfMembership;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -278,7 +275,7 @@ Room.prototype.getDMInviter = function() {
|
||||
return me.getDMInviter();
|
||||
}
|
||||
}
|
||||
if (this._syncedMembership === "invite") {
|
||||
if (this._selfMembership === "invite") {
|
||||
// fall back to summary information
|
||||
const memberCount = this.getInvitedAndJoinedMemberCount();
|
||||
if (memberCount == 2 && this._summaryHeroes.length) {
|
||||
@@ -362,8 +359,15 @@ Room.prototype.getAvatarFallbackMember = function() {
|
||||
* Sets the membership this room was received as during sync
|
||||
* @param {string} membership join | leave | invite
|
||||
*/
|
||||
Room.prototype.setSyncedMembership = function(membership) {
|
||||
this._syncedMembership = membership;
|
||||
Room.prototype.updateMyMembership = function(membership) {
|
||||
const prevMembership = this._selfMembership;
|
||||
this._selfMembership = membership;
|
||||
if (prevMembership !== membership) {
|
||||
if (membership === "leave") {
|
||||
this._cleanupAfterLeaving();
|
||||
}
|
||||
this.emit("Room.myMembership", this, membership, prevMembership);
|
||||
}
|
||||
};
|
||||
|
||||
Room.prototype._loadMembersFromServer = async function() {
|
||||
@@ -413,8 +417,21 @@ Room.prototype.loadMembersIfNeeded = function() {
|
||||
// the OOB members
|
||||
this.currentState.markOutOfBandMembersStarted();
|
||||
|
||||
const promise = this._loadMembers().then(({memberEvents, fromServer}) => {
|
||||
this.currentState.setOutOfBandMembers(memberEvents);
|
||||
const inMemoryUpdate = this._loadMembers().then((result) => {
|
||||
this.currentState.setOutOfBandMembers(result.memberEvents);
|
||||
// now the members are loaded, start to track the e2e devices if needed
|
||||
if (this._client.isRoomEncrypted(this.roomId)) {
|
||||
this._client._crypto.trackRoomDevices(this.roomId);
|
||||
}
|
||||
return result.fromServer;
|
||||
}).catch((err) => {
|
||||
// allow retries on fail
|
||||
this._membersPromise = null;
|
||||
this.currentState.markOutOfBandMembersFailed();
|
||||
throw err;
|
||||
});
|
||||
// update members in storage, but don't wait for it
|
||||
inMemoryUpdate.then((fromServer) => {
|
||||
if (fromServer) {
|
||||
const oobMembers = this.currentState.getMembers()
|
||||
.filter((m) => m.isOutOfBand())
|
||||
@@ -431,17 +448,12 @@ Room.prototype.loadMembersIfNeeded = function() {
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
// allow retries on fail
|
||||
this._membersPromise = null;
|
||||
this.currentState.markOutOfBandMembersFailed();
|
||||
throw err;
|
||||
// as this is not awaited anywhere,
|
||||
// at least show the error in the console
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
this._membersPromise = promise;
|
||||
// now the members are loaded, start to track the e2e devices if needed
|
||||
if (this._client.isRoomEncrypted(this.roomId)) {
|
||||
this._client._crypto.trackRoomDevices(this.roomId);
|
||||
}
|
||||
this._membersPromise = inMemoryUpdate;
|
||||
|
||||
return this._membersPromise;
|
||||
};
|
||||
@@ -462,7 +474,7 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() {
|
||||
* called when sync receives this room in the leave section
|
||||
* to do cleanup after leaving a room. Possibly called multiple times.
|
||||
*/
|
||||
Room.prototype.onLeft = function() {
|
||||
Room.prototype._cleanupAfterLeaving = function() {
|
||||
this.clearLoadedMembersIfNeeded().catch((err) => {
|
||||
console.error(`error after clearing loaded members from ` +
|
||||
`room ${this.roomId} after leaving`);
|
||||
@@ -987,6 +999,10 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
this._txnToEvent[txnId] = event;
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
console.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
}
|
||||
this._pendingEventList.push(event);
|
||||
} else {
|
||||
for (let i = 0; i < this._timelineSets.length; i++) {
|
||||
@@ -1151,7 +1167,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
this.removeEvent(oldEventId);
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus);
|
||||
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
|
||||
};
|
||||
|
||||
|
||||
@@ -1594,7 +1610,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
function memberNamesToRoomName(names, count = (names.length + 1)) {
|
||||
const countWithoutMe = count - 1;
|
||||
if (!names.length) {
|
||||
return count <= 1 ? "Empty room" : null;
|
||||
return "Empty room";
|
||||
} else if (names.length === 1 && countWithoutMe <= 1) {
|
||||
return names[0];
|
||||
} else if (names.length === 2 && countWithoutMe <= 2) {
|
||||
|
||||
@@ -39,6 +39,9 @@ limitations under the License.
|
||||
* when a user was last active.
|
||||
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
||||
* an approximation and that the user should be seen as active 'now'
|
||||
* @prop {string} _unstable_statusMessage The status message for the user, if known. This is
|
||||
* different from the presenceStatusMsg in that this is not tied to
|
||||
* the user's presence, and should be represented differently.
|
||||
* @prop {Object} events The events describing this user.
|
||||
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
||||
*/
|
||||
@@ -46,6 +49,7 @@ function User(userId) {
|
||||
this.userId = userId;
|
||||
this.presence = "offline";
|
||||
this.presenceStatusMsg = null;
|
||||
this._unstable_statusMessage = "";
|
||||
this.displayName = userId;
|
||||
this.rawDisplayName = userId;
|
||||
this.avatarUrl = null;
|
||||
@@ -179,6 +183,16 @@ User.prototype.getLastActiveTs = function() {
|
||||
return this.lastPresenceTs - this.lastActiveAgo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually set the user's status message.
|
||||
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||
*/
|
||||
User.prototype._unstable_updateStatusMessage = function(event) {
|
||||
if (!event.getContent()) this._unstable_statusMessage = "";
|
||||
else this._unstable_statusMessage = event.getContent()["status"];
|
||||
this._updateModifiedTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* The User class.
|
||||
*/
|
||||
|
||||
+3
-20
@@ -14,6 +14,9 @@ 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.
|
||||
*/
|
||||
|
||||
import {escapeRegExp, globToRegexp} from "./utils";
|
||||
|
||||
/**
|
||||
* @module pushprocessor
|
||||
*/
|
||||
@@ -26,10 +29,6 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'
|
||||
* @param {Object} client The Matrix client object to use
|
||||
*/
|
||||
function PushProcessor(client) {
|
||||
const escapeRegExp = function(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
const cachedGlobToRegex = {
|
||||
// $glob: RegExp,
|
||||
};
|
||||
@@ -244,22 +243,6 @@ function PushProcessor(client) {
|
||||
return cachedGlobToRegex[glob];
|
||||
};
|
||||
|
||||
const globToRegexp = function(glob) {
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
// Because micromatch is about 130KB with dependencies,
|
||||
// and minimatch is not much better.
|
||||
let pat = escapeRegExp(glob);
|
||||
pat = pat.replace(/\\\*/g, '.*');
|
||||
pat = pat.replace(/\?/g, '.');
|
||||
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
|
||||
const first = p1 && '^' || '';
|
||||
const second = p2.replace(/\\\-/, '-');
|
||||
return '[' + first + second + ']';
|
||||
});
|
||||
return pat;
|
||||
};
|
||||
|
||||
const valueForDottedKey = function(key, ev) {
|
||||
const parts = key.split('.');
|
||||
let val;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
export function randomString(len) {
|
||||
let ret = "";
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import Promise from 'bluebird';
|
||||
import SyncAccumulator from "../sync-accumulator";
|
||||
import utils from "../utils";
|
||||
|
||||
const VERSION = 2;
|
||||
const VERSION = 3;
|
||||
|
||||
function createDatabase(db) {
|
||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||
@@ -41,6 +41,12 @@ function upgradeSchemaV2(db) {
|
||||
oobMembersStore.createIndex("room", "room_id");
|
||||
}
|
||||
|
||||
function upgradeSchemaV3(db) {
|
||||
db.createObjectStore("client_options",
|
||||
{ keyPath: ["clobber"]});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to collect results from a Cursor and promiseify it.
|
||||
* @param {ObjectStore|Index} store The store to perform openCursor on.
|
||||
@@ -77,7 +83,7 @@ function txnAsPromise(txn) {
|
||||
resolve(event);
|
||||
};
|
||||
txn.onerror = function(event) {
|
||||
reject(event);
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -88,7 +94,7 @@ function reqAsEventPromise(req) {
|
||||
resolve(event);
|
||||
};
|
||||
req.onerror = function(event) {
|
||||
reject(event);
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -123,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
|
||||
this.db = null;
|
||||
this._disconnected = true;
|
||||
this._syncAccumulator = new SyncAccumulator();
|
||||
this._isNewlyCreated = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -153,11 +160,15 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
||||
);
|
||||
if (oldVersion < 1) { // The database did not previously exist.
|
||||
this._isNewlyCreated = true;
|
||||
createDatabase(db);
|
||||
}
|
||||
if (oldVersion < 2) {
|
||||
upgradeSchemaV2(db);
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
upgradeSchemaV3(db);
|
||||
}
|
||||
// Expand as needed.
|
||||
};
|
||||
|
||||
@@ -185,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
return this._init();
|
||||
});
|
||||
},
|
||||
/** @return {bool} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
return Promise.resolve(this._isNewlyCreated);
|
||||
},
|
||||
|
||||
/**
|
||||
* Having connected, load initial data from the database and prepare for use
|
||||
@@ -265,42 +280,28 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
setOutOfBandMembers: function(roomId, membershipEvents) {
|
||||
setOutOfBandMembers: async function(roomId, membershipEvents) {
|
||||
console.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||
` members for ${roomId}`);
|
||||
function ignoreResult() {}
|
||||
// run everything in a promise so anything that throws will reject
|
||||
return new Promise((resolve) =>{
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||
const store = tx.objectStore("oob_membership_events");
|
||||
const eventPuts = membershipEvents.map((e) => {
|
||||
const putPromise = reqAsEventPromise(store.put(e));
|
||||
// ignoring the result makes sure we discard the IDB success event
|
||||
// ASAP, and not create a potentially big array containing them
|
||||
// unneccesarily later on by calling Promise.all.
|
||||
return putPromise.then(ignoreResult);
|
||||
});
|
||||
// aside from all the events, we also write a marker object to the store
|
||||
// to mark the fact that OOB members have been written for this room.
|
||||
// It's possible that 0 members need to be written as all where previously know
|
||||
// but we still need to know whether to return null or [] from getOutOfBandMembers
|
||||
// where null means out of band members haven't been stored yet for this room
|
||||
const markerObject = {
|
||||
room_id: roomId,
|
||||
oob_written: true,
|
||||
state_key: 0,
|
||||
};
|
||||
const markerPut = reqAsEventPromise(store.put(markerObject));
|
||||
const allPuts = eventPuts.concat(markerPut);
|
||||
// ignore the empty array Promise.all creates
|
||||
// as this method should just resolve
|
||||
// to undefined on success
|
||||
resolve(Promise.all(allPuts).then(ignoreResult));
|
||||
}).then(() => {
|
||||
console.log(`LL: backend done storing for ${roomId}!`);
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||
const store = tx.objectStore("oob_membership_events");
|
||||
membershipEvents.forEach((e) => {
|
||||
store.put(e);
|
||||
});
|
||||
// aside from all the events, we also write a marker object to the store
|
||||
// to mark the fact that OOB members have been written for this room.
|
||||
// It's possible that 0 members need to be written as all where previously know
|
||||
// but we still need to know whether to return null or [] from getOutOfBandMembers
|
||||
// where null means out of band members haven't been stored yet for this room
|
||||
const markerObject = {
|
||||
room_id: roomId,
|
||||
oob_written: true,
|
||||
state_key: 0,
|
||||
};
|
||||
store.put(markerObject);
|
||||
await txnAsPromise(tx);
|
||||
console.log(`LL: backend done storing for ${roomId}!`);
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: async function(roomId) {
|
||||
@@ -543,6 +544,28 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return Promise.resolve().then(() => {
|
||||
const txn = this.db.transaction(["client_options"], "readonly");
|
||||
const store = txn.objectStore("client_options");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
if (cursor.value && cursor.value && cursor.value.options) {
|
||||
return cursor.value.options;
|
||||
}
|
||||
}).then((results) => results[0]);
|
||||
});
|
||||
},
|
||||
|
||||
storeClientOptions: async function(options) {
|
||||
const txn = this.db.transaction(["client_options"], "readwrite");
|
||||
const store = txn.objectStore("client_options");
|
||||
store.put({
|
||||
clobber: "-", // constant key so will always clobber
|
||||
options: options,
|
||||
}); // put == UPSERT
|
||||
await txnAsPromise(txn);
|
||||
},
|
||||
};
|
||||
|
||||
export default LocalIndexedDBStoreBackend;
|
||||
|
||||
@@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
clearDatabase: function() {
|
||||
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
|
||||
},
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
return this._doCmd('isNewlyCreated');
|
||||
},
|
||||
/**
|
||||
* @return {Promise} Resolves with a sync response to restore the
|
||||
* client state to where it was at the last save, or null if there
|
||||
@@ -114,6 +117,14 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
return this._doCmd('clearOutOfBandMembers', [roomId]);
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return this._doCmd('getClientOptions');
|
||||
},
|
||||
|
||||
storeClientOptions: function(options) {
|
||||
return this._doCmd('storeClientOptions', [options]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all user presence events from the database. This is not cached.
|
||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||
@@ -173,7 +184,9 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
if (msg.command == 'cmd_success') {
|
||||
def.resolve(msg.result);
|
||||
} else {
|
||||
def.reject(msg.error);
|
||||
const error = new Error(msg.error.message);
|
||||
error.name = msg.error.name;
|
||||
def.reject(error);
|
||||
}
|
||||
} else {
|
||||
console.warn("Unrecognised message from worker: " + msg);
|
||||
|
||||
@@ -67,6 +67,9 @@ class IndexedDBStoreWorker {
|
||||
case 'connect':
|
||||
prom = this.backend.connect();
|
||||
break;
|
||||
case 'isNewlyCreated':
|
||||
prom = this.backend.isNewlyCreated();
|
||||
break;
|
||||
case 'clearDatabase':
|
||||
prom = this.backend.clearDatabase().then((result) => {
|
||||
// This returns special classes which can't be cloned
|
||||
@@ -101,10 +104,16 @@ class IndexedDBStoreWorker {
|
||||
case 'setOutOfBandMembers':
|
||||
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
|
||||
break;
|
||||
case 'getClientOptions':
|
||||
prom = this.backend.getClientOptions();
|
||||
break;
|
||||
case 'storeClientOptions':
|
||||
prom = this.backend.storeClientOptions(msg.args[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
if (prom === undefined) {
|
||||
postMessage({
|
||||
this.postMessage({
|
||||
command: 'cmd_fail',
|
||||
seq: msg.seq,
|
||||
// Can't be an Error because they're not structured cloneable
|
||||
@@ -126,7 +135,10 @@ class IndexedDBStoreWorker {
|
||||
command: 'cmd_fail',
|
||||
seq: msg.seq,
|
||||
// Just send a string because Error objects aren't cloneable
|
||||
error: "Error running command",
|
||||
error: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() {
|
||||
return this.backend.getSavedSync();
|
||||
};
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
IndexedDBStore.prototype.isNewlyCreated = function() {
|
||||
return this.backend.isNewlyCreated();
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||
* for this sync, otherwise null.
|
||||
@@ -246,4 +251,12 @@ IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
|
||||
return this.backend.clearOutOfBandMembers(roomId);
|
||||
};
|
||||
|
||||
IndexedDBStore.prototype.getClientOptions = function() {
|
||||
return this.backend.getClientOptions();
|
||||
};
|
||||
|
||||
IndexedDBStore.prototype.storeClientOptions = function(options) {
|
||||
return this.backend.storeClientOptions(options);
|
||||
};
|
||||
|
||||
module.exports.IndexedDBStore = IndexedDBStore;
|
||||
|
||||
@@ -55,6 +55,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
this._oobMembers = {
|
||||
// roomId: [member events]
|
||||
};
|
||||
this._clientOptions = {};
|
||||
};
|
||||
|
||||
module.exports.MatrixInMemoryStore.prototype = {
|
||||
@@ -67,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
return this.syncToken;
|
||||
},
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the token to stream from.
|
||||
@@ -402,4 +407,13 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
this._oobMembers[roomId] = membershipEvents;
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return Promise.resolve(this._clientOptions);
|
||||
},
|
||||
|
||||
storeClientOptions: function(options) {
|
||||
this._clientOptions = Object.assign({}, options);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -32,6 +32,11 @@ function StubStore() {
|
||||
|
||||
StubStore.prototype = {
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the sync token.
|
||||
* @return {string}
|
||||
@@ -276,6 +281,14 @@ StubStore.prototype = {
|
||||
clearOutOfBandMembers: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
storeClientOptions: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
/** Stub Store class. */
|
||||
|
||||
@@ -34,7 +34,6 @@ import utils from "./utils";
|
||||
* rather than asking the server to do an initial sync on startup.
|
||||
*/
|
||||
class SyncAccumulator {
|
||||
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
|
||||
|
||||
+87
-8
@@ -33,6 +33,8 @@ const utils = require("./utils");
|
||||
const Filter = require("./filter");
|
||||
const EventTimeline = require("./models/event-timeline");
|
||||
|
||||
import {InvalidStoreError} from './errors';
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
// /sync requests allow you to set a timeout= but the request may continue
|
||||
@@ -100,6 +102,7 @@ function SyncApi(client, opts) {
|
||||
this._connectionReturnedDefer = null;
|
||||
this._notifEvents = []; // accumulator of sync events in the current sync response
|
||||
this._failedSyncCount = 0; // Number of consecutive failed /sync requests
|
||||
this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
|
||||
|
||||
if (client.getNotifTimelineSet()) {
|
||||
client.reEmitter.reEmit(client.getNotifTimelineSet(),
|
||||
@@ -123,6 +126,7 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
"Room.accountData",
|
||||
"Room.myMembership",
|
||||
]);
|
||||
this._registerStateListeners(room);
|
||||
return room;
|
||||
@@ -421,6 +425,26 @@ SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise,
|
||||
await keepaliveProm;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the lazy loading option different than in previous session?
|
||||
* @param {bool} lazyLoadMembers current options for lazy loading
|
||||
* @return {bool} whether or not the option has changed compared to the previous session */
|
||||
SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
|
||||
lazyLoadMembers = !!lazyLoadMembers;
|
||||
// assume it was turned off before
|
||||
// if we don't know any better
|
||||
let lazyLoadMembersBefore = false;
|
||||
const isStoreNewlyCreated = await this.client.store.isNewlyCreated();
|
||||
if (!isStoreNewlyCreated) {
|
||||
const prevClientOptions = await this.client.store.getClientOptions();
|
||||
if (prevClientOptions) {
|
||||
lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
|
||||
}
|
||||
return lazyLoadMembersBefore !== lazyLoadMembers;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
@@ -443,6 +467,8 @@ SyncApi.prototype.sync = function() {
|
||||
// 1) We need to get push rules so we can check if events should bing as we get
|
||||
// them from /sync.
|
||||
// 2) We need to get/create a filter which we can use for /sync.
|
||||
// 3) We need to check the lazy loading option matches what was used in the
|
||||
// stored sync. If it doesn't, we can't use the stored sync.
|
||||
|
||||
async function getPushRules() {
|
||||
try {
|
||||
@@ -457,9 +483,47 @@ SyncApi.prototype.sync = function() {
|
||||
getPushRules();
|
||||
return;
|
||||
}
|
||||
getFilter(); // Now get the filter and start syncing
|
||||
checkLazyLoadStatus(); // advance to the next stage
|
||||
}
|
||||
|
||||
const checkLazyLoadStatus = async () => {
|
||||
if (this.opts.lazyLoadMembers && client.isGuest()) {
|
||||
this.opts.lazyLoadMembers = false;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers) {
|
||||
const supported = await client.doesServerSupportLazyLoading();
|
||||
if (supported) {
|
||||
this.opts.filter = await client.createFilter(
|
||||
Filter.LAZY_LOADING_SYNC_FILTER,
|
||||
);
|
||||
} else {
|
||||
console.log("LL: lazy loading requested but not supported " +
|
||||
"by server, so disabling");
|
||||
this.opts.lazyLoadMembers = false;
|
||||
}
|
||||
}
|
||||
// need to vape the store when enabling LL and wasn't enabled before
|
||||
const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers);
|
||||
if (shouldClear) {
|
||||
this._storeIsInvalid = true;
|
||||
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
|
||||
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
|
||||
this._updateSyncState("ERROR", { error });
|
||||
// bail out of the sync loop now: the app needs to respond to this error.
|
||||
// we leave the state as 'ERROR' which isn't great since this normally means
|
||||
// we're retrying. The client must be stopped before clearing the stores anyway
|
||||
// so the app should stop the client, clear the store and start it again.
|
||||
console.warn("InvalidStoreError: store is not usable: stopping sync.");
|
||||
return;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers && this.opts.crypto) {
|
||||
this.opts.crypto.enableLazyLoading();
|
||||
}
|
||||
await this.client._storeClientOptions();
|
||||
|
||||
getFilter(); // Now get the filter and start syncing
|
||||
};
|
||||
|
||||
async function getFilter() {
|
||||
let filter;
|
||||
if (self.opts.filter) {
|
||||
@@ -587,7 +651,12 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
|
||||
console.error("Error processing cached sync", e.stack || e);
|
||||
}
|
||||
|
||||
this._updateSyncState("PREPARED", syncEventData);
|
||||
// Don't emit a prepared if we've bailed because the store is invalid:
|
||||
// in this case the client will not be usable until stopped & restarted
|
||||
// so this would be useless and misleading.
|
||||
if (!this._storeIsInvalid) {
|
||||
this._updateSyncState("PREPARED", syncEventData);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -976,9 +1045,10 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
// Handle invites
|
||||
inviteRooms.forEach(function(inviteObj) {
|
||||
const room = inviteObj.room;
|
||||
room.setSyncedMembership("invite");
|
||||
const stateEvents =
|
||||
self._mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
|
||||
room.updateMyMembership("invite");
|
||||
self._processRoomEvents(room, stateEvents);
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
@@ -993,7 +1063,6 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
// Handle joins
|
||||
await Promise.mapSeries(joinRooms, async function(joinObj) {
|
||||
const room = joinObj.room;
|
||||
room.setSyncedMembership("join");
|
||||
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
|
||||
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
|
||||
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
|
||||
@@ -1009,6 +1078,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
);
|
||||
}
|
||||
|
||||
room.updateMyMembership("join");
|
||||
|
||||
joinObj.timeline = joinObj.timeline || {};
|
||||
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
@@ -1101,6 +1172,16 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) {
|
||||
await self.opts.crypto.onCryptoEvent(e);
|
||||
}
|
||||
if (e.isState() && e.getType() === "im.vector.user_status") {
|
||||
let user = client.store.getUser(e.getStateKey());
|
||||
if (user) {
|
||||
user._unstable_updateStatusMessage(e);
|
||||
} else {
|
||||
user = createNewUser(client, e.getStateKey());
|
||||
user._unstable_updateStatusMessage(e);
|
||||
client.store.storeUser(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.mapSeries(stateEvents, processRoomEvent);
|
||||
@@ -1116,8 +1197,6 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
leaveRooms.forEach(function(leaveObj) {
|
||||
const room = leaveObj.room;
|
||||
room.setSyncedMembership("leave");
|
||||
|
||||
const stateEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.state, room);
|
||||
const timelineEvents =
|
||||
@@ -1125,6 +1204,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
const accountDataEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
room.updateMyMembership("leave");
|
||||
|
||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
@@ -1145,8 +1226,6 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
|
||||
room.onLeft();
|
||||
});
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
|
||||
+29
-2
@@ -19,6 +19,8 @@ limitations under the License.
|
||||
* @module utils
|
||||
*/
|
||||
|
||||
const unhomoglyph = require('unhomoglyph');
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
* @param {Object} params A dict of key/values to encode e.g.
|
||||
@@ -328,7 +330,7 @@ const deepCompare = module.exports.deepCompare = function(x, y) {
|
||||
}
|
||||
|
||||
// finally, compare each of x's keys with y
|
||||
for (p in y) {
|
||||
for (p in y) { // eslint-disable-line guard-for-in
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
@@ -665,10 +667,35 @@ module.exports.isNumber = function(value) {
|
||||
|
||||
/**
|
||||
* Removes zero width chars, diacritics and whitespace from the string
|
||||
* Also applies an unhomoglyph on the string, to prevent similar looking chars
|
||||
* @param {string} str the string to remove hidden characters from
|
||||
* @return {string} a string with the hidden characters removed
|
||||
*/
|
||||
module.exports.removeHiddenChars = function(str) {
|
||||
return str.normalize('NFD').replace(removeHiddenCharsRegex, '');
|
||||
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
|
||||
};
|
||||
const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g;
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
module.exports.escapeRegExp = escapeRegExp;
|
||||
|
||||
module.exports.globToRegexp = function(glob, extended) {
|
||||
extended = typeof(extended) === 'boolean' ? extended : true;
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
// Because micromatch is about 130KB with dependencies,
|
||||
// and minimatch is not much better.
|
||||
let pat = escapeRegExp(glob);
|
||||
pat = pat.replace(/\\\*/g, '.*');
|
||||
pat = pat.replace(/\?/g, '.');
|
||||
if (extended) {
|
||||
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
|
||||
const first = p1 && '^' || '';
|
||||
const second = p2.replace(/\\-/, '-');
|
||||
return '[' + first + second + ']';
|
||||
});
|
||||
}
|
||||
return pat;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user