Compare commits
259 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e107b3cd | |||
| 881b60c85f | |||
| e3a8631faa | |||
| 3eab51ce79 | |||
| 9db0fe0795 | |||
| 228af037e3 | |||
| 654f250fd8 | |||
| a8693d9d68 | |||
| f9f345e428 | |||
| e678706414 | |||
| 8018259480 | |||
| 9f713781cd | |||
| 48d56dc5c0 | |||
| 701dfa09b4 | |||
| d965648fd7 | |||
| 08c15e7203 | |||
| 9b9e52d0a2 | |||
| 38cc8fe7dc | |||
| 590fac0fa9 | |||
| 9590c8aaf0 | |||
| e2b79e4e7e | |||
| 2df588f95a | |||
| 7c3af91b42 | |||
| 4cc4b28c47 | |||
| fad9b4c67f | |||
| ade3b3a021 | |||
| d8c4101fdd | |||
| a12c250f2b | |||
| 57eef2d832 | |||
| b67a179a54 | |||
| 06044b39c3 | |||
| d16cf26c5f | |||
| b060c5af38 | |||
| b28bad651e | |||
| 5c4b7a3213 | |||
| 7f21c591ae | |||
| 65b24f595c | |||
| 9d9c2720c2 | |||
| f845100062 | |||
| e6155f9e37 | |||
| e9590e9093 | |||
| 49f2d1501c | |||
| c6819e0450 | |||
| f518ea95f4 | |||
| 92c6332143 | |||
| 0df1a7da21 | |||
| 487a9c0967 | |||
| fb89761671 | |||
| d1d3ae074d | |||
| 7dedaf90c3 | |||
| 687b98a09d | |||
| c6b2e9873c | |||
| 1e80491675 | |||
| a727da9193 | |||
| 0b7754581a | |||
| 452e8ea385 | |||
| a189de9a2e | |||
| 8632ca6e37 | |||
| 293860b6c5 | |||
| 07667172cd | |||
| f5240cdac6 | |||
| 7529d2b638 | |||
| 03fc12e888 | |||
| e05a50528e | |||
| 356ee90417 | |||
| 5dced57724 | |||
| 8ec3b88c5d | |||
| d7ef128510 | |||
| bb2502409b | |||
| aa6aab4245 | |||
| 12aab0caeb | |||
| 2f316c558d | |||
| f447273b75 | |||
| c8a18d51e6 | |||
| d77af1e67a | |||
| 81c95224d1 | |||
| a0e66291df | |||
| 5733f46f4c | |||
| da2128feff | |||
| d413faefdb | |||
| 0d1d767d96 | |||
| 5619554023 | |||
| 53880a4bb2 | |||
| 812ae227b6 | |||
| c6b44098ac | |||
| 92b95a98d0 | |||
| a3505ff42d | |||
| 15b2e3ff1d | |||
| 1845d1ac55 | |||
| 463819caa7 | |||
| a0317d9587 | |||
| fa6ce0cb70 | |||
| 7471ff4b0d | |||
| 65a5bfac88 | |||
| d2321410f8 | |||
| f90b5a99cd | |||
| cc4656b36a | |||
| 34bc63a146 | |||
| 09bd91a588 | |||
| bb3e082e5b | |||
| 60c863f829 | |||
| 2d2a73bf52 | |||
| 7b9f73709d | |||
| 1dc89f642d | |||
| a78b59010a | |||
| a2e1d94fcf | |||
| b181c83b93 | |||
| cf7c84c4ba | |||
| 6e5230f9f9 | |||
| f03f7c0acb | |||
| aa9b807b82 | |||
| fa9921e091 | |||
| a9a6b2de48 | |||
| 1ef746658f | |||
| fe0099b497 | |||
| 7e9c4146a5 | |||
| 3e978c64e4 | |||
| 13c5920a46 | |||
| 3b19203fd2 | |||
| 84cd05b218 | |||
| 9b9a9642ee | |||
| 639d2317ed | |||
| 52379d7655 | |||
| 29827362d6 | |||
| f33315f610 | |||
| aec92a41da | |||
| d570811ddc | |||
| 9cd015a218 | |||
| 8d2cc5096e | |||
| 7ec0bf69f3 | |||
| f3a8306107 | |||
| 2b29e9934c | |||
| 4b1104e463 | |||
| 60b9ef959d | |||
| 0074c2cf57 | |||
| b0204ab54e | |||
| bc5b07aa75 | |||
| cfe90dbed5 | |||
| fa913655bc | |||
| af0b6fc6ac | |||
| 6347031f61 | |||
| 3eda039898 | |||
| 5447d99481 | |||
| 9d08744fe2 | |||
| 848fa257ea | |||
| 54553fb671 | |||
| aad7484c8d | |||
| ad658ead37 | |||
| c787f0e9d0 | |||
| 6f5da701aa | |||
| a01368bd60 | |||
| 16dccd75c1 | |||
| 5151b52688 | |||
| f682097e45 | |||
| 89ebffd69f | |||
| cdeb4ead9e | |||
| 79cc835874 | |||
| 8df866865d | |||
| fa7eff50e5 | |||
| 6558881952 | |||
| 9cbe2c985d | |||
| 7769c53cd6 | |||
| 7a5a47ecc7 | |||
| 492c1d77d8 | |||
| 8c315ebd15 | |||
| 6044b2cbad | |||
| 7acfd0b423 | |||
| e201aab440 | |||
| 3952768667 | |||
| fb25fa3a27 | |||
| 857ad9b180 | |||
| 14843a1bca | |||
| 6331b34cf7 | |||
| 2ebe1dfa16 | |||
| 6bdbee533c | |||
| b9886d4f34 | |||
| 666cbbce08 | |||
| 69c575a4be | |||
| c920de0d28 | |||
| 53cdf53f63 | |||
| d9dda6aebc | |||
| 3d4a9a24ce | |||
| de339d3098 | |||
| 0f83234be9 | |||
| bcd9b45589 | |||
| 19f3996e09 | |||
| 25c2cc1768 | |||
| eec7c4c61b | |||
| 25b4b049b7 | |||
| 2191eb3f41 | |||
| bebdbf7e05 | |||
| 646c091966 | |||
| 952729cb1b | |||
| c6992e2056 | |||
| 77ed79e9a9 | |||
| c4f4add0ec | |||
| 7eeb60c838 | |||
| 438861ae5e | |||
| d42cdbbc5b | |||
| 66237e1ea6 | |||
| a51c0450c3 | |||
| 7a2416bb6d | |||
| a1528e9e33 | |||
| 71cc4d535e | |||
| c06723df3d | |||
| 06b285c013 | |||
| 49c06ef0ca | |||
| 5f92357fec | |||
| 3e0dd3d918 | |||
| f19d76b08d | |||
| 8b6b16067b | |||
| a2da0de17d | |||
| 93ff3edb6b | |||
| 7c67fd69dd | |||
| 5ef5412a55 | |||
| e88a384aa7 | |||
| 9067feeafb | |||
| ed978f69fb | |||
| 743f2465ea | |||
| 41fffa233a | |||
| e45377166b | |||
| 24939bf0b0 | |||
| 3221be4855 | |||
| 3135f1ed24 | |||
| 1b0834ffb0 | |||
| ad85740ae2 | |||
| d79d613cb7 | |||
| d8cc1f7b7a | |||
| d7c8856fdd | |||
| 9d80a332aa | |||
| e14f7b63c7 | |||
| 3bd2880923 | |||
| 2401ad7159 | |||
| 6e8e3e4150 | |||
| 5d95398621 | |||
| 64cdd73b93 | |||
| 48a9236ea8 | |||
| 8b3126e9d8 | |||
| 74d497cd2d | |||
| 5070a5c598 | |||
| 2e30b08e74 | |||
| 8bf63f5f0b | |||
| 11665d18ee | |||
| a8a9fc0c9d | |||
| 098cd1b8d4 | |||
| 3166a4880d | |||
| 9d1c7136cc | |||
| e100943edf | |||
| a6fe4cdf1c | |||
| 69c4496dfe | |||
| 8a4440c314 | |||
| a9a4ba33aa | |||
| 7116ad9f58 | |||
| 3d18bdb2aa | |||
| 4c14581606 | |||
| a9c9ec3977 | |||
| 694d1f9631 | |||
| 2b9cfae18a | |||
| 02b37e1219 |
+3
-1
@@ -28,9 +28,11 @@ module.exports = {
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.{ts, tsx}"],
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// While we're converting to ts we make heavy use of this
|
||||
|
||||
@@ -12,7 +12,6 @@ lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
|
||||
+215
@@ -1,3 +1,218 @@
|
||||
Changes in [9.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0-rc.1) (2020-10-21)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0...v9.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `hasPendingEvent` now returns false instead of throwing when pending ordering mode is not `detached`
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Don't cache failures when fetching /versions
|
||||
[\#1521](https://github.com/matrix-org/matrix-js-sdk/pull/1521)
|
||||
* Install deps first as part of release
|
||||
[\#1518](https://github.com/matrix-org/matrix-js-sdk/pull/1518)
|
||||
* [Breaking] Change hasPendingEvent to return false if pending ordering
|
||||
!detached
|
||||
[\#1517](https://github.com/matrix-org/matrix-js-sdk/pull/1517)
|
||||
* Skip editor prompts for merges
|
||||
[\#1519](https://github.com/matrix-org/matrix-js-sdk/pull/1519)
|
||||
* Convert call test to TypeScript
|
||||
[\#1516](https://github.com/matrix-org/matrix-js-sdk/pull/1516)
|
||||
* Support party_id
|
||||
[\#1512](https://github.com/matrix-org/matrix-js-sdk/pull/1512)
|
||||
* Support m.call.reject
|
||||
[\#1510](https://github.com/matrix-org/matrix-js-sdk/pull/1510)
|
||||
* Remove specbuild from .gitignore
|
||||
[\#1515](https://github.com/matrix-org/matrix-js-sdk/pull/1515)
|
||||
* Log the error when we failed to send candidates
|
||||
[\#1514](https://github.com/matrix-org/matrix-js-sdk/pull/1514)
|
||||
* Fixes for call state machine
|
||||
[\#1503](https://github.com/matrix-org/matrix-js-sdk/pull/1503)
|
||||
* Fix call event handler listener removing
|
||||
[\#1506](https://github.com/matrix-org/matrix-js-sdk/pull/1506)
|
||||
* Set the type of the call based on the tracks
|
||||
[\#1501](https://github.com/matrix-org/matrix-js-sdk/pull/1501)
|
||||
* Use new local timestamp for calls
|
||||
[\#1499](https://github.com/matrix-org/matrix-js-sdk/pull/1499)
|
||||
* Adjust types and APIs to match React SDK
|
||||
[\#1502](https://github.com/matrix-org/matrix-js-sdk/pull/1502)
|
||||
* Make an accurate version of 'age' for events
|
||||
[\#1495](https://github.com/matrix-org/matrix-js-sdk/pull/1495)
|
||||
* Make 'options' parameter optional
|
||||
[\#1498](https://github.com/matrix-org/matrix-js-sdk/pull/1498)
|
||||
* Create a giant event type enum
|
||||
[\#1497](https://github.com/matrix-org/matrix-js-sdk/pull/1497)
|
||||
* Convert call.js to Typescript & update WebRTC APIs (re-apply)
|
||||
[\#1494](https://github.com/matrix-org/matrix-js-sdk/pull/1494)
|
||||
|
||||
Changes in [8.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0) (2020-10-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0-rc.1...v8.5.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0-rc.1) (2020-10-07)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.1...v8.5.0-rc.1)
|
||||
|
||||
* Add support for olm fallback keys
|
||||
[\#1467](https://github.com/matrix-org/matrix-js-sdk/pull/1467)
|
||||
* Fix editing local echoes not updating them in real time
|
||||
[\#1492](https://github.com/matrix-org/matrix-js-sdk/pull/1492)
|
||||
* Fix re-emit of Event.replaced to be on client and not room
|
||||
[\#1491](https://github.com/matrix-org/matrix-js-sdk/pull/1491)
|
||||
* Add space to log line
|
||||
[\#1496](https://github.com/matrix-org/matrix-js-sdk/pull/1496)
|
||||
* Revert "Convert call.js to Typescript & update WebRTC APIs"
|
||||
[\#1493](https://github.com/matrix-org/matrix-js-sdk/pull/1493)
|
||||
* Convert call.js to Typescript & update WebRTC APIs
|
||||
[\#1487](https://github.com/matrix-org/matrix-js-sdk/pull/1487)
|
||||
* Dehydrate and rehydrate devices
|
||||
[\#1436](https://github.com/matrix-org/matrix-js-sdk/pull/1436)
|
||||
* Keep local device after processing device list sync
|
||||
[\#1490](https://github.com/matrix-org/matrix-js-sdk/pull/1490)
|
||||
* Enforce logger module via lint rules
|
||||
[\#1489](https://github.com/matrix-org/matrix-js-sdk/pull/1489)
|
||||
* Extend method redactEvent with reason
|
||||
[\#1462](https://github.com/matrix-org/matrix-js-sdk/pull/1462)
|
||||
* Catch exception from call event handler
|
||||
[\#1484](https://github.com/matrix-org/matrix-js-sdk/pull/1484)
|
||||
* Ignore invalid candidates
|
||||
[\#1483](https://github.com/matrix-org/matrix-js-sdk/pull/1483)
|
||||
* Always push docs if they are generated
|
||||
[\#1478](https://github.com/matrix-org/matrix-js-sdk/pull/1478)
|
||||
* Only sign key backup with cross-signing keys when available
|
||||
[\#1481](https://github.com/matrix-org/matrix-js-sdk/pull/1481)
|
||||
* Upgrade dependencies
|
||||
[\#1479](https://github.com/matrix-org/matrix-js-sdk/pull/1479)
|
||||
|
||||
Changes in [8.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.1) (2020-09-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.0...v8.4.1)
|
||||
|
||||
* Catch exception from call event handler
|
||||
[\#1486](https://github.com/matrix-org/matrix-js-sdk/pull/1486)
|
||||
* Ignore invalid candidates
|
||||
[\#1485](https://github.com/matrix-org/matrix-js-sdk/pull/1485)
|
||||
|
||||
Changes in [8.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.0) (2020-09-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.0-rc.1...v8.4.0)
|
||||
|
||||
* Only sign key backup with cross-signing keys when available
|
||||
[\#1482](https://github.com/matrix-org/matrix-js-sdk/pull/1482)
|
||||
|
||||
Changes in [8.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.0-rc.1) (2020-09-23)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.3.0...v8.4.0-rc.1)
|
||||
|
||||
* If there are extraParams set, ensure that queryParams is defined
|
||||
[\#1477](https://github.com/matrix-org/matrix-js-sdk/pull/1477)
|
||||
* Add diagnostics to security bootstrap paths
|
||||
[\#1475](https://github.com/matrix-org/matrix-js-sdk/pull/1475)
|
||||
* Switch to a combination of better-docs and docdash
|
||||
[\#1459](https://github.com/matrix-org/matrix-js-sdk/pull/1459)
|
||||
* Undo attempts to cache private keys aggressively
|
||||
[\#1474](https://github.com/matrix-org/matrix-js-sdk/pull/1474)
|
||||
* Repair secret storage reset, cache keys when missing
|
||||
[\#1472](https://github.com/matrix-org/matrix-js-sdk/pull/1472)
|
||||
* Prevent parallel getVersions calls
|
||||
[\#1471](https://github.com/matrix-org/matrix-js-sdk/pull/1471)
|
||||
* Send end-of-candidates
|
||||
[\#1473](https://github.com/matrix-org/matrix-js-sdk/pull/1473)
|
||||
* Add a function for checking the /versions flag for forced e2ee
|
||||
[\#1470](https://github.com/matrix-org/matrix-js-sdk/pull/1470)
|
||||
* Add option to allow users of pantialaimon to use the SDK
|
||||
[\#1469](https://github.com/matrix-org/matrix-js-sdk/pull/1469)
|
||||
* Fixed Yarn broken link
|
||||
[\#1468](https://github.com/matrix-org/matrix-js-sdk/pull/1468)
|
||||
* some TypeScript and doc fixes
|
||||
[\#1466](https://github.com/matrix-org/matrix-js-sdk/pull/1466)
|
||||
* Remove Travis CI reference
|
||||
[\#1464](https://github.com/matrix-org/matrix-js-sdk/pull/1464)
|
||||
* Inject identity server token for 3pid invites on createRoom
|
||||
[\#1463](https://github.com/matrix-org/matrix-js-sdk/pull/1463)
|
||||
|
||||
Changes in [8.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0) (2020-09-14)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.3.0-rc.1...v8.3.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0-rc.1) (2020-09-09)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0...v8.3.0-rc.1)
|
||||
|
||||
* Add missing options in ICreateClientOpts
|
||||
[\#1452](https://github.com/matrix-org/matrix-js-sdk/pull/1452)
|
||||
* Ensure ready functions return boolean values
|
||||
[\#1457](https://github.com/matrix-org/matrix-js-sdk/pull/1457)
|
||||
* Handle missing cross-signing keys gracefully
|
||||
[\#1456](https://github.com/matrix-org/matrix-js-sdk/pull/1456)
|
||||
* Fix eslint ts override tsx matching
|
||||
[\#1451](https://github.com/matrix-org/matrix-js-sdk/pull/1451)
|
||||
* Untangle cross-signing and secret storage
|
||||
[\#1450](https://github.com/matrix-org/matrix-js-sdk/pull/1450)
|
||||
|
||||
Changes in [8.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0) (2020-09-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0-rc.1...v8.2.0)
|
||||
|
||||
## Security notice
|
||||
|
||||
JS SDK 8.2.0 fixes an issue where encrypted state events could break incoming call handling.
|
||||
Thanks to @awesome-michael from Awesome Technologies for responsibly disclosing this via Matrix's
|
||||
Security Disclosure Policy.
|
||||
|
||||
## All changes
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0-rc.1) (2020-08-26)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.1.0...v8.2.0-rc.1)
|
||||
|
||||
* Add state event check
|
||||
[\#1449](https://github.com/matrix-org/matrix-js-sdk/pull/1449)
|
||||
* Add method to check whether client .well-known has been fetched
|
||||
[\#1444](https://github.com/matrix-org/matrix-js-sdk/pull/1444)
|
||||
* Handle auth errors during cross-signing key upload
|
||||
[\#1443](https://github.com/matrix-org/matrix-js-sdk/pull/1443)
|
||||
* Don't fail if the requested audio output isn't available
|
||||
[\#1448](https://github.com/matrix-org/matrix-js-sdk/pull/1448)
|
||||
* Fix logging failures
|
||||
[\#1447](https://github.com/matrix-org/matrix-js-sdk/pull/1447)
|
||||
* Log the constraints we pass to getUserMedia
|
||||
[\#1446](https://github.com/matrix-org/matrix-js-sdk/pull/1446)
|
||||
* Use SAS emoji data from matrix-doc
|
||||
[\#1440](https://github.com/matrix-org/matrix-js-sdk/pull/1440)
|
||||
|
||||
Changes in [8.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.1.0) (2020-08-17)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.1.0-rc.1...v8.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.1.0-rc.1) (2020-08-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.1...v8.1.0-rc.1)
|
||||
|
||||
* Update on Promises
|
||||
[\#1438](https://github.com/matrix-org/matrix-js-sdk/pull/1438)
|
||||
* Store and request master cross-signing key
|
||||
[\#1437](https://github.com/matrix-org/matrix-js-sdk/pull/1437)
|
||||
* Filter out non-string display names
|
||||
[\#1433](https://github.com/matrix-org/matrix-js-sdk/pull/1433)
|
||||
* Bump elliptic from 6.5.2 to 6.5.3
|
||||
[\#1427](https://github.com/matrix-org/matrix-js-sdk/pull/1427)
|
||||
* Replace Riot with Element in docs and comments
|
||||
[\#1431](https://github.com/matrix-org/matrix-js-sdk/pull/1431)
|
||||
* Remove leftover bits of TSLint
|
||||
[\#1430](https://github.com/matrix-org/matrix-js-sdk/pull/1430)
|
||||
|
||||
Changes in [8.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.1) (2020-08-05)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.1-rc.1...v8.0.1)
|
||||
|
||||
+3
-4
@@ -28,10 +28,9 @@ use GitHub's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use Travis for continuous integration, and all pull requests get
|
||||
automatically tested by Travis: if your change breaks the build, then the PR
|
||||
will show that there are failed checks, so please check back after a few
|
||||
minutes.
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Code style
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -30,7 +30,7 @@ This SDK targets Node 10 for compatibility, which translates to ES6. If you're u
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/)
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
@@ -182,10 +182,8 @@ you can pass the result of the promise into it with something like:
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is an error to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved. If
|
||||
you have nothing better to do with the result, just call ``.done()`` on it. See
|
||||
http://documentup.com/kriskowal/q/#the-end for more information.
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
|
||||
+8
-1
@@ -18,6 +18,13 @@
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/better-docs"
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
|
||||
+34
-33
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "8.0.1",
|
||||
"version": "9.0.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
@@ -14,7 +14,7 @@
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 76 src spec",
|
||||
"lint:js": "eslint --max-warnings 73 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
@@ -46,49 +46,50 @@
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"loglevel": "^1.6.4",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.0",
|
||||
"qs": "^6.9.4",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/cli": "^7.11.6",
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.11.5",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@babel/register": "^7.11.5",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.4",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"@types/request": "^2.48.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^1.4.7",
|
||||
"browserify": "^16.5.0",
|
||||
"eslint": "7.3.1",
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^16.5.2",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.9.0",
|
||||
"eslint-config-matrix-org": "^0.1.2",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^24.9.0",
|
||||
"jest-localstorage-mock": "^2.4.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"jest-localstorage-mock": "^2.4.3",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||
"rimraf": "^3.0.0",
|
||||
"terser": "^4.4.3",
|
||||
"tsify": "^4.0.1",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.7.3"
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^4.8.0",
|
||||
"tsify": "^4.0.2",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
|
||||
+13
-12
@@ -10,7 +10,7 @@
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
#
|
||||
# Note: this script is also used to release matrix-react-sdk and riot-web.
|
||||
# Note: this script is also used to release matrix-react-sdk and element-web.
|
||||
|
||||
set -e
|
||||
|
||||
@@ -94,6 +94,14 @@ if [ $# -ne 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
@@ -204,11 +212,6 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
yarn install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
@@ -327,6 +330,7 @@ if [ -z "$skip_jsdoc" ]; then
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
@@ -339,18 +343,15 @@ fi
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch"
|
||||
git merge "$rel_branch" --no-edit
|
||||
|
||||
# push master and docs (if generated) to github
|
||||
# push master to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# finally, merge master back onto develop (if it exists)
|
||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
git merge master --no-edit
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
@@ -69,6 +69,9 @@ export function TestClient(
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this._callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
@@ -230,3 +233,7 @@ TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.isFallbackICEServerAllowed = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -99,5 +99,5 @@ describe("Browserify Test", function() {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
]);
|
||||
}, 10000);
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
|
||||
@@ -140,7 +140,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
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
@@ -271,7 +271,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
}).timeout(3000);
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/4983
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
await aliceTestClient.start();
|
||||
|
||||
@@ -346,7 +346,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message before the session keys", function() {
|
||||
// https://github.com/vector-im/riot-web/issues/2273
|
||||
// https://github.com/vector-im/element-web/issues/2273
|
||||
let roomKeyEncrypted;
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
@@ -726,7 +726,7 @@ describe("megolm", function() {
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
// https://github.com/vector-im/element-web/issues/2676
|
||||
it("Alice should send to her other devices", function() {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get included in messages Alice sends.
|
||||
|
||||
+1
-1
@@ -31,5 +31,5 @@ try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support: some tests will fail');
|
||||
logger.log('nodejs was compiled without crypto support: some tests will fail');
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {logger} from '../../../src/logger';
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
@@ -38,7 +39,7 @@ const testKey = new Uint8Array([
|
||||
]);
|
||||
|
||||
const types = [
|
||||
{ type: "master", shouldCache: false },
|
||||
{ type: "master", shouldCache: true },
|
||||
{ type: "self_signing", shouldCache: true },
|
||||
{ type: "user_signing", shouldCache: true },
|
||||
{ type: "invalid", shouldCache: false },
|
||||
@@ -51,7 +52,7 @@ const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,9 +84,16 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => testKey,
|
||||
});
|
||||
const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(ab).toEqual({a: 106712, b: 106712});
|
||||
// check that the pkSigning object corresponds to the pubKey
|
||||
const signature = pkSigning.sign("message");
|
||||
const util = new global.Olm.Utility();
|
||||
try {
|
||||
util.ed25519_verify(pubKey, "message", signature);
|
||||
} finally {
|
||||
util.free();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should request a key from the cache callback (if set)" +
|
||||
|
||||
@@ -72,6 +72,8 @@ describe('DeviceList', function() {
|
||||
function createTestDeviceList() {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
};
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
|
||||
@@ -20,7 +20,9 @@ import anotherjson from 'another-json';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
import {logger} from '../../../src/logger';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
@@ -48,7 +50,7 @@ async function makeTestClient(userInfo, options, keys) {
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,13 +72,63 @@ describe("Cross Signing", function() {
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
const errorResponse = {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{
|
||||
stages: [
|
||||
"m.login.password",
|
||||
],
|
||||
},
|
||||
],
|
||||
params: {},
|
||||
};
|
||||
|
||||
// If we're not just polling for flows, add on error rejecting the
|
||||
// auth attempt.
|
||||
if (auth) {
|
||||
Object.assign(errorResponse, {
|
||||
completed: [],
|
||||
error: "Invalid password",
|
||||
errcode: "M_FORBIDDEN",
|
||||
});
|
||||
}
|
||||
|
||||
const error = new MatrixError(errorResponse);
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => { };
|
||||
const authUploadDeviceSigningKeys = async func => await func({});
|
||||
|
||||
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
|
||||
// through failure, stopping before actually applying changes.
|
||||
let bootstrapDidThrow = false;
|
||||
try {
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_FORBIDDEN") {
|
||||
bootstrapDidThrow = true;
|
||||
}
|
||||
}
|
||||
expect(bootstrapDidThrow).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
|
||||
@@ -22,6 +22,7 @@ import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
import {logger} from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
@@ -29,7 +30,7 @@ try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support');
|
||||
logger.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
@@ -60,7 +61,7 @@ function sign(obj, key, userId) {
|
||||
|
||||
describe("Secrets", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ describe("Secrets", function() {
|
||||
};
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
@@ -225,8 +226,8 @@ describe("Secrets", function() {
|
||||
],
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
onSecretRequested: e => {
|
||||
expect(e.name).toBe("foo");
|
||||
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
|
||||
expect(secretName).toBe("foo");
|
||||
return "bar";
|
||||
},
|
||||
},
|
||||
@@ -326,9 +327,11 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
@@ -379,13 +382,15 @@ describe("Secrets", function() {
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
}),
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
@@ -394,7 +399,7 @@ describe("Secrets", function() {
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapSecretStorage({
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
@@ -517,9 +522,7 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "key_id"});
|
||||
@@ -659,9 +662,7 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
|
||||
@@ -54,6 +54,7 @@ describe("self-verifications", () => {
|
||||
cacheCallbacks,
|
||||
);
|
||||
_crossSigningInfo.keys = {
|
||||
master: { keys: { X: testKeyPub } },
|
||||
self_signing: { keys: { X: testKeyPub } },
|
||||
user_signing: { keys: { X: testKeyPub } },
|
||||
};
|
||||
@@ -96,9 +97,9 @@ describe("self-verifications", () => {
|
||||
|
||||
const result = await verification.done();
|
||||
|
||||
/* We should request, and store, two cross signing key and the key backup key */
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(2);
|
||||
expect(_secretStorage.request.mock.calls.length).toBe(3);
|
||||
/* We should request, and store, 3 cross signing keys and the key backup key */
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
|
||||
expect(_secretStorage.request.mock.calls.length).toBe(4);
|
||||
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
|
||||
.toEqual(testKey);
|
||||
|
||||
@@ -18,12 +18,13 @@ limitations under the License.
|
||||
import {TestClient} from '../../../TestClient';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import {logger} from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// console.log(this.getUserId(), "sends", type, map);
|
||||
// logger.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)) {
|
||||
@@ -67,7 +68,7 @@ export async function makeTestClients(userInfos, options) {
|
||||
setImmediate(() => {
|
||||
for (const tc of clients) {
|
||||
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
|
||||
console.log("sending remote echo!!");
|
||||
logger.log("sending remote echo!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
} else {
|
||||
tc.client.emit("Room.timeline", event);
|
||||
|
||||
@@ -17,6 +17,39 @@ limitations under the License.
|
||||
|
||||
import {SyncAccumulator} from "../../src/sync-accumulator";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
// by the impl
|
||||
const RES_WITH_AGE = {
|
||||
next_batch: "abc",
|
||||
rooms: {
|
||||
invite: {},
|
||||
leave: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
timeline: {
|
||||
events: [
|
||||
Object.freeze({
|
||||
content: {
|
||||
body: "This thing is happening right now!",
|
||||
},
|
||||
origin_server_ts: 123456789,
|
||||
sender: "@alice:localhost",
|
||||
type: "m.room.message",
|
||||
unsigned: Object.freeze({
|
||||
age: 50,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
prev_batch: "something",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
|
||||
@@ -368,6 +401,39 @@ describe("SyncAccumulator", function() {
|
||||
expect(summary["m.joined_member_count"]).toEqual(5);
|
||||
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
|
||||
});
|
||||
|
||||
it("should return correctly adjusted age attributes", () => {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
const oldDateNow = Date.now;
|
||||
try {
|
||||
Date.now = jest.fn();
|
||||
Date.now.mockReturnValue(startingTs);
|
||||
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
|
||||
Date.now.mockReturnValue(startingTs + delta);
|
||||
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual(
|
||||
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta,
|
||||
);
|
||||
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
} finally {
|
||||
Date.now = oldDateNow;
|
||||
}
|
||||
});
|
||||
|
||||
it("should mangle age without adding extra keys", () => {
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
const output = sa.getJSON();
|
||||
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {MatrixCall} from '../../../src/webrtc/call';
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
|
||||
class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
let client;
|
||||
let call;
|
||||
let prevNavigator;
|
||||
let prevDocument;
|
||||
let prevWindow;
|
||||
|
||||
beforeEach(function() {
|
||||
prevNavigator = global.navigator;
|
||||
prevDocument = global.document;
|
||||
prevWindow = global.window;
|
||||
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => {
|
||||
return {
|
||||
getTracks: () => [],
|
||||
getAudioTracks: () => [],
|
||||
getVideoTracks: () => [],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: {},
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!foo:bar',
|
||||
});
|
||||
// call checks one of these is wired up
|
||||
call.on('error', () => {});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
global.navigator = prevNavigator;
|
||||
global.window = prevWindow;
|
||||
global.document = prevDocument;
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', async function() {
|
||||
await call.placeVoiceCall();
|
||||
await call.receivedAnswer({
|
||||
version: 0,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: '',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: '',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum EventType {
|
||||
// Room state events
|
||||
RoomCanonicalAlias = "m.room.canonical_alias",
|
||||
RoomCreate = "m.room.create",
|
||||
RoomJoinRules = "m.room.join_rules",
|
||||
RoomMember = "m.room.member",
|
||||
RoomThirdPartyInvite = "m.room.third_party_invite",
|
||||
RoomPowerLevels = "m.room.power_levels",
|
||||
RoomName = "m.room.name",
|
||||
RoomTopic = "m.room.topic",
|
||||
RoomAvatar = "m.room.avatar",
|
||||
RoomPinnedEvents = "m.room.pinned_events",
|
||||
RoomEncryption = "m.room.encryption",
|
||||
RoomHistoryVisibility = "m.room.history_visibility",
|
||||
RoomGuestAccess = "m.room.guest_access",
|
||||
RoomServerAcl = "m.room_server_acl",
|
||||
RoomTombstone = "m.room.tombstone",
|
||||
/**
|
||||
* @deprecated Should not be used.
|
||||
*/
|
||||
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||
|
||||
// Room timeline events
|
||||
RoomRedaction = "m.room.redaction",
|
||||
RoomMessage = "m.room.message",
|
||||
RoomMessageEncrypted = "m.room.encrypted",
|
||||
Sticker = "m.sticker",
|
||||
CallInvite = "m.call.invite",
|
||||
CallCandidates = "m.call.candidates",
|
||||
CallAnswer = "m.call.answer",
|
||||
CallHangup = "m.call.hangup",
|
||||
CallReject = "m.call.reject",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
KeyVerificationMac = "m.key.verification.mac",
|
||||
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
||||
RoomMessageFeedback = "m.room.message.feedback",
|
||||
|
||||
// Room ephemeral events
|
||||
Typing = "m.typing",
|
||||
Receipt = "m.receipt",
|
||||
Presence = "m.presence",
|
||||
|
||||
// Room account_data events
|
||||
FullyRead = "m.fully_read",
|
||||
Tag = "m.tag",
|
||||
|
||||
// User account_data events
|
||||
PushRules = "m.push_rules",
|
||||
Direct = "m.direct",
|
||||
IgnoredUserList = "m.ignored_user_list",
|
||||
|
||||
// to_device events
|
||||
RoomKey = "m.room_key",
|
||||
RoomKeyRequest = "m.room_key_request",
|
||||
ForwardedRoomKey = "m.forwarded_room_key",
|
||||
Dummy = "m.dummy",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
Text = "m.text",
|
||||
Emote = "m.emote",
|
||||
Notice = "m.notice",
|
||||
Image = "m.image",
|
||||
File = "m.file",
|
||||
Audio = "m.audio",
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
}
|
||||
Vendored
+23
@@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
@@ -22,4 +25,24 @@ declare global {
|
||||
localStorage: Storage;
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface DummyInterfaceWeShouldntBeUsingThis {}
|
||||
|
||||
interface Navigator {
|
||||
// We check for the webkit-prefixed getUserMedia to detect if we're
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-3
@@ -66,7 +66,7 @@ function termsUrlForService(serviceType, baseUrl) {
|
||||
* callback that returns a Promise<String> of an identity access token to supply
|
||||
* with identity requests. If the object is unset, no access token will be
|
||||
* supplied.
|
||||
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
|
||||
* See also https://github.com/vector-im/element-web/issues/10615 which seeks to
|
||||
* replace the previous approach of manual access tokens params with this
|
||||
* callback throughout the SDK.
|
||||
*
|
||||
@@ -462,8 +462,26 @@ MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId)
|
||||
* room_alias: {string(opt)}}</code>
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createRoom = function(options, callback) {
|
||||
// valid options include: room_alias_name, visibility, invite
|
||||
MatrixBaseApis.prototype.createRoom = async function(options, callback) {
|
||||
// some valid options include: room_alias_name, visibility, invite
|
||||
|
||||
// inject the id_access_token if inviting 3rd party addresses
|
||||
const invitesNeedingToken = (options.invite_3pid || [])
|
||||
.filter(i => !i.id_access_token);
|
||||
if (
|
||||
invitesNeedingToken.length > 0 &&
|
||||
this.identityServer &&
|
||||
this.identityServer.getAccessToken &&
|
||||
await this.doesServerAcceptIdentityAccessToken()
|
||||
) {
|
||||
const identityAccessToken = await this.identityServer.getAccessToken();
|
||||
if (identityAccessToken) {
|
||||
for (const invite of invitesNeedingToken) {
|
||||
invite.id_access_token = identityAccessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/createRoom", undefined, options,
|
||||
);
|
||||
|
||||
+340
-268
@@ -33,6 +33,7 @@ import {EventTimeline} from "./models/event-timeline";
|
||||
import {SearchResult} from "./models/search-result";
|
||||
import {StubStore} from "./store/stub";
|
||||
import {createNewMatrixCall} from "./webrtc/call";
|
||||
import {CallEventHandler} from './webrtc/callEventHandler';
|
||||
import * as utils from './utils';
|
||||
import {sleep} from './utils';
|
||||
import {
|
||||
@@ -55,6 +56,7 @@ import {PushProcessor} from "./pushprocessor";
|
||||
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
|
||||
import { User } from "./models/user";
|
||||
import {AutoDiscovery} from "./autodiscovery";
|
||||
import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
@@ -119,7 +121,7 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* callback that returns a Promise<String> of an identity access token to supply
|
||||
* with identity requests. If the object is unset, no access token will be
|
||||
* supplied.
|
||||
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
|
||||
* See also https://github.com/vector-im/element-web/issues/10615 which seeks to
|
||||
* replace the previous approach of manual access tokens params with this
|
||||
* callback throughout the SDK.
|
||||
*
|
||||
@@ -182,6 +184,11 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* Optional. Whether to allow a fallback ICE server should be used for negotiating a
|
||||
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
|
||||
*
|
||||
* @param {boolean} [opts.usingExternalCrypto]
|
||||
* Optional. Whether to allow sending messages to encrypted rooms when encryption
|
||||
* is not available internally within this SDK. This is useful if you are using an external
|
||||
* E2E proxy, for example. Defaults to false.
|
||||
*
|
||||
* @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing.
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
@@ -229,23 +236,39 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* {
|
||||
* keys: {
|
||||
* <key name>: {
|
||||
* pubkey: {UInt8Array}
|
||||
* "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
* "passphrase": {
|
||||
* "algorithm": "m.pbkdf2",
|
||||
* "iterations": 500000,
|
||||
* "salt": "..."
|
||||
* },
|
||||
* "iv": "...",
|
||||
* "mac": "..."
|
||||
* }, ...
|
||||
* }
|
||||
* }
|
||||
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey]
|
||||
* Optional. Function called when a new encryption key for secret storage
|
||||
* has been created. This allows the application a chance to cache this key if
|
||||
* desired to avoid user prompts.
|
||||
* Args:
|
||||
* {string} keyId the ID of the new key
|
||||
* {object} keyInfo Infomation about the key as above for `getSecretStorageKey`
|
||||
* {Uint8Array} key the new private key
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
|
||||
* Optional. Function called when a request for a secret is received from another
|
||||
* device.
|
||||
* Args:
|
||||
* {string} name The name of the secret being requested.
|
||||
* {string} user_id (string) The user ID of the client requesting
|
||||
* {string} device_id The device ID of the client requesting the secret.
|
||||
* {string} request_id The ID of the request. Used to match a
|
||||
* {string} userId The user ID of the client requesting
|
||||
* {string} deviceId The device ID of the client requesting the secret.
|
||||
* {string} requestId The ID of the request. Used to match a
|
||||
* corresponding `crypto.secrets.request_cancelled`. The request ID will be
|
||||
* unique per sender, device pair.
|
||||
* {DeviceTrustLevel} device_trust: The trust status of the device requesting
|
||||
* {DeviceTrustLevel} deviceTrust: The trust status of the device requesting
|
||||
* the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}.
|
||||
*/
|
||||
export function MatrixClient(opts) {
|
||||
@@ -258,6 +281,8 @@ export function MatrixClient(opts) {
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
this.usingExternalCrypto = opts.usingExternalCrypto;
|
||||
|
||||
this.store = opts.store || new StubStore();
|
||||
|
||||
this.deviceId = opts.deviceId || null;
|
||||
@@ -312,17 +337,15 @@ export function MatrixClient(opts) {
|
||||
}
|
||||
this.clientRunning = false;
|
||||
|
||||
this.callList = {
|
||||
// callId: MatrixCall
|
||||
};
|
||||
|
||||
// try constructing a MatrixCall to see if we are running in an environment
|
||||
// which has WebRTC. If we are, listen for and handle m.call.* events.
|
||||
const call = createNewMatrixCall(this);
|
||||
this._supportsVoip = false;
|
||||
if (call) {
|
||||
setupCallEventHandler(this);
|
||||
this._callEventHandler = new CallEventHandler(this);
|
||||
this._supportsVoip = true;
|
||||
} else {
|
||||
this._callEventHandler = null;
|
||||
}
|
||||
this._syncingRetry = null;
|
||||
this._syncApi = null;
|
||||
@@ -351,12 +374,15 @@ export function MatrixClient(opts) {
|
||||
// The pushprocessor caches useful things, so keep one and re-use it
|
||||
this._pushProcessor = new PushProcessor(this);
|
||||
|
||||
// Cache of the server's /versions response
|
||||
// Promise to a response of the server's /versions response
|
||||
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
|
||||
this._serverVersionsCache = null;
|
||||
this._serverVersionsPromise = null;
|
||||
|
||||
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
|
||||
|
||||
this._clientWellKnown = undefined;
|
||||
this._clientWellKnownPromise = undefined;
|
||||
|
||||
// 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
|
||||
@@ -380,7 +406,7 @@ export function MatrixClient(opts) {
|
||||
? !!actions.tweaks.highlight : false;
|
||||
if (oldHighlight !== newHighlight || currentCount > 0) {
|
||||
// TODO: Handle mentions received while the client is offline
|
||||
// See also https://github.com/vector-im/riot-web/issues/9069
|
||||
// See also https://github.com/vector-im/element-web/issues/9069
|
||||
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
|
||||
let newCount = currentCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
@@ -398,7 +424,7 @@ export function MatrixClient(opts) {
|
||||
|
||||
// Like above, we have to listen for read receipts from ourselves in order to
|
||||
// correctly handle notification counts on encrypted rooms.
|
||||
// This fixes https://github.com/vector-im/riot-web/issues/9421
|
||||
// This fixes https://github.com/vector-im/element-web/issues/9421
|
||||
this.on("Room.receipt", (event, room) => {
|
||||
if (room && this.isRoomEncrypted(room.roomId)) {
|
||||
// Figure out if we've read something or if it's just informational
|
||||
@@ -441,6 +467,121 @@ export function MatrixClient(opts) {
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
||||
|
||||
/**
|
||||
* Try to rehydrate a device if available. The client must have been
|
||||
* initialized with a `cryptoCallback.getDehydrationKey` option, and this
|
||||
* function must be called before initCrypto and startClient are called.
|
||||
*
|
||||
* @return {Promise} Resolves to undefined if a device could not be dehydrated, or
|
||||
* to the new device ID if the dehydration was successful.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.rehydrateDevice = async function() {
|
||||
if (this._crypto) {
|
||||
throw new Error("Cannot rehydrate device after crypto is initialized");
|
||||
}
|
||||
|
||||
if (!this._cryptoCallbacks.getDehydrationKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let getDeviceResult;
|
||||
try {
|
||||
getDeviceResult = await this._http.authedRequest(
|
||||
undefined,
|
||||
"GET",
|
||||
"/dehydrated_device",
|
||||
undefined, undefined,
|
||||
{
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.info("could not get dehydrated device", e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getDeviceResult.device_data || !getDeviceResult.device_id) {
|
||||
logger.info("no dehydrated device found");
|
||||
return;
|
||||
}
|
||||
|
||||
const account = new global.Olm.Account();
|
||||
try {
|
||||
const deviceData = getDeviceResult.device_data;
|
||||
if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) {
|
||||
logger.warn("Wrong algorithm for dehydrated device");
|
||||
return;
|
||||
}
|
||||
logger.log("unpickling dehydrated device");
|
||||
const key = await this._cryptoCallbacks.getDehydrationKey(
|
||||
deviceData,
|
||||
(k) => {
|
||||
// copy the key so that it doesn't get clobbered
|
||||
account.unpickle(new Uint8Array(k), deviceData.account);
|
||||
},
|
||||
);
|
||||
account.unpickle(key, deviceData.account);
|
||||
logger.log("unpickled device");
|
||||
|
||||
const rehydrateResult = await this._http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
"/dehydrated_device/claim",
|
||||
undefined,
|
||||
{
|
||||
device_id: getDeviceResult.device_id,
|
||||
},
|
||||
{
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
|
||||
},
|
||||
);
|
||||
|
||||
if (rehydrateResult.success === true) {
|
||||
this.deviceId = getDeviceResult.device_id;
|
||||
logger.info("using dehydrated device");
|
||||
const pickleKey = this.pickleKey || "DEFAULT_KEY";
|
||||
this._exportedOlmDeviceToImport = {
|
||||
pickledAccount: account.pickle(pickleKey),
|
||||
sessions: [],
|
||||
pickleKey: pickleKey,
|
||||
};
|
||||
account.free();
|
||||
return this.deviceId;
|
||||
} else {
|
||||
account.free();
|
||||
logger.info("not using dehydrated device");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
account.free();
|
||||
logger.warn("could not unpickle", e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the dehydration key. This will also periodically dehydrate devices to
|
||||
* the server.
|
||||
*
|
||||
* @param {Uint8Array} key the dehydration key
|
||||
* @param {object} [keyInfo] Information about the key. Primarily for
|
||||
* information about how to generate the key from a passphrase.
|
||||
* @param {string} [deviceDisplayName] The device display name for the
|
||||
* dehydrated device.
|
||||
* @return {Promise} A promise that resolves when the dehydrated device is stored.
|
||||
*/
|
||||
MatrixClient.prototype.setDehydrationKey = async function(
|
||||
key, keyInfo = {}, deviceDisplayName = undefined,
|
||||
) {
|
||||
if (!(this._crypto)) {
|
||||
logger.warn('not dehydrating device if crypto is not enabled');
|
||||
return;
|
||||
}
|
||||
return await this._crypto._dehydrationManager.setDehydrationKey(
|
||||
key, keyInfo, deviceDisplayName,
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.exportDevice = async function() {
|
||||
if (!(this._crypto)) {
|
||||
logger.warn('not exporting device if crypto is not enabled');
|
||||
@@ -1091,6 +1232,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Get the user's cross-signing key ID.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getCrossSigningId
|
||||
@@ -1102,6 +1244,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Get the cross signing information for a given user.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getStoredCrossSigningForUser
|
||||
@@ -1112,6 +1255,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Check whether a given user is trusted.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkUserTrust
|
||||
@@ -1122,6 +1266,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
|
||||
/**
|
||||
* Check whether a given device is trusted.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkDeviceTrust
|
||||
@@ -1134,6 +1279,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
/**
|
||||
* Check the copy of our cross-signing key that we have in the device list and
|
||||
* see if we can get the private key. If so, mark it as trusted.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkOwnCrossSigningTrust
|
||||
@@ -1143,6 +1289,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
* Checks that a given cross-signing private key matches a given public key.
|
||||
* This can be used by the getCrossSigningKey callback to verify that the
|
||||
* private key it is about to supply is the one that was requested.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkCrossSigningPrivateKey
|
||||
@@ -1155,8 +1302,48 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @function module:client~MatrixClient#prepareToEncrypt
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks whether cross signing:
|
||||
* - is enabled on this account and trusted by this device
|
||||
* - has private keys either cached locally or stored in secret storage
|
||||
*
|
||||
* If this function returns false, bootstrapCrossSigning() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapCrossSigning() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#isCrossSigningReady
|
||||
* @return {bool} True if cross-signing is ready to be used on this device
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bootstrap cross-signing by creating keys if needed. If everything is already
|
||||
* set up, then no changes are made, so this is safe to run to ensure
|
||||
* cross-signing is ready for use.
|
||||
*
|
||||
* This function:
|
||||
* - creates new cross-signing keys if they are not found locally cached nor in
|
||||
* secret storage (if it has been setup)
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#bootstrapCrossSigning
|
||||
* @param {function} opts.authUploadDeviceSigningKeys Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
|
||||
* already exist.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
*/
|
||||
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"getCrossSigningId",
|
||||
"getStoredCrossSigningForUser",
|
||||
@@ -1167,6 +1354,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
"legacyDeviceVerification",
|
||||
"prepareToEncrypt",
|
||||
"isCrossSigningReady",
|
||||
"bootstrapCrossSigning",
|
||||
"getCryptoTrustCrossSignedDevices",
|
||||
"setCryptoTrustCrossSignedDevices",
|
||||
"countSessionsNeedingBackup",
|
||||
@@ -1193,6 +1381,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Create a recovery key from a user-supplied passphrase.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#createRecoveryKeyFromPassphrase
|
||||
@@ -1205,22 +1394,59 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key and signing it with
|
||||
* the cross-signing master key. If everything is already set up, then no
|
||||
* changes are made, so this is safe to run to ensure secret storage is ready
|
||||
* for use.
|
||||
* Checks whether secret storage:
|
||||
* - is enabled on this account
|
||||
* - is storing cross-signing private keys
|
||||
* - is storing session backup key (if enabled)
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#isSecretStorageReady
|
||||
* @return {bool} True if secret storage is ready to be used on this device
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
|
||||
* already set up, then no changes are made, so this is safe to run to ensure secret
|
||||
* storage is ready for use.
|
||||
*
|
||||
* This function
|
||||
* - creates a new Secure Secret Storage key if no default key exists
|
||||
* - if a key backup exists, it is migrated to store the key in the Secret
|
||||
* Storage
|
||||
* - creates a backup if none exists, and one is requested
|
||||
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
||||
* algorithm is found
|
||||
*
|
||||
* @function module:client~MatrixClient#bootstrapSecretStorage
|
||||
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object.
|
||||
* @param {function} [opts.createSecretStorageKey] Optional. Function
|
||||
* called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
* {Promise<Object>} Object with public key metadata, encoded private
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
* @param {object} [opts.keyBackupInfo] The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
* @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
* is supplied.
|
||||
* @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist.
|
||||
* @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Buffer
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
* Returns:
|
||||
* {Promise} A promise which resolves to key creation data for
|
||||
* SecretStorage#addKey: an object with `passphrase` etc fields.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a key for encrypting secrets.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#addSecretStorageKey
|
||||
@@ -1230,11 +1456,14 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
* @param {string} [keyName] the name of the key. If not given, a random
|
||||
* name will be generated.
|
||||
*
|
||||
* @return {string} the name of the key
|
||||
* @return {object} An object with:
|
||||
* keyId: {string} the ID of the key
|
||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check whether we have a key with a given ID.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#hasSecretStorageKey
|
||||
@@ -1244,7 +1473,8 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
*/
|
||||
|
||||
/**
|
||||
* Store an encrypted secret on the server
|
||||
* Store an encrypted secret on the server.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#storeSecret
|
||||
@@ -1256,6 +1486,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Get a secret from storage.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getSecret
|
||||
@@ -1266,6 +1497,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Check if a secret is stored on the server.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#isSecretStored
|
||||
@@ -1280,6 +1512,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Request a secret from another device.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#requestSecret
|
||||
@@ -1291,6 +1524,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Get the current default key ID for encrypting secrets.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getDefaultSecretStorageKeyId
|
||||
@@ -1300,6 +1534,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
|
||||
/**
|
||||
* Set the current default key ID for encrypting secrets.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#setDefaultSecretStorageKeyId
|
||||
@@ -1310,6 +1545,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
* Checks that a given secret storage private key matches a given public key.
|
||||
* This can be used by the getSecretStorageKey callback to verify that the
|
||||
* private key it is about to supply is the one that was requested.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#checkSecretStoragePrivateKey
|
||||
@@ -1321,6 +1557,7 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"getEventEncryptionInfo",
|
||||
"createRecoveryKeyFromPassphrase",
|
||||
"isSecretStorageReady",
|
||||
"bootstrapSecretStorage",
|
||||
"addSecretStorageKey",
|
||||
"hasSecretStorageKey",
|
||||
@@ -1781,7 +2018,7 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
*
|
||||
* @param {string} password Passphrase
|
||||
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
|
||||
* @return {Promise<Buffer>} key backup key
|
||||
* @return {Promise<Uint8Array>} key backup key
|
||||
*/
|
||||
MatrixClient.prototype.keyBackupKeyFromPassword = function(
|
||||
password, backupInfo,
|
||||
@@ -1796,7 +2033,7 @@ MatrixClient.prototype.keyBackupKeyFromPassword = function(
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {string} recoveryKey The recovery key
|
||||
* @return {Buffer} key backup key
|
||||
* @return {Uint8Array} key backup key
|
||||
*/
|
||||
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
|
||||
return decodeRecoveryKey(recoveryKey);
|
||||
@@ -1942,7 +2179,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
// This is async.
|
||||
this._crypto.storeSessionBackupPrivateKey(privKey)
|
||||
.catch((e) => {
|
||||
console.warn("Error caching session backup key:", e);
|
||||
logger.warn("Error caching session backup key:", e);
|
||||
}).then(cacheCompleteCallback);
|
||||
|
||||
if (progressCallback) {
|
||||
@@ -2644,6 +2881,13 @@ function _encryptEventIfNeeded(client, event, room) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!client._crypto && client.usingExternalCrypto) {
|
||||
// The client has opted to allow sending messages to encrypted
|
||||
// rooms even if the room is encrypted, and we haven't setup
|
||||
// crypto. This is useful for users of matrix-org/pantalaimon
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.getType() === "m.reaction") {
|
||||
// For reactions, there is a very little gained by encrypting the entire
|
||||
// event, as relation data is already kept in the clear. Event
|
||||
@@ -2739,14 +2983,19 @@ function _sendEventHttpRequest(client, event) {
|
||||
* @param {string} eventId
|
||||
* @param {string} [txnId] transaction id. One will be made up if not
|
||||
* supplied.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {object|module:client.callback} callbackOrOpts
|
||||
* Options to pass on, may contain `reason`.
|
||||
* Can be callback for backwards compatibility.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback) {
|
||||
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callbackOrOpts) {
|
||||
const opts = typeof(callbackOrOpts) === 'object' ? callbackOrOpts : {};
|
||||
const reason = opts.reason;
|
||||
const callback = typeof(callbackOrOpts) === 'function' ? callbackOrOpts : undefined;
|
||||
return this._sendCompleteEvent(roomId, {
|
||||
type: "m.room.redaction",
|
||||
content: {},
|
||||
content: { reason: reason },
|
||||
redacts: eventId,
|
||||
}, txnId, callback);
|
||||
};
|
||||
@@ -4810,20 +5059,23 @@ MatrixClient.prototype.startClient = async function(opts) {
|
||||
};
|
||||
|
||||
MatrixClient.prototype._fetchClientWellKnown = async function() {
|
||||
try {
|
||||
this._clientWellKnown = await AutoDiscovery.getRawClientConfig(this.getDomain());
|
||||
this.emit("WellKnown.client", this._clientWellKnown);
|
||||
} catch (err) {
|
||||
logger.error("Failed to get client well-known", err);
|
||||
this._clientWellKnown = undefined;
|
||||
this.emit("WellKnown.error", err);
|
||||
}
|
||||
// `getRawClientConfig` does not throw or reject on network errors, instead
|
||||
// it absorbs errors and returns `{}`.
|
||||
this._clientWellKnownPromise = AutoDiscovery.getRawClientConfig(
|
||||
this.getDomain(),
|
||||
);
|
||||
this._clientWellKnown = await this._clientWellKnownPromise;
|
||||
this.emit("WellKnown.client", this._clientWellKnown);
|
||||
};
|
||||
|
||||
MatrixClient.prototype.getClientWellKnown = function() {
|
||||
return this._clientWellKnown;
|
||||
};
|
||||
|
||||
MatrixClient.prototype.waitForClientWellKnown = function() {
|
||||
return this._clientWellKnownPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* store client options with boolean/string/numeric values
|
||||
* to know in the next session what flags the sync data was
|
||||
@@ -4862,6 +5114,11 @@ MatrixClient.prototype.stopClient = function() {
|
||||
if (this._peekSync) {
|
||||
this._peekSync.stopPeeking();
|
||||
}
|
||||
if (this._callEventHandler) {
|
||||
this._callEventHandler.stop();
|
||||
this._callEventHandler = null;
|
||||
}
|
||||
|
||||
global.clearTimeout(this._checkTurnServersTimeoutID);
|
||||
if (this._clientWellKnownIntervalID !== undefined) {
|
||||
global.clearInterval(this._clientWellKnownIntervalID);
|
||||
@@ -4873,19 +5130,27 @@ MatrixClient.prototype.stopClient = function() {
|
||||
* unstable APIs it supports
|
||||
* @return {Promise<object>} The server /versions response
|
||||
*/
|
||||
MatrixClient.prototype.getVersions = async function() {
|
||||
if (this._serverVersionsCache === null) {
|
||||
this._serverVersionsCache = await this._http.request(
|
||||
undefined, // callback
|
||||
"GET", "/_matrix/client/versions",
|
||||
undefined, // queryParams
|
||||
undefined, // data
|
||||
{
|
||||
prefix: '',
|
||||
},
|
||||
);
|
||||
MatrixClient.prototype.getVersions = function() {
|
||||
if (this._serverVersionsPromise) {
|
||||
return this._serverVersionsPromise;
|
||||
}
|
||||
return this._serverVersionsCache;
|
||||
|
||||
this._serverVersionsPromise = this._http.request(
|
||||
undefined, // callback
|
||||
"GET", "/_matrix/client/versions",
|
||||
undefined, // queryParams
|
||||
undefined, // data
|
||||
{
|
||||
prefix: '',
|
||||
},
|
||||
).catch((e) => {
|
||||
// Need to unset this if it fails, otherwise we'll never retry
|
||||
this._serverVersionsPromise = null;
|
||||
// but rethrow the exception to anything that was waiting
|
||||
throw e;
|
||||
});
|
||||
|
||||
return this._serverVersionsPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -4984,6 +5249,20 @@ MatrixClient.prototype.doesServerSupportUnstableFeature = async function(feature
|
||||
return unstableFeatures && !!unstableFeatures[feature];
|
||||
};
|
||||
|
||||
/**
|
||||
* Query the server to see if it is forcing encryption to be enabled for
|
||||
* a given room preset, based on the /versions response.
|
||||
* @param {string} presetName The name of the preset to check.
|
||||
* @returns {Promise<boolean>} true if the server is forcing encryption
|
||||
* for the preset.
|
||||
*/
|
||||
MatrixClient.prototype.doesServerForceEncryptionForPreset = async function(presetName) {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${presetName}`];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get if lazy loading members is being used.
|
||||
* @return {boolean} Whether or not members are lazy loaded by this client
|
||||
@@ -5054,219 +5333,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
function setupCallEventHandler(client) {
|
||||
const candidatesByCall = {
|
||||
// callId: [Candidate]
|
||||
};
|
||||
|
||||
// The sync code always emits one event at a time, so it will patiently
|
||||
// wait for us to finish processing a call invite before delivering the
|
||||
// next event, even if that next event is a hangup. We therefore accumulate
|
||||
// all our call events and then process them on the 'sync' event, ie.
|
||||
// each time a sync has completed. This way, we can avoid emitting incoming
|
||||
// call events if we get both the invite and answer/hangup in the same sync.
|
||||
// This happens quite often, eg. replaying sync from storage, catchup sync
|
||||
// after loading and after we've been offline for a bit.
|
||||
let callEventBuffer = [];
|
||||
function evaluateEventBuffer() {
|
||||
if (client.getSyncState() === "SYNCING") {
|
||||
// don't process any events until they are all decrypted
|
||||
if (callEventBuffer.some((e) => e.isBeingDecrypted())) return;
|
||||
|
||||
const ignoreCallIds = {}; // Set<String>
|
||||
// inspect the buffer and mark all calls which have been answered
|
||||
// or hung up before passing them to the call event handler.
|
||||
for (let i = callEventBuffer.length - 1; i >= 0; i--) {
|
||||
const ev = callEventBuffer[i];
|
||||
if (ev.getType() === "m.call.answer" ||
|
||||
ev.getType() === "m.call.hangup") {
|
||||
ignoreCallIds[ev.getContent().call_id] = "yep";
|
||||
}
|
||||
}
|
||||
// now loop through the buffer chronologically and inject them
|
||||
callEventBuffer.forEach(function(e) {
|
||||
if (
|
||||
e.getType() === "m.call.invite" &&
|
||||
ignoreCallIds[e.getContent().call_id]
|
||||
) {
|
||||
// This call has previously been answered or hung up: ignore it
|
||||
return;
|
||||
}
|
||||
callEventHandler(e);
|
||||
});
|
||||
callEventBuffer = [];
|
||||
}
|
||||
}
|
||||
client.on("sync", evaluateEventBuffer);
|
||||
|
||||
function onEvent(event) {
|
||||
// any call events or ones that might be once they're decrypted
|
||||
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
|
||||
// queue up for processing once all events from this sync have been
|
||||
// processed (see above).
|
||||
callEventBuffer.push(event);
|
||||
}
|
||||
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once("Event.decrypted", () => {
|
||||
if (event.getType().indexOf("m.call.") === -1) return;
|
||||
|
||||
if (callEventBuffer.includes(event)) {
|
||||
// we were waiting for that event to decrypt, so recheck the buffer
|
||||
evaluateEventBuffer();
|
||||
} else {
|
||||
// This one wasn't buffered so just run the event handler for it
|
||||
// straight away
|
||||
callEventHandler(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
client.on("event", onEvent);
|
||||
|
||||
function callEventHandler(event) {
|
||||
const content = event.getContent();
|
||||
let call = content.call_id ? client.callList[content.call_id] : undefined;
|
||||
let i;
|
||||
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
|
||||
|
||||
if (event.getType() === "m.call.invite") {
|
||||
if (event.getSender() === client.credentials.userId) {
|
||||
return; // ignore invites you send
|
||||
}
|
||||
|
||||
if (event.getAge() > content.lifetime) {
|
||||
return; // expired call
|
||||
}
|
||||
|
||||
if (call && call.state === "ended") {
|
||||
return; // stale/old invite event
|
||||
}
|
||||
if (call) {
|
||||
logger.log(
|
||||
"WARN: Already have a MatrixCall with id %s but got an " +
|
||||
"invite. Clobbering.",
|
||||
content.call_id,
|
||||
);
|
||||
}
|
||||
|
||||
call = createNewMatrixCall(client, event.getRoomId(), {
|
||||
forceTURN: client._forceTURN,
|
||||
});
|
||||
if (!call) {
|
||||
logger.log(
|
||||
"Incoming call ID " + content.call_id + " but this client " +
|
||||
"doesn't support WebRTC",
|
||||
);
|
||||
// don't hang up the call: there could be other clients
|
||||
// connected that do support WebRTC and declining the
|
||||
// the call on their behalf would be really annoying.
|
||||
return;
|
||||
}
|
||||
|
||||
call.callId = content.call_id;
|
||||
call._initWithInvite(event);
|
||||
client.callList[call.callId] = call;
|
||||
|
||||
// if we stashed candidate events for that call ID, play them back now
|
||||
if (candidatesByCall[call.callId]) {
|
||||
for (i = 0; i < candidatesByCall[call.callId].length; i++) {
|
||||
call._gotRemoteIceCandidate(
|
||||
candidatesByCall[call.callId][i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Were we trying to call that user (room)?
|
||||
let existingCall;
|
||||
const existingCalls = utils.values(client.callList);
|
||||
for (i = 0; i < existingCalls.length; ++i) {
|
||||
const thisCall = existingCalls[i];
|
||||
if (call.roomId === thisCall.roomId &&
|
||||
thisCall.direction === 'outbound' &&
|
||||
(["wait_local_media", "create_offer", "invite_sent"].indexOf(
|
||||
thisCall.state) !== -1)) {
|
||||
existingCall = thisCall;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingCall) {
|
||||
// If we've only got to wait_local_media or create_offer and
|
||||
// we've got an invite, pick the incoming call because we know
|
||||
// we haven't sent our invite yet otherwise, pick whichever
|
||||
// call has the lowest call ID (by string comparison)
|
||||
if (existingCall.state === 'wait_local_media' ||
|
||||
existingCall.state === 'create_offer' ||
|
||||
existingCall.callId > call.callId) {
|
||||
logger.log(
|
||||
"Glare detected: answering incoming call " + call.callId +
|
||||
" and canceling outgoing call " + existingCall.callId,
|
||||
);
|
||||
existingCall._replacedBy(call);
|
||||
call.answer();
|
||||
} else {
|
||||
logger.log(
|
||||
"Glare detected: rejecting incoming call " + call.callId +
|
||||
" and keeping outgoing call " + existingCall.callId,
|
||||
);
|
||||
call.hangup();
|
||||
}
|
||||
} else {
|
||||
client.emit("Call.incoming", call);
|
||||
}
|
||||
} else if (event.getType() === 'm.call.answer') {
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
if (event.getSender() === client.credentials.userId) {
|
||||
if (call.state === 'ringing') {
|
||||
call._onAnsweredElsewhere(content);
|
||||
}
|
||||
} else {
|
||||
call._receivedAnswer(content);
|
||||
}
|
||||
} else if (event.getType() === 'm.call.candidates') {
|
||||
if (event.getSender() === client.credentials.userId) {
|
||||
return;
|
||||
}
|
||||
if (!call) {
|
||||
// store the candidates; we may get a call eventually.
|
||||
if (!candidatesByCall[content.call_id]) {
|
||||
candidatesByCall[content.call_id] = [];
|
||||
}
|
||||
candidatesByCall[content.call_id] = candidatesByCall[
|
||||
content.call_id
|
||||
].concat(content.candidates);
|
||||
} else {
|
||||
for (i = 0; i < content.candidates.length; i++) {
|
||||
call._gotRemoteIceCandidate(content.candidates[i]);
|
||||
}
|
||||
}
|
||||
} else if (event.getType() === 'm.call.hangup') {
|
||||
// Note that we also observe our own hangups here so we can see
|
||||
// if we've already rejected a call that would otherwise be valid
|
||||
if (!call) {
|
||||
// if not live, store the fact that the call has ended because
|
||||
// we're probably getting events backwards so
|
||||
// the hangup will come before the invite
|
||||
call = createNewMatrixCall(client, event.getRoomId());
|
||||
if (call) {
|
||||
call.callId = content.call_id;
|
||||
call._initWithHangup(event);
|
||||
client.callList[content.call_id] = call;
|
||||
}
|
||||
} else {
|
||||
if (call.state !== 'ended') {
|
||||
call._onHangupReceived(content);
|
||||
delete client.callList[content.call_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkTurnServers(client) {
|
||||
if (!client._supportsVoip) {
|
||||
return;
|
||||
@@ -5323,9 +5389,8 @@ function _PojoToMatrixEventMapper(client, options) {
|
||||
}
|
||||
event.attemptDecryption(client._crypto);
|
||||
}
|
||||
const room = client.getRoom(event.getRoomId());
|
||||
if (room && !preventReEmit) {
|
||||
room.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
@@ -5698,6 +5763,13 @@ MatrixClient.prototype.generateClientSecret = function() {
|
||||
* @param {string} data.request_id The ID of the original request.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the client .well-known info is fetched.
|
||||
*
|
||||
* @event module:client~MatrixClient#"WellKnown.client"
|
||||
* @param {object} data The JSON object returned by the server
|
||||
*/
|
||||
|
||||
// EventEmitter JSDocs
|
||||
|
||||
/**
|
||||
|
||||
+156
-19
@@ -26,6 +26,8 @@ import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo) {
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
@@ -64,15 +66,34 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
this.crossSigningVerifiedBefore = false;
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toStorage() {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the app callback to ask for a private key
|
||||
*
|
||||
* @param {string} type The key type ("master", "self_signing", or "user_signing")
|
||||
* @param {string} expectedPubkey The matching public key or undefined to use
|
||||
* the stored public key for the given key type.
|
||||
* @returns {Array} An array with [ public key, Olm.PkSigning ]
|
||||
*/
|
||||
async getCrossSigningKey(type, expectedPubkey) {
|
||||
const shouldCache = ["self_signing", "user_signing"].indexOf(type) >= 0;
|
||||
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
|
||||
|
||||
if (!this._callbacks.getCrossSigningKey) {
|
||||
throw new Error("No getCrossSigningKey callback supplied");
|
||||
@@ -125,24 +146,6 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toStorage() {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the private keys exist in secret storage.
|
||||
* XXX: This could be static, be we often seem to have an instance when we
|
||||
@@ -199,9 +202,50 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*/
|
||||
static async getFromSecretStorage(type, secretStorage) {
|
||||
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
|
||||
if (!encodedKey) {
|
||||
return null;
|
||||
}
|
||||
return decodeBase64(encodedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the private keys exist in the local key cache.
|
||||
*
|
||||
* @param {string} [type] The type of key to get. One of "master",
|
||||
* "self_signing", or "user_signing". Optional, will check all by default.
|
||||
* @returns {boolean} True if all keys are stored in the local cache.
|
||||
*/
|
||||
async isStoredInKeyCache(type) {
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
if (!cacheCallbacks) return false;
|
||||
const types = type ? [type] : ["master", "self_signing", "user_signing"];
|
||||
for (const t of types) {
|
||||
if (!await cacheCallbacks.getCrossSigningKeyCache(t)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cross-signing private keys from the local cache.
|
||||
*
|
||||
* @returns {Map} A map from key type (string) to private key (Uint8Array)
|
||||
*/
|
||||
async getCrossSigningKeysFromCache() {
|
||||
const keys = new Map();
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
if (!cacheCallbacks) return keys;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
|
||||
if (!privKey) {
|
||||
continue;
|
||||
}
|
||||
keys.set(type, privKey);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID used to identify the user. This can also be used to test for
|
||||
* the existence of a given key type.
|
||||
@@ -677,3 +721,96 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Request cross-signing keys from another device during verification.
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface
|
||||
* @param {string} userId The user ID being verified
|
||||
* @param {string} deviceId The device ID being verified
|
||||
*/
|
||||
export async function requestKeysDuringVerification(baseApis, userId, deviceId) {
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (baseApis.getUserId() !== userId) {
|
||||
return;
|
||||
}
|
||||
logger.log("Cross-signing: Self-verification done; requesting keys");
|
||||
// This happens asynchronously, and we're not concerned about waiting for
|
||||
// it. We return here in order to test.
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client._crypto._crossSigningInfo;
|
||||
|
||||
// We already have all of the infrastructure we need to validate and
|
||||
// cache cross-signing keys, so instead of replicating that, here we set
|
||||
// up callbacks that request them from the other device and call
|
||||
// CrossSigningInfo.getCrossSigningKey() to validate/cache
|
||||
const crossSigning = new CrossSigningInfo(
|
||||
original.userId,
|
||||
{ getCrossSigningKey: async (type) => {
|
||||
logger.debug("Cross-signing: requesting secret",
|
||||
type, deviceId);
|
||||
const { promise } = client.requestSecret(
|
||||
`m.cross_signing.${type}`, [deviceId],
|
||||
);
|
||||
const result = await promise;
|
||||
const decoded = decodeBase64(result);
|
||||
return Uint8Array.from(decoded);
|
||||
} },
|
||||
original._cacheCallbacks,
|
||||
);
|
||||
crossSigning.keys = original.keys;
|
||||
|
||||
// XXX: get all keys out if we get one key out
|
||||
// https://github.com/vector-im/element-web/issues/12604
|
||||
// then change here to reject on the timeout
|
||||
// Requests can be ignored, so don't wait around forever
|
||||
const timeout = new Promise((resolve, reject) => {
|
||||
setTimeout(
|
||||
resolve,
|
||||
KEY_REQUEST_TIMEOUT_MS,
|
||||
new Error("Timeout"),
|
||||
);
|
||||
});
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = new Promise(async resolve => {
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
const secretReq = client.requestSecret(
|
||||
'm.megolm_backup.v1', [deviceId],
|
||||
);
|
||||
const base64Key = await secretReq.promise;
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client._crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
// no need to await for this - just let it go in the bg
|
||||
client.restoreKeyBackupWithCache(
|
||||
undefined, undefined, backupInfo,
|
||||
).then(() => {
|
||||
logger.info("Backup restored.");
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
// We call getCrossSigningKey() for its side-effects
|
||||
return Promise.race([
|
||||
Promise.all([
|
||||
crossSigning.getCrossSigningKey("master"),
|
||||
crossSigning.getCrossSigningKey("self_signing"),
|
||||
crossSigning.getCrossSigningKey("user_signing"),
|
||||
backupKeyPromise,
|
||||
]),
|
||||
timeout,
|
||||
]).then(resolve, reject);
|
||||
}).catch((e) => {
|
||||
logger.warn("Cross-signing: failure while requesting keys:", e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -790,7 +790,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-web/issues/3158)
|
||||
// (https://github.com/vector-im/element-web/issues/3158)
|
||||
//
|
||||
// of course we ought to do this in a web worker or similar, but
|
||||
// this serves as an easy solution for now.
|
||||
@@ -848,6 +848,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
await _updateStoredDeviceKeysForUser(
|
||||
this._olmDevice, userId, userStore, dkResponse || {},
|
||||
this._baseApis.getUserId(), this._baseApis.deviceId,
|
||||
);
|
||||
|
||||
// put the updates into the object that will be returned as our results
|
||||
@@ -885,8 +886,9 @@ class DeviceListUpdateSerialiser {
|
||||
}
|
||||
|
||||
|
||||
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
userResult) {
|
||||
async function _updateStoredDeviceKeysForUser(
|
||||
_olmDevice, userId, userStore, userResult, localUserId, localDeviceId,
|
||||
) {
|
||||
let updated = false;
|
||||
|
||||
// remove any devices in the store which aren't in the response
|
||||
@@ -896,6 +898,13 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
}
|
||||
|
||||
if (!(deviceId in userResult)) {
|
||||
if (userId === localUserId && deviceId === localDeviceId) {
|
||||
logger.warn(
|
||||
`Local device ${deviceId} missing from sync, skipping removal`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.log("Device " + userId + ":" + deviceId +
|
||||
" has been removed");
|
||||
delete userStore[deviceId];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logger } from "../logger";
|
||||
import {MatrixEvent} from "../models/event";
|
||||
import {EventEmitter} from "events";
|
||||
import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
*/
|
||||
export class EncryptionSetupBuilder {
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData, delegateCryptoCallbacks) {
|
||||
@@ -32,11 +33,17 @@ export class EncryptionSetupBuilder {
|
||||
|
||||
/**
|
||||
* Adds new cross-signing public keys
|
||||
* @param {Object} auth auth dictionary needed to upload the new keys
|
||||
*
|
||||
* @param {function} authUpload Function called to await an interactive auth
|
||||
* flow when uploading device signing keys.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives
|
||||
* the auth data as an object. Can be called multiple times, first with
|
||||
* an empty authDict, to obtain the flows.
|
||||
* @param {Object} keys the new keys
|
||||
*/
|
||||
addCrossSigningKeys(auth, keys) {
|
||||
this._crossSigningKeys = {auth, keys};
|
||||
addCrossSigningKeys(authUpload, keys) {
|
||||
this._crossSigningKeys = {authUpload, keys};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,12 +120,12 @@ export class EncryptionSetupBuilder {
|
||||
* @return {Promise}
|
||||
*/
|
||||
async persist(crypto) {
|
||||
// store self_signing and user_signing private key in cache
|
||||
// store private keys in cache
|
||||
if (this._crossSigningKeys) {
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(
|
||||
crypto._cryptoStore, crypto._olmDevice);
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
// logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
|
||||
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
|
||||
}
|
||||
@@ -164,25 +171,28 @@ export class EncryptionSetupOperation {
|
||||
*/
|
||||
async apply(crypto) {
|
||||
const baseApis = crypto._baseApis;
|
||||
// set account data
|
||||
if (this._accountData) {
|
||||
for (const [type, content] of this._accountData) {
|
||||
await baseApis.setAccountData(type, content);
|
||||
}
|
||||
}
|
||||
// upload cross-signing keys
|
||||
if (this._crossSigningKeys) {
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
await baseApis.uploadDeviceSigningKeys(
|
||||
this._crossSigningKeys.auth,
|
||||
keys,
|
||||
);
|
||||
|
||||
// We must only call `uploadDeviceSigningKeys` from inside this auth
|
||||
// helper to ensure we properly handle auth errors.
|
||||
await this._crossSigningKeys.authUpload(authDict => {
|
||||
return baseApis.uploadDeviceSigningKeys(authDict, keys);
|
||||
});
|
||||
|
||||
// pass the new keys to the main instance of our own CrossSigningInfo.
|
||||
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
||||
}
|
||||
// set account data
|
||||
if (this._accountData) {
|
||||
for (const [type, content] of this._accountData) {
|
||||
await baseApis.setAccountData(type, content);
|
||||
}
|
||||
}
|
||||
// upload first cross-signing signatures with the new key
|
||||
// (e.g. signing our own device)
|
||||
if (this._keySignatures) {
|
||||
@@ -334,7 +344,14 @@ class SSSSCryptoCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
addPrivateKey(keyId, privKey) {
|
||||
addPrivateKey(keyId, keyInfo, privKey) {
|
||||
this._privateKeys.set(keyId, privKey);
|
||||
// Also pass along to application to cache if it wishes
|
||||
if (
|
||||
this._delegateCryptoCallbacks &&
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey
|
||||
) {
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-1
@@ -144,7 +144,7 @@ OlmDevice.prototype.init = async function(opts = {}) {
|
||||
try {
|
||||
if (fromExportedDevice) {
|
||||
if (pickleKey) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
'ignoring opts.pickleKey'
|
||||
+ ' because opts.fromExportedDevice is present.',
|
||||
);
|
||||
@@ -477,6 +477,36 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new fallback keys
|
||||
*
|
||||
* @return {Promise} Resolved once the account is saved back having generated the key
|
||||
*/
|
||||
OlmDevice.prototype.generateFallbackKey = async function() {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._getAccount(txn, (account) => {
|
||||
account.generate_fallback_key();
|
||||
this._storeAccount(txn, account);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
OlmDevice.prototype.getFallbackKey = async function() {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._getAccount(txn, (account) => {
|
||||
result = JSON.parse(account.fallback_key());
|
||||
});
|
||||
},
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new outbound session
|
||||
*
|
||||
|
||||
+26
-16
@@ -81,25 +81,27 @@ export class SecretStorage extends EventEmitter {
|
||||
* @param {string} [keyId] the ID of the key. If not given, a random
|
||||
* ID will be generated.
|
||||
*
|
||||
* @return {string} the ID of the key
|
||||
* @return {object} An object with:
|
||||
* keyId: {string} the ID of the key
|
||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||
*/
|
||||
async addKey(algorithm, opts, keyId) {
|
||||
const keyData = {algorithm};
|
||||
const keyInfo = {algorithm};
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
if (opts.name) {
|
||||
keyData.name = opts.name;
|
||||
keyInfo.name = opts.name;
|
||||
}
|
||||
|
||||
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (opts.passphrase) {
|
||||
keyData.passphrase = opts.passphrase;
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
keyData.iv = iv;
|
||||
keyData.mac = mac;
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
||||
@@ -116,10 +118,13 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyData,
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
|
||||
return keyId;
|
||||
return {
|
||||
keyId,
|
||||
keyInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,7 +252,7 @@ export class SecretStorage extends EventEmitter {
|
||||
) {
|
||||
const hasKey = await this.hasKey(keys[0]);
|
||||
if (hasKey) {
|
||||
console.log("Fixing up passthrough secret: " + name);
|
||||
logger.log("Fixing up passthrough secret: " + name);
|
||||
await this.storePassthrough(name, keys[0]);
|
||||
const newData = await this._baseApis.getAccountDataFromServer(name);
|
||||
return newData;
|
||||
@@ -373,6 +378,7 @@ export class SecretStorage extends EventEmitter {
|
||||
const requestId = this._baseApis.makeTxnId();
|
||||
|
||||
const requestControl = this._requests[requestId] = {
|
||||
name,
|
||||
devices,
|
||||
};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
@@ -456,13 +462,13 @@ export class SecretStorage extends EventEmitter {
|
||||
if (!this._cryptoCallbacks.onSecretRequested) {
|
||||
return;
|
||||
}
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested({
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
name: content.name,
|
||||
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
});
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested(
|
||||
sender,
|
||||
deviceId,
|
||||
content.request_id,
|
||||
content.name,
|
||||
this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
);
|
||||
if (secret) {
|
||||
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
||||
const payload = {
|
||||
@@ -536,6 +542,10 @@ export class SecretStorage extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`Successfully received secret ${requestControl.name} ` +
|
||||
`from ${deviceInfo.deviceId}`,
|
||||
);
|
||||
requestControl.resolve(content.secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1015,7 +1015,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
|
||||
// with them, which means that they will have announced any new devices via
|
||||
// device_lists in their /sync response. This cache should then be maintained
|
||||
// using all the device_lists changes and left fields.
|
||||
// See https://github.com/vector-im/riot-web/issues/2305 for details.
|
||||
// See https://github.com/vector-im/element-web/issues/2305 for details.
|
||||
const devices = await this._crypto.downloadKeys(roomMembers, false);
|
||||
const blocked = {};
|
||||
// remove any blocked devices
|
||||
@@ -1109,7 +1109,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
//
|
||||
// then, if the key turns up while decryption is in progress (and
|
||||
// decryption fails), we will schedule a retry.
|
||||
// (fixes https://github.com/vector-im/riot-web/issues/5001)
|
||||
// (fixes https://github.com/vector-im/element-web/issues/5001)
|
||||
this._addEventToPendingList(event);
|
||||
|
||||
let res;
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import anotherjson from "another-json";
|
||||
import {logger} from '../logger';
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
|
||||
interface DeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: Signatures;
|
||||
}
|
||||
|
||||
interface OneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: Signatures;
|
||||
}
|
||||
|
||||
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||
|
||||
const oneweek = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export class DehydrationManager {
|
||||
private inProgress = false;
|
||||
private timeoutId: any;
|
||||
private key: Uint8Array;
|
||||
private keyInfo: {[props: string]: any};
|
||||
private deviceDisplayName: string;
|
||||
constructor(private crypto) {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
async getDehydrationKeyFromCache(): Promise<void> {
|
||||
return this.crypto._cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.getSecretStorePrivateKey(
|
||||
txn,
|
||||
async (result) => {
|
||||
if (result) {
|
||||
const {key, keyInfo, deviceDisplayName, time} = result;
|
||||
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.key = decodeBase64(decrypted);
|
||||
this.keyInfo = keyInfo;
|
||||
this.deviceDisplayName = deviceDisplayName;
|
||||
const now = Date.now();
|
||||
const delay = Math.max(1, time + oneweek - now);
|
||||
this.timeoutId = global.setTimeout(
|
||||
this.dehydrateDevice.bind(this), delay,
|
||||
);
|
||||
}
|
||||
},
|
||||
"dehydration",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
async setDehydrationKey(
|
||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||
deviceDisplayName: string = undefined,
|
||||
): Promise<void> {
|
||||
if (!key) {
|
||||
// unsetting the key -- cancel any pending dehydration task
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
// clear storage
|
||||
this.crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.storeSecretStorePrivateKey(
|
||||
txn, "dehydration", null,
|
||||
);
|
||||
},
|
||||
);
|
||||
this.key = undefined;
|
||||
this.keyInfo = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if it's the same key as before. If it's different,
|
||||
// dehydrate a new device. If it's the same, we can keep the same
|
||||
// device. (Assume that keyInfo and deviceDisplayNamme will be the
|
||||
// same if the key is the same.)
|
||||
let matches: boolean = this.key && key.length == this.key.length;
|
||||
for (let i = 0; matches && i < key.length; i++) {
|
||||
if (key[i] != this.key[i]) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
this.key = key;
|
||||
this.keyInfo = keyInfo;
|
||||
this.deviceDisplayName = deviceDisplayName;
|
||||
// start dehydration in the background
|
||||
this.dehydrateDevice();
|
||||
}
|
||||
}
|
||||
private async dehydrateDevice(): Promise<void> {
|
||||
if (this.inProgress) {
|
||||
logger.log("Dehydration already in progress -- not starting new dehydration");
|
||||
return;
|
||||
}
|
||||
this.inProgress = true;
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
try {
|
||||
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
|
||||
|
||||
// update the crypto store with the timestamp
|
||||
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.storeSecretStorePrivateKey(
|
||||
txn, "dehydration",
|
||||
{
|
||||
keyInfo: this.keyInfo,
|
||||
key,
|
||||
deviceDisplayName: this.deviceDisplayName,
|
||||
time: Date.now(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
logger.log("Attempting to dehydrate device");
|
||||
|
||||
logger.log("Creating account");
|
||||
// create the account and all the necessary keys
|
||||
const account = new global.Olm.Account();
|
||||
account.create();
|
||||
const e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
const maxKeys = account.max_number_of_one_time_keys();
|
||||
// FIXME: generate in small batches?
|
||||
account.generate_one_time_keys(maxKeys / 2);
|
||||
account.generate_fallback_key();
|
||||
const otks: Record<string, string> = JSON.parse(account.one_time_keys());
|
||||
const fallbacks: Record<string, string> = JSON.parse(account.fallback_key());
|
||||
account.mark_keys_as_published();
|
||||
|
||||
// dehydrate the account and store it on the server
|
||||
const pickledAccount = account.pickle(new Uint8Array(this.key));
|
||||
|
||||
const deviceData: {[props: string]: any} = {
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: pickledAccount,
|
||||
};
|
||||
if (this.keyInfo.passphrase) {
|
||||
deviceData.passphrase = this.keyInfo.passphrase;
|
||||
}
|
||||
|
||||
logger.log("Uploading account to server");
|
||||
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
|
||||
undefined,
|
||||
"PUT",
|
||||
"/dehydrated_device",
|
||||
undefined,
|
||||
{
|
||||
device_data: deviceData,
|
||||
initial_device_display_name: this.deviceDisplayName,
|
||||
},
|
||||
{
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
|
||||
},
|
||||
);
|
||||
|
||||
// send the keys to the server
|
||||
const deviceId = dehydrateResult.device_id;
|
||||
logger.log("Preparing device keys", deviceId);
|
||||
const deviceKeys: DeviceKeys = {
|
||||
algorithms: this.crypto._supportedAlgorithms,
|
||||
device_id: deviceId,
|
||||
user_id: this.crypto._userId,
|
||||
keys: {
|
||||
[`ed25519:${deviceId}`]: e2eKeys.ed25519,
|
||||
[`curve25519:${deviceId}`]: e2eKeys.curve25519,
|
||||
},
|
||||
};
|
||||
const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
|
||||
deviceKeys.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[`ed25519:${deviceId}`]: deviceSignature,
|
||||
},
|
||||
};
|
||||
if (this.crypto._crossSigningInfo.getId("self_signing")) {
|
||||
await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing");
|
||||
}
|
||||
|
||||
logger.log("Preparing one-time keys");
|
||||
const oneTimeKeys = {};
|
||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||
const k: OneTimeKey = {key};
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[`ed25519:${deviceId}`]: signature,
|
||||
},
|
||||
};
|
||||
oneTimeKeys[`signed_curve25519:${keyId}`] = k;
|
||||
}
|
||||
|
||||
logger.log("Preparing fallback keys");
|
||||
const fallbackKeys = {};
|
||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
||||
const k: OneTimeKey = {key, fallback: true};
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[`ed25519:${deviceId}`]: signature,
|
||||
},
|
||||
};
|
||||
fallbackKeys[`signed_curve25519:${keyId}`] = k;
|
||||
}
|
||||
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto._baseApis._http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
undefined,
|
||||
{
|
||||
"device_keys": deviceKeys,
|
||||
"one_time_keys": oneTimeKeys,
|
||||
"org.matrix.msc2732.fallback_keys": fallbackKeys,
|
||||
},
|
||||
);
|
||||
logger.log("Done dehydrating");
|
||||
|
||||
// dehydrate again in a week
|
||||
this.timeoutId = global.setTimeout(
|
||||
this.dehydrateDevice.bind(this), oneweek,
|
||||
);
|
||||
} finally {
|
||||
this.inProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+359
-167
@@ -56,6 +56,7 @@ import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDevice
|
||||
import {IllegalMethod} from "./verification/IllegalMethod";
|
||||
import {KeySignatureUploadError} from "../errors";
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import {DehydrationManager} from './dehydration';
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -146,7 +147,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
method,
|
||||
);
|
||||
} else {
|
||||
console.warn(`Excluding unknown verification method ${method}`);
|
||||
logger.warn(`Excluding unknown verification method ${method}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -243,6 +244,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
baseApis, cryptoCallbacks,
|
||||
);
|
||||
|
||||
this._dehydrationManager = new DehydrationManager(this);
|
||||
|
||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||
if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
|
||||
cryptoCallbacks.getCrossSigningKey = async (type) => {
|
||||
@@ -406,14 +409,12 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
|
||||
|
||||
/**
|
||||
* Checks whether cross signing:
|
||||
* - is enabled on this account
|
||||
* - is trusted by this device
|
||||
* - has private keys stored in secret storage
|
||||
* and that the account has a secret storage key
|
||||
* - is enabled on this account and trusted by this device
|
||||
* - has private keys either cached locally or stored in secret storage
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* If this function returns false, bootstrapCrossSigning() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes sucessfully, this function should
|
||||
* bootstrapCrossSigning() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
@@ -422,24 +423,188 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
|
||||
*/
|
||||
Crypto.prototype.isCrossSigningReady = async function() {
|
||||
const publicKeysOnDevice = this._crossSigningInfo.getId();
|
||||
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
const privateKeysExistSomewhere = (
|
||||
await this._crossSigningInfo.isStoredInKeyCache() ||
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
)
|
||||
);
|
||||
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
|
||||
|
||||
return (
|
||||
return !!(
|
||||
publicKeysOnDevice &&
|
||||
privateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
privateKeysExistSomewhere
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether secret storage:
|
||||
* - is enabled on this account
|
||||
* - is storing cross-signing private keys
|
||||
* - is storing session backup key (if enabled)
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @return {bool} True if secret storage is ready to be used on this device
|
||||
*/
|
||||
Crypto.prototype.isSecretStorageReady = async function() {
|
||||
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
|
||||
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
);
|
||||
const sessionBackupInStorage = (
|
||||
!this._baseApis.getKeyBackupEnabled() ||
|
||||
this._baseApis.isKeyBackupKeyStored()
|
||||
);
|
||||
|
||||
return !!(
|
||||
secretStorageKeyInAccount &&
|
||||
privateKeysInStorage &&
|
||||
sessionBackupInStorage
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key and
|
||||
* signing it with the cross-signing master key. If everything is already set
|
||||
* up, then no changes are made, so this is safe to run to ensure secret storage
|
||||
* is ready for use.
|
||||
* Bootstrap cross-signing by creating keys if needed. If everything is already
|
||||
* set up, then no changes are made, so this is safe to run to ensure
|
||||
* cross-signing is ready for use.
|
||||
*
|
||||
* This function:
|
||||
* - creates new cross-signing keys if they are not found locally cached nor in
|
||||
* secret storage (if it has been setup)
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {function} opts.authUploadDeviceSigningKeys Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
|
||||
* already exist.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
*/
|
||||
Crypto.prototype.bootstrapCrossSigning = async function({
|
||||
authUploadDeviceSigningKeys,
|
||||
setupNewCrossSigning,
|
||||
} = {}) {
|
||||
logger.log("Bootstrapping cross-signing");
|
||||
|
||||
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
this._baseApis.store.accountData,
|
||||
delegateCryptoCallbacks,
|
||||
);
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
this._userId,
|
||||
builder.crossSigningCallbacks,
|
||||
builder.crossSigningCallbacks,
|
||||
);
|
||||
|
||||
// Reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
crossSigningInfo.resetKeys();
|
||||
// Sign master key with device key
|
||||
await this._signObject(crossSigningInfo.keys.master);
|
||||
|
||||
// Store auth flow helper function, as we need to call it when uploading
|
||||
// to ensure we handle auth errors properly.
|
||||
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
|
||||
|
||||
// Cross-sign own device
|
||||
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
|
||||
const deviceSignature = await crossSigningInfo.signDevice(this._userId, device);
|
||||
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
||||
|
||||
// Sign message key backup with cross-signing master key
|
||||
if (this.backupInfo) {
|
||||
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
|
||||
builder.addSessionBackup(this.backupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const publicKeysOnDevice = this._crossSigningInfo.getId();
|
||||
const privateKeysInCache = await this._crossSigningInfo.isStoredInKeyCache();
|
||||
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
|
||||
this._secretStorage,
|
||||
);
|
||||
const privateKeysExistSomewhere = (
|
||||
privateKeysInCache ||
|
||||
privateKeysInStorage
|
||||
);
|
||||
|
||||
// Log all relevant state for easier parsing of debug logs.
|
||||
logger.log({
|
||||
setupNewCrossSigning,
|
||||
publicKeysOnDevice,
|
||||
privateKeysInCache,
|
||||
privateKeysInStorage,
|
||||
privateKeysExistSomewhere,
|
||||
});
|
||||
|
||||
if (!privateKeysExistSomewhere || setupNewCrossSigning) {
|
||||
logger.log(
|
||||
"Cross-signing private keys not found locally or in secret storage, " +
|
||||
"creating new keys",
|
||||
);
|
||||
// If a user has multiple devices, it important to only call bootstrap
|
||||
// as part of some UI flow (and not silently during startup), as they
|
||||
// may have setup cross-signing on a platform which has not saved keys
|
||||
// to secret storage, and this would reset them. In such a case, you
|
||||
// should prompt the user to verify any existing devices first (and
|
||||
// request private keys from those devices) before calling bootstrap.
|
||||
await resetCrossSigning();
|
||||
} else if (publicKeysOnDevice && privateKeysInCache) {
|
||||
logger.log(
|
||||
"Cross-signing public keys trusted and private keys found locally",
|
||||
);
|
||||
} else if (privateKeysInStorage) {
|
||||
logger.log(
|
||||
"Cross-signing private keys not found locally, but they are available " +
|
||||
"in secret storage, reading storage and caching locally",
|
||||
);
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
}
|
||||
|
||||
// Assuming no app-supplied callback, default to storing new private keys in
|
||||
// secret storage if it exists. If it does not, it is assumed this will be
|
||||
// done as part of setting up secret storage later.
|
||||
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
|
||||
if (
|
||||
crossSigningPrivateKeys.size &&
|
||||
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys
|
||||
) {
|
||||
const secretStorage = new SecretStorage(
|
||||
builder.accountDataClientAdapter,
|
||||
builder.ssssCryptoCallbacks);
|
||||
if (await secretStorage.hasKey()) {
|
||||
logger.log("Storing new cross-signing private keys in secret storage");
|
||||
// This is writing to in-memory account data in
|
||||
// builder.accountDataClientAdapter so won't fail
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
secretStorage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const operation = builder.buildOperation();
|
||||
await operation.apply(this);
|
||||
// This persists private keys and public keys as trusted,
|
||||
// only do this if apply succeeded for now as retry isn't in place yet
|
||||
await builder.persist(this);
|
||||
|
||||
logger.log("Cross-signing ready");
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
|
||||
* already set up, then no changes are made, so this is safe to run to ensure secret
|
||||
* storage is ready for use.
|
||||
*
|
||||
* This function
|
||||
* - creates a new Secure Secret Storage key if no default key exists
|
||||
@@ -449,11 +614,8 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
||||
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
||||
* algorithm is found
|
||||
*
|
||||
* @param {function} opts.authUploadDeviceSigningKeys Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object. Can be called multiple times, first with an empty authDict, to obtain the flows.
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param {function} [opts.createSecretStorageKey] Optional. Function
|
||||
* called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
@@ -471,11 +633,9 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
* Returns:
|
||||
* {Promise} A promise which resolves to key creation data for
|
||||
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
|
||||
* SecretStorage#addKey: an object with `passphrase` etc fields.
|
||||
*/
|
||||
|
||||
Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
authUploadDeviceSigningKeys,
|
||||
createSecretStorageKey = async () => ({ }),
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
@@ -490,11 +650,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
const secretStorage = new SecretStorage(
|
||||
builder.accountDataClientAdapter,
|
||||
builder.ssssCryptoCallbacks);
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
this._userId,
|
||||
builder.crossSigningCallbacks,
|
||||
builder.crossSigningCallbacks);
|
||||
builder.ssssCryptoCallbacks,
|
||||
);
|
||||
|
||||
// the ID of the new SSSS key, if we create one
|
||||
let newKeyId = null;
|
||||
@@ -506,57 +663,28 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
opts.key = privateKey;
|
||||
}
|
||||
|
||||
const keyId = await secretStorage.addKey(
|
||||
const { keyId, keyInfo } = await secretStorage.addKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
||||
);
|
||||
|
||||
if (privateKey) {
|
||||
// make the private key available to encrypt 4S secrets
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
|
||||
}
|
||||
|
||||
await secretStorage.setDefaultKeyId(keyId);
|
||||
return keyId;
|
||||
};
|
||||
|
||||
// reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
crossSigningInfo.resetKeys();
|
||||
// sign master key with device key
|
||||
await this._signObject(crossSigningInfo.keys.master);
|
||||
|
||||
await authUploadDeviceSigningKeys(authDict => {
|
||||
if (authDict) {
|
||||
builder.addCrossSigningKeys(authDict, crossSigningInfo.keys);
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
// This callback also gets called to obtain the IUA flows,
|
||||
// so do a call to obtain those if we don't have the authDict yet
|
||||
// We should get called again at a later point with the authDict.
|
||||
return this._baseApis.uploadDeviceSigningKeys(null, {});
|
||||
}
|
||||
});
|
||||
|
||||
// cross-sign own device
|
||||
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
|
||||
const deviceSignature = await crossSigningInfo.signDevice(this._userId, device);
|
||||
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
||||
|
||||
if (keyBackupInfo) {
|
||||
await crossSigningInfo.signObject(keyBackupInfo.auth_data, "master");
|
||||
builder.addSessionBackup(keyBackupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||
if (!keyInfo.mac) {
|
||||
const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey(
|
||||
{keys: {[keyId]: keyInfo}}, "",
|
||||
);
|
||||
if (key) {
|
||||
const keyData = key[1];
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
|
||||
const privateKey = key[1];
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(privateKey);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
|
||||
@@ -569,46 +697,45 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
|
||||
const oldSSSSKey = await this.getSecretStorageKey();
|
||||
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||
const decryptionKeys =
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||
const storageExists = (
|
||||
!setupNewSecretStorage &&
|
||||
oldKeyInfo &&
|
||||
oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
);
|
||||
|
||||
if (!inStorage && !keyBackupInfo) {
|
||||
// Log all relevant state for easier parsing of debug logs.
|
||||
logger.log({
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
setupNewSecretStorage,
|
||||
storageExists,
|
||||
oldKeyInfo,
|
||||
});
|
||||
|
||||
if (!storageExists && !keyBackupInfo) {
|
||||
// either we don't have anything, or we've been asked to restart
|
||||
// from scratch
|
||||
logger.log(
|
||||
"Cross-signing private keys not found in secret storage, " +
|
||||
"creating new keys",
|
||||
"Secret storage does not exist, creating new storage key",
|
||||
);
|
||||
|
||||
await resetCrossSigning();
|
||||
|
||||
if (
|
||||
setupNewSecretStorage ||
|
||||
!oldKeyInfo ||
|
||||
oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
) {
|
||||
// if we already have a usable default SSSS key and aren't resetting SSSS just use it.
|
||||
// otherwise, create a new one
|
||||
// Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
|
||||
// We could move them to the new key but a) that would mean we'd need to prompt for the old
|
||||
// passphrase, and b) it's not clear that would be the right thing to do anyway.
|
||||
const { keyInfo, privateKey } = await createSecretStorageKey();
|
||||
newKeyId = await createSSSS(keyInfo, privateKey);
|
||||
}
|
||||
} else if (!inStorage && keyBackupInfo) {
|
||||
// if we already have a usable default SSSS key and aren't resetting
|
||||
// SSSS just use it. otherwise, create a new one
|
||||
// Note: we leave the old SSSS key in place: there could be other
|
||||
// secrets using it, in theory. We could move them to the new key but a)
|
||||
// that would mean we'd need to prompt for the old passphrase, and b)
|
||||
// it's not clear that would be the right thing to do anyway.
|
||||
const { keyInfo, privateKey } = await createSecretStorageKey();
|
||||
newKeyId = await createSSSS(keyInfo, privateKey);
|
||||
} else if (!storageExists && keyBackupInfo) {
|
||||
// we have an existing backup, but no SSSS
|
||||
|
||||
logger.log("Secret storage default key not found, using key backup key");
|
||||
logger.log("Secret storage does not exist, using key backup key");
|
||||
|
||||
// if we have the backup key already cached, use it; otherwise use the
|
||||
// callback to prompt for the key
|
||||
const backupKey = await this.getSessionBackupPrivateKey() ||
|
||||
await getKeyBackupPassphrase();
|
||||
|
||||
// create new cross-signing keys
|
||||
await resetCrossSigning();
|
||||
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts = {};
|
||||
|
||||
@@ -632,36 +759,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// Sign the backup with the cross-signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
logger.log("Adding cross signing signature to key backup");
|
||||
await crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
builder.addSessionBackup(keyBackupInfo);
|
||||
} else if (!this._crossSigningInfo.getId()) {
|
||||
// we have SSSS, but we don't know if the server's cross-signing
|
||||
// keys should be trusted
|
||||
logger.log("Cross-signing private keys found in secret storage");
|
||||
|
||||
// TODO: take this use case out of bootstrapping
|
||||
// fetch the private keys and set up our local copy of the keys for
|
||||
// use
|
||||
//
|
||||
// so if some other device resets the cross-signing keys,
|
||||
// we mark them as untrusted from _onDeviceListUserCrossSigningUpdated
|
||||
// you can either fix this by hitting the verify this session which (might?) call this method,
|
||||
// or the reset button in the settings
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
// check the passphrase
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
if (
|
||||
this._crossSigningInfo.getId() &&
|
||||
this._crossSigningInfo.isStoredInKeyCache("master")
|
||||
) {
|
||||
logger.log("Adding cross-signing signature to key backup");
|
||||
await this._crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Cross-signing keys not available, skipping signature on key backup",
|
||||
);
|
||||
}
|
||||
|
||||
builder.addSessionBackup(keyBackupInfo);
|
||||
} else {
|
||||
// we have SSSS and we cross-signing is already set up
|
||||
logger.log("Cross signing keys are present in secret storage");
|
||||
// 4S is already set up
|
||||
logger.log("Secret storage exists");
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
@@ -670,21 +787,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
}
|
||||
}
|
||||
|
||||
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
|
||||
if (crossSigningPrivateKeys.size) {
|
||||
logger.log("Storing cross-signing private keys in secret storage");
|
||||
// Assuming no app-supplied callback, default to storing in SSSS.
|
||||
if (!this._baseApis._cryptoCallbacks.saveCrossSigningKeys) {
|
||||
// this is writing to in-memory account data in builder.accountDataClientAdapter
|
||||
// so won't fail
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
secretStorage,
|
||||
);
|
||||
}
|
||||
// If we have cross-signing private keys cached, store them in secret
|
||||
// storage if they are not there already.
|
||||
if (
|
||||
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys &&
|
||||
await this.isCrossSigningReady() &&
|
||||
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
|
||||
) {
|
||||
logger.log("Copying cross-signing private keys from cache to secret storage");
|
||||
const crossSigningPrivateKeys =
|
||||
await this._crossSigningInfo.getCrossSigningKeysFromCache();
|
||||
// This is writing to in-memory account data in
|
||||
// builder.accountDataClientAdapter so won't fail
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
secretStorage,
|
||||
);
|
||||
}
|
||||
|
||||
if (setupNewKeyBackup && !keyBackupInfo) {
|
||||
logger.log("Creating new message key backup version");
|
||||
const info = await this._baseApis.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
// don't write to secret storage, as it will write to this._secretStorage.
|
||||
@@ -701,16 +823,27 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
};
|
||||
// sign with cross-sign master key
|
||||
await crossSigningInfo.signObject(data.auth_data, "master");
|
||||
|
||||
if (
|
||||
this._crossSigningInfo.getId() &&
|
||||
this._crossSigningInfo.isStoredInKeyCache("master")
|
||||
) {
|
||||
// sign with cross-sign master key
|
||||
logger.log("Adding cross-signing signature to key backup");
|
||||
await this._crossSigningInfo.signObject(data.auth_data, "master");
|
||||
} else {
|
||||
logger.warn(
|
||||
"Cross-signing keys not available, skipping signature on key backup",
|
||||
);
|
||||
}
|
||||
|
||||
// sign with the device fingerprint
|
||||
await this._signObject(data.auth_data);
|
||||
|
||||
|
||||
builder.addSessionBackup(data);
|
||||
}
|
||||
|
||||
// and likewise for the session backup key
|
||||
// Cache the session backup key
|
||||
const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1');
|
||||
if (sessionBackupKey) {
|
||||
logger.info("Got session backup key from secret storage: caching");
|
||||
@@ -1191,23 +1324,20 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
const seenPubkey = newCrossSigning.getId();
|
||||
const masterChanged = this._crossSigningInfo.getId() !== seenPubkey;
|
||||
if (masterChanged) {
|
||||
// try to get the private key if the master key changed
|
||||
logger.info("Got new master public key", seenPubkey);
|
||||
|
||||
logger.info("Attempting to retrieve cross-signing master private key");
|
||||
let signing = null;
|
||||
try {
|
||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||
'master', seenPubkey,
|
||||
);
|
||||
signing = ret[1];
|
||||
if (!signing) {
|
||||
throw new Error("Cross-signing master private key not available");
|
||||
}
|
||||
logger.info("Got cross-signing master private key");
|
||||
} catch (e) {
|
||||
logger.error("Cross-signing master private key not available", e);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
|
||||
logger.info("Got matching private key from callback for new public master key");
|
||||
}
|
||||
|
||||
const oldSelfSigningId = this._crossSigningInfo.getId("self_signing");
|
||||
@@ -1216,10 +1346,26 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
// Update the version of our keys in our cross-signing object and the local store
|
||||
this._storeTrustedSelfKeys(newCrossSigning.keys);
|
||||
|
||||
const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing");
|
||||
const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing");
|
||||
|
||||
const keySignatures = {};
|
||||
|
||||
if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) {
|
||||
if (selfSigningChanged) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
logger.info("Attempting to retrieve cross-signing self-signing private key");
|
||||
let signing = null;
|
||||
try {
|
||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||
"self_signing", newCrossSigning.getId("self_signing"),
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info("Got cross-signing self-signing private key");
|
||||
} catch (e) {
|
||||
logger.error("Cross-signing self-signing private key not available", e);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
|
||||
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
|
||||
const signedDevice = await this._crossSigningInfo.signDevice(
|
||||
@@ -1227,8 +1373,21 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
);
|
||||
keySignatures[this._deviceId] = signedDevice;
|
||||
}
|
||||
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
|
||||
if (userSigningChanged) {
|
||||
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
|
||||
logger.info("Attempting to retrieve cross-signing user-signing private key");
|
||||
let signing = null;
|
||||
try {
|
||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||
"user_signing", newCrossSigning.getId("user_signing"),
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info("Got cross-signing user-signing private key");
|
||||
} catch (e) {
|
||||
logger.error("Cross-signing user-signing private key not available", e);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
}
|
||||
|
||||
if (masterChanged) {
|
||||
@@ -1403,7 +1562,7 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
// uploaded to the new backup. This is a bit of a workaround to upload
|
||||
// keys to a new backup in *most* cases, but it won't cover all cases
|
||||
// because we don't remember what backup version we uploaded keys to:
|
||||
// see https://github.com/vector-im/riot-web/issues/14833
|
||||
// see https://github.com/vector-im/element-web/issues/14833
|
||||
await this.scheduleAllGroupSessionsForBackup();
|
||||
} else {
|
||||
logger.log("Backup version " + backupInfo.version + " still current");
|
||||
@@ -1700,6 +1859,14 @@ Crypto.prototype.updateOneTimeKeyCount = function(currentCount) {
|
||||
}
|
||||
};
|
||||
|
||||
Crypto.prototype.setNeedsNewFallback = function(needsNewFallback) {
|
||||
this._needsNewFallback = !!needsNewFallback;
|
||||
};
|
||||
|
||||
Crypto.prototype.getNeedsNewFallback = function() {
|
||||
return this._needsNewFallback;
|
||||
};
|
||||
|
||||
// check if it's time to upload one-time keys, and do so if so.
|
||||
function _maybeUploadOneTimeKeys(crypto) {
|
||||
// frequency with which to check & upload one-time keys
|
||||
@@ -1747,27 +1914,31 @@ function _maybeUploadOneTimeKeys(crypto) {
|
||||
// out stale private keys that won't receive a message.
|
||||
const keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||||
|
||||
function uploadLoop(keyCount) {
|
||||
if (keyLimit <= keyCount) {
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return Promise.resolve();
|
||||
}
|
||||
async function uploadLoop(keyCount) {
|
||||
while (keyLimit > keyCount || crypto.getNeedsNewFallback()) {
|
||||
// Ask olm to generate new one time keys, then upload them to synapse.
|
||||
if (keyLimit > keyCount) {
|
||||
logger.info("generating oneTimeKeys");
|
||||
const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
|
||||
await crypto._olmDevice.generateOneTimeKeys(keysThisLoop);
|
||||
}
|
||||
|
||||
const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
|
||||
if (crypto.getNeedsNewFallback()) {
|
||||
logger.info("generating fallback key");
|
||||
await crypto._olmDevice.generateFallbackKey();
|
||||
}
|
||||
|
||||
// Ask olm to generate new one time keys, then upload them to synapse.
|
||||
return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(() => {
|
||||
return _uploadOneTimeKeys(crypto);
|
||||
}).then((res) => {
|
||||
logger.info("calling _uploadOneTimeKeys");
|
||||
const res = await _uploadOneTimeKeys(crypto);
|
||||
if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) {
|
||||
// if the response contains a more up to date value use this
|
||||
// for the next loop
|
||||
return uploadLoop(res.one_time_key_counts.signed_curve25519);
|
||||
keyCount = res.one_time_key_counts.signed_curve25519;
|
||||
} else {
|
||||
throw new Error("response for uploading keys does not contain "
|
||||
+ "one_time_key_counts.signed_curve25519");
|
||||
throw new Error("response for uploading keys does not contain " +
|
||||
"one_time_key_counts.signed_curve25519");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
crypto._oneTimeKeyCheckInProgress = true;
|
||||
@@ -1799,11 +1970,22 @@ function _maybeUploadOneTimeKeys(crypto) {
|
||||
|
||||
// returns a promise which resolves to the response
|
||||
async function _uploadOneTimeKeys(crypto) {
|
||||
const promises = [];
|
||||
|
||||
const fallbackJson = {};
|
||||
if (crypto.getNeedsNewFallback()) {
|
||||
const fallbackKeys = await crypto._olmDevice.getFallbackKey();
|
||||
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||
const k = { key, fallback: true };
|
||||
fallbackJson["signed_curve25519:" + keyId] = k;
|
||||
promises.push(crypto._signObject(k));
|
||||
}
|
||||
crypto.setNeedsNewFallback(false);
|
||||
}
|
||||
|
||||
const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys();
|
||||
const oneTimeJson = {};
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
const k = {
|
||||
@@ -1817,7 +1999,8 @@ async function _uploadOneTimeKeys(crypto) {
|
||||
await Promise.all(promises);
|
||||
|
||||
const res = await crypto._baseApis.uploadKeysRequest({
|
||||
one_time_keys: oneTimeJson,
|
||||
"one_time_keys": oneTimeJson,
|
||||
"org.matrix.msc2732.fallback_keys": fallbackJson,
|
||||
});
|
||||
|
||||
await crypto._olmDevice.markKeysAsPublished();
|
||||
@@ -2004,9 +2187,18 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
// do cross-signing
|
||||
if (verified && userId === this._userId) {
|
||||
logger.info("Own device " + deviceId + " marked verified: signing");
|
||||
const device = await this._crossSigningInfo.signDevice(
|
||||
userId, DeviceInfo.fromStorage(dev, deviceId),
|
||||
);
|
||||
|
||||
// Signing only needed if other device not already signed
|
||||
let device;
|
||||
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
|
||||
if (deviceTrust.isCrossSigningVerified()) {
|
||||
logger.log(`Own device ${deviceId} already cross-signing verified`);
|
||||
} else {
|
||||
device = await this._crossSigningInfo.signDevice(
|
||||
userId, DeviceInfo.fromStorage(dev, deviceId),
|
||||
);
|
||||
}
|
||||
|
||||
if (device) {
|
||||
const upload = async ({shouldEmit}) => {
|
||||
logger.info("Uploading signature for " + deviceId);
|
||||
@@ -2235,7 +2427,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
if (claimedKey !== device.getFingerprint()) {
|
||||
logger.warn(
|
||||
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
|
||||
"but sender device has key " + device.getFingerprint());
|
||||
" but sender device has key " + device.getFingerprint());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2410,7 +2602,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
|
||||
|
||||
await this.trackRoomDevices(roomId);
|
||||
// TODO: this flag is only not used from MatrixClient::setRoomEncryption
|
||||
// which is never used (inside riot at least)
|
||||
// which is never used (inside Element at least)
|
||||
// but didn't want to remove it as it technically would
|
||||
// be a breaking change.
|
||||
if (!this.inhibitDeviceQuery) {
|
||||
@@ -2984,7 +3176,7 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
|
||||
// we don't start uploading one-time keys until we've caught up with
|
||||
// to-device messages, to help us avoid throwing away one-time-keys that we
|
||||
// are about to receive messages for
|
||||
// (https://github.com/vector-im/riot-web/issues/2782).
|
||||
// (https://github.com/vector-im/element-web/issues/2782).
|
||||
if (!syncData.catchingUp) {
|
||||
_maybeUploadOneTimeKeys(this);
|
||||
this._processReceivedRoomKeyRequests();
|
||||
|
||||
@@ -25,8 +25,7 @@ import {EventEmitter} from 'events';
|
||||
import {logger} from '../../logger';
|
||||
import {DeviceInfo} from '../deviceinfo';
|
||||
import {newTimeoutError} from "./Error";
|
||||
import {CrossSigningInfo} from "../CrossSigning";
|
||||
import {decodeBase64} from "../olmlib";
|
||||
import {requestKeysDuringVerification} from "../CrossSigning";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
@@ -79,8 +78,6 @@ export class VerificationBase extends EventEmitter {
|
||||
this._transactionTimeoutTimer = null;
|
||||
}
|
||||
|
||||
static keyRequestTimeoutMs = 1000 * 60;
|
||||
|
||||
get initiatedByMe() {
|
||||
// if there is no start event yet,
|
||||
// we probably want to send it,
|
||||
@@ -198,93 +195,7 @@ export class VerificationBase extends EventEmitter {
|
||||
if (!this._done) {
|
||||
this.request.onVerifierFinished();
|
||||
this._resolve();
|
||||
|
||||
//#region Cross-signing keys request
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (this._baseApis.getUserId() !== this.userId) {
|
||||
return;
|
||||
}
|
||||
// FIXME: This is a lot of logic that isn't anything to do with verification
|
||||
// and probably ought to be somewhere else.
|
||||
console.log("VerificationBase.done: Self-verification done; requesting keys");
|
||||
/* This happens asynchronously, and we're not concerned about
|
||||
* waiting for it. We return here in order to test. */
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = this._baseApis;
|
||||
const original = client._crypto._crossSigningInfo;
|
||||
|
||||
/* We already have all of the infrastructure we need to validate and
|
||||
* cache cross-signing keys, so instead of replicating that, here we
|
||||
* set up callbacks that request them from the other device and call
|
||||
* CrossSigningInfo.getCrossSigningKey() to validate/cache */
|
||||
const crossSigning = new CrossSigningInfo(
|
||||
original.userId,
|
||||
{ getCrossSigningKey: async (type) => {
|
||||
console.debug("VerificationBase.done: requesting secret",
|
||||
type, this.deviceId);
|
||||
const { promise } = client.requestSecret(
|
||||
`m.cross_signing.${type}`, [this.deviceId],
|
||||
);
|
||||
const result = await promise;
|
||||
const decoded = decodeBase64(result);
|
||||
return Uint8Array.from(decoded);
|
||||
} },
|
||||
original._cacheCallbacks,
|
||||
);
|
||||
crossSigning.keys = original.keys;
|
||||
|
||||
// XXX: get all keys out if we get one key out
|
||||
// https://github.com/vector-im/riot-web/issues/12604
|
||||
// then change here to reject on the timeout
|
||||
/* Requests can be ignored, so don't wait around forever */
|
||||
const timeout = new Promise((resolve, reject) => {
|
||||
setTimeout(
|
||||
resolve,
|
||||
VerificationBase.keyRequestTimeoutMs,
|
||||
new Error("Timeout"),
|
||||
);
|
||||
});
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = new Promise(async resolve => {
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
const secretReq = client.requestSecret(
|
||||
'm.megolm_backup.v1', [this.deviceId],
|
||||
);
|
||||
const base64Key = await secretReq.promise;
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client._crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
// no need to await for this - just let it go in the bg
|
||||
client.restoreKeyBackupWithCache(
|
||||
undefined, undefined, backupInfo,
|
||||
).then(() => {
|
||||
logger.info("Backup restored.");
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
/* We call getCrossSigningKey() for its side-effects */
|
||||
return Promise.race([
|
||||
Promise.all([
|
||||
crossSigning.getCrossSigningKey("self_signing"),
|
||||
crossSigning.getCrossSigningKey("user_signing"),
|
||||
backupKeyPromise,
|
||||
]),
|
||||
timeout,
|
||||
]).then(resolve, reject);
|
||||
}).catch((e) => {
|
||||
console.warn("VerificationBase: failure while requesting keys:", e);
|
||||
});
|
||||
//#endregion
|
||||
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
|
||||
import {logger} from '../../logger';
|
||||
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
@@ -94,7 +95,7 @@ export class ReciprocateQRCode extends Base {
|
||||
if (!targetKey) throw newKeyMismatchError();
|
||||
|
||||
if (keyInfo !== targetKey) {
|
||||
console.error("key ID from key info does not match");
|
||||
logger.error("key ID from key info does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
for (const deviceKeyId in device.keys) {
|
||||
@@ -102,7 +103,7 @@ export class ReciprocateQRCode extends Base {
|
||||
const deviceTargetKey = keys[deviceKeyId];
|
||||
if (!deviceTargetKey) throw newKeyMismatchError();
|
||||
if (device.keys[deviceKeyId] !== deviceTargetKey) {
|
||||
console.error("master key does not match");
|
||||
logger.error("master key does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -651,12 +651,10 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
const self = this;
|
||||
if (this.opts.extraParams) {
|
||||
for (const key in this.opts.extraParams) {
|
||||
if (!this.opts.extraParams.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
queryParams[key] = this.opts.extraParams[key];
|
||||
}
|
||||
queryParams = {
|
||||
...queryParams,
|
||||
...this.opts.extraParams,
|
||||
};
|
||||
}
|
||||
|
||||
const headers = utils.extend({}, opts.headers || {});
|
||||
@@ -963,7 +961,7 @@ export async function retryNetworkOperation(maxAttempts, callback) {
|
||||
try {
|
||||
if (attempts > 0) {
|
||||
const timeout = 1000 * Math.pow(2, attempts);
|
||||
console.log(`network operation failed ${attempts} times,` +
|
||||
logger.log(`network operation failed ${attempts} times,` +
|
||||
` retrying in ${timeout}ms...`);
|
||||
await new Promise(r => setTimeout(r, timeout));
|
||||
}
|
||||
|
||||
@@ -40,11 +40,13 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
methodName === "warn" ||
|
||||
methodName === "trace" ||
|
||||
methodName === "info";
|
||||
/* eslint-disable no-console */
|
||||
if (supportedByConsole) {
|
||||
return console[methodName](...args);
|
||||
} else {
|
||||
return console.log(...args);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+67
-2
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -113,13 +113,78 @@ export function setCryptoStoreFactory(fac) {
|
||||
cryptoStoreFactory = fac;
|
||||
}
|
||||
|
||||
interface ICreateClientOpts {
|
||||
export interface ICreateClientOpts {
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
store?: Store;
|
||||
cryptoStore?: CryptoStore;
|
||||
scheduler?: MatrixScheduler;
|
||||
request?: Request;
|
||||
userId?: string;
|
||||
deviceId?: string;
|
||||
accessToken?: string;
|
||||
identityServer?: any;
|
||||
localTimeoutMs?: number;
|
||||
useAuthorizationHeader?: boolean;
|
||||
timelineSupport?: boolean;
|
||||
queryParams?: Record<string, unknown>;
|
||||
deviceToImport?: {
|
||||
olmDevice: {
|
||||
pickledAccount: string;
|
||||
sessions: Array<Record<string, any>>;
|
||||
pickleKey: string;
|
||||
};
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
};
|
||||
pickleKey?: string;
|
||||
sessionStore?: any;
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
verificationMethods?: Array<any>;
|
||||
forceTURN?: boolean;
|
||||
fallbackICEServerAllowed?: boolean;
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
}
|
||||
|
||||
export interface ICryptoCallbacks {
|
||||
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
shouldUpgradeDeviceVerifications?: (
|
||||
users: Record<string, any>
|
||||
) => Promise<string[]>;
|
||||
getSecretStorageKey?: (
|
||||
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
cacheSecretStorageKey?: (
|
||||
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
|
||||
) => void;
|
||||
onSecretRequested?: (
|
||||
userId: string, deviceId: string,
|
||||
requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel
|
||||
) => Promise<string>;
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
checkFunc: (Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
// TODO: Move this to `SecretStorage` once converted
|
||||
export interface ISecretStorageKeyInfo {
|
||||
passphrase?: {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
iv?: string;
|
||||
mac?: string;
|
||||
}
|
||||
|
||||
// TODO: Move this to `CrossSigning` once converted
|
||||
export interface IDeviceTrustLevel {
|
||||
isVerified(): boolean;
|
||||
isCrossSigningVerified(): boolean;
|
||||
isLocallyVerified(): boolean;
|
||||
isTofu(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -806,7 +806,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
this.room,
|
||||
);
|
||||
isNewRelations = true;
|
||||
relatesToEvent = this.findEventById(relatesToEventId);
|
||||
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||
if (relatesToEvent) {
|
||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||
}
|
||||
|
||||
+15
-4
@@ -87,7 +87,7 @@ export const MatrixEvent = function(
|
||||
// amount of needless string duplication. This can save moderate amounts of
|
||||
// memory (~10% on a 350MB heap).
|
||||
// 'membership' at the event level (rather than the content level) is a legacy
|
||||
// field that Riot never otherwise looks at, but it will still take up a lot
|
||||
// field that Element never otherwise looks at, but it will still take up a lot
|
||||
// of space if we don't intern it.
|
||||
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
|
||||
if (!event[prop]) {
|
||||
@@ -169,6 +169,14 @@ export const MatrixEvent = function(
|
||||
allows for a unique ID which does not change when the event comes back down sync.
|
||||
*/
|
||||
this._txnId = null;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
* the time between the server putting the 'age' field on the event as it sent
|
||||
* it to us and the time we're now constructing this event, but that's better
|
||||
* than assuming the local clock is in sync with the origin HS's clock.
|
||||
*/
|
||||
this._localTimestamp = Date.now() - this.getAge();
|
||||
};
|
||||
utils.inherits(MatrixEvent, EventEmitter);
|
||||
|
||||
@@ -311,11 +319,12 @@ utils.extend(MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the age of the event when this function was called.
|
||||
* Relies on the local clock being in sync with the clock of the original homeserver.
|
||||
* This is the 'age' field adjusted according to how long this client has
|
||||
* had the event.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getLocalAge: function() {
|
||||
return Date.now() - this.getTs();
|
||||
return Date.now() - this._localTimestamp;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -626,7 +635,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return this.event.type === "m.room.encrypted";
|
||||
return !this.isState() && this.event.type === "m.room.encrypted";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -904,6 +913,8 @@ utils.extend(MatrixEvent.prototype, {
|
||||
/**
|
||||
* Set an event that replaces the content of this event, through an m.replace relation.
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.replaced"
|
||||
*
|
||||
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
|
||||
*/
|
||||
makeReplaced(newEvent) {
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {EventStatus} from '../models/event';
|
||||
import {logger} from '../logger';
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
@@ -60,7 +61,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
console.error("Event must have relation info");
|
||||
logger.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ export class Relations extends EventEmitter {
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
logger.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
console.error("Event must have relation info");
|
||||
logger.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ export class Relations extends EventEmitter {
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
logger.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+16
-4
@@ -358,21 +358,33 @@ Room.prototype.getPendingEvents = function() {
|
||||
|
||||
/**
|
||||
* Check whether the pending event list contains a given event by ID.
|
||||
* If pending event ordering is not "detached" then this returns false.
|
||||
*
|
||||
* @param {string} eventId The event ID to check for.
|
||||
* @return {boolean}
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
Room.prototype.hasPendingEvent = function(eventId) {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
throw new Error(
|
||||
"Cannot call hasPendingEvent with pendingEventOrdering == " +
|
||||
this._opts.pendingEventOrdering);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._pendingEventList.some(event => event.getId() === eventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a specific event from the pending event list, if configured, null otherwise.
|
||||
*
|
||||
* @param {string} eventId The event ID to check for.
|
||||
* @return {MatrixEvent}
|
||||
*/
|
||||
Room.prototype.getPendingEvent = function(eventId) {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._pendingEventList.find(event => event.getId() === eventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the live unfiltered timeline for this room.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
|
||||
import {logger} from './logger';
|
||||
|
||||
/**
|
||||
* @module pushprocessor
|
||||
@@ -444,7 +445,7 @@ PushProcessor.rewriteDefaultRules = function(incomingRules) {
|
||||
} else {
|
||||
// Add the rule
|
||||
const ruleId = override.rule_id;
|
||||
console.warn(`Adding default global override for ${ruleId}`);
|
||||
logger.warn(`Adding default global override for ${ruleId}`);
|
||||
globalOverrides.push(override);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
account_data: {
|
||||
events: accountData,
|
||||
},
|
||||
});
|
||||
}, true);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -416,7 +416,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
},
|
||||
|
||||
syncToDatabase: function(userTuples) {
|
||||
const syncData = this._syncAccumulator.getJSON();
|
||||
const syncData = this._syncAccumulator.getJSON(true);
|
||||
|
||||
return Promise.all([
|
||||
this._persistUserPresenceEvents(userTuples),
|
||||
|
||||
+47
-12
@@ -87,8 +87,8 @@ export class SyncAccumulator {
|
||||
};
|
||||
}
|
||||
|
||||
accumulate(syncResponse) {
|
||||
this._accumulateRooms(syncResponse);
|
||||
accumulate(syncResponse, fromDatabase) {
|
||||
this._accumulateRooms(syncResponse, fromDatabase);
|
||||
this._accumulateGroups(syncResponse);
|
||||
this._accumulateAccountData(syncResponse);
|
||||
this.nextBatch = syncResponse.next_batch;
|
||||
@@ -107,35 +107,36 @@ export class SyncAccumulator {
|
||||
/**
|
||||
* Accumulate incremental /sync room data.
|
||||
* @param {Object} syncResponse the complete /sync JSON
|
||||
* @param {boolean} fromDatabase True if the sync response is one saved to the database
|
||||
*/
|
||||
_accumulateRooms(syncResponse) {
|
||||
_accumulateRooms(syncResponse, fromDatabase) {
|
||||
if (!syncResponse.rooms) {
|
||||
return;
|
||||
}
|
||||
if (syncResponse.rooms.invite) {
|
||||
Object.keys(syncResponse.rooms.invite).forEach((roomId) => {
|
||||
this._accumulateRoom(
|
||||
roomId, "invite", syncResponse.rooms.invite[roomId],
|
||||
roomId, "invite", syncResponse.rooms.invite[roomId], fromDatabase,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (syncResponse.rooms.join) {
|
||||
Object.keys(syncResponse.rooms.join).forEach((roomId) => {
|
||||
this._accumulateRoom(
|
||||
roomId, "join", syncResponse.rooms.join[roomId],
|
||||
roomId, "join", syncResponse.rooms.join[roomId], fromDatabase,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (syncResponse.rooms.leave) {
|
||||
Object.keys(syncResponse.rooms.leave).forEach((roomId) => {
|
||||
this._accumulateRoom(
|
||||
roomId, "leave", syncResponse.rooms.leave[roomId],
|
||||
roomId, "leave", syncResponse.rooms.leave[roomId], fromDatabase,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_accumulateRoom(roomId, category, data) {
|
||||
_accumulateRoom(roomId, category, data, fromDatabase) {
|
||||
// Valid /sync state transitions
|
||||
// +--------+ <======+ 1: Accept an invite
|
||||
// +== | INVITE | | (5) 2: Leave a room
|
||||
@@ -159,7 +160,7 @@ export class SyncAccumulator {
|
||||
delete this.inviteRooms[roomId];
|
||||
}
|
||||
// (3)
|
||||
this._accumulateJoinState(roomId, data);
|
||||
this._accumulateJoinState(roomId, data, fromDatabase);
|
||||
break;
|
||||
case "leave":
|
||||
if (this.inviteRooms[roomId]) { // (4)
|
||||
@@ -203,7 +204,7 @@ export class SyncAccumulator {
|
||||
}
|
||||
|
||||
// Accumulate timeline and state events in a room.
|
||||
_accumulateJoinState(roomId, data) {
|
||||
_accumulateJoinState(roomId, data, fromDatabase) {
|
||||
// We expect this function to be called a lot (every /sync) so we want
|
||||
// this to be fast. /sync stores events in an array but we often want
|
||||
// to clobber based on type/state_key. Rather than convert arrays to
|
||||
@@ -337,8 +338,20 @@ export class SyncAccumulator {
|
||||
setState(currentData._currentState, e);
|
||||
// append the event to the timeline. The back-pagination token
|
||||
// corresponds to the first event in the timeline
|
||||
let transformedEvent;
|
||||
if (!fromDatabase) {
|
||||
transformedEvent = Object.assign({}, e);
|
||||
if (transformedEvent.unsigned !== undefined) {
|
||||
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
|
||||
}
|
||||
const age = e.unsigned ? e.unsigned.age : e.age;
|
||||
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
|
||||
} else {
|
||||
transformedEvent = e;
|
||||
}
|
||||
|
||||
currentData._timeline.push({
|
||||
event: e,
|
||||
event: transformedEvent,
|
||||
token: index === 0 ? data.timeline.prev_batch : null,
|
||||
});
|
||||
});
|
||||
@@ -405,6 +418,7 @@ export class SyncAccumulator {
|
||||
* represents all room data that should be stored. This should be paired
|
||||
* with the sync token which represents the most recent /sync response
|
||||
* provided to accumulate().
|
||||
* @param {boolean} forDatabase True to generate a sync to be saved to storage
|
||||
* @return {Object} An object with a "nextBatch", "roomsData" and "accountData"
|
||||
* keys.
|
||||
* The "nextBatch" key is a string which represents at what point in the
|
||||
@@ -414,7 +428,7 @@ export class SyncAccumulator {
|
||||
* /sync response from the 'rooms' key onwards. The "accountData" key is
|
||||
* a list of raw events which represent global account data.
|
||||
*/
|
||||
getJSON() {
|
||||
getJSON(forDatabase) {
|
||||
const data = {
|
||||
join: {},
|
||||
invite: {},
|
||||
@@ -486,7 +500,28 @@ export class SyncAccumulator {
|
||||
}
|
||||
roomJson.timeline.prev_batch = msgData.token;
|
||||
}
|
||||
roomJson.timeline.events.push(msgData.event);
|
||||
|
||||
let transformedEvent;
|
||||
if (!forDatabase && msgData.event._localTs) {
|
||||
// This means we have to copy each event so we can fix it up to
|
||||
// set a correct 'age' parameter whilst keeping the local timestamp
|
||||
// on our stored event. If this turns out to be a bottleneck, it could
|
||||
// be optimised either by doing this in the main process after the data
|
||||
// has been structured-cloned to go between the worker & main process,
|
||||
// or special-casing data from saved syncs to read the local timstamp
|
||||
// directly rather than turning it into age to then immediately be
|
||||
// transformed back again into a local timestamp.
|
||||
transformedEvent = Object.assign({}, msgData.event);
|
||||
if (transformedEvent.unsigned !== undefined) {
|
||||
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
|
||||
}
|
||||
delete transformedEvent._localTs;
|
||||
transformedEvent.unsigned = transformedEvent.unsigned || {};
|
||||
transformedEvent.unsigned.age = Date.now() - msgData.event._localTs;
|
||||
} else {
|
||||
transformedEvent = msgData.event;
|
||||
}
|
||||
roomJson.timeline.events.push(transformedEvent);
|
||||
});
|
||||
|
||||
// Add state data: roll back current state to the start of timeline,
|
||||
|
||||
+10
@@ -1361,6 +1361,16 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
|
||||
this.opts.crypto.updateOneTimeKeyCount(currentCount);
|
||||
}
|
||||
if (this.opts.crypto && data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
|
||||
// The presence of device_unused_fallback_key_types indicates that the
|
||||
// server supports fallback keys. If there's no unused
|
||||
// signed_curve25519 fallback key we need a new one.
|
||||
const unusedFallbackKeys = data["org.matrix.msc2732.device_unused_fallback_key_types"];
|
||||
this.opts.crypto.setNeedsNewFallback(
|
||||
unusedFallbackKeys instanceof Array &&
|
||||
!unusedFallbackKeys.includes("signed_curve25519"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
-1400
File diff suppressed because it is too large
Load Diff
+1393
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MatrixEvent from '../models/event';
|
||||
import {logger} from '../logger';
|
||||
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
|
||||
import { EventType } from '../@types/event';
|
||||
import { MatrixClient } from '../client';
|
||||
|
||||
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
|
||||
// time to press the 'accept' button
|
||||
const RING_GRACE_PERIOD = 3000;
|
||||
|
||||
export class CallEventHandler {
|
||||
client: MatrixClient;
|
||||
calls: Map<string, MatrixCall>;
|
||||
callEventBuffer: MatrixEvent[];
|
||||
candidateEventsByCall: Map<string, Array<MatrixEvent>>;
|
||||
|
||||
constructor(client: MatrixClient) {
|
||||
this.client = client;
|
||||
this.calls = new Map<string, MatrixCall>();
|
||||
// The sync code always emits one event at a time, so it will patiently
|
||||
// wait for us to finish processing a call invite before delivering the
|
||||
// next event, even if that next event is a hangup. We therefore accumulate
|
||||
// all our call events and then process them on the 'sync' event, ie.
|
||||
// each time a sync has completed. This way, we can avoid emitting incoming
|
||||
// call events if we get both the invite and answer/hangup in the same sync.
|
||||
// This happens quite often, eg. replaying sync from storage, catchup sync
|
||||
// after loading and after we've been offline for a bit.
|
||||
this.callEventBuffer = [];
|
||||
this.candidateEventsByCall = new Map<string, Array<MatrixEvent>>();
|
||||
this.client.on("sync", this.evaluateEventBuffer);
|
||||
this.client.on("event", this.onEvent);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||
this.client.removeListener("event", this.onEvent);
|
||||
}
|
||||
|
||||
private evaluateEventBuffer = () => {
|
||||
if (this.client.getSyncState() === "SYNCING") {
|
||||
// don't process any events until they are all decrypted
|
||||
if (this.callEventBuffer.some((e) => e.isBeingDecrypted())) return;
|
||||
|
||||
const ignoreCallIds = new Set<String>();
|
||||
// inspect the buffer and mark all calls which have been answered
|
||||
// or hung up before passing them to the call event handler.
|
||||
for (const ev of this.callEventBuffer) {
|
||||
if (ev.getType() === EventType.CallAnswer ||
|
||||
ev.getType() === EventType.CallHangup) {
|
||||
ignoreCallIds.add(ev.getContent().call_id);
|
||||
}
|
||||
}
|
||||
// now loop through the buffer chronologically and inject them
|
||||
for (const e of this.callEventBuffer) {
|
||||
if (
|
||||
e.getType() === EventType.CallInvite &&
|
||||
ignoreCallIds.has(e.getContent().call_id)
|
||||
) {
|
||||
// This call has previously been answered or hung up: ignore it
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
this.handleCallEvent(e);
|
||||
} catch (e) {
|
||||
logger.error("Caught exception handling call event", e);
|
||||
}
|
||||
}
|
||||
this.callEventBuffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
private onEvent = (event: MatrixEvent) => {
|
||||
// any call events or ones that might be once they're decrypted
|
||||
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
|
||||
// queue up for processing once all events from this sync have been
|
||||
// processed (see above).
|
||||
this.callEventBuffer.push(event);
|
||||
}
|
||||
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once("Event.decrypted", () => {
|
||||
if (event.getType().indexOf("m.call.") === -1) return;
|
||||
|
||||
if (this.callEventBuffer.includes(event)) {
|
||||
// we were waiting for that event to decrypt, so recheck the buffer
|
||||
this.evaluateEventBuffer();
|
||||
} else {
|
||||
// This one wasn't buffered so just run the event handler for it
|
||||
// straight away
|
||||
try {
|
||||
this.handleCallEvent(event);
|
||||
} catch (e) {
|
||||
logger.error("Caught exception handling call event", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleCallEvent(event: MatrixEvent) {
|
||||
const content = event.getContent();
|
||||
let call = content.call_id ? this.calls.get(content.call_id) : undefined;
|
||||
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
|
||||
|
||||
if (event.getType() === EventType.CallInvite) {
|
||||
if (event.getSender() === this.client.credentials.userId) {
|
||||
return; // ignore invites you send
|
||||
}
|
||||
|
||||
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) {
|
||||
return; // expired call
|
||||
}
|
||||
|
||||
if (call && call.state === CallState.Ended) {
|
||||
return; // stale/old invite event
|
||||
}
|
||||
if (call) {
|
||||
logger.log(
|
||||
`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` +
|
||||
`invite. Clobbering.`,
|
||||
);
|
||||
}
|
||||
|
||||
call = createNewMatrixCall(this.client, event.getRoomId(), {
|
||||
forceTURN: this.client._forceTURN,
|
||||
});
|
||||
if (!call) {
|
||||
logger.log(
|
||||
"Incoming call ID " + content.call_id + " but this client " +
|
||||
"doesn't support WebRTC",
|
||||
);
|
||||
// don't hang up the call: there could be other clients
|
||||
// connected that do support WebRTC and declining the
|
||||
// the call on their behalf would be really annoying.
|
||||
return;
|
||||
}
|
||||
|
||||
call.callId = content.call_id;
|
||||
call.initWithInvite(event);
|
||||
this.calls.set(call.callId, call);
|
||||
|
||||
// if we stashed candidate events for that call ID, play them back now
|
||||
if (this.candidateEventsByCall.get(call.callId)) {
|
||||
for (const ev of this.candidateEventsByCall.get(call.callId)) {
|
||||
call.onRemoteIceCandidatesReceived(ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Were we trying to call that user (room)?
|
||||
let existingCall;
|
||||
for (const thisCall of this.calls.values()) {
|
||||
const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes(
|
||||
thisCall.state,
|
||||
);
|
||||
|
||||
if (
|
||||
call.roomId === thisCall.roomId &&
|
||||
thisCall.direction === CallDirection.Outbound &&
|
||||
isCalling
|
||||
) {
|
||||
existingCall = thisCall;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingCall) {
|
||||
// If we've only got to wait_local_media or create_offer and
|
||||
// we've got an invite, pick the incoming call because we know
|
||||
// we haven't sent our invite yet otherwise, pick whichever
|
||||
// call has the lowest call ID (by string comparison)
|
||||
if (existingCall.state === CallState.WaitLocalMedia ||
|
||||
existingCall.state === CallState.CreateOffer ||
|
||||
existingCall.callId > call.callId) {
|
||||
logger.log(
|
||||
"Glare detected: answering incoming call " + call.callId +
|
||||
" and canceling outgoing call " + existingCall.callId,
|
||||
);
|
||||
existingCall.replacedBy(call);
|
||||
call.answer();
|
||||
} else {
|
||||
logger.log(
|
||||
"Glare detected: rejecting incoming call " + call.callId +
|
||||
" and keeping outgoing call " + existingCall.callId,
|
||||
);
|
||||
call.hangup(CallErrorCode.Replaced, true);
|
||||
}
|
||||
} else {
|
||||
this.client.emit("Call.incoming", call);
|
||||
}
|
||||
} else if (event.getType() === EventType.CallAnswer) {
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
if (event.getSender() === this.client.credentials.userId) {
|
||||
if (call.state === CallState.Ringing) {
|
||||
call.onAnsweredElsewhere(content);
|
||||
}
|
||||
} else {
|
||||
call.receivedAnswer(content);
|
||||
}
|
||||
} else if (event.getType() === EventType.CallCandidates) {
|
||||
if (event.getSender() === this.client.credentials.userId) {
|
||||
return;
|
||||
}
|
||||
if (!call) {
|
||||
// store the candidates; we may get a call eventually.
|
||||
if (!this.candidateEventsByCall.has(content.call_id)) {
|
||||
this.candidateEventsByCall.set(content.call_id, []);
|
||||
}
|
||||
this.candidateEventsByCall.get(content.call_id).push(event);
|
||||
} else {
|
||||
call.onRemoteIceCandidatesReceived(event);
|
||||
}
|
||||
} else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType())) {
|
||||
// Note that we also observe our own hangups here so we can see
|
||||
// if we've already rejected a call that would otherwise be valid
|
||||
if (!call) {
|
||||
// if not live, store the fact that the call has ended because
|
||||
// we're probably getting events backwards so
|
||||
// the hangup will come before the invite
|
||||
call = createNewMatrixCall(this.client, event.getRoomId());
|
||||
if (call) {
|
||||
call.callId = content.call_id;
|
||||
call.initWithHangup(event);
|
||||
this.calls.set(content.call_id, call);
|
||||
}
|
||||
} else {
|
||||
if (call.state !== CallState.Ended) {
|
||||
if (event.getType() === EventType.CallHangup) {
|
||||
call.onHangupReceived(content);
|
||||
} else {
|
||||
call.onRejectReceived(content);
|
||||
}
|
||||
this.calls.delete(content.call_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"comment-format": [
|
||||
true
|
||||
],
|
||||
"curly": false,
|
||||
"eofline": false,
|
||||
"forin": false,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"label-position": true,
|
||||
"max-line-length": false,
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
"static-after-instance",
|
||||
"variables-before-functions"
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": false,
|
||||
"no-console": false,
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-empty": false,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": false,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": false,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": false,
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
"always"
|
||||
],
|
||||
"triple-equals": [],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user