You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-28 05:03:59 +03:00
Merge branch 'develop' into fix-register-auth-with-new-spec
This commit is contained in:
218
CHANGELOG.md
218
CHANGELOG.md
@@ -1,3 +1,221 @@
|
|||||||
|
Changes in [6.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.1.0) (2020-05-19)
|
||||||
|
================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.1.0-rc.1...v6.1.0)
|
||||||
|
|
||||||
|
* No changes since rc.1
|
||||||
|
|
||||||
|
Changes in [6.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.1.0-rc.1) (2020-05-14)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0...v6.1.0-rc.1)
|
||||||
|
|
||||||
|
* Remove support for asymmetric 4S encryption
|
||||||
|
[\#1373](https://github.com/matrix-org/matrix-js-sdk/pull/1373)
|
||||||
|
* Increase timeout for 2nd phase of Olm session creation
|
||||||
|
[\#1367](https://github.com/matrix-org/matrix-js-sdk/pull/1367)
|
||||||
|
* Add logging on decryption retries
|
||||||
|
[\#1366](https://github.com/matrix-org/matrix-js-sdk/pull/1366)
|
||||||
|
* Emit event when a trusted self-key is stored
|
||||||
|
[\#1364](https://github.com/matrix-org/matrix-js-sdk/pull/1364)
|
||||||
|
* Customize error payload for oversized messages
|
||||||
|
[\#1352](https://github.com/matrix-org/matrix-js-sdk/pull/1352)
|
||||||
|
* Return null for key backup state when we haven't checked yet
|
||||||
|
[\#1363](https://github.com/matrix-org/matrix-js-sdk/pull/1363)
|
||||||
|
* Added a progressCallback for backup key loading
|
||||||
|
[\#1351](https://github.com/matrix-org/matrix-js-sdk/pull/1351)
|
||||||
|
* Add initialFetch param to willUpdateDevices / devicesUpdated
|
||||||
|
[\#1360](https://github.com/matrix-org/matrix-js-sdk/pull/1360)
|
||||||
|
* Fix race between sending .request and receiving .ready over to_device
|
||||||
|
[\#1359](https://github.com/matrix-org/matrix-js-sdk/pull/1359)
|
||||||
|
* Handle race between sending and await next event from other party
|
||||||
|
[\#1357](https://github.com/matrix-org/matrix-js-sdk/pull/1357)
|
||||||
|
* Add crypto.willUpdateDevices event and make
|
||||||
|
getStoredDevices/getStoredDevicesForUser synchronous
|
||||||
|
[\#1354](https://github.com/matrix-org/matrix-js-sdk/pull/1354)
|
||||||
|
* Fix sender of local echo events in unsigned redactions
|
||||||
|
[\#1350](https://github.com/matrix-org/matrix-js-sdk/pull/1350)
|
||||||
|
* Remove redundant key backup setup path
|
||||||
|
[\#1353](https://github.com/matrix-org/matrix-js-sdk/pull/1353)
|
||||||
|
* Remove some dead code from _retryDecryption
|
||||||
|
[\#1349](https://github.com/matrix-org/matrix-js-sdk/pull/1349)
|
||||||
|
* Don't send key requests until after sync processing is finished
|
||||||
|
[\#1348](https://github.com/matrix-org/matrix-js-sdk/pull/1348)
|
||||||
|
* Prevent attempts to send olm messages to ourselves
|
||||||
|
[\#1346](https://github.com/matrix-org/matrix-js-sdk/pull/1346)
|
||||||
|
* Retry account data upload requests
|
||||||
|
[\#1345](https://github.com/matrix-org/matrix-js-sdk/pull/1345)
|
||||||
|
* Log first known index with megolm session updates
|
||||||
|
[\#1344](https://github.com/matrix-org/matrix-js-sdk/pull/1344)
|
||||||
|
* Prune to_device messages to avoid sending empty messages
|
||||||
|
[\#1343](https://github.com/matrix-org/matrix-js-sdk/pull/1343)
|
||||||
|
* Convert bunch of things to TypeScript
|
||||||
|
[\#1335](https://github.com/matrix-org/matrix-js-sdk/pull/1335)
|
||||||
|
* Add logging when making new Olm sessions
|
||||||
|
[\#1342](https://github.com/matrix-org/matrix-js-sdk/pull/1342)
|
||||||
|
* Fix: handle filter not found
|
||||||
|
[\#1340](https://github.com/matrix-org/matrix-js-sdk/pull/1340)
|
||||||
|
* Make getAccountDataFromServer return null if not found
|
||||||
|
[\#1338](https://github.com/matrix-org/matrix-js-sdk/pull/1338)
|
||||||
|
* Fix setDefaultKeyId to fail if the request fails
|
||||||
|
[\#1336](https://github.com/matrix-org/matrix-js-sdk/pull/1336)
|
||||||
|
* Document setRoomEncryption not modifying room state
|
||||||
|
[\#1328](https://github.com/matrix-org/matrix-js-sdk/pull/1328)
|
||||||
|
* Fix: don't do extra /filter request when enabling lazy loading of members
|
||||||
|
[\#1332](https://github.com/matrix-org/matrix-js-sdk/pull/1332)
|
||||||
|
* Reject attemptAuth promise if no auth flow found
|
||||||
|
[\#1329](https://github.com/matrix-org/matrix-js-sdk/pull/1329)
|
||||||
|
* Fix FilterComponent allowed_values check
|
||||||
|
[\#1327](https://github.com/matrix-org/matrix-js-sdk/pull/1327)
|
||||||
|
* Serialise Olm prekey decryptions
|
||||||
|
[\#1326](https://github.com/matrix-org/matrix-js-sdk/pull/1326)
|
||||||
|
* Fix: crash when backup key needs fixing from corruption issue
|
||||||
|
[\#1324](https://github.com/matrix-org/matrix-js-sdk/pull/1324)
|
||||||
|
* Fix cross-signing/SSSS reset
|
||||||
|
[\#1322](https://github.com/matrix-org/matrix-js-sdk/pull/1322)
|
||||||
|
* Implement QR code reciprocate for self-verification with untrusted MSK
|
||||||
|
[\#1320](https://github.com/matrix-org/matrix-js-sdk/pull/1320)
|
||||||
|
|
||||||
|
Changes in [6.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0) (2020-05-05)
|
||||||
|
================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0-rc.2...v6.0.0)
|
||||||
|
|
||||||
|
* Add progress callback for key backups
|
||||||
|
[\#1368](https://github.com/matrix-org/matrix-js-sdk/pull/1368)
|
||||||
|
|
||||||
|
Changes in [6.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0-rc.2) (2020-05-01)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0-rc.1...v6.0.0-rc.2)
|
||||||
|
|
||||||
|
* Emit event when a trusted self-key is stored
|
||||||
|
[\#1365](https://github.com/matrix-org/matrix-js-sdk/pull/1365)
|
||||||
|
|
||||||
|
Changes in [6.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0-rc.1) (2020-04-30)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.4...v6.0.0-rc.1)
|
||||||
|
|
||||||
|
BREAKING CHANGES
|
||||||
|
---
|
||||||
|
|
||||||
|
* client.getStoredDevicesForUser and client.getStoredDevices are no longer async
|
||||||
|
|
||||||
|
All Changes
|
||||||
|
---
|
||||||
|
|
||||||
|
* Add initialFetch param to willUpdateDevices / devicesUpdated
|
||||||
|
[\#1362](https://github.com/matrix-org/matrix-js-sdk/pull/1362)
|
||||||
|
* Fix race between sending .request and receiving .ready over to_device
|
||||||
|
[\#1361](https://github.com/matrix-org/matrix-js-sdk/pull/1361)
|
||||||
|
* Handle race between sending and await next event from other party
|
||||||
|
[\#1358](https://github.com/matrix-org/matrix-js-sdk/pull/1358)
|
||||||
|
* Add crypto.willUpdateDevices event and make
|
||||||
|
getStoredDevices/getStoredDevicesForUser synchronous
|
||||||
|
[\#1356](https://github.com/matrix-org/matrix-js-sdk/pull/1356)
|
||||||
|
* Remove redundant key backup setup path
|
||||||
|
[\#1355](https://github.com/matrix-org/matrix-js-sdk/pull/1355)
|
||||||
|
|
||||||
|
Changes in [5.3.1-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.4) (2020-04-23)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.3...v5.3.1-rc.4)
|
||||||
|
|
||||||
|
* Retry account data upload requests
|
||||||
|
[\#1347](https://github.com/matrix-org/matrix-js-sdk/pull/1347)
|
||||||
|
* Fix: handle filter not found
|
||||||
|
[\#1341](https://github.com/matrix-org/matrix-js-sdk/pull/1341)
|
||||||
|
* Make getAccountDataFromServer return null if not found
|
||||||
|
[\#1339](https://github.com/matrix-org/matrix-js-sdk/pull/1339)
|
||||||
|
* Fix setDefaultKeyId to fail if the request fails
|
||||||
|
[\#1337](https://github.com/matrix-org/matrix-js-sdk/pull/1337)
|
||||||
|
* Fix: don't do extra /filter request when enabling lazy loading of members
|
||||||
|
[\#1333](https://github.com/matrix-org/matrix-js-sdk/pull/1333)
|
||||||
|
* Reject attemptAuth promise if no auth flow found
|
||||||
|
[\#1331](https://github.com/matrix-org/matrix-js-sdk/pull/1331)
|
||||||
|
* Serialise Olm prekey decryptions
|
||||||
|
[\#1330](https://github.com/matrix-org/matrix-js-sdk/pull/1330)
|
||||||
|
|
||||||
|
Changes in [5.3.1-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.3) (2020-04-17)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.2...v5.3.1-rc.3)
|
||||||
|
|
||||||
|
* Fix cross-signing/SSSS reset
|
||||||
|
[\#1323](https://github.com/matrix-org/matrix-js-sdk/pull/1323)
|
||||||
|
* Fix: crash when backup key needs fixing from corruption issue
|
||||||
|
[\#1325](https://github.com/matrix-org/matrix-js-sdk/pull/1325)
|
||||||
|
|
||||||
|
Changes in [5.3.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.2) (2020-04-16)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.1...v5.3.1-rc.2)
|
||||||
|
|
||||||
|
* Implement QR code reciprocate for self-verification with untrusted MSK
|
||||||
|
[\#1321](https://github.com/matrix-org/matrix-js-sdk/pull/1321)
|
||||||
|
|
||||||
|
Changes in [5.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.1) (2020-04-15)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.0-rc.1...v5.3.1-rc.1)
|
||||||
|
|
||||||
|
* Adapt release script for riot-desktop
|
||||||
|
[\#1319](https://github.com/matrix-org/matrix-js-sdk/pull/1319)
|
||||||
|
* Fix: prevent spurious notifications from indexer
|
||||||
|
[\#1318](https://github.com/matrix-org/matrix-js-sdk/pull/1318)
|
||||||
|
* Always create our own user object
|
||||||
|
[\#1317](https://github.com/matrix-org/matrix-js-sdk/pull/1317)
|
||||||
|
* Fix incorrect backup key format in SSSS
|
||||||
|
[\#1311](https://github.com/matrix-org/matrix-js-sdk/pull/1311)
|
||||||
|
* Fix e2ee crash after refreshing after having received a cross-singing key
|
||||||
|
reset
|
||||||
|
[\#1315](https://github.com/matrix-org/matrix-js-sdk/pull/1315)
|
||||||
|
* Fix: catch send errors in SAS verifier
|
||||||
|
[\#1314](https://github.com/matrix-org/matrix-js-sdk/pull/1314)
|
||||||
|
* Clear cross-signing keys when detecting the keys have changed
|
||||||
|
[\#1312](https://github.com/matrix-org/matrix-js-sdk/pull/1312)
|
||||||
|
* Upgrade deps
|
||||||
|
[\#1310](https://github.com/matrix-org/matrix-js-sdk/pull/1310)
|
||||||
|
|
||||||
|
Changes in [5.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.0-rc.1) (2020-04-08)
|
||||||
|
==========================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0...v5.3.0-rc.1)
|
||||||
|
|
||||||
|
* Store key backup key in cache as Uint8Array
|
||||||
|
[\#1308](https://github.com/matrix-org/matrix-js-sdk/pull/1308)
|
||||||
|
* Use the correct request body for the /keys/query endpoint.
|
||||||
|
[\#1307](https://github.com/matrix-org/matrix-js-sdk/pull/1307)
|
||||||
|
* Avoid creating two devices on registration
|
||||||
|
[\#1305](https://github.com/matrix-org/matrix-js-sdk/pull/1305)
|
||||||
|
* Lower max-warnings to 81
|
||||||
|
[\#1306](https://github.com/matrix-org/matrix-js-sdk/pull/1306)
|
||||||
|
* Move key backup key creation before caching
|
||||||
|
[\#1303](https://github.com/matrix-org/matrix-js-sdk/pull/1303)
|
||||||
|
* Expose function to force-reset outgoing room key requests
|
||||||
|
[\#1298](https://github.com/matrix-org/matrix-js-sdk/pull/1298)
|
||||||
|
* Add isSelfVerification property to VerificationRequest
|
||||||
|
[\#1302](https://github.com/matrix-org/matrix-js-sdk/pull/1302)
|
||||||
|
* QR code reciprocation
|
||||||
|
[\#1297](https://github.com/matrix-org/matrix-js-sdk/pull/1297)
|
||||||
|
* Add ability to check symmetric SSSS key before we try to use it
|
||||||
|
[\#1294](https://github.com/matrix-org/matrix-js-sdk/pull/1294)
|
||||||
|
* Add some debug logging for events stuck to bottom of timeline
|
||||||
|
[\#1296](https://github.com/matrix-org/matrix-js-sdk/pull/1296)
|
||||||
|
* Fix: spontanous verification request cancellation under some circumstances
|
||||||
|
[\#1295](https://github.com/matrix-org/matrix-js-sdk/pull/1295)
|
||||||
|
* Receive private key for caching from the app layer
|
||||||
|
[\#1293](https://github.com/matrix-org/matrix-js-sdk/pull/1293)
|
||||||
|
* Track whether we have verified a user before
|
||||||
|
[\#1292](https://github.com/matrix-org/matrix-js-sdk/pull/1292)
|
||||||
|
* Fix: error during tests
|
||||||
|
[\#1222](https://github.com/matrix-org/matrix-js-sdk/pull/1222)
|
||||||
|
* Send .done event for to_device verification
|
||||||
|
[\#1288](https://github.com/matrix-org/matrix-js-sdk/pull/1288)
|
||||||
|
* Request the key backup key & restore backup
|
||||||
|
[\#1291](https://github.com/matrix-org/matrix-js-sdk/pull/1291)
|
||||||
|
* Make screen sharing works on Chrome using getDisplayMedia()
|
||||||
|
[\#1276](https://github.com/matrix-org/matrix-js-sdk/pull/1276)
|
||||||
|
* Fix isVerified returning false
|
||||||
|
[\#1289](https://github.com/matrix-org/matrix-js-sdk/pull/1289)
|
||||||
|
* Fix: verification gets cancelled when event gets duplicated
|
||||||
|
[\#1286](https://github.com/matrix-org/matrix-js-sdk/pull/1286)
|
||||||
|
* Use requestSecret on the client to request secrets
|
||||||
|
[\#1287](https://github.com/matrix-org/matrix-js-sdk/pull/1287)
|
||||||
|
* Allow guests to fetch TURN servers
|
||||||
|
[\#1277](https://github.com/matrix-org/matrix-js-sdk/pull/1277)
|
||||||
|
|
||||||
Changes in [5.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0) (2020-03-30)
|
Changes in [5.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0) (2020-03-30)
|
||||||
================================================================================================
|
================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0-rc.1...v5.2.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0-rc.1...v5.2.0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "5.2.0",
|
"version": "6.1.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "yarn build",
|
"prepare": "yarn build",
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"files": [
|
"files": [
|
||||||
|
"dist",
|
||||||
"lib",
|
"lib",
|
||||||
"src",
|
"src",
|
||||||
"git-revision.txt",
|
"git-revision.txt",
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
"@babel/preset-typescript": "^7.7.4",
|
"@babel/preset-typescript": "^7.7.4",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.7.4",
|
||||||
"@types/node": "12",
|
"@types/node": "12",
|
||||||
|
"@types/request": "^2.48.4",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"babelify": "^10.0.0",
|
"babelify": "^10.0.0",
|
||||||
|
|||||||
15
release.sh
15
release.sh
@@ -38,6 +38,7 @@ $USAGE
|
|||||||
-c changelog_file: specify name of file containing changelog
|
-c changelog_file: specify name of file containing changelog
|
||||||
-x: skip updating the changelog
|
-x: skip updating the changelog
|
||||||
-z: skip generating the jsdoc
|
-z: skip generating the jsdoc
|
||||||
|
-n: skip publish to NPM
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +61,10 @@ fi
|
|||||||
|
|
||||||
skip_changelog=
|
skip_changelog=
|
||||||
skip_jsdoc=
|
skip_jsdoc=
|
||||||
|
skip_npm=
|
||||||
changelog_file="CHANGELOG.md"
|
changelog_file="CHANGELOG.md"
|
||||||
expected_npm_user="matrixdotorg"
|
expected_npm_user="matrixdotorg"
|
||||||
while getopts hc:u:xz f; do
|
while getopts hc:u:xzn f; do
|
||||||
case $f in
|
case $f in
|
||||||
h)
|
h)
|
||||||
help
|
help
|
||||||
@@ -77,6 +79,9 @@ while getopts hc:u:xz f; do
|
|||||||
z)
|
z)
|
||||||
skip_jsdoc=1
|
skip_jsdoc=1
|
||||||
;;
|
;;
|
||||||
|
n)
|
||||||
|
skip_npm=1
|
||||||
|
;;
|
||||||
u)
|
u)
|
||||||
expected_npm_user="$OPTARG"
|
expected_npm_user="$OPTARG"
|
||||||
;;
|
;;
|
||||||
@@ -96,11 +101,13 @@ fi
|
|||||||
|
|
||||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||||
# defined options and semantics than `yarn` for writing to the registry.
|
# defined options and semantics than `yarn` for writing to the registry.
|
||||||
|
if [ -z "$skip_npm" ]; then
|
||||||
actual_npm_user=`npm whoami`;
|
actual_npm_user=`npm whoami`;
|
||||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ignore leading v on release
|
# ignore leading v on release
|
||||||
release="${1#v}"
|
release="${1#v}"
|
||||||
@@ -298,12 +305,14 @@ rm "${latest_changes}"
|
|||||||
# defined options and semantics than `yarn` for writing to the registry.
|
# defined options and semantics than `yarn` for writing to the registry.
|
||||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
# Tag both releases and prereleases as `next` so the last stable release remains
|
||||||
# the default.
|
# the default.
|
||||||
|
if [ -z "$skip_npm" ]; then
|
||||||
npm publish --tag next
|
npm publish --tag next
|
||||||
if [ $prerelease -eq 0 ]; then
|
if [ $prerelease -eq 0 ]; then
|
||||||
# For a release, also add the default `latest` tag.
|
# For a release, also add the default `latest` tag.
|
||||||
package=$(cat package.json | jq -er .name)
|
package=$(cat package.json | jq -er .name)
|
||||||
npm dist-tag add "$package@$release" latest
|
npm dist-tag add "$package@$release" latest
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$skip_jsdoc" ]; then
|
if [ -z "$skip_jsdoc" ]; then
|
||||||
echo "generating jsdocs"
|
echo "generating jsdocs"
|
||||||
@@ -338,8 +347,10 @@ if [ -z "$skip_jsdoc" ]; then
|
|||||||
git push origin gh-pages
|
git push origin gh-pages
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# finally, merge master back onto develop
|
# finally, merge master back onto develop (if it exists)
|
||||||
|
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||||
git checkout develop
|
git checkout develop
|
||||||
git pull
|
git pull
|
||||||
git merge master
|
git merge master
|
||||||
git push origin develop
|
git push origin develop
|
||||||
|
fi
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ TestClient.prototype.expectKeyQuery = function(response) {
|
|||||||
200, (path, content) => {
|
200, (path, content) => {
|
||||||
Object.keys(response.device_keys).forEach((userId) => {
|
Object.keys(response.device_keys).forEach((userId) => {
|
||||||
expect(content.device_keys[userId]).toEqual(
|
expect(content.device_keys[userId]).toEqual(
|
||||||
{},
|
[],
|
||||||
"Expected key query for " + userId + ", got " +
|
"Expected key query for " + userId + ", got " +
|
||||||
Object.keys(content.device_keys),
|
Object.keys(content.device_keys),
|
||||||
);
|
);
|
||||||
|
|||||||
23
spec/browserify/setupTests.js
Normal file
23
spec/browserify/setupTests.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// stub for browser-matrix browserify tests
|
||||||
|
global.XMLHttpRequest = jest.fn();
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// clean up XMLHttpRequest mock
|
||||||
|
global.XMLHttpRequest = undefined;
|
||||||
|
});
|
||||||
103
spec/browserify/sync-browserify.spec.js
Normal file
103
spec/browserify/sync-browserify.spec.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// load XmlHttpRequest mock
|
||||||
|
import "./setupTests";
|
||||||
|
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||||
|
import {MockStorageApi} from "../MockStorageApi";
|
||||||
|
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
|
||||||
|
import MockHttpBackend from "matrix-mock-request";
|
||||||
|
import {LocalStorageCryptoStore} from "../../src/crypto/store/localStorage-crypto-store";
|
||||||
|
import * as utils from "../test-utils";
|
||||||
|
|
||||||
|
const USER_ID = "@user:test.server";
|
||||||
|
const DEVICE_ID = "device_id";
|
||||||
|
const ACCESS_TOKEN = "access_token";
|
||||||
|
const ROOM_ID = "!room_id:server.test";
|
||||||
|
|
||||||
|
/* global matrixcs */
|
||||||
|
|
||||||
|
describe("Browserify Test", function() {
|
||||||
|
let client;
|
||||||
|
let httpBackend;
|
||||||
|
|
||||||
|
async function createTestClient() {
|
||||||
|
const sessionStoreBackend = new MockStorageApi();
|
||||||
|
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||||
|
const httpBackend = new MockHttpBackend();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
baseUrl: "http://" + USER_ID + ".test.server",
|
||||||
|
userId: USER_ID,
|
||||||
|
accessToken: ACCESS_TOKEN,
|
||||||
|
deviceId: DEVICE_ID,
|
||||||
|
sessionStore: sessionStore,
|
||||||
|
request: httpBackend.requestFn,
|
||||||
|
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = matrixcs.createClient(options);
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||||
|
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||||
|
|
||||||
|
return { client, httpBackend };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({client, httpBackend} = await createTestClient());
|
||||||
|
await client.startClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
client.stopClient();
|
||||||
|
await httpBackend.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync", async function() {
|
||||||
|
const event = utils.mkMembership({
|
||||||
|
room: ROOM_ID,
|
||||||
|
mship: "join",
|
||||||
|
user: "@other_user:server.test",
|
||||||
|
name: "Displayname",
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncData = {
|
||||||
|
next_batch: "batch1",
|
||||||
|
rooms: {
|
||||||
|
join: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
syncData.rooms.join[ROOM_ID] = {
|
||||||
|
timeline: {
|
||||||
|
events: [
|
||||||
|
event,
|
||||||
|
],
|
||||||
|
limited: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||||
|
await Promise.race([
|
||||||
|
Promise.all([
|
||||||
|
httpBackend.flushAllExpected(),
|
||||||
|
]),
|
||||||
|
new Promise((_, reject) => {
|
||||||
|
client.once("sync.unexpectedError", reject);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
@@ -70,7 +70,7 @@ function expectAliQueryKeys() {
|
|||||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||||
.respond(200, function(path, content) {
|
.respond(200, function(path, content) {
|
||||||
expect(content.device_keys[bobUserId]).toEqual(
|
expect(content.device_keys[bobUserId]).toEqual(
|
||||||
{},
|
[],
|
||||||
"Expected Alice to key query for " + bobUserId + ", got " +
|
"Expected Alice to key query for " + bobUserId + ", got " +
|
||||||
Object.keys(content.device_keys),
|
Object.keys(content.device_keys),
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ function expectBobQueryKeys() {
|
|||||||
"POST", "/keys/query",
|
"POST", "/keys/query",
|
||||||
).respond(200, function(path, content) {
|
).respond(200, function(path, content) {
|
||||||
expect(content.device_keys[aliUserId]).toEqual(
|
expect(content.device_keys[aliUserId]).toEqual(
|
||||||
{},
|
[],
|
||||||
"Expected Bob to key query for " + aliUserId + ", got " +
|
"Expected Bob to key query for " + aliUserId + ", got " +
|
||||||
Object.keys(content.device_keys),
|
Object.keys(content.device_keys),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -347,8 +347,8 @@ describe("MatrixClient", function() {
|
|||||||
|
|
||||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||||
expect(req.data).toEqual({device_keys: {
|
expect(req.data).toEqual({device_keys: {
|
||||||
'boris': {},
|
'boris': [],
|
||||||
'chaz': {},
|
'chaz': [],
|
||||||
}});
|
}});
|
||||||
}).respond(200, {
|
}).respond(200, {
|
||||||
device_keys: {
|
device_keys: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import HttpBackend from "matrix-mock-request";
|
|||||||
import {MatrixClient} from "../../src/matrix";
|
import {MatrixClient} from "../../src/matrix";
|
||||||
import {MatrixScheduler} from "../../src/scheduler";
|
import {MatrixScheduler} from "../../src/scheduler";
|
||||||
import {MemoryStore} from "../../src/store/memory";
|
import {MemoryStore} from "../../src/store/memory";
|
||||||
|
import {MatrixError} from "../../src/http-api";
|
||||||
|
|
||||||
describe("MatrixClient opts", function() {
|
describe("MatrixClient opts", function() {
|
||||||
const baseUrl = "http://localhost.or.something";
|
const baseUrl = "http://localhost.or.something";
|
||||||
@@ -132,10 +133,10 @@ describe("MatrixClient opts", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shouldn't retry sending events", function(done) {
|
it("shouldn't retry sending events", function(done) {
|
||||||
httpBackend.when("PUT", "/txn1").fail(500, {
|
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
|
||||||
errcode: "M_SOMETHING",
|
errcode: "M_SOMETHING",
|
||||||
error: "Ruh roh",
|
error: "Ruh roh",
|
||||||
});
|
}));
|
||||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||||
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
|||||||
@@ -313,6 +313,10 @@ describe("Crypto", function() {
|
|||||||
// make a room key request, and record the transaction ID for the
|
// make a room key request, and record the transaction ID for the
|
||||||
// sendToDevice call
|
// sendToDevice call
|
||||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||||
|
// key requests get queued until the sync has finished, but we don't
|
||||||
|
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||||
|
// to force it to send now.
|
||||||
|
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ async function makeTestClient(userInfo, options) {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper around pkSign to return a signed object. pkSign returns the
|
||||||
|
// signature, rather than the signed object.
|
||||||
|
function sign(obj, key, userId) {
|
||||||
|
olmlib.pkSign(obj, key, userId);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
describe("Secrets", function() {
|
describe("Secrets", function() {
|
||||||
if (!global.Olm) {
|
if (!global.Olm) {
|
||||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||||
@@ -266,6 +273,26 @@ describe("Secrets", function() {
|
|||||||
expect(secret).toBe("bar");
|
expect(secret).toBe("bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("bootstrap", function() {
|
||||||
|
// keys used in some of the tests
|
||||||
|
const XSK = new Uint8Array(
|
||||||
|
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
|
||||||
|
);
|
||||||
|
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
|
||||||
|
const USK = new Uint8Array(
|
||||||
|
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
|
||||||
|
);
|
||||||
|
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
|
||||||
|
const SSK = new Uint8Array(
|
||||||
|
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
|
||||||
|
);
|
||||||
|
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
|
||||||
|
const SSSSKey = new Uint8Array(
|
||||||
|
olmlib.decodeBase64(
|
||||||
|
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
||||||
const key = new Uint8Array(16);
|
const key = new Uint8Array(16);
|
||||||
for (let i = 0; i < 16; i++) key[i] = i;
|
for (let i = 0; i < 16; i++) key[i] = i;
|
||||||
@@ -303,7 +330,8 @@ describe("Secrets", function() {
|
|||||||
const secretStorage = bob._crypto._secretStorage;
|
const secretStorage = bob._crypto._secretStorage;
|
||||||
|
|
||||||
expect(crossSigning.getId()).toBeTruthy();
|
expect(crossSigning.getId()).toBeTruthy();
|
||||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||||
|
.toBeTruthy();
|
||||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -363,7 +391,141 @@ describe("Secrets", function() {
|
|||||||
await bob.bootstrapSecretStorage();
|
await bob.bootstrapSecretStorage();
|
||||||
|
|
||||||
expect(crossSigning.getId()).toBeTruthy();
|
expect(crossSigning.getId()).toBeTruthy();
|
||||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||||
|
.toBeTruthy();
|
||||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds passphrase checking if it's lacking", async function() {
|
||||||
|
let crossSigningKeys = {
|
||||||
|
master: XSK,
|
||||||
|
user_signing: USK,
|
||||||
|
self_signing: SSK,
|
||||||
|
};
|
||||||
|
const secretStorageKeys = {
|
||||||
|
key_id: SSSSKey,
|
||||||
|
};
|
||||||
|
const alice = await makeTestClient(
|
||||||
|
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||||
|
{
|
||||||
|
cryptoCallbacks: {
|
||||||
|
getCrossSigningKey: t => crossSigningKeys[t],
|
||||||
|
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||||
|
getSecretStorageKey: ({keys}, name) => {
|
||||||
|
for (const keyId of Object.keys(keys)) {
|
||||||
|
if (secretStorageKeys[keyId]) {
|
||||||
|
return [keyId, secretStorageKeys[keyId]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
alice.store.storeAccountDataEvents([
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.secret_storage.default_key",
|
||||||
|
content: {
|
||||||
|
key: "key_id",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.secret_storage.key.key_id",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||||
|
passphrase: {
|
||||||
|
algorithm: "m.pbkdf2",
|
||||||
|
iterations: 500000,
|
||||||
|
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// we never use these values, other than checking that they
|
||||||
|
// exist, so just use dummy values
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.cross_signing.master",
|
||||||
|
content: {
|
||||||
|
encrypted: {
|
||||||
|
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.cross_signing.self_signing",
|
||||||
|
content: {
|
||||||
|
encrypted: {
|
||||||
|
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.cross_signing.user_signing",
|
||||||
|
content: {
|
||||||
|
encrypted: {
|
||||||
|
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||||
|
keys: {
|
||||||
|
master: {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["master"],
|
||||||
|
keys: {
|
||||||
|
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
self_signing: sign({
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["self_signing"],
|
||||||
|
keys: {
|
||||||
|
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||||
|
},
|
||||||
|
}, XSK, "@alice:example.com"),
|
||||||
|
user_signing: sign({
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["user_signing"],
|
||||||
|
keys: {
|
||||||
|
[`ed25519:${USPubKey}`]: USPubKey,
|
||||||
|
},
|
||||||
|
}, XSK, "@alice:example.com"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
alice.getKeyBackupVersion = async () => {
|
||||||
|
return {
|
||||||
|
version: "1",
|
||||||
|
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
|
auth_data: sign({
|
||||||
|
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||||
|
}, XSK, "@alice:example.com"),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
alice.setAccountData = async function(name, data) {
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: name,
|
||||||
|
content: data,
|
||||||
|
});
|
||||||
|
alice.store.storeAccountDataEvents([event]);
|
||||||
|
this.emit("accountData", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
await alice.bootstrapSecretStorage();
|
||||||
|
|
||||||
|
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||||
|
.toEqual({key: "key_id"});
|
||||||
|
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||||
|
.getContent();
|
||||||
|
expect(keyInfo.algorithm)
|
||||||
|
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||||
|
expect(keyInfo.passphrase).toEqual({
|
||||||
|
algorithm: "m.pbkdf2",
|
||||||
|
iterations: 500000,
|
||||||
|
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||||
|
});
|
||||||
|
expect(keyInfo).toHaveProperty("iv");
|
||||||
|
expect(keyInfo).toHaveProperty("mac");
|
||||||
|
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
|
||||||
|
.toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,7 +44,16 @@ describe("SAS verification", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should error on an unexpected event", async function() {
|
it("should error on an unexpected event", async function() {
|
||||||
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
|
//channel, baseApis, userId, deviceId, startEvent, request
|
||||||
|
const request = {
|
||||||
|
onVerifierCancelled: function() {},
|
||||||
|
};
|
||||||
|
const channel = {
|
||||||
|
send: function() {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
|
||||||
sas.handleEvent(new MatrixEvent({
|
sas.handleEvent(new MatrixEvent({
|
||||||
sender: "@alice:example.com",
|
sender: "@alice:example.com",
|
||||||
type: "es.inquisition",
|
type: "es.inquisition",
|
||||||
@@ -172,11 +181,14 @@ describe("SAS verification", function() {
|
|||||||
|
|
||||||
it("should verify a key", async () => {
|
it("should verify a key", async () => {
|
||||||
let macMethod;
|
let macMethod;
|
||||||
|
let keyAgreement;
|
||||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||||
bob.client.sendToDevice = function(type, map) {
|
bob.client.sendToDevice = function(type, map) {
|
||||||
if (type === "m.key.verification.accept") {
|
if (type === "m.key.verification.accept") {
|
||||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||||
.message_authentication_code;
|
.message_authentication_code;
|
||||||
|
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
|
||||||
|
.key_agreement_protocol;
|
||||||
}
|
}
|
||||||
return origSendToDevice(type, map);
|
return origSendToDevice(type, map);
|
||||||
};
|
};
|
||||||
@@ -203,6 +215,7 @@ describe("SAS verification", function() {
|
|||||||
|
|
||||||
// make sure that it uses the preferred method
|
// make sure that it uses the preferred method
|
||||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||||
|
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||||
|
|
||||||
// make sure Alice and Bob verified each other
|
// make sure Alice and Bob verified each other
|
||||||
const bobDevice
|
const bobDevice
|
||||||
|
|||||||
34
spec/unit/filter-component.spec.js
Normal file
34
spec/unit/filter-component.spec.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {FilterComponent} from "../../src/filter-component";
|
||||||
|
import {mkEvent} from '../test-utils';
|
||||||
|
|
||||||
|
describe("Filter Component", function() {
|
||||||
|
describe("types", function() {
|
||||||
|
it("should filter out events with other types", function() {
|
||||||
|
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||||
|
const event = mkEvent({
|
||||||
|
type: 'm.room.member',
|
||||||
|
content: { },
|
||||||
|
room: 'roomId',
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkResult = filter.check(event);
|
||||||
|
|
||||||
|
expect(checkResult).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should validate events with the same type", function() {
|
||||||
|
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||||
|
const event = mkEvent({
|
||||||
|
type: 'm.room.message',
|
||||||
|
content: { },
|
||||||
|
room: 'roomId',
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkResult = filter.check(event);
|
||||||
|
|
||||||
|
expect(checkResult).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -143,4 +143,33 @@ describe("InteractiveAuth", function() {
|
|||||||
expect(stateUpdated).toBeCalledTimes(1);
|
expect(stateUpdated).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should start an auth stage and reject if no auth flow", function() {
|
||||||
|
const doRequest = jest.fn();
|
||||||
|
const stateUpdated = jest.fn();
|
||||||
|
|
||||||
|
const ia = new InteractiveAuth({
|
||||||
|
matrixClient: new FakeClient(),
|
||||||
|
doRequest: doRequest,
|
||||||
|
stateUpdated: stateUpdated,
|
||||||
|
});
|
||||||
|
|
||||||
|
doRequest.mockImplementation(function(authData) {
|
||||||
|
logger.log("request1", authData);
|
||||||
|
expect(authData).toEqual({});
|
||||||
|
const err = new MatrixError({
|
||||||
|
session: "sessionId",
|
||||||
|
flows: [],
|
||||||
|
params: {
|
||||||
|
"logintype": { param: "aa" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
err.httpStatus = 401;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ia.attemptAuth().catch(function(error) {
|
||||||
|
expect(error.message).toBe('No appropriate authentication flow found');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
25
src/@types/global.d.ts
vendored
Normal file
25
src/@types/global.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface Global {
|
||||||
|
localStorage: Storage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1754,7 +1754,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
|
|||||||
content.token = opts.token;
|
content.token = opts.token;
|
||||||
}
|
}
|
||||||
userIds.forEach((u) => {
|
userIds.forEach((u) => {
|
||||||
content.device_keys[u] = {};
|
content.device_keys[u] = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
|
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
|
||||||
|
|||||||
150
src/client.js
150
src/client.js
@@ -35,7 +35,12 @@ import {StubStore} from "./store/stub";
|
|||||||
import {createNewMatrixCall} from "./webrtc/call";
|
import {createNewMatrixCall} from "./webrtc/call";
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import {sleep} from './utils';
|
import {sleep} from './utils';
|
||||||
import {MatrixError, PREFIX_MEDIA_R0, PREFIX_UNSTABLE} from "./http-api";
|
import {
|
||||||
|
MatrixError,
|
||||||
|
PREFIX_MEDIA_R0,
|
||||||
|
PREFIX_UNSTABLE,
|
||||||
|
retryNetworkOperation,
|
||||||
|
} from "./http-api";
|
||||||
import {getHttpUriForMxc} from "./content-repo";
|
import {getHttpUriForMxc} from "./content-repo";
|
||||||
import * as ContentHelpers from "./content-helpers";
|
import * as ContentHelpers from "./content-helpers";
|
||||||
import * as olmlib from "./crypto/olmlib";
|
import * as olmlib from "./crypto/olmlib";
|
||||||
@@ -48,6 +53,7 @@ import {keyFromAuthData} from './crypto/key_passphrase';
|
|||||||
import {randomString} from './randomstring';
|
import {randomString} from './randomstring';
|
||||||
import {PushProcessor} from "./pushprocessor";
|
import {PushProcessor} from "./pushprocessor";
|
||||||
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
|
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
|
||||||
|
import { User } from "./models/user";
|
||||||
|
|
||||||
const SCROLLBACK_DELAY_MS = 3000;
|
const SCROLLBACK_DELAY_MS = 3000;
|
||||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
export const CRYPTO_ENABLED = isCryptoAvailable();
|
||||||
@@ -731,6 +737,7 @@ MatrixClient.prototype.initCrypto = async function() {
|
|||||||
"crypto.roomKeyRequestCancellation",
|
"crypto.roomKeyRequestCancellation",
|
||||||
"crypto.warning",
|
"crypto.warning",
|
||||||
"crypto.devicesUpdated",
|
"crypto.devicesUpdated",
|
||||||
|
"crypto.willUpdateDevices",
|
||||||
"deviceVerificationChanged",
|
"deviceVerificationChanged",
|
||||||
"userTrustStatusChanged",
|
"userTrustStatusChanged",
|
||||||
"crossSigning.keysChanged",
|
"crossSigning.keysChanged",
|
||||||
@@ -819,9 +826,9 @@ MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
|
|||||||
*
|
*
|
||||||
* @param {string} userId the user to list keys for.
|
* @param {string} userId the user to list keys for.
|
||||||
*
|
*
|
||||||
* @return {Promise<module:crypto/deviceinfo[]>} list of devices
|
* @return {module:crypto/deviceinfo[]} list of devices
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
|
MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
|
||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
@@ -834,9 +841,9 @@ MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
|
|||||||
* @param {string} userId the user to list keys for.
|
* @param {string} userId the user to list keys for.
|
||||||
* @param {string} deviceId unique identifier for the device
|
* @param {string} deviceId unique identifier for the device
|
||||||
*
|
*
|
||||||
* @return {Promise<?module:crypto/deviceinfo>} device or null
|
* @return {module:crypto/deviceinfo} device or null
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
|
MatrixClient.prototype.getStoredDevice = function(userId, deviceId) {
|
||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
@@ -1303,7 +1310,6 @@ wrapCryptoFuncs(MatrixClient, [
|
|||||||
"bootstrapSecretStorage",
|
"bootstrapSecretStorage",
|
||||||
"addSecretStorageKey",
|
"addSecretStorageKey",
|
||||||
"hasSecretStorageKey",
|
"hasSecretStorageKey",
|
||||||
"secretStorageKeyNeedsUpgrade",
|
|
||||||
"storeSecret",
|
"storeSecret",
|
||||||
"getSecret",
|
"getSecret",
|
||||||
"isSecretStored",
|
"isSecretStored",
|
||||||
@@ -1357,7 +1363,8 @@ MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable end-to-end encryption for a room.
|
* Enable end-to-end encryption for a room. This does not modify room state.
|
||||||
|
* Any messages sent before the returned promise resolves will be sent unencrypted.
|
||||||
* @param {string} roomId The room ID to enable encryption in.
|
* @param {string} roomId The room ID to enable encryption in.
|
||||||
* @param {object} config The encryption config for the room.
|
* @param {object} config The encryption config for the room.
|
||||||
* @return {Promise} A promise that will resolve when encryption is set up.
|
* @return {Promise} A promise that will resolve when encryption is set up.
|
||||||
@@ -1429,15 +1436,17 @@ MatrixClient.prototype.exportRoomKeys = function() {
|
|||||||
* Import a list of room keys previously exported by exportRoomKeys
|
* Import a list of room keys previously exported by exportRoomKeys
|
||||||
*
|
*
|
||||||
* @param {Object[]} keys a list of session export objects
|
* @param {Object[]} keys a list of session export objects
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {Function} opts.progressCallback called with an object that has a "stage" param
|
||||||
*
|
*
|
||||||
* @return {Promise} a promise which resolves when the keys
|
* @return {Promise} a promise which resolves when the keys
|
||||||
* have been imported
|
* have been imported
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.importRoomKeys = function(keys) {
|
MatrixClient.prototype.importRoomKeys = function(keys, opts) {
|
||||||
if (!this._crypto) {
|
if (!this._crypto) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
return this._crypto.importRoomKeys(keys);
|
return this._crypto.importRoomKeys(keys, opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1497,12 +1506,16 @@ MatrixClient.prototype.isKeyBackupTrusted = function(info) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {bool} true if the client is configured to back up keys to
|
* @returns {bool} true if the client is configured to back up keys to
|
||||||
* the server, otherwise false.
|
* the server, otherwise false. If we haven't completed a successful check
|
||||||
|
* of key backup status yet, returns null.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getKeyBackupEnabled = function() {
|
MatrixClient.prototype.getKeyBackupEnabled = function() {
|
||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
|
if (!this._crypto._checkedForBackup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Boolean(this._crypto.backupKey);
|
return Boolean(this._crypto.backupKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1870,7 +1883,10 @@ MatrixClient.prototype.restoreKeyBackupWithCache = async function(
|
|||||||
|
|
||||||
MatrixClient.prototype._restoreKeyBackup = function(
|
MatrixClient.prototype._restoreKeyBackup = function(
|
||||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||||
{ cacheCompleteCallback }={}, // For sequencing during tests
|
{
|
||||||
|
cacheCompleteCallback, // For sequencing during tests
|
||||||
|
progressCallback,
|
||||||
|
}={},
|
||||||
) {
|
) {
|
||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
@@ -1905,6 +1921,12 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
|||||||
console.warn("Error caching session backup key:", e);
|
console.warn("Error caching session backup key:", e);
|
||||||
}).then(cacheCompleteCallback);
|
}).then(cacheCompleteCallback);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback({
|
||||||
|
stage: "fetch",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this._http.authedRequest(
|
return this._http.authedRequest(
|
||||||
undefined, "GET", path.path, path.queryData, undefined,
|
undefined, "GET", path.path, path.queryData, undefined,
|
||||||
{prefix: PREFIX_UNSTABLE},
|
{prefix: PREFIX_UNSTABLE},
|
||||||
@@ -1939,7 +1961,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.importRoomKeys(keys);
|
return this.importRoomKeys(keys, { progressCallback });
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -2075,6 +2097,7 @@ MatrixClient.prototype.getUsers = function() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set account data event for the current user.
|
* Set account data event for the current user.
|
||||||
|
* It will retry the request up to 5 times.
|
||||||
* @param {string} eventType The event type
|
* @param {string} eventType The event type
|
||||||
* @param {Object} contents the contents object for the event
|
* @param {Object} contents the contents object for the event
|
||||||
* @param {module:client.callback} callback Optional.
|
* @param {module:client.callback} callback Optional.
|
||||||
@@ -2086,9 +2109,13 @@ MatrixClient.prototype.setAccountData = function(eventType, contents, callback)
|
|||||||
$userId: this.credentials.userId,
|
$userId: this.credentials.userId,
|
||||||
$type: eventType,
|
$type: eventType,
|
||||||
});
|
});
|
||||||
return this._http.authedRequest(
|
const promise = retryNetworkOperation(5, () => {
|
||||||
callback, "PUT", path, undefined, contents,
|
return this._http.authedRequest(undefined, "PUT", path, undefined, contents);
|
||||||
);
|
});
|
||||||
|
if (callback) {
|
||||||
|
promise.then(result => callback(null, result), callback);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2123,9 +2150,17 @@ MatrixClient.prototype.getAccountDataFromServer = async function(eventType) {
|
|||||||
$userId: this.credentials.userId,
|
$userId: this.credentials.userId,
|
||||||
$type: eventType,
|
$type: eventType,
|
||||||
});
|
});
|
||||||
return this._http.authedRequest(
|
try {
|
||||||
|
const result = await this._http.authedRequest(
|
||||||
undefined, "GET", path, undefined,
|
undefined, "GET", path, undefined,
|
||||||
);
|
);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.data && e.data.errcode === 'M_NOT_FOUND') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2439,6 +2474,7 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
|
|||||||
const localEvent = new MatrixEvent(Object.assign(eventObject, {
|
const localEvent = new MatrixEvent(Object.assign(eventObject, {
|
||||||
event_id: "~" + roomId + ":" + txnId,
|
event_id: "~" + roomId + ":" + txnId,
|
||||||
user_id: this.credentials.userId,
|
user_id: this.credentials.userId,
|
||||||
|
sender: this.credentials.userId,
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
origin_server_ts: new Date().getTime(),
|
origin_server_ts: new Date().getTime(),
|
||||||
}));
|
}));
|
||||||
@@ -4499,16 +4535,16 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
|
|||||||
* @param {Filter} filter
|
* @param {Filter} filter
|
||||||
* @return {Promise<String>} Filter ID
|
* @return {Promise<String>} Filter ID
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
|
MatrixClient.prototype.getOrCreateFilter = async function(filterName, filter) {
|
||||||
const filterId = this.store.getFilterIdByName(filterName);
|
const filterId = this.store.getFilterIdByName(filterName);
|
||||||
let promise = Promise.resolve();
|
let existingId = undefined;
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (filterId) {
|
if (filterId) {
|
||||||
// check that the existing filter matches our expectations
|
// check that the existing filter matches our expectations
|
||||||
promise = self.getFilter(self.credentials.userId,
|
try {
|
||||||
filterId, true,
|
const existingFilter =
|
||||||
).then(function(existingFilter) {
|
await this.getFilter(this.credentials.userId, filterId, true);
|
||||||
|
if (existingFilter) {
|
||||||
const oldDef = existingFilter.getDefinition();
|
const oldDef = existingFilter.getDefinition();
|
||||||
const newDef = filter.getDefinition();
|
const newDef = filter.getDefinition();
|
||||||
|
|
||||||
@@ -4516,47 +4552,37 @@ MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
|
|||||||
// super, just use that.
|
// super, just use that.
|
||||||
// debuglog("Using existing filter ID %s: %s", filterId,
|
// debuglog("Using existing filter ID %s: %s", filterId,
|
||||||
// JSON.stringify(oldDef));
|
// JSON.stringify(oldDef));
|
||||||
return Promise.resolve(filterId);
|
existingId = filterId;
|
||||||
}
|
}
|
||||||
// debuglog("Existing filter ID %s: %s; new filter: %s",
|
}
|
||||||
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
|
} catch (error) {
|
||||||
self.store.setFilterIdByName(filterName, undefined);
|
|
||||||
return undefined;
|
|
||||||
}, function(error) {
|
|
||||||
// Synapse currently returns the following when the filter cannot be found:
|
// Synapse currently returns the following when the filter cannot be found:
|
||||||
// {
|
// {
|
||||||
// errcode: "M_UNKNOWN",
|
// errcode: "M_UNKNOWN",
|
||||||
// name: "M_UNKNOWN",
|
// name: "M_UNKNOWN",
|
||||||
// message: "No row found",
|
// message: "No row found",
|
||||||
// data: Object, httpStatus: 404
|
|
||||||
// }
|
// }
|
||||||
if (error.httpStatus === 404 &&
|
if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") {
|
||||||
(error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) {
|
|
||||||
// Clear existing filterId from localStorage
|
|
||||||
// if it no longer exists on the server
|
|
||||||
self.store.setFilterIdByName(filterName, undefined);
|
|
||||||
// Return a undefined value for existingId further down the promise chain
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
// if the filter doesn't exist anymore on the server, remove from store
|
||||||
|
if (!existingId) {
|
||||||
|
this.store.setFilterIdByName(filterName, undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(function(existingId) {
|
|
||||||
if (existingId) {
|
if (existingId) {
|
||||||
return existingId;
|
return existingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new filter
|
// create a new filter
|
||||||
return self.createFilter(filter.getDefinition(),
|
const createdFilter = await this.createFilter(filter.getDefinition());
|
||||||
).then(function(createdFilter) {
|
|
||||||
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
||||||
// JSON.stringify(createdFilter.getDefinition()));
|
// JSON.stringify(createdFilter.getDefinition()));
|
||||||
self.store.setFilterIdByName(filterName, createdFilter.filterId);
|
this.store.setFilterIdByName(filterName, createdFilter.filterId);
|
||||||
return createdFilter.filterId;
|
return createdFilter.filterId;
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -4734,6 +4760,13 @@ MatrixClient.prototype.startClient = async function(opts) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create our own user object artificially (instead of waiting for sync)
|
||||||
|
// so it's always available, even if the user is not in any rooms etc.
|
||||||
|
const userId = this.getUserId();
|
||||||
|
if (userId) {
|
||||||
|
this.store.storeUser(new User(userId));
|
||||||
|
}
|
||||||
|
|
||||||
if (this._crypto) {
|
if (this._crypto) {
|
||||||
this._crypto.uploadDeviceKeys();
|
this._crypto.uploadDeviceKeys();
|
||||||
this._crypto.start();
|
this._crypto.start();
|
||||||
@@ -5247,17 +5280,20 @@ function _resolve(callback, resolve, res) {
|
|||||||
resolve(res);
|
resolve(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _PojoToMatrixEventMapper(client) {
|
function _PojoToMatrixEventMapper(client, options) {
|
||||||
|
const preventReEmit = Boolean(options && options.preventReEmit);
|
||||||
function mapper(plainOldJsObject) {
|
function mapper(plainOldJsObject) {
|
||||||
const event = new MatrixEvent(plainOldJsObject);
|
const event = new MatrixEvent(plainOldJsObject);
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
|
if (!preventReEmit) {
|
||||||
client.reEmitter.reEmit(event, [
|
client.reEmitter.reEmit(event, [
|
||||||
"Event.decrypted",
|
"Event.decrypted",
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
event.attemptDecryption(client._crypto);
|
event.attemptDecryption(client._crypto);
|
||||||
}
|
}
|
||||||
const room = client.getRoom(event.getRoomId());
|
const room = client.getRoom(event.getRoomId());
|
||||||
if (room) {
|
if (room && !preventReEmit) {
|
||||||
room.reEmitter.reEmit(event, ["Event.replaced"]);
|
room.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
@@ -5266,10 +5302,12 @@ function _PojoToMatrixEventMapper(client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client
|
||||||
* @return {Function}
|
* @return {Function}
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getEventMapper = function() {
|
MatrixClient.prototype.getEventMapper = function(options = undefined) {
|
||||||
return _PojoToMatrixEventMapper(this);
|
return _PojoToMatrixEventMapper(this, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5555,6 +5593,22 @@ MatrixClient.prototype.generateClientSecret = function() {
|
|||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever the stored devices for a user have changed
|
||||||
|
* @event module:client~MatrixClient#"crypto.devicesUpdated"
|
||||||
|
* @param {String[]} users A list of user IDs that were updated
|
||||||
|
* @param {bool} initialFetch If true, the store was empty (apart
|
||||||
|
* from our own device) and has been seeded.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever the stored devices for a user will be updated
|
||||||
|
* @event module:client~MatrixClient#"crypto.willUpdateDevices"
|
||||||
|
* @param {String[]} users A list of user IDs that will be updated
|
||||||
|
* @param {bool} initialFetch If true, the store is empty (apart
|
||||||
|
* from our own device) and is being seeded.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
|
* Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
|
||||||
* @event module:client~MatrixClient#"crypto.keyBackupStatus"
|
* @event module:client~MatrixClient#"crypto.keyBackupStatus"
|
||||||
|
|||||||
@@ -310,6 +310,13 @@ export class CrossSigningInfo extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unsets the keys, used when another session has reset the keys, to disable cross-signing
|
||||||
|
*/
|
||||||
|
clearKeys() {
|
||||||
|
this.keys = {};
|
||||||
|
}
|
||||||
|
|
||||||
setKeys(keys) {
|
setKeys(keys) {
|
||||||
const signingKeys = {};
|
const signingKeys = {};
|
||||||
if (keys.master) {
|
if (keys.master) {
|
||||||
@@ -644,6 +651,11 @@ export function createCryptoStoreCacheCallbacks(store) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
storeCrossSigningKeyCache: function(type, key) {
|
storeCrossSigningKeyCache: function(type, key) {
|
||||||
|
if (!(key instanceof Uint8Array)) {
|
||||||
|
throw new Error(
|
||||||
|
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return store.doTxn(
|
return store.doTxn(
|
||||||
'readwrite',
|
'readwrite',
|
||||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ export class DeviceList extends EventEmitter {
|
|||||||
this._savePromiseTime = null;
|
this._savePromiseTime = null;
|
||||||
// The timer used to delay the save
|
// The timer used to delay the save
|
||||||
this._saveTimer = null;
|
this._saveTimer = null;
|
||||||
|
// True if we have fetched data from the server or loaded a non-empty
|
||||||
|
// set of device data from the store
|
||||||
|
this._hasFetched = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +121,7 @@ export class DeviceList extends EventEmitter {
|
|||||||
await this._cryptoStore.doTxn(
|
await this._cryptoStore.doTxn(
|
||||||
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||||
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||||
|
this._hasFetched = Boolean(deviceData && deviceData.devices);
|
||||||
this._devices = deviceData ? deviceData.devices : {},
|
this._devices = deviceData ? deviceData.devices : {},
|
||||||
this._crossSigningInfo = deviceData ?
|
this._crossSigningInfo = deviceData ?
|
||||||
deviceData.crossSigningInfo || {} : {};
|
deviceData.crossSigningInfo || {} : {};
|
||||||
@@ -652,6 +656,7 @@ export class DeviceList extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const finished = (success) => {
|
const finished = (success) => {
|
||||||
|
this.emit("crypto.willUpdateDevices", users, !this._hasFetched);
|
||||||
users.forEach((u) => {
|
users.forEach((u) => {
|
||||||
this._dirty = true;
|
this._dirty = true;
|
||||||
|
|
||||||
@@ -677,7 +682,8 @@ export class DeviceList extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.saveIfDirty();
|
this.saveIfDirty();
|
||||||
this.emit("crypto.devicesUpdated", users);
|
this.emit("crypto.devicesUpdated", users, !this._hasFetched);
|
||||||
|
this._hasFetched = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
return prom;
|
return prom;
|
||||||
|
|||||||
@@ -36,9 +36,15 @@ function checkPayloadLength(payloadString) {
|
|||||||
// Note that even if we manage to do the encryption, the message send may fail,
|
// Note that even if we manage to do the encryption, the message send may fail,
|
||||||
// because by the time we've wrapped the ciphertext in the event object, it may
|
// because by the time we've wrapped the ciphertext in the event object, it may
|
||||||
// exceed 65K. But at least we won't just fail with "abort()" in that case.
|
// exceed 65K. But at least we won't just fail with "abort()" in that case.
|
||||||
throw new Error("Message too long (" + payloadString.length + " bytes). " +
|
const err = new Error("Message too long (" + payloadString.length + " bytes). " +
|
||||||
"The maximum for an encrypted message is " +
|
"The maximum for an encrypted message is " +
|
||||||
MAX_PLAINTEXT_LENGTH + " bytes.");
|
MAX_PLAINTEXT_LENGTH + " bytes.");
|
||||||
|
// TODO: [TypeScript] We should have our own error types
|
||||||
|
err.data = {
|
||||||
|
errcode: "M_TOO_LARGE",
|
||||||
|
error: "Payload too large for encrypted message",
|
||||||
|
};
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +111,9 @@ export function OlmDevice(cryptoStore) {
|
|||||||
// Keep track of sessions that we're starting, so that we don't start
|
// Keep track of sessions that we're starting, so that we don't start
|
||||||
// multiple sessions for the same device at the same time.
|
// multiple sessions for the same device at the same time.
|
||||||
this._sessionsInProgress = {};
|
this._sessionsInProgress = {};
|
||||||
|
|
||||||
|
// Used by olm to serialise prekey message decryptions
|
||||||
|
this._olmPrekeyPromise = Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1029,6 +1038,11 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Storing megolm session " + senderKey + "/" + sessionId +
|
||||||
|
" with first index " + session.first_known_index(),
|
||||||
|
);
|
||||||
|
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
session: session.pickle(this._pickleKey),
|
session: session.pickle(this._pickleKey),
|
||||||
|
|||||||
@@ -97,10 +97,6 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
this._clientRunning = true;
|
this._clientRunning = true;
|
||||||
|
|
||||||
// set the timer going, to handle any requests which didn't get sent
|
|
||||||
// on the previous run of the client.
|
|
||||||
this._startTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +109,14 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send off a room key request, if we haven't already done so.
|
* Send any requests that have been queued
|
||||||
|
*/
|
||||||
|
sendQueuedRequests() {
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue up a room key request, if we haven't already queued or sent one.
|
||||||
*
|
*
|
||||||
* The `requestBody` is compared (with a deep-equality check) against
|
* The `requestBody` is compared (with a deep-equality check) against
|
||||||
* previous queued or sent requests and if it matches, no change is made.
|
* previous queued or sent requests and if it matches, no change is made.
|
||||||
@@ -129,7 +132,7 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
* pending list (or we have established that a similar request already
|
* pending list (or we have established that a similar request already
|
||||||
* exists)
|
* exists)
|
||||||
*/
|
*/
|
||||||
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
|
async queueRoomKeyRequest(requestBody, recipients, resend=false) {
|
||||||
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
|
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
|
||||||
requestBody,
|
requestBody,
|
||||||
);
|
);
|
||||||
@@ -184,7 +187,7 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
||||||
// raced with another tab to mark the request cancelled.
|
// raced with another tab to mark the request cancelled.
|
||||||
// Try again, to make sure the request is resent.
|
// Try again, to make sure the request is resent.
|
||||||
return await this.sendRoomKeyRequest(
|
return await this.queueRoomKeyRequest(
|
||||||
requestBody, recipients, resend,
|
requestBody, recipients, resend,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -220,9 +223,6 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
throw new Error('unhandled state: ' + req.state);
|
throw new Error('unhandled state: ' + req.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// some of the branches require the timer to be started. Just start it
|
|
||||||
// all the time, because it doesn't hurt to start it.
|
|
||||||
this._startTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -332,14 +332,14 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
* This is intended for situations where something substantial has changed, and we
|
* This is intended for situations where something substantial has changed, and we
|
||||||
* don't really expect the other end to even care about the cancellation.
|
* don't really expect the other end to even care about the cancellation.
|
||||||
* For example, after initialization or self-verification.
|
* For example, after initialization or self-verification.
|
||||||
* @return {Promise} An array of `sendRoomKeyRequest` outputs.
|
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
|
||||||
*/
|
*/
|
||||||
async cancelAndResendAllOutgoingRequests() {
|
async cancelAndResendAllOutgoingRequests() {
|
||||||
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
|
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
|
||||||
ROOM_KEY_REQUEST_STATES.SENT,
|
ROOM_KEY_REQUEST_STATES.SENT,
|
||||||
);
|
);
|
||||||
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
|
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
|
||||||
this.sendRoomKeyRequest(requestBody, recipients, true)));
|
this.queueRoomKeyRequest(requestBody, recipients, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the background timer to send queued requests, if the timer isn't
|
// start the background timer to send queued requests, if the timer isn't
|
||||||
@@ -381,15 +381,12 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("Looking for queued outgoing room key requests");
|
|
||||||
|
|
||||||
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
|
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
|
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||||
ROOM_KEY_REQUEST_STATES.UNSENT,
|
ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||||
]).then((req) => {
|
]).then((req) => {
|
||||||
if (!req) {
|
if (!req) {
|
||||||
logger.log("No more outgoing room key requests");
|
|
||||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -413,7 +410,6 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
logger.error("Error sending room key request; will retry later.", e);
|
logger.error("Error sending room key request; will retry later.", e);
|
||||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||||
this._startTimer();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,12 @@ limitations under the License.
|
|||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import {logger} from '../logger';
|
import {logger} from '../logger';
|
||||||
import * as olmlib from './olmlib';
|
import * as olmlib from './olmlib';
|
||||||
import {pkVerify} from './olmlib';
|
|
||||||
import {randomString} from '../randomstring';
|
import {randomString} from '../randomstring';
|
||||||
import {encryptAES, decryptAES} from './aes';
|
import {encryptAES, decryptAES} from './aes';
|
||||||
|
import {encodeBase64} from "./olmlib";
|
||||||
|
|
||||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||||
// don't use curve25519 for writing data.
|
|
||||||
export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519
|
|
||||||
= "m.secret_storage.v1.curve25519-aes-sha2";
|
|
||||||
|
|
||||||
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||||
|
|
||||||
@@ -34,11 +31,10 @@ const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
|
|||||||
* @module crypto/SecretStorage
|
* @module crypto/SecretStorage
|
||||||
*/
|
*/
|
||||||
export class SecretStorage extends EventEmitter {
|
export class SecretStorage extends EventEmitter {
|
||||||
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
|
constructor(baseApis, cryptoCallbacks) {
|
||||||
super();
|
super();
|
||||||
this._baseApis = baseApis;
|
this._baseApis = baseApis;
|
||||||
this._cryptoCallbacks = cryptoCallbacks;
|
this._cryptoCallbacks = cryptoCallbacks;
|
||||||
this._crossSigningInfo = crossSigningInfo;
|
|
||||||
this._requests = {};
|
this._requests = {};
|
||||||
this._incomingRequests = {};
|
this._incomingRequests = {};
|
||||||
}
|
}
|
||||||
@@ -52,7 +48,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDefaultKeyId(keyId) {
|
setDefaultKeyId(keyId) {
|
||||||
return new Promise((resolve) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const listener = (ev) => {
|
const listener = (ev) => {
|
||||||
if (
|
if (
|
||||||
ev.getType() === 'm.secret_storage.default_key' &&
|
ev.getType() === 'm.secret_storage.default_key' &&
|
||||||
@@ -64,10 +60,15 @@ export class SecretStorage extends EventEmitter {
|
|||||||
};
|
};
|
||||||
this._baseApis.on('accountData', listener);
|
this._baseApis.on('accountData', listener);
|
||||||
|
|
||||||
this._baseApis.setAccountData(
|
try {
|
||||||
|
await this._baseApis.setAccountData(
|
||||||
'm.secret_storage.default_key',
|
'm.secret_storage.default_key',
|
||||||
{ key: keyId },
|
{ key: keyId },
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this._baseApis.removeListener('accountData', listener);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,20 +92,16 @@ export class SecretStorage extends EventEmitter {
|
|||||||
keyData.name = opts.name;
|
keyData.name = opts.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (algorithm) {
|
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
|
||||||
{
|
|
||||||
if (opts.passphrase) {
|
if (opts.passphrase) {
|
||||||
keyData.passphrase = opts.passphrase;
|
keyData.passphrase = opts.passphrase;
|
||||||
}
|
}
|
||||||
if (opts.key) {
|
if (opts.key) {
|
||||||
const {iv, mac} = await encryptAES(ZERO_STR, opts.key, "");
|
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
|
||||||
keyData.iv = iv;
|
keyData.iv = iv;
|
||||||
keyData.mac = mac;
|
keyData.mac = mac;
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +115,6 @@ export class SecretStorage extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._crossSigningInfo.signObject(keyData, 'master');
|
|
||||||
|
|
||||||
await this._baseApis.setAccountData(
|
await this._baseApis.setAccountData(
|
||||||
`m.secret_storage.key.${keyId}`, keyData,
|
`m.secret_storage.key.${keyId}`, keyData,
|
||||||
);
|
);
|
||||||
@@ -127,33 +122,6 @@ export class SecretStorage extends EventEmitter {
|
|||||||
return keyId;
|
return keyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs a given secret storage key with the cross-signing master key.
|
|
||||||
*
|
|
||||||
* @param {string} [keyId = default key's ID] The ID of the key to sign.
|
|
||||||
* Defaults to the default key ID if not provided.
|
|
||||||
*/
|
|
||||||
async signKey(keyId) {
|
|
||||||
if (!keyId) {
|
|
||||||
keyId = await this.getDefaultKeyId();
|
|
||||||
}
|
|
||||||
if (!keyId) {
|
|
||||||
throw new Error("signKey requires a key ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
|
||||||
`m.secret_storage.key.${keyId}`,
|
|
||||||
);
|
|
||||||
if (!keyInfo) {
|
|
||||||
throw new Error(`Key ${keyId} does not exist in account data`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._crossSigningInfo.signObject(keyInfo, 'master');
|
|
||||||
await this._baseApis.setAccountData(
|
|
||||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the key information for a given ID.
|
* Get the key information for a given ID.
|
||||||
*
|
*
|
||||||
@@ -187,15 +155,6 @@ export class SecretStorage extends EventEmitter {
|
|||||||
return !!(await this.getKey(keyId));
|
return !!(await this.getKey(keyId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async keyNeedsUpgrade(keyId) {
|
|
||||||
const keyInfo = await this.getKey(keyId);
|
|
||||||
if (keyInfo && keyInfo[1].algorithm === SECRET_STORAGE_ALGORITHM_V1_CURVE25519) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a key matches what we expect based on the key info
|
* Check whether a key matches what we expect based on the key info
|
||||||
*
|
*
|
||||||
@@ -205,36 +164,23 @@ export class SecretStorage extends EventEmitter {
|
|||||||
* @return {boolean} whether or not the key matches
|
* @return {boolean} whether or not the key matches
|
||||||
*/
|
*/
|
||||||
async checkKey(key, info) {
|
async checkKey(key, info) {
|
||||||
switch (info.algorithm) {
|
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
|
||||||
{
|
|
||||||
if (info.mac) {
|
if (info.mac) {
|
||||||
const {mac} = await encryptAES(ZERO_STR, key, "", info.iv);
|
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||||
return info.mac === mac;
|
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
|
||||||
} else {
|
} else {
|
||||||
// if we have no information, we have to assume the key is right
|
// if we have no information, we have to assume the key is right
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
|
||||||
{
|
|
||||||
let decryption = null;
|
|
||||||
try {
|
|
||||||
decryption = new global.Olm.PkDecryption();
|
|
||||||
const gotPubkey = decryption.init_with_private_key(key);
|
|
||||||
// make sure it agrees with the given pubkey
|
|
||||||
return gotPubkey === info.pubkey;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
if (decryption) decryption.free();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown algorithm");
|
throw new Error("Unknown algorithm");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async _calculateKeyCheck(key, iv) {
|
||||||
|
return await encryptAES(ZERO_STR, key, "", iv);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store an encrypted secret on the server
|
* Store an encrypted secret on the server
|
||||||
*
|
*
|
||||||
@@ -268,15 +214,11 @@ export class SecretStorage extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encrypt secret, based on the algorithm
|
// encrypt secret, based on the algorithm
|
||||||
switch (keyInfo.algorithm) {
|
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
|
||||||
{
|
|
||||||
const keys = {[keyId]: keyInfo};
|
const keys = {[keyId]: keyInfo};
|
||||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||||
encrypted[keyId] = await encryption.encrypt(secret);
|
encrypted[keyId] = await encryption.encrypt(secret);
|
||||||
break;
|
} else {
|
||||||
}
|
|
||||||
default:
|
|
||||||
logger.warn("unknown algorithm for secret storage key " + keyId
|
logger.warn("unknown algorithm for secret storage key " + keyId
|
||||||
+ ": " + keyInfo.algorithm);
|
+ ": " + keyInfo.algorithm);
|
||||||
// do nothing if we don't understand the encryption algorithm
|
// do nothing if we don't understand the encryption algorithm
|
||||||
@@ -342,24 +284,11 @@ export class SecretStorage extends EventEmitter {
|
|||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
);
|
||||||
const encInfo = secretInfo.encrypted[keyId];
|
const encInfo = secretInfo.encrypted[keyId];
|
||||||
switch (keyInfo.algorithm) {
|
// only use keys we understand the encryption algorithm of
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||||
keys[keyId] = keyInfo;
|
keys[keyId] = keyInfo;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
|
||||||
if (
|
|
||||||
keyInfo.pubkey && (
|
|
||||||
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
|
|
||||||
encInfo.passthrough
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
keys[keyId] = keyInfo;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// do nothing if we don't understand the encryption algorithm
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +301,9 @@ export class SecretStorage extends EventEmitter {
|
|||||||
const encInfo = secretInfo.encrypted[keyId];
|
const encInfo = secretInfo.encrypted[keyId];
|
||||||
|
|
||||||
// We don't actually need the decryption object if it's a passthrough
|
// We don't actually need the decryption object if it's a passthrough
|
||||||
// since we just want to return the key itself.
|
// since we just want to return the key itself. It must be base64
|
||||||
if (encInfo.passthrough) return decryption.get_private_key();
|
// encoded, since this is how a key would normally be stored.
|
||||||
|
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
|
||||||
|
|
||||||
return await decryption.decrypt(encInfo);
|
return await decryption.decrypt(encInfo);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -407,8 +337,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
|
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
// check if secret is encrypted by a known/trusted secret and
|
// filter secret encryption keys with supported algorithm
|
||||||
// encryption looks sane
|
|
||||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||||
// get key information from key storage
|
// get key information from key storage
|
||||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||||
@@ -417,49 +346,11 @@ export class SecretStorage extends EventEmitter {
|
|||||||
if (!keyInfo) continue;
|
if (!keyInfo) continue;
|
||||||
const encInfo = secretInfo.encrypted[keyId];
|
const encInfo = secretInfo.encrypted[keyId];
|
||||||
|
|
||||||
// We don't actually need the decryption object if it's a passthrough
|
// only use keys we understand the encryption algorithm of
|
||||||
// since we just want to return the key itself.
|
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
if (encInfo.passthrough) {
|
|
||||||
try {
|
|
||||||
pkVerify(
|
|
||||||
keyInfo,
|
|
||||||
this._crossSigningInfo.getId('master'),
|
|
||||||
this._crossSigningInfo.userId,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// not trusted, so move on to the next key
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ret[keyId] = keyInfo;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (keyInfo.algorithm) {
|
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
|
||||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||||
ret[keyId] = keyInfo;
|
ret[keyId] = keyInfo;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
|
||||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
|
||||||
&& encInfo.ephemeral) {
|
|
||||||
if (checkKey) {
|
|
||||||
try {
|
|
||||||
pkVerify(
|
|
||||||
keyInfo,
|
|
||||||
this._crossSigningInfo.getId('master'),
|
|
||||||
this._crossSigningInfo.userId,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// not trusted, so move on to the next key
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret[keyId] = keyInfo;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// do nothing if we don't understand the encryption algorithm
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.keys(ret).length ? ret : null;
|
return Object.keys(ret).length ? ret : null;
|
||||||
@@ -586,7 +477,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
this._baseApis,
|
this._baseApis,
|
||||||
{
|
{
|
||||||
[sender]: [
|
[sender]: [
|
||||||
await this._baseApis.getStoredDevice(sender, deviceId),
|
this._baseApis.getStoredDevice(sender, deviceId),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -596,7 +487,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
this._baseApis.deviceId,
|
this._baseApis.deviceId,
|
||||||
this._baseApis._crypto._olmDevice,
|
this._baseApis._crypto._olmDevice,
|
||||||
sender,
|
sender,
|
||||||
this._baseApis._crypto.getStoredDevice(sender, deviceId),
|
this._baseApis.getStoredDevice(sender, deviceId),
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
const contentMap = {
|
const contentMap = {
|
||||||
@@ -663,9 +554,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
throw new Error("App returned unknown key from getSecretStorageKey!");
|
throw new Error("App returned unknown key from getSecretStorageKey!");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keys[keyId].algorithm) {
|
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
|
||||||
{
|
|
||||||
const decryption = {
|
const decryption = {
|
||||||
encrypt: async function(secret) {
|
encrypt: async function(secret) {
|
||||||
return await encryptAES(secret, privateKey, name);
|
return await encryptAES(secret, privateKey, name);
|
||||||
@@ -675,36 +564,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
return [keyId, decryption];
|
return [keyId, decryption];
|
||||||
}
|
} else {
|
||||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
|
||||||
{
|
|
||||||
const pkDecryption = new global.Olm.PkDecryption();
|
|
||||||
let pubkey;
|
|
||||||
try {
|
|
||||||
pubkey = pkDecryption.init_with_private_key(privateKey);
|
|
||||||
} catch (e) {
|
|
||||||
pkDecryption.free();
|
|
||||||
throw new Error("getSecretStorageKey callback returned invalid key");
|
|
||||||
}
|
|
||||||
if (pubkey !== keys[keyId].pubkey) {
|
|
||||||
pkDecryption.free();
|
|
||||||
throw new Error(
|
|
||||||
"getSecretStorageKey callback returned incorrect key",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const decryption = {
|
|
||||||
free: pkDecryption.free.bind(pkDecryption),
|
|
||||||
decrypt: async function(encInfo) {
|
|
||||||
return pkDecryption.decrypt(
|
|
||||||
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// needed for passthrough
|
|
||||||
get_private_key: pkDecryption.get_private_key.bind(pkDecryption),
|
|
||||||
};
|
|
||||||
return [keyId, decryption];
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ async function decryptNode(data, key, name) {
|
|||||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||||
|
|
||||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||||
.update(data.ciphertext, "base64").digest("base64");
|
.update(data.ciphertext, "base64").digest("base64").replace(/=+$/g, '');
|
||||||
|
|
||||||
if (hmac !== data.mac) {
|
if (hmac !== data.mac.replace(/=+$/g, '')) {
|
||||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this._shareKeyWithDevices(
|
await this._shareKeyWithDevices(
|
||||||
session, key, payload, retryDevices, failedDevices,
|
session, key, payload, retryDevices, failedDevices, 30000,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this._notifyFailedOlmDevices(session, key, failedDevices);
|
await this._notifyFailedOlmDevices(session, key, failedDevices);
|
||||||
@@ -521,6 +521,33 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
return Promise.all(promises).then(() => {
|
||||||
|
// prune out any devices that encryptMessageForDevice could not encrypt for,
|
||||||
|
// in which case it will have just not added anything to the ciphertext object.
|
||||||
|
// There's no point sending messages to devices if we couldn't encrypt to them,
|
||||||
|
// since that's effectively a blank message.
|
||||||
|
for (const userId of Object.keys(contentMap)) {
|
||||||
|
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||||
|
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
|
||||||
|
logger.log(
|
||||||
|
"No ciphertext for device " +
|
||||||
|
userId + ":" + deviceId + ": pruning",
|
||||||
|
);
|
||||||
|
delete contentMap[userId][deviceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No devices left for that user? Strip that too.
|
||||||
|
if (Object.keys(contentMap[userId]).length === 0) {
|
||||||
|
logger.log("Pruned all devices for user " + userId);
|
||||||
|
delete contentMap[userId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is there anything left?
|
||||||
|
if (Object.keys(contentMap).length === 0) {
|
||||||
|
logger.log("No users left to send to: aborting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => {
|
return this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => {
|
||||||
// store that we successfully uploaded the keys of the current slice
|
// store that we successfully uploaded the keys of the current slice
|
||||||
for (const userId of Object.keys(contentMap)) {
|
for (const userId of Object.keys(contentMap)) {
|
||||||
@@ -1296,7 +1323,6 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
|||||||
keysClaimed = event.getKeysClaimed();
|
keysClaimed = event.getKeysClaimed();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`Received and adding key for megolm session ${senderKey}|${sessionId}`);
|
|
||||||
return this._olmDevice.addInboundGroupSession(
|
return this._olmDevice.addInboundGroupSession(
|
||||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||||
content.session_key, keysClaimed,
|
content.session_key, keysClaimed,
|
||||||
@@ -1555,7 +1581,8 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Have another go at decrypting events after we receive a key
|
* Have another go at decrypting events after we receive a key. Resolves once
|
||||||
|
* decryption has been re-attempted on all events.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {String} senderKey
|
* @param {String} senderKey
|
||||||
@@ -1574,21 +1601,17 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pending.delete(sessionId);
|
logger.debug("Retrying decryption on events", [...pending]);
|
||||||
if (pending.size === 0) {
|
|
||||||
this._pendingEvents[senderKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([...pending].map(async (ev) => {
|
await Promise.all([...pending].map(async (ev) => {
|
||||||
try {
|
try {
|
||||||
await ev.attemptDecryption(this._crypto);
|
await ev.attemptDecryption(this._crypto, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't die if something goes wrong
|
// don't die if something goes wrong
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ev.attemptDecryption will re-add to this._pendingEvents if an event
|
// If decrypted successfully, they'll have been removed from _pendingEvents
|
||||||
// couldn't be decrypted
|
|
||||||
return !((this._pendingEvents[senderKey] || {})[sessionId]);
|
return !((this._pendingEvents[senderKey] || {})[sessionId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,25 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
|||||||
*/
|
*/
|
||||||
OlmDecryption.prototype._decryptMessage = async function(
|
OlmDecryption.prototype._decryptMessage = async function(
|
||||||
theirDeviceIdentityKey, message,
|
theirDeviceIdentityKey, message,
|
||||||
|
) {
|
||||||
|
// This is a wrapper that serialises decryptions of prekey messages, because
|
||||||
|
// otherwise we race between deciding we have no active sessions for the message
|
||||||
|
// and creating a new one, which we can only do once because it removes the OTK.
|
||||||
|
if (message.type !== 0) {
|
||||||
|
// not a prekey message: we can safely just try & decrypt it
|
||||||
|
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||||
|
} else {
|
||||||
|
const myPromise = this._olmDevice._olmPrekeyPromise.then(() => {
|
||||||
|
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||||
|
});
|
||||||
|
// we want the error, but don't propagate it to the next decryption
|
||||||
|
this._olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
|
||||||
|
return await myPromise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
OlmDecryption.prototype._reallyDecryptMessage = async function(
|
||||||
|
theirDeviceIdentityKey, message,
|
||||||
) {
|
) {
|
||||||
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
|
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
|
||||||
theirDeviceIdentityKey,
|
theirDeviceIdentityKey,
|
||||||
|
|||||||
@@ -169,7 +169,9 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
this._deviceList.on(
|
this._deviceList.on(
|
||||||
'userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated,
|
'userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated,
|
||||||
);
|
);
|
||||||
this._reEmitter.reEmit(this._deviceList, ["crypto.devicesUpdated"]);
|
this._reEmitter.reEmit(this._deviceList, [
|
||||||
|
"crypto.devicesUpdated", "crypto.willUpdateDevices",
|
||||||
|
]);
|
||||||
|
|
||||||
// the last time we did a check for the number of one-time-keys on the
|
// the last time we did a check for the number of one-time-keys on the
|
||||||
// server.
|
// server.
|
||||||
@@ -223,6 +225,11 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
this._toDeviceVerificationRequests = new ToDeviceRequests();
|
this._toDeviceVerificationRequests = new ToDeviceRequests();
|
||||||
this._inRoomVerificationRequests = new InRoomRequests();
|
this._inRoomVerificationRequests = new InRoomRequests();
|
||||||
|
|
||||||
|
// This flag will be unset whilst the client processes a sync response
|
||||||
|
// so that we don't start requesting keys until we've actually finished
|
||||||
|
// processing the response.
|
||||||
|
this._sendKeyRequestsImmediately = false;
|
||||||
|
|
||||||
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
|
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
|
||||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore);
|
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore);
|
||||||
|
|
||||||
@@ -233,7 +240,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
);
|
);
|
||||||
|
|
||||||
this._secretStorage = new SecretStorage(
|
this._secretStorage = new SecretStorage(
|
||||||
baseApis, cryptoCallbacks, this._crossSigningInfo,
|
baseApis, cryptoCallbacks,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||||
@@ -306,7 +313,8 @@ Crypto.prototype.init = async function(opts) {
|
|||||||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
(txn) => {
|
(txn) => {
|
||||||
this._cryptoStore.getCrossSigningKeys(txn, (keys) => {
|
this._cryptoStore.getCrossSigningKeys(txn, (keys) => {
|
||||||
if (keys) {
|
// can be an empty object after resetting cross-signing keys, see _storeTrustedSelfKeys
|
||||||
|
if (keys && Object.keys(keys).length !== 0) {
|
||||||
logger.log("Loaded cross-signing public keys from crypto store");
|
logger.log("Loaded cross-signing public keys from crypto store");
|
||||||
this._crossSigningInfo.setKeys(keys);
|
this._crossSigningInfo.setKeys(keys);
|
||||||
}
|
}
|
||||||
@@ -432,6 +440,14 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
|||||||
* up, then no changes are made, so this is safe to run to ensure secret storage
|
* up, then no changes are made, so this is safe to run to ensure secret storage
|
||||||
* is ready for use.
|
* 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
|
||||||
|
*
|
||||||
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
|
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
|
||||||
* called to await an interactive auth flow when uploading device signing keys.
|
* called to await an interactive auth flow when uploading device signing keys.
|
||||||
* Args:
|
* Args:
|
||||||
@@ -501,73 +517,27 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
// create a new SSSS key and set it as default
|
||||||
const decryptionKeys =
|
const createSSSS = async (opts, privateKey) => {
|
||||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
opts = opts || {};
|
||||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
if (privateKey) {
|
||||||
if (decryptionKeys && !(Object.values(decryptionKeys).some(
|
opts.key = privateKey;
|
||||||
info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES,
|
|
||||||
))) {
|
|
||||||
// we already have cross-signing keys, but they're encrypted using
|
|
||||||
// the old algorithm
|
|
||||||
logger.log("Switching to symmetric");
|
|
||||||
const keys = {};
|
|
||||||
// fetch the cross-signing private keys (needed to sign the new
|
|
||||||
// SSSS key). We store the cross-signing keys, and temporarily set
|
|
||||||
// a callback so that when the private key is needed while setting
|
|
||||||
// things up, we can provide it.
|
|
||||||
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
|
||||||
name => crossSigningPrivateKeys[name];
|
|
||||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
|
||||||
const secretName = `m.cross_signing.${type}`;
|
|
||||||
const secret = await this.getSecret(secretName);
|
|
||||||
keys[type] = secret;
|
|
||||||
crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret);
|
|
||||||
}
|
}
|
||||||
await this.checkOwnCrossSigningTrust();
|
|
||||||
const opts = {};
|
const keyId = await this.addSecretStorageKey(
|
||||||
let oldKeyId = null;
|
|
||||||
for (const [keyId, keyInfo] of Object.entries(decryptionKeys)) {
|
|
||||||
// See if the old key was generated from a passphrase. If
|
|
||||||
// yes, use the same settings.
|
|
||||||
if (keyId in ssssKeys) {
|
|
||||||
oldKeyId = keyId;
|
|
||||||
if (keyInfo.passphrase) {
|
|
||||||
opts.passphrase = keyInfo.passphrase;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldKeyId) {
|
|
||||||
opts.key = ssssKeys[oldKeyId];
|
|
||||||
}
|
|
||||||
// create new symmetric SSSS key and set it as default
|
|
||||||
newKeyId = await this.addSecretStorageKey(
|
|
||||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
||||||
);
|
);
|
||||||
if (oldKeyId) {
|
await this.setDefaultSecretStorageKeyId(keyId);
|
||||||
ssssKeys[newKeyId] = ssssKeys[oldKeyId];
|
|
||||||
|
if (privateKey) {
|
||||||
|
// cache the private key so that we can access it again
|
||||||
|
ssssKeys[keyId] = privateKey;
|
||||||
}
|
}
|
||||||
await this.setDefaultSecretStorageKeyId(newKeyId);
|
return keyId;
|
||||||
// re-encrypt all the keys with the new key
|
};
|
||||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
|
||||||
const secretName = `m.cross_signing.${type}`;
|
// reset the cross-signing keys
|
||||||
await this.storeSecret(secretName, keys[type], [newKeyId]);
|
const resetCrossSigning = async () => {
|
||||||
}
|
|
||||||
} else if (!this._crossSigningInfo.getId() || !inStorage) {
|
|
||||||
// create new cross-signing keys if necessary.
|
|
||||||
logger.log(
|
|
||||||
"Cross-signing public and/or private keys not found, " +
|
|
||||||
"checking secret storage for private keys",
|
|
||||||
);
|
|
||||||
if (inStorage) {
|
|
||||||
logger.log("Cross-signing private keys found in secret storage");
|
|
||||||
await this.checkOwnCrossSigningTrust();
|
|
||||||
} else {
|
|
||||||
logger.log(
|
|
||||||
"Cross-signing private keys not found in secret storage, " +
|
|
||||||
"creating new keys",
|
|
||||||
);
|
|
||||||
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
|
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
|
||||||
keys => Object.assign(crossSigningPrivateKeys, keys);
|
keys => Object.assign(crossSigningPrivateKeys, keys);
|
||||||
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
||||||
@@ -576,28 +546,75 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
CrossSigningLevel.MASTER,
|
CrossSigningLevel.MASTER,
|
||||||
{ authUploadDeviceSigningKeys },
|
{ authUploadDeviceSigningKeys },
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||||
|
if (!keyInfo.mac) {
|
||||||
|
const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey(
|
||||||
|
{keys: {[keyId]: keyInfo}}, "",
|
||||||
|
);
|
||||||
|
if (key) {
|
||||||
|
const keyData = key[1];
|
||||||
|
ssssKeys[keyId] = keyData;
|
||||||
|
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
|
||||||
|
keyInfo.iv = iv;
|
||||||
|
keyInfo.mac = mac;
|
||||||
|
|
||||||
|
await this._baseApis.setAccountData(
|
||||||
|
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
logger.log("Cross signing keys are present in secret storage");
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oldSSSSKey = await this.getSecretStorageKey();
|
||||||
|
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||||
|
const decryptionKeys =
|
||||||
|
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||||
|
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||||
|
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
|
||||||
|
await resetCrossSigning();
|
||||||
|
|
||||||
|
if (
|
||||||
|
setupNewSecretStorage ||
|
||||||
|
!oldKeyInfo ||
|
||||||
|
oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
|
||||||
|
) {
|
||||||
|
// if we already have a usable default SSSS key and aren't resetting SSSS just use it.
|
||||||
|
// otherwise, create a new one
|
||||||
|
// Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
|
||||||
|
// We could move them to the new key but a) that would mean we'd need to prompt for the old
|
||||||
|
// passphrase, and b) it's not clear that would be the right thing to do anyway.
|
||||||
|
const { keyInfo, privateKey } = await createSecretStorageKey();
|
||||||
|
newKeyId = await createSSSS(keyInfo, privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to create a new secret storage key
|
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
// - we're resetting secret storage
|
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||||
// - we don't have a default secret storage key yet
|
}
|
||||||
// - our default secret storage key is using an older algorithm
|
} else if (!inStorage && keyBackupInfo) {
|
||||||
// We will also run this part if we created a new secret storage key
|
// we have an existing backup, but no SSSS
|
||||||
// above, so that we can (re-)encrypt the backup with it.
|
|
||||||
const defaultSSSSKey = await this.getSecretStorageKey();
|
|
||||||
if (setupNewSecretStorage || newKeyId || !defaultSSSSKey
|
|
||||||
|| defaultSSSSKey[1].algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) {
|
|
||||||
if (keyBackupInfo) {
|
|
||||||
// if we already have a backup key, use the same key as the
|
|
||||||
// secret storage key
|
|
||||||
logger.log("Secret storage default key not found, using key backup key");
|
logger.log("Secret storage default key not found, using key backup key");
|
||||||
|
|
||||||
const backupKey = await getKeyBackupPassphrase();
|
// 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 (!newKeyId) {
|
// create new cross-signing keys
|
||||||
|
await resetCrossSigning();
|
||||||
|
|
||||||
|
// create a new SSSS key and use the backup key as the new SSSS key
|
||||||
const opts = {};
|
const opts = {};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -612,20 +629,16 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the backup key as the new ssss key
|
newKeyId = await createSSSS(opts, backupKey);
|
||||||
ssssKeys[newKeyId] = backupKey;
|
|
||||||
opts.key = backupKey;
|
|
||||||
|
|
||||||
newKeyId = await this.addSecretStorageKey(
|
// store the backup key in secret storage
|
||||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
await this.storeSecret(
|
||||||
|
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||||
);
|
);
|
||||||
await this.setDefaultSecretStorageKeyId(newKeyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this key backup is trusted, sign it with the cross signing key
|
// The backup is trusted because the user provided the private key.
|
||||||
// so the key backup can be trusted via cross-signing.
|
// Sign the backup with the cross signing key so the key backup can
|
||||||
const backupSigStatus = await this.checkKeyBackup(keyBackupInfo);
|
// be trusted via cross-signing.
|
||||||
if (backupSigStatus.trustInfo.usable) {
|
|
||||||
logger.log("Adding cross signing signature to key backup");
|
logger.log("Adding cross signing signature to key backup");
|
||||||
await this._crossSigningInfo.signObject(
|
await this._crossSigningInfo.signObject(
|
||||||
keyBackupInfo.auth_data, "master",
|
keyBackupInfo.auth_data, "master",
|
||||||
@@ -635,38 +648,29 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
undefined, keyBackupInfo,
|
undefined, keyBackupInfo,
|
||||||
{prefix: httpApi.PREFIX_UNSTABLE},
|
{prefix: httpApi.PREFIX_UNSTABLE},
|
||||||
);
|
);
|
||||||
await this.storeSecret(
|
} else if (!this._crossSigningInfo.getId()) {
|
||||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
// we have SSSS, but we don't know if the server's cross-signing
|
||||||
);
|
// keys should be trusted
|
||||||
} else {
|
logger.log("Cross-signing private keys found in secret storage");
|
||||||
logger.log(
|
|
||||||
"Key backup is NOT TRUSTED: NOT adding cross signing signature",
|
// 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 {
|
} else {
|
||||||
if (!newKeyId) {
|
// we have SSSS and we cross-signing is already set up
|
||||||
logger.log("Secret storage default key not found, creating new key");
|
logger.log("Cross signing keys are present in secret storage");
|
||||||
const { keyInfo, privateKey } = await createSecretStorageKey();
|
|
||||||
if (keyInfo && privateKey) {
|
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
keyInfo.key = privateKey;
|
// make sure that the default key has the information needed to
|
||||||
|
// check the passphrase
|
||||||
|
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||||
}
|
}
|
||||||
newKeyId = await this.addSecretStorageKey(
|
|
||||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
|
||||||
keyInfo,
|
|
||||||
);
|
|
||||||
await this.setDefaultSecretStorageKeyId(newKeyId);
|
|
||||||
ssssKeys[newKeyId] = privateKey;
|
|
||||||
}
|
|
||||||
if (await this.isSecretStored("m.megolm_backup.v1")) {
|
|
||||||
// we created a new SSSS, and we previously encrypted the
|
|
||||||
// backup key with the old SSSS key, so re-encrypt with the
|
|
||||||
// new key
|
|
||||||
const backupKey = await this.getSecret("m.megolm_backup.v1");
|
|
||||||
await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.log("Have secret storage key");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cross-signing keys were reset, store them in Secure Secret Storage.
|
// If cross-signing keys were reset, store them in Secure Secret Storage.
|
||||||
@@ -676,11 +680,6 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
// See also https://github.com/vector-im/riot-web/issues/11635
|
// See also https://github.com/vector-im/riot-web/issues/11635
|
||||||
if (Object.keys(crossSigningPrivateKeys).length) {
|
if (Object.keys(crossSigningPrivateKeys).length) {
|
||||||
logger.log("Storing cross-signing private keys in secret storage");
|
logger.log("Storing cross-signing private keys in secret storage");
|
||||||
// SSSS expects its keys to be signed by cross-signing master key.
|
|
||||||
// Since we have just reset cross-signing keys, we need to re-sign the
|
|
||||||
// SSSS default key with the new cross-signing master key so that the
|
|
||||||
// following storage step can proceed.
|
|
||||||
await this._secretStorage.signKey();
|
|
||||||
// Assuming no app-supplied callback, default to storing in SSSS.
|
// Assuming no app-supplied callback, default to storing in SSSS.
|
||||||
if (!appCallbacks.saveCrossSigningKeys) {
|
if (!appCallbacks.saveCrossSigningKeys) {
|
||||||
await CrossSigningInfo.storeInSecretStorage(
|
await CrossSigningInfo.storeInSecretStorage(
|
||||||
@@ -711,7 +710,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
|
const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
|
||||||
if (sessionBackupKey) {
|
if (sessionBackupKey) {
|
||||||
logger.info("Got session backup key from secret storage: caching");
|
logger.info("Got session backup key from secret storage: caching");
|
||||||
await this.storeSessionBackupPrivateKey(sessionBackupKey);
|
const decodedBackupKey =
|
||||||
|
new Uint8Array(olmlib.decodeBase64(sessionBackupKey));
|
||||||
|
await this.storeSessionBackupPrivateKey(decodedBackupKey);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Restore the original callbacks. NB. we must do this by manipulating
|
// Restore the original callbacks. NB. we must do this by manipulating
|
||||||
@@ -736,10 +737,6 @@ Crypto.prototype.hasSecretStorageKey = function(keyID) {
|
|||||||
return this._secretStorage.hasKey(keyID);
|
return this._secretStorage.hasKey(keyID);
|
||||||
};
|
};
|
||||||
|
|
||||||
Crypto.prototype.secretStorageKeyNeedsUpgrade = function(keyID) {
|
|
||||||
return this._secretStorage.keyNeedsUpgrade(keyID);
|
|
||||||
};
|
|
||||||
|
|
||||||
Crypto.prototype.getSecretStorageKey = function(keyID) {
|
Crypto.prototype.getSecretStorageKey = function(keyID) {
|
||||||
return this._secretStorage.getKey(keyID);
|
return this._secretStorage.getKey(keyID);
|
||||||
};
|
};
|
||||||
@@ -800,7 +797,7 @@ Crypto.prototype.checkSecretStoragePrivateKey = function(privateKey, expectedPub
|
|||||||
* Fetches the backup private key, if cached
|
* Fetches the backup private key, if cached
|
||||||
* @returns {Promise} the key, if any, or null
|
* @returns {Promise} the key, if any, or null
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.getSessionBackupPrivateKey = async function() {
|
Crypto.prototype.getSessionBackupPrivateKey = function() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this._cryptoStore.doTxn(
|
this._cryptoStore.doTxn(
|
||||||
'readonly',
|
'readonly',
|
||||||
@@ -822,6 +819,9 @@ Crypto.prototype.getSessionBackupPrivateKey = async function() {
|
|||||||
* @returns {Promise} so you can catch failures
|
* @returns {Promise} so you can catch failures
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.storeSessionBackupPrivateKey = async function(key) {
|
Crypto.prototype.storeSessionBackupPrivateKey = async function(key) {
|
||||||
|
if (!(key instanceof Uint8Array)) {
|
||||||
|
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||||
|
}
|
||||||
return this._cryptoStore.doTxn(
|
return this._cryptoStore.doTxn(
|
||||||
'readwrite',
|
'readwrite',
|
||||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
@@ -1141,13 +1141,18 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
|
|||||||
// If it's not changed, just make sure everything is up to date
|
// If it's not changed, just make sure everything is up to date
|
||||||
await this.checkOwnCrossSigningTrust();
|
await this.checkOwnCrossSigningTrust();
|
||||||
} else {
|
} else {
|
||||||
this.emit("crossSigning.keysChanged", {});
|
|
||||||
// We'll now be in a state where cross-signing on the account is not trusted
|
// We'll now be in a state where cross-signing on the account is not trusted
|
||||||
// because our locally stored cross-signing keys will not match the ones
|
// because our locally stored cross-signing keys will not match the ones
|
||||||
// on the server for our account. The app must call checkOwnCrossSigningTrust()
|
// on the server for our account. So we clear our own stored cross-signing keys,
|
||||||
// to fix this.
|
// effectively disabling cross-signing until the user gets verified by the device
|
||||||
// XXX: Do we need to do something to emit events saying every device has become
|
// that reset the keys
|
||||||
// untrusted?
|
this._storeTrustedSelfKeys(null);
|
||||||
|
// emit cross-signing has been disabled
|
||||||
|
this.emit("crossSigning.keysChanged", {});
|
||||||
|
// as the trust for our own user has changed,
|
||||||
|
// also emit an event for this
|
||||||
|
this.emit("userTrustStatusChanged",
|
||||||
|
this._userId, this.checkUserTrust(userId));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this._checkDeviceVerifications(userId);
|
await this._checkDeviceVerifications(userId);
|
||||||
@@ -1303,7 +1308,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
|||||||
* @param {object} keys The new trusted set of keys
|
* @param {object} keys The new trusted set of keys
|
||||||
*/
|
*/
|
||||||
Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
|
Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
|
||||||
|
if (keys) {
|
||||||
this._crossSigningInfo.setKeys(keys);
|
this._crossSigningInfo.setKeys(keys);
|
||||||
|
} else {
|
||||||
|
this._crossSigningInfo.clearKeys();
|
||||||
|
}
|
||||||
await this._cryptoStore.doTxn(
|
await this._cryptoStore.doTxn(
|
||||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
(txn) => {
|
(txn) => {
|
||||||
@@ -1368,8 +1377,9 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
|
|||||||
backupInfo = await this._baseApis.getKeyBackupVersion();
|
backupInfo = await this._baseApis.getKeyBackupVersion();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.log("Error checking for active key backup", e);
|
logger.log("Error checking for active key backup", e);
|
||||||
if (e.httpStatus / 100 === 4) {
|
if (e.httpStatus === 404) {
|
||||||
// well that's told us. we won't try again.
|
// 404 is returned when the key backup does not exist, so that
|
||||||
|
// counts as successfully checking.
|
||||||
this._checkedForBackup = true;
|
this._checkedForBackup = true;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -1912,6 +1922,10 @@ Crypto.prototype.setDeviceVerification = async function(
|
|||||||
|
|
||||||
if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) {
|
if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) {
|
||||||
this._storeTrustedSelfKeys(xsk.keys);
|
this._storeTrustedSelfKeys(xsk.keys);
|
||||||
|
// This will cause our own user trust to change, so emit the event
|
||||||
|
this.emit(
|
||||||
|
"userTrustStatusChanged", this._userId, this.checkUserTrust(userId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now sign the master key with our user signing key (unless it's ourself)
|
// Now sign the master key with our user signing key (unless it's ourself)
|
||||||
@@ -2054,7 +2068,8 @@ Crypto.prototype.requestVerification = function(userId, devices) {
|
|||||||
if (existingRequest) {
|
if (existingRequest) {
|
||||||
return Promise.resolve(existingRequest);
|
return Promise.resolve(existingRequest);
|
||||||
}
|
}
|
||||||
const channel = new ToDeviceChannel(this._baseApis, userId, devices);
|
const channel = new ToDeviceChannel(this._baseApis, userId, devices,
|
||||||
|
ToDeviceChannel.makeTransactionId());
|
||||||
return this._requestVerificationWithChannel(
|
return this._requestVerificationWithChannel(
|
||||||
userId,
|
userId,
|
||||||
channel,
|
channel,
|
||||||
@@ -2067,6 +2082,10 @@ Crypto.prototype._requestVerificationWithChannel = async function(
|
|||||||
) {
|
) {
|
||||||
let request = new VerificationRequest(
|
let request = new VerificationRequest(
|
||||||
channel, this._verificationMethods, this._baseApis);
|
channel, this._verificationMethods, this._baseApis);
|
||||||
|
// if transaction id is already known, add request
|
||||||
|
if (channel.transactionId) {
|
||||||
|
requestsMap.setRequestByChannel(channel, request);
|
||||||
|
}
|
||||||
await request.sendRequest();
|
await request.sendRequest();
|
||||||
// don't replace the request created by a racing remote echo
|
// don't replace the request created by a racing remote echo
|
||||||
const racingRequest = requestsMap.getRequestByChannel(channel);
|
const racingRequest = requestsMap.getRequestByChannel(channel);
|
||||||
@@ -2434,17 +2453,37 @@ Crypto.prototype.exportRoomKeys = async function() {
|
|||||||
* Import a list of room keys previously exported by exportRoomKeys
|
* Import a list of room keys previously exported by exportRoomKeys
|
||||||
*
|
*
|
||||||
* @param {Object[]} keys a list of session export objects
|
* @param {Object[]} keys a list of session export objects
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {Function} opts.progressCallback called with an object which has a stage param
|
||||||
* @return {Promise} a promise which resolves once the keys have been imported
|
* @return {Promise} a promise which resolves once the keys have been imported
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.importRoomKeys = function(keys) {
|
Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
|
||||||
|
let successes = 0;
|
||||||
|
let failures = 0;
|
||||||
|
const total = keys.length;
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
opts.progressCallback({
|
||||||
|
stage: "load_keys",
|
||||||
|
successes,
|
||||||
|
failures,
|
||||||
|
total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(keys.map((key) => {
|
return Promise.all(keys.map((key) => {
|
||||||
if (!key.room_id || !key.algorithm) {
|
if (!key.room_id || !key.algorithm) {
|
||||||
logger.warn("ignoring room key entry with missing fields", key);
|
logger.warn("ignoring room key entry with missing fields", key);
|
||||||
|
failures++;
|
||||||
|
if (opts.progressCallback) { updateProgress(); }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
|
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
|
||||||
return alg.importRoomKey(key);
|
return alg.importRoomKey(key).finally((r) => {
|
||||||
|
successes++;
|
||||||
|
if (opts.progressCallback) { updateProgress(); }
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2760,9 +2799,13 @@ Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLi
|
|||||||
* @return {Promise} a promise that resolves when the key request is queued
|
* @return {Promise} a promise that resolves when the key request is queued
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) {
|
Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) {
|
||||||
return this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
|
return this._outgoingRoomKeyRequestManager.queueRoomKeyRequest(
|
||||||
requestBody, recipients, resend,
|
requestBody, recipients, resend,
|
||||||
).catch((e) => {
|
).then(() => {
|
||||||
|
if (this._sendKeyRequestsImmediately) {
|
||||||
|
this._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
// this normally means we couldn't talk to the store
|
// this normally means we couldn't talk to the store
|
||||||
logger.error(
|
logger.error(
|
||||||
'Error requesting key for event', e,
|
'Error requesting key for event', e,
|
||||||
@@ -2827,6 +2870,8 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) {
|
|||||||
this._deviceList.startTrackingDeviceList(this._userId);
|
this._deviceList.startTrackingDeviceList(this._userId);
|
||||||
this._roomDeviceTrackingState = {};
|
this._roomDeviceTrackingState = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sendKeyRequestsImmediately = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2858,6 +2903,14 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
|
|||||||
if (!syncData.catchingUp) {
|
if (!syncData.catchingUp) {
|
||||||
_maybeUploadOneTimeKeys(this);
|
_maybeUploadOneTimeKeys(this);
|
||||||
this._processReceivedRoomKeyRequests();
|
this._processReceivedRoomKeyRequests();
|
||||||
|
|
||||||
|
// likewise don't start requesting keys until we've caught up
|
||||||
|
// on to_device messages, otherwise we'll request keys that we're
|
||||||
|
// just about to get.
|
||||||
|
this._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||||
|
|
||||||
|
// Sync has finished so send key requests straight away.
|
||||||
|
this._sendKeyRequestsImmediately = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3377,6 +3430,19 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deviceId === this._deviceId) {
|
||||||
|
// We'll always get these because we send room key requests to
|
||||||
|
// '*' (ie. 'all devices') which includes the sending device,
|
||||||
|
// so ignore requests from ourself because apart from it being
|
||||||
|
// very silly, it won't work because an Olm session cannot send
|
||||||
|
// messages to itself.
|
||||||
|
// The log here is probably superfluous since we know this will
|
||||||
|
// always happen, but let's log anyway for now just in case it
|
||||||
|
// causes issues.
|
||||||
|
logger.log("Ignoring room key request from ourselves");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// todo: should we queue up requests we don't yet have keys for,
|
// todo: should we queue up requests we don't yet have keys for,
|
||||||
// in case they turn up later?
|
// in case they turn up later?
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,25 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
for (const deviceInfo of devices) {
|
for (const deviceInfo of devices) {
|
||||||
const deviceId = deviceInfo.deviceId;
|
const deviceId = deviceInfo.deviceId;
|
||||||
const key = deviceInfo.getIdentityKey();
|
const key = deviceInfo.getIdentityKey();
|
||||||
|
|
||||||
|
if (key === olmDevice.deviceCurve25519Key) {
|
||||||
|
// We should never be trying to start a session with ourself.
|
||||||
|
// Apart from talking to yourself being the first sign of madness,
|
||||||
|
// olm sessions can't do this because they get confused when
|
||||||
|
// they get a message and see that the 'other side' has started a
|
||||||
|
// new chain when this side has an active sender chain.
|
||||||
|
// If you see this message being logged in the wild, we should find
|
||||||
|
// the thing that is trying to send Olm messages to itself and fix it.
|
||||||
|
logger.info("Attempted to start session with ourself! Ignoring");
|
||||||
|
// We must fill in the section in the return value though, as callers
|
||||||
|
// expect it to be there.
|
||||||
|
result[userId][deviceId] = {
|
||||||
|
device: deviceInfo,
|
||||||
|
sessionId: null,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!olmDevice._sessionsInProgress[key]) {
|
if (!olmDevice._sessionsInProgress[key]) {
|
||||||
// pre-emptively mark the session as in-progress to avoid race
|
// pre-emptively mark the session as in-progress to avoid race
|
||||||
// conditions. If we find that we already have a session, then
|
// conditions. If we find that we already have a session, then
|
||||||
@@ -238,6 +257,11 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
delete resolveSession[key];
|
delete resolveSession[key];
|
||||||
}
|
}
|
||||||
if (sessionId === null || force) {
|
if (sessionId === null || force) {
|
||||||
|
if (force) {
|
||||||
|
logger.info("Forcing new Olm session for " + userId + ":" + deviceId);
|
||||||
|
} else {
|
||||||
|
logger.info("Making new Olm session for " + userId + ":" + deviceId);
|
||||||
|
}
|
||||||
devicesWithoutSession.push([userId, deviceId]);
|
devicesWithoutSession.push([userId, deviceId]);
|
||||||
}
|
}
|
||||||
result[userId][deviceId] = {
|
result[userId][deviceId] = {
|
||||||
@@ -277,6 +301,14 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
const deviceInfo = devices[j];
|
const deviceInfo = devices[j];
|
||||||
const deviceId = deviceInfo.deviceId;
|
const deviceId = deviceInfo.deviceId;
|
||||||
const key = deviceInfo.getIdentityKey();
|
const key = deviceInfo.getIdentityKey();
|
||||||
|
|
||||||
|
if (key === olmDevice.deviceCurve25519Key) {
|
||||||
|
// We've already logged about this above. Skip here too
|
||||||
|
// otherwise we'll log saying there are no one-time keys
|
||||||
|
// which will be confusing.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (result[userId][deviceId].sessionId && !force) {
|
if (result[userId][deviceId].sessionId && !force) {
|
||||||
// we already have a result for this device
|
// we already have a result for this device
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export function decodeRecoveryKey(recoverykey) {
|
|||||||
throw new Error("Incorrect length");
|
throw new Error("Incorrect length");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.slice(
|
return Uint8Array.from(result.slice(
|
||||||
OLM_RECOVERY_KEY_PREFIX.length,
|
OLM_RECOVERY_KEY_PREFIX.length,
|
||||||
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
|
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ export class VerificationBase extends EventEmitter {
|
|||||||
if (this._done) {
|
if (this._done) {
|
||||||
return Promise.reject(new Error("Verification is already done"));
|
return Promise.reject(new Error("Verification is already done"));
|
||||||
}
|
}
|
||||||
|
const existingEvent = this.request.getEventFromOtherParty(type);
|
||||||
|
if (existingEvent) {
|
||||||
|
return Promise.resolve(existingEvent);
|
||||||
|
}
|
||||||
|
|
||||||
this._expectedEvent = type;
|
this._expectedEvent = type;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this._resolveEvent = resolve;
|
this._resolveEvent = resolve;
|
||||||
@@ -287,6 +292,7 @@ export class VerificationBase extends EventEmitter {
|
|||||||
this._endTimer(); // always kill the activity timer
|
this._endTimer(); // always kill the activity timer
|
||||||
if (!this._done) {
|
if (!this._done) {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
|
this.request.onVerifierCancelled();
|
||||||
if (this.userId && this.deviceId) {
|
if (this.userId && this.deviceId) {
|
||||||
// send a cancellation to the other user (if it wasn't
|
// send a cancellation to the other user (if it wasn't
|
||||||
// cancelled by the other user)
|
// cancelled by the other user)
|
||||||
@@ -369,7 +375,7 @@ export class VerificationBase extends EventEmitter {
|
|||||||
|
|
||||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||||
const deviceId = keyId.split(':', 2)[1];
|
const deviceId = keyId.split(':', 2)[1];
|
||||||
const device = await this._baseApis.getStoredDevice(userId, deviceId);
|
const device = this._baseApis.getStoredDevice(userId, deviceId);
|
||||||
if (device) {
|
if (device) {
|
||||||
await verifier(keyId, device, keyInfo);
|
await verifier(keyId, device, keyInfo);
|
||||||
verifiedDevices.push(deviceId);
|
verifiedDevices.push(deviceId);
|
||||||
|
|||||||
@@ -65,20 +65,29 @@ export class ReciprocateQRCode extends Base {
|
|||||||
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. determine key to sign
|
// 3. determine key to sign / mark as trusted
|
||||||
const keys = {};
|
const keys = {};
|
||||||
if (qrCodeData.mode === MODE_VERIFY_OTHER_USER) {
|
|
||||||
|
switch (qrCodeData.mode) {
|
||||||
|
case MODE_VERIFY_OTHER_USER: {
|
||||||
// add master key to keys to be signed, only if we're not doing self-verification
|
// add master key to keys to be signed, only if we're not doing self-verification
|
||||||
const masterKey = qrCodeData.otherUserMasterKey;
|
const masterKey = qrCodeData.otherUserMasterKey;
|
||||||
keys[`ed25519:${masterKey}`] = masterKey;
|
keys[`ed25519:${masterKey}`] = masterKey;
|
||||||
} else if (qrCodeData.mode === MODE_VERIFY_SELF_TRUSTED) {
|
break;
|
||||||
|
}
|
||||||
|
case MODE_VERIFY_SELF_TRUSTED: {
|
||||||
const deviceId = this.request.targetDevice.deviceId;
|
const deviceId = this.request.targetDevice.deviceId;
|
||||||
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
|
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
|
||||||
} else {
|
break;
|
||||||
// TODO: not sure if MODE_VERIFY_SELF_UNTRUSTED makes sense to sign anything here?
|
}
|
||||||
|
case MODE_VERIFY_SELF_UNTRUSTED: {
|
||||||
|
const masterKey = qrCodeData.myMasterKey;
|
||||||
|
keys[`ed25519:${masterKey}`] = masterKey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. sign the key
|
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
|
||||||
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||||
// make sure the device has the expected keys
|
// make sure the device has the expected keys
|
||||||
const targetKey = keys[keyId];
|
const targetKey = keys[keyId];
|
||||||
@@ -108,11 +117,15 @@ const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
|||||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||||
|
|
||||||
export class QRCodeData {
|
export class QRCodeData {
|
||||||
constructor(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, buffer) {
|
constructor(
|
||||||
|
mode, sharedSecret, otherUserMasterKey,
|
||||||
|
otherDeviceKey, myMasterKey, buffer,
|
||||||
|
) {
|
||||||
this._sharedSecret = sharedSecret;
|
this._sharedSecret = sharedSecret;
|
||||||
this._mode = mode;
|
this._mode = mode;
|
||||||
this._otherUserMasterKey = otherUserMasterKey;
|
this._otherUserMasterKey = otherUserMasterKey;
|
||||||
this._otherDeviceKey = otherDeviceKey;
|
this._otherDeviceKey = otherDeviceKey;
|
||||||
|
this._myMasterKey = myMasterKey;
|
||||||
this._buffer = buffer;
|
this._buffer = buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,22 +134,28 @@ export class QRCodeData {
|
|||||||
const mode = QRCodeData._determineMode(request, client);
|
const mode = QRCodeData._determineMode(request, client);
|
||||||
let otherUserMasterKey = null;
|
let otherUserMasterKey = null;
|
||||||
let otherDeviceKey = null;
|
let otherDeviceKey = null;
|
||||||
|
let myMasterKey = null;
|
||||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||||
const otherUserCrossSigningInfo =
|
const otherUserCrossSigningInfo =
|
||||||
client.getStoredCrossSigningForUser(request.otherUserId);
|
client.getStoredCrossSigningForUser(request.otherUserId);
|
||||||
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
|
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
|
||||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||||
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
|
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
|
||||||
|
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||||
|
const myUserId = client.getUserId();
|
||||||
|
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||||
|
myMasterKey = myCrossSigningInfo.getId("master");
|
||||||
}
|
}
|
||||||
const qrData = QRCodeData._generateQrData(
|
const qrData = QRCodeData._generateQrData(
|
||||||
request, client, mode,
|
request, client, mode,
|
||||||
sharedSecret,
|
sharedSecret,
|
||||||
otherUserMasterKey,
|
otherUserMasterKey,
|
||||||
otherDeviceKey,
|
otherDeviceKey,
|
||||||
|
myMasterKey,
|
||||||
);
|
);
|
||||||
const buffer = QRCodeData._generateBuffer(qrData);
|
const buffer = QRCodeData._generateBuffer(qrData);
|
||||||
return new QRCodeData(mode, sharedSecret,
|
return new QRCodeData(mode, sharedSecret,
|
||||||
otherUserMasterKey, otherDeviceKey, buffer);
|
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
get buffer() {
|
get buffer() {
|
||||||
@@ -147,14 +166,30 @@ export class QRCodeData {
|
|||||||
return this._mode;
|
return this._mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only set when mode is MODE_VERIFY_SELF_TRUSTED
|
||||||
|
* @return {string} device key of other party at time of generating QR code
|
||||||
|
*/
|
||||||
get otherDeviceKey() {
|
get otherDeviceKey() {
|
||||||
return this._otherDeviceKey;
|
return this._otherDeviceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only set when mode is MODE_VERIFY_OTHER_USER
|
||||||
|
* @return {string} master key of other party at time of generating QR code
|
||||||
|
*/
|
||||||
get otherUserMasterKey() {
|
get otherUserMasterKey() {
|
||||||
return this._otherUserMasterKey;
|
return this._otherUserMasterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
|
||||||
|
* @return {string} own master key at time of generating QR code
|
||||||
|
*/
|
||||||
|
get myMasterKey() {
|
||||||
|
return this._myMasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unpadded base64 encoded shared secret.
|
* The unpadded base64 encoded shared secret.
|
||||||
*/
|
*/
|
||||||
@@ -172,7 +207,7 @@ export class QRCodeData {
|
|||||||
const myUserId = client.getUserId();
|
const myUserId = client.getUserId();
|
||||||
const otherDevice = request.targetDevice;
|
const otherDevice = request.targetDevice;
|
||||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||||
const device = await client.getStoredDevice(myUserId, otherDeviceId);
|
const device = client.getStoredDevice(myUserId, otherDeviceId);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error("could not find device " + otherDeviceId);
|
throw new Error("could not find device " + otherDeviceId);
|
||||||
}
|
}
|
||||||
@@ -198,7 +233,8 @@ export class QRCodeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static _generateQrData(request, client, mode,
|
static _generateQrData(request, client, mode,
|
||||||
encodedSharedSecret, otherUserMasterKey, otherDeviceKey,
|
encodedSharedSecret, otherUserMasterKey,
|
||||||
|
otherDeviceKey, myMasterKey,
|
||||||
) {
|
) {
|
||||||
const myUserId = client.getUserId();
|
const myUserId = client.getUserId();
|
||||||
const transactionId = request.channel.transactionId;
|
const transactionId = request.channel.transactionId;
|
||||||
@@ -213,16 +249,15 @@ export class QRCodeData {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||||
const myMasterKey = myCrossSigningInfo.getId("master");
|
|
||||||
|
|
||||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||||
// First key is our master cross signing key
|
// First key is our master cross signing key
|
||||||
qrData.firstKeyB64 = myMasterKey;
|
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||||
// Second key is the other user's master cross signing key
|
// Second key is the other user's master cross signing key
|
||||||
qrData.secondKeyB64 = otherUserMasterKey;
|
qrData.secondKeyB64 = otherUserMasterKey;
|
||||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||||
// First key is our master cross signing key
|
// First key is our master cross signing key
|
||||||
qrData.firstKeyB64 = myMasterKey;
|
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||||
qrData.secondKeyB64 = otherDeviceKey;
|
qrData.secondKeyB64 = otherDeviceKey;
|
||||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||||
// First key is our device's key
|
// First key is our device's key
|
||||||
|
|||||||
@@ -175,11 +175,33 @@ function calculateMAC(olmSAS, method) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calculateKeyAgreement = {
|
||||||
|
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
|
||||||
|
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
|
||||||
|
+ `${sas.ourSASPubKey}|`;
|
||||||
|
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
|
||||||
|
const sasInfo =
|
||||||
|
"MATRIX_KEY_VERIFICATION_SAS|"
|
||||||
|
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||||
|
+ sas._channel.transactionId;
|
||||||
|
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||||
|
},
|
||||||
|
"curve25519": function(sas, olmSAS, bytes) {
|
||||||
|
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
|
||||||
|
const theirInfo = `${sas.userId}${sas.deviceId}`;
|
||||||
|
const sasInfo =
|
||||||
|
"MATRIX_KEY_VERIFICATION_SAS"
|
||||||
|
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||||
|
+ sas._channel.transactionId;
|
||||||
|
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/* lists of algorithms/methods that are supported. The key agreement, hashes,
|
/* lists of algorithms/methods that are supported. The key agreement, hashes,
|
||||||
* and MAC lists should be sorted in order of preference (most preferred
|
* and MAC lists should be sorted in order of preference (most preferred
|
||||||
* first).
|
* first).
|
||||||
*/
|
*/
|
||||||
const KEY_AGREEMENT_LIST = ["curve25519"];
|
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
|
||||||
const HASHES_LIST = ["sha256"];
|
const HASHES_LIST = ["sha256"];
|
||||||
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
|
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
|
||||||
const SAS_LIST = Object.keys(sasGenerators);
|
const SAS_LIST = Object.keys(sasGenerators);
|
||||||
@@ -291,12 +313,14 @@ export class SAS extends Base {
|
|||||||
if (typeof content.commitment !== "string") {
|
if (typeof content.commitment !== "string") {
|
||||||
throw newInvalidMessageError();
|
throw newInvalidMessageError();
|
||||||
}
|
}
|
||||||
|
const keyAgreement = content.key_agreement_protocol;
|
||||||
const macMethod = content.message_authentication_code;
|
const macMethod = content.message_authentication_code;
|
||||||
const hashCommitment = content.commitment;
|
const hashCommitment = content.commitment;
|
||||||
const olmSAS = new global.Olm.SAS();
|
const olmSAS = new global.Olm.SAS();
|
||||||
try {
|
try {
|
||||||
this._send("m.key.verification.key", {
|
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||||
key: olmSAS.get_pubkey(),
|
await this._send("m.key.verification.key", {
|
||||||
|
key: this.ourSASPubKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -308,19 +332,20 @@ export class SAS extends Base {
|
|||||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||||
throw newMismatchedCommitmentError();
|
throw newMismatchedCommitmentError();
|
||||||
}
|
}
|
||||||
|
this.theirSASPubKey = content.key;
|
||||||
olmSAS.set_their_key(content.key);
|
olmSAS.set_their_key(content.key);
|
||||||
|
|
||||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
|
||||||
+ this.userId + this.deviceId
|
|
||||||
+ this._channel.transactionId;
|
|
||||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
|
||||||
const verifySAS = new Promise((resolve, reject) => {
|
const verifySAS = new Promise((resolve, reject) => {
|
||||||
this.sasEvent = {
|
this.sasEvent = {
|
||||||
sas: generateSas(sasBytes, sasMethods),
|
sas: generateSas(sasBytes, sasMethods),
|
||||||
confirm: () => {
|
confirm: async () => {
|
||||||
this._sendMAC(olmSAS, macMethod);
|
try {
|
||||||
|
await this._sendMAC(olmSAS, macMethod);
|
||||||
resolve();
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cancel: () => reject(newUserCancelledError()),
|
cancel: () => reject(newUserCancelledError()),
|
||||||
mismatch: () => reject(newMismatchedSASError()),
|
mismatch: () => reject(newMismatchedSASError()),
|
||||||
@@ -377,7 +402,7 @@ export class SAS extends Base {
|
|||||||
const olmSAS = new global.Olm.SAS();
|
const olmSAS = new global.Olm.SAS();
|
||||||
try {
|
try {
|
||||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||||
this._send("m.key.verification.accept", {
|
await this._send("m.key.verification.accept", {
|
||||||
key_agreement_protocol: keyAgreement,
|
key_agreement_protocol: keyAgreement,
|
||||||
hash: hashMethod,
|
hash: hashMethod,
|
||||||
message_authentication_code: macMethod,
|
message_authentication_code: macMethod,
|
||||||
@@ -390,22 +415,24 @@ export class SAS extends Base {
|
|||||||
let e = await this._waitForEvent("m.key.verification.key");
|
let e = await this._waitForEvent("m.key.verification.key");
|
||||||
// FIXME: make sure event is properly formed
|
// FIXME: make sure event is properly formed
|
||||||
content = e.getContent();
|
content = e.getContent();
|
||||||
|
this.theirSASPubKey = content.key;
|
||||||
olmSAS.set_their_key(content.key);
|
olmSAS.set_their_key(content.key);
|
||||||
this._send("m.key.verification.key", {
|
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||||
key: olmSAS.get_pubkey(),
|
await this._send("m.key.verification.key", {
|
||||||
|
key: this.ourSASPubKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||||
+ this.userId + this.deviceId
|
|
||||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
|
||||||
+ this._channel.transactionId;
|
|
||||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
|
||||||
const verifySAS = new Promise((resolve, reject) => {
|
const verifySAS = new Promise((resolve, reject) => {
|
||||||
this.sasEvent = {
|
this.sasEvent = {
|
||||||
sas: generateSas(sasBytes, sasMethods),
|
sas: generateSas(sasBytes, sasMethods),
|
||||||
confirm: () => {
|
confirm: async () => {
|
||||||
this._sendMAC(olmSAS, macMethod);
|
try {
|
||||||
|
await this._sendMAC(olmSAS, macMethod);
|
||||||
resolve();
|
resolve();
|
||||||
|
} catch(err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cancel: () => reject(newUserCancelledError()),
|
cancel: () => reject(newUserCancelledError()),
|
||||||
mismatch: () => reject(newMismatchedSASError()),
|
mismatch: () => reject(newMismatchedSASError()),
|
||||||
@@ -461,7 +488,7 @@ export class SAS extends Base {
|
|||||||
keyList.sort().join(","),
|
keyList.sort().join(","),
|
||||||
baseInfo + "KEY_IDS",
|
baseInfo + "KEY_IDS",
|
||||||
);
|
);
|
||||||
this._send("m.key.verification.mac", { mac, keys });
|
return this._send("m.key.verification.mac", { mac, keys });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkMAC(olmSAS, content, method) {
|
async _checkMAC(olmSAS, content, method) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
this._accepting = false;
|
this._accepting = false;
|
||||||
this._declining = false;
|
this._declining = false;
|
||||||
this._verifierHasFinished = false;
|
this._verifierHasFinished = false;
|
||||||
|
this._cancelled = false;
|
||||||
this._chosenMethod = null;
|
this._chosenMethod = null;
|
||||||
// we keep a copy of the QR Code data (including other user master key) around
|
// we keep a copy of the QR Code data (including other user master key) around
|
||||||
// for QR reciprocate verification, to protect against
|
// for QR reciprocate verification, to protect against
|
||||||
@@ -525,7 +526,7 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
|
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
|
||||||
if (cancelEvent && phase() !== PHASE_DONE) {
|
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
|
||||||
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
|
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
|
||||||
return transitions;
|
return transitions;
|
||||||
}
|
}
|
||||||
@@ -858,6 +859,15 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVerifierCancelled() {
|
||||||
|
this._cancelled = true;
|
||||||
|
// move to cancelled phase
|
||||||
|
const newTransitions = this._applyPhaseTransitions();
|
||||||
|
if (newTransitions.length) {
|
||||||
|
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onVerifierFinished() {
|
onVerifierFinished() {
|
||||||
this.channel.send("m.key.verification.done", {});
|
this.channel.send("m.key.verification.done", {});
|
||||||
this._verifierHasFinished = true;
|
this._verifierHasFinished = true;
|
||||||
@@ -867,4 +877,8 @@ export class VerificationRequest extends EventEmitter {
|
|||||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEventFromOtherParty(type) {
|
||||||
|
return this._eventsByThem.get(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ FilterComponent.prototype._checkFields =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allowed_values = self[name];
|
const allowed_values = self[name];
|
||||||
if (allowed_values) {
|
if (allowed_values && allowed_values.length > 0) {
|
||||||
if (!allowed_values.map(match_func)) {
|
const anyMatch = allowed_values.some(match_func);
|
||||||
|
if (!anyMatch) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,6 @@ Filter.LAZY_LOADING_MESSAGES_FILTER = {
|
|||||||
lazy_load_members: true,
|
lazy_load_members: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
Filter.LAZY_LOADING_SYNC_FILTER = {
|
|
||||||
room: {
|
|
||||||
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID of this filter on your homeserver (if known)
|
* Get the ID of this filter on your homeserver (if known)
|
||||||
* @return {?Number} The filter ID
|
* @return {?Number} The filter ID
|
||||||
@@ -96,6 +89,7 @@ Filter.prototype.setDefinition = function(definition) {
|
|||||||
// "state": {
|
// "state": {
|
||||||
// "types": ["m.room.*"],
|
// "types": ["m.room.*"],
|
||||||
// "not_rooms": ["!726s6s6q:example.com"],
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// "lazy_load_members": true,
|
||||||
// },
|
// },
|
||||||
// "timeline": {
|
// "timeline": {
|
||||||
// "limit": 10,
|
// "limit": 10,
|
||||||
@@ -177,6 +171,10 @@ Filter.prototype.setTimelineLimit = function(limit) {
|
|||||||
setProp(this.definition, "room.timeline.limit", limit);
|
setProp(this.definition, "room.timeline.limit", limit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Filter.prototype.setLazyLoadMembers = function(enabled) {
|
||||||
|
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Control whether left rooms should be included in responses.
|
* Control whether left rooms should be included in responses.
|
||||||
* @param {boolean} includeLeave True to make rooms the user has left appear
|
* @param {boolean} includeLeave True to make rooms the user has left appear
|
||||||
|
|||||||
@@ -276,6 +276,9 @@ MatrixHttpApi.prototype = {
|
|||||||
callbacks.clearTimeout(xhr.timeout_timer);
|
callbacks.clearTimeout(xhr.timeout_timer);
|
||||||
var resp;
|
var resp;
|
||||||
try {
|
try {
|
||||||
|
if (xhr.status === 0) {
|
||||||
|
throw new AbortError();
|
||||||
|
}
|
||||||
if (!xhr.responseText) {
|
if (!xhr.responseText) {
|
||||||
throw new Error('No response body.');
|
throw new Error('No response body.');
|
||||||
}
|
}
|
||||||
@@ -789,6 +792,17 @@ const requestCallback = function(
|
|||||||
userDefinedCallback = userDefinedCallback || function() {};
|
userDefinedCallback = userDefinedCallback || function() {};
|
||||||
|
|
||||||
return function(err, response, body) {
|
return function(err, response, body) {
|
||||||
|
if (err) {
|
||||||
|
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
|
||||||
|
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
|
||||||
|
const aborted = err.name === "AbortError" || err === "aborted";
|
||||||
|
if (!aborted && !(err instanceof MatrixError)) {
|
||||||
|
// browser-request just throws normal Error objects,
|
||||||
|
// not `TypeError`s like fetch does. So just assume any
|
||||||
|
// error is due to the connection.
|
||||||
|
err = new ConnectionError("request failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!err) {
|
if (!err) {
|
||||||
try {
|
try {
|
||||||
if (response.statusCode >= 400) {
|
if (response.statusCode >= 400) {
|
||||||
@@ -892,12 +906,76 @@ function getResponseContentType(response) {
|
|||||||
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
||||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
* @prop {integer} httpStatus The numeric HTTP status code given
|
||||||
*/
|
*/
|
||||||
export function MatrixError(errorJson) {
|
export class MatrixError extends Error {
|
||||||
|
constructor(errorJson) {
|
||||||
errorJson = errorJson || {};
|
errorJson = errorJson || {};
|
||||||
|
super(`MatrixError: ${errorJson.errcode}`);
|
||||||
this.errcode = errorJson.errcode;
|
this.errcode = errorJson.errcode;
|
||||||
this.name = errorJson.errcode || "Unknown error code";
|
this.name = errorJson.errcode || "Unknown error code";
|
||||||
this.message = errorJson.error || "Unknown message";
|
this.message = errorJson.error || "Unknown message";
|
||||||
this.data = errorJson;
|
this.data = errorJson;
|
||||||
}
|
}
|
||||||
MatrixError.prototype = Object.create(Error.prototype);
|
}
|
||||||
MatrixError.prototype.constructor = MatrixError;
|
|
||||||
|
/**
|
||||||
|
* Construct a ConnectionError. This is a JavaScript Error indicating
|
||||||
|
* that a request failed because of some error with the connection, either
|
||||||
|
* CORS was not correctly configured on the server, the server didn't response,
|
||||||
|
* the request timed out, or the internet connection on the client side went down.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export class ConnectionError extends Error {
|
||||||
|
constructor(message, cause = undefined) {
|
||||||
|
super(message + (cause ? `: ${cause.message}` : ""));
|
||||||
|
this._cause = cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return "ConnectionError";
|
||||||
|
}
|
||||||
|
|
||||||
|
get cause() {
|
||||||
|
return this._cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbortError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Operation aborted");
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return "AbortError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries a network operation run in a callback.
|
||||||
|
* @param {number} maxAttempts maximum attempts to try
|
||||||
|
* @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
|
||||||
|
* @return {any} the result of the network operation
|
||||||
|
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||||
|
*/
|
||||||
|
export async function retryNetworkOperation(maxAttempts, callback) {
|
||||||
|
let attempts = 0;
|
||||||
|
let lastConnectionError = null;
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
try {
|
||||||
|
if (attempts > 0) {
|
||||||
|
const timeout = 1000 * Math.pow(2, attempts);
|
||||||
|
console.log(`network operation failed ${attempts} times,` +
|
||||||
|
` retrying in ${timeout}ms...`);
|
||||||
|
await new Promise(r => setTimeout(r, timeout));
|
||||||
|
}
|
||||||
|
return await callback();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ConnectionError) {
|
||||||
|
attempts += 1;
|
||||||
|
lastConnectionError = err;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastConnectionError;
|
||||||
|
}
|
||||||
|
|||||||
@@ -357,7 +357,13 @@ InteractiveAuth.prototype = {
|
|||||||
error.data.session = this._data.session;
|
error.data.session = this._data.session;
|
||||||
}
|
}
|
||||||
this._data = error.data;
|
this._data = error.data;
|
||||||
|
try {
|
||||||
this._startNextAuthStage();
|
this._startNextAuthStage();
|
||||||
|
} catch (e) {
|
||||||
|
this._rejectFunc(e);
|
||||||
|
this._resolveFunc = null;
|
||||||
|
this._rejectFunc = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this._emailSid &&
|
!this._emailSid &&
|
||||||
|
|||||||
@@ -16,8 +16,15 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type Request from "request";
|
||||||
|
|
||||||
import {MemoryCryptoStore} from "./crypto/store/memory-crypto-store";
|
import {MemoryCryptoStore} from "./crypto/store/memory-crypto-store";
|
||||||
|
import {LocalStorageCryptoStore} from "./crypto/store/localStorage-crypto-store";
|
||||||
|
import {IndexedDBCryptoStore} from "./crypto/store/indexeddb-crypto-store";
|
||||||
import {MemoryStore} from "./store/memory";
|
import {MemoryStore} from "./store/memory";
|
||||||
|
import {StubStore} from "./store/stub";
|
||||||
|
import {LocalIndexedDBStoreBackend} from "./store/indexeddb-local-backend";
|
||||||
|
import {RemoteIndexedDBStoreBackend} from "./store/indexeddb-remote-backend";
|
||||||
import {MatrixScheduler} from "./scheduler";
|
import {MatrixScheduler} from "./scheduler";
|
||||||
import {MatrixClient} from "./client";
|
import {MatrixClient} from "./client";
|
||||||
|
|
||||||
@@ -89,6 +96,10 @@ export function wrapRequest(wrapper) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Store =
|
||||||
|
StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||||
|
|
||||||
|
type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
|
||||||
|
|
||||||
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
||||||
|
|
||||||
@@ -102,6 +113,15 @@ export function setCryptoStoreFactory(fac) {
|
|||||||
cryptoStoreFactory = fac;
|
cryptoStoreFactory = fac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ICreateClientOpts {
|
||||||
|
baseUrl: string;
|
||||||
|
store?: Store;
|
||||||
|
cryptoStore?: CryptoStore;
|
||||||
|
scheduler?: MatrixScheduler;
|
||||||
|
request?: Request;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||||
@@ -125,10 +145,10 @@ export function setCryptoStoreFactory(fac) {
|
|||||||
* @see {@link module:client.MatrixClient} for the full list of options for
|
* @see {@link module:client.MatrixClient} for the full list of options for
|
||||||
* <code>opts</code>.
|
* <code>opts</code>.
|
||||||
*/
|
*/
|
||||||
export function createClient(opts) {
|
export function createClient(opts: ICreateClientOpts | string) {
|
||||||
if (typeof opts === "string") {
|
if (typeof opts === "string") {
|
||||||
opts = {
|
opts = {
|
||||||
"baseUrl": opts,
|
"baseUrl": opts as string,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
opts.request = opts.request || requestInstance;
|
opts.request = opts.request || requestInstance;
|
||||||
@@ -390,11 +390,12 @@ utils.extend(MatrixEvent.prototype, {
|
|||||||
* @internal
|
* @internal
|
||||||
*
|
*
|
||||||
* @param {module:crypto} crypto crypto module
|
* @param {module:crypto} crypto crypto module
|
||||||
|
* @param {bool} isRetry True if this is a retry (enables more logging)
|
||||||
*
|
*
|
||||||
* @returns {Promise} promise which resolves (to undefined) when the decryption
|
* @returns {Promise} promise which resolves (to undefined) when the decryption
|
||||||
* attempt is completed.
|
* attempt is completed.
|
||||||
*/
|
*/
|
||||||
attemptDecryption: async function(crypto) {
|
attemptDecryption: async function(crypto, isRetry) {
|
||||||
// start with a couple of sanity checks.
|
// start with a couple of sanity checks.
|
||||||
if (!this.isEncrypted()) {
|
if (!this.isEncrypted()) {
|
||||||
throw new Error("Attempt to decrypt event which isn't encrypted");
|
throw new Error("Attempt to decrypt event which isn't encrypted");
|
||||||
@@ -406,7 +407,7 @@ utils.extend(MatrixEvent.prototype, {
|
|||||||
) {
|
) {
|
||||||
// we may want to just ignore this? let's start with rejecting it.
|
// we may want to just ignore this? let's start with rejecting it.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempt to decrypt event which has already been encrypted",
|
"Attempt to decrypt event which has already been decrypted",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +425,7 @@ utils.extend(MatrixEvent.prototype, {
|
|||||||
return this._decryptionPromise;
|
return this._decryptionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._decryptionPromise = this._decryptionLoop(crypto);
|
this._decryptionPromise = this._decryptionLoop(crypto, isRetry);
|
||||||
return this._decryptionPromise;
|
return this._decryptionPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -469,7 +470,7 @@ utils.extend(MatrixEvent.prototype, {
|
|||||||
return recipients;
|
return recipients;
|
||||||
},
|
},
|
||||||
|
|
||||||
_decryptionLoop: async function(crypto) {
|
_decryptionLoop: async function(crypto, isRetry) {
|
||||||
// make sure that this method never runs completely synchronously.
|
// make sure that this method never runs completely synchronously.
|
||||||
// (doing so would mean that we would clear _decryptionPromise *before*
|
// (doing so would mean that we would clear _decryptionPromise *before*
|
||||||
// it is set in attemptDecryption - and hence end up with a stuck
|
// it is set in attemptDecryption - and hence end up with a stuck
|
||||||
@@ -486,13 +487,18 @@ utils.extend(MatrixEvent.prototype, {
|
|||||||
res = this._badEncryptedMessage("Encryption not enabled");
|
res = this._badEncryptedMessage("Encryption not enabled");
|
||||||
} else {
|
} else {
|
||||||
res = await crypto.decryptEvent(this);
|
res = await crypto.decryptEvent(this);
|
||||||
|
if (isRetry) {
|
||||||
|
logger.info(`Decrypted event on retry (id=${this.getId()})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name !== "DecryptionError") {
|
if (e.name !== "DecryptionError") {
|
||||||
// not a decryption error: log the whole exception as an error
|
// not a decryption error: log the whole exception as an error
|
||||||
// (and don't bother with a retry)
|
// (and don't bother with a retry)
|
||||||
|
const re = isRetry ? 're' : '';
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
|
`Error ${re}decrypting event ` +
|
||||||
|
`(id=${this.getId()}): ${e.stack || e}`,
|
||||||
);
|
);
|
||||||
this._decryptionPromise = null;
|
this._decryptionPromise = null;
|
||||||
this._retryDecryption = false;
|
this._retryDecryption = false;
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ utils.inherits(Room, EventEmitter);
|
|||||||
Room.prototype.getVersion = function() {
|
Room.prototype.getVersion = function() {
|
||||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||||
if (!createEvent) {
|
if (!createEvent) {
|
||||||
logger.warn("Room " + this.room_id + " does not have an m.room.create event");
|
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
|
||||||
return '1';
|
return '1';
|
||||||
}
|
}
|
||||||
const ver = createEvent.getContent()['room_version'];
|
const ver = createEvent.getContent()['room_version'];
|
||||||
@@ -675,7 +675,7 @@ Room.prototype.hasUnverifiedDevices = async function() {
|
|||||||
}
|
}
|
||||||
const e2eMembers = await this.getEncryptionTargetMembers();
|
const e2eMembers = await this.getEncryptionTargetMembers();
|
||||||
for (const member of e2eMembers) {
|
for (const member of e2eMembers) {
|
||||||
const devices = await this._client.getStoredDevicesForUser(member.userId);
|
const devices = this._client.getStoredDevicesForUser(member.userId);
|
||||||
if (devices.some((device) => device.isUnverified())) {
|
if (devices.some((device) => device.isUnverified())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
|
|||||||
|
|
||||||
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
|
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
|
||||||
|
|
||||||
// The default override rules to apply when calculating actions for an event. These
|
// The default override rules to apply to the push rules that arrive from the server.
|
||||||
// defaults apply under no other circumstances to avoid confusing the client with server
|
// We do this for two reasons:
|
||||||
// state. We do this for two reasons:
|
|
||||||
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
|
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
|
||||||
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
|
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
|
||||||
// more details.
|
// more details.
|
||||||
@@ -364,33 +363,6 @@ export function PushProcessor(client) {
|
|||||||
return actionObj;
|
return actionObj;
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyRuleDefaults = function(clientRuleset) {
|
|
||||||
// Deep clone the object before we mutate it
|
|
||||||
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
|
|
||||||
|
|
||||||
if (!clientRuleset['global']) {
|
|
||||||
clientRuleset['global'] = {};
|
|
||||||
}
|
|
||||||
if (!clientRuleset['global']['override']) {
|
|
||||||
clientRuleset['global']['override'] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply default overrides
|
|
||||||
const globalOverrides = clientRuleset['global']['override'];
|
|
||||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
|
||||||
const existingRule = globalOverrides
|
|
||||||
.find((r) => r.rule_id === override.rule_id);
|
|
||||||
|
|
||||||
if (!existingRule) {
|
|
||||||
const ruleId = override.rule_id;
|
|
||||||
console.warn(`Adding default global override for ${ruleId}`);
|
|
||||||
globalOverrides.push(override);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ruleset;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ruleMatchesEvent = function(rule, ev) {
|
this.ruleMatchesEvent = function(rule, ev) {
|
||||||
let ret = true;
|
let ret = true;
|
||||||
for (let i = 0; i < rule.conditions.length; ++i) {
|
for (let i = 0; i < rule.conditions.length; ++i) {
|
||||||
@@ -410,8 +382,7 @@ export function PushProcessor(client) {
|
|||||||
* @return {PushAction}
|
* @return {PushAction}
|
||||||
*/
|
*/
|
||||||
this.actionsForEvent = function(ev) {
|
this.actionsForEvent = function(ev) {
|
||||||
const rules = applyRuleDefaults(client.pushRules);
|
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||||
return pushActionsForEventAndRulesets(ev, rules);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -476,18 +447,25 @@ PushProcessor.rewriteDefaultRules = function(incomingRules) {
|
|||||||
if (!newRules.global) newRules.global = {};
|
if (!newRules.global) newRules.global = {};
|
||||||
if (!newRules.global.override) newRules.global.override = [];
|
if (!newRules.global.override) newRules.global.override = [];
|
||||||
|
|
||||||
// Fix default override rules
|
// Merge the client-level defaults with the ones from the server
|
||||||
newRules.global.override = newRules.global.override.map(r => {
|
const globalOverrides = newRules.global.override;
|
||||||
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
|
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||||
if (!defaultRule) return r;
|
const existingRule = globalOverrides
|
||||||
|
.find((r) => r.rule_id === override.rule_id);
|
||||||
|
|
||||||
|
if (existingRule) {
|
||||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||||
// preference.
|
// preference.
|
||||||
r.default = defaultRule.default;
|
existingRule.default = override.default;
|
||||||
r.conditions = defaultRule.conditions;
|
existingRule.conditions = override.conditions;
|
||||||
r.actions = defaultRule.actions;
|
existingRule.actions = override.actions;
|
||||||
return r;
|
} else {
|
||||||
});
|
// Add the rule
|
||||||
|
const ruleId = override.rule_id;
|
||||||
|
console.warn(`Adding default global override for ${ruleId}`);
|
||||||
|
globalOverrides.push(override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return newRules;
|
return newRules;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ limitations under the License.
|
|||||||
import {User} from "../models/user";
|
import {User} from "../models/user";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
|
|
||||||
|
function isValidFilterId(filterId) {
|
||||||
|
const isValidStr = typeof filterId === "string" &&
|
||||||
|
!!filterId &&
|
||||||
|
filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before
|
||||||
|
filterId !== "null";
|
||||||
|
|
||||||
|
return isValidStr || typeof filterId === "number";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new in-memory data store for the Matrix Client.
|
* Construct a new in-memory data store for the Matrix Client.
|
||||||
* @constructor
|
* @constructor
|
||||||
@@ -273,8 +282,17 @@ MemoryStore.prototype = {
|
|||||||
if (!this.localStorage) {
|
if (!this.localStorage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const key = "mxjssdk_memory_filter_" + filterName;
|
||||||
|
// XXX Storage.getItem doesn't throw ...
|
||||||
|
// or are we using something different
|
||||||
|
// than window.localStorage in some cases
|
||||||
|
// that does throw?
|
||||||
|
// that would be very naughty
|
||||||
try {
|
try {
|
||||||
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
|
const value = this.localStorage.getItem(key);
|
||||||
|
if (isValidFilterId(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -288,8 +306,13 @@ MemoryStore.prototype = {
|
|||||||
if (!this.localStorage) {
|
if (!this.localStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const key = "mxjssdk_memory_filter_" + filterName;
|
||||||
try {
|
try {
|
||||||
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
|
if (isValidFilterId(filterId)) {
|
||||||
|
this.localStorage.setItem(key, filterId);
|
||||||
|
} else {
|
||||||
|
this.localStorage.removeItem(key);
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
25
src/sync.js
25
src/sync.js
@@ -511,6 +511,12 @@ SyncApi.prototype.sync = function() {
|
|||||||
checkLazyLoadStatus(); // advance to the next stage
|
checkLazyLoadStatus(); // advance to the next stage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDefaultFilter() {
|
||||||
|
const filter = new Filter(client.credentials.userId);
|
||||||
|
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
const checkLazyLoadStatus = async () => {
|
const checkLazyLoadStatus = async () => {
|
||||||
debuglog("Checking lazy load status...");
|
debuglog("Checking lazy load status...");
|
||||||
if (this.opts.lazyLoadMembers && client.isGuest()) {
|
if (this.opts.lazyLoadMembers && client.isGuest()) {
|
||||||
@@ -520,19 +526,11 @@ SyncApi.prototype.sync = function() {
|
|||||||
debuglog("Checking server lazy load support...");
|
debuglog("Checking server lazy load support...");
|
||||||
const supported = await client.doesServerSupportLazyLoading();
|
const supported = await client.doesServerSupportLazyLoading();
|
||||||
if (supported) {
|
if (supported) {
|
||||||
try {
|
debuglog("Enabling lazy load on sync filter...");
|
||||||
debuglog("Creating and storing lazy load sync filter...");
|
if (!this.opts.filter) {
|
||||||
this.opts.filter = await client.createFilter(
|
this.opts.filter = buildDefaultFilter();
|
||||||
Filter.LAZY_LOADING_SYNC_FILTER,
|
|
||||||
);
|
|
||||||
debuglog("Created and stored lazy load sync filter");
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
"Creating and storing lazy load sync filter failed",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
this.opts.filter.setLazyLoadMembers(true);
|
||||||
} else {
|
} else {
|
||||||
debuglog("LL: lazy loading requested but not supported " +
|
debuglog("LL: lazy loading requested but not supported " +
|
||||||
"by server, so disabling");
|
"by server, so disabling");
|
||||||
@@ -575,8 +573,7 @@ SyncApi.prototype.sync = function() {
|
|||||||
if (self.opts.filter) {
|
if (self.opts.filter) {
|
||||||
filter = self.opts.filter;
|
filter = self.opts.filter;
|
||||||
} else {
|
} else {
|
||||||
filter = new Filter(client.credentials.userId);
|
filter = buildDefaultFilter();
|
||||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let filterId;
|
let filterId;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import unhomoglyph from 'unhomoglyph';
|
import unhomoglyph from 'unhomoglyph';
|
||||||
|
import {ConnectionError} from "./http-api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a dictionary of query parameters.
|
* Encode a dictionary of query parameters.
|
||||||
@@ -265,7 +266,7 @@ export function checkObjectHasNoAdditionalKeys(obj: object, allowedKeys: string[
|
|||||||
* @param {Object} obj The object to deep copy.
|
* @param {Object} obj The object to deep copy.
|
||||||
* @return {Object} A copy of the object without any references to the original.
|
* @return {Object} A copy of the object without any references to the original.
|
||||||
*/
|
*/
|
||||||
export function deepCopy(obj: object): object {
|
export function deepCopy<T>(obj: T): T {
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es2016",
|
"target": "es2016",
|
||||||
|
|||||||
Reference in New Issue
Block a user