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)
================================================================================================
[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",
"version": "5.2.0",
"version": "6.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -35,6 +35,7 @@
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
@@ -68,6 +69,7 @@
"@babel/preset-typescript": "^7.7.4",
"@babel/register": "^7.7.4",
"@types/node": "12",
"@types/request": "^2.48.4",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babelify": "^10.0.0",

View File

@@ -38,6 +38,7 @@ $USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
-n: skip publish to NPM
EOF
}
@@ -60,9 +61,10 @@ fi
skip_changelog=
skip_jsdoc=
skip_npm=
changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg"
while getopts hc:u:xz f; do
while getopts hc:u:xzn f; do
case $f in
h)
help
@@ -77,6 +79,9 @@ while getopts hc:u:xz f; do
z)
skip_jsdoc=1
;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
@@ -96,10 +101,12 @@ fi
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release
@@ -298,11 +305,13 @@ rm "${latest_changes}"
# defined options and semantics than `yarn` for writing to the registry.
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
if [ -z "$skip_npm" ]; then
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
fi
if [ -z "$skip_jsdoc" ]; then
@@ -338,8 +347,10 @@ if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# finally, merge master back onto develop
git checkout develop
git pull
git merge master
git push origin 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 pull
git merge master
git push origin develop
fi

View File

@@ -185,7 +185,7 @@ TestClient.prototype.expectKeyQuery = function(response) {
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual(
{},
[],
"Expected key query for " + userId + ", got " +
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")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual(
{},
[],
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
@@ -98,7 +98,7 @@ function expectBobQueryKeys() {
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual(
{},
[],
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);

View File

@@ -347,8 +347,8 @@ describe("MatrixClient", function() {
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {
'boris': {},
'chaz': {},
'boris': [],
'chaz': [],
}});
}).respond(200, {
device_keys: {

View File

@@ -3,6 +3,7 @@ import HttpBackend from "matrix-mock-request";
import {MatrixClient} from "../../src/matrix";
import {MatrixScheduler} from "../../src/scheduler";
import {MemoryStore} from "../../src/store/memory";
import {MatrixError} from "../../src/http-api";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
@@ -132,10 +133,10 @@ describe("MatrixClient opts", function() {
});
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",
error: "Ruh roh",
});
}));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
}, function(err) {

View File

@@ -313,6 +313,10 @@ describe("Crypto", function() {
// make a room key request, and record the transaction ID for the
// sendToDevice call
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();
await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1);

View File

@@ -49,6 +49,13 @@ async function makeTestClient(userInfo, options) {
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() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
@@ -266,104 +273,259 @@ describe("Secrets", function() {
expect(secret).toBe("bar");
});
it("bootstraps when no storage or cross-signing keys locally", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
return [Object.keys(e.keys)[0], key];
});
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: getKey,
},
},
describe("bootstrap", function() {
// keys used in some of the tests
const XSK = new Uint8Array(
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
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() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
return [Object.keys(e.keys)[0], key];
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: getKey,
},
},
},
);
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey },
privateKey: storagePrivateKey,
}),
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
},
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey },
privateKey: storagePrivateKey,
}),
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.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() {
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({
sender: "@alice:example.com",
type: "es.inquisition",
@@ -172,11 +181,14 @@ describe("SAS verification", function() {
it("should verify a key", async () => {
let macMethod;
let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
.message_authentication_code;
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
.key_agreement_protocol;
}
return origSendToDevice(type, map);
};
@@ -203,6 +215,7 @@ describe("SAS verification", function() {
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
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);
});
});
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;
}
userIds.forEach((u) => {
content.device_keys[u] = {};
content.device_keys[u] = [];
});
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 * as utils 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 * as ContentHelpers from "./content-helpers";
import * as olmlib from "./crypto/olmlib";
@@ -48,6 +53,7 @@ import {keyFromAuthData} from './crypto/key_passphrase';
import {randomString} from './randomstring';
import {PushProcessor} from "./pushprocessor";
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
import { User } from "./models/user";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
@@ -731,6 +737,7 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.roomKeyRequestCancellation",
"crypto.warning",
"crypto.devicesUpdated",
"crypto.willUpdateDevices",
"deviceVerificationChanged",
"userTrustStatusChanged",
"crossSigning.keysChanged",
@@ -819,9 +826,9 @@ MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
*
* @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) {
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} 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) {
throw new Error("End-to-end encryption disabled");
}
@@ -1303,7 +1310,6 @@ wrapCryptoFuncs(MatrixClient, [
"bootstrapSecretStorage",
"addSecretStorageKey",
"hasSecretStorageKey",
"secretStorageKeyNeedsUpgrade",
"storeSecret",
"getSecret",
"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 {object} config The encryption config for the room.
* @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
*
* @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
* have been imported
*/
MatrixClient.prototype.importRoomKeys = function(keys) {
MatrixClient.prototype.importRoomKeys = function(keys, opts) {
if (!this._crypto) {
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
* 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() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
if (!this._crypto._checkedForBackup) {
return null;
}
return Boolean(this._crypto.backupKey);
};
@@ -1870,7 +1883,10 @@ MatrixClient.prototype.restoreKeyBackupWithCache = async function(
MatrixClient.prototype._restoreKeyBackup = function(
privKey, targetRoomId, targetSessionId, backupInfo,
{ cacheCompleteCallback }={}, // For sequencing during tests
{
cacheCompleteCallback, // For sequencing during tests
progressCallback,
}={},
) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
@@ -1905,6 +1921,12 @@ MatrixClient.prototype._restoreKeyBackup = function(
console.warn("Error caching session backup key:", e);
}).then(cacheCompleteCallback);
if (progressCallback) {
progressCallback({
stage: "fetch",
});
}
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData, undefined,
{prefix: PREFIX_UNSTABLE},
@@ -1939,7 +1961,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
}
}
return this.importRoomKeys(keys);
return this.importRoomKeys(keys, { progressCallback });
}).then(() => {
return this._crypto.setTrustedBackupPubKey(backupPubKey);
}).then(() => {
@@ -2075,6 +2097,7 @@ MatrixClient.prototype.getUsers = function() {
/**
* Set account data event for the current user.
* It will retry the request up to 5 times.
* @param {string} eventType The event type
* @param {Object} contents the contents object for the event
* @param {module:client.callback} callback Optional.
@@ -2086,9 +2109,13 @@ MatrixClient.prototype.setAccountData = function(eventType, contents, callback)
$userId: this.credentials.userId,
$type: eventType,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, contents,
);
const promise = retryNetworkOperation(5, () => {
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,
$type: eventType,
});
return this._http.authedRequest(
undefined, "GET", path, undefined,
);
try {
const result = await this._http.authedRequest(
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, {
event_id: "~" + roomId + ":" + txnId,
user_id: this.credentials.userId,
sender: this.credentials.userId,
room_id: roomId,
origin_server_ts: new Date().getTime(),
}));
@@ -4499,64 +4535,54 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
* @param {Filter} filter
* @return {Promise<String>} Filter ID
*/
MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
MatrixClient.prototype.getOrCreateFilter = async function(filterName, filter) {
const filterId = this.store.getFilterIdByName(filterName);
let promise = Promise.resolve();
const self = this;
let existingId = undefined;
if (filterId) {
// check that the existing filter matches our expectations
promise = self.getFilter(self.credentials.userId,
filterId, true,
).then(function(existingFilter) {
const oldDef = existingFilter.getDefinition();
const newDef = filter.getDefinition();
try {
const existingFilter =
await this.getFilter(this.credentials.userId, filterId, true);
if (existingFilter) {
const oldDef = existingFilter.getDefinition();
const newDef = filter.getDefinition();
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
// debuglog("Using existing filter ID %s: %s", filterId,
// JSON.stringify(oldDef));
return Promise.resolve(filterId);
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
// debuglog("Using existing filter ID %s: %s", filterId,
// JSON.stringify(oldDef));
existingId = filterId;
}
}
// debuglog("Existing filter ID %s: %s; new filter: %s",
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
self.store.setFilterIdByName(filterName, undefined);
return undefined;
}, function(error) {
} catch (error) {
// Synapse currently returns the following when the filter cannot be found:
// {
// errcode: "M_UNKNOWN",
// name: "M_UNKNOWN",
// message: "No row found",
// data: Object, httpStatus: 404
// }
if (error.httpStatus === 404 &&
(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 {
if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") {
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) {
return existingId;
}
if (existingId) {
return existingId;
}
// create a new filter
return self.createFilter(filter.getDefinition(),
).then(function(createdFilter) {
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
// JSON.stringify(createdFilter.getDefinition()));
self.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
});
});
// create a new filter
const createdFilter = await this.createFilter(filter.getDefinition());
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
// JSON.stringify(createdFilter.getDefinition()));
this.store.setFilterIdByName(filterName, 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) {
this._crypto.uploadDeviceKeys();
this._crypto.start();
@@ -5247,17 +5280,20 @@ function _resolve(callback, resolve, res) {
resolve(res);
}
function _PojoToMatrixEventMapper(client) {
function _PojoToMatrixEventMapper(client, options) {
const preventReEmit = Boolean(options && options.preventReEmit);
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
}
event.attemptDecryption(client._crypto);
}
const room = client.getRoom(event.getRoomId());
if (room) {
if (room && !preventReEmit) {
room.reEmitter.reEmit(event, ["Event.replaced"]);
}
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}
*/
MatrixClient.prototype.getEventMapper = function() {
return _PojoToMatrixEventMapper(this);
MatrixClient.prototype.getEventMapper = function(options = undefined) {
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()
* @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) {
const signingKeys = {};
if (keys.master) {
@@ -644,6 +651,11 @@ export function createCryptoStoreCacheCallbacks(store) {
});
},
storeCrossSigningKeyCache: function(type, key) {
if (!(key instanceof Uint8Array)) {
throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
);
}
return store.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],

View File

@@ -109,6 +109,9 @@ export class DeviceList extends EventEmitter {
this._savePromiseTime = null;
// The timer used to delay the save
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(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._hasFetched = Boolean(deviceData && deviceData.devices);
this._devices = deviceData ? deviceData.devices : {},
this._crossSigningInfo = deviceData ?
deviceData.crossSigningInfo || {} : {};
@@ -652,6 +656,7 @@ export class DeviceList extends EventEmitter {
});
const finished = (success) => {
this.emit("crypto.willUpdateDevices", users, !this._hasFetched);
users.forEach((u) => {
this._dirty = true;
@@ -677,7 +682,8 @@ export class DeviceList extends EventEmitter {
}
});
this.saveIfDirty();
this.emit("crypto.devicesUpdated", users);
this.emit("crypto.devicesUpdated", users, !this._hasFetched);
this._hasFetched = true;
};
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,
// 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.
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 " +
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
// multiple sessions for the same device at the same time.
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 = {
room_id: roomId,
session: session.pickle(this._pickleKey),

View File

@@ -97,10 +97,6 @@ export class OutgoingRoomKeyRequestManager {
*/
start() {
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
* 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
* exists)
*/
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
async queueRoomKeyRequest(requestBody, recipients, resend=false) {
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
@@ -184,7 +187,7 @@ export class OutgoingRoomKeyRequestManager {
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.sendRoomKeyRequest(
return await this.queueRoomKeyRequest(
requestBody, recipients, resend,
);
}
@@ -220,9 +223,6 @@ export class OutgoingRoomKeyRequestManager {
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
* don't really expect the other end to even care about the cancellation.
* For example, after initialization or self-verification.
* @return {Promise} An array of `sendRoomKeyRequest` outputs.
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
*/
async cancelAndResendAllOutgoingRequests() {
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
ROOM_KEY_REQUEST_STATES.SENT,
);
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
@@ -381,15 +381,12 @@ export class OutgoingRoomKeyRequestManager {
return Promise.resolve();
}
logger.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
logger.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
@@ -413,7 +410,6 @@ export class OutgoingRoomKeyRequestManager {
}).catch((e) => {
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
});
});
}

View File

@@ -17,15 +17,12 @@ limitations under the License.
import {EventEmitter} from 'events';
import {logger} from '../logger';
import * as olmlib from './olmlib';
import {pkVerify} from './olmlib';
import {randomString} from '../randomstring';
import {encryptAES, decryptAES} from './aes';
import {encodeBase64} from "./olmlib";
export const SECRET_STORAGE_ALGORITHM_V1_AES
= "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";
@@ -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
*/
export class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
constructor(baseApis, cryptoCallbacks) {
super();
this._baseApis = baseApis;
this._cryptoCallbacks = cryptoCallbacks;
this._crossSigningInfo = crossSigningInfo;
this._requests = {};
this._incomingRequests = {};
}
@@ -52,7 +48,7 @@ export class SecretStorage extends EventEmitter {
}
setDefaultKeyId(keyId) {
return new Promise((resolve) => {
return new Promise(async (resolve, reject) => {
const listener = (ev) => {
if (
ev.getType() === 'm.secret_storage.default_key' &&
@@ -64,10 +60,15 @@ export class SecretStorage extends EventEmitter {
};
this._baseApis.on('accountData', listener);
this._baseApis.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
try {
await this._baseApis.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
} catch (e) {
this._baseApis.removeListener('accountData', listener);
reject(e);
}
});
}
@@ -91,20 +92,16 @@ export class SecretStorage extends EventEmitter {
keyData.name = opts.name;
}
switch (algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
}
if (opts.key) {
const {iv, mac} = await encryptAES(ZERO_STR, opts.key, "");
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
keyData.iv = iv;
keyData.mac = mac;
}
break;
}
default:
} else {
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(
`m.secret_storage.key.${keyId}`, keyData,
);
@@ -127,33 +122,6 @@ export class SecretStorage extends EventEmitter {
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.
*
@@ -187,15 +155,6 @@ export class SecretStorage extends EventEmitter {
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
*
@@ -205,36 +164,23 @@ export class SecretStorage extends EventEmitter {
* @return {boolean} whether or not the key matches
*/
async checkKey(key, info) {
switch (info.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (info.mac) {
const {mac} = await encryptAES(ZERO_STR, key, "", info.iv);
return info.mac === mac;
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
} else {
// if we have no information, we have to assume the key is right
return true;
}
}
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:
} else {
throw new Error("Unknown algorithm");
}
}
static async _calculateKeyCheck(key, iv) {
return await encryptAES(ZERO_STR, key, "", iv);
}
/**
* Store an encrypted secret on the server
*
@@ -268,15 +214,11 @@ export class SecretStorage extends EventEmitter {
}
// encrypt secret, based on the algorithm
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const keys = {[keyId]: keyInfo};
const [, encryption] = await this._getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret);
break;
}
default:
} else {
logger.warn("unknown algorithm for secret storage key " + keyId
+ ": " + keyInfo.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,
);
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
// only use keys we understand the encryption algorithm of
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
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];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return decryption.get_private_key();
// since we just want to return the key itself. It must be base64
// 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);
} finally {
@@ -407,8 +337,7 @@ export class SecretStorage extends EventEmitter {
const ret = {};
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
@@ -417,49 +346,11 @@ export class SecretStorage extends EventEmitter {
if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
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:
// only use keys we understand the encryption algorithm of
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
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;
@@ -586,7 +477,7 @@ export class SecretStorage extends EventEmitter {
this._baseApis,
{
[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._crypto._olmDevice,
sender,
this._baseApis._crypto.getStoredDevice(sender, deviceId),
this._baseApis.getStoredDevice(sender, deviceId),
payload,
);
const contentMap = {
@@ -663,9 +554,7 @@ export class SecretStorage extends EventEmitter {
throw new Error("App returned unknown key from getSecretStorageKey!");
}
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: async function(secret) {
return await encryptAES(secret, privateKey, name);
@@ -675,36 +564,7 @@ export class SecretStorage extends EventEmitter {
},
};
return [keyId, decryption];
}
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:
} else {
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 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`);
}

View File

@@ -311,7 +311,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices,
session, key, payload, retryDevices, failedDevices, 30000,
);
await this._notifyFailedOlmDevices(session, key, failedDevices);
@@ -521,6 +521,33 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
}
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(() => {
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
@@ -1296,7 +1323,6 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
logger.log(`Received and adding key for megolm session ${senderKey}|${sessionId}`);
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
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
* @param {String} senderKey
@@ -1574,21 +1601,17 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
return true;
}
pending.delete(sessionId);
if (pending.size === 0) {
this._pendingEvents[senderKey];
}
logger.debug("Retrying decryption on events", [...pending]);
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this._crypto);
await ev.attemptDecryption(this._crypto, true);
} catch (e) {
// don't die if something goes wrong
}
}));
// ev.attemptDecryption will re-add to this._pendingEvents if an event
// couldn't be decrypted
// If decrypted successfully, they'll have been removed from _pendingEvents
return !((this._pendingEvents[senderKey] || {})[sessionId]);
};

View File

@@ -264,6 +264,25 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
*/
OlmDecryption.prototype._decryptMessage = async function(
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(
theirDeviceIdentityKey,

View File

@@ -169,7 +169,9 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
this._deviceList.on(
'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
// server.
@@ -223,6 +225,11 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
this._toDeviceVerificationRequests = new ToDeviceRequests();
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 cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore);
@@ -233,7 +240,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
);
this._secretStorage = new SecretStorage(
baseApis, cryptoCallbacks, this._crossSigningInfo,
baseApis, cryptoCallbacks,
);
// Assuming no app-supplied callback, default to getting from SSSS.
@@ -306,7 +313,8 @@ Crypto.prototype.init = async function(opts) {
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
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");
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
* 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
* called to await an interactive auth flow when uploading device signing keys.
* Args:
@@ -501,172 +517,160 @@ Crypto.prototype.bootstrapSecretStorage = async function({
return key;
};
// create a new SSSS key and set it as default
const createSSSS = async (opts, privateKey) => {
opts = opts || {};
if (privateKey) {
opts.key = privateKey;
}
const keyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
await this.setDefaultSecretStorageKeyId(keyId);
if (privateKey) {
// cache the private key so that we can access it again
ssssKeys[keyId] = privateKey;
}
return keyId;
};
// reset the cross-signing keys
const resetCrossSigning = async () => {
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
keys => Object.assign(crossSigningPrivateKeys, keys);
this._baseApis._cryptoCallbacks.getCrossSigningKey =
name => crossSigningPrivateKeys[name];
await this.resetCrossSigningKeys(
CrossSigningLevel.MASTER,
{ authUploadDeviceSigningKeys },
);
};
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,
);
}
}
};
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 (decryptionKeys && !(Object.values(decryptionKeys).some(
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 = {};
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,
);
if (oldKeyId) {
ssssKeys[newKeyId] = ssssKeys[oldKeyId];
}
await this.setDefaultSecretStorageKeyId(newKeyId);
// re-encrypt all the keys with the new key
for (const type of ["master", "self_signing", "user_signing"]) {
const secretName = `m.cross_signing.${type}`;
await this.storeSecret(secretName, keys[type], [newKeyId]);
}
} else if (!this._crossSigningInfo.getId() || !inStorage) {
// create new cross-signing keys if necessary.
if (!inStorage && !keyBackupInfo) {
// either we don't have anything, or we've been asked to restart
// from scratch
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, " +
"Cross-signing private keys not found in secret storage, " +
"creating new keys",
);
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
keys => Object.assign(crossSigningPrivateKeys, keys);
this._baseApis._cryptoCallbacks.getCrossSigningKey =
name => crossSigningPrivateKeys[name];
await this.resetCrossSigningKeys(
CrossSigningLevel.MASTER,
{ authUploadDeviceSigningKeys },
);
);
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);
}
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
} else if (!inStorage && keyBackupInfo) {
// we have an existing backup, but no SSSS
logger.log("Secret storage default key not found, using key backup key");
// if we have the backup key already cached, use it; otherwise use the
// callback to prompt for the key
const backupKey = await this.getSessionBackupPrivateKey() ||
await getKeyBackupPassphrase();
// create new cross-signing keys
await resetCrossSigning();
// create a new SSSS key and use the backup key as the new SSSS key
const opts = {};
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
bits: 256,
};
}
newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage
await this.storeSecret(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
logger.log("Adding cross signing signature to key backup");
await this._crossSigningInfo.signObject(
keyBackupInfo.auth_data, "master",
);
await this._baseApis._http.authedRequest(
undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version,
undefined, keyBackupInfo,
{prefix: httpApi.PREFIX_UNSTABLE},
);
} else if (!this._crossSigningInfo.getId()) {
// we have SSSS, but we don't know if the server's cross-signing
// keys should be trusted
logger.log("Cross-signing private keys found in secret storage");
// fetch the private keys and set up our local copy of the keys for
// use
await this.checkOwnCrossSigningTrust();
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
// make sure that the default key has the information needed to
// check the passphrase
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
} else {
// we have SSSS and we cross-signing is already set up
logger.log("Cross signing keys are present in secret storage");
}
// Check if we need to create a new secret storage key
// - we're resetting secret storage
// - we don't have a default secret storage key yet
// - our default secret storage key is using an older algorithm
// We will also run this part if we created a new secret storage key
// 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");
const backupKey = await getKeyBackupPassphrase();
if (!newKeyId) {
const opts = {};
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
bits: 256,
};
}
// use the backup key as the new ssss key
ssssKeys[newKeyId] = backupKey;
opts.key = backupKey;
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
await this.setDefaultSecretStorageKeyId(newKeyId);
}
// if this key backup is trusted, sign it with the cross signing key
// so the key backup can be trusted via cross-signing.
const backupSigStatus = await this.checkKeyBackup(keyBackupInfo);
if (backupSigStatus.trustInfo.usable) {
logger.log("Adding cross signing signature to key backup");
await this._crossSigningInfo.signObject(
keyBackupInfo.auth_data, "master",
);
await this._baseApis._http.authedRequest(
undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version,
undefined, keyBackupInfo,
{prefix: httpApi.PREFIX_UNSTABLE},
);
await this.storeSecret(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
} else {
logger.log(
"Key backup is NOT TRUSTED: NOT adding cross signing signature",
);
}
} else {
if (!newKeyId) {
logger.log("Secret storage default key not found, creating new key");
const { keyInfo, privateKey } = await createSecretStorageKey();
if (keyInfo && privateKey) {
keyInfo.key = privateKey;
}
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]);
}
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 {
logger.log("Have secret storage key");
}
// 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
if (Object.keys(crossSigningPrivateKeys).length) {
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.
if (!appCallbacks.saveCrossSigningKeys) {
await CrossSigningInfo.storeInSecretStorage(
@@ -711,7 +710,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({
const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
if (sessionBackupKey) {
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 {
// 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);
};
Crypto.prototype.secretStorageKeyNeedsUpgrade = function(keyID) {
return this._secretStorage.keyNeedsUpgrade(keyID);
};
Crypto.prototype.getSecretStorageKey = function(keyID) {
return this._secretStorage.getKey(keyID);
};
@@ -800,7 +797,7 @@ Crypto.prototype.checkSecretStoragePrivateKey = function(privateKey, expectedPub
* Fetches the backup private key, if cached
* @returns {Promise} the key, if any, or null
*/
Crypto.prototype.getSessionBackupPrivateKey = async function() {
Crypto.prototype.getSessionBackupPrivateKey = function() {
return new Promise((resolve) => {
this._cryptoStore.doTxn(
'readonly',
@@ -822,6 +819,9 @@ Crypto.prototype.getSessionBackupPrivateKey = async function() {
* @returns {Promise} so you can catch failures
*/
Crypto.prototype.storeSessionBackupPrivateKey = async function(key) {
if (!(key instanceof Uint8Array)) {
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
}
return this._cryptoStore.doTxn(
'readwrite',
[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
await this.checkOwnCrossSigningTrust();
} else {
this.emit("crossSigning.keysChanged", {});
// 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
// on the server for our account. The app must call checkOwnCrossSigningTrust()
// to fix this.
// XXX: Do we need to do something to emit events saying every device has become
// untrusted?
// on the server for our account. So we clear our own stored cross-signing keys,
// effectively disabling cross-signing until the user gets verified by the device
// that reset the keys
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 {
await this._checkDeviceVerifications(userId);
@@ -1303,7 +1308,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
* @param {object} keys The new trusted set of keys
*/
Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
this._crossSigningInfo.setKeys(keys);
if (keys) {
this._crossSigningInfo.setKeys(keys);
} else {
this._crossSigningInfo.clearKeys();
}
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
@@ -1368,8 +1377,9 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
backupInfo = await this._baseApis.getKeyBackupVersion();
} catch (e) {
logger.log("Error checking for active key backup", e);
if (e.httpStatus / 100 === 4) {
// well that's told us. we won't try again.
if (e.httpStatus === 404) {
// 404 is returned when the key backup does not exist, so that
// counts as successfully checking.
this._checkedForBackup = true;
}
return null;
@@ -1912,6 +1922,10 @@ Crypto.prototype.setDeviceVerification = async function(
if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) {
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)
@@ -2054,7 +2068,8 @@ Crypto.prototype.requestVerification = function(userId, devices) {
if (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(
userId,
channel,
@@ -2067,6 +2082,10 @@ Crypto.prototype._requestVerificationWithChannel = async function(
) {
let request = new VerificationRequest(
channel, this._verificationMethods, this._baseApis);
// if transaction id is already known, add request
if (channel.transactionId) {
requestsMap.setRequestByChannel(channel, request);
}
await request.sendRequest();
// don't replace the request created by a racing remote echo
const racingRequest = requestsMap.getRequestByChannel(channel);
@@ -2434,17 +2453,37 @@ Crypto.prototype.exportRoomKeys = async function() {
* Import a list of room keys previously exported by exportRoomKeys
*
* @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
*/
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) => {
if (!key.room_id || !key.algorithm) {
logger.warn("ignoring room key entry with missing fields", key);
failures++;
if (opts.progressCallback) { updateProgress(); }
return null;
}
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
*/
Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) {
return this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
return this._outgoingRoomKeyRequestManager.queueRoomKeyRequest(
requestBody, recipients, resend,
).catch((e) => {
).then(() => {
if (this._sendKeyRequestsImmediately) {
this._outgoingRoomKeyRequestManager.sendQueuedRequests();
}
}).catch((e) => {
// this normally means we couldn't talk to the store
logger.error(
'Error requesting key for event', e,
@@ -2827,6 +2870,8 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) {
this._deviceList.startTrackingDeviceList(this._userId);
this._roomDeviceTrackingState = {};
}
this._sendKeyRequestsImmediately = false;
};
/**
@@ -2858,6 +2903,14 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
if (!syncData.catchingUp) {
_maybeUploadOneTimeKeys(this);
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;
}
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,
// in case they turn up later?

View File

@@ -207,6 +207,25 @@ export async function ensureOlmSessionsForDevices(
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
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]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
@@ -238,6 +257,11 @@ export async function ensureOlmSessionsForDevices(
delete resolveSession[key];
}
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]);
}
result[userId][deviceId] = {
@@ -277,6 +301,14 @@ export async function ensureOlmSessionsForDevices(
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
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) {
// we already have a result for this device
continue;

View File

@@ -59,8 +59,8 @@ export function decodeRecoveryKey(recoverykey) {
throw new Error("Incorrect length");
}
return result.slice(
return Uint8Array.from(result.slice(
OLM_RECOVERY_KEY_PREFIX.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) {
return Promise.reject(new Error("Verification is already done"));
}
const existingEvent = this.request.getEventFromOtherParty(type);
if (existingEvent) {
return Promise.resolve(existingEvent);
}
this._expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
@@ -287,6 +292,7 @@ export class VerificationBase extends EventEmitter {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
this.request.onVerifierCancelled();
if (this.userId && this.deviceId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
@@ -369,7 +375,7 @@ export class VerificationBase extends EventEmitter {
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = await this._baseApis.getStoredDevice(userId, deviceId);
const device = this._baseApis.getStoredDevice(userId, deviceId);
if (device) {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);

View File

@@ -65,20 +65,29 @@ export class ReciprocateQRCode extends Base {
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
});
// 3. determine key to sign
// 3. determine key to sign / mark as trusted
const keys = {};
if (qrCodeData.mode === MODE_VERIFY_OTHER_USER) {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
} else if (qrCodeData.mode === MODE_VERIFY_SELF_TRUSTED) {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
} else {
// TODO: not sure if MODE_VERIFY_SELF_UNTRUSTED makes sense to sign anything here?
switch (qrCodeData.mode) {
case MODE_VERIFY_OTHER_USER: {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
case MODE_VERIFY_SELF_TRUSTED: {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
break;
}
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) => {
// make sure the device has the expected keys
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
export class QRCodeData {
constructor(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, buffer) {
constructor(
mode, sharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey, buffer,
) {
this._sharedSecret = sharedSecret;
this._mode = mode;
this._otherUserMasterKey = otherUserMasterKey;
this._otherDeviceKey = otherDeviceKey;
this._myMasterKey = myMasterKey;
this._buffer = buffer;
}
@@ -121,22 +134,28 @@ export class QRCodeData {
const mode = QRCodeData._determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
let myMasterKey = null;
if (mode === MODE_VERIFY_OTHER_USER) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
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(
request, client, mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
myMasterKey,
);
const buffer = QRCodeData._generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, buffer);
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
}
get buffer() {
@@ -147,14 +166,30 @@ export class QRCodeData {
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() {
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() {
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.
*/
@@ -172,7 +207,7 @@ export class QRCodeData {
const myUserId = client.getUserId();
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = await client.getStoredDevice(myUserId, otherDeviceId);
const device = client.getStoredDevice(myUserId, otherDeviceId);
if (!device) {
throw new Error("could not find device " + otherDeviceId);
}
@@ -198,7 +233,8 @@ export class QRCodeData {
}
static _generateQrData(request, client, mode,
encodedSharedSecret, otherUserMasterKey, otherDeviceKey,
encodedSharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey,
) {
const myUserId = client.getUserId();
const transactionId = request.channel.transactionId;
@@ -213,16 +249,15 @@ export class QRCodeData {
};
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
const myMasterKey = myCrossSigningInfo.getId("master");
if (mode === MODE_VERIFY_OTHER_USER) {
// 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
qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrData.firstKeyB64 = myMasterKey;
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// 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,
* and MAC lists should be sorted in order of preference (most preferred
* first).
*/
const KEY_AGREEMENT_LIST = ["curve25519"];
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators);
@@ -291,12 +313,14 @@ export class SAS extends Base {
if (typeof content.commitment !== "string") {
throw newInvalidMessageError();
}
const keyAgreement = content.key_agreement_protocol;
const macMethod = content.message_authentication_code;
const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS();
try {
this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
this.ourSASPubKey = 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) {
throw newMismatchedCommitmentError();
}
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
@@ -377,7 +402,7 @@ export class SAS extends Base {
const olmSAS = new global.Olm.SAS();
try {
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,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -390,22 +415,24 @@ export class SAS extends Base {
let e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
key: this.ourSASPubKey,
});
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch(err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
@@ -461,7 +488,7 @@ export class SAS extends Base {
keyList.sort().join(","),
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) {

View File

@@ -73,6 +73,7 @@ export class VerificationRequest extends EventEmitter {
this._accepting = false;
this._declining = false;
this._verifierHasFinished = false;
this._cancelled = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
@@ -525,7 +526,7 @@ export class VerificationRequest extends EventEmitter {
}
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});
return transitions;
}
@@ -858,6 +859,15 @@ export class VerificationRequest extends EventEmitter {
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() {
this.channel.send("m.key.verification.done", {});
this._verifierHasFinished = true;
@@ -867,4 +877,8 @@ export class VerificationRequest extends EventEmitter {
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];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
if (allowed_values && allowed_values.length > 0) {
const anyMatch = allowed_values.some(match_func);
if (!anyMatch) {
return false;
}
}

View File

@@ -56,13 +56,6 @@ Filter.LAZY_LOADING_MESSAGES_FILTER = {
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)
* @return {?Number} The filter ID
@@ -96,6 +89,7 @@ Filter.prototype.setDefinition = function(definition) {
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// "lazy_load_members": true,
// },
// "timeline": {
// "limit": 10,
@@ -177,6 +171,10 @@ Filter.prototype.setTimelineLimit = function(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.
* @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);
var resp;
try {
if (xhr.status === 0) {
throw new AbortError();
}
if (!xhr.responseText) {
throw new Error('No response body.');
}
@@ -789,6 +792,17 @@ const requestCallback = function(
userDefinedCallback = userDefinedCallback || function() {};
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) {
try {
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 {integer} httpStatus The numeric HTTP status code given
*/
export function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
export class MatrixError extends Error {
constructor(errorJson) {
errorJson = errorJson || {};
super(`MatrixError: ${errorJson.errcode}`);
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
}
}
/**
* 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;
}
MatrixError.prototype = Object.create(Error.prototype);
MatrixError.prototype.constructor = MatrixError;

View File

@@ -357,7 +357,13 @@ InteractiveAuth.prototype = {
error.data.session = this._data.session;
}
this._data = error.data;
this._startNextAuthStage();
try {
this._startNextAuthStage();
} catch (e) {
this._rejectFunc(e);
this._resolveFunc = null;
this._rejectFunc = null;
}
if (
!this._emailSid &&

View File

@@ -16,8 +16,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type Request from "request";
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 {StubStore} from "./store/stub";
import {LocalIndexedDBStoreBackend} from "./store/indexeddb-local-backend";
import {RemoteIndexedDBStoreBackend} from "./store/indexeddb-remote-backend";
import {MatrixScheduler} from "./scheduler";
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;
@@ -102,6 +113,15 @@ export function setCryptoStoreFactory(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}
* 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
* <code>opts</code>.
*/
export function createClient(opts) {
export function createClient(opts: ICreateClientOpts | string) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts,
"baseUrl": opts as string,
};
}
opts.request = opts.request || requestInstance;

View File

@@ -390,11 +390,12 @@ utils.extend(MatrixEvent.prototype, {
* @internal
*
* @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
* attempt is completed.
*/
attemptDecryption: async function(crypto) {
attemptDecryption: async function(crypto, isRetry) {
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
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.
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;
}
this._decryptionPromise = this._decryptionLoop(crypto);
this._decryptionPromise = this._decryptionLoop(crypto, isRetry);
return this._decryptionPromise;
},
@@ -469,7 +470,7 @@ utils.extend(MatrixEvent.prototype, {
return recipients;
},
_decryptionLoop: async function(crypto) {
_decryptionLoop: async function(crypto, isRetry) {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before*
// 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");
} else {
res = await crypto.decryptEvent(this);
if (isRetry) {
logger.info(`Decrypted event on retry (id=${this.getId()})`);
}
}
} catch (e) {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
const re = isRetry ? 're' : '';
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._retryDecryption = false;

View File

@@ -209,7 +209,7 @@ utils.inherits(Room, EventEmitter);
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
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';
}
const ver = createEvent.getContent()['room_version'];
@@ -675,7 +675,7 @@ Room.prototype.hasUnverifiedDevices = async function() {
}
const e2eMembers = await this.getEncryptionTargetMembers();
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())) {
return true;
}

View File

@@ -23,9 +23,8 @@ import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
// The default override rules to apply when calculating actions for an event. These
// defaults apply under no other circumstances to avoid confusing the client with server
// state. We do this for two reasons:
// The default override rules to apply to the push rules that arrive from the server.
// We do this for two reasons:
// 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
// more details.
@@ -364,33 +363,6 @@ export function PushProcessor(client) {
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) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
@@ -410,8 +382,7 @@ export function PushProcessor(client) {
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
const rules = applyRuleDefaults(client.pushRules);
return pushActionsForEventAndRulesets(ev, rules);
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
/**
@@ -476,18 +447,25 @@ PushProcessor.rewriteDefaultRules = function(incomingRules) {
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Fix default override rules
newRules.global.override = newRules.global.override.map(r => {
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
if (!defaultRule) return r;
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
r.default = defaultRule.default;
r.conditions = defaultRule.conditions;
r.actions = defaultRule.actions;
return r;
});
if (existingRule) {
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
existingRule.default = override.default;
existingRule.conditions = override.conditions;
existingRule.actions = override.actions;
} else {
// Add the rule
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return newRules;
};

View File

@@ -25,6 +25,15 @@ limitations under the License.
import {User} from "../models/user";
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.
* @constructor
@@ -273,8 +282,17 @@ MemoryStore.prototype = {
if (!this.localStorage) {
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 {
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
const value = this.localStorage.getItem(key);
if (isValidFilterId(value)) {
return value;
}
} catch (e) {}
return null;
},
@@ -288,8 +306,13 @@ MemoryStore.prototype = {
if (!this.localStorage) {
return;
}
const key = "mxjssdk_memory_filter_" + filterName;
try {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
if (isValidFilterId(filterId)) {
this.localStorage.setItem(key, filterId);
} else {
this.localStorage.removeItem(key);
}
} catch (e) {}
},

View File

@@ -511,6 +511,12 @@ SyncApi.prototype.sync = function() {
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 () => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && client.isGuest()) {
@@ -520,19 +526,11 @@ SyncApi.prototype.sync = function() {
debuglog("Checking server lazy load support...");
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
try {
debuglog("Creating and storing lazy load sync filter...");
this.opts.filter = await client.createFilter(
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;
debuglog("Enabling lazy load on sync filter...");
if (!this.opts.filter) {
this.opts.filter = buildDefaultFilter();
}
this.opts.filter.setLazyLoadMembers(true);
} else {
debuglog("LL: lazy loading requested but not supported " +
"by server, so disabling");
@@ -575,8 +573,7 @@ SyncApi.prototype.sync = function() {
if (self.opts.filter) {
filter = self.opts.filter;
} else {
filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit);
filter = buildDefaultFilter();
}
let filterId;

View File

@@ -21,6 +21,7 @@ limitations under the License.
*/
import unhomoglyph from 'unhomoglyph';
import {ConnectionError} from "./http-api";
/**
* 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.
* @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));
}

View File

@@ -2,6 +2,7 @@
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",

831
yarn.lock

File diff suppressed because it is too large Load Diff