Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00f5ddc93c | |||
| f79f2105fd | |||
| 828c51467f | |||
| 963e271bce | |||
| 0ab41215f0 | |||
| d153c4da07 | |||
| 91aa783c3d | |||
| a54845bf76 | |||
| 8a56a5f1ed | |||
| e2c17528c2 | |||
| e00f565f37 | |||
| 085e797c30 | |||
| d753db590b | |||
| feac096dc2 | |||
| 67985d449a | |||
| a6de59c198 | |||
| 01eeb98e35 | |||
| 49a7defbf0 | |||
| c1ba5de686 | |||
| 81428f23d1 | |||
| 8af86bb746 | |||
| 8513f5c413 | |||
| eadec35093 | |||
| d6dbd621b8 | |||
| 7168f76614 | |||
| 1cda95f23c | |||
| bb1cd2bbce | |||
| 3f90ac5712 | |||
| 8d249a843c | |||
| 858b41d835 | |||
| 61aea05af0 | |||
| e7c764d5f5 | |||
| 09a9afe4e7 | |||
| 5a26503da7 | |||
| 5faf5ea1f8 | |||
| 0754c29c22 | |||
| d5c6dcf111 | |||
| 6a57ddd33c | |||
| bd711cdc1f | |||
| e669e493c9 | |||
| 48f290196c | |||
| f8985dbb39 | |||
| ef594d52e4 | |||
| 23bbb2f8c6 | |||
| 73c7733ebc | |||
| 87f7f9443e | |||
| af6bbbc59b | |||
| 5b35a364a9 | |||
| d56ebadbc4 | |||
| 04accdeddc | |||
| 70575f9e33 | |||
| 8e16586d84 | |||
| 6920dfb800 | |||
| 02d93770aa | |||
| cd124231c5 | |||
| cd75848882 | |||
| 1bae15ede9 | |||
| 8c2001adbf | |||
| 79ca235e7c | |||
| 4570fcaa8a | |||
| 90670cf1be | |||
| cc86f427d2 | |||
| 2144791d52 | |||
| 33aabf44e7 | |||
| fc1ea27380 | |||
| 81946294d8 | |||
| 77270fa78c | |||
| 9e29289dcc | |||
| 6198943976 | |||
| 8beb836ccd | |||
| b7c0e39c1a | |||
| 777acae2e5 | |||
| e77389c1ce | |||
| eb24e2e1f1 | |||
| 114244f8bb | |||
| 54769d9136 | |||
| 2f2deb5333 | |||
| 37f106d4af | |||
| 36ee7cdbfc | |||
| 2b564498ee | |||
| 0ddba16fa1 | |||
| 550086eb67 | |||
| 055ce673cd | |||
| 5308595658 | |||
| e726e29f39 | |||
| 33b12fa6b5 | |||
| 829cd05cba | |||
| 4834e12a3a | |||
| bcd4ad130c | |||
| 998d9e010e | |||
| 5480e8e1d5 | |||
| 98fdcabc00 | |||
| 236397816d | |||
| 755c55de3e | |||
| 526da71992 | |||
| 86ef262799 | |||
| 282904d4be | |||
| a1be24307a | |||
| 4b5623691b | |||
| 7bdf1e9b92 | |||
| af1db8a606 | |||
| a99bb3c4c9 | |||
| fd155c15bd | |||
| aaa43631aa | |||
| d2557bc943 | |||
| 33a3506981 | |||
| 03a54353be | |||
| c6328923e6 | |||
| 1ecb820bb0 | |||
| 0be2319288 | |||
| 073a025b83 | |||
| e83836d487 | |||
| 065c61e05c | |||
| 139a6bd903 | |||
| 3fa0ee59d4 | |||
| bd3d26422d | |||
| 370ef9fc69 | |||
| a087fb37a3 | |||
| 68c8fe0fa9 | |||
| 4309749979 | |||
| 1a677804a4 | |||
| a427e2a75c | |||
| 3c735b0ac1 | |||
| 4f446c3909 | |||
| 999ed1b5b3 | |||
| 8fa19f4a0f | |||
| 71a01ec234 | |||
| 32f033a9da | |||
| dade385147 | |||
| 6cf2e54f9a | |||
| fb673b0304 | |||
| 1a425af3f2 | |||
| 9bafed2c26 | |||
| bb2d0b0f62 | |||
| 5e4f10a80c | |||
| 9e12fc4d7d | |||
| 1caf2b7f83 | |||
| 6f17e3e659 | |||
| 17e2cd755d | |||
| a6970d4de8 | |||
| bc99c1f3ce | |||
| 5e3ff7fc27 | |||
| ffe3f966fe | |||
| c60c19a28e | |||
| 4ea785b604 | |||
| 2d4e9d0d3f | |||
| 971d572fbf | |||
| 244e1b84f7 | |||
| e5cdc99a34 | |||
| 9a5768219f | |||
| cee8f57318 | |||
| 1a40e0a83a | |||
| d0072d930f | |||
| 385062c4d7 | |||
| 9245638b25 | |||
| 1865542192 | |||
| 2563abda11 | |||
| 59b80d8fbd | |||
| 68bb8182e4 | |||
| c979ff6696 | |||
| 25681e888c | |||
| 5cfd082b00 | |||
| 0cbced43bd | |||
| b3e8d7e07e | |||
| f4a7395e3a | |||
| 14b42abfa4 | |||
| e8022e985e | |||
| 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 | |||
| eece5d318e | |||
| 84ab0fde51 | |||
| ec18df2c2a | |||
| 55ca03f100 | |||
| 83708725b2 | |||
| b34716f7e9 | |||
| 9cdcbf6bf8 | |||
| 81de2b3afc | |||
| 9a53fa3876 |
@@ -0,0 +1,24 @@
|
||||
steps:
|
||||
- label: ":eslint: Lint"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn lint"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: ":karma: Tests"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn test"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: "📃 Docs"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn gendoc"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
+8
-2
@@ -1,7 +1,6 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint",
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
@@ -67,5 +66,12 @@ 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",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,6 +2,9 @@
|
||||
/.jsdoc
|
||||
|
||||
node_modules
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
@@ -12,6 +15,6 @@ reports
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10.11.0"
|
||||
script:
|
||||
- ./travis.sh
|
||||
+167
-1
@@ -1,3 +1,169 @@
|
||||
Changes in [1.0.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.4) (2019-04-08)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3...v1.0.4)
|
||||
|
||||
* Hotfix: more logging and potential fixes for timeline corruption issue, see ticket https://github.com/vector-im/riot-web/issues/8593.
|
||||
|
||||
Changes in [1.0.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3) (2019-04-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3-rc.1...v1.0.3)
|
||||
|
||||
* Add existence check to local storage based crypto store
|
||||
[\#874](https://github.com/matrix-org/matrix-js-sdk/pull/874)
|
||||
|
||||
Changes in [1.0.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3-rc.1) (2019-03-27)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2...v1.0.3-rc.1)
|
||||
|
||||
* Add IndexedDB existence checks
|
||||
[\#871](https://github.com/matrix-org/matrix-js-sdk/pull/871)
|
||||
* Emit sync errors for capturing by clients
|
||||
[\#869](https://github.com/matrix-org/matrix-js-sdk/pull/869)
|
||||
* Add functions for getting room upgrade history and leaving those rooms
|
||||
[\#868](https://github.com/matrix-org/matrix-js-sdk/pull/868)
|
||||
* Clarify the meaning of 'real name' for contribution
|
||||
[\#867](https://github.com/matrix-org/matrix-js-sdk/pull/867)
|
||||
* Remove `sessionStore` to `cryptoStore` migration path
|
||||
[\#865](https://github.com/matrix-org/matrix-js-sdk/pull/865)
|
||||
* Add debugging for spurious room version warnings
|
||||
[\#866](https://github.com/matrix-org/matrix-js-sdk/pull/866)
|
||||
* Add investigation notes for browser storage
|
||||
[\#864](https://github.com/matrix-org/matrix-js-sdk/pull/864)
|
||||
* make sure resolve object is defined before calling it
|
||||
[\#862](https://github.com/matrix-org/matrix-js-sdk/pull/862)
|
||||
* Rename `MatrixInMemoryStore` to `MemoryStore`
|
||||
[\#861](https://github.com/matrix-org/matrix-js-sdk/pull/861)
|
||||
* Use Buildkite for CI
|
||||
[\#859](https://github.com/matrix-org/matrix-js-sdk/pull/859)
|
||||
* only create one session at a time per device
|
||||
[\#857](https://github.com/matrix-org/matrix-js-sdk/pull/857)
|
||||
|
||||
Changes in [1.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2) (2019-03-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2-rc.1...v1.0.2)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [1.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2-rc.1) (2019-03-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1...v1.0.2-rc.1)
|
||||
|
||||
* Use modern Yarn version on Travis CI
|
||||
[\#858](https://github.com/matrix-org/matrix-js-sdk/pull/858)
|
||||
* Switch to `yarn` for dependency management
|
||||
[\#856](https://github.com/matrix-org/matrix-js-sdk/pull/856)
|
||||
* More key request fixes
|
||||
[\#855](https://github.com/matrix-org/matrix-js-sdk/pull/855)
|
||||
* Calculate encrypted notification counts
|
||||
[\#851](https://github.com/matrix-org/matrix-js-sdk/pull/851)
|
||||
* Update dependencies
|
||||
[\#854](https://github.com/matrix-org/matrix-js-sdk/pull/854)
|
||||
* make sure key requests get sent
|
||||
[\#850](https://github.com/matrix-org/matrix-js-sdk/pull/850)
|
||||
* Use 'ideal' rather than 'exact' for deviceid
|
||||
[\#852](https://github.com/matrix-org/matrix-js-sdk/pull/852)
|
||||
* handle partially-shared sessions better
|
||||
[\#848](https://github.com/matrix-org/matrix-js-sdk/pull/848)
|
||||
|
||||
Changes in [1.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1) (2019-03-06)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.2...v1.0.1)
|
||||
|
||||
* No changes since rc.2
|
||||
|
||||
Changes in [1.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.2) (2019-03-05)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.1...v1.0.1-rc.2)
|
||||
|
||||
* dont swallow txn errors in crypto store
|
||||
[\#853](https://github.com/matrix-org/matrix-js-sdk/pull/853)
|
||||
* Don't swallow txn errors in crypto store
|
||||
[\#849](https://github.com/matrix-org/matrix-js-sdk/pull/849)
|
||||
|
||||
Changes in [1.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.1) (2019-02-28)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0...v1.0.1-rc.1)
|
||||
|
||||
* Fix "e is undefined" masking the original error in MegolmDecryption
|
||||
[\#847](https://github.com/matrix-org/matrix-js-sdk/pull/847)
|
||||
|
||||
Changes in [1.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0) (2019-02-14)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.2...v1.0.0)
|
||||
|
||||
* Try again to commit package-lock.json
|
||||
[\#841](https://github.com/matrix-org/matrix-js-sdk/pull/841)
|
||||
|
||||
Changes in [1.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.2) (2019-02-14)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.1...v1.0.0-rc.2)
|
||||
|
||||
* Release script: commit package-lock.json
|
||||
[\#839](https://github.com/matrix-org/matrix-js-sdk/pull/839)
|
||||
* Add method to force re-check of key backup
|
||||
[\#840](https://github.com/matrix-org/matrix-js-sdk/pull/840)
|
||||
* Fix: dont check for unverified devices in left members
|
||||
[\#838](https://github.com/matrix-org/matrix-js-sdk/pull/838)
|
||||
|
||||
Changes in [1.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.1) (2019-02-08)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3...v1.0.0-rc.1)
|
||||
|
||||
* change hex SAS verification to decimal and emoji
|
||||
[\#837](https://github.com/matrix-org/matrix-js-sdk/pull/837)
|
||||
* Trust on decrypt
|
||||
[\#836](https://github.com/matrix-org/matrix-js-sdk/pull/836)
|
||||
* Always track our own devices
|
||||
[\#835](https://github.com/matrix-org/matrix-js-sdk/pull/835)
|
||||
* Make linting rules more consistent
|
||||
[\#834](https://github.com/matrix-org/matrix-js-sdk/pull/834)
|
||||
* add method to room to check for unverified devices
|
||||
[\#833](https://github.com/matrix-org/matrix-js-sdk/pull/833)
|
||||
* Merge redesign into develop
|
||||
[\#831](https://github.com/matrix-org/matrix-js-sdk/pull/831)
|
||||
* Supporting infrastructure for educated decisions on when to upgrade rooms
|
||||
[\#830](https://github.com/matrix-org/matrix-js-sdk/pull/830)
|
||||
* Include signature info for unknown devices
|
||||
[\#826](https://github.com/matrix-org/matrix-js-sdk/pull/826)
|
||||
* Flag v2 rooms as "safe"
|
||||
[\#828](https://github.com/matrix-org/matrix-js-sdk/pull/828)
|
||||
* Update ESLint
|
||||
[\#821](https://github.com/matrix-org/matrix-js-sdk/pull/821)
|
||||
|
||||
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)
|
||||
@@ -51,7 +217,7 @@ Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
|
||||
|
||||
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.
|
||||
|
||||
+16
-7
@@ -24,7 +24,7 @@ works. Develop is the unstable branch where all the development actually
|
||||
happens: the workflow is that contributors should fork the develop branch to
|
||||
make a 'feature' branch for a particular contribution, and then make a pull
|
||||
request to merge this back into the matrix.org 'official' develop branch. We
|
||||
use github's pull request workflow to review the contribution, and either ask
|
||||
use GitHub's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
@@ -60,8 +60,8 @@ Sign off
|
||||
~~~~~~~~
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've adopted the
|
||||
same lightweight approach that the Linux Kernel
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
@@ -109,7 +109,16 @@ include the line in your commit or pull request comment::
|
||||
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
|
||||
...using your real name; unfortunately pseudonyms and anonymous contributions
|
||||
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 :)
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the ``-s`` flag to
|
||||
``git commit``, which uses the name and email set in your ``user.name`` and
|
||||
``user.email`` git configs.
|
||||
|
||||
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
|
||||
|
||||
@@ -21,7 +21,11 @@ Please check [the working browser example](examples/browser) for more informatio
|
||||
In Node.js
|
||||
----------
|
||||
|
||||
``npm install matrix-js-sdk``
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
@@ -76,7 +80,7 @@ client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
});
|
||||
```
|
||||
|
||||
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:
|
||||
By default, the `matrix-js-sdk` client uses the `MemoryStore` 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) => {
|
||||
@@ -283,7 +287,7 @@ This SDK uses JSDoc3 style comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ npm run gendoc
|
||||
$ yarn gendoc
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
```
|
||||
@@ -319,15 +323,15 @@ 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-3.0.0.tgz``
|
||||
* ``yarn add 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-3.0.0.tgz
|
||||
--save-optional`` (if your application also works without e2e crypto enabled)
|
||||
or ``--save`` (if it doesn't) to do so.
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``. If your
|
||||
application also works without e2e crypto enabled, add ``--optional`` to mark it
|
||||
as an optional dependency.
|
||||
|
||||
|
||||
Contributing
|
||||
@@ -337,7 +341,7 @@ want to use this SDK, skip this section.*
|
||||
|
||||
First, you need to pull in the right build tools:
|
||||
```
|
||||
$ npm install
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Building
|
||||
@@ -345,20 +349,20 @@ Building
|
||||
|
||||
To build a browser version from scratch when developing::
|
||||
```
|
||||
$ npm run build
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To constantly do builds when files are modified (using ``watchify``)::
|
||||
```
|
||||
$ npm run watch
|
||||
$ yarn watch
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
```
|
||||
$ npm test
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
```
|
||||
$ npm run lint
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Browser Storage Notes
|
||||
|
||||
## Overview
|
||||
|
||||
Browsers examined: Firefox 67, Chrome 75
|
||||
|
||||
The examination below applies to the default, non-persistent storage policy.
|
||||
|
||||
## Quota Measurement
|
||||
|
||||
Browsers appear to enforce and measure the quota in terms of space on disk, not
|
||||
data stored, so you may be able to store more data than the simple sum of all
|
||||
input data depending on how compressible your data is.
|
||||
|
||||
## Quota Limit
|
||||
|
||||
Specs and documentation suggest we should consistently receive
|
||||
`QuotaExceededError` when we're near space limits, but the reality is a bit
|
||||
blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
* Chrome
|
||||
* Log database may fail to start with AbortError
|
||||
* IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
* When near the quota, QuotaExceededError is used more consistently
|
||||
* Firefox
|
||||
* The first error will be QuotaExceededError
|
||||
* Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
* Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
* Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
While the Storage Standard says all storage for an origin group should be
|
||||
limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
* Chrome, Firefox
|
||||
* IndexedDB for origin deleted
|
||||
* Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
* Chrome
|
||||
* Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
* Firefox
|
||||
* Firefox 67 shows a prompt to grant
|
||||
* Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
* Chrome, Firefox
|
||||
* Can run this at any time to request an estimate of space remaining
|
||||
* Firefox
|
||||
* Returns `0` for `usage` if a site is persisted
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
npm run lint
|
||||
yarn lint
|
||||
|
||||
+8
-6
@@ -6,7 +6,7 @@ export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm use 10 || exit $?
|
||||
npm install || exit $?
|
||||
yarn install || exit $?
|
||||
|
||||
RC=0
|
||||
|
||||
@@ -18,17 +18,19 @@ function fail {
|
||||
# don't use last time's test reports
|
||||
rm -rf reports coverage || exit $?
|
||||
|
||||
npm test || fail "npm test finished with return code $?"
|
||||
yarn test || fail "yarn test finished with return code $?"
|
||||
|
||||
npm run -s lint -- -f checkstyle > eslint.xml ||
|
||||
yarn -s lint -f checkstyle > eslint.xml ||
|
||||
fail "eslint finished with return code $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
# `yarn pack` doesn't seem to run scripts, however that seems okay here as we
|
||||
# just built as part of `install` above.
|
||||
yarn pack ||
|
||||
fail "yarn pack finished with return code $?"
|
||||
|
||||
npm run gendoc || fail "JSDoc failed with code $?"
|
||||
yarn gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
|
||||
Generated
-7124
File diff suppressed because it is too large
Load Diff
+15
-12
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.14.2",
|
||||
"version": "1.0.4",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test:build": "babel -s -d specbuild spec",
|
||||
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
|
||||
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
|
||||
"test": "npm run test:build && npm run test:run",
|
||||
"check": "npm run test:build && _mocha --recursive specbuild --colors",
|
||||
"test": "yarn test:build && yarn test:run",
|
||||
"check": "yarn 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": "npm run start:init && npm run start:watch",
|
||||
"start": "yarn start:init && yarn 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",
|
||||
"dist": "yarn 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",
|
||||
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
|
||||
"lint": "eslint --max-warnings 101 src spec",
|
||||
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -54,37 +54,40 @@
|
||||
"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",
|
||||
"loglevel": "1.6.1",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0"
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^8.1.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.18.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.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-jenkins-reporter": "^0.4.0",
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-3.1.0-pre1.tgz",
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglify-js": "^2.8.26",
|
||||
"watchify": "^3.11.0"
|
||||
"watchify": "^3.11.1"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
+48
-13
@@ -6,12 +6,26 @@
|
||||
# github-changelog-generator; install via:
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (OSX) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
|
||||
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
|
||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
@@ -45,7 +59,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 +75,9 @@ while getopts hc:xz f; do
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
@@ -74,6 +92,14 @@ if [ -z "$skip_changelog" ]; then
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
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}"
|
||||
@@ -127,14 +153,22 @@ cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_cha
|
||||
set -x
|
||||
|
||||
# Bump package.json and build the dist
|
||||
echo "npm version"
|
||||
# npm version will automatically commit its modification
|
||||
echo "yarn version"
|
||||
# yarn version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
pkglock=''
|
||||
fi
|
||||
git commit package.json $pkglock -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
@@ -150,7 +184,7 @@ fi
|
||||
# assets.
|
||||
# We make a completely separate checkout to be sure
|
||||
# we're using released versions of the dependencies
|
||||
# (rather than whatever we're pulling in from npm link)
|
||||
# (rather than whatever we're pulling in from yarn link)
|
||||
assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
@@ -161,10 +195,10 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
npm install
|
||||
yarn install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" npm run dist
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
popd
|
||||
|
||||
@@ -253,12 +287,13 @@ fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# publish to npmjs
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
npm publish
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
yarn gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
@@ -283,7 +318,7 @@ git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch"
|
||||
|
||||
# push master and docs (if generated) to github
|
||||
# push master and docs (if generated) to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
|
||||
+17
-9
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2019 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.
|
||||
@@ -38,9 +38,10 @@ import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-sto
|
||||
*
|
||||
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
* @param {object} options additional options to pass to the client
|
||||
*/
|
||||
export default function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend,
|
||||
userId, deviceId, accessToken, sessionStoreBackend, options,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
@@ -50,19 +51,22 @@ export default function TestClient(
|
||||
}
|
||||
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
this.client = sdk.createClient({
|
||||
|
||||
options = Object.assign({
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: this.cryptoStore,
|
||||
request: this.httpBackend.requestFn,
|
||||
});
|
||||
}, options);
|
||||
if (!options.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
options.cryptoStore = this.cryptoStore;
|
||||
}
|
||||
this.client = sdk.createClient(options);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
@@ -181,7 +185,11 @@ TestClient.prototype.expectKeyQuery = function(response) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual({});
|
||||
expect(content.device_keys[userId]).toEqual(
|
||||
{},
|
||||
"Expected key query for " + userId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -101,6 +101,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
@@ -143,6 +144,7 @@ describe("DeviceList management:", function() {
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
|
||||
@@ -72,7 +72,11 @@ function expectAliQueryKeys() {
|
||||
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
expect(content.device_keys[bobUserId]).toEqual(
|
||||
{},
|
||||
"Expected Alice to key query for " + bobUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
@@ -96,7 +100,11 @@ function expectBobQueryKeys() {
|
||||
bobTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
expect(content.device_keys[aliUserId]).toEqual(
|
||||
{},
|
||||
"Expected Bob to key query for " + aliUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
@@ -544,6 +552,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -554,6 +563,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -564,6 +574,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -617,6 +628,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -636,6 +648,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -649,6 +662,8 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -659,13 +674,14 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
|
||||
}).then(aliRecvMessage);
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", function() {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
@@ -696,7 +712,6 @@ describe("MatrixClient crypto", function() {
|
||||
}).then(() => {
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[aliUserId]: {},
|
||||
[bobUserId]: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const publicGlobals = require("../../lib/matrix");
|
||||
const Room = publicGlobals.Room;
|
||||
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
|
||||
const MemoryStore = publicGlobals.MemoryStore;
|
||||
const Filter = publicGlobals.Filter;
|
||||
const utils = require("../test-utils");
|
||||
const MockStorageApi = require("../MockStorageApi");
|
||||
@@ -23,7 +23,7 @@ describe("MatrixClient", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MatrixInMemoryStore();
|
||||
store = new MemoryStore();
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("MatrixClient opts", function() {
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
store: new sdk.MatrixInMemoryStore(),
|
||||
store: new sdk.MemoryStore(),
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
|
||||
@@ -499,6 +499,7 @@ describe("megolm", function() {
|
||||
it('Alice sends a megolm message', function() {
|
||||
let p2pSession;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -581,6 +582,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -634,6 +636,7 @@ describe("megolm", function() {
|
||||
let p2pSession;
|
||||
let megolmSessionId;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -843,6 +846,7 @@ describe("megolm", function() {
|
||||
let downloadPromise;
|
||||
let sendPromise;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -886,6 +890,7 @@ describe("megolm", function() {
|
||||
it("Alice exports megolm keys and imports them to a new device", function() {
|
||||
let messageEncrypted;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,18 @@
|
||||
import 'source-map-support/register';
|
||||
|
||||
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';
|
||||
import TestClient from '../TestClient';
|
||||
import {MatrixEvent} from '../../lib/models/event';
|
||||
import Room from '../../lib/models/room';
|
||||
import olmlib from '../../lib/crypto/olmlib';
|
||||
import lolex from 'lolex';
|
||||
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
@@ -70,6 +78,7 @@ describe("Crypto", function() {
|
||||
mockBaseApis = {
|
||||
sendToDevice: expect.createSpy(),
|
||||
getKeyBackupVersion: expect.createSpy(),
|
||||
isGuest: expect.createSpy(),
|
||||
};
|
||||
mockRoomList = {};
|
||||
|
||||
@@ -115,4 +124,243 @@ describe("Crypto", function() {
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Key requests', function() {
|
||||
let aliceClient;
|
||||
let bobClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await aliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it(
|
||||
"does not cancel keyshare requests if some messages are not decrypted",
|
||||
async function() {
|
||||
function awaitEvent(emitter, event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
emitter.once(event, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function keyshareEventForEvent(event, index) {
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await aliceClient._crypto._olmDevice
|
||||
.getInboundGroupSessionKey(
|
||||
roomId, eventContent.sender_key, eventContent.session_id,
|
||||
index,
|
||||
);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.forwarded_room_key",
|
||||
sender: "@alice:example.com",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
ksEvent._senderCurve25519Key = "akey";
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
event._clearEvent = {};
|
||||
event._senderCurve25519Key = null;
|
||||
event._claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient._crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
let eventPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(events[1], 1);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient._cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
// the room key request should still be there, since we haven't
|
||||
// decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
eventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
ksEvent = await keyshareEventForEvent(events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
// the room key request should be gone since we've now decypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toNotExist();
|
||||
},
|
||||
);
|
||||
|
||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||
// make sure that cancelAndResend... creates a new keyshare request
|
||||
// if there wasn't an already-existing one
|
||||
const event = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
const cryptoStore = aliceClient._cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
};
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
});
|
||||
|
||||
it("uses a new txnid for re-requesting keys", async function() {
|
||||
const event = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
/* return a promise and a function. When the function is called,
|
||||
* the promise will be resolved.
|
||||
*/
|
||||
function awaitFunctionCall() {
|
||||
let func;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
func = function(...args) {
|
||||
resolve(args);
|
||||
return new Promise((resolve, reject) => {
|
||||
// give us some time to process the result before
|
||||
// continuing
|
||||
global.setTimeout(resolve, 1);
|
||||
});
|
||||
};
|
||||
});
|
||||
return {func, promise};
|
||||
}
|
||||
|
||||
aliceClient.startClient();
|
||||
|
||||
const clock = lolex.install();
|
||||
|
||||
try {
|
||||
let promise;
|
||||
// make a room key request, and record the transaction ID for the
|
||||
// sendToDevice call
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
clock.runToLast();
|
||||
let args = await promise;
|
||||
const txnId = args[2];
|
||||
clock.runToLast();
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
// of the request
|
||||
await Promise.resolve();
|
||||
|
||||
// cancel and resend the room key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
clock.runToLast();
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
args = await promise;
|
||||
// the second call to sendToDevice will be the key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
clock.runToLast();
|
||||
args = await promise;
|
||||
clock.runToLast();
|
||||
expect(args[2]).toNotBe(txnId);
|
||||
} finally {
|
||||
clock.uninstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 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.
|
||||
@@ -16,8 +16,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
import MockStorageApi from '../../MockStorageApi';
|
||||
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import testUtils from '../../test-utils';
|
||||
import utils from '../../../lib/utils';
|
||||
@@ -57,7 +55,6 @@ const signedDeviceList = {
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
|
||||
@@ -67,8 +64,6 @@ describe('DeviceList', function() {
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
@@ -85,7 +80,7 @@ describe('DeviceList', function() {
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
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';
|
||||
|
||||
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';
|
||||
@@ -44,10 +39,9 @@ describe("MegolmDecryption", function() {
|
||||
mockBaseApis = {};
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
@@ -268,10 +262,9 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
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);
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = expect.createSpy();
|
||||
await olmDevice.init();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018,2019 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.
|
||||
@@ -14,25 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
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 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 olmlib from '../../../../lib/crypto/olmlib';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
return olmDevice;
|
||||
}
|
||||
|
||||
@@ -86,5 +82,61 @@ describe("OlmDecryption", function() {
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates only one session at a time", async function() {
|
||||
// if we call ensureOlmSessionsForDevices multiple times, it should
|
||||
// only try to create one session at a time, even if the server is
|
||||
// slow
|
||||
let count = 0;
|
||||
const baseApis = {
|
||||
claimOneTimeKeys: () => {
|
||||
// simulate a very slow server (.5 seconds to respond)
|
||||
count++;
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
},
|
||||
}, "ABCDEFG"),
|
||||
],
|
||||
};
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
// start two tasks that try to ensure that there's an olm session
|
||||
const promises = Promise.all([
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUser,
|
||||
)),
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUser,
|
||||
)),
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
|
||||
// after .2s, both tasks should have started, but one should be
|
||||
// waiting on the other before trying to create a session, so
|
||||
// claimOneTimeKeys should have only been called once
|
||||
expect(count).toBe(1);
|
||||
|
||||
await promises;
|
||||
|
||||
// after waiting for both tasks to complete, the first task should
|
||||
// have failed, so the second task should have tried to create a
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,11 +13,8 @@ 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.
|
||||
*/
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run megolm backup tests: libolm not available");
|
||||
}
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
@@ -77,6 +74,14 @@ const KEY_BACKUP_DATA = {
|
||||
},
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
@@ -127,19 +132,13 @@ describe("MegolmBackup", function() {
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
mockCrypto.backupInfo = BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
@@ -439,6 +438,7 @@ describe("MegolmBackup", function() {
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
@@ -460,6 +460,7 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null, null, BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2018-2019 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.
|
||||
*/
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("QR code verification", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
it("should emit an event to show a QR code", async function() {
|
||||
const qrCode = new ShowQRCode({
|
||||
getUserId: () => "@alice:example.com",
|
||||
deviceId: "ABCDEFG",
|
||||
getDeviceEd25519Key: function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = expect.createSpy().andCall((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
await qrCode.verify();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanning", function() {
|
||||
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
|
||||
it("should verify when a QR code is sent", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
if (userId === "@alice:example.com") {
|
||||
confirm();
|
||||
} else {
|
||||
qrCode.cancel(new Error("Incorrect user"));
|
||||
}
|
||||
});
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
await qrCode.verify();
|
||||
expect(client.getStoredDevice)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
expect(client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
});
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy(),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2019 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.
|
||||
*/
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
describe("verification request", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.on("crypto.verification.request", (request) => {
|
||||
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
bobVerifier.verify();
|
||||
});
|
||||
const aliceVerifier = await alice.requestVerification("@bob:example.com");
|
||||
expect(aliceVerifier).toBeAn(SAS);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
Copyright 2018-2019 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.
|
||||
*/
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import sdk from '../../../..';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
describe("SAS verification", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}));
|
||||
const spy = expect.createSpy();
|
||||
await sas.verify()
|
||||
.catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Osborne2": "alice+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Osborne2",
|
||||
);
|
||||
};
|
||||
bob.getDeviceEd25519Key = () => {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
bobSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
aliceSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
|
||||
const aliceSpy = expect.createSpy();
|
||||
const bobSpy = expect.createSpy();
|
||||
await Promise.all([
|
||||
aliceVerifier.verify().catch(aliceSpy),
|
||||
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
expect(bob.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2019 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 TestClient from '../../../TestClient';
|
||||
|
||||
import sdk from '../../../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// console.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
setTimeout(
|
||||
() => clientMap[userId][deviceId]
|
||||
.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined,
|
||||
options,
|
||||
)).client;
|
||||
if (!(userInfo.userId in clientMap)) {
|
||||
clientMap[userInfo.userId] = {};
|
||||
}
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = client;
|
||||
client.sendToDevice = sendToDevice;
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
await Promise.all(clients.map((client) => client.initCrypto()));
|
||||
|
||||
return clients;
|
||||
}
|
||||
@@ -285,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -1833,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
|
||||
*/
|
||||
@@ -1848,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
|
||||
*/
|
||||
|
||||
+341
-25
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2019 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.
|
||||
@@ -59,6 +59,7 @@ Promise.config({warnings: false});
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
||||
|
||||
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
|
||||
const keys = [];
|
||||
@@ -105,19 +106,26 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
*
|
||||
* @param {string} opts.userId The user ID for this user.
|
||||
*
|
||||
* @param {Object=} opts.store The data store to use. If not specified,
|
||||
* this client will not store any HTTP responses.
|
||||
* @param {Object=} opts.store
|
||||
* The data store used for sync data from the homeserver. If not specified,
|
||||
* this client will not store any HTTP responses. The `createClient` helper
|
||||
* will create a default store if needed.
|
||||
*
|
||||
* @param {module:store/session/webstorage~WebStorageSessionStore} opts.sessionStore
|
||||
* A store to be used for end-to-end crypto session data. Most data has been
|
||||
* migrated out of here to `cryptoStore` instead. If not specified,
|
||||
* end-to-end crypto will be disabled. The `createClient` helper
|
||||
* _will not_ create this store at the moment.
|
||||
*
|
||||
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
|
||||
* A store to be used for end-to-end crypto session data. If not specified,
|
||||
* end-to-end crypto will be disabled. The `createClient` helper will create
|
||||
* a default store if needed.
|
||||
*
|
||||
* @param {string=} opts.deviceId A unique identifier for this device; used for
|
||||
* tracking things like crypto keys and access tokens. If not specified,
|
||||
* end-to-end crypto will be disabled.
|
||||
*
|
||||
* @param {Object=} opts.sessionStore A store to be used for end-to-end crypto
|
||||
* session data. This should be a {@link
|
||||
* module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore},
|
||||
* or an object implementing the same interface. If not specified,
|
||||
* end-to-end crypto will be disabled.
|
||||
*
|
||||
* @param {Object} opts.scheduler Optional. The scheduler to use. If not
|
||||
* specified, this client will not retry requests on failure. This client
|
||||
* will supply its own processing function to
|
||||
@@ -140,8 +148,10 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* maintain support for back-paginating the live timeline after a '/sync'
|
||||
* result with a gap.
|
||||
*
|
||||
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
|
||||
* crypto store implementation.
|
||||
* @param {Array} [opts.verificationMethods] Optional. The verification method
|
||||
* that the application can handle. Each element should be an item from {@link
|
||||
* module:crypto~verificationMethods verificationMethods}, or a class that
|
||||
* implements the {$link module:crypto/verification/Base verifier interface}.
|
||||
*/
|
||||
function MatrixClient(opts) {
|
||||
// Allow trailing slash in HS url
|
||||
@@ -207,18 +217,47 @@ function MatrixClient(opts) {
|
||||
this._crypto = null;
|
||||
this._cryptoStore = opts.cryptoStore;
|
||||
this._sessionStore = opts.sessionStore;
|
||||
this._verificationMethods = opts.verificationMethods;
|
||||
|
||||
this._forceTURN = opts.forceTURN || false;
|
||||
|
||||
// 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.
|
||||
this._roomList = new RoomList(this._cryptoStore, this._sessionStore);
|
||||
this._roomList = new RoomList(this._cryptoStore);
|
||||
|
||||
// The pushprocessor caches useful things, so keep one and re-use it
|
||||
this._pushProcessor = new PushProcessor(this);
|
||||
|
||||
this._serverSupportsLazyLoading = null;
|
||||
|
||||
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
|
||||
|
||||
// The SDK doesn't really provide a clean way for events to recalculate the push
|
||||
// actions for themselves, so we have to kinda help them out when they are encrypted.
|
||||
// We do this so that push rules are correctly executed on events in their decrypted
|
||||
// state, such as highlights when the user's name is mentioned.
|
||||
this.on("Event.decrypted", (event) => {
|
||||
const oldActions = event.getPushActions();
|
||||
const actions = this._pushProcessor.actionsForEvent(event);
|
||||
event.setPushActions(actions); // Might as well while we're here
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
const oldHighlight = oldActions && oldActions.tweaks
|
||||
? !!oldActions.tweaks.highlight : false;
|
||||
const newHighlight = actions && actions.tweaks
|
||||
? !!actions.tweaks.highlight : false;
|
||||
if (oldHighlight !== newHighlight) {
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
// TODO: Handle mentions received while the client is offline
|
||||
// See also https://github.com/vector-im/riot-web/issues/9069
|
||||
if (room && !room.hasUserReadEvent(this.getUserId(), event.getId())) {
|
||||
const current = room.getUnreadNotificationCount("highlight");
|
||||
const newCount = newHighlight ? current + 1 : current - 1;
|
||||
room.setUnreadNotificationCount("highlight", newCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
||||
@@ -386,6 +425,37 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
|
||||
this._notifTimelineSet = notifTimelineSet;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the capabilities of the homeserver. Always returns an object of
|
||||
* capability keys and their options, which may be empty.
|
||||
* @return {module:client.Promise} Resolves to the capabilities of the homeserver
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getCapabilities = function() {
|
||||
if (this._cachedCapabilities) {
|
||||
const now = new Date().getTime();
|
||||
if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) {
|
||||
console.log("Returning cached capabilities");
|
||||
return Promise.resolve(this._cachedCapabilities.capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
// We swallow errors because we need a default object anyhow
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", "/capabilities",
|
||||
).catch(() => null).then((r) => {
|
||||
if (!r) r = {};
|
||||
const capabilities = r["capabilities"] || {};
|
||||
this._cachedCapabilities = {
|
||||
capabilities: capabilities,
|
||||
lastUpdated: new Date().getTime(),
|
||||
};
|
||||
|
||||
console.log("Caching capabilities: ", capabilities);
|
||||
return capabilities;
|
||||
});
|
||||
};
|
||||
|
||||
// Crypto bits
|
||||
// ===========
|
||||
|
||||
@@ -444,9 +514,12 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
this.store,
|
||||
this._cryptoStore,
|
||||
this._roomList,
|
||||
this._verificationMethods,
|
||||
);
|
||||
|
||||
this.reEmitter.reEmit(crypto, [
|
||||
"crypto.keyBackupFailed",
|
||||
"crypto.keyBackupSessionsRemaining",
|
||||
"crypto.roomKeyRequest",
|
||||
"crypto.roomKeyRequestCancellation",
|
||||
"crypto.warning",
|
||||
@@ -623,6 +696,43 @@ async function _setDeviceVerification(
|
||||
client.emit("deviceVerificationChanged", userId, deviceId, dev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a key verification from another user.
|
||||
*
|
||||
* @param {string} userId the user to request verification with
|
||||
* @param {Array} devices array of device IDs to send requests to. Defaults to
|
||||
* all devices owned by the user
|
||||
* @param {Array} methods array of verification methods to use. Defaults to
|
||||
* all known methods
|
||||
*
|
||||
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
|
||||
* when the request is accepted by the other user
|
||||
*/
|
||||
MatrixClient.prototype.requestVerification = function(userId, devices, methods) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.requestVerification(userId, devices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Begin a key verification.
|
||||
*
|
||||
* @param {string} method the verification method to use
|
||||
* @param {string} userId the user to verify keys with
|
||||
* @param {string} deviceId the device to verify
|
||||
*
|
||||
* @returns {module:crypto/verification/Base} a verification object
|
||||
*/
|
||||
MatrixClient.prototype.beginKeyVerification = function(
|
||||
method, userId, deviceId,
|
||||
) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.beginKeyVerification(method, userId, deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the global override for whether the client should ever send encrypted
|
||||
* messages to unverified devices. This provides the default for rooms which
|
||||
@@ -683,9 +793,10 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) {
|
||||
* request.
|
||||
* @param {MatrixEvent} event event of which to cancel and resend the room
|
||||
* key request.
|
||||
* @return {Promise} A promise that will resolve when the key request is queued
|
||||
*/
|
||||
MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
|
||||
event.cancelAndResendKeyRequest(this._crypto);
|
||||
return event.cancelAndResendKeyRequest(this._crypto, this.getUserId());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -772,6 +883,19 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
|
||||
return this._crypto.importRoomKeys(keys);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a re-check of the local key backup status against
|
||||
* what's on the server.
|
||||
*
|
||||
* @returns {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
MatrixClient.prototype.checkKeyBackup = function() {
|
||||
return this._crypto.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information about the current key backup.
|
||||
* @returns {Promise} Information object from API or null
|
||||
@@ -841,6 +965,10 @@ MatrixClient.prototype.enableKeyBackup = function(info) {
|
||||
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
|
||||
|
||||
this.emit('crypto.keyBackupStatus', true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
this._crypto.scheduleKeyBackupSend();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -991,12 +1119,16 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.backupAllGroupSessions = function(version) {
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
return this._crypto.backupAllGroupSessions(version);
|
||||
await this._crypto.scheduleAllGroupSessionsForBackup();
|
||||
};
|
||||
|
||||
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
@@ -1008,28 +1140,28 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
}
|
||||
};
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
|
||||
password, targetRoomId, targetSessionId, version,
|
||||
) {
|
||||
const backupInfo = await this.getKeyBackupVersion();
|
||||
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
|
||||
password, targetRoomId, targetSessionId, backupInfo,
|
||||
) {
|
||||
const privKey = await keyForExistingBackup(backupInfo, password);
|
||||
return this._restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
|
||||
recoveryKey, targetRoomId, targetSessionId, version,
|
||||
recoveryKey, targetRoomId, targetSessionId, backupInfo,
|
||||
) {
|
||||
const privKey = decodeRecoveryKey(recoveryKey);
|
||||
return this._restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype._restoreKeyBackup = function(
|
||||
privKey, targetRoomId, targetSessionId, version,
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
@@ -1037,16 +1169,26 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
let totalKeyCount = 0;
|
||||
let keys = [];
|
||||
|
||||
const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version);
|
||||
const path = this._makeKeyBackupPath(
|
||||
targetRoomId, targetSessionId, backupInfo.version,
|
||||
);
|
||||
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let backupPubKey;
|
||||
try {
|
||||
decryption.init_with_private_key(privKey);
|
||||
backupPubKey = decryption.init_with_private_key(privKey);
|
||||
} catch(e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If the pubkey computed from the private data we've been given
|
||||
// doesn't match the one in the auth_data, the user has enetered
|
||||
// a different recovery key / the wrong passphrase.
|
||||
if (backupPubKey !== backupInfo.auth_data.public_key) {
|
||||
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
|
||||
}
|
||||
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", path.path, path.queryData,
|
||||
).then((res) => {
|
||||
@@ -1081,6 +1223,8 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
}
|
||||
|
||||
return this.importRoomKeys(keys);
|
||||
}).then(() => {
|
||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||
}).then(() => {
|
||||
return {total: totalKeyCount, imported: keys.length};
|
||||
}).finally(() => {
|
||||
@@ -1986,6 +2130,80 @@ MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callba
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the history of room upgrades for a given room, as far as the
|
||||
* client can see. Returns an array of Rooms where the first entry is the
|
||||
* oldest and the last entry is the newest (likely current) room. If the
|
||||
* provided room is not found, this returns an empty list. This works in
|
||||
* both directions, looking for older and newer rooms of the given room.
|
||||
* @param {string} roomId The room ID to search from
|
||||
* @param {boolean} verifyLinks If true, the function will only return rooms
|
||||
* which can be proven to be linked. For example, rooms which have a create
|
||||
* event pointing to an old room which the client is not aware of or doesn't
|
||||
* have a matching tombstone would not be returned.
|
||||
* @return {Room[]} An array of rooms representing the upgrade
|
||||
* history.
|
||||
*/
|
||||
MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=false) {
|
||||
let currentRoom = this.getRoom(roomId);
|
||||
if (!currentRoom) return [];
|
||||
|
||||
const upgradeHistory = [currentRoom];
|
||||
|
||||
// Work backwards first, looking at create events.
|
||||
let createEvent = currentRoom.currentState.getStateEvents("m.room.create", "");
|
||||
while (createEvent) {
|
||||
console.log(`Looking at ${createEvent.getId()}`);
|
||||
const predecessor = createEvent.getContent()['predecessor'];
|
||||
if (predecessor && predecessor['room_id']) {
|
||||
console.log(`Looking at predecessor ${predecessor['room_id']}`);
|
||||
const refRoom = this.getRoom(predecessor['room_id']);
|
||||
if (!refRoom) break; // end of the chain
|
||||
|
||||
if (verifyLinks) {
|
||||
const tombstone = refRoom.currentState
|
||||
.getStateEvents("m.room.tombstone", "");
|
||||
|
||||
if (!tombstone
|
||||
|| tombstone.getContent()['replacement_room'] !== refRoom.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
upgradeHistory.splice(0, 0, refRoom);
|
||||
createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
|
||||
} else {
|
||||
// No further create events to look at
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Work forwards next, looking at tombstone events
|
||||
let tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", "");
|
||||
while (tombstoneEvent) {
|
||||
const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']);
|
||||
if (!refRoom) break; // end of the chain
|
||||
|
||||
if (verifyLinks) {
|
||||
createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent || !createEvent.getContent()['predecessor']) break;
|
||||
|
||||
const predecessor = createEvent.getContent()['predecessor'];
|
||||
if (predecessor['room_id'] !== currentRoom.roomId) break;
|
||||
}
|
||||
|
||||
// Push to the end because we're looking forwards
|
||||
upgradeHistory.push(refRoom);
|
||||
|
||||
// Set the current room to the reference room so we know where we're at
|
||||
currentRoom = refRoom;
|
||||
tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", "");
|
||||
}
|
||||
|
||||
return upgradeHistory;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} userId
|
||||
@@ -2053,6 +2271,50 @@ MatrixClient.prototype.leave = function(roomId, callback) {
|
||||
callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Leaves all rooms in the chain of room upgrades based on the given room. By
|
||||
* default, this will leave all the previous and upgraded rooms, including the
|
||||
* given room. To only leave the given room and any previous rooms, keeping the
|
||||
* upgraded (modern) rooms untouched supply `false` to `includeFuture`.
|
||||
* @param {string} roomId The room ID to start leaving at
|
||||
* @param {boolean} includeFuture If true, the whole chain (past and future) of
|
||||
* upgraded rooms will be left.
|
||||
* @return {module:client.Promise} Resolves when completed with an object keyed
|
||||
* by room ID and value of the error encountered when leaving or null.
|
||||
*/
|
||||
MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) {
|
||||
const upgradeHistory = this.getRoomUpgradeHistory(roomId);
|
||||
|
||||
let eligibleToLeave = upgradeHistory;
|
||||
if (!includeFuture) {
|
||||
eligibleToLeave = [];
|
||||
for (const room of upgradeHistory) {
|
||||
eligibleToLeave.push(room);
|
||||
if (room.roomId === roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const populationResults = {}; // {roomId: Error}
|
||||
const promises = [];
|
||||
|
||||
const doLeave = (roomId) => {
|
||||
return this.leave(roomId).then(() => {
|
||||
populationResults[roomId] = null;
|
||||
}).catch((err) => {
|
||||
populationResults[roomId] = err;
|
||||
return null; // suppress error
|
||||
});
|
||||
};
|
||||
|
||||
for (const room of eligibleToLeave) {
|
||||
promises.push(doLeave(room.roomId));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => populationResults);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} userId
|
||||
@@ -2262,6 +2524,32 @@ 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) {
|
||||
const type = "im.vector.user_status";
|
||||
return Promise.all(this.getRooms().map((room) => {
|
||||
const isJoined = room.getMyMembership() === "join";
|
||||
const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
|
||||
if (!isJoined || !looksLikeDm) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Check power level separately as it's a bit more expensive.
|
||||
const maySend = room.currentState.mayClientSendStateEvent(type, this);
|
||||
if (!maySend) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.sendStateEvent(room.roomId, type, {
|
||||
status: newMessage,
|
||||
}, this.getUserId());
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} opts Options to apply
|
||||
* @param {string} opts.presence One of "online", "offline" or "unavailable"
|
||||
@@ -4126,6 +4414,34 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a key verification is requested.
|
||||
* @event module:client~MatrixClient#"crypto.verification.request"
|
||||
* @param {object} data
|
||||
* @param {MatrixEvent} data.event the original verification request message
|
||||
* @param {Array} data.methods the verification methods that can be used
|
||||
* @param {Function} data.beginKeyVerification a function to call if a key
|
||||
* verification should be performed. The function takes one argument: the
|
||||
* name of the key verification method (taken from data.methods) to use.
|
||||
* @param {Function} data.cancel a function to call if the key verification is
|
||||
* rejected.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a key verification is requested with an unknown method.
|
||||
* @event module:client~MatrixClient#"crypto.verification.request.unknown"
|
||||
* @param {string} userId the user ID who requested the key verification
|
||||
* @param {Function} cancel a function that will send a cancellation message to
|
||||
* reject the key verification.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a key verification started message is received.
|
||||
* @event module:client~MatrixClient#"crypto.verification.start"
|
||||
* @param {module:crypto/verification/Base} verifier a verifier object to
|
||||
* perform the key verification
|
||||
*/
|
||||
|
||||
// EventEmitter JSDocs
|
||||
|
||||
/**
|
||||
|
||||
+10
-33
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 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.
|
||||
@@ -61,9 +61,8 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export default class DeviceList {
|
||||
constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
|
||||
constructor(baseApis, cryptoStore, olmDevice) {
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._sessionStore = sessionStore;
|
||||
|
||||
// userId -> {
|
||||
// deviceId -> {
|
||||
@@ -108,30 +107,13 @@ export default class DeviceList {
|
||||
* Load the device tracking state from storage
|
||||
*/
|
||||
async load() {
|
||||
let shouldDeleteSessionStore = false;
|
||||
await this._cryptoStore.doTxn(
|
||||
// migrate from session store if there's data there and not here
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||
if (deviceData === null) {
|
||||
logger.log("Migrating e2e device data...");
|
||||
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
|
||||
this._deviceTrackingStatus = (
|
||||
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
|
||||
);
|
||||
this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
|
||||
this._cryptoStore.storeEndToEndDeviceData({
|
||||
devices: this._devices,
|
||||
trackingStatus: this._deviceTrackingStatus,
|
||||
syncToken: this._syncToken,
|
||||
}, txn);
|
||||
shouldDeleteSessionStore = true;
|
||||
} else {
|
||||
this._devices = deviceData ? deviceData.devices : {},
|
||||
this._deviceTrackingStatus = deviceData ?
|
||||
deviceData.trackingStatus : {};
|
||||
this._syncToken = deviceData ? deviceData.syncToken : null;
|
||||
}
|
||||
this._devices = deviceData ? deviceData.devices : {},
|
||||
this._deviceTrackingStatus = deviceData ?
|
||||
deviceData.trackingStatus : {};
|
||||
this._syncToken = deviceData ? deviceData.syncToken : null;
|
||||
this._userByIdentityKey = {};
|
||||
for (const user of Object.keys(this._devices)) {
|
||||
const userDevices = this._devices[user];
|
||||
@@ -146,11 +128,6 @@ export default class DeviceList {
|
||||
},
|
||||
);
|
||||
|
||||
if (shouldDeleteSessionStore) {
|
||||
// migrated data is now safely persisted: remove from old store
|
||||
this._sessionStore.removeEndToEndDeviceData();
|
||||
}
|
||||
|
||||
for (const u of Object.keys(this._deviceTrackingStatus)) {
|
||||
// if a download was in progress when we got shut down, it isn't any more.
|
||||
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||
@@ -475,10 +452,10 @@ export default class DeviceList {
|
||||
if (!this._deviceTrackingStatus[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
|
||||
// of calls; we save all data together once the sync is done
|
||||
this._dirty = true;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; we save all data together once the sync is done
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+57
-114
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2019 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.
|
||||
@@ -59,20 +59,17 @@ function checkPayloadLength(payloadString) {
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
*
|
||||
* Accounts and sessions are kept pickled in a sessionStore.
|
||||
* Accounts and sessions are kept pickled in the cryptoStore.
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/OlmDevice
|
||||
*
|
||||
* @param {Object} sessionStore A store to be used for data in end-to-end
|
||||
* crypto. This is deprecated and being replaced by cryptoStore.
|
||||
* @param {Object} cryptoStore A store for crypto data
|
||||
*
|
||||
* @property {string} deviceCurve25519Key Curve25519 key for the account
|
||||
* @property {string} deviceEd25519Key Ed25519 key for the account
|
||||
*/
|
||||
function OlmDevice(sessionStore, cryptoStore) {
|
||||
this._sessionStore = sessionStore;
|
||||
function OlmDevice(cryptoStore) {
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._pickleKey = "DEFAULT_KEY";
|
||||
|
||||
@@ -81,7 +78,7 @@ function OlmDevice(sessionStore, cryptoStore) {
|
||||
this.deviceEd25519Key = null;
|
||||
this._maxOneTimeKeys = null;
|
||||
|
||||
// we don't bother stashing outboundgroupsessions in the sessionstore -
|
||||
// we don't bother stashing outboundgroupsessions in the cryptoStore -
|
||||
// instead we keep them here.
|
||||
this._outboundGroupSessionStore = {};
|
||||
|
||||
@@ -102,6 +99,10 @@ function OlmDevice(sessionStore, cryptoStore) {
|
||||
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are objects of the form "{id: <event id>, timestamp: <ts>}"
|
||||
this._inboundGroupSessionMessageIndexes = {};
|
||||
|
||||
// Keep track of sessions that we're starting, so that we don't start
|
||||
// multiple sessions for the same device at the same time.
|
||||
this._sessionsInProgress = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,14 +115,10 @@ function OlmDevice(sessionStore, cryptoStore) {
|
||||
* Reads the device keys from the OlmAccount object.
|
||||
*/
|
||||
OlmDevice.prototype.init = async function() {
|
||||
await this._migrateFromSessionStore();
|
||||
|
||||
let e2eKeys;
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
await _initialiseAccount(
|
||||
this._sessionStore, this._cryptoStore, this._pickleKey, account,
|
||||
);
|
||||
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
|
||||
@@ -133,7 +130,7 @@ OlmDevice.prototype.init = async function() {
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
};
|
||||
|
||||
async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account) {
|
||||
async function _initialiseAccount(cryptoStore, pickleKey, account) {
|
||||
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
if (pickledAccount !== null) {
|
||||
@@ -154,95 +151,6 @@ OlmDevice.getOlmVersion = function() {
|
||||
return global.Olm.get_library_version();
|
||||
};
|
||||
|
||||
OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
// account
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
this._cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
if (pickledAccount === null) {
|
||||
// Migrate from sessionStore
|
||||
pickledAccount = this._sessionStore.getEndToEndAccount();
|
||||
if (pickledAccount !== null) {
|
||||
logger.log("Migrating account from session store");
|
||||
this._cryptoStore.storeAccount(txn, pickledAccount);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// remove the old account now the transaction has completed. Either we've
|
||||
// migrated it or decided not to, either way we want to blow away the old data.
|
||||
this._sessionStore.removeEndToEndAccount();
|
||||
|
||||
// sessions
|
||||
const sessions = this._sessionStore.getAllEndToEndSessions();
|
||||
if (Object.keys(sessions).length > 0) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
// Don't migrate sessions from localstorage if we already have sessions
|
||||
// in indexeddb, since this means we've already migrated and an old version
|
||||
// has run against the same localstorage and created some spurious sessions.
|
||||
this._cryptoStore.countEndToEndSessions(txn, (count) => {
|
||||
if (count) {
|
||||
logger.log("Crypto store already has sessions: not migrating");
|
||||
return;
|
||||
}
|
||||
let numSessions = 0;
|
||||
for (const deviceKey of Object.keys(sessions)) {
|
||||
for (const sessionId of Object.keys(sessions[deviceKey])) {
|
||||
numSessions++;
|
||||
this._cryptoStore.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessions[deviceKey][sessionId], txn,
|
||||
);
|
||||
}
|
||||
}
|
||||
logger.log(
|
||||
"Migrating " + numSessions + " sessions from session store",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this._sessionStore.removeAllEndToEndSessions();
|
||||
}
|
||||
|
||||
// inbound group sessions
|
||||
const ibGroupSessions = this._sessionStore.getAllEndToEndInboundGroupSessionKeys();
|
||||
if (Object.keys(ibGroupSessions).length > 0) {
|
||||
let numIbSessions = 0;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
// We always migrate inbound group sessions, even if we already have some
|
||||
// in the new store. They should be be safe to migrate.
|
||||
for (const s of ibGroupSessions) {
|
||||
try {
|
||||
this._cryptoStore.addEndToEndInboundGroupSession(
|
||||
s.senderKey, s.sessionId,
|
||||
JSON.parse(
|
||||
this._sessionStore.getEndToEndInboundGroupSession(
|
||||
s.senderKey, s.sessionId,
|
||||
),
|
||||
), txn,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to migrate session " + s.senderKey + "/" +
|
||||
s.sessionId + ": " + e.stack || e,
|
||||
);
|
||||
}
|
||||
++numIbSessions;
|
||||
}
|
||||
logger.log(
|
||||
"Migrated " + numIbSessions +
|
||||
" inbound group sessions from session store",
|
||||
);
|
||||
},
|
||||
);
|
||||
this._sessionStore.removeAllEndToEndInboundGroupSessions();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* extract our OlmAccount from the crypto store and call the given function
|
||||
* with the account object
|
||||
@@ -553,6 +461,15 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
* @return {Promise<string[]>} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
|
||||
console.log("waiting for session to be created");
|
||||
try {
|
||||
await this._sessionsInProgress[theirDeviceIdentityKey];
|
||||
} catch (e) {
|
||||
// if the session failed to be created, just fall through and
|
||||
// return an empty result
|
||||
}
|
||||
}
|
||||
let sessionIds;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
@@ -573,10 +490,18 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @return {Promise<?string>} session id, or null if no established session
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey);
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
) {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
);
|
||||
|
||||
if (sessionInfos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -611,9 +536,21 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe
|
||||
* message and is therefore past the pre-key stage), and 'sessionId'.
|
||||
*
|
||||
* @param {string} deviceIdentityKey Curve25519 identity key for the device
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) {
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
|
||||
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
|
||||
logger.log("waiting for session to be created");
|
||||
try {
|
||||
await this._sessionsInProgress[deviceIdentityKey];
|
||||
} catch (e) {
|
||||
// if the session failed to be created, then just fall through and
|
||||
// return an empty result
|
||||
}
|
||||
}
|
||||
const info = [];
|
||||
|
||||
await this._cryptoStore.doTxn(
|
||||
@@ -915,14 +852,6 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
(existingSession, existingSessionData) => {
|
||||
if (existingSession) {
|
||||
logger.log(
|
||||
"Update for megolm session " + senderKey + "/" + sessionId,
|
||||
);
|
||||
// for now we just ignore updates. TODO: implement something here
|
||||
return;
|
||||
}
|
||||
|
||||
// new session.
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
try {
|
||||
@@ -938,6 +867,20 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingSession) {
|
||||
logger.log(
|
||||
"Update for megolm session "
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), so keep it
|
||||
logger.log("Keeping existing session");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sessionData = {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this._pickleKey),
|
||||
@@ -945,7 +888,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
};
|
||||
|
||||
this._cryptoStore.addEndToEndInboundGroupSession(
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -124,35 +124,118 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
*
|
||||
* @param {module:crypto~RoomKeyRequestBody} requestBody
|
||||
* @param {Array<{userId: string, deviceId: string}>} recipients
|
||||
* @param {boolean} resend whether to resend the key request if there is
|
||||
* already one
|
||||
*
|
||||
* @returns {Promise} resolves when the request has been added to the
|
||||
* pending list (or we have established that a similar request already
|
||||
* exists)
|
||||
*/
|
||||
sendRoomKeyRequest(requestBody, recipients) {
|
||||
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
|
||||
requestBody: requestBody,
|
||||
recipients: recipients,
|
||||
requestId: this._baseApis.makeTxnId(),
|
||||
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
}).then((req) => {
|
||||
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
|
||||
this._startTimer();
|
||||
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
|
||||
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
|
||||
requestBody,
|
||||
);
|
||||
if (!req) {
|
||||
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
|
||||
requestBody: requestBody,
|
||||
recipients: recipients,
|
||||
requestId: this._baseApis.makeTxnId(),
|
||||
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
});
|
||||
} else {
|
||||
switch (req.state) {
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
|
||||
case ROOM_KEY_REQUEST_STATES.UNSENT:
|
||||
// nothing to do here, since we're going to send a request anyways
|
||||
return;
|
||||
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
|
||||
// existing request is about to be cancelled. If we want to
|
||||
// resend, then change the state so that it resends after
|
||||
// cancelling. Otherwise, just cancel the cancellation.
|
||||
const state = resend ?
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
|
||||
ROOM_KEY_REQUEST_STATES.SENT;
|
||||
await this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
|
||||
state,
|
||||
cancellationTxnId: this._baseApis.makeTxnId(),
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
case ROOM_KEY_REQUEST_STATES.SENT: {
|
||||
// a request has already been sent. If we don't want to
|
||||
// resend, then do nothing. If we do want to, then cancel the
|
||||
// existing request and send a new one.
|
||||
if (resend) {
|
||||
const state =
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||
const updatedReq =
|
||||
await this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
|
||||
state,
|
||||
cancellationTxnId: this._baseApis.makeTxnId(),
|
||||
// need to use a new transaction ID so that
|
||||
// the request gets sent
|
||||
requestTxnId: this._baseApis.makeTxnId(),
|
||||
},
|
||||
);
|
||||
if (!updatedReq) {
|
||||
// updateOutgoingRoomKeyRequest couldn't find the request
|
||||
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
||||
// raced with another tab to mark the request cancelled.
|
||||
// Try again, to make sure the request is resent.
|
||||
return await this.sendRoomKeyRequest(
|
||||
requestBody, recipients, resend,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't want to wait for the timer, so we send it
|
||||
// immediately. (We might actually end up racing with the timer,
|
||||
// but that's ok: even if we make the request twice, we'll do it
|
||||
// with the same transaction_id, so only one message will get
|
||||
// sent).
|
||||
//
|
||||
// (We also don't want to wait for the response from the server
|
||||
// here, as it will slow down processing of received keys if we
|
||||
// do.)
|
||||
try {
|
||||
await this._sendOutgoingRoomKeyRequestCancellation(
|
||||
updatedReq,
|
||||
true,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error sending room key request cancellation;"
|
||||
+ " will retry later.", e,
|
||||
);
|
||||
}
|
||||
// The request has transitioned from
|
||||
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
|
||||
// still need to resend the request which is now UNSENT, so
|
||||
// start the timer if it isn't already started.
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('unhandled state: ' + req.state);
|
||||
}
|
||||
}
|
||||
// some of the branches require the timer to be started. Just start it
|
||||
// all the time, because it doesn't hurt to start it.
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given requestBody
|
||||
*
|
||||
* @param {module:crypto~RoomKeyRequestBody} requestBody
|
||||
* @param {boolean} andResend if true, transition to UNSENT instead of
|
||||
* deleting after sending cancellation.
|
||||
*
|
||||
* @returns {Promise} resolves when the request has been updated in our
|
||||
* pending list.
|
||||
*/
|
||||
cancelRoomKeyRequest(requestBody, andResend=false) {
|
||||
cancelRoomKeyRequest(requestBody) {
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequest(
|
||||
requestBody,
|
||||
).then((req) => {
|
||||
@@ -183,15 +266,10 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
);
|
||||
|
||||
case ROOM_KEY_REQUEST_STATES.SENT: {
|
||||
// If `andResend` is set, transition to UNSENT once the
|
||||
// cancellation has successfully been sent.
|
||||
const state = andResend ?
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING;
|
||||
// send a cancellation.
|
||||
return this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
|
||||
state,
|
||||
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||
cancellationTxnId: this._baseApis.makeTxnId(),
|
||||
},
|
||||
).then((updatedReq) => {
|
||||
@@ -221,20 +299,12 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
// do.)
|
||||
this._sendOutgoingRoomKeyRequestCancellation(
|
||||
updatedReq,
|
||||
andResend,
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
"Error sending room key request cancellation;"
|
||||
+ " will retry later.", e,
|
||||
);
|
||||
this._startTimer();
|
||||
}).then(() => {
|
||||
if (!andResend) return;
|
||||
// The request has transitioned from
|
||||
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
|
||||
// still need to resend the request which is now UNSENT, so
|
||||
// start the timer if it isn't already started.
|
||||
this._startTimer();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -280,7 +350,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
logger.warn(
|
||||
`error in OutgoingRoomKeyRequestManager: ${e}`,
|
||||
);
|
||||
}).done();
|
||||
});
|
||||
};
|
||||
|
||||
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
||||
@@ -331,7 +401,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
this._startTimer();
|
||||
}).done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +421,7 @@ export default class OutgoingRoomKeyRequestManager {
|
||||
};
|
||||
|
||||
return this._sendMessageToDevices(
|
||||
requestMessage, req.recipients, req.requestId,
|
||||
requestMessage, req.recipients, req.requestTxnId || req.requestId,
|
||||
).then(() => {
|
||||
return this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
|
||||
+3
-22
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 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.
|
||||
@@ -26,40 +26,21 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
* @alias module:crypto/RoomList
|
||||
*/
|
||||
export default class RoomList {
|
||||
constructor(cryptoStore, sessionStore) {
|
||||
constructor(cryptoStore) {
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._sessionStore = sessionStore;
|
||||
|
||||
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
|
||||
this._roomEncryption = {};
|
||||
}
|
||||
|
||||
async init() {
|
||||
let removeSessionStoreRooms = false;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this._cryptoStore.getEndToEndRooms(txn, (result) => {
|
||||
if (result === null || Object.keys(result).length === 0) {
|
||||
// migrate from session store, if there's data there
|
||||
const sessStoreRooms = this._sessionStore.getAllEndToEndRooms();
|
||||
if (sessStoreRooms !== null) {
|
||||
for (const roomId of Object.keys(sessStoreRooms)) {
|
||||
this._cryptoStore.storeEndToEndRoom(
|
||||
roomId, sessStoreRooms[roomId], txn,
|
||||
);
|
||||
}
|
||||
}
|
||||
this._roomEncryption = sessStoreRooms;
|
||||
removeSessionStoreRooms = true;
|
||||
} else {
|
||||
this._roomEncryption = result;
|
||||
}
|
||||
this._roomEncryption = result;
|
||||
});
|
||||
},
|
||||
);
|
||||
if (removeSessionStoreRooms) {
|
||||
this._sessionStore.removeAllEndToEndRooms();
|
||||
}
|
||||
}
|
||||
|
||||
getRoomEncryption(roomId) {
|
||||
|
||||
@@ -758,7 +758,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
} catch (e) {
|
||||
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
|
||||
|
||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this._requestKeysForEvent(event);
|
||||
|
||||
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
|
||||
@@ -766,7 +766,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
|
||||
throw new base.DecryptionError(
|
||||
errorCode,
|
||||
e.toString(), {
|
||||
e ? e.toString() : "Unknown Error: Error is undefined", {
|
||||
session: content.sender_key + '|' + content.session_id,
|
||||
},
|
||||
);
|
||||
@@ -815,19 +815,9 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
};
|
||||
|
||||
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
|
||||
const sender = event.getSender();
|
||||
const wireContent = event.getWireContent();
|
||||
|
||||
// send the request to all of our own devices, and the
|
||||
// original sending device if it wasn't us.
|
||||
const recipients = [{
|
||||
userId: this._userId, deviceId: '*',
|
||||
}];
|
||||
if (sender != this._userId) {
|
||||
recipients.push({
|
||||
userId: sender, deviceId: wireContent.device_id,
|
||||
});
|
||||
}
|
||||
const recipients = event.getKeyRequestRecipients(this._userId);
|
||||
|
||||
this._crypto.requestRoomKey({
|
||||
room_id: event.getRoomId(),
|
||||
@@ -938,16 +928,23 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
).then(() => {
|
||||
// cancel any outstanding room key requests for this session
|
||||
this._crypto.cancelRoomKeyRequest({
|
||||
algorithm: content.algorithm,
|
||||
room_id: content.room_id,
|
||||
session_id: content.session_id,
|
||||
sender_key: senderKey,
|
||||
});
|
||||
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId);
|
||||
this._retryDecryption(senderKey, sessionId)
|
||||
.then((success) => {
|
||||
// cancel any outstanding room key requests for this session.
|
||||
// Only do this if we managed to decrypt every message in the
|
||||
// session, because if we didn't, we leave the other key
|
||||
// requests in the hopes that someone sends us a key that
|
||||
// includes an earlier index.
|
||||
if (success) {
|
||||
this._crypto.cancelRoomKeyRequest({
|
||||
algorithm: content.algorithm,
|
||||
room_id: content.room_id,
|
||||
session_id: content.session_id,
|
||||
sender_key: senderKey,
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for the keys to be backed up for the server
|
||||
@@ -1105,19 +1102,27 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
* @private
|
||||
* @param {String} senderKey
|
||||
* @param {String} sessionId
|
||||
*
|
||||
* @return {Boolean} whether all messages were successfully decrypted
|
||||
*/
|
||||
MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
|
||||
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
|
||||
const k = senderKey + "|" + sessionId;
|
||||
const pending = this._pendingEvents[k];
|
||||
if (!pending) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
delete this._pendingEvents[k];
|
||||
|
||||
for (const ev of pending) {
|
||||
ev.attemptDecryption(this._crypto);
|
||||
}
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this._crypto);
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
}));
|
||||
|
||||
return !this._pendingEvents[k];
|
||||
};
|
||||
|
||||
base.registerAlgorithm(
|
||||
|
||||
+602
-157
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2019 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.
|
||||
@@ -33,15 +33,40 @@ const algorithms = require("./algorithms");
|
||||
const DeviceInfo = require("./deviceinfo");
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
const DeviceList = require('./DeviceList').default;
|
||||
import { randomString } from '../randomstring';
|
||||
|
||||
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from './verification/QRCode';
|
||||
import SAS from './verification/SAS';
|
||||
import {
|
||||
newUserCancelledError,
|
||||
newUnexpectedMessageError,
|
||||
newUnknownMethodError,
|
||||
} from './verification/Error';
|
||||
|
||||
const defaultVerificationMethods = {
|
||||
[ScanQRCode.NAME]: ScanQRCode,
|
||||
[ShowQRCode.NAME]: ShowQRCode,
|
||||
[SAS.NAME]: SAS,
|
||||
};
|
||||
|
||||
/**
|
||||
* verification method names
|
||||
*/
|
||||
export const verificationMethods = {
|
||||
QR_CODE_SCAN: ScanQRCode.NAME,
|
||||
QR_CODE_SHOW: ShowQRCode.NAME,
|
||||
SAS: SAS.NAME,
|
||||
};
|
||||
|
||||
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
|
||||
@@ -68,9 +93,13 @@ const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
* storage for the crypto layer.
|
||||
*
|
||||
* @param {RoomList} roomList An initialised RoomList object
|
||||
*
|
||||
* @param {Array} verificationMethods Array of verification methods to use.
|
||||
* Each element can either be a string from MatrixClient.verificationMethods
|
||||
* or a class that implements a verification method.
|
||||
*/
|
||||
export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
clientStore, cryptoStore, roomList) {
|
||||
clientStore, cryptoStore, roomList, verificationMethods) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
@@ -78,6 +107,24 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._clientStore = clientStore;
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._roomList = roomList;
|
||||
this._verificationMethods = new Map();
|
||||
if (verificationMethods) {
|
||||
for (const method of verificationMethods) {
|
||||
if (typeof method === "string") {
|
||||
if (defaultVerificationMethods[method]) {
|
||||
this._verificationMethods.set(
|
||||
method,
|
||||
defaultVerificationMethods[method],
|
||||
);
|
||||
}
|
||||
} else if (method.NAME) {
|
||||
this._verificationMethods.set(
|
||||
method.NAME,
|
||||
method,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// track whether this device's megolm keys are being backed up incrementally
|
||||
// to the server or not.
|
||||
@@ -87,9 +134,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
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._olmDevice = new OlmDevice(cryptoStore);
|
||||
this._deviceList = new DeviceList(
|
||||
baseApis, cryptoStore, sessionStore, this._olmDevice,
|
||||
baseApis, cryptoStore, this._olmDevice,
|
||||
);
|
||||
|
||||
// the last time we did a check for the number of one-time-keys on the
|
||||
@@ -139,6 +186,8 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
// },
|
||||
// }
|
||||
this._lastNewSessionForced = {};
|
||||
|
||||
this._verificationTransactions = new Map();
|
||||
}
|
||||
utils.inherits(Crypto, EventEmitter);
|
||||
|
||||
@@ -149,25 +198,6 @@ utils.inherits(Crypto, EventEmitter);
|
||||
*/
|
||||
Crypto.prototype.init = async function() {
|
||||
await global.Olm.init();
|
||||
|
||||
const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount());
|
||||
let cryptoStoreHasAccount;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
this._cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
cryptoStoreHasAccount = Boolean(pickledAccount);
|
||||
});
|
||||
},
|
||||
);
|
||||
if (sessionStoreHasAccount && !cryptoStoreHasAccount) {
|
||||
// we're about to migrate to the crypto store
|
||||
this.emit("crypto.warning", 'CRYPTO_WARNING_ACCOUNT_MIGRATED');
|
||||
} else if (sessionStoreHasAccount && cryptoStoreHasAccount) {
|
||||
// There's an account in both stores: an old version of
|
||||
// the code has been run against this store.
|
||||
this.emit("crypto.warning", 'CRYPTO_WARNING_OLD_VERSION_DETECTED');
|
||||
}
|
||||
|
||||
await this._olmDevice.init();
|
||||
await this._deviceList.load();
|
||||
|
||||
@@ -186,7 +216,7 @@ Crypto.prototype.init = async function() {
|
||||
}
|
||||
|
||||
if (!myDevices[this._deviceId]) {
|
||||
// add our own deviceinfo to the sessionstore
|
||||
// add our own deviceinfo to the cryptoStore
|
||||
const deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._supportedAlgorithms,
|
||||
@@ -212,6 +242,11 @@ Crypto.prototype.init = async function() {
|
||||
*/
|
||||
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 null;
|
||||
}
|
||||
let backupInfo;
|
||||
try {
|
||||
backupInfo = await this._baseApis.getKeyBackupVersion();
|
||||
@@ -221,32 +256,60 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
// well that's told us. we won't try again.
|
||||
this._checkedForBackup = true;
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
this._checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
console.log("Found usable key backup: enabling key backups");
|
||||
console.log(
|
||||
"Found usable key backup v" + backupInfo.version +
|
||||
": 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");
|
||||
} else if (trustInfo.usable && this.backupInfo) {
|
||||
// may not be the same version: if not, we should switch
|
||||
if (backupInfo.version !== this.backupInfo.version) {
|
||||
console.log(
|
||||
"On backup version " + this.backupInfo.version + " but found " +
|
||||
"version " + backupInfo.version + ": switching.",
|
||||
);
|
||||
this._baseApis.disableKeyBackup();
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
} else {
|
||||
console.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
}
|
||||
|
||||
return {backupInfo, trustInfo};
|
||||
};
|
||||
|
||||
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
|
||||
// This should be redundant post cross-signing is a thing, so just
|
||||
// plonk it in localStorage for now.
|
||||
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
||||
await this.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate
|
||||
* as appropriate.
|
||||
*
|
||||
* @param {object} backupInfo Backup info from /room_keys/version endpoint
|
||||
* @return {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
Crypto.prototype.checkKeyBackup = async function(backupInfo) {
|
||||
Crypto.prototype.checkKeyBackup = async function() {
|
||||
this._checkedForBackup = false;
|
||||
await this._checkAndStartKeyBackup();
|
||||
const returnInfo = await this._checkAndStartKeyBackup();
|
||||
return returnInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -254,14 +317,16 @@ Crypto.prototype.checkKeyBackup = async function(backupInfo) {
|
||||
* @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],
|
||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
||||
* deviceId: [string],
|
||||
* device: [DeviceInfo || null],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
@@ -272,42 +337,50 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
console.log("Key backup is absent or missing required data");
|
||||
logger.info("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;
|
||||
const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const sigInfo = { deviceId: keyId.split(':')[1] }; // XXX: is this how we're supposed to get the device ID?
|
||||
const device = this._deviceList.getStoredDevice(
|
||||
this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID?
|
||||
this._userId, sigInfo.deviceId,
|
||||
);
|
||||
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;
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this._userId,
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info("Bad signature from device " + device.deviceId, e);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
||||
logger.info("Ignoring signature from unknown key " + keyId);
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified());
|
||||
ret.usable = (
|
||||
ret.sigs.some((s) => s.valid && s.device.isVerified()) ||
|
||||
ret.trusted_locally
|
||||
);
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -666,6 +739,81 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
};
|
||||
|
||||
|
||||
Crypto.prototype.requestVerification = function(userId, methods, devices) {
|
||||
if (!methods) {
|
||||
// .keys() returns an iterator, so we need to explicitly turn it into an array
|
||||
methods = [...this._verificationMethods.keys()];
|
||||
}
|
||||
if (!devices) {
|
||||
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
|
||||
}
|
||||
if (!this._verificationTransactions.has(userId)) {
|
||||
this._verificationTransactions.set(userId, new Map);
|
||||
}
|
||||
|
||||
const transactionId = randomString(32);
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this._verificationTransactions.get(userId).set(transactionId, {
|
||||
request: {
|
||||
methods: methods,
|
||||
devices: devices,
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const message = {
|
||||
transaction_id: transactionId,
|
||||
from_device: this._baseApis.deviceId,
|
||||
methods: methods,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const msgMap = {};
|
||||
for (const deviceId of devices) {
|
||||
msgMap[deviceId] = message;
|
||||
}
|
||||
this._baseApis.sendToDevice("m.key.verification.request", {[userId]: msgMap});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
Crypto.prototype.beginKeyVerification = function(
|
||||
method, userId, deviceId, transactionId,
|
||||
) {
|
||||
if (!this._verificationTransactions.has(userId)) {
|
||||
this._verificationTransactions.set(userId, new Map());
|
||||
}
|
||||
transactionId = transactionId || randomString(32);
|
||||
if (method instanceof Array) {
|
||||
if (method.length !== 2
|
||||
|| !this._verificationMethods.has(method[0])
|
||||
|| !this._verificationMethods.has(method[1])) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
/*
|
||||
return new TwoPartVerification(
|
||||
this._verificationMethods[method[0]],
|
||||
this._verificationMethods[method[1]],
|
||||
userId, deviceId, transactionId,
|
||||
);
|
||||
*/
|
||||
} else if (this._verificationMethods.has(method)) {
|
||||
const verifier = new (this._verificationMethods.get(method))(
|
||||
this._baseApis, userId, deviceId, transactionId,
|
||||
);
|
||||
if (!this._verificationTransactions.get(userId).has(transactionId)) {
|
||||
this._verificationTransactions.get(userId).set(transactionId, {});
|
||||
}
|
||||
this._verificationTransactions.get(userId).get(transactionId).verifier = verifier;
|
||||
return verifier;
|
||||
} else {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get information on the active olm sessions with a user
|
||||
* <p>
|
||||
@@ -775,7 +923,7 @@ Crypto.prototype.forceDiscardSession = function(roomId) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure a room to use encryption (ie, save a flag in the sessionstore).
|
||||
* Configure a room to use encryption (ie, save a flag in the cryptoStore).
|
||||
*
|
||||
* @param {string} roomId The room ID to enable encryption in.
|
||||
*
|
||||
@@ -981,96 +1129,124 @@ Crypto.prototype.importRoomKeys = function(keys) {
|
||||
);
|
||||
};
|
||||
|
||||
Crypto.prototype._maybeSendKeyBackup = async function(delay, retry) {
|
||||
if (retry === undefined) retry = true;
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*
|
||||
* @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
|
||||
if (this._sendingBackups) return;
|
||||
|
||||
if (!this._sendingBackups) {
|
||||
this._sendingBackups = true;
|
||||
try {
|
||||
if (delay === undefined) {
|
||||
// by default, 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
|
||||
delay = Math.random() * 10000;
|
||||
this._sendingBackups = true;
|
||||
|
||||
try {
|
||||
// wait between 0 and `maxDelay` 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() * maxDelay;
|
||||
await Promise.delay(delay);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
while (1) {
|
||||
if (!this.backupKey) {
|
||||
return;
|
||||
}
|
||||
await Promise.delay(delay);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
while (1) {
|
||||
if (!this.backupKey) {
|
||||
try {
|
||||
const numBackedUp =
|
||||
await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
return;
|
||||
}
|
||||
// FIXME: figure out what limit is reasonable
|
||||
const sessions = await this._cryptoStore.getSessionsNeedingBackup(10);
|
||||
if (!sessions.length) {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await this._baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{rooms: data},
|
||||
);
|
||||
numFailures = 0;
|
||||
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
console.log("send failed", err);
|
||||
if (err.httpStatus === 400
|
||||
|| err.httpStatus === 403
|
||||
|| err.httpStatus === 401
|
||||
|| !retry) {
|
||||
// retrying probably won't help much, so we should give up
|
||||
// FIXME: disable backups?
|
||||
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'
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
// 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 new Promise((resolve, reject) => {
|
||||
setTimeout(
|
||||
resolve,
|
||||
1000 * Math.pow(2, Math.min(numFailures - 1, 4)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._sendingBackups = false;
|
||||
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;
|
||||
}
|
||||
|
||||
let remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
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);
|
||||
remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
return sessions.length;
|
||||
};
|
||||
|
||||
Crypto.prototype.backupGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
@@ -1085,10 +1261,16 @@ Crypto.prototype.backupGroupSession = async function(
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
await this._maybeSendKeyBackup();
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
};
|
||||
|
||||
Crypto.prototype.backupAllGroupSessions = async function(version) {
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
@@ -1104,7 +1286,11 @@ Crypto.prototype.backupAllGroupSessions = async function(version) {
|
||||
},
|
||||
);
|
||||
|
||||
await this._maybeSendKeyBackup(0, false);
|
||||
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
// Schedule keys to upload in the background as soon as possible.
|
||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
||||
};
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
@@ -1221,10 +1407,14 @@ Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLi
|
||||
*
|
||||
* @param {module:crypto~RoomKeyRequestBody} requestBody
|
||||
* @param {Array<{userId: string, deviceId: string}>} recipients
|
||||
* @param {boolean} resend whether to resend the key request if there is
|
||||
* already one
|
||||
*
|
||||
* @return {Promise} a promise that resolves when the key request is queued
|
||||
*/
|
||||
Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
|
||||
this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
|
||||
requestBody, recipients,
|
||||
Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) {
|
||||
return this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
|
||||
requestBody, recipients, resend,
|
||||
).catch((e) => {
|
||||
// this normally means we couldn't talk to the store
|
||||
logger.error(
|
||||
@@ -1238,11 +1428,9 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
|
||||
*
|
||||
* @param {module:crypto~RoomKeyRequestBody} requestBody
|
||||
* parameters to match for cancellation
|
||||
* @param {boolean} andResend
|
||||
* if true, resend the key request after cancelling.
|
||||
*/
|
||||
Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) {
|
||||
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend)
|
||||
Crypto.prototype.cancelRoomKeyRequest = function(requestBody) {
|
||||
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
.catch((e) => {
|
||||
logger.warn("Error clearing pending room key requests", e);
|
||||
}).done();
|
||||
@@ -1300,6 +1488,10 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
|
||||
|
||||
// catch up on any new devices we got told about during the sync.
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
|
||||
// we always track our own device list (for key backups etc)
|
||||
this._deviceList.startTrackingDeviceList(this._userId);
|
||||
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
|
||||
// we don't start uploading one-time keys until we've caught up with
|
||||
@@ -1389,6 +1581,12 @@ Crypto.prototype._onToDeviceEvent = function(event) {
|
||||
this._onRoomKeyEvent(event);
|
||||
} else if (event.getType() == "m.room_key_request") {
|
||||
this._onRoomKeyRequestEvent(event);
|
||||
} else if (event.getType() === "m.key.verification.request") {
|
||||
this._onKeyVerificationRequest(event);
|
||||
} else if (event.getType() === "m.key.verification.start") {
|
||||
this._onKeyVerificationStart(event);
|
||||
} else if (event.getContent().transaction_id) {
|
||||
this._onKeyVerificationMessage(event);
|
||||
} else if (event.getContent().msgtype === "m.bad.encrypted") {
|
||||
this._onToDeviceBadEncrypted(event);
|
||||
} else if (event.isBeingDecrypted()) {
|
||||
@@ -1426,6 +1624,264 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
alg.onRoomKeyEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a key verification request event.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event verification request event
|
||||
*/
|
||||
Crypto.prototype._onKeyVerificationRequest = function(event) {
|
||||
const content = event.getContent();
|
||||
if (!("from_device" in content) || typeof content.from_device !== "string"
|
||||
|| !("transaction_id" in content) || typeof content.from_device !== "string"
|
||||
|| !("methods" in content) || !(content.methods instanceof Array)
|
||||
|| !("timestamp" in content) || typeof content.timestamp !== "number") {
|
||||
logger.warn("received invalid verification request from " + event.getSender());
|
||||
// ignore event if malformed
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now < content.timestamp - (5 * 60 * 1000)
|
||||
|| now > content.timestamp + (10 * 60 * 1000)) {
|
||||
// ignore if event is too far in the past or too far in the future
|
||||
logger.log("received verification that is too old or from the future");
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = event.getSender();
|
||||
if (this._verificationTransactions.has(sender)) {
|
||||
if (this._verificationTransactions.get(sender).has(content.transaction_id)) {
|
||||
// transaction already exists: cancel it and drop the existing
|
||||
// request because someone has gotten confused
|
||||
const err = newUnexpectedMessageError({
|
||||
transaction_id: content.transaction_id,
|
||||
});
|
||||
if (this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.verifier) {
|
||||
this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.verifier.cancel(err);
|
||||
} else {
|
||||
this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.reject(err);
|
||||
this.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[content.from_device]: err.getContent(),
|
||||
},
|
||||
});
|
||||
}
|
||||
this._verificationTransactions.get(sender).delete(content.transaction_id);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this._verificationTransactions.set(sender, new Map());
|
||||
}
|
||||
|
||||
// determine what requested methods we support
|
||||
const methods = [];
|
||||
for (const method of content.methods) {
|
||||
if (typeof method !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (this._verificationMethods.has(method)) {
|
||||
methods.push(method);
|
||||
}
|
||||
}
|
||||
if (methods.length === 0) {
|
||||
this._baseApis.emit(
|
||||
"crypto.verification.request.unknown",
|
||||
event.getSender(),
|
||||
() => {
|
||||
this.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[content.from_device]: newUserCancelledError({
|
||||
transaction_id: content.transaction_id,
|
||||
}).getContent(),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// notify the application that of the verification request, so it can
|
||||
// decide what to do with it
|
||||
const request = {
|
||||
event: event,
|
||||
methods: methods,
|
||||
beginKeyVerification: (method) => {
|
||||
const verifier = this.beginKeyVerification(
|
||||
method,
|
||||
sender,
|
||||
content.from_device,
|
||||
content.transaction_id,
|
||||
);
|
||||
this._verificationTransactions.get(sender).get(content.transaction_id)
|
||||
.verifier = verifier;
|
||||
return verifier;
|
||||
},
|
||||
cancel: () => {
|
||||
this._baseApis.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[content.from_device]: newUserCancelledError({
|
||||
transaction_id: content.transaction_id,
|
||||
}).getContent(),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
this._verificationTransactions.get(sender).set(content.transaction_id, {
|
||||
request: request,
|
||||
});
|
||||
this._baseApis.emit("crypto.verification.request", request);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a key verification start event.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event verification start event
|
||||
*/
|
||||
Crypto.prototype._onKeyVerificationStart = function(event) {
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent();
|
||||
const transactionId = content.transaction_id;
|
||||
const deviceId = content.from_device;
|
||||
if (!transactionId || !deviceId) {
|
||||
// invalid request, and we don't have enough information to send a
|
||||
// cancellation, so just ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
let handler = this._verificationTransactions.has(sender)
|
||||
&& this._verificationTransactions.get(sender).get(transactionId);
|
||||
// if the verification start message is invalid, send a cancel message to
|
||||
// the other side, and also send a cancellation event
|
||||
const cancel = (err) => {
|
||||
if (handler.verifier) {
|
||||
handler.verifier.cancel(err);
|
||||
} else if (handler.request && handler.request.cancel) {
|
||||
handler.request.cancel(err);
|
||||
}
|
||||
this.sendToDevice(
|
||||
"m.key.verification.cancel", {
|
||||
[sender]: {
|
||||
[deviceId]: err.getContent(),
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
if (!this._verificationMethods.has(content.method)) {
|
||||
cancel(newUnknownMethodError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
} else if (content.next_method) {
|
||||
if (!this._verificationMethods.has(content.next_method)) {
|
||||
cancel(newUnknownMethodError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
} else {
|
||||
/* TODO:
|
||||
const verification = new TwoPartVerification(
|
||||
this._verificationMethods[content.method],
|
||||
this._verificationMethods[content.next_method],
|
||||
userId, deviceId,
|
||||
);
|
||||
this.emit(verification.event_type, verification);
|
||||
this.emit(verification.first.event_type, verification);*/
|
||||
}
|
||||
} else {
|
||||
const verifier = new (this._verificationMethods.get(content.method))(
|
||||
this._baseApis, sender, deviceId, content.transaction_id,
|
||||
event, handler && handler.request,
|
||||
);
|
||||
if (!handler) {
|
||||
if (!this._verificationTransactions.has(sender)) {
|
||||
this._verificationTransactions.set(sender, new Map());
|
||||
}
|
||||
handler = this._verificationTransactions.get(sender).set(transactionId, {
|
||||
verifier: verifier,
|
||||
});
|
||||
} else {
|
||||
if (!handler.verifier) {
|
||||
handler.verifier = verifier;
|
||||
if (handler.request) {
|
||||
// the verification start was sent as a response to a
|
||||
// verification request
|
||||
|
||||
if (!handler.request.devices.includes(deviceId)) {
|
||||
// didn't send a request to that device, so it
|
||||
// shouldn't have responded
|
||||
cancel(newUnexpectedMessageError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!handler.request.methods.includes(content.method)) {
|
||||
// verification method wasn't one that was requested
|
||||
cancel(newUnknownMethodError({
|
||||
transaction_id: content.transactionId,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// send cancellation messages to all the other devices that
|
||||
// the request was sent to
|
||||
const message = {
|
||||
transaction_id: transactionId,
|
||||
code: "m.accepted",
|
||||
reason: "Verification request accepted by another device",
|
||||
};
|
||||
const msgMap = {};
|
||||
for (const devId of handler.request.devices) {
|
||||
if (devId !== deviceId) {
|
||||
msgMap[devId] = message;
|
||||
}
|
||||
}
|
||||
this._baseApis.sendToDevice("m.key.verification.cancel", {
|
||||
[sender]: msgMap,
|
||||
});
|
||||
|
||||
handler.request.resolve(verifier);
|
||||
}
|
||||
} else {
|
||||
// FIXME: make sure we're in a two-part verification, and the start matches the second part
|
||||
}
|
||||
}
|
||||
this._baseApis.emit("crypto.verification.start", verifier);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a general key verification event.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event verification start event
|
||||
*/
|
||||
Crypto.prototype._onKeyVerificationMessage = function(event) {
|
||||
const sender = event.getSender();
|
||||
const transactionId = event.getContent().transaction_id;
|
||||
const handler = this._verificationTransactions.has(sender)
|
||||
&& this._verificationTransactions.get(sender).get(transactionId);
|
||||
if (!handler) {
|
||||
return;
|
||||
} else if (event.getType() === "m.key.verification.cancel") {
|
||||
console.log(event);
|
||||
if (handler.verifier) {
|
||||
handler.verifier.cancel(event);
|
||||
} else if (handler.request && handler.request.cancel) {
|
||||
handler.request.cancel(event);
|
||||
}
|
||||
} else if (handler.verifier) {
|
||||
const verifier = handler.verifier;
|
||||
if (verifier.events
|
||||
&& verifier.events.includes(event.getType())) {
|
||||
verifier.handleEvent(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a toDevice event that couldn't be decrypted
|
||||
*
|
||||
@@ -1511,7 +1967,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
|
||||
sender, device.deviceId,
|
||||
);
|
||||
for (const keyReq of requestsToResend) {
|
||||
this.cancelRoomKeyRequest(keyReq.requestBody, true);
|
||||
this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1883,17 +2339,6 @@ class IncomingRoomKeyRequestCancellation {
|
||||
* Fires when the app may wish to warn the user about something related
|
||||
* the end-to-end crypto.
|
||||
*
|
||||
* Comes with a type which is one of:
|
||||
* * CRYPTO_WARNING_ACCOUNT_MIGRATED: Account data has been migrated from an older
|
||||
* version of the store in such a way that older clients will no longer be
|
||||
* able to read it. The app may wish to warn the user against going back to
|
||||
* an older version of the app.
|
||||
* * CRYPTO_WARNING_OLD_VERSION_DETECTED: js-sdk has detected that an older version
|
||||
* of js-sdk has been run against the same store after a migration has been
|
||||
* performed. This is likely have caused unexpected behaviour in the old
|
||||
* version. For example, the old version and the new version may have two
|
||||
* different identity keys.
|
||||
*
|
||||
* @event module:client~MatrixClient#"crypto.warning"
|
||||
* @param {string} type One of the strings listed above
|
||||
*/
|
||||
|
||||
+59
-14
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
@@ -137,6 +138,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result = {};
|
||||
const resolveSession = {};
|
||||
|
||||
for (const userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
@@ -148,7 +150,36 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key);
|
||||
if (!olmDevice._sessionsInProgress[key]) {
|
||||
// pre-emptively mark the session as in-progress to avoid race
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(
|
||||
(resolve, reject) => {
|
||||
resolveSession[key] = {
|
||||
resolve: (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
},
|
||||
reject: (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
reject(...args);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, resolveSession[key],
|
||||
);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so unmark it and unblock anything that was
|
||||
// waiting
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolveSession[key].resolve();
|
||||
delete resolveSession[key];
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
@@ -163,16 +194,19 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: this has a race condition - if we try to send another message
|
||||
// while we are claiming a key, we will end up claiming two and setting up
|
||||
// two sessions.
|
||||
//
|
||||
// That should eventually resolve itself, but it's poor form.
|
||||
|
||||
const oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
const res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm,
|
||||
);
|
||||
let res;
|
||||
try {
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm,
|
||||
);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
resolver.resolve();
|
||||
}
|
||||
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const otk_res = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
@@ -185,6 +219,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
if (result[userId][deviceId].sessionId && !force) {
|
||||
// we already have a result for this device
|
||||
continue;
|
||||
@@ -199,10 +234,12 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
logger.warn(
|
||||
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId,
|
||||
);
|
||||
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId;
|
||||
logger.warn(msg);
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -210,7 +247,15 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
_verifyKeyAndStartSession(
|
||||
olmDevice, oneTimeKey, userId, deviceInfo,
|
||||
).then((sid) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve(sid);
|
||||
}
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
}, (e) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -384,6 +384,24 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
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({
|
||||
@@ -545,8 +563,22 @@ export class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
const txn = this._db.transaction("sessions_needing_backup", "readwrite");
|
||||
countSessionsNeedingBackup(txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction("sessions_needing_backup", "readonly");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.count();
|
||||
req.onerror = reject;
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
});
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(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) => {
|
||||
@@ -658,11 +690,12 @@ function promiseifyTxn(txn) {
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
txn.onerror = () => {
|
||||
txn.onerror = (event) => {
|
||||
if (txn._mx_abortexception !== undefined) {
|
||||
reject(txn._mx_abortexception);
|
||||
} else {
|
||||
reject(event.target.error);
|
||||
}
|
||||
reject();
|
||||
};
|
||||
txn.onabort = () => reject(txn._mx_abortexception);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import LocalStorageCryptoStore from './localStorage-crypto-store';
|
||||
import MemoryCryptoStore from './memory-crypto-store';
|
||||
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
|
||||
import {InvalidCryptoStoreError} from '../../errors';
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers";
|
||||
|
||||
/**
|
||||
* Internal module. indexeddb storage for e2e.
|
||||
@@ -48,6 +49,10 @@ export default class IndexedDBCryptoStore {
|
||||
this._backendPromise = null;
|
||||
}
|
||||
|
||||
static exists(indexedDB, dbName) {
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date, or fall back to
|
||||
* a local storage or in-memory store.
|
||||
@@ -336,6 +341,17 @@ export default class IndexedDBCryptoStore {
|
||||
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.
|
||||
@@ -469,14 +485,26 @@ export default class IndexedDBCryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the inbound group sessions that need to be backed up.
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} resolves to the number of sessions
|
||||
*/
|
||||
countSessionsNeedingBackup(txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.countSessionsNeedingBackup(txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark 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 unmarked
|
||||
*/
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
unmarkSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.unmarkSessionsNeedingBackup(sessions);
|
||||
return backend.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
this.store = webStore;
|
||||
}
|
||||
|
||||
static exists(webStore) {
|
||||
const length = webStore.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (webStore.key(i).startsWith(E2E_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
@@ -94,6 +104,17 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
func(this._getEndToEndSessions(deviceKey) || {});
|
||||
}
|
||||
|
||||
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] = sessionInfo;
|
||||
@@ -210,6 +231,12 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
countSessionsNeedingBackup() {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
const sessionsNeedingBackup
|
||||
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
|
||||
@@ -249,6 +249,14 @@ export default class MemoryCryptoStore {
|
||||
func(this._sessions[deviceKey] || {});
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -328,6 +336,10 @@ export default class MemoryCryptoStore {
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
countSessionsNeedingBackup() {
|
||||
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
unmarkSessionsNeedingBackup(sessions) {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = session.senderKey + '/' + session.sessionId;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
* @module crypto/verification/Base
|
||||
*/
|
||||
|
||||
import {MatrixEvent} from '../../models/event';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
export default class VerificationBase extends EventEmitter {
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
*
|
||||
* <p>Once a verifier object is created, the verification can be started by
|
||||
* calling the verify() method, which will return a promise that will
|
||||
* resolve when the verification is completed, or reject if it could not
|
||||
* complete.</p>
|
||||
*
|
||||
* <p>Subclasses must have a NAME class property.</p>
|
||||
*
|
||||
* @class
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
*
|
||||
* @param {string} userId the user ID that is being verified
|
||||
*
|
||||
* @param {string} deviceId the device ID that is being verified
|
||||
*
|
||||
* @param {string} transactionId the transaction ID to be used when sending events
|
||||
*
|
||||
* @param {object} startEvent the m.key.verification.start event that
|
||||
* initiated this verification, if any
|
||||
*
|
||||
* @param {object} request the key verification request object related to
|
||||
* this verification, if any
|
||||
*
|
||||
* @param {object} parent parent verification for this verification, if any
|
||||
*/
|
||||
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.transactionId = transactionId;
|
||||
this.startEvent = startEvent;
|
||||
this.request = request;
|
||||
this._parent = parent;
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
}
|
||||
|
||||
_sendToDevice(type, content) {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
content.transaction_id = this.transactionId;
|
||||
return this._baseApis.sendToDevice(type, {
|
||||
[this.userId]: { [this.deviceId]: content },
|
||||
});
|
||||
}
|
||||
|
||||
_waitForEvent(type) {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
this._expectedEvent = type;
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveEvent = resolve;
|
||||
this._rejectEvent = reject;
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
if (this._done) {
|
||||
return;
|
||||
} else if (e.getType() === this._expectedEvent) {
|
||||
this._expectedEvent = undefined;
|
||||
this._rejectEvent = undefined;
|
||||
this._resolveEvent(e);
|
||||
} else {
|
||||
this._expectedEvent = undefined;
|
||||
const exception = new Error(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
);
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
reject(exception);
|
||||
}
|
||||
this.cancel(exception);
|
||||
}
|
||||
}
|
||||
|
||||
done() {
|
||||
if (!this._done) {
|
||||
this._resolve();
|
||||
}
|
||||
}
|
||||
|
||||
cancel(e) {
|
||||
if (!this._done) {
|
||||
if (this.userId && this.deviceId && this.transactionId) {
|
||||
// send a cancellation to the other user (if it wasn't
|
||||
// cancelled by the other user)
|
||||
if (e instanceof MatrixEvent) {
|
||||
const sender = e.getSender();
|
||||
if (sender !== this.userId) {
|
||||
const content = e.getContent();
|
||||
if (e.getType() === "m.key.verification.cancel") {
|
||||
content.code = content.code || "m.unknown";
|
||||
content.reason = content.reason || content.body
|
||||
|| "Unknown reason";
|
||||
content.transaction_id = this.transactionId;
|
||||
this._sendToDevice("m.key.verification.cancel", content);
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: content.body || "Unknown reason",
|
||||
transaction_id: this.transactionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: e.toString(),
|
||||
transaction_id: this.transactionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this._promise !== null) {
|
||||
this._reject(e);
|
||||
} else {
|
||||
this._promise = Promise.reject(e);
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
// before calling verify()
|
||||
this.emit('cancel', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin the key verification
|
||||
*
|
||||
* @returns {Promise} Promise which resolves when the verification has
|
||||
* completed.
|
||||
*/
|
||||
verify() {
|
||||
if (this._promise) return this._promise;
|
||||
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = (...args) => {
|
||||
this._done = true;
|
||||
resolve(...args);
|
||||
};
|
||||
this._reject = (...args) => {
|
||||
this._done = true;
|
||||
reject(...args);
|
||||
};
|
||||
});
|
||||
if (this._doVerification && !this._started) {
|
||||
this._started = true;
|
||||
Promise.resolve(this._doVerification())
|
||||
.then(this.done.bind(this), this.cancel.bind(this));
|
||||
}
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
async _verifyKeys(userId, keys, verifier) {
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = await this._baseApis.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
throw new Error(`Could not find device ${deviceId}`);
|
||||
} else {
|
||||
await verifier(keyId, device, keyInfo);
|
||||
}
|
||||
}
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error messages.
|
||||
*
|
||||
* @module crypto/verification/Error
|
||||
*/
|
||||
|
||||
import {MatrixEvent} from "../../models/event";
|
||||
|
||||
export function newVerificationError(code, reason, extradata) {
|
||||
extradata = extradata || {};
|
||||
extradata.code = code;
|
||||
extradata.reason = reason;
|
||||
return new MatrixEvent({
|
||||
type: "m.key.verification.cancel",
|
||||
content: extradata,
|
||||
});
|
||||
}
|
||||
|
||||
export function errorFactory(code, reason) {
|
||||
return function(extradata) {
|
||||
return newVerificationError(code, reason, extradata);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The verification was cancelled by the user.
|
||||
*/
|
||||
export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
|
||||
|
||||
/**
|
||||
* The verification timed out.
|
||||
*/
|
||||
export const newTimeoutError = errorFactory("m.timeout", "Timed out");
|
||||
|
||||
/**
|
||||
* The transaction is unknown.
|
||||
*/
|
||||
export const newUnknownTransactionError = errorFactory(
|
||||
"m.unknown_transaction", "Unknown transaction",
|
||||
);
|
||||
|
||||
/**
|
||||
* An unknown method was selected.
|
||||
*/
|
||||
export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
|
||||
|
||||
/**
|
||||
* An unexpected message was sent.
|
||||
*/
|
||||
export const newUnexpectedMessageError = errorFactory(
|
||||
"m.unexpected_message", "Unexpected message",
|
||||
);
|
||||
|
||||
/**
|
||||
* The key does not match.
|
||||
*/
|
||||
export const newKeyMismatchError = errorFactory(
|
||||
"m.key_mismatch", "Key mismatch",
|
||||
);
|
||||
|
||||
/**
|
||||
* The user does not match.
|
||||
*/
|
||||
export const newUserMismatchError = errorFactory("m.user_error", "User mismatch");
|
||||
|
||||
/**
|
||||
* An invalid message was sent.
|
||||
*/
|
||||
export const newInvalidMessageError = errorFactory(
|
||||
"m.invalid_message", "Invalid message",
|
||||
);
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* QR code key verification.
|
||||
* @module crypto/verification/QRCode
|
||||
*/
|
||||
|
||||
import Base from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newUserCancelledError,
|
||||
newKeyMismatchError,
|
||||
newUserMismatchError,
|
||||
} from './Error';
|
||||
|
||||
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
|
||||
const KEY_REGEXP = /^key_([^:]+:.+)$/;
|
||||
|
||||
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ShowQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ShowQRCode extends Base {
|
||||
_doVerification() {
|
||||
if (!this._done) {
|
||||
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
|
||||
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
|
||||
+ "&action=verify&key_ed25519%3A"
|
||||
+ encodeURIComponent(this._baseApis.deviceId) + "="
|
||||
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
|
||||
this.emit("show_qr_code", {
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowQRCode.NAME = "m.qr_code.show.v1";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ScanQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ScanQRCode extends Base {
|
||||
static factory(...args) {
|
||||
return new ScanQRCode(...args);
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
const code = await new Promise((resolve, reject) => {
|
||||
this.emit("scan", {
|
||||
done: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
});
|
||||
});
|
||||
|
||||
const match = code.match(MATRIXTO_REGEXP);
|
||||
let deviceId;
|
||||
const keys = {};
|
||||
if (!match) {
|
||||
throw newQRCodeError();
|
||||
}
|
||||
const userId = match[1];
|
||||
const params = match[2].split("&").map(
|
||||
(x) => x.split("=", 2).map(decodeURIComponent),
|
||||
);
|
||||
let action;
|
||||
for (const [name, value] of params) {
|
||||
if (name === "device") {
|
||||
deviceId = value;
|
||||
} else if (name === "action") {
|
||||
action = value;
|
||||
} else {
|
||||
const keyMatch = name.match(KEY_REGEXP);
|
||||
if (keyMatch) {
|
||||
keys[keyMatch[1]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
|
||||
throw newQRCodeError();
|
||||
}
|
||||
|
||||
if (!this.userId) {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.emit("confirm_user_id", {
|
||||
userId: userId,
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserMismatchError()),
|
||||
});
|
||||
});
|
||||
} else if (this.userId !== userId) {
|
||||
throw newUserMismatchError({
|
||||
expected: this.userId,
|
||||
actual: userId,
|
||||
});
|
||||
}
|
||||
|
||||
await this._verifyKeys(userId, keys, (keyId, device, key) => {
|
||||
if (device.keys[keyId] !== key) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ScanQRCode.NAME = "m.qr_code.scan.v1";
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Short Authentication String (SAS) verification.
|
||||
* @module crypto/verification/SAS
|
||||
*/
|
||||
|
||||
import Base from "./Base";
|
||||
import anotherjson from 'another-json';
|
||||
import {
|
||||
errorFactory,
|
||||
newUserCancelledError,
|
||||
newUnknownMethodError,
|
||||
newKeyMismatchError,
|
||||
newInvalidMessageError,
|
||||
} from './Error';
|
||||
|
||||
const EVENTS = [
|
||||
"m.key.verification.accept",
|
||||
"m.key.verification.key",
|
||||
"m.key.verification.mac",
|
||||
];
|
||||
|
||||
let olmutil;
|
||||
|
||||
const newMismatchedSASError = errorFactory(
|
||||
"m.mismatched_sas", "Mismatched short authentication string",
|
||||
);
|
||||
|
||||
const newMismatchedCommitmentError = errorFactory(
|
||||
"m.mismatched_commitment", "Mismatched commitment",
|
||||
);
|
||||
|
||||
function generateDecimalSas(sasBytes) {
|
||||
/**
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* bits: 87654321 87654321 87654321 87654321 87654321
|
||||
* \____________/\_____________/\____________/
|
||||
* 1st number 2nd number 3rd number
|
||||
*/
|
||||
return [
|
||||
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
|
||||
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
|
||||
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
|
||||
];
|
||||
}
|
||||
|
||||
const emojiMapping = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐧", "penguin"], // 10
|
||||
["🐢", "turtle"], // 11
|
||||
["🐟", "fish"], // 12
|
||||
["🐙", "octopus"], // 13
|
||||
["🦋", "butterfly"], // 14
|
||||
["🌷", "flower"], // 15
|
||||
["🌳", "tree"], // 16
|
||||
["🌵", "cactus"], // 17
|
||||
["🍄", "mushroom"], // 18
|
||||
["🌏", "globe"], // 19
|
||||
["🌙", "moon"], // 20
|
||||
["☁️", "cloud"], // 21
|
||||
["🔥", "fire"], // 22
|
||||
["🍌", "banana"], // 23
|
||||
["🍎", "apple"], // 24
|
||||
["🍓", "strawberry"], // 25
|
||||
["🌽", "corn"], // 26
|
||||
["🍕", "pizza"], // 27
|
||||
["🎂", "cake"], // 28
|
||||
["❤️", "heart"], // 29
|
||||
["🙂", "smiley"], // 30
|
||||
["🤖", "robot"], // 31
|
||||
["🎩", "hat"], // 32
|
||||
["👓", "glasses"], // 33
|
||||
["🔧", "spanner"], // 34
|
||||
["🎅", "santa"], // 35
|
||||
["👍", "thumbs up"], // 36
|
||||
["☂️", "umbrella"], // 37
|
||||
["⌛", "hourglass"], // 38
|
||||
["⏰", "clock"], // 39
|
||||
["🎁", "gift"], // 40
|
||||
["💡", "light bulb"], // 41
|
||||
["📕", "book"], // 42
|
||||
["✏️", "pencil"], // 43
|
||||
["📎", "paperclip"], // 44
|
||||
["✂️", "scissors"], // 45
|
||||
["🔒", "padlock"], // 46
|
||||
["🔑", "key"], // 47
|
||||
["🔨", "hammer"], // 48
|
||||
["☎️", "telephone"], // 49
|
||||
["🏁", "flag"], // 50
|
||||
["🚂", "train"], // 51
|
||||
["🚲", "bicycle"], // 52
|
||||
["✈️", "aeroplane"], // 53
|
||||
["🚀", "rocket"], // 54
|
||||
["🏆", "trophy"], // 55
|
||||
["⚽", "ball"], // 56
|
||||
["🎸", "guitar"], // 57
|
||||
["🎺", "trumpet"], // 58
|
||||
["🔔", "bell"], // 59
|
||||
["⚓️", "anchor"], // 60
|
||||
["🎧", "headphones"], // 61
|
||||
["📁", "folder"], // 62
|
||||
["📌", "pin"], // 63
|
||||
];
|
||||
|
||||
function generateEmojiSas(sasBytes) {
|
||||
const emojis = [
|
||||
// just like base64 encoding
|
||||
sasBytes[0] >> 2,
|
||||
(sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4,
|
||||
(sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6,
|
||||
sasBytes[2] & 0x3f,
|
||||
sasBytes[3] >> 2,
|
||||
(sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4,
|
||||
(sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6,
|
||||
];
|
||||
|
||||
return emojis.map((num) => emojiMapping[num]);
|
||||
}
|
||||
|
||||
function generateSas(sasBytes, methods) {
|
||||
const sas = {};
|
||||
if (methods.includes("decimal")) {
|
||||
sas["decimal"] = generateDecimalSas(sasBytes);
|
||||
}
|
||||
if (methods.includes("emoji")) {
|
||||
sas["emoji"] = generateEmojiSas(sasBytes);
|
||||
}
|
||||
return sas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export default class SAS extends Base {
|
||||
get events() {
|
||||
return EVENTS;
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
await global.Olm.init();
|
||||
olmutil = olmutil || new global.Olm.Utility();
|
||||
|
||||
// make sure user's keys are downloaded
|
||||
await this._baseApis.downloadKeys([this.userId]);
|
||||
|
||||
if (this.startEvent) {
|
||||
return await this._doRespondVerification();
|
||||
} else {
|
||||
return await this._doSendVerification();
|
||||
}
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
const initialMessage = {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
key_agreement_protocols: ["curve25519"],
|
||||
hashes: ["sha256"],
|
||||
message_authentication_codes: ["hmac-sha256"],
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
transaction_id: this.transactionId,
|
||||
};
|
||||
this._sendToDevice("m.key.verification.start", initialMessage);
|
||||
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.accept");
|
||||
let content = e.getContent();
|
||||
if (!(content.key_agreement_protocol === "curve25519"
|
||||
&& content.hash === "sha256"
|
||||
&& content.message_authentication_code === "hmac-sha256"
|
||||
&& content.short_authentication_string instanceof Array
|
||||
&& (content.short_authentication_string.includes("decimal")
|
||||
|| content.short_authentication_string.includes("emoji")))) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
if (typeof content.commitment !== "string") {
|
||||
throw newInvalidMessageError();
|
||||
}
|
||||
const hashCommitment = content.commitment;
|
||||
const sasMethods = content.short_authentication_string;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
|
||||
e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
|
||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||
throw newMismatchedCommitmentError();
|
||||
}
|
||||
olmSAS.set_their_key(content.key);
|
||||
|
||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.emit("show_sas", {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS);
|
||||
resolve();
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac"),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
async _doRespondVerification() {
|
||||
let content = this.startEvent.getContent();
|
||||
if (!(content.key_agreement_protocols instanceof Array
|
||||
&& content.key_agreement_protocols.includes("curve25519")
|
||||
&& content.hashes instanceof Array
|
||||
&& content.hashes.includes("sha256")
|
||||
&& content.message_authentication_codes instanceof Array
|
||||
&& content.message_authentication_codes.includes("hmac-sha256")
|
||||
&& content.short_authentication_string instanceof Array
|
||||
&& (content.short_authentication_string.includes("decimal")
|
||||
|| content.short_authentication_string.includes("emoji")))) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const sasMethods = [];
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
if (content.short_authentication_string.includes("decimal")) {
|
||||
sasMethods.push("decimal");
|
||||
}
|
||||
if (content.short_authentication_string.includes("emoji")) {
|
||||
sasMethods.push("emoji");
|
||||
}
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._sendToDevice("m.key.verification.accept", {
|
||||
key_agreement_protocol: "curve25519",
|
||||
hash: "sha256",
|
||||
message_authentication_code: "hmac-sha256",
|
||||
short_authentication_string: sasMethods,
|
||||
commitment: olmutil.sha256(commitmentStr),
|
||||
});
|
||||
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
olmSAS.set_their_key(content.key);
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.emit("show_sas", {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS);
|
||||
resolve();
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac"),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
_sendMAC(olmSAS) {
|
||||
const keyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
const mac = {};
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
mac[keyId] = olmSAS.calculate_mac(
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + keyId,
|
||||
);
|
||||
const keys = olmSAS.calculate_mac(
|
||||
keyId,
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._sendToDevice("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content) {
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
if (content.keys !== olmSAS.calculate_mac(
|
||||
Object.keys(content.mac).sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
)) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
if (keyInfo !== olmSAS.calculate_mac(
|
||||
device.keys[keyId],
|
||||
baseInfo + keyId,
|
||||
)) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SAS.NAME = "m.sas.v1";
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2019 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 Promise from 'bluebird';
|
||||
|
||||
/**
|
||||
* Check if an IndexedDB database exists. The only way to do so is to try opening it, so
|
||||
* we do that and then delete it did not exist before.
|
||||
*
|
||||
* @param {Object} indexedDB The `indexedDB` interface
|
||||
* @param {string} dbName The database name to test for
|
||||
* @returns {boolean} Whether the database exists
|
||||
*/
|
||||
export function exists(indexedDB, dbName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let exists = true;
|
||||
const req = indexedDB.open(dbName);
|
||||
req.onupgradeneeded = () => {
|
||||
// Since we did not provide an explicit version when opening, this event
|
||||
// should only fire if the DB did not exist before at any version.
|
||||
exists = false;
|
||||
};
|
||||
req.onblocked = () => reject();
|
||||
req.onsuccess = () => {
|
||||
const db = req.result;
|
||||
db.close();
|
||||
if (!exists) {
|
||||
// The DB did not exist before, but has been created as part of this
|
||||
// existence check. Delete it now to restore previous state. Delete can
|
||||
// actually take a while to complete in some browsers, so don't wait for
|
||||
// it. This won't block future open calls that a store might issue next to
|
||||
// properly set up the DB.
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
}
|
||||
resolve(exists);
|
||||
};
|
||||
req.onerror = ev => reject(ev.target.error);
|
||||
});
|
||||
}
|
||||
+15
-7
@@ -22,8 +22,14 @@ module.exports.ContentHelpers = require("./content-helpers");
|
||||
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
|
||||
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
|
||||
module.exports.EventStatus = require("./models/event").EventStatus;
|
||||
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
|
||||
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
|
||||
/** The {@link module:store/memory.MemoryStore|MemoryStore} class. */
|
||||
module.exports.MemoryStore = require("./store/memory").MemoryStore;
|
||||
/**
|
||||
* The {@link module:store/memory.MemoryStore|MemoryStore} class was previously
|
||||
* exported as `MatrixInMemoryStore`, so this is preserved for SDK consumers.
|
||||
* @deprecated Prefer `MemoryStore` going forward.
|
||||
*/
|
||||
module.exports.MatrixInMemoryStore = module.exports.MemoryStore;
|
||||
/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */
|
||||
module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore;
|
||||
/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */
|
||||
@@ -67,6 +73,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 =
|
||||
@@ -86,21 +94,21 @@ module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCal
|
||||
|
||||
|
||||
/**
|
||||
* Set an audio output device to use for MatrixCalls
|
||||
* Set a preferred audio output device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* Set a preferred audio input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
|
||||
/**
|
||||
* Set a video input device to use for MatrixCalls
|
||||
* Set a preferred video input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
@@ -162,7 +170,7 @@ module.exports.setCryptoStoreFactory = function(fac) {
|
||||
* this is a string, it is assumed to be the base URL. These configuration
|
||||
* options will be passed directly to {@link module:client~MatrixClient}.
|
||||
* @param {Object} opts.store If not set, defaults to
|
||||
* {@link module:store/memory.MatrixInMemoryStore}.
|
||||
* {@link module:store/memory.MemoryStore}.
|
||||
* @param {Object} opts.scheduler If not set, defaults to
|
||||
* {@link module:scheduler~MatrixScheduler}.
|
||||
* @param {requestFunction} opts.request If not set, defaults to the function
|
||||
@@ -185,7 +193,7 @@ module.exports.createClient = function(opts) {
|
||||
};
|
||||
}
|
||||
opts.request = opts.request || request;
|
||||
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
|
||||
opts.store = opts.store || new module.exports.MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
|
||||
|
||||
@@ -408,8 +408,36 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
console.info("Already have timeline for " + eventId +
|
||||
" - joining timeline " + timeline + " to " +
|
||||
existingTimeline);
|
||||
|
||||
// Variables to keep the line length limited below.
|
||||
const existingIsLive = existingTimeline === this._liveTimeline;
|
||||
const timelineIsLive = timeline === this._liveTimeline;
|
||||
|
||||
const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
|
||||
const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
|
||||
|
||||
if (backwardsIsLive || forwardsIsLive) {
|
||||
// The live timeline should never be spliced into a non-live position.
|
||||
// We use independent logging to better discover the problem at a glance.
|
||||
console.warn({backwardsIsLive, forwardsIsLive}); // debugging
|
||||
if (backwardsIsLive) {
|
||||
console.warn(
|
||||
"Refusing to set a preceding existingTimeLine on our " +
|
||||
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
|
||||
);
|
||||
}
|
||||
if (forwardsIsLive) {
|
||||
console.warn(
|
||||
"Refusing to set our preceding timeline on a existingTimeLine " +
|
||||
"as our timeline is live (" + timeline + ")",
|
||||
);
|
||||
}
|
||||
continue; // abort splicing - try next event
|
||||
}
|
||||
|
||||
timeline.setNeighbouringTimeline(existingTimeline, direction);
|
||||
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
||||
|
||||
timeline = existingTimeline;
|
||||
didUpdate = true;
|
||||
}
|
||||
@@ -418,6 +446,14 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
// new information, we update the pagination token for whatever
|
||||
// timeline we ended up on.
|
||||
if (lastEventWasNew || !didUpdate) {
|
||||
if (direction === EventTimeline.FORWARDS && timeline === this._liveTimeline) {
|
||||
console.warn({lastEventWasNew, didUpdate}); // for debugging
|
||||
console.warn(
|
||||
`Refusing to set forwards pagination token of live timeline ` +
|
||||
`${timeline} to ${paginationToken}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
timeline.setPaginationToken(paginationToken, direction);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -276,7 +276,7 @@ EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
|
||||
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
|
||||
if (this.getNeighbouringTimeline(direction)) {
|
||||
throw new Error("timeline already has a neighbouring timeline - " +
|
||||
"cannot reset neighbour");
|
||||
"cannot reset neighbour (direction: " + direction + ")");
|
||||
}
|
||||
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
|
||||
+48
-3
@@ -382,15 +382,41 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* Cancel any room key request for this event and resend another.
|
||||
*
|
||||
* @param {module:crypto} crypto crypto module
|
||||
* @param {string} userId the user who received this event
|
||||
*
|
||||
* @returns {Promise} a promise that resolves when the request is queued
|
||||
*/
|
||||
cancelAndResendKeyRequest: function(crypto) {
|
||||
cancelAndResendKeyRequest: function(crypto, userId) {
|
||||
const wireContent = this.getWireContent();
|
||||
crypto.cancelRoomKeyRequest({
|
||||
return crypto.requestRoomKey({
|
||||
algorithm: wireContent.algorithm,
|
||||
room_id: this.getRoomId(),
|
||||
session_id: wireContent.session_id,
|
||||
sender_key: wireContent.sender_key,
|
||||
}, true);
|
||||
}, this.getKeyRequestRecipients(userId), true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate the recipients for keyshare requests.
|
||||
*
|
||||
* @param {string} userId the user who received this event.
|
||||
*
|
||||
* @returns {Array} array of recipients
|
||||
*/
|
||||
getKeyRequestRecipients: function(userId) {
|
||||
// send the request to all of our own devices, and the
|
||||
// original sending device if it wasn't us.
|
||||
const wireContent = this.getWireContent();
|
||||
const recipients = [{
|
||||
userId, deviceId: '*',
|
||||
}];
|
||||
const sender = this.getSender();
|
||||
if (sender !== userId) {
|
||||
recipients.push({
|
||||
userId: sender, deviceId: wireContent.device_id,
|
||||
});
|
||||
}
|
||||
return recipients;
|
||||
},
|
||||
|
||||
_decryptionLoop: async function(crypto) {
|
||||
@@ -471,6 +497,14 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
this._retryDecryption = false;
|
||||
this._setClearData(res);
|
||||
|
||||
// Before we emit the event, clear the push actions so that they can be recalculated
|
||||
// by relevant code. We do this because the clear event has now changed, making it
|
||||
// so that existing rules can be re-run over the applicable properties. Stuff like
|
||||
// highlighting when the user's name is mentioned rely on this happening. We also want
|
||||
// to set the push actions before emitting so that any notification listeners don't
|
||||
// pick up the wrong contents.
|
||||
this.setPushActions(null);
|
||||
|
||||
this.emit("Event.decrypted", this, err);
|
||||
|
||||
return;
|
||||
@@ -511,6 +545,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the cleartext content for this event. If the event is not encrypted,
|
||||
* or encryption has not been completed, this will return null.
|
||||
*
|
||||
* @returns {Object} The cleartext (decrypted) content for the event
|
||||
*/
|
||||
getClearContent: function() {
|
||||
const ev = this._clearEvent;
|
||||
return ev && ev.content ? ev.content : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the event is encrypted.
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
|
||||
@@ -298,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -519,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)] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+134
-6
@@ -31,7 +31,14 @@ const EventTimelineSet = require("./event-timeline-set");
|
||||
|
||||
import ReEmitter from '../ReEmitter';
|
||||
|
||||
const LATEST_ROOM_VERSION = '1';
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
|
||||
// room versions which are considered okay for people to run without being asked
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '1';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -211,12 +218,81 @@ Room.prototype.getVersion = function() {
|
||||
* Determines whether this room needs to be upgraded to a new version
|
||||
* @returns {string?} What version the room should be upgraded to, or null if
|
||||
* the room does not require upgrading at this time.
|
||||
* @deprecated Use #getRecommendedVersion() instead
|
||||
*/
|
||||
Room.prototype.shouldUpgradeToVersion = function() {
|
||||
// This almost certainly won't be the way this actually works - this
|
||||
// is essentially a stub method.
|
||||
if (this.getVersion() === LATEST_ROOM_VERSION) return null;
|
||||
return LATEST_ROOM_VERSION;
|
||||
// TODO: Remove this function.
|
||||
// This makes assumptions about which versions are safe, and can easily
|
||||
// be wrong. Instead, people are encouraged to use getRecommendedVersion
|
||||
// which determines a safer value. This function doesn't use that function
|
||||
// because this is not async-capable, and to avoid breaking the contract
|
||||
// we're deprecating this.
|
||||
|
||||
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
|
||||
return KNOWN_SAFE_ROOM_VERSION;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the recommended room version for the room. This returns an
|
||||
* object with 3 properties: <code>version</code> as the new version the
|
||||
* room should be upgraded to (may be the same as the current version);
|
||||
* <code>needsUpgrade</code> to indicate if the room actually can be
|
||||
* upgraded (ie: does the current version not match?); and <code>urgent</code>
|
||||
* to indicate if the new version patches a vulnerability in a previous
|
||||
* version.
|
||||
* @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>}
|
||||
* Resolves to the version the room should be upgraded to.
|
||||
*/
|
||||
Room.prototype.getRecommendedVersion = async function() {
|
||||
const capabilities = await this._client.getCapabilities();
|
||||
let versionCap = capabilities["m.room_versions"];
|
||||
if (!versionCap) {
|
||||
versionCap = {
|
||||
default: KNOWN_SAFE_ROOM_VERSION,
|
||||
available: {},
|
||||
};
|
||||
for (const safeVer of SAFE_ROOM_VERSIONS) {
|
||||
versionCap.available[safeVer] = "stable";
|
||||
}
|
||||
}
|
||||
|
||||
const currentVersion = this.getVersion();
|
||||
console.log(`[${this.roomId}] Current version: ${currentVersion}`);
|
||||
console.log(`[${this.roomId}] Version capability: `, versionCap);
|
||||
|
||||
const result = {
|
||||
version: currentVersion,
|
||||
needsUpgrade: false,
|
||||
urgent: false,
|
||||
};
|
||||
|
||||
// If the room is on the default version then nothing needs to change
|
||||
if (currentVersion === versionCap.default) return Promise.resolve(result);
|
||||
|
||||
const stableVersions = Object.keys(versionCap.available)
|
||||
.filter((v) => versionCap.available[v] === 'stable');
|
||||
|
||||
// Check if the room is on an unstable version. We determine urgency based
|
||||
// off the version being in the Matrix spec namespace or not (if the version
|
||||
// is in the current namespace and unstable, the room is probably vulnerable).
|
||||
if (!stableVersions.includes(currentVersion)) {
|
||||
result.version = versionCap.default;
|
||||
result.needsUpgrade = true;
|
||||
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
|
||||
if (result.urgent) {
|
||||
console.warn(`URGENT upgrade required on ${this.roomId}`);
|
||||
} else {
|
||||
console.warn(`Non-urgent upgrade required on ${this.roomId}`);
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
// The room is on a stable, but non-default, version by this point.
|
||||
// No upgrade needed.
|
||||
return Promise.resolve(result);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -520,6 +596,24 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
|
||||
.getState(EventTimeline.FORWARDS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether there are any devices in the room that are unverified
|
||||
* @return {bool} the result
|
||||
*/
|
||||
Room.prototype.hasUnverifiedDevices = async function() {
|
||||
if (!this._client.isRoomEncrypted(this.roomId)) {
|
||||
return false;
|
||||
}
|
||||
const e2eMembers = await this.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const devices = await this._client.getStoredDevicesForUser(member.userId);
|
||||
if (devices.some((device) => device.isUnverified())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the timeline sets for this room.
|
||||
* @return {EventTimelineSet[]} array of timeline sets for this room
|
||||
@@ -1167,7 +1261,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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1337,6 +1431,40 @@ Room.prototype.getEventReadUpTo = function(userId, ignoreSynthesized) {
|
||||
return receipts["m.read"][userId].eventId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the given user has read a particular event ID with the known
|
||||
* history of the room. This is not a definitive check as it relies only on
|
||||
* what is available to the room at the time of execution.
|
||||
* @param {String} userId The user ID to check the read state of.
|
||||
* @param {String} eventId The event ID to check if the user read.
|
||||
* @returns {Boolean} True if the user has read the event, false otherwise.
|
||||
*/
|
||||
Room.prototype.hasUserReadEvent = function(userId, eventId) {
|
||||
const readUpToId = this.getEventReadUpTo(userId, false);
|
||||
if (readUpToId === eventId) return true;
|
||||
|
||||
if (this.timeline.length
|
||||
&& this.timeline[this.timeline.length - 1].getSender()
|
||||
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
|
||||
// It doesn't matter where the event is in the timeline, the user has read
|
||||
// it because they've sent the latest event.
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = this.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = this.timeline[i];
|
||||
|
||||
// If we encounter the target event first, the user hasn't read it
|
||||
// however if we encounter the readUpToId first then the user has read
|
||||
// it. These rules apply because we're iterating bottom-up.
|
||||
if (ev.getId() === eventId) return false;
|
||||
if (ev.getId() === readUpToId) return true;
|
||||
}
|
||||
|
||||
// We don't know if the user has read it, so assume not.
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of receipts for the given event.
|
||||
* @param {MatrixEvent} event the event to get receipts for
|
||||
|
||||
@@ -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,18 @@ 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.
|
||||
* @fires module:client~MatrixClient#event:"User._unstable_statusMessage"
|
||||
*/
|
||||
User.prototype._unstable_updateStatusMessage = function(event) {
|
||||
if (!event.getContent()) this._unstable_statusMessage = "";
|
||||
else this._unstable_statusMessage = event.getContent()["status"];
|
||||
this._updateModifiedTime();
|
||||
this.emit("User._unstable_statusMessage", this);
|
||||
};
|
||||
|
||||
/**
|
||||
* The User class.
|
||||
*/
|
||||
|
||||
+7
-21
@@ -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,
|
||||
};
|
||||
@@ -185,7 +184,10 @@ function PushProcessor(client) {
|
||||
};
|
||||
|
||||
const eventFulfillsDisplayNameCondition = function(cond, ev) {
|
||||
const content = ev.getContent();
|
||||
let content = ev.getContent();
|
||||
if (ev.isEncrypted() && ev.getClearContent()) {
|
||||
content = ev.getClearContent();
|
||||
}
|
||||
if (!content || !content.body || typeof content.body != 'string') {
|
||||
return false;
|
||||
}
|
||||
@@ -244,22 +246,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;
|
||||
|
||||
@@ -18,6 +18,7 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
import SyncAccumulator from "../sync-accumulator";
|
||||
import utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
|
||||
const VERSION = 3;
|
||||
|
||||
@@ -132,6 +133,10 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
|
||||
this._isNewlyCreated = false;
|
||||
};
|
||||
|
||||
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
|
||||
dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
};
|
||||
|
||||
LocalIndexedDBStoreBackend.prototype = {
|
||||
/**
|
||||
|
||||
+11
-7
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import {MatrixInMemoryStore} from "./memory";
|
||||
import {MemoryStore} from "./memory";
|
||||
import utils from "../utils";
|
||||
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
|
||||
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
|
||||
@@ -37,9 +37,9 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new Indexed Database store, which extends MatrixInMemoryStore.
|
||||
* Construct a new Indexed Database store, which extends MemoryStore.
|
||||
*
|
||||
* This store functions like a MatrixInMemoryStore except it periodically persists
|
||||
* This store functions like a MemoryStore except it periodically persists
|
||||
* the contents of the store to an IndexedDB backend.
|
||||
*
|
||||
* All data is still kept in-memory but can be loaded from disk by calling
|
||||
@@ -62,7 +62,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
||||
* </pre>
|
||||
*
|
||||
* @constructor
|
||||
* @extends MatrixInMemoryStore
|
||||
* @extends MemoryStore
|
||||
* @param {Object} opts Options object.
|
||||
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
|
||||
* <code>window.indexedDB</code>
|
||||
@@ -79,7 +79,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
||||
* database.
|
||||
*/
|
||||
const IndexedDBStore = function IndexedDBStore(opts) {
|
||||
MatrixInMemoryStore.call(this, opts);
|
||||
MemoryStore.call(this, opts);
|
||||
|
||||
if (!opts.indexedDB) {
|
||||
throw new Error('Missing required option: indexedDB');
|
||||
@@ -109,7 +109,11 @@ const IndexedDBStore = function IndexedDBStore(opts) {
|
||||
// user_id : timestamp
|
||||
};
|
||||
};
|
||||
utils.inherits(IndexedDBStore, MatrixInMemoryStore);
|
||||
utils.inherits(IndexedDBStore, MemoryStore);
|
||||
|
||||
IndexedDBStore.exists = function(indexedDB, dbName) {
|
||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {Promise} Resolved when loaded from indexed db.
|
||||
@@ -164,7 +168,7 @@ IndexedDBStore.prototype.getSavedSyncToken = function() {
|
||||
* @return {Promise} Resolves if the data was deleted from the database.
|
||||
*/
|
||||
IndexedDBStore.prototype.deleteAllData = function() {
|
||||
MatrixInMemoryStore.prototype.deleteAllData.call(this);
|
||||
MemoryStore.prototype.deleteAllData.call(this);
|
||||
return this.backend.clearDatabase().then(() => {
|
||||
console.log("Deleted indexeddb data.");
|
||||
}, (err) => {
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
|
||||
* This is an internal module. See {@link MemoryStore} for the public class.
|
||||
* @module store/memory
|
||||
*/
|
||||
const utils = require("../utils");
|
||||
@@ -31,7 +31,7 @@ import Promise from 'bluebird';
|
||||
* @param {LocalStorage} opts.localStorage The local storage instance to persist
|
||||
* some forms of data such as tokens. Rooms will NOT be stored.
|
||||
*/
|
||||
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
module.exports.MemoryStore = function MemoryStore(opts) {
|
||||
opts = opts || {};
|
||||
this.rooms = {
|
||||
// roomId: Room
|
||||
@@ -58,7 +58,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
this._clientOptions = {};
|
||||
};
|
||||
|
||||
module.exports.MatrixInMemoryStore.prototype = {
|
||||
module.exports.MemoryStore.prototype = {
|
||||
|
||||
/**
|
||||
* Retrieve the token to stream from.
|
||||
|
||||
@@ -190,11 +190,22 @@ WebStorageSessionStore.prototype = {
|
||||
removeAllEndToEndRooms: function() {
|
||||
removeByPrefix(this.store, keyEndToEndRoom(''));
|
||||
},
|
||||
|
||||
setLocalTrustedBackupPubKey: function(pubkey) {
|
||||
this.store.setItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY, pubkey);
|
||||
},
|
||||
|
||||
// XXX: This store is deprecated really, but added this as a temporary
|
||||
// thing until cross-signing lands.
|
||||
getLocalTrustedBackupPubKey: function() {
|
||||
return this.store.getItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY);
|
||||
},
|
||||
};
|
||||
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
|
||||
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
|
||||
const KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY = E2E_PREFIX + "trusted_backup_pubkey";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
@@ -724,6 +724,9 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
||||
// log the exception with stack if we have it, else fall back
|
||||
// to the plain description
|
||||
console.error("Caught /sync error", e.stack || e);
|
||||
|
||||
// Emit the exception for client handling
|
||||
this.client.emit("sync.unexpectedError", e);
|
||||
}
|
||||
|
||||
// update this as it may have changed
|
||||
@@ -1172,6 +1175,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);
|
||||
|
||||
+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;
|
||||
};
|
||||
|
||||
+5
-5
@@ -583,7 +583,7 @@ MatrixCall.prototype._maybeGotUserMediaForInvite = function(stream) {
|
||||
' Or possibly you are using an insecure domain. Receiving only.');
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
} else {
|
||||
debuglog('Failed to getUserMedia.');
|
||||
debuglog('Failed to getUserMedia: ' + error.name);
|
||||
this._getUserMediaFailed(error);
|
||||
return;
|
||||
}
|
||||
@@ -652,7 +652,7 @@ MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
|
||||
debuglog('User denied access to camera/microphone.' +
|
||||
' Or possibly you are using an insecure domain. Receiving only.');
|
||||
} else {
|
||||
debuglog('Failed to getUserMedia.');
|
||||
debuglog('Failed to getUserMedia: ' + error.name);
|
||||
this._getUserMediaFailed(error);
|
||||
return;
|
||||
}
|
||||
@@ -1274,15 +1274,15 @@ const _getUserMediaVideoContraints = function(callType) {
|
||||
case 'voice':
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {exact: audioInput} : undefined,
|
||||
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
||||
}, video: false,
|
||||
};
|
||||
case 'video':
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {exact: audioInput} : undefined,
|
||||
deviceId: audioInput ? {ideal: audioInput} : undefined,
|
||||
}, video: {
|
||||
deviceId: videoInput ? {exact: videoInput} : undefined,
|
||||
deviceId: videoInput ? {ideal: videoInput} : undefined,
|
||||
/* We want 640x360. Chrome will give it only if we ask exactly,
|
||||
FF refuses entirely if we ask exactly, so have to ask for ideal
|
||||
instead */
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
npm run lint
|
||||
|
||||
# install Olm so that we can run the crypto tests.
|
||||
npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz
|
||||
|
||||
npm run test
|
||||
|
||||
npm run gendoc
|
||||
|
||||
Reference in New Issue
Block a user