1
0
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:
Travis Ralston
2020-05-27 12:14:42 -06:00
45 changed files with 2074 additions and 1055 deletions

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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),
); );

View 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;
});

View 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);
});

View File

@@ -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),
); );

View File

@@ -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: {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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();
});
});
}); });

View File

@@ -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

View 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);
});
});
});

View File

@@ -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
View 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;
}
}
}

View File

@@ -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);

View File

@@ -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"

View File

@@ -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],

View File

@@ -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;

View File

@@ -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),

View File

@@ -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();
}); });
}); });
} }

View File

@@ -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);
} }
} }

View File

@@ -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`);
} }

View File

@@ -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]);
}; };

View File

@@ -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,

View File

@@ -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?

View File

@@ -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;

View File

@@ -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,
); ));
} }

View File

@@ -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);

View File

@@ -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

View File

@@ -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) {

View File

@@ -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);
}
} }

View File

@@ -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;
} }
} }

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 &&

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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;
}; };

View File

@@ -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) {}
}, },

View File

@@ -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;

View File

@@ -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));
} }

View File

@@ -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",

831
yarn.lock

File diff suppressed because it is too large Load Diff