Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 666cbbce08 | |||
| 69c575a4be | |||
| de339d3098 | |||
| 0f83234be9 | |||
| eec7c4c61b | |||
| 25b4b049b7 | |||
| 2191eb3f41 | |||
| bebdbf7e05 | |||
| 646c091966 | |||
| 952729cb1b | |||
| c6992e2056 | |||
| 77ed79e9a9 | |||
| c4f4add0ec | |||
| 7eeb60c838 | |||
| 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 | |||
| d79d613cb7 | |||
| d8cc1f7b7a | |||
| d7c8856fdd | |||
| 9d80a332aa | |||
| e14f7b63c7 | |||
| 3bd2880923 | |||
| 2401ad7159 | |||
| 5d95398621 | |||
| 64cdd73b93 | |||
| 48a9236ea8 | |||
| 8b3126e9d8 | |||
| 74d497cd2d | |||
| 5070a5c598 | |||
| 2e30b08e74 | |||
| 8bf63f5f0b | |||
| 11665d18ee | |||
| a8a9fc0c9d | |||
| 098cd1b8d4 | |||
| 3166a4880d | |||
| 9d1c7136cc | |||
| e100943edf | |||
| a6fe4cdf1c | |||
| 8b5213c09a | |||
| 23a133c825 | |||
| 69c4496dfe | |||
| 4d74cca206 | |||
| 955e081699 | |||
| b7e73422ab | |||
| a9a4ba33aa | |||
| 7116ad9f58 | |||
| 3d18bdb2aa | |||
| 4c14581606 | |||
| a9c9ec3977 | |||
| 694d1f9631 | |||
| 2b9cfae18a | |||
| 800d8380ce | |||
| 621ca28f68 | |||
| 4792241ff6 | |||
| 0b984df5f9 | |||
| 2e260155ea | |||
| b15c8a2d1c | |||
| 94ab317f23 | |||
| 02b37e1219 | |||
| 9d25848a21 | |||
| 3958768e1f | |||
| cec00cd303 | |||
| 45cfef4294 | |||
| 2a01e99635 | |||
| a9c3aee447 | |||
| c669382e12 | |||
| 19251c427a | |||
| a7aec9e2a4 | |||
| 99d7622b42 | |||
| 8728619d2c | |||
| f6d51fdfb8 | |||
| 0ffaa8d617 | |||
| 8a974172ab | |||
| 72675f7266 | |||
| 0f559050d8 | |||
| 58084774bf | |||
| c7aee7cebd | |||
| c68d135ae8 | |||
| 9ed6c99ec8 | |||
| 7d398d41d0 | |||
| 0af763252c | |||
| 1c9dbbbb19 | |||
| 2a688bdac8 | |||
| 3c53c818cb | |||
| 8e53cb324e | |||
| efbd454775 | |||
| 68c7273f56 | |||
| 8b56ff4eff | |||
| 8134eedd93 | |||
| a87ae770cc | |||
| 355da0f9a9 | |||
| 10bdd63762 | |||
| 69dc518c2c | |||
| fa1dddf06c | |||
| f683f4544a | |||
| bc5b587651 | |||
| 8083031029 | |||
| 7b8102a42c | |||
| dbe2f5e4db | |||
| f27791b7de | |||
| 2a78170395 | |||
| cfca1c7b06 | |||
| fb7a67025f | |||
| 8a6cd48b8e | |||
| e21d1f539d | |||
| f62049559c | |||
| 1fe9dd03a3 | |||
| c3283a7297 | |||
| f423164a1c | |||
| 75fe596e24 | |||
| c5eb290e66 | |||
| d3b2c8246d | |||
| c11796af4b | |||
| 8591815d66 | |||
| b82870adb2 | |||
| 3c5b304b6b | |||
| f656698061 | |||
| d4b4bc5031 | |||
| 9bcf33b6d3 | |||
| fa550e8f03 | |||
| 2d73564eba | |||
| 1bd80247fd | |||
| 1c194e8163 | |||
| aa2d0d9a08 | |||
| fd126b8563 | |||
| bc97e7a5ea | |||
| 3dece4f46b | |||
| be05452c70 | |||
| 583650cf7d | |||
| 505915528f | |||
| ace8a787b4 | |||
| ed2ea9ac8e | |||
| 8d09a4abe6 | |||
| 1da959ab02 | |||
| 997dd9b88a | |||
| ebe66bdd6e | |||
| 013fbb87a7 | |||
| 73764d23dc | |||
| 8f62703bf2 | |||
| 6dedae2e4d | |||
| d32131b2b8 | |||
| 0a790b2ae3 | |||
| ef1d5e3d76 | |||
| c525a19df5 | |||
| a987a31667 | |||
| 10329c3436 | |||
| 29f10bcd44 | |||
| 19fe9b8ac7 | |||
| 76da708352 | |||
| 145f01ff2d | |||
| 18b1e00875 | |||
| d021498fa9 | |||
| b83aa54661 | |||
| 429550ca3e | |||
| 2a6d8c2b1d | |||
| 01c9159830 | |||
| 3b3ed5159c | |||
| 636661dd45 | |||
| cc8e8434ec | |||
| 1b94b3c4de |
+21
-68
@@ -1,71 +1,15 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
},
|
||||
extends: ["matrix-org"],
|
||||
plugins: [
|
||||
"babel",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"jest",
|
||||
],
|
||||
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
"max-len": ["error", {
|
||||
code: 90,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
curly: ["error", "multi-line"],
|
||||
"prefer-const": ["error"],
|
||||
"comma-dangle": ["error", {
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "always-multiline",
|
||||
}],
|
||||
|
||||
// loosen jsdoc requirements a little
|
||||
"require-jsdoc": ["error", {
|
||||
require: {
|
||||
FunctionDeclaration: false,
|
||||
}
|
||||
}],
|
||||
"valid-jsdoc": ["error", {
|
||||
requireParamDescription: false,
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false,
|
||||
}],
|
||||
|
||||
// rules we do not want from eslint-recommended
|
||||
"no-console": ["off"],
|
||||
"no-constant-condition": ["off"],
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
|
||||
// rules we do not want from the google styleguide
|
||||
"object-curly-spacing": ["off"],
|
||||
"spaced-comment": ["off"],
|
||||
"guard-for-in": ["off"],
|
||||
|
||||
// in principle we prefer single quotes, but life is too short
|
||||
quotes: ["off"],
|
||||
|
||||
// rules we'd ideally like to adhere to, but the current
|
||||
// code does not (in most cases because it's still ES5)
|
||||
// we set these to warnings, and assert that the number
|
||||
// of warnings doesn't exceed a given threshold
|
||||
"no-var": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
@@ -79,10 +23,19 @@ module.exports = {
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
|
||||
// eslint's built in no-invalid-this rule breaks with class properties
|
||||
"no-invalid-this": "off",
|
||||
// so we replace it with a version that is class property aware
|
||||
"babel/no-invalid-this": "error",
|
||||
}
|
||||
}
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// While we're converting to ts we make heavy use of this
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"quotes": "off",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
+184
@@ -1,3 +1,187 @@
|
||||
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)
|
||||
|
||||
* Filter out non-string display names
|
||||
[\#1434](https://github.com/matrix-org/matrix-js-sdk/pull/1434)
|
||||
|
||||
Changes in [8.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.1-rc.1) (2020-07-31)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.0...v8.0.1-rc.1)
|
||||
|
||||
* Remove redundant lint dependencies
|
||||
[\#1426](https://github.com/matrix-org/matrix-js-sdk/pull/1426)
|
||||
* Upload all keys when we start using a new key backup version
|
||||
[\#1428](https://github.com/matrix-org/matrix-js-sdk/pull/1428)
|
||||
* Expose countSessionsNeedingBackup
|
||||
[\#1429](https://github.com/matrix-org/matrix-js-sdk/pull/1429)
|
||||
* Configure and use new eslint package
|
||||
[\#1422](https://github.com/matrix-org/matrix-js-sdk/pull/1422)
|
||||
|
||||
Changes in [8.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.0) (2020-07-27)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0...v8.0.0)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
* Properly support txnId
|
||||
[\#1424](https://github.com/matrix-org/matrix-js-sdk/pull/1424)
|
||||
* [BREAKING] Remove deprecated getIdenticonUri
|
||||
[\#1423](https://github.com/matrix-org/matrix-js-sdk/pull/1423)
|
||||
* Bump lodash from 4.17.15 to 4.17.19
|
||||
[\#1421](https://github.com/matrix-org/matrix-js-sdk/pull/1421)
|
||||
* [BREAKING] Convert RoomState's stored state map to a real map
|
||||
[\#1419](https://github.com/matrix-org/matrix-js-sdk/pull/1419)
|
||||
|
||||
Changes in [7.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0) (2020-07-03)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0-rc.1...v7.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [7.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0-rc.1) (2020-07-01)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0...v7.1.0-rc.1)
|
||||
|
||||
* Ask general crypto callbacks for 4S privkey if operation adapter doesn't
|
||||
have it yet
|
||||
[\#1414](https://github.com/matrix-org/matrix-js-sdk/pull/1414)
|
||||
* Fix ICreateClientOpts missing idBaseUrl
|
||||
[\#1413](https://github.com/matrix-org/matrix-js-sdk/pull/1413)
|
||||
* Increase max event listeners for rooms
|
||||
[\#1411](https://github.com/matrix-org/matrix-js-sdk/pull/1411)
|
||||
* Don't trust keys megolm received from backup for verifying the sender
|
||||
[\#1406](https://github.com/matrix-org/matrix-js-sdk/pull/1406)
|
||||
* Raise the last known account data / state event for an update
|
||||
[\#1410](https://github.com/matrix-org/matrix-js-sdk/pull/1410)
|
||||
* Isolate encryption bootstrap side-effects
|
||||
[\#1380](https://github.com/matrix-org/matrix-js-sdk/pull/1380)
|
||||
* Add method to get current in-flight to-device requests
|
||||
[\#1405](https://github.com/matrix-org/matrix-js-sdk/pull/1405)
|
||||
|
||||
Changes in [7.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0) (2020-06-23)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0-rc.1...v7.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [7.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0-rc.1) (2020-06-17)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.2...v7.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* Presence lists were removed from the spec in r0.5.0, and the corresponding methods have now been removed here as well:
|
||||
* `getPresenceList`
|
||||
* `inviteToPresenceList`
|
||||
* `dropFromPresenceList`
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Remove support for unspecced device-specific push rules
|
||||
[\#1404](https://github.com/matrix-org/matrix-js-sdk/pull/1404)
|
||||
* Use existing session id for fetching flows as to not get a new session
|
||||
[\#1403](https://github.com/matrix-org/matrix-js-sdk/pull/1403)
|
||||
* Upgrade deps
|
||||
[\#1400](https://github.com/matrix-org/matrix-js-sdk/pull/1400)
|
||||
* Bring back backup key format migration
|
||||
[\#1398](https://github.com/matrix-org/matrix-js-sdk/pull/1398)
|
||||
* Fix: more informative error message when we cant find a key to decrypt with
|
||||
[\#1313](https://github.com/matrix-org/matrix-js-sdk/pull/1313)
|
||||
* Add js-sdk mechanism for polling client well-known for config
|
||||
[\#1394](https://github.com/matrix-org/matrix-js-sdk/pull/1394)
|
||||
* Fix verification request timeouts to match spec
|
||||
[\#1388](https://github.com/matrix-org/matrix-js-sdk/pull/1388)
|
||||
* Drop presence list methods
|
||||
[\#1391](https://github.com/matrix-org/matrix-js-sdk/pull/1391)
|
||||
* Batch up URL previews to prevent excessive requests
|
||||
[\#1395](https://github.com/matrix-org/matrix-js-sdk/pull/1395)
|
||||
|
||||
Changes in [6.2.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.2) (2020-06-16)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.1...v6.2.2)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ function printMemberList(room) {
|
||||
}
|
||||
|
||||
function printRoomInfo(room) {
|
||||
var eventDict = room.currentState.events;
|
||||
var eventMap = room.currentState.events;
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
@@ -300,14 +300,15 @@ function printRoomInfo(room) {
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
Object.keys(eventDict).forEach(function(eventType) {
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventDict[eventType][stateKey];
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
|
||||
+5
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "6.2.2",
|
||||
"version": "8.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
@@ -13,10 +13,9 @@
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"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:ts && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 81 src spec",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 76 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
},
|
||||
@@ -75,10 +74,9 @@
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^1.4.7",
|
||||
"browserify": "^16.5.0",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint": "7.3.1",
|
||||
"eslint-config-matrix-org": "^0.1.2",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"jest": "^24.9.0",
|
||||
@@ -89,7 +87,6 @@
|
||||
"rimraf": "^3.0.0",
|
||||
"terser": "^4.4.3",
|
||||
"tsify": "^4.0.1",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.7.3"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("MatrixClient retrying", function() {
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
if(ev0 === ev1) {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
|
||||
import {getHttpUriForMxc} from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
@@ -56,31 +56,4 @@ describe("ContentRepo", function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=96&height=96",
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as olmlib from "../../src/crypto/olmlib";
|
||||
import {sleep} from "../../src/utils";
|
||||
import {EventEmitter} from "events";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {DeviceInfo} from "../../src/crypto/deviceinfo";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -26,6 +27,66 @@ describe("Crypto", function() {
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||
});
|
||||
|
||||
describe("encrypted events", function() {
|
||||
it("provides encryption information", async function() {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "deviceid",
|
||||
)).client;
|
||||
await client.initCrypto();
|
||||
|
||||
// unencrypted event
|
||||
const event = {
|
||||
getId: () => "$event_id",
|
||||
getSenderKey: () => null,
|
||||
getWireContent: () => {return {};},
|
||||
};
|
||||
|
||||
let encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeFalsy();
|
||||
|
||||
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeFalsy();
|
||||
|
||||
// known sender, megolm key from backup
|
||||
event.getForwardingCurve25519KeyChain = () => [];
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
const device = new DeviceInfo("FLIBBLE");
|
||||
device.keys["curve25519:FLIBBLE"] =
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeFalsy();
|
||||
|
||||
// known sender, trusted megolm key, but bad ed25519key
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeTruthy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeTruthy();
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session management', function() {
|
||||
const otkResponse = {
|
||||
|
||||
@@ -38,7 +38,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 },
|
||||
|
||||
@@ -27,6 +27,7 @@ import {MockStorageApi} from "../../MockStorageApi";
|
||||
import * as testUtils from "../../test-utils";
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../src/crypto";
|
||||
import {resetCrossSigningKeys} from "./crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -332,7 +333,7 @@ describe("MegolmBackup", function() {
|
||||
client.on("crossSigning.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
await client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
@@ -517,6 +518,7 @@ describe("MegolmBackup", function() {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import anotherjson from 'another-json';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
@@ -66,11 +68,66 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
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"},
|
||||
@@ -78,7 +135,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
@@ -273,7 +330,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -363,7 +420,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
@@ -520,7 +577,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
@@ -588,7 +645,7 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -740,7 +797,7 @@ describe("Cross Signing", function() {
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
await bob.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
@@ -766,7 +823,7 @@ describe("Cross Signing", function() {
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {IndexedDBCryptoStore} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
const crypto = client._crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
||||
try {
|
||||
await crypto._crossSigningInfo.resetKeys(level);
|
||||
await crypto._signObject(crypto._crossSigningInfo.keys.master);
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await crypto._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
txn, crypto._crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
crypto._crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto._baseApis.emit("crossSigning.keysChanged", {});
|
||||
await crypto._afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
@@ -190,7 +191,7 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
};
|
||||
alice.resetCrossSigningKeys();
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
@@ -325,7 +326,12 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await bob.bootstrapSecretStorage();
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
@@ -375,6 +381,9 @@ 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
|
||||
@@ -389,7 +398,9 @@ describe("Secrets", function() {
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapSecretStorage();
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
|
||||
@@ -22,6 +22,7 @@ import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {resetCrossSigningKeys} from "../crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -288,12 +289,12 @@ describe("SAS verification", function() {
|
||||
);
|
||||
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
alice.httpBackend.flush(undefined, 2);
|
||||
await alice.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice.client);
|
||||
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
|
||||
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 2);
|
||||
|
||||
await bob.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -119,6 +119,8 @@ async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
@@ -246,4 +248,38 @@ describe("verification request unit tests", function() {
|
||||
expect(bob1Request.done).toBe(true);
|
||||
expect(bob2Request.done).toBe(true);
|
||||
});
|
||||
|
||||
it("request times out after 10 minutes", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
|
||||
true, true);
|
||||
|
||||
expect(aliceRequest.cancelled).toBe(false);
|
||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
|
||||
});
|
||||
|
||||
it("request times out 2 minutes after receipt", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"), new Map(), bob);
|
||||
|
||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||
|
||||
expect(bobRequest.cancelled).toBe(false);
|
||||
expect(bobRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(2 * 60 * 1000);
|
||||
expect(bobRequest._cancellingUserId).toBe(bob.getUserId());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,12 +33,6 @@ describe("RoomMember", function() {
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.member event", function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||
function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
|
||||
@@ -45,12 +45,6 @@ describe("Room", function() {
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.avatar event", function() {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.avatar and allowDefault=false",
|
||||
function() {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
@@ -1379,7 +1373,7 @@ describe("Room", function() {
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
hasThrown = true;
|
||||
}
|
||||
expect(hasThrown).toEqual(true);
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("MatrixScheduler", function() {
|
||||
deferA.reject({});
|
||||
try {
|
||||
await globalA;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.
|
||||
*
|
||||
|
||||
@@ -34,7 +34,7 @@ matrixcs.request(function(opts, fn) {
|
||||
let indexedDB;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
} catch(e) {}
|
||||
} catch (e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
|
||||
+225
-94
@@ -54,6 +54,7 @@ import {randomString} from './randomstring';
|
||||
import {PushProcessor} from "./pushprocessor";
|
||||
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
|
||||
import { User } from "./models/user";
|
||||
import {AutoDiscovery} from "./autodiscovery";
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
@@ -118,7 +119,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.
|
||||
*
|
||||
@@ -234,6 +235,14 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* }
|
||||
* {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
|
||||
* {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.
|
||||
@@ -329,7 +338,7 @@ export function MatrixClient(opts) {
|
||||
this._isGuest = false;
|
||||
this._ongoingScrollbacks = {};
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.urlPreviewCache = {};
|
||||
this.urlPreviewCache = {}; // key=preview key, value=Promise for preview (may be an error)
|
||||
this._notifTimelineSet = null;
|
||||
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
|
||||
@@ -356,6 +365,9 @@ export function MatrixClient(opts) {
|
||||
|
||||
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
|
||||
@@ -379,7 +391,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++;
|
||||
@@ -397,7 +409,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
|
||||
@@ -968,6 +980,20 @@ MatrixClient.prototype.findVerificationRequestDMInProgress = function(roomId) {
|
||||
return this._crypto.findVerificationRequestDMInProgress(roomId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all to-device verification requests that are already in progress for the given user id
|
||||
*
|
||||
* @param {string} userId the ID of the user to query
|
||||
*
|
||||
* @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress
|
||||
*/
|
||||
MatrixClient.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.getVerificationRequestsToDeviceInProgress(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a key verification from another user.
|
||||
*
|
||||
@@ -1074,19 +1100,9 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new cross-signing keys.
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#resetCrossSigningKeys
|
||||
* @param {object} authDict Auth data to supply for User-Interactive auth.
|
||||
* @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
|
||||
* keys will be created for the given level and below. Defaults to
|
||||
* regenerating all keys.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1098,6 +1114,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
|
||||
@@ -1108,6 +1125,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
|
||||
@@ -1118,6 +1136,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
|
||||
@@ -1130,6 +1149,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
|
||||
@@ -1139,6 +1159,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
|
||||
@@ -1151,10 +1172,49 @@ 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, [
|
||||
"resetCrossSigningKeys",
|
||||
"getCrossSigningId",
|
||||
"getStoredCrossSigningForUser",
|
||||
"checkUserTrust",
|
||||
@@ -1164,28 +1224,34 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
"legacyDeviceVerification",
|
||||
"prepareToEncrypt",
|
||||
"isCrossSigningReady",
|
||||
"bootstrapCrossSigning",
|
||||
"getCryptoTrustCrossSignedDevices",
|
||||
"setCryptoTrustCrossSignedDevices",
|
||||
"countSessionsNeedingBackup",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if the sender of an event is verified
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param {MatrixEvent} event event to be checked
|
||||
* @function module:client~MatrixClient#getEventEncryptionInfo
|
||||
*
|
||||
* @returns {DeviceTrustLevel}
|
||||
* @param {module:models/event.MatrixEvent} event event to be checked
|
||||
*
|
||||
* @return {object} An object with the fields:
|
||||
* - encrypted: whether the event is encrypted (if not encrypted, some of the
|
||||
* other properties may not be set)
|
||||
* - senderKey: the sender's key
|
||||
* - algorithm: the algorithm used to encrypt the event
|
||||
* - authenticated: whether we can be sure that the owner of the senderKey
|
||||
* sent the event
|
||||
* - sender: the sender's device information, if available
|
||||
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
|
||||
* (only meaningful if `sender` is set)
|
||||
*/
|
||||
MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
const device = await this.getEventSenderDeviceInfo(event);
|
||||
if (!device) {
|
||||
return 0;
|
||||
}
|
||||
return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1198,22 +1264,59 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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` and/or `pubkey` 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
|
||||
@@ -1228,6 +1331,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1237,7 +1341,8 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1249,6 +1354,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
|
||||
/**
|
||||
* Get a secret from storage.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#getSecret
|
||||
@@ -1259,6 +1365,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1273,6 +1380,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
|
||||
/**
|
||||
* Request a secret from another device.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @function module:client~MatrixClient#requestSecret
|
||||
@@ -1284,6 +1392,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1293,6 +1402,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1303,6 +1413,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
* 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
|
||||
@@ -1312,7 +1423,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
*/
|
||||
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"getEventEncryptionInfo",
|
||||
"createRecoveryKeyFromPassphrase",
|
||||
"isSecretStorageReady",
|
||||
"bootstrapSecretStorage",
|
||||
"addSecretStorageKey",
|
||||
"hasSecretStorageKey",
|
||||
@@ -1918,7 +2031,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
let backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privKey);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
@@ -1977,7 +2090,11 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
}
|
||||
}
|
||||
|
||||
return this.importRoomKeys(keys, { progressCallback });
|
||||
return this.importRoomKeys(keys, {
|
||||
progressCallback,
|
||||
untrusted: true,
|
||||
source: "backup",
|
||||
});
|
||||
}).then(() => {
|
||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||
}).then(() => {
|
||||
@@ -2512,7 +2629,7 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
|
||||
const type = localEvent.getType();
|
||||
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
|
||||
|
||||
localEvent._txnId = txnId;
|
||||
localEvent.setTxnId(txnId);
|
||||
localEvent.setStatus(EventStatus.SENDING);
|
||||
|
||||
// add this event immediately to the local store as 'sending'.
|
||||
@@ -2680,7 +2797,11 @@ function _updatePendingEventStatus(room, event, newStatus) {
|
||||
}
|
||||
|
||||
function _sendEventHttpRequest(client, event) {
|
||||
const txnId = event._txnId ? event._txnId : client.makeTxnId();
|
||||
let txnId = event.getTxnId();
|
||||
if (!txnId) {
|
||||
txnId = client.makeTxnId();
|
||||
event.setTxnId(txnId);
|
||||
}
|
||||
|
||||
const pathParams = {
|
||||
$roomId: event.getRoomId(),
|
||||
@@ -3001,25 +3122,32 @@ MatrixClient.prototype.setRoomReadMarkers = async function(
|
||||
* May return synthesized attributes if the URL lacked OG meta.
|
||||
*/
|
||||
MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
|
||||
// bucket the timestamp to the nearest minute to prevent excessive spam to the server
|
||||
// Surely 60-second accuracy is enough for anyone.
|
||||
ts = Math.floor(ts / 60000) * 60000;
|
||||
|
||||
const key = ts + "_" + url;
|
||||
const og = this.urlPreviewCache[key];
|
||||
if (og) {
|
||||
return Promise.resolve(og);
|
||||
|
||||
// If there's already a request in flight (or we've handled it), return that instead.
|
||||
const cachedPreview = this.urlPreviewCache[key];
|
||||
if (cachedPreview) {
|
||||
if (callback) {
|
||||
cachedPreview.then(callback).catch(callback);
|
||||
}
|
||||
return cachedPreview;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
return this._http.authedRequest(
|
||||
const resp = this._http.authedRequest(
|
||||
callback, "GET", "/preview_url", {
|
||||
url: url,
|
||||
ts: ts,
|
||||
}, undefined, {
|
||||
prefix: PREFIX_MEDIA_R0,
|
||||
},
|
||||
).then(function(response) {
|
||||
// TODO: expire cache occasionally
|
||||
self.urlPreviewCache[key] = response;
|
||||
return response;
|
||||
});
|
||||
);
|
||||
// TODO: Expire the URL preview cache sometimes
|
||||
this.urlPreviewCache[key] = resp;
|
||||
return resp;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3521,46 +3649,6 @@ MatrixClient.prototype.setPresence = function(opts, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
function _presenceList(callback, client, opts, method) {
|
||||
const path = utils.encodeUri("/presence/list/$userId", {
|
||||
$userId: client.credentials.userId,
|
||||
});
|
||||
return client._http.authedRequest(callback, method, path, undefined, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getPresenceList = function(callback) {
|
||||
return _presenceList(callback, this, undefined, "GET");
|
||||
};
|
||||
|
||||
/**
|
||||
* Add users to the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
|
||||
const opts = {"invite": userIds};
|
||||
return _presenceList(callback, this, opts, "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
* Drop users from the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
**/
|
||||
MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
|
||||
const opts = {"drop": userIds};
|
||||
return _presenceList(callback, this, opts, "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve older messages from the given room and put them in the timeline.
|
||||
@@ -3819,7 +3907,9 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
let path, params, promise;
|
||||
let path;
|
||||
let params;
|
||||
let promise;
|
||||
const self = this;
|
||||
|
||||
if (isNotifTimeline) {
|
||||
@@ -4239,7 +4329,8 @@ MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
|
||||
*/
|
||||
MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
|
||||
const self = this;
|
||||
let deferred, hasDontNotifyRule;
|
||||
let deferred;
|
||||
let hasDontNotifyRule;
|
||||
|
||||
// Get the existing room-kind push rule if any
|
||||
const roomPushRule = this.getRoomPushRule(scope, roomId);
|
||||
@@ -4762,6 +4853,9 @@ MatrixClient.prototype.deactivateSynapseUser = function(userId) {
|
||||
* @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during
|
||||
* initial sync but fetch them when needed by calling `loadOutOfBandMembers`
|
||||
* This will override the filter option at this moment.
|
||||
* @param {Number=} opts.clientWellKnownPollPeriod The number of seconds between polls
|
||||
* to /.well-known/matrix/client, undefined to disable. This should be in the order of hours.
|
||||
* Default: undefined.
|
||||
*/
|
||||
MatrixClient.prototype.startClient = async function(opts) {
|
||||
if (this.clientRunning) {
|
||||
@@ -4810,6 +4904,32 @@ MatrixClient.prototype.startClient = async function(opts) {
|
||||
this._clientOpts = opts;
|
||||
this._syncApi = new SyncApi(this, opts);
|
||||
this._syncApi.sync();
|
||||
|
||||
if (opts.clientWellKnownPollPeriod !== undefined) {
|
||||
this._clientWellKnownIntervalID =
|
||||
setInterval(() => {
|
||||
this._fetchClientWellKnown();
|
||||
}, 1000 * opts.clientWellKnownPollPeriod);
|
||||
this._fetchClientWellKnown();
|
||||
}
|
||||
};
|
||||
|
||||
MatrixClient.prototype._fetchClientWellKnown = async function() {
|
||||
// `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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -4851,6 +4971,9 @@ MatrixClient.prototype.stopClient = function() {
|
||||
this._peekSync.stopPeeking();
|
||||
}
|
||||
global.clearTimeout(this._checkTurnServersTimeoutID);
|
||||
if (this._clientWellKnownIntervalID !== undefined) {
|
||||
global.clearInterval(this._clientWellKnownIntervalID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -5603,8 +5726,9 @@ MatrixClient.prototype.generateClientSecret = function() {
|
||||
* Fires whenever new user-scoped account_data is added.
|
||||
* @event module:client~MatrixClient#"accountData"
|
||||
* @param {MatrixEvent} event The event describing the account_data just added
|
||||
* @param {MatrixEvent} event The previous account data, if known.
|
||||
* @example
|
||||
* matrixClient.on("accountData", function(event){
|
||||
* matrixClient.on("accountData", function(event, oldEvent){
|
||||
* myAccountData[event.type] = event.content;
|
||||
* });
|
||||
*/
|
||||
@@ -5682,6 +5806,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
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,35 +75,3 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
* @deprecated This is no longer in the specification.
|
||||
*/
|
||||
export function getIdenticonUri(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) {
|
||||
width = 96;
|
||||
}
|
||||
if (!height) {
|
||||
height = 96;
|
||||
}
|
||||
const params = {
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
|
||||
$ident: identiconString,
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
|
||||
+153
-22
@@ -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
|
||||
@@ -178,12 +181,12 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* typically called in conjunction with the creation of new cross-signing
|
||||
* keys.
|
||||
*
|
||||
* @param {object} keys The keys to store
|
||||
* @param {Map} keys The keys to store
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
*/
|
||||
static async storeInSecretStorage(keys, secretStorage) {
|
||||
for (const type of Object.keys(keys)) {
|
||||
const encodedKey = encodeBase64(keys[type]);
|
||||
for (const [type, privateKey] of keys) {
|
||||
const encodedKey = encodeBase64(privateKey);
|
||||
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
|
||||
}
|
||||
}
|
||||
@@ -199,9 +202,44 @@ 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.
|
||||
*
|
||||
* @returns {boolean} True if all keys are stored in the local cache.
|
||||
*/
|
||||
async isStoredInKeyCache() {
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
if (!cacheCallbacks) return false;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
if (!await cacheCallbacks.getCrossSigningKeyCache(type)) {
|
||||
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);
|
||||
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 +715,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;
|
||||
}
|
||||
console.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) => {
|
||||
console.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) => {
|
||||
console.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.
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
import { logger } from "../logger";
|
||||
import {MatrixEvent} from "../models/event";
|
||||
import {EventEmitter} from "events";
|
||||
import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {
|
||||
PREFIX_UNSTABLE,
|
||||
} from "../http-api";
|
||||
|
||||
/**
|
||||
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
|
||||
* Once done, `buildOperation()` can be called which allows to apply to operation.
|
||||
*
|
||||
* This is used as a helper by Crypto to keep track of all the network requests
|
||||
* and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
|
||||
* Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
|
||||
* more than once.
|
||||
*/
|
||||
export class EncryptionSetupBuilder {
|
||||
/**
|
||||
* @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) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
|
||||
this._crossSigningKeys = null;
|
||||
this._keySignatures = null;
|
||||
this._keyBackupInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new cross-signing public 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(authUpload, keys) {
|
||||
this._crossSigningKeys = {authUpload, keys};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the key backup info to be updated on the server
|
||||
*
|
||||
* Used either to create a new key backup, or add signatures
|
||||
* from the new MSK.
|
||||
*
|
||||
* @param {Object} keyBackupInfo as received from/sent to the server
|
||||
*/
|
||||
addSessionBackup(keyBackupInfo) {
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the session backup private key to be updated in the local cache
|
||||
*
|
||||
* Used after fixing the format of the key
|
||||
*
|
||||
* @param {Uint8Array} privateKey
|
||||
*/
|
||||
addSessionBackupPrivateKeyToCache(privateKey) {
|
||||
this._sessionBackupPrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add signatures from a given user and device/x-sign key
|
||||
* Used to sign the new cross-signing key with the device key
|
||||
*
|
||||
* @param {String} userId
|
||||
* @param {String} deviceId
|
||||
* @param {String} signature
|
||||
*/
|
||||
addKeySignature(userId, deviceId, signature) {
|
||||
if (!this._keySignatures) {
|
||||
this._keySignatures = {};
|
||||
}
|
||||
const userSignatures = this._keySignatures[userId] || {};
|
||||
this._keySignatures[userId] = userSignatures;
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* builds the operation containing all the parts that have been added to the builder
|
||||
* @return {EncryptionSetupOperation}
|
||||
*/
|
||||
buildOperation() {
|
||||
const accountData = this.accountDataClientAdapter._values;
|
||||
return new EncryptionSetupOperation(
|
||||
accountData,
|
||||
this._crossSigningKeys,
|
||||
this._keyBackupInfo,
|
||||
this._keySignatures,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the created keys locally.
|
||||
*
|
||||
* This does not yet store the operation in a way that it can be restored,
|
||||
* but that is the idea in the future.
|
||||
*
|
||||
* @param {Crypto} crypto
|
||||
* @return {Promise}
|
||||
*/
|
||||
async persist(crypto) {
|
||||
// store private keys in cache
|
||||
if (this._crossSigningKeys) {
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(
|
||||
crypto._cryptoStore, crypto._olmDevice);
|
||||
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);
|
||||
}
|
||||
// store own cross-sign pubkeys as trusted
|
||||
await crypto._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
txn, this._crossSigningKeys.keys);
|
||||
},
|
||||
);
|
||||
}
|
||||
// store session backup key in cache
|
||||
if (this._sessionBackupPrivateKey) {
|
||||
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be created from EncryptionSetupBuilder, or
|
||||
* (in a follow-up PR, not implemented yet) restored from storage, to retry.
|
||||
*
|
||||
* It does not have knowledge of any private keys, unlike the builder.
|
||||
*/
|
||||
export class EncryptionSetupOperation {
|
||||
/**
|
||||
* @param {Map<String, Object>} accountData
|
||||
* @param {Object} crossSigningKeys
|
||||
* @param {Object} keyBackupInfo
|
||||
* @param {Object} keySignatures
|
||||
*/
|
||||
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
|
||||
this._accountData = accountData;
|
||||
this._crossSigningKeys = crossSigningKeys;
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
this._keySignatures = keySignatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the (remaining part of, in the future) operation by sending requests to the server.
|
||||
* @param {Crypto} crypto
|
||||
*/
|
||||
async apply(crypto) {
|
||||
const baseApis = crypto._baseApis;
|
||||
// upload cross-signing keys
|
||||
if (this._crossSigningKeys) {
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
await baseApis.uploadKeySignatures(this._keySignatures);
|
||||
}
|
||||
// need to create/update key backup info
|
||||
if (this._keyBackupInfo) {
|
||||
if (this._keyBackupInfo.version) {
|
||||
// session backup signature
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis._http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this._keyBackupInfo.algorithm,
|
||||
auth_data: this._keyBackupInfo.auth_data,
|
||||
},
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis._http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, this._keyBackupInfo,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData existing account data
|
||||
*/
|
||||
constructor(accountData) {
|
||||
super();
|
||||
this._existingValues = accountData;
|
||||
this._values = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Promise<Object>} the content of the account data
|
||||
*/
|
||||
getAccountDataFromServer(type) {
|
||||
return Promise.resolve(this.getAccountData(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Object} the content of the account data
|
||||
*/
|
||||
getAccountData(type) {
|
||||
const modifiedValue = this._values.get(type);
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
}
|
||||
const existingValue = this._existingValues[type];
|
||||
if (existingValue) {
|
||||
return existingValue.getContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
const lastEvent = this._values.get(type);
|
||||
this._values.set(type, content);
|
||||
// ensure accountData is emitted on the next tick,
|
||||
// as SecretStorage listens for it while calling this method
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({type, content});
|
||||
this.emit("accountData", event, lastEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the private cross-signing keys set during bootstrapping
|
||||
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
|
||||
* See CrossSigningInfo constructor
|
||||
*/
|
||||
class CrossSigningCallbacks {
|
||||
constructor() {
|
||||
this.privateKeys = new Map();
|
||||
}
|
||||
|
||||
// cache callbacks
|
||||
getCrossSigningKeyCache(type, expectedPublicKey) {
|
||||
return this.getCrossSigningKey(type, expectedPublicKey);
|
||||
}
|
||||
|
||||
storeCrossSigningKeyCache(type, key) {
|
||||
this.privateKeys.set(type, key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// non-cache callbacks
|
||||
getCrossSigningKey(type, _expectedPubkey) {
|
||||
return Promise.resolve(this.privateKeys.get(type));
|
||||
}
|
||||
|
||||
saveCrossSigningKeys(privateKeys) {
|
||||
for (const [type, privateKey] of Object.entries(privateKeys)) {
|
||||
this.privateKeys.set(type, privateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the 4S private key set during bootstrapping by implementing
|
||||
* the SecretStorage crypto callbacks
|
||||
*/
|
||||
class SSSSCryptoCallbacks {
|
||||
constructor(delegateCryptoCallbacks) {
|
||||
this._privateKeys = new Map();
|
||||
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
|
||||
}
|
||||
|
||||
async getSecretStorageKey({ keys }, name) {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const privateKey = this._privateKeys.get(keyId);
|
||||
if (privateKey) {
|
||||
return [keyId, privateKey];
|
||||
}
|
||||
}
|
||||
// if we don't have the key cached yet, ask
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this._delegateCryptoCallbacks) {
|
||||
const result = await this._delegateCryptoCallbacks.
|
||||
getSecretStorageKey({keys}, name);
|
||||
if (result) {
|
||||
const [keyId, privateKey] = result;
|
||||
this._privateKeys.set(keyId, privateKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
addPrivateKey(keyId, 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, privKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -992,11 +992,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
|
||||
* @param {boolean} exportFormat true if the megolm keys are in export format
|
||||
* (ie, they lack an ed25519 signature)
|
||||
* @param {Object} [extraSessionData={}] any other data to be include with the session
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
exportFormat, extraSessionData = {},
|
||||
) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [
|
||||
@@ -1043,12 +1044,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
" with first index " + session.first_known_index(),
|
||||
);
|
||||
|
||||
const sessionData = {
|
||||
const sessionData = Object.assign({}, extraSessionData, {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this._pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
};
|
||||
});
|
||||
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
@@ -1224,6 +1225,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
forwardingCurve25519KeyChain: (
|
||||
sessionData.forwardingCurve25519KeyChain || []
|
||||
),
|
||||
untrusted: sessionData.untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -292,6 +292,11 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(keys).length === 0) {
|
||||
throw new Error(`Could not decrypt ${name} because none of ` +
|
||||
`the keys it is encrypted with are for a supported algorithm`);
|
||||
}
|
||||
|
||||
let keyId;
|
||||
let decryption;
|
||||
try {
|
||||
@@ -368,6 +373,7 @@ export class SecretStorage extends EventEmitter {
|
||||
const requestId = this._baseApis.makeTxnId();
|
||||
|
||||
const requestControl = this._requests[requestId] = {
|
||||
name,
|
||||
devices,
|
||||
};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
@@ -531,6 +537,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;
|
||||
@@ -1201,6 +1201,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
senderCurve25519Key: res.senderKey,
|
||||
claimedEd25519Key: res.keysClaimed.ed25519,
|
||||
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
|
||||
untrusted: res.untrusted,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1548,8 +1549,11 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} session
|
||||
* @param {object} [opts={}] options for the import
|
||||
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
|
||||
* @param {string} [opts.source] where the key came from
|
||||
*/
|
||||
MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
@@ -1558,8 +1562,9 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
||||
).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
if (this._crypto.backupInfo && opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
|
||||
+458
-291
@@ -34,11 +34,11 @@ import {DeviceInfo} from "./deviceinfo";
|
||||
import * as algorithms from "./algorithms";
|
||||
import {
|
||||
CrossSigningInfo,
|
||||
CrossSigningLevel,
|
||||
DeviceTrustLevel,
|
||||
UserTrustLevel,
|
||||
createCryptoStoreCacheCallbacks,
|
||||
} from './CrossSigning';
|
||||
import {EncryptionSetupBuilder} from "./EncryptionSetup";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage';
|
||||
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
@@ -49,11 +49,10 @@ import {
|
||||
} from './verification/QRCode';
|
||||
import {SAS} from './verification/SAS';
|
||||
import {keyFromPassphrase} from './key_passphrase';
|
||||
import {encodeRecoveryKey} from './recoverykey';
|
||||
import {encodeRecoveryKey, decodeRecoveryKey} from './recoverykey';
|
||||
import {VerificationRequest} from "./verification/request/VerificationRequest";
|
||||
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
|
||||
import * as httpApi from "../http-api";
|
||||
import {IllegalMethod} from "./verification/IllegalMethod";
|
||||
import {KeySignatureUploadError} from "../errors";
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
@@ -407,14 +406,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.
|
||||
@@ -423,24 +420,179 @@ 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
|
||||
);
|
||||
|
||||
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
|
||||
@@ -450,11 +602,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] 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.
|
||||
* 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:
|
||||
@@ -475,7 +624,6 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
||||
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
|
||||
*/
|
||||
Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
authUploadDeviceSigningKeys,
|
||||
createSecretStorageKey = async () => ({ }),
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
@@ -483,42 +631,19 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
getKeyBackupPassphrase,
|
||||
} = {}) {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
|
||||
// Create cross-signing keys if they don't exist, as we want to sign the SSSS default
|
||||
// key with the cross-signing master key. The cross-signing master key is also used
|
||||
// to verify the signature on the SSSS default key when adding secrets, so we
|
||||
// effectively need it for both reading and writing secrets.
|
||||
const crossSigningPrivateKeys = {};
|
||||
|
||||
// If we happen to reset cross-signing keys here, then we want access to the
|
||||
// cross-signing private keys, but only for the scope of this method, so we
|
||||
// use temporary callbacks to weave them through the various APIs.
|
||||
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
|
||||
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
this._baseApis.store.accountData,
|
||||
delegateCryptoCallbacks,
|
||||
);
|
||||
const secretStorage = new SecretStorage(
|
||||
builder.accountDataClientAdapter,
|
||||
builder.ssssCryptoCallbacks,
|
||||
);
|
||||
|
||||
// the ID of the new SSSS key, if we create one
|
||||
let newKeyId = null;
|
||||
|
||||
// cache SSSS keys so that we don't need to constantly pester the user about it
|
||||
const ssssKeys = {};
|
||||
|
||||
this._baseApis._cryptoCallbacks.getSecretStorageKey =
|
||||
async ({keys}, name) => {
|
||||
// if we already have a key that works, return it
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (ssssKeys[keyId]) {
|
||||
return [keyId, ssssKeys[keyId]];
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, prompt the user and cache it
|
||||
const key = await appCallbacks.getSecretStorageKey({keys}, name);
|
||||
if (key) {
|
||||
const [keyId, keyData] = key;
|
||||
ssssKeys[keyId] = keyData;
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
// create a new SSSS key and set it as default
|
||||
const createSSSS = async (opts, privateKey) => {
|
||||
opts = opts || {};
|
||||
@@ -526,28 +651,17 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
opts.key = privateKey;
|
||||
}
|
||||
|
||||
const keyId = await this.addSecretStorageKey(
|
||||
const keyId = await secretStorage.addKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
||||
);
|
||||
await this.setDefaultSecretStorageKeyId(keyId);
|
||||
|
||||
if (privateKey) {
|
||||
// cache the private key so that we can access it again
|
||||
ssssKeys[keyId] = privateKey;
|
||||
// make the private key available to encrypt 4S secrets
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
|
||||
}
|
||||
return keyId;
|
||||
};
|
||||
|
||||
// reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
|
||||
keys => Object.assign(crossSigningPrivateKeys, keys);
|
||||
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
||||
name => crossSigningPrivateKeys[name];
|
||||
await this.resetCrossSigningKeys(
|
||||
CrossSigningLevel.MASTER,
|
||||
{ authUploadDeviceSigningKeys },
|
||||
);
|
||||
await secretStorage.setDefaultKeyId(keyId);
|
||||
return keyId;
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||
@@ -557,186 +671,159 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
if (key) {
|
||||
const keyData = key[1];
|
||||
ssssKeys[keyId] = keyData;
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
await builder.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const oldSSSSKey = await this.getSecretStorageKey();
|
||||
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||
const decryptionKeys =
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||
const oldSSSSKey = await this.getSecretStorageKey();
|
||||
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||
const storageExists = (
|
||||
!setupNewSecretStorage &&
|
||||
oldKeyInfo &&
|
||||
oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
);
|
||||
|
||||
if (!inStorage && !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",
|
||||
);
|
||||
if (!storageExists && !keyBackupInfo) {
|
||||
// either we don't have anything, or we've been asked to restart
|
||||
// from scratch
|
||||
logger.log(
|
||||
"Secret storage does not exist, creating new storage key",
|
||||
);
|
||||
|
||||
await resetCrossSigning();
|
||||
// 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 does not exist, using key backup key");
|
||||
|
||||
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);
|
||||
}
|
||||
// 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();
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
}
|
||||
} else if (!inStorage && keyBackupInfo) {
|
||||
// we have an existing backup, but no SSSS
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts = {};
|
||||
|
||||
logger.log("Secret storage default key not found, 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 = {};
|
||||
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
salt: keyBackupInfo.auth_data.private_key_salt,
|
||||
bits: 256,
|
||||
};
|
||||
}
|
||||
|
||||
newKeyId = await createSSSS(opts, backupKey);
|
||||
|
||||
// store the backup key in secret storage
|
||||
await this.storeSecret(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// 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 this._crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
await this._baseApis._http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version,
|
||||
undefined, keyBackupInfo,
|
||||
{prefix: httpApi.PREFIX_UNSTABLE},
|
||||
);
|
||||
} 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");
|
||||
|
||||
// fetch the private keys and set up our local copy of the keys for
|
||||
// use
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// we have SSSS and we cross-signing is already set up
|
||||
logger.log("Cross signing keys are present in secret storage");
|
||||
|
||||
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 (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
salt: keyBackupInfo.auth_data.private_key_salt,
|
||||
bits: 256,
|
||||
};
|
||||
}
|
||||
|
||||
// If cross-signing keys were reset, store them in Secure Secret Storage.
|
||||
// This is done in a separate step so we can ensure secret storage has its
|
||||
// own key first.
|
||||
// XXX: We need to think about how to re-do these steps if they fail.
|
||||
// See also https://github.com/vector-im/riot-web/issues/11635
|
||||
if (Object.keys(crossSigningPrivateKeys).length) {
|
||||
logger.log("Storing cross-signing private keys in secret storage");
|
||||
// Assuming no app-supplied callback, default to storing in SSSS.
|
||||
if (!appCallbacks.saveCrossSigningKeys) {
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
this._secretStorage,
|
||||
);
|
||||
}
|
||||
}
|
||||
newKeyId = await createSSSS(opts, backupKey);
|
||||
|
||||
if (setupNewKeyBackup && !keyBackupInfo) {
|
||||
const info = await this._baseApis.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
await this._baseApis.createKeyBackupVersion(info);
|
||||
}
|
||||
// store the backup key in secret storage
|
||||
await secretStorage.store(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
|
||||
// Call `getCrossSigningKey` for side effect of caching private keys for
|
||||
// future gossiping to other devices if enabled via app level callbacks.
|
||||
if (this._crossSigningInfo._cacheCallbacks) {
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
await this._crossSigningInfo.getCrossSigningKey(type);
|
||||
}
|
||||
}
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// 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 this._crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
builder.addSessionBackup(keyBackupInfo);
|
||||
} else {
|
||||
// 4S is already set up
|
||||
logger.log("Secret storage exists");
|
||||
|
||||
// and likewise for the session backup key
|
||||
const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
|
||||
if (sessionBackupKey) {
|
||||
logger.info("Got session backup key from secret storage: caching");
|
||||
// fix up the backup key if it's in the wrong format, and replace
|
||||
// in secret storage
|
||||
const fixedBackupKey = fixBackupKey(sessionBackupKey);
|
||||
if (fixedBackupKey) {
|
||||
await this.storeSecret(
|
||||
"m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId],
|
||||
);
|
||||
}
|
||||
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
||||
fixedBackupKey || sessionBackupKey,
|
||||
));
|
||||
await this.storeSessionBackupPrivateKey(decodedBackupKey);
|
||||
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);
|
||||
}
|
||||
} finally {
|
||||
// Restore the original callbacks. NB. we must do this by manipulating
|
||||
// the same object since the CrossSigning class has a reference to the
|
||||
// object, so if we assign the object here then our callbacks will change
|
||||
// but the instances of the CrossSigning class will be left with our
|
||||
// random, otherwise dead closures.
|
||||
for (const cb of Object.keys(this._baseApis._cryptoCallbacks)) {
|
||||
delete this._baseApis._cryptoCallbacks[cb];
|
||||
}
|
||||
Object.assign(this._baseApis._cryptoCallbacks, appCallbacks);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Here, we want to capture all the side-effects of bootstrapping,
|
||||
// and want to write to the local secretStorage object
|
||||
{ secureSecretStorage: false },
|
||||
);
|
||||
// write the key ourselves to 4S
|
||||
const privateKey = decodeRecoveryKey(info.recovery_key);
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
|
||||
|
||||
// create keyBackupInfo object to add to builder
|
||||
const data = {
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
};
|
||||
// sign with cross-sign master key
|
||||
await this._crossSigningInfo.signObject(data.auth_data, "master");
|
||||
// sign with the device fingerprint
|
||||
await this._signObject(data.auth_data);
|
||||
|
||||
builder.addSessionBackup(data);
|
||||
}
|
||||
|
||||
// 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");
|
||||
// fix up the backup key if it's in the wrong format, and replace
|
||||
// in secret storage
|
||||
const fixedBackupKey = fixBackupKey(sessionBackupKey);
|
||||
if (fixedBackupKey) {
|
||||
await secretStorage.store("m.megolm_backup.v1",
|
||||
fixedBackupKey, [newKeyId || oldKeyId],
|
||||
);
|
||||
}
|
||||
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
||||
fixedBackupKey || sessionBackupKey,
|
||||
));
|
||||
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
||||
}
|
||||
|
||||
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("Secure Secret Storage ready");
|
||||
};
|
||||
|
||||
@@ -897,57 +984,6 @@ Crypto.prototype.checkCrossSigningPrivateKey = function(privateKey, expectedPubl
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate new cross-signing keys.
|
||||
*
|
||||
* @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
|
||||
* keys will be created for the given level and below. Defaults to
|
||||
* regenerating all keys.
|
||||
* @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.
|
||||
*/
|
||||
Crypto.prototype.resetCrossSigningKeys = async function(level, {
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
logger.info(`Resetting cross-signing keys at level ${level}`);
|
||||
// Copy old keys (usually empty) in case we need to revert
|
||||
const oldKeys = Object.assign({}, this._crossSigningInfo.keys);
|
||||
try {
|
||||
await this._crossSigningInfo.resetKeys(level);
|
||||
await this._signObject(this._crossSigningInfo.keys.master);
|
||||
|
||||
// send keys to server first before storing as trusted locally
|
||||
// to ensure upload succeeds
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
await authUploadDeviceSigningKeys(async authDict => {
|
||||
await this._baseApis.uploadDeviceSigningKeys(authDict, keys);
|
||||
});
|
||||
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
logger.error("Resetting cross-signing keys failed, revert to previous keys", e);
|
||||
this._crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
this._baseApis.emit("crossSigning.keysChanged", {});
|
||||
await this._afterCrossSigningLocalKeyChange();
|
||||
logger.info("Cross-signing key reset complete");
|
||||
};
|
||||
|
||||
/**
|
||||
* Run various follow-up actions after cross-signing keys have changed locally
|
||||
* (either by resetting the keys for the account or by getting them from secret
|
||||
@@ -1275,6 +1311,20 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
|
||||
// Try to cache the self-signing private key as a side-effect
|
||||
let signing = null;
|
||||
try {
|
||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||
"self_signing", newCrossSigning.getId("self_signing"),
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info(
|
||||
"Got matching private key from callback for new public self-signing key",
|
||||
);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
|
||||
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
|
||||
const signedDevice = await this._crossSigningInfo.signDevice(
|
||||
this._userId, device,
|
||||
@@ -1283,6 +1333,20 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
}
|
||||
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
|
||||
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
|
||||
|
||||
// Try to cache the user-signing private key as a side-effect
|
||||
let signing = null;
|
||||
try {
|
||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||
"user_signing", newCrossSigning.getId("user_signing"),
|
||||
);
|
||||
signing = ret[1];
|
||||
logger.info(
|
||||
"Got matching private key from callback for new public user-signing key",
|
||||
);
|
||||
} finally {
|
||||
if (signing) signing.free();
|
||||
}
|
||||
}
|
||||
|
||||
if (masterChanged) {
|
||||
@@ -1453,6 +1517,12 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
);
|
||||
this._baseApis.disableKeyBackup();
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
// We're now using a new backup, so schedule all the keys we have to be
|
||||
// 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/element-web/issues/14833
|
||||
await this.scheduleAllGroupSessionsForBackup();
|
||||
} else {
|
||||
logger.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
@@ -2052,9 +2122,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);
|
||||
@@ -2090,6 +2169,10 @@ Crypto.prototype.findVerificationRequestDMInProgress = function(roomId) {
|
||||
return this._inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
};
|
||||
|
||||
Crypto.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
|
||||
return this._toDeviceVerificationRequests.getRequestsInProgress(userId);
|
||||
};
|
||||
|
||||
Crypto.prototype.requestVerificationDM = function(userId, roomId) {
|
||||
const existingRequest = this._inRoomVerificationRequests.
|
||||
findRequestInProgress(roomId);
|
||||
@@ -2238,11 +2321,16 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0) {
|
||||
// we got this event from somewhere else
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.isKeySourceUntrusted()) {
|
||||
// we got the key for this event from a source that we consider untrusted
|
||||
return null;
|
||||
}
|
||||
|
||||
// senderKey is the Curve25519 identity key of the device which the event
|
||||
// was sent from. In the case of Megolm, it's actually the Curve25519
|
||||
// identity key of the device which set up the Megolm session.
|
||||
@@ -2281,6 +2369,76 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
return device;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event to be checked
|
||||
*
|
||||
* @return {object} An object with the fields:
|
||||
* - encrypted: whether the event is encrypted (if not encrypted, some of the
|
||||
* other properties may not be set)
|
||||
* - senderKey: the sender's key
|
||||
* - algorithm: the algorithm used to encrypt the event
|
||||
* - authenticated: whether we can be sure that the owner of the senderKey
|
||||
* sent the event
|
||||
* - sender: the sender's device information, if available
|
||||
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
|
||||
* (only meaningful if `sender` is set)
|
||||
*/
|
||||
Crypto.prototype.getEventEncryptionInfo = function(event) {
|
||||
const ret = {};
|
||||
|
||||
ret.senderKey = event.getSenderKey();
|
||||
ret.algorithm = event.getWireContent().algorithm;
|
||||
|
||||
if (!ret.senderKey || !ret.algorithm) {
|
||||
ret.encrypted = false;
|
||||
return ret;
|
||||
}
|
||||
ret.encrypted = true;
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
ret.authenticated = false;
|
||||
} else {
|
||||
ret.authenticated = true;
|
||||
}
|
||||
|
||||
// senderKey is the Curve25519 identity key of the device which the event
|
||||
// was sent from. In the case of Megolm, it's actually the Curve25519
|
||||
// identity key of the device which set up the Megolm session.
|
||||
|
||||
ret.sender = this._deviceList.getDeviceByIdentityKey(
|
||||
ret.algorithm, ret.senderKey,
|
||||
);
|
||||
|
||||
// so far so good, but now we need to check that the sender of this event
|
||||
// hadn't advertised someone else's Curve25519 key as their own. We do that
|
||||
// by checking the Ed25519 claimed by the event (or, in the case of megolm,
|
||||
// the event which set up the megolm session), to check that it matches the
|
||||
// fingerprint of the purported sending device.
|
||||
//
|
||||
// (see https://github.com/vector-im/vector-web/issues/2215)
|
||||
|
||||
const claimedKey = event.getClaimedEd25519Key();
|
||||
if (!claimedKey) {
|
||||
logger.warn("Event " + event.getId() + " claims no ed25519 key: " +
|
||||
"cannot verify sending device");
|
||||
ret.mismatchedSender = true;
|
||||
}
|
||||
|
||||
if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
|
||||
logger.warn(
|
||||
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
|
||||
"but sender device has key " + ret.sender.getFingerprint());
|
||||
ret.mismatchedSender = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces the current outbound group session to be discarded such
|
||||
* that another one will be created next time an event is sent.
|
||||
@@ -2349,7 +2507,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
|
||||
// after all the in-memory state (_roomEncryptors and _roomList) has been updated
|
||||
// to avoid races when calling this method multiple times. Hence keep a hold of the promise.
|
||||
let storeConfigPromise = null;
|
||||
if(!existingConfig) {
|
||||
if (!existingConfig) {
|
||||
storeConfigPromise = this._roomList.setRoomEncryption(roomId, config);
|
||||
}
|
||||
|
||||
@@ -2379,10 +2537,10 @@ 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) {
|
||||
if (!this.inhibitDeviceQuery) {
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
}
|
||||
} else {
|
||||
@@ -2525,7 +2683,7 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
|
||||
}
|
||||
|
||||
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
|
||||
return alg.importRoomKey(key).finally((r) => {
|
||||
return alg.importRoomKey(key, opts).finally((r) => {
|
||||
successes++;
|
||||
if (opts.progressCallback) { updateProgress(); }
|
||||
});
|
||||
@@ -2687,7 +2845,8 @@ Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up without scheduling
|
||||
* them to upload in the background.
|
||||
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
|
||||
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
|
||||
* (which will be equal to the number of sessions in the store).
|
||||
*/
|
||||
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
|
||||
await this._cryptoStore.doTxn(
|
||||
@@ -2710,6 +2869,14 @@ Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
|
||||
return remaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* Counts the number of end to end session keys that are waiting to be backed up
|
||||
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||
*/
|
||||
Crypto.prototype.countSessionsNeedingBackup = function() {
|
||||
return this._cryptoStore.countSessionsNeedingBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
@@ -2944,7 +3111,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();
|
||||
|
||||
@@ -131,7 +131,7 @@ export class Backend {
|
||||
|
||||
cursorReq.onsuccess = (ev) => {
|
||||
const cursor = ev.target.result;
|
||||
if(!cursor) {
|
||||
if (!cursor) {
|
||||
// no match found
|
||||
callback(null);
|
||||
return;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -430,7 +430,7 @@ export class SAS extends Base {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -356,4 +356,12 @@ export class ToDeviceRequests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRequestsInProgress(userId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,17 @@ import {
|
||||
} from "../Error";
|
||||
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
|
||||
|
||||
// the recommended amount of time before a verification request
|
||||
// should be (automatically) cancelled without user interaction
|
||||
// and ignored.
|
||||
const VERIFICATION_REQUEST_TIMEOUT = 10 * 60 * 1000; //10m
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
// How long after we receive the event that the request times out
|
||||
const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
// to avoid almost expired verification notifications
|
||||
// from showing a notification and almost immediately
|
||||
// disappearing, also ignore verification requests that
|
||||
// are this amount of time away from expiring.
|
||||
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; //3s
|
||||
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
|
||||
|
||||
|
||||
export const EVENT_PREFIX = "m.key.verification.";
|
||||
@@ -80,6 +82,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
this._qrCodeData = null;
|
||||
|
||||
// The timestamp when we received the request event from the other side
|
||||
this._requestReceivedAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,12 +170,26 @@ export class VerificationRequest extends EventEmitter {
|
||||
return this._chosenMethod;
|
||||
}
|
||||
|
||||
calculateEventTimeout(event) {
|
||||
let effectiveExpiresAt = this.channel.getTimestamp(event)
|
||||
+ TIMEOUT_FROM_EVENT_TS;
|
||||
|
||||
if (this._requestReceivedAt && !this.initiatedByMe &&
|
||||
this.phase <= PHASE_REQUESTED
|
||||
) {
|
||||
const expiresAtByReceipt = this._requestReceivedAt
|
||||
+ TIMEOUT_FROM_EVENT_RECEIPT;
|
||||
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
|
||||
}
|
||||
|
||||
return Math.max(0, effectiveExpiresAt - Date.now());
|
||||
}
|
||||
|
||||
/** The current remaining amount of ms before the request should be automatically cancelled */
|
||||
get timeout() {
|
||||
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||
if (requestEvent) {
|
||||
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
|
||||
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
|
||||
return this.calculateEventTimeout(requestEvent);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -735,7 +754,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
_setupTimeout(phase) {
|
||||
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED && this.initiatedByMe;
|
||||
phase === PHASE_REQUESTED;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||
@@ -754,7 +773,17 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
_cancelOnTimeout = () => {
|
||||
try {
|
||||
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
|
||||
if (this.initiatedByMe) {
|
||||
this.cancel({
|
||||
reason: "Other party didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
} else {
|
||||
this.cancel({
|
||||
reason: "User didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while cancelling verification request", err);
|
||||
}
|
||||
@@ -791,16 +820,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (!isLiveEvent) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
// a timestamp is not provided on all to_device events
|
||||
const timestamp = this.channel.getTimestamp(event);
|
||||
if (Number.isFinite(timestamp)) {
|
||||
const elapsed = Date.now() - timestamp;
|
||||
// don't allow interaction on old requests
|
||||
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
|
||||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
|
||||
) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,6 +840,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
this._eventsByThem.delete(type);
|
||||
}
|
||||
}
|
||||
// also remember when we received the request event
|
||||
this._requestReceivedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -890,7 +890,7 @@ function getResponseContentType(response) {
|
||||
|
||||
try {
|
||||
return parseContentType(contentType);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ matrixcs.request(request);
|
||||
utils.runPolyfills();
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
|
||||
+8
-2
@@ -113,13 +113,19 @@ export function setCryptoStoreFactory(fac) {
|
||||
cryptoStoreFactory = fac;
|
||||
}
|
||||
|
||||
|
||||
interface ICreateClientOpts {
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
store?: Store;
|
||||
cryptoStore?: CryptoStore;
|
||||
scheduler?: MatrixScheduler;
|
||||
request?: Request;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
identityServer?: any;
|
||||
localTimeoutMs?: number;
|
||||
useAuthorizationHeader?: boolean;
|
||||
queryParams?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,7 +193,7 @@ export function createClient(opts: ICreateClientOpts | string) {
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will implement a callback which meets this
|
||||
|
||||
@@ -647,7 +647,8 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||
if (timeline1 === timeline2) {
|
||||
// both events are in the same timeline - figure out their
|
||||
// relative indices
|
||||
let idx1, idx2;
|
||||
let idx1;
|
||||
let idx2;
|
||||
const events = timeline1.getEvents();
|
||||
for (let idx = 0; idx < events.length &&
|
||||
(idx1 === undefined || idx2 === undefined); idx++) {
|
||||
|
||||
+30
-3
@@ -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]) {
|
||||
@@ -144,6 +144,10 @@ export const MatrixEvent = function(
|
||||
*/
|
||||
this._forwardingCurve25519KeyChain = [];
|
||||
|
||||
/* where the decryption key is untrusted
|
||||
*/
|
||||
this._untrusted = null;
|
||||
|
||||
/* if we have a process decrypting this event, a Promise which resolves
|
||||
* when it is finished. Normally null.
|
||||
*/
|
||||
@@ -160,12 +164,16 @@ export const MatrixEvent = function(
|
||||
* so it can be easily accessed from the timeline.
|
||||
*/
|
||||
this.verificationRequest = null;
|
||||
|
||||
/* The txnId with which this event was sent if it was during this session,
|
||||
allows for a unique ID which does not change when the event comes back down sync.
|
||||
*/
|
||||
this._txnId = null;
|
||||
};
|
||||
utils.inherits(MatrixEvent, EventEmitter);
|
||||
|
||||
|
||||
utils.extend(MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||
@@ -599,6 +607,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
decryptionResult.claimedEd25519Key || null;
|
||||
this._forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this._untrusted = decryptionResult.untrusted || false;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -617,7 +626,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";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -689,6 +698,16 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this._forwardingCurve25519KeyChain;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the decryption key was obtained from an untrusted source. If so,
|
||||
* we cannot verify the authenticity of the message.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isKeySourceUntrusted: function() {
|
||||
return this._untrusted;
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
return this.event.unsigned || {};
|
||||
},
|
||||
@@ -1070,6 +1089,14 @@ utils.extend(MatrixEvent.prototype, {
|
||||
setVerificationRequest: function(request) {
|
||||
this.verificationRequest = request;
|
||||
},
|
||||
|
||||
setTxnId(txnId) {
|
||||
this._txnId = txnId;
|
||||
},
|
||||
|
||||
getTxnId() {
|
||||
return this._txnId;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from "events";
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
|
||||
import {getHttpUriForMxc} from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
|
||||
/**
|
||||
@@ -274,10 +274,6 @@ RoomMember.prototype.getAvatarUrl =
|
||||
);
|
||||
if (httpUrl) {
|
||||
return httpUrl;
|
||||
} else if (allowDefault) {
|
||||
return getIdenticonUri(
|
||||
baseUrl, this.userId, width, height,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -286,9 +282,9 @@ RoomMember.prototype.getAvatarUrl =
|
||||
* @return {string} the mxc avatar url
|
||||
*/
|
||||
RoomMember.prototype.getMxcAvatarUrl = function() {
|
||||
if(this.events.member) {
|
||||
if (this.events.member) {
|
||||
return this.events.member.getDirectionalContent().avatar_url;
|
||||
} else if(this.user) {
|
||||
} else if (this.user) {
|
||||
return this.user.avatarUrl;
|
||||
}
|
||||
return null;
|
||||
|
||||
+24
-17
@@ -68,9 +68,7 @@ export function RoomState(roomId, oobMemberFlags = undefined) {
|
||||
this.members = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this.events = {
|
||||
// eventType: { stateKey: MatrixEvent }
|
||||
};
|
||||
this.events = new Map(); // Map<eventType, Map<stateKey, MatrixEvent>>
|
||||
this.paginationToken = null;
|
||||
|
||||
this._sentinels = {
|
||||
@@ -211,14 +209,14 @@ RoomState.prototype.getSentinelMember = function(userId) {
|
||||
* <code>undefined</code>, else a single event (or null if no match found).
|
||||
*/
|
||||
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
|
||||
if (!this.events[eventType]) {
|
||||
if (!this.events.has(eventType)) {
|
||||
// no match
|
||||
return stateKey === undefined ? [] : null;
|
||||
}
|
||||
if (stateKey === undefined) { // return all values
|
||||
return utils.values(this.events[eventType]);
|
||||
return Array.from(this.events.get(eventType).values());
|
||||
}
|
||||
const event = this.events[eventType][stateKey];
|
||||
const event = this.events.get(eventType).get(stateKey);
|
||||
return event ? event : null;
|
||||
};
|
||||
|
||||
@@ -238,9 +236,8 @@ RoomState.prototype.clone = function() {
|
||||
const status = this._oobMemberFlags.status;
|
||||
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
||||
|
||||
Object.values(this.events).forEach((eventsByStateKey) => {
|
||||
const eventsForType = Object.values(eventsByStateKey);
|
||||
copy.setStateEvents(eventsForType);
|
||||
Array.from(this.events.values()).forEach((eventsByStateKey) => {
|
||||
copy.setStateEvents(Array.from(eventsByStateKey.values()));
|
||||
});
|
||||
|
||||
// Ugly hack: see above
|
||||
@@ -276,8 +273,8 @@ RoomState.prototype.clone = function() {
|
||||
*/
|
||||
RoomState.prototype.setUnknownStateEvents = function(events) {
|
||||
const unknownStateEvents = events.filter((event) => {
|
||||
return this.events[event.getType()] === undefined ||
|
||||
this.events[event.getType()][event.getStateKey()] === undefined;
|
||||
return !this.events.has(event.getType()) ||
|
||||
!this.events.get(event.getType()).has(event.getStateKey());
|
||||
});
|
||||
|
||||
this.setStateEvents(unknownStateEvents);
|
||||
@@ -306,6 +303,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastStateEvent = self._getStateEventMatching(event);
|
||||
self._setStateEvent(event);
|
||||
if (event.getType() === "m.room.member") {
|
||||
_updateDisplayNameCache(
|
||||
@@ -313,7 +311,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
);
|
||||
_updateThirdPartyTokenCache(self, event);
|
||||
}
|
||||
self.emit("RoomState.events", event, self);
|
||||
self.emit("RoomState.events", event, self, lastStateEvent);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
@@ -385,10 +383,15 @@ RoomState.prototype._getOrCreateMember = function(userId, event) {
|
||||
};
|
||||
|
||||
RoomState.prototype._setStateEvent = function(event) {
|
||||
if (this.events[event.getType()] === undefined) {
|
||||
this.events[event.getType()] = {};
|
||||
if (!this.events.has(event.getType())) {
|
||||
this.events.set(event.getType(), new Map());
|
||||
}
|
||||
this.events[event.getType()][event.getStateKey()] = event;
|
||||
this.events.get(event.getType()).set(event.getStateKey(), event);
|
||||
};
|
||||
|
||||
RoomState.prototype._getStateEventMatching = function(event) {
|
||||
if (!this.events.has(event.getType())) return null;
|
||||
return this.events.get(event.getType()).get(event.getStateKey());
|
||||
};
|
||||
|
||||
RoomState.prototype._updateMember = function(member) {
|
||||
@@ -670,7 +673,7 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
|
||||
const userPowerLevel = power_levels.users && power_levels.users[userId];
|
||||
if (Number.isFinite(userPowerLevel)) {
|
||||
powerLevel = userPowerLevel;
|
||||
} else if(Number.isFinite(power_levels.users_default)) {
|
||||
} else if (Number.isFinite(power_levels.users_default)) {
|
||||
powerLevel = power_levels.users_default;
|
||||
}
|
||||
|
||||
@@ -769,8 +772,12 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.events dictionary
|
||||
* was updated.
|
||||
* @param {MatrixEvent} prevEvent The event being replaced by the new state, if
|
||||
* known. Note that this can differ from `getPrevContent()` on the new state event
|
||||
* as this is the store's view of the last state, not the previous state provided
|
||||
* by the server.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.events", function(event, state){
|
||||
* matrixClient.on("RoomState.events", function(event, state, prevEvent){
|
||||
* var newStateEvent = event;
|
||||
* });
|
||||
*/
|
||||
|
||||
+12
-9
@@ -23,7 +23,7 @@ limitations under the License.
|
||||
import {EventEmitter} from "events";
|
||||
import {EventTimelineSet} from "./event-timeline-set";
|
||||
import {EventTimeline} from "./event-timeline";
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
|
||||
import {getHttpUriForMxc} from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import {EventStatus, MatrixEvent} from "./event";
|
||||
import {RoomMember} from "./room-member";
|
||||
@@ -122,6 +122,10 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
opts = opts || {};
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
|
||||
// In some cases, we add listeners for every displayed Matrix event, so it's
|
||||
// common to have quite a few more than the default limit.
|
||||
this.setMaxListeners(100);
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
@@ -814,10 +818,6 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
return getHttpUriForMxc(
|
||||
baseUrl, mainUrl, width, height, resizeMethod,
|
||||
);
|
||||
} else if (allowDefault) {
|
||||
return getIdenticonUri(
|
||||
baseUrl, this.roomId, width, height,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1793,8 +1793,9 @@ Room.prototype.addAccountData = function(events) {
|
||||
if (event.getType() === "m.tag") {
|
||||
this.addTags(event);
|
||||
}
|
||||
const lastEvent = this.accountData[event.getType()];
|
||||
this.accountData[event.getType()] = event;
|
||||
this.emit("Room.accountData", event, this);
|
||||
this.emit("Room.accountData", event, this, lastEvent);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1899,14 +1900,14 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
// let's try to figure out who was here before
|
||||
let leftNames = otherNames;
|
||||
// if we didn't have heroes, try finding them in the room state
|
||||
if(!leftNames.length) {
|
||||
if (!leftNames.length) {
|
||||
leftNames = room.currentState.getMembers().filter((m) => {
|
||||
return m.userId !== userId &&
|
||||
m.membership !== "invite" &&
|
||||
m.membership !== "join";
|
||||
}).map((m) => m.name);
|
||||
}
|
||||
if(leftNames.length) {
|
||||
if (leftNames.length) {
|
||||
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
|
||||
} else {
|
||||
return "Empty room";
|
||||
@@ -1991,8 +1992,10 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
|
||||
* @event module:client~MatrixClient#"Room.accountData"
|
||||
* @param {event} event The account_data event
|
||||
* @param {Room} room The room whose account_data was updated.
|
||||
* @param {MatrixEvent} prevEvent The event being replaced by
|
||||
* the new account data, if known.
|
||||
* @example
|
||||
* matrixClient.on("Room.accountData", function(event, room){
|
||||
* matrixClient.on("Room.accountData", function(event, room, oldEvent){
|
||||
* if (event.getType() === "m.room.colorscheme") {
|
||||
* applyColorScheme(event.getContents());
|
||||
* }
|
||||
|
||||
+10
-2
@@ -129,7 +129,11 @@ User.prototype.setPresenceEvent = function(event) {
|
||||
*/
|
||||
User.prototype.setDisplayName = function(name) {
|
||||
const oldName = this.displayName;
|
||||
this.displayName = name;
|
||||
if (typeof name === "string") {
|
||||
this.displayName = name;
|
||||
} else {
|
||||
this.displayName = undefined;
|
||||
}
|
||||
if (name !== oldName) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
@@ -142,7 +146,11 @@ User.prototype.setDisplayName = function(name) {
|
||||
* @param {string} name The new display name.
|
||||
*/
|
||||
User.prototype.setRawDisplayName = function(name) {
|
||||
this.rawDisplayName = name;
|
||||
if (typeof name === "string") {
|
||||
this.rawDisplayName = name;
|
||||
} else {
|
||||
this.rawDisplayName = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
+9
-27
@@ -84,12 +84,15 @@ export function PushProcessor(client) {
|
||||
// $glob: RegExp,
|
||||
};
|
||||
|
||||
const matchingRuleFromKindSet = (ev, kindset, device) => {
|
||||
const matchingRuleFromKindSet = (ev, kindset) => {
|
||||
for (let ruleKindIndex = 0;
|
||||
ruleKindIndex < RULEKINDS_IN_ORDER.length;
|
||||
++ruleKindIndex) {
|
||||
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
|
||||
const ruleset = kindset[kind];
|
||||
if (!ruleset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
|
||||
const rule = ruleset[ruleIndex];
|
||||
@@ -97,7 +100,7 @@ export function PushProcessor(client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawrule = templateRuleToRaw(kind, rule, device);
|
||||
const rawrule = templateRuleToRaw(kind, rule);
|
||||
if (!rawrule) {
|
||||
continue;
|
||||
}
|
||||
@@ -111,7 +114,7 @@ export function PushProcessor(client) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const templateRuleToRaw = function(kind, tprule, device) {
|
||||
const templateRuleToRaw = function(kind, tprule) {
|
||||
const rawrule = {
|
||||
'rule_id': tprule.rule_id,
|
||||
'actions': tprule.actions,
|
||||
@@ -153,19 +156,12 @@ export function PushProcessor(client) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (device) {
|
||||
rawrule.conditions.push({
|
||||
'kind': 'device',
|
||||
'profile_tag': device,
|
||||
});
|
||||
}
|
||||
return rawrule;
|
||||
};
|
||||
|
||||
const eventFulfillsCondition = function(cond, ev) {
|
||||
const condition_functions = {
|
||||
"event_match": eventFulfillsEventMatchCondition,
|
||||
"device": eventFulfillsDeviceCondition,
|
||||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||||
"room_member_count": eventFulfillsRoomMemberCountCondition,
|
||||
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
|
||||
@@ -257,10 +253,6 @@ export function PushProcessor(client) {
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
const eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
return false; // XXX: Allow a profile tag to be set for the web client instance
|
||||
};
|
||||
|
||||
const eventFulfillsEventMatchCondition = function(cond, ev) {
|
||||
if (!cond.key) {
|
||||
return false;
|
||||
@@ -325,23 +317,13 @@ export function PushProcessor(client) {
|
||||
};
|
||||
|
||||
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) {
|
||||
if (!rulesets) {
|
||||
return null;
|
||||
}
|
||||
if (ev.getSender() == client.credentials.userId) {
|
||||
if (ev.getSender() === client.credentials.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allDevNames = Object.keys(rulesets.device);
|
||||
for (let i = 0; i < allDevNames.length; ++i) {
|
||||
const devname = allDevNames[i];
|
||||
const devrules = rulesets.device[devname];
|
||||
|
||||
const matchingRule = matchingRuleFromKindSet(devrules, devname);
|
||||
if (matchingRule) {
|
||||
return matchingRule;
|
||||
}
|
||||
}
|
||||
return matchingRuleFromKindSet(ev, rulesets.global);
|
||||
};
|
||||
|
||||
@@ -392,7 +374,7 @@ export function PushProcessor(client) {
|
||||
* @return {object} The push rule, or null if no such rule was found
|
||||
*/
|
||||
this.getPushRuleById = function(ruleId) {
|
||||
for (const scope of ['device', 'global']) {
|
||||
for (const scope of ['global']) {
|
||||
if (client.pushRules[scope] === undefined) continue;
|
||||
|
||||
for (const kind of RULEKINDS_IN_ORDER) {
|
||||
|
||||
@@ -185,8 +185,8 @@ function _runCallbacks() {
|
||||
*/
|
||||
function binarySearch(array, func) {
|
||||
// min is inclusive, max exclusive.
|
||||
let min = 0,
|
||||
max = array.length;
|
||||
let min = 0;
|
||||
let max = array.length;
|
||||
|
||||
while (min < max) {
|
||||
const mid = (min + max) >> 1;
|
||||
|
||||
+10
-5
@@ -701,7 +701,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
|
||||
|
||||
try {
|
||||
await this._processSyncResponse(syncEventData, data);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
logger.error("Error processing cached sync", e.stack || e);
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
||||
|
||||
try {
|
||||
await this._processSyncResponse(syncEventData, data);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// log the exception with stack if we have it, else fall back
|
||||
// to the plain description
|
||||
logger.error("Caught /sync error", e.stack || e);
|
||||
@@ -894,7 +894,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
|
||||
logger.error("/sync error %s", err);
|
||||
logger.error(err);
|
||||
|
||||
if(this._shouldAbortSync(err)) {
|
||||
if (this._shouldAbortSync(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1023,18 +1023,23 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
// handle non-room account_data
|
||||
if (data.account_data && utils.isArray(data.account_data.events)) {
|
||||
const events = data.account_data.events.map(client.getEventMapper());
|
||||
const prevEventsMap = events.reduce((m, c) => {
|
||||
m[c.getId()] = client.store.getAccountData(c.getType());
|
||||
return m;
|
||||
}, {});
|
||||
client.store.storeAccountDataEvents(events);
|
||||
events.forEach(
|
||||
function(accountDataEvent) {
|
||||
// Honour push rules that come down the sync stream but also
|
||||
// honour push rules that were previously cached. Base rules
|
||||
// will be updated when we recieve push rules via getPushRules
|
||||
// will be updated when we receive push rules via getPushRules
|
||||
// (see SyncApi.prototype.sync) before syncing over the network.
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
const rules = accountDataEvent.getContent();
|
||||
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
}
|
||||
client.emit("accountData", accountDataEvent);
|
||||
const prevEvent = prevEventsMap[accountDataEvent.getId()];
|
||||
client.emit("accountData", accountDataEvent, prevEvent);
|
||||
return accountDataEvent;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -396,7 +396,8 @@ TimelineWindow.prototype.getEvents = function() {
|
||||
// (Note that both this._start.index and this._end.index are relative
|
||||
// to their respective timelines' BaseIndex).
|
||||
//
|
||||
let startIndex = 0, endIndex = events.length;
|
||||
let startIndex = 0;
|
||||
let endIndex = events.length;
|
||||
if (timeline === this._start.timeline) {
|
||||
startIndex = this._start.index + timeline.getBaseIndex();
|
||||
}
|
||||
|
||||
+169
-161
@@ -21,7 +21,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import unhomoglyph from 'unhomoglyph';
|
||||
import {ConnectionError} from "./http-api";
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
@@ -50,7 +49,7 @@ export function encodeParams(params: Record<string, string>): string {
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
export function encodeUri(pathTemplate: string,
|
||||
variables: Record<string, string>): string {
|
||||
variables: Record<string, string>): string {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
@@ -86,7 +85,7 @@ export function map<T, S>(array: T[], fn: (t: T) => S): S[] {
|
||||
* @return {Array} A new array with the results of the function.
|
||||
*/
|
||||
export function filter<T>(array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
|
||||
const results: T[] = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
@@ -152,10 +151,10 @@ export function forEach<T>(array: T[], fn: (t: T, i: number) => void) {
|
||||
* the given function.
|
||||
*/
|
||||
export function findElement<T>(
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean
|
||||
) {
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean,
|
||||
) {
|
||||
let i;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
@@ -183,10 +182,10 @@ export function findElement<T>(
|
||||
* @return {boolean} True if an element was removed.
|
||||
*/
|
||||
export function removeElement<T>(
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean
|
||||
) {
|
||||
array: T[],
|
||||
fn: (t: T, i?: number, a?: T[]) => boolean,
|
||||
reverse?: boolean,
|
||||
) {
|
||||
let i;
|
||||
let removed;
|
||||
if (reverse) {
|
||||
@@ -294,7 +293,7 @@ export function deepCompare(x: any, y: any): boolean {
|
||||
|
||||
// special-case NaN (since NaN !== NaN)
|
||||
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// special-case null (since typeof null == 'object', but null.constructor
|
||||
@@ -369,10 +368,10 @@ export function deepCompare(x: any, y: any): boolean {
|
||||
*
|
||||
* @return {Object} target
|
||||
*/
|
||||
export function extend() {
|
||||
const target = arguments[0] || {};
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
const source = arguments[i];
|
||||
export function extend(...restParams) {
|
||||
const target = restParams[0] || {};
|
||||
for (let i = 1; i < restParams.length; i++) {
|
||||
const source = restParams[i];
|
||||
if (!source) continue;
|
||||
for (const propName in source) { // eslint-disable-line guard-for-in
|
||||
target[propName] = source[propName];
|
||||
@@ -390,36 +389,36 @@ export function runPolyfills() {
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function(fun: Function/*, thisArg*/) {
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const t = Object(this);
|
||||
const len = t.length >>> 0;
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const res = [];
|
||||
const thisArg = arguments.length >= 2 ? arguments[1] : void 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
const val = t[i];
|
||||
|
||||
// NOTE: Technically this should Object.defineProperty at
|
||||
// the next index, as push can be affected by
|
||||
// properties on Object.prototype and Array.prototype.
|
||||
// But that method's new, and collisions should be
|
||||
// rare, so use the more-compatible alternative.
|
||||
if (fun.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype.filter = function(fun: Function/*, thisArg*/, ...restProps) {
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
const t = Object(this);
|
||||
const len = t.length >>> 0;
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const res = [];
|
||||
const thisArg = restProps ? restProps[0] : void 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
const val = t[i];
|
||||
|
||||
// NOTE: Technically this should Object.defineProperty at
|
||||
// the next index, as push can be affected by
|
||||
// properties on Object.prototype and Array.prototype.
|
||||
// But that method's new, and collisions should be
|
||||
// rare, so use the more-compatible alternative.
|
||||
if (fun.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.map
|
||||
@@ -429,151 +428,156 @@ export function runPolyfills() {
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.19
|
||||
// Reference: http://es5.github.io/#x15.4.4.19
|
||||
if (!Array.prototype.map) {
|
||||
Array.prototype.map = function(callback, thisArg) {
|
||||
let T, k;
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype.map = function(callback, thisArg) {
|
||||
let T;
|
||||
let k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this|
|
||||
// value as the argument.
|
||||
const O = Object(this);
|
||||
// 1. Let O be the result of calling ToObject passing the |this|
|
||||
// value as the argument.
|
||||
const O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal
|
||||
// method of O with the argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
const len = O.length >>> 0;
|
||||
// 2. Let lenValue be the result of calling the Get internal
|
||||
// method of O with the argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
const len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let A be a new array created as if by the expression new Array(len)
|
||||
// where Array is the standard built-in constructor with that name and
|
||||
// len is the value of len.
|
||||
const A = new Array(len);
|
||||
// 6. Let A be a new array created as if by the expression new Array(len)
|
||||
// where Array is the standard built-in constructor with that name and
|
||||
// len is the value of len.
|
||||
const A = new Array(len);
|
||||
|
||||
// 7. Let k be 0
|
||||
k = 0;
|
||||
// 7. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 8. Repeat, while k < len
|
||||
while (k < len) {
|
||||
let kValue, mappedValue;
|
||||
// 8. Repeat, while k < len
|
||||
while (k < len) {
|
||||
let kValue;
|
||||
let mappedValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
// i. Let kValue be the result of calling the Get internal
|
||||
// method of O with argument Pk.
|
||||
kValue = O[k];
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
// i. Let kValue be the result of calling the Get internal
|
||||
// method of O with argument Pk.
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Let mappedValue be the result of calling the Call internal
|
||||
// method of callback with T as the this value and argument
|
||||
// list containing kValue, k, and O.
|
||||
mappedValue = callback.call(T, kValue, k, O);
|
||||
// ii. Let mappedValue be the result of calling the Call internal
|
||||
// method of callback with T as the this value and argument
|
||||
// list containing kValue, k, and O.
|
||||
mappedValue = callback.call(T, kValue, k, O);
|
||||
|
||||
// iii. Call the DefineOwnProperty internal method of A with arguments
|
||||
// Pk, Property Descriptor
|
||||
// { Value: mappedValue,
|
||||
// Writable: true,
|
||||
// Enumerable: true,
|
||||
// Configurable: true },
|
||||
// and false.
|
||||
// iii. Call the DefineOwnProperty internal method of A with arguments
|
||||
// Pk, Property Descriptor
|
||||
// { Value: mappedValue,
|
||||
// Writable: true,
|
||||
// Enumerable: true,
|
||||
// Configurable: true },
|
||||
// and false.
|
||||
|
||||
// In browsers that support Object.defineProperty, use the following:
|
||||
// Object.defineProperty(A, k, {
|
||||
// value: mappedValue,
|
||||
// writable: true,
|
||||
// enumerable: true,
|
||||
// configurable: true
|
||||
// });
|
||||
// In browsers that support Object.defineProperty, use the following:
|
||||
// Object.defineProperty(A, k, {
|
||||
// value: mappedValue,
|
||||
// writable: true,
|
||||
// enumerable: true,
|
||||
// configurable: true
|
||||
// });
|
||||
|
||||
// For best browser support, use the following:
|
||||
A[k] = mappedValue;
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
// For best browser support, use the following:
|
||||
A[k] = mappedValue;
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 9. return A
|
||||
return A;
|
||||
};
|
||||
// 9. return A
|
||||
return A;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.forEach
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.18
|
||||
// Reference: http://es5.github.io/#x15.4.4.18
|
||||
if (!Array.prototype.forEach) {
|
||||
Array.prototype.forEach = function(callback, thisArg) {
|
||||
let T, k;
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype.forEach = function(callback, thisArg) {
|
||||
let T;
|
||||
let k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this| value as the
|
||||
// argument.
|
||||
const O = Object(this);
|
||||
// 1. Let O be the result of calling ToObject passing the |this| value as the
|
||||
// argument.
|
||||
const O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal method of O with the
|
||||
// argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
const len = O.length >>> 0;
|
||||
// 2. Let lenValue be the result of calling the Get internal method of O with the
|
||||
// argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
const len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let k be 0
|
||||
k = 0;
|
||||
// 6. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
let kValue;
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
let kValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with
|
||||
// argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
// i. Let kValue be the result of calling the Get internal method of O with
|
||||
// argument Pk
|
||||
kValue = O[k];
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with
|
||||
// argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
// i. Let kValue be the result of calling the Get internal method of O with
|
||||
// argument Pk
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Call the Call internal method of callback with T as the this value and
|
||||
// argument list containing kValue, k, and O.
|
||||
callback.call(T, kValue, k, O);
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
// ii. Call the Call internal method of callback with T as the this value and
|
||||
// argument list containing kValue, k, and O.
|
||||
callback.call(T, kValue, k, O);
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
// 8. return undefined
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,7 +661,10 @@ export function isNumber(value: any): boolean {
|
||||
* @return {string} a string with the hidden characters removed
|
||||
*/
|
||||
export function removeHiddenChars(str: string): string {
|
||||
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
|
||||
if (typeof str === "string") {
|
||||
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters.
|
||||
@@ -667,6 +674,7 @@ export function removeHiddenChars(str: string): string {
|
||||
// LTR/RTL and other directional formatting marks U+202A - U+202F
|
||||
// Combining characters U+0300 - U+036F
|
||||
// Zero width no-break space (BOM) U+FEFF
|
||||
// eslint-disable-next-line no-misleading-character-class
|
||||
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
|
||||
|
||||
export function escapeRegExp(string: string): string {
|
||||
@@ -725,9 +733,9 @@ export function defer() {
|
||||
}
|
||||
|
||||
export async function promiseMapSeries<T>(
|
||||
promises: Promise<T>[],
|
||||
fn: (t: T) => void
|
||||
): Promise<void> {
|
||||
promises: Promise<T>[],
|
||||
fn: (t: T) => void,
|
||||
): Promise<void> {
|
||||
for (const o of await promises) {
|
||||
await fn(await o);
|
||||
}
|
||||
|
||||
+14
-7
@@ -176,7 +176,7 @@ MatrixCall.prototype.placeScreenSharingCall =
|
||||
debuglog("Got screen stream, requesting audio stream...");
|
||||
const audioConstraints = _getUserMediaVideoContraints('voice');
|
||||
_placeCallWithConstraints(self, audioConstraints);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
self.emit("error",
|
||||
callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
@@ -403,7 +403,7 @@ MatrixCall.prototype._initWithHangup = function(event) {
|
||||
* Answer a call.
|
||||
*/
|
||||
MatrixCall.prototype.answer = function() {
|
||||
debuglog("Answering call %s of type %s", this.callId, this.type);
|
||||
debuglog(`Answering call ${this.callId} of type ${this.type}`);
|
||||
const self = this;
|
||||
|
||||
if (self._answerContent) {
|
||||
@@ -412,8 +412,10 @@ MatrixCall.prototype.answer = function() {
|
||||
}
|
||||
|
||||
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
||||
const constraints = _getUserMediaVideoContraints(this.type);
|
||||
logger.log("Getting user media with constraints", constraints);
|
||||
this.webRtc.getUserMedia(
|
||||
_getUserMediaVideoContraints(this.type),
|
||||
constraints,
|
||||
hookCallback(self, self._maybeGotUserMediaForAnswer),
|
||||
hookCallback(self, self._maybeGotUserMediaForAnswer),
|
||||
);
|
||||
@@ -1065,7 +1067,7 @@ const terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||||
};
|
||||
|
||||
const stopAllMedia = function(self) {
|
||||
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
|
||||
debuglog(`stopAllMedia (stream=${self.localAVStream})`);
|
||||
if (self.localAVStream) {
|
||||
forAllTracksOnStream(self.localAVStream, function(t) {
|
||||
if (t.stop) {
|
||||
@@ -1127,7 +1129,11 @@ const _tryPlayRemoteAudioStream = async function(self) {
|
||||
const player = self.getRemoteAudioElement();
|
||||
|
||||
// if audioOutput is non-default:
|
||||
if (audioOutput) await player.setSinkId(audioOutput);
|
||||
try {
|
||||
if (audioOutput) await player.setSinkId(audioOutput);
|
||||
} catch (e) {
|
||||
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||
}
|
||||
|
||||
player.autoplay = true;
|
||||
self.assignElement(player, self.remoteAStream, "remoteAudio");
|
||||
@@ -1188,8 +1194,8 @@ const _sendCandidateQueue = function(self) {
|
||||
|
||||
if (self.candidateSendTries > 5) {
|
||||
debuglog(
|
||||
"Failed to send candidates on attempt %s. Giving up for now.",
|
||||
self.candidateSendTries,
|
||||
"Failed to send candidates on attempt " + self.candidateSendTries +
|
||||
". Giving up for now.",
|
||||
);
|
||||
self.candidateSendTries = 0;
|
||||
return;
|
||||
@@ -1205,6 +1211,7 @@ const _sendCandidateQueue = function(self) {
|
||||
};
|
||||
|
||||
const _placeCallWithConstraints = function(self, constraints) {
|
||||
logger.log("Getting user media with constraints", constraints);
|
||||
self.client.callList[self.callId] = self;
|
||||
self.webRtc.getUserMedia(
|
||||
constraints,
|
||||
|
||||
-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