Compare commits

..

115 Commits

Author SHA1 Message Date
David Baker ef594d52e4 v1.0.2 2019-03-18 13:47:54 +00:00
David Baker 23bbb2f8c6 Prepare changelog for v1.0.2 2019-03-18 13:47:53 +00:00
David Baker 70575f9e33 v1.0.2-rc.1 2019-03-13 14:24:57 +00:00
David Baker 8e16586d84 Merge branch 'develop' into release-v1.0.2 2019-03-13 14:24:26 +00:00
David Baker 6920dfb800 Yarn needs --new-version to create a new version 2019-03-13 14:23:51 +00:00
David Baker 02d93770aa Prepare changelog for v1.0.2-rc.1 2019-03-13 14:20:18 +00:00
David Baker cd124231c5 Merge pull request #858 from jryans/yarn-ci
Use modern Yarn version on Travis CI
2019-03-13 13:22:39 +00:00
J. Ryan Stinnett cd75848882 Use modern Yarn version on Travis CI
Travis CI uses a quite old version of Yarn by default. This adds Yarn's
recommended incantation for using the latest stable version.
2019-03-13 12:28:07 +00:00
J. Ryan Stinnett 1bae15ede9 Recommend using the latest Node LTS version 2019-03-13 10:50:58 +00:00
J. Ryan Stinnett 4570fcaa8a Ignore .npmrc 2019-03-12 12:02:04 +00:00
J. Ryan Stinnett 90670cf1be Merge pull request #856 from jryans/yarn
Switch to `yarn` for dependency management
2019-03-11 13:27:04 +00:00
J. Ryan Stinnett cc86f427d2 Convert Olm to a dev dependency
This gives more natural behavior of downloading Olm by default when working on
this module, so that all tests including crypto are run.
2019-03-08 15:58:08 +00:00
J. Ryan Stinnett 2144791d52 Update scripts and docs to use yarn where appropriate
Most `npm` operations are replaced with `yarn`, which generally has better
behavior. However, steps like publish that write to the NPM registry are left to
`npm`, which currently handles these tasks best.
2019-03-08 15:45:57 +00:00
J. Ryan Stinnett 33aabf44e7 Convert from npm to yarn lock file
The npm lock file was imported into yarn. A yarn install pass was then run to
double-check the lock file for sanity.
2019-03-08 10:47:58 +00:00
Hubert Chathi fc1ea27380 Merge pull request #855 from uhoreg/fix_key_requests
More key request fixes
2019-03-06 14:46:43 -05:00
Hubert Chathi 81946294d8 use lolex to fake the timer 2019-03-06 14:42:52 -05:00
Matthew Hodgson 77270fa78c Merge pull request #851 from matrix-org/travis/e2e-notifs
Calculate encrypted notification counts
2019-03-06 17:11:22 +00:00
Hubert Chathi 9e29289dcc use a different transaction ID when re-sending a key request 2019-03-06 12:02:48 -05:00
Travis Ralston 6198943976 Add a mention that we should be handling gaps in /sync 2019-03-06 09:56:36 -07:00
David Baker 8beb836ccd Merge pull request #854 from matrix-org/dbkr/deps_190206
Update dependencies
2019-03-06 14:08:26 +00:00
David Baker b7c0e39c1a Update dependencies
To get non-vulnerable versions
2019-03-06 13:50:35 +00:00
David Baker 777acae2e5 Merge branch 'master' into develop 2019-03-06 11:17:25 +00:00
David Baker e77389c1ce v1.0.1 2019-03-06 11:10:42 +00:00
David Baker eb24e2e1f1 Prepare changelog for v1.0.1 2019-03-06 11:10:41 +00:00
Hubert Chathi 114244f8bb fix typo 2019-03-06 00:23:01 -05:00
Travis Ralston 54769d9136 Add jsdoc 2019-03-05 14:25:32 -07:00
Travis Ralston 2f2deb5333 Expose the clear event content directly from an event 2019-03-05 14:04:51 -07:00
Travis Ralston 37f106d4af More safely set the push actions for an encrypted event 2019-03-05 14:04:39 -07:00
David Baker 36ee7cdbfc v1.0.1-rc.2 2019-03-05 18:53:16 +00:00
David Baker 2b564498ee Prepare changelog for v1.0.1-rc.2 2019-03-05 18:53:15 +00:00
David Baker 0ddba16fa1 Merge pull request #853 from matrix-org/bwindels/dontswallowcryptotxnerrors
dont swallow txn errors in crypto store
2019-03-05 18:36:15 +00:00
Hubert Chathi 550086eb67 Merge pull request #850 from uhoreg/ensure_key_request
make sure key requests get sent
2019-03-05 10:57:15 -05:00
Hubert Chathi 055ce673cd fix jsdoc 2019-03-05 10:54:02 -05:00
David Baker 5308595658 Merge pull request #852 from matrix-org/dbkr/getusermedia_deviceid_ideal
Use 'ideal' rather than 'exact' for deviceid
2019-03-05 14:23:39 +00:00
David Baker e726e29f39 Better logging here too 2019-03-05 13:02:55 +00:00
David Baker 33b12fa6b5 Use 'ideal' rather than 'exact' for deviceid
We were using 'exact' which means we fail outright if the device
we wanted isn't available. This means if a user selects a specific
device then later unplugs it, we fail to open a capture device
the next time they make a call even if there's one available.
Using 'ideal' uses the chosen device in preference, but something
else if it isn't available.

Also log the name of the exception when we fail to open a capture
device to give us more of an idea of what's gone wrong.

Should help fix https://github.com/vector-im/riot-web/issues/8993
2019-03-05 12:59:29 +00:00
Travis Ralston 829cd05cba Appease the linter 2019-03-04 21:46:48 -07:00
Travis Ralston 4834e12a3a Calculate unread badges for encrypted events 2019-03-04 21:41:20 -07:00
Travis Ralston bcd4ad130c Use the decrypted event content when checking the push rules
Otherwise we'll be looking at the encrypted source, and that doesn't help anyone.
2019-03-04 21:33:57 -07:00
Travis Ralston 998d9e010e Support flushing the cache on calculated push rules
Needed for encrypted events to be able to pass some push rules.
2019-03-04 21:24:25 -07:00
Hubert Chathi 5480e8e1d5 refactor key sharing requests
use sendRoomKeyRequest with a new resend flag, instead of cancelRoomKeyRequest,
when requesting keys, so that we make sure that we send a new request if there
is no previous request

fixes https://github.com/vector-im/riot-web/issues/6838
2019-03-04 17:09:56 -05:00
Hubert Chathi 98fdcabc00 stop client after each test 2019-03-04 16:59:54 -05:00
Bruno Windels 236397816d Merge pull request #849 from matrix-org/bwindels/dontswallowcryptotxnerrors
Don't swallow txn errors in crypto store
2019-03-04 13:05:17 +01:00
Bruno Windels 755c55de3e dont swallow txn errors in crypto store 2019-03-04 12:58:05 +01:00
Hubert Chathi 526da71992 Merge pull request #848 from uhoreg/fix_partial_keyshare
handle partially-shared sessions better
2019-03-01 12:18:54 -05:00
Hubert Chathi 86ef262799 fix c+p comment 2019-03-01 12:15:59 -05:00
Hubert Chathi 282904d4be restore the order of backup vs retry decryption 2019-03-01 08:54:04 -05:00
Hubert Chathi a1be24307a lint 2019-02-28 22:54:46 -05:00
Hubert Chathi 4b5623691b handle partially-shared sessions better
- don't cancel key requests if we can't decrypt everything in the session
- overwrite the session key if we get a better version
2019-02-28 16:01:29 -05:00
David Baker 7bdf1e9b92 v1.0.1-rc.1 2019-02-28 14:28:36 +00:00
David Baker af1db8a606 Prepare changelog for v1.0.1-rc.1 2019-02-28 14:28:36 +00:00
Travis Ralston a99bb3c4c9 Merge pull request #847 from matrix-org/travis/fix-megolm-error
Fix "e is undefined" masking the original error in MegolmDecryption
2019-02-26 13:59:16 -07:00
Travis Ralston fd155c15bd Excessive checks are excessive 2019-02-26 13:50:09 -07:00
Travis Ralston aaa43631aa Fix "e is undefined" masking the original error in MegolmDecryption 2019-02-26 13:15:03 -07:00
Hubert Chathi d2557bc943 only set the dirty flag if something was actually changed 2019-02-22 08:37:34 -05:00
Hubert Chathi 33a3506981 speling is hard 2019-02-15 17:14:29 -05:00
J. Ryan Stinnett 03a54353be v1.0.0 2019-02-14 16:48:12 +00:00
J. Ryan Stinnett c6328923e6 Prepare changelog for v1.0.0 2019-02-14 16:48:11 +00:00
J. Ryan Stinnett 1ecb820bb0 Merge branch 'develop' into release-v1.0.0 2019-02-14 16:46:34 +00:00
J. Ryan Stinnett 0be2319288 Merge pull request #841 from jryans/package-lock-release
Try again to commit package-lock.json
2019-02-14 15:57:21 +00:00
J. Ryan Stinnett 073a025b83 Try again to commit package-lock.json 2019-02-14 14:20:31 +00:00
J. Ryan Stinnett e83836d487 v1.0.0-rc.2 2019-02-14 10:47:41 +00:00
J. Ryan Stinnett 065c61e05c Prepare changelog for v1.0.0-rc.2 2019-02-14 10:47:41 +00:00
J. Ryan Stinnett 139a6bd903 Merge branch 'develop' into release-v1.0.0 2019-02-14 10:43:04 +00:00
J. Ryan Stinnett 3fa0ee59d4 Merge pull request #839 from matrix-org/dbkr/commit_package_lock
Release script: commit package-lock.json
2019-02-13 18:57:38 +00:00
David Baker bd3d26422d git st is not a standard thing 2019-02-13 18:20:16 +00:00
David Baker 370ef9fc69 Merge pull request #840 from matrix-org/dbkr/recheck_key_backup
Add method to force re-check of key backup
2019-02-13 18:19:35 +00:00
David Baker a087fb37a3 Add method to force re-check of key backup
Also detect when the key backup version changes and do the right
thing

https://github.com/vector-im/riot-web/issues/8524
2019-02-13 15:40:07 +00:00
David Baker 68c8fe0fa9 Release script: commit package-lock.json
Commit the package-lock.json when bumping the version, otherwise
the versions get out of sync, and this is going to matter more now
that jenkins runs `npm ci` which is fussy about these things.
2019-02-13 13:40:46 +00:00
Bruno Windels 4309749979 Merge pull request #838 from matrix-org/bwindels/e2eiconsanddialog
Fix: dont check for unverified devices in left members
2019-02-13 11:41:57 +01:00
Bruno Windels 1a677804a4 use getEncryptionTargetMembers instead of doing membership checks ourselves 2019-02-13 11:22:38 +01:00
Bruno Windels a427e2a75c dont check devices for left members 2019-02-12 18:33:44 +01:00
David Baker 3c735b0ac1 v1.0.0-rc.1 2019-02-08 18:33:52 +00:00
David Baker 4f446c3909 Prepare changelog for v1.0.0-rc.1 2019-02-08 18:33:51 +00:00
Hubert Chathi 999ed1b5b3 Merge pull request #837 from uhoreg/emoji_sas
change hex SAS verification to decimal and emoji
2019-02-08 11:44:53 -05:00
David Baker 8fa19f4a0f More en_GBification 2019-02-08 14:56:08 +00:00
David Baker 71a01ec234 Replace symbol characters with the emoji variants
The symbol ones are often rendered as little black & white icons: replace them with their variants from the emoji range

Also change to en_GB
2019-02-08 14:11:47 +00:00
David Baker 32f033a9da Merge pull request #836 from matrix-org/dbkr/trust_on_decrypt
Trust on decrypt
2019-02-08 11:36:44 +00:00
David Baker dade385147 Put error constant on the class 2019-02-08 10:43:17 +00:00
David Baker 6cf2e54f9a Fix double-santa 2019-02-08 09:59:42 +00:00
Hubert Chathi fb673b0304 change hex SAS verification to decimal and emoji 2019-02-08 00:56:30 -05:00
David Baker 1a425af3f2 Pass backup info here too 2019-02-07 15:51:51 +00:00
David Baker 9bafed2c26 OK fine, we'll use the session store 2019-02-07 15:33:49 +00:00
David Baker bb2d0b0f62 lint 2019-02-07 14:47:03 +00:00
David Baker 5e4f10a80c Trust on decrypt
Trust backups that we've restored by saving the matching pubkey
locally.

NB. Contains technically breaking API changes to the backup restore
(takes backupInfo rather than version).
2019-02-07 14:37:25 +00:00
David Baker 9e12fc4d7d Merge pull request #835 from matrix-org/dbkr/always_track_own_device_list
Always track our own devices
2019-02-07 09:30:13 +00:00
David Baker 1caf2b7f83 Always track our own devices
It's generally a reasonable assumption that we'll be interested in
them, and important for key backup.

Fixes https://github.com/vector-im/riot-web/issues/8213
2019-02-06 16:48:57 +00:00
David Baker 6f17e3e659 Merge pull request #834 from matrix-org/dbkr/lint_consistency
Make linting rules more consistent
2019-02-05 13:12:25 +00:00
David Baker 17e2cd755d Make linting rules more consistent
* Put back babel-eslint for class-properties
 * Allow arrow functions without params

This makes the style more consistent with react-sdk.

NB. The line lengths are still inconsistent but it's not clear which
way to go on that yet.
2019-02-05 11:58:53 +00:00
Bruno Windels a6970d4de8 Merge pull request #833 from matrix-org/bwindels/e2eicons
add method to room to check for unverified devices
2019-02-01 18:14:51 +00:00
Bruno Windels bc99c1f3ce add method to room to check for unverified devices 2019-02-01 14:37:11 +01:00
David Baker 5e3ff7fc27 Re-apply changelog that somehow got lost 2019-01-30 13:12:32 +00:00
Bruno Windels ffe3f966fe Merge pull request #831 from matrix-org/experimental
Merge redesign into develop
2019-01-30 12:47:33 +00:00
Travis Ralston c60c19a28e Merge pull request #830 from matrix-org/travis/rver-cap-support
Supporting infrastructure for educated decisions on when to upgrade rooms
2019-01-29 11:28:57 -07:00
Travis Ralston 4ea785b604 Add some prose for what safe versions are 2019-01-29 10:46:40 -07:00
Travis Ralston 2d4e9d0d3f Add safety for when the endpoint doesn't exist 2019-01-28 17:18:57 -07:00
Travis Ralston 971d572fbf Supporting infrastructure for educated decisions on when to upgrade rooms
Part of https://github.com/vector-im/riot-web/issues/8251
2019-01-28 16:03:27 -07:00
Hubert Chathi 244e1b84f7 Initial implementation of key verification 2019-01-23 13:34:25 -05:00
David Baker e5cdc99a34 Merge pull request #826 from matrix-org/dbkr/key_backup_status_unknown_device
Include signature info for unknown devices
2019-01-18 10:58:21 +00:00
David Baker 9a5768219f Doc function API changes 2019-01-18 10:04:22 +00:00
Travis Ralston cee8f57318 Merge pull request #828 from matrix-org/travis/v2-is-safe
Flag v2 rooms as "safe"
2019-01-17 16:36:27 -07:00
Travis Ralston 1a40e0a83a Flag v2 rooms as "safe"
We'll still need something like https://github.com/matrix-org/matrix-doc/pull/1804 to make this work correctly, but this fixes the immediate issue in https://github.com/vector-im/riot-web/issues/8154
2019-01-17 16:14:53 -07:00
Travis Ralston d0072d930f Merge pull request #827 from matrix-org/develop
Develop->Experimental
2019-01-17 15:51:22 -07:00
David Baker 385062c4d7 Include signature info for unknown devices
Add a 'deviceId' property and leave 'device' undefined for unknown
devices.

https://github.com/vector-im/riot-web/issues/8142
2019-01-17 16:43:26 +00:00
J. Ryan Stinnett 9245638b25 Set key backup guard flag to avoid duplicate uploads
Not sure how this was missed, but anyway, guard against duplicate uploads with
the existing flag.
2019-01-17 08:19:09 -06:00
J. Ryan Stinnett 1865542192 Emit when user's status message changes 2019-01-15 08:59:31 -06:00
J. Ryan Stinnett 2563abda11 Ensure we have power to set custom status
If we're in a non-DM room of 2 people, we may not have power to set state events
like custom status.  Ensure that we do before sending.
2019-01-15 08:59:31 -06:00
David Baker c979ff6696 Merge pull request #821 from matrix-org/dbkr/update_eslint
Update ESLint
2019-01-10 09:35:24 +00:00
J. Ryan Stinnett 25681e888c Change initial key backup to background
Alters the APIs used for initial key backup so that the actual upload happens in
the background after all session are marked for backup.
2019-01-09 12:03:55 -06:00
J. Ryan Stinnett 5cfd082b00 Schedule key upload on enabling backup
This ensures a partially completed backup will continue to make progress.
2019-01-09 12:03:55 -06:00
J. Ryan Stinnett 0cbced43bd Emit when count of sessions to backup changes
This will be used in the React SDK to display upload progress when there are
many sessions to upload.
2019-01-09 12:03:55 -06:00
David Baker b3e8d7e07e Remove babel-eslint as we no longer need it 2019-01-09 17:33:42 +00:00
David Baker f4a7395e3a Update ESLint
* Bump version
 * Add a couple of rules to match our existing precedent
 * Fix a few genuine lint errors
 * Ignore a guard-for-in (not sure why eslint doesn't like this?)
 * Update max warnings
2019-01-09 17:29:30 +00:00
J. Ryan Stinnett 14b42abfa4 Re-check key backup status on version mismatch
This ensures we will report the updated status when consumer code asks for it.

Fixes part of https://github.com/vector-im/riot-web/issues/8048.
2019-01-09 04:07:58 -06:00
J. Ryan Stinnett e8022e985e Merge pull request #815 from matrix-org/develop
Merge develop into experimental
2019-01-03 13:54:34 -06:00
43 changed files with 7692 additions and 7001 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
module.exports = {
parser: "babel-eslint", // now needed for class properties
parserOptions: {
ecmaVersion: 2017,
sourceType: "module",
ecmaFeatures: {
}
@@ -72,5 +72,6 @@ module.exports = {
"named": "never",
"asyncArrow": "always",
}],
"arrow-parens": "off",
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
/.jsdoc
node_modules
/.npmrc
.lock-wscript
build/Release
coverage
@@ -12,6 +13,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
+3
View File
@@ -1,5 +1,8 @@
language: node_js
node_js:
- "10.11.0"
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
- export PATH=$HOME/.yarn/bin:$PATH
script:
- ./travis.sh
+92
View File
@@ -1,3 +1,95 @@
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)
+16 -12
View File
@@ -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");
@@ -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
```
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
yarn lint
+8 -6
View File
@@ -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
-6772
View File
File diff suppressed because it is too large Load Diff
+9 -7
View File
@@ -1,24 +1,24 @@
{
"name": "matrix-js-sdk",
"version": "0.14.3",
"version": "1.0.2",
"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 101 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"type": "git",
@@ -66,6 +66,7 @@
},
"devDependencies": {
"babel-cli": "^6.18.0",
"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",
@@ -81,11 +82,12 @@
"matrix-mock-request": "^1.2.2",
"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": [
+26 -11
View File
@@ -6,7 +6,9 @@
# 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
@@ -22,6 +24,8 @@ 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"
@@ -88,6 +92,8 @@ 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
@@ -147,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
@@ -170,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=$?
@@ -181,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
@@ -273,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
@@ -303,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
View File
@@ -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;
});
+2
View File
@@ -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']));
+19 -4
View File
@@ -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]: {},
},
});
+5
View File
@@ -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);
+244
View File
@@ -8,6 +8,11 @@ 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;
@@ -119,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();
}
});
});
});
+11 -7
View File
@@ -74,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",
@@ -124,13 +132,7 @@ 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);
@@ -436,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) => {
@@ -457,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);
});
});
+207
View File
@@ -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();
});
});
+63
View File
@@ -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;
}
+188 -19
View File
@@ -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 = [];
@@ -142,6 +143,11 @@ function keyFromRecoverySession(session, decryptionKey) {
*
* @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,6 +213,7 @@ function MatrixClient(opts) {
this._crypto = null;
this._cryptoStore = opts.cryptoStore;
this._sessionStore = opts.sessionStore;
this._verificationMethods = opts.verificationMethods;
this._forceTURN = opts.forceTURN || false;
@@ -219,6 +226,34 @@ function MatrixClient(opts) {
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 +421,34 @@ 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) {
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(),
};
return capabilities;
});
};
// Crypto bits
// ===========
@@ -444,10 +507,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",
@@ -624,6 +689,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
@@ -684,9 +786,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());
};
/**
@@ -773,6 +876,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
@@ -842,6 +958,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();
};
/**
@@ -992,12 +1112,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) {
@@ -1009,28 +1133,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");
@@ -1038,16 +1162,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) => {
@@ -1082,6 +1216,8 @@ MatrixClient.prototype._restoreKeyBackup = function(
}
return this.importRoomKeys(keys);
}).then(() => {
return this._crypto.setTrustedBackupPubKey(backupPubKey);
}).then(() => {
return {total: totalKeyCount, imported: keys.length};
}).finally(() => {
@@ -2271,16 +2407,21 @@ MatrixClient.prototype.mxcUrlToHttp =
* @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 this.sendStateEvent(room.roomId, "im.vector.user_status", {
status: newMessage,
}, this.getUserId());
} else {
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());
}));
};
@@ -4148,6 +4289,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
/**
+3 -3
View File
@@ -475,10 +475,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;
}
/**
+15 -9
View File
@@ -915,14 +915,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 +930,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 +951,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
this._cryptoStore.addEndToEndInboundGroupSession(
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
} finally {
+100 -30
View File
@@ -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,
+32 -27
View File
@@ -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(
+501 -52
View File
@@ -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,10 +33,34 @@ 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);
}
@@ -69,9 +93,13 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
* 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;
@@ -79,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.
@@ -140,6 +186,8 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
// },
// }
this._lastNewSessionForced = {};
this._verificationTransactions = new Map();
}
utils.inherits(Crypto, EventEmitter);
@@ -216,7 +264,7 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
if (this._baseApis.isGuest()) {
console.log("Skipping key backup check since user is guest");
this._checkedForBackup = true;
return;
return null;
}
let backupInfo;
try {
@@ -227,32 +275,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;
};
/**
@@ -260,14 +336,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: [],
};
@@ -278,42 +356,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;
};
@@ -672,6 +758,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>
@@ -990,15 +1151,19 @@ Crypto.prototype.importRoomKeys = function(keys) {
/**
* 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() {
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
if (this._sendingBackups) return;
this._sendingBackups = true;
try {
// wait between 0 and 10 seconds, to avoid backup
// 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() * 10000;
const delay = Math.random() * maxDelay;
await Promise.delay(delay);
let numFailures = 0; // number of consecutive failures
while (1) {
@@ -1021,6 +1186,9 @@ Crypto.prototype._scheduleKeyBackupSend = async function() {
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);
@@ -1051,6 +1219,9 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
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;
@@ -1087,7 +1258,10 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
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;
};
@@ -1108,10 +1282,14 @@ Crypto.prototype.backupGroupSession = async function(
// don't wait for this to complete: it will delay so
// happens in the background
this._scheduleKeyBackupSend();
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',
[
@@ -1127,10 +1305,11 @@ Crypto.prototype.backupAllGroupSessions = async function(version) {
},
);
let numKeysBackedUp;
do {
numKeysBackedUp = await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
} while (numKeysBackedUp > 0);
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
@@ -1247,10 +1426,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(
@@ -1264,11 +1447,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();
@@ -1326,6 +1507,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
@@ -1415,6 +1600,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()) {
@@ -1452,6 +1643,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
*
@@ -1537,7 +1986,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);
}
};
@@ -563,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) => {
@@ -676,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);
});
+14 -2
View File
@@ -480,14 +480,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);
});
}
@@ -221,6 +221,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) || {};
+4
View File
@@ -336,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;
+200
View File
@@ -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);
}
}
}
+87
View File
@@ -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",
);
+123
View File
@@ -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";
+368
View File
@@ -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";
+3 -3
View File
@@ -88,21 +88,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
+48 -3
View File
@@ -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.
+126 -5
View File
@@ -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,74 @@ 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();
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);
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 +589,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
@@ -1337,6 +1424,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
+2
View File
@@ -186,11 +186,13 @@ User.prototype.getLastActiveTs = function() {
/**
* 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);
};
/**
+4 -1
View File
@@ -184,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;
}
+11
View File
@@ -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;
+5 -5
View File
@@ -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 */
+3 -7
View File
@@ -2,12 +2,8 @@
set -ex
npm run lint
yarn 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
yarn test
yarn gendoc
+4882
View File
File diff suppressed because it is too large Load Diff