You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Typescriptify crypto integration tests (#2508)
This commit is contained in:
@@ -39,8 +39,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
|||||||
export class TestClient {
|
export class TestClient {
|
||||||
public readonly httpBackend: MockHttpBackend;
|
public readonly httpBackend: MockHttpBackend;
|
||||||
public readonly client: MatrixClient;
|
public readonly client: MatrixClient;
|
||||||
private deviceKeys: IDeviceKeys;
|
public deviceKeys: IDeviceKeys;
|
||||||
private oneTimeKeys: Record<string, IOneTimeKey>;
|
public oneTimeKeys: Record<string, IOneTimeKey>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly userId?: string,
|
public readonly userId?: string,
|
||||||
|
@@ -1,758 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* This file consists of a set of integration tests which try to simulate
|
|
||||||
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
|
||||||
*
|
|
||||||
* Note that megolm (group) conversation is not tested here.
|
|
||||||
*
|
|
||||||
* See also `megolm.spec.js`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// load olm before the sdk if possible
|
|
||||||
import '../olm-loader';
|
|
||||||
|
|
||||||
import { logger } from '../../src/logger';
|
|
||||||
import * as testUtils from "../test-utils/test-utils";
|
|
||||||
import { TestClient } from "../TestClient";
|
|
||||||
import { CRYPTO_ENABLED } from "../../src/client";
|
|
||||||
|
|
||||||
let aliTestClient;
|
|
||||||
const roomId = "!room:localhost";
|
|
||||||
const aliUserId = "@ali:localhost";
|
|
||||||
const aliDeviceId = "zxcvb";
|
|
||||||
const aliAccessToken = "aseukfgwef";
|
|
||||||
let bobTestClient;
|
|
||||||
const bobUserId = "@bob:localhost";
|
|
||||||
const bobDeviceId = "bvcxz";
|
|
||||||
const bobAccessToken = "fewgfkuesa";
|
|
||||||
let aliMessages;
|
|
||||||
let bobMessages;
|
|
||||||
|
|
||||||
function bobUploadsDeviceKeys() {
|
|
||||||
bobTestClient.expectDeviceKeyUpload();
|
|
||||||
return Promise.all([
|
|
||||||
bobTestClient.client.uploadKeys(),
|
|
||||||
bobTestClient.httpBackend.flush(),
|
|
||||||
]).then(() => {
|
|
||||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an expectation that ali will query bobs keys; then flush the http request.
|
|
||||||
*
|
|
||||||
* @return {promise} resolves once the http request has completed.
|
|
||||||
*/
|
|
||||||
function expectAliQueryKeys() {
|
|
||||||
// can't query keys before bob has uploaded them
|
|
||||||
expect(bobTestClient.deviceKeys).toBeTruthy();
|
|
||||||
|
|
||||||
const bobKeys = {};
|
|
||||||
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
const result = {};
|
|
||||||
result[bobUserId] = bobKeys;
|
|
||||||
return { device_keys: result };
|
|
||||||
});
|
|
||||||
return aliTestClient.httpBackend.flush("/keys/query", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an expectation that bob will query alis keys; then flush the http request.
|
|
||||||
*
|
|
||||||
* @return {promise} which resolves once the http request has completed.
|
|
||||||
*/
|
|
||||||
function expectBobQueryKeys() {
|
|
||||||
// can't query keys before ali has uploaded them
|
|
||||||
expect(aliTestClient.deviceKeys).toBeTruthy();
|
|
||||||
|
|
||||||
const aliKeys = {};
|
|
||||||
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
|
|
||||||
logger.log("query result will be", aliKeys);
|
|
||||||
|
|
||||||
bobTestClient.httpBackend.when(
|
|
||||||
"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),
|
|
||||||
);
|
|
||||||
const result = {};
|
|
||||||
result[aliUserId] = aliKeys;
|
|
||||||
return { device_keys: result };
|
|
||||||
});
|
|
||||||
return bobTestClient.httpBackend.flush("/keys/query", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
|
||||||
*
|
|
||||||
* @return {promise} resolves once the http request has completed.
|
|
||||||
*/
|
|
||||||
function expectAliClaimKeys() {
|
|
||||||
return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
|
||||||
aliTestClient.httpBackend.when(
|
|
||||||
"POST", "/keys/claim",
|
|
||||||
).respond(200, function(path, content) {
|
|
||||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
|
||||||
expect(claimType).toEqual("signed_curve25519");
|
|
||||||
let keyId = null;
|
|
||||||
for (keyId in keys) {
|
|
||||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
|
||||||
if (keyId.indexOf(claimType + ":") === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = {};
|
|
||||||
result[bobUserId] = {};
|
|
||||||
result[bobUserId][bobDeviceId] = {};
|
|
||||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
|
||||||
return { one_time_keys: result };
|
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
// it can take a while to process the key query, so give it some extra
|
|
||||||
// time, and make sure the claim actually happens rather than ploughing on
|
|
||||||
// confusingly.
|
|
||||||
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
|
|
||||||
expect(r).toEqual(1, "Ali did not claim Bob's keys");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function aliDownloadsKeys() {
|
|
||||||
// can't query keys before bob has uploaded them
|
|
||||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
|
||||||
|
|
||||||
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
|
|
||||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
|
||||||
}).then((devices) => {
|
|
||||||
expect(devices.length).toEqual(1);
|
|
||||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
|
||||||
});
|
|
||||||
const p2 = expectAliQueryKeys();
|
|
||||||
|
|
||||||
// check that the localStorage is updated as we expect (not sure this is
|
|
||||||
// an integration test, but meh)
|
|
||||||
return Promise.all([p1, p2]).then(() => {
|
|
||||||
return aliTestClient.client.crypto.deviceList.saveIfDirty();
|
|
||||||
}).then(() => {
|
|
||||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
|
||||||
const devices = data.devices[bobUserId];
|
|
||||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
|
||||||
expect(devices[bobDeviceId].verified).
|
|
||||||
toBe(0); // DeviceVerification.UNVERIFIED
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function aliEnablesEncryption() {
|
|
||||||
return aliTestClient.client.setRoomEncryption(roomId, {
|
|
||||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
|
||||||
}).then(function() {
|
|
||||||
expect(aliTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bobEnablesEncryption() {
|
|
||||||
return bobTestClient.client.setRoomEncryption(roomId, {
|
|
||||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
|
||||||
}).then(function() {
|
|
||||||
expect(bobTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
|
||||||
* check the results.
|
|
||||||
*
|
|
||||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
|
||||||
*/
|
|
||||||
function aliSendsFirstMessage() {
|
|
||||||
return Promise.all([
|
|
||||||
sendMessage(aliTestClient.client),
|
|
||||||
expectAliQueryKeys()
|
|
||||||
.then(expectAliClaimKeys)
|
|
||||||
.then(expectAliSendMessageRequest),
|
|
||||||
]).then(function([_, ciphertext]) {
|
|
||||||
return ciphertext;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
|
||||||
* and check the results.
|
|
||||||
*
|
|
||||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
|
||||||
*/
|
|
||||||
function aliSendsMessage() {
|
|
||||||
return Promise.all([
|
|
||||||
sendMessage(aliTestClient.client),
|
|
||||||
expectAliSendMessageRequest(),
|
|
||||||
]).then(function([_, ciphertext]) {
|
|
||||||
return ciphertext;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
|
||||||
* expectations and check the results.
|
|
||||||
*
|
|
||||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
|
||||||
*/
|
|
||||||
function bobSendsReplyMessage() {
|
|
||||||
return Promise.all([
|
|
||||||
sendMessage(bobTestClient.client),
|
|
||||||
expectBobQueryKeys()
|
|
||||||
.then(expectBobSendMessageRequest),
|
|
||||||
]).then(function([_, ciphertext]) {
|
|
||||||
return ciphertext;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an expectation that Ali will send a message, and flush the request
|
|
||||||
*
|
|
||||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
|
||||||
*/
|
|
||||||
function expectAliSendMessageRequest() {
|
|
||||||
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
|
|
||||||
aliMessages.push(content);
|
|
||||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
|
||||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
|
||||||
expect(ciphertext).toBeTruthy();
|
|
||||||
return ciphertext;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an expectation that Bob will send a message, and flush the request
|
|
||||||
*
|
|
||||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
|
||||||
*/
|
|
||||||
function expectBobSendMessageRequest() {
|
|
||||||
return expectSendMessageRequest(bobTestClient.httpBackend).then(function(content) {
|
|
||||||
bobMessages.push(content);
|
|
||||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
|
||||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
|
||||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
|
||||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
|
||||||
expect(ciphertext).toBeTruthy();
|
|
||||||
return ciphertext;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage(client) {
|
|
||||||
return client.sendMessage(
|
|
||||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectSendMessageRequest(httpBackend) {
|
|
||||||
const path = "/send/m.room.encrypted/";
|
|
||||||
const prom = new Promise((resolve) => {
|
|
||||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
|
||||||
resolve(content);
|
|
||||||
return {
|
|
||||||
event_id: "asdfgh",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// it can take a while to process the key query
|
|
||||||
return httpBackend.flush(path, 1).then(() => prom);
|
|
||||||
}
|
|
||||||
|
|
||||||
function aliRecvMessage() {
|
|
||||||
const message = bobMessages.shift();
|
|
||||||
return recvMessage(
|
|
||||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bobRecvMessage() {
|
|
||||||
const message = aliMessages.shift();
|
|
||||||
return recvMessage(
|
|
||||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function recvMessage(httpBackend, client, sender, message) {
|
|
||||||
const syncData = {
|
|
||||||
next_batch: "x",
|
|
||||||
rooms: {
|
|
||||||
join: {
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
syncData.rooms.join[roomId] = {
|
|
||||||
timeline: {
|
|
||||||
events: [
|
|
||||||
testUtils.mkEvent({
|
|
||||||
type: "m.room.encrypted",
|
|
||||||
room: roomId,
|
|
||||||
content: message,
|
|
||||||
sender: sender,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
|
||||||
|
|
||||||
const eventPromise = new Promise((resolve, reject) => {
|
|
||||||
const onEvent = function(event) {
|
|
||||||
// ignore the m.room.member events
|
|
||||||
if (event.getType() == "m.room.member") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.log(client.credentials.userId + " received event",
|
|
||||||
event);
|
|
||||||
|
|
||||||
client.removeListener("event", onEvent);
|
|
||||||
resolve(event);
|
|
||||||
};
|
|
||||||
client.on("event", onEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
httpBackend.flush();
|
|
||||||
|
|
||||||
return eventPromise.then((event) => {
|
|
||||||
expect(event.isEncrypted()).toBeTruthy();
|
|
||||||
|
|
||||||
// it may still be being decrypted
|
|
||||||
return testUtils.awaitDecryption(event);
|
|
||||||
}).then((event) => {
|
|
||||||
expect(event.getType()).toEqual("m.room.message");
|
|
||||||
expect(event.getContent()).toMatchObject({
|
|
||||||
msgtype: "m.text",
|
|
||||||
body: "Hello, World",
|
|
||||||
});
|
|
||||||
expect(event.isEncrypted()).toBeTruthy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an initial sync response to the client (which just includes the member
|
|
||||||
* list for our test room).
|
|
||||||
*
|
|
||||||
* @param {TestClient} testClient
|
|
||||||
* @returns {Promise} which resolves when the sync has been flushed.
|
|
||||||
*/
|
|
||||||
function firstSync(testClient) {
|
|
||||||
// send a sync response including our test room.
|
|
||||||
const syncData = {
|
|
||||||
next_batch: "x",
|
|
||||||
rooms: {
|
|
||||||
join: { },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
syncData.rooms.join[roomId] = {
|
|
||||||
state: {
|
|
||||||
events: [
|
|
||||||
testUtils.mkMembership({
|
|
||||||
mship: "join",
|
|
||||||
user: aliUserId,
|
|
||||||
}),
|
|
||||||
testUtils.mkMembership({
|
|
||||||
mship: "join",
|
|
||||||
user: bobUserId,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
timeline: {
|
|
||||||
events: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
|
||||||
return testClient.flushSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("MatrixClient crypto", function() {
|
|
||||||
if (!CRYPTO_ENABLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async function() {
|
|
||||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
|
||||||
await aliTestClient.client.initCrypto();
|
|
||||||
|
|
||||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
|
||||||
await bobTestClient.client.initCrypto();
|
|
||||||
|
|
||||||
aliMessages = [];
|
|
||||||
bobMessages = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
|
||||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
|
||||||
|
|
||||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Bob uploads device keys", function() {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(bobUploadsDeviceKeys);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali downloads Bobs device keys", function() {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(bobUploadsDeviceKeys)
|
|
||||||
.then(aliDownloadsKeys);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali gets keys with an invalid signature", function() {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(bobUploadsDeviceKeys)
|
|
||||||
.then(function() {
|
|
||||||
// tamper bob's keys
|
|
||||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
|
||||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
|
||||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
aliTestClient.client.downloadKeys([bobUserId]),
|
|
||||||
expectAliQueryKeys(),
|
|
||||||
]);
|
|
||||||
}).then(function() {
|
|
||||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
|
||||||
}).then((devices) => {
|
|
||||||
// should get an empty list
|
|
||||||
expect(devices).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali gets keys with an incorrect userId", function() {
|
|
||||||
const eveUserId = "@eve:localhost";
|
|
||||||
|
|
||||||
const bobDeviceKeys = {
|
|
||||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
|
||||||
device_id: 'bvcxz',
|
|
||||||
keys: {
|
|
||||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
|
||||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
|
||||||
},
|
|
||||||
user_id: '@eve:localhost',
|
|
||||||
signatures: {
|
|
||||||
'@eve:localhost': {
|
|
||||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
|
||||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const bobKeys = {};
|
|
||||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
|
||||||
aliTestClient.httpBackend.when(
|
|
||||||
"POST", "/keys/query",
|
|
||||||
).respond(200, function(path, content) {
|
|
||||||
const result = {};
|
|
||||||
result[bobUserId] = bobKeys;
|
|
||||||
return { device_keys: result };
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
|
||||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
|
||||||
]).then(function() {
|
|
||||||
return Promise.all([
|
|
||||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
|
||||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
|
||||||
]);
|
|
||||||
}).then(([bobDevices, eveDevices]) => {
|
|
||||||
// should get an empty list
|
|
||||||
expect(bobDevices).toEqual([]);
|
|
||||||
expect(eveDevices).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali gets keys with an incorrect deviceId", function() {
|
|
||||||
const bobDeviceKeys = {
|
|
||||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
|
||||||
device_id: 'bad_device',
|
|
||||||
keys: {
|
|
||||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
|
||||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
|
||||||
},
|
|
||||||
user_id: '@bob:localhost',
|
|
||||||
signatures: {
|
|
||||||
'@bob:localhost': {
|
|
||||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
|
||||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const bobKeys = {};
|
|
||||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
|
||||||
aliTestClient.httpBackend.when(
|
|
||||||
"POST", "/keys/query",
|
|
||||||
).respond(200, function(path, content) {
|
|
||||||
const result = {};
|
|
||||||
result[bobUserId] = bobKeys;
|
|
||||||
return { device_keys: result };
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
aliTestClient.client.downloadKeys([bobUserId]),
|
|
||||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
|
||||||
]).then(function() {
|
|
||||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
|
||||||
}).then((devices) => {
|
|
||||||
// should get an empty list
|
|
||||||
expect(devices).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Bob starts his client and uploads device keys and one-time keys", function() {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => bobTestClient.awaitOneTimeKeyUpload())
|
|
||||||
.then((keys) => {
|
|
||||||
expect(Object.keys(keys).length).toEqual(5);
|
|
||||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali sends a message", function() {
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(aliEnablesEncryption)
|
|
||||||
.then(aliSendsFirstMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Bob receives a message", function() {
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(aliEnablesEncryption)
|
|
||||||
.then(aliSendsFirstMessage)
|
|
||||||
.then(bobRecvMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Bob receives a message with a bogus sender", function() {
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(aliEnablesEncryption)
|
|
||||||
.then(aliSendsFirstMessage)
|
|
||||||
.then(function() {
|
|
||||||
const message = aliMessages.shift();
|
|
||||||
const syncData = {
|
|
||||||
next_batch: "x",
|
|
||||||
rooms: {
|
|
||||||
join: {
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
syncData.rooms.join[roomId] = {
|
|
||||||
timeline: {
|
|
||||||
events: [
|
|
||||||
testUtils.mkEvent({
|
|
||||||
type: "m.room.encrypted",
|
|
||||||
room: roomId,
|
|
||||||
content: message,
|
|
||||||
sender: "@bogus:sender",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
|
||||||
|
|
||||||
const eventPromise = new Promise((resolve, reject) => {
|
|
||||||
const onEvent = function(event) {
|
|
||||||
logger.log(bobUserId + " received event",
|
|
||||||
event);
|
|
||||||
resolve(event);
|
|
||||||
};
|
|
||||||
bobTestClient.client.once("event", onEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
bobTestClient.httpBackend.flush();
|
|
||||||
return eventPromise;
|
|
||||||
}).then((event) => {
|
|
||||||
expect(event.isEncrypted()).toBeTruthy();
|
|
||||||
|
|
||||||
// it may still be being decrypted
|
|
||||||
return testUtils.awaitDecryption(event);
|
|
||||||
}).then((event) => {
|
|
||||||
expect(event.getType()).toEqual("m.room.message");
|
|
||||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali blocks Bob's device", function() {
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(aliEnablesEncryption)
|
|
||||||
.then(aliDownloadsKeys)
|
|
||||||
.then(function() {
|
|
||||||
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
|
||||||
const p1 = sendMessage(aliTestClient.client);
|
|
||||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
|
|
||||||
.then(function(sentContent) {
|
|
||||||
// no unblocked devices, so the ciphertext should be empty
|
|
||||||
expect(sentContent.ciphertext).toEqual({});
|
|
||||||
});
|
|
||||||
return Promise.all([p1, p2]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Bob receives two pre-key messages", function() {
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(aliEnablesEncryption)
|
|
||||||
.then(aliSendsFirstMessage)
|
|
||||||
.then(bobRecvMessage)
|
|
||||||
.then(aliSendsMessage)
|
|
||||||
.then(bobRecvMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Bob replies to the message", function() {
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => bobTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(() => firstSync(bobTestClient))
|
|
||||||
.then(aliEnablesEncryption)
|
|
||||||
.then(aliSendsFirstMessage)
|
|
||||||
.then(bobRecvMessage)
|
|
||||||
.then(bobEnablesEncryption)
|
|
||||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
|
||||||
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
|
|
||||||
}).then(aliRecvMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ali does a key query when encryption is enabled", function() {
|
|
||||||
// enabling encryption in the room should make alice download devices
|
|
||||||
// for both members.
|
|
||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => aliTestClient.start())
|
|
||||||
.then(() => firstSync(aliTestClient))
|
|
||||||
.then(() => {
|
|
||||||
const syncData = {
|
|
||||||
next_batch: '2',
|
|
||||||
rooms: {
|
|
||||||
join: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
syncData.rooms.join[roomId] = {
|
|
||||||
state: {
|
|
||||||
events: [
|
|
||||||
testUtils.mkEvent({
|
|
||||||
type: 'm.room.encryption',
|
|
||||||
skey: '',
|
|
||||||
content: {
|
|
||||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
|
||||||
200, syncData);
|
|
||||||
return aliTestClient.httpBackend.flush('/sync', 1);
|
|
||||||
}).then(() => {
|
|
||||||
aliTestClient.expectKeyQuery({
|
|
||||||
device_keys: {
|
|
||||||
[bobUserId]: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return aliTestClient.httpBackend.flushAllExpected();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Upload new oneTimeKeys based on a /sync request - no count-asking", function() {
|
|
||||||
// Send a response which causes a key upload
|
|
||||||
const httpBackend = aliTestClient.httpBackend;
|
|
||||||
const syncDataEmpty = {
|
|
||||||
next_batch: "a",
|
|
||||||
device_one_time_keys_count: {
|
|
||||||
signed_curve25519: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// enqueue expectations:
|
|
||||||
// * Sync with empty one_time_keys => upload keys
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
logger.log(aliTestClient + ': starting');
|
|
||||||
httpBackend.when("GET", "/versions").respond(200, {});
|
|
||||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
|
||||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
|
||||||
aliTestClient.expectDeviceKeyUpload();
|
|
||||||
|
|
||||||
// we let the client do a very basic initial sync, which it needs before
|
|
||||||
// it will upload one-time keys.
|
|
||||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
|
||||||
|
|
||||||
aliTestClient.client.startClient({});
|
|
||||||
|
|
||||||
return httpBackend.flushAllExpected().then(() => {
|
|
||||||
logger.log(aliTestClient + ': started');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => httpBackend.when("POST", "/keys/upload")
|
|
||||||
.respond(200, (path, content) => {
|
|
||||||
expect(content.one_time_keys).toBeTruthy();
|
|
||||||
expect(content.one_time_keys).not.toEqual({});
|
|
||||||
expect(Object.keys(content.one_time_keys).length)
|
|
||||||
.toBeGreaterThanOrEqual(1);
|
|
||||||
logger.log('received %i one-time keys',
|
|
||||||
Object.keys(content.one_time_keys).length);
|
|
||||||
// cancel futher calls by telling the client
|
|
||||||
// we have more than we need
|
|
||||||
return {
|
|
||||||
one_time_key_counts: {
|
|
||||||
signed_curve25519: 70,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}))
|
|
||||||
.then(() => httpBackend.flushAllExpected());
|
|
||||||
});
|
|
||||||
});
|
|
676
spec/integ/matrix-client-crypto.spec.ts
Normal file
676
spec/integ/matrix-client-crypto.spec.ts
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* This file consists of a set of integration tests which try to simulate
|
||||||
|
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
||||||
|
*
|
||||||
|
* Note that megolm (group) conversation is not tested here.
|
||||||
|
*
|
||||||
|
* See also `megolm.spec.js`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// load olm before the sdk if possible
|
||||||
|
import '../olm-loader';
|
||||||
|
|
||||||
|
import { logger } from '../../src/logger';
|
||||||
|
import * as testUtils from "../test-utils/test-utils";
|
||||||
|
import { TestClient } from "../TestClient";
|
||||||
|
import { CRYPTO_ENABLED } from "../../src/client";
|
||||||
|
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||||
|
|
||||||
|
let aliTestClient: TestClient;
|
||||||
|
const roomId = "!room:localhost";
|
||||||
|
const aliUserId = "@ali:localhost";
|
||||||
|
const aliDeviceId = "zxcvb";
|
||||||
|
const aliAccessToken = "aseukfgwef";
|
||||||
|
let bobTestClient: TestClient;
|
||||||
|
const bobUserId = "@bob:localhost";
|
||||||
|
const bobDeviceId = "bvcxz";
|
||||||
|
const bobAccessToken = "fewgfkuesa";
|
||||||
|
let aliMessages: IContent[];
|
||||||
|
let bobMessages: IContent[];
|
||||||
|
|
||||||
|
// IMessage isn't exported by src/crypto/algorithms/olm.ts
|
||||||
|
interface OlmPayload {
|
||||||
|
type: number;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bobUploadsDeviceKeys(): Promise<void> {
|
||||||
|
bobTestClient.expectDeviceKeyUpload();
|
||||||
|
await Promise.all([
|
||||||
|
bobTestClient.client.uploadKeys(),
|
||||||
|
bobTestClient.httpBackend.flushAllExpected(),
|
||||||
|
]);
|
||||||
|
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an expectation that querier will query uploader's keys; then flush the http request.
|
||||||
|
*
|
||||||
|
* @return {promise} resolves once the http request has completed.
|
||||||
|
*/
|
||||||
|
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
|
||||||
|
// can't query keys before bob has uploaded them
|
||||||
|
expect(uploader.deviceKeys).toBeTruthy();
|
||||||
|
|
||||||
|
const uploaderKeys = {};
|
||||||
|
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
|
||||||
|
querier.httpBackend.when("POST", "/keys/query")
|
||||||
|
.respond(200, function(_path, content) {
|
||||||
|
expect(content.device_keys[uploader.userId]).toEqual([]);
|
||||||
|
const result = {};
|
||||||
|
result[uploader.userId] = uploaderKeys;
|
||||||
|
return { device_keys: result };
|
||||||
|
});
|
||||||
|
return querier.httpBackend.flush("/keys/query", 1);
|
||||||
|
}
|
||||||
|
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
|
||||||
|
const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||||
|
*
|
||||||
|
* @return {promise} resolves once the http request has completed.
|
||||||
|
*/
|
||||||
|
async function expectAliClaimKeys(): Promise<void> {
|
||||||
|
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||||
|
aliTestClient.httpBackend.when(
|
||||||
|
"POST", "/keys/claim",
|
||||||
|
).respond(200, function(_path, content) {
|
||||||
|
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||||
|
expect(claimType).toEqual("signed_curve25519");
|
||||||
|
let keyId = null;
|
||||||
|
for (keyId in keys) {
|
||||||
|
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||||
|
if (keyId.indexOf(claimType + ":") === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = {};
|
||||||
|
result[bobUserId] = {};
|
||||||
|
result[bobUserId][bobDeviceId] = {};
|
||||||
|
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||||
|
return { one_time_keys: result };
|
||||||
|
});
|
||||||
|
// it can take a while to process the key query, so give it some extra
|
||||||
|
// time, and make sure the claim actually happens rather than ploughing on
|
||||||
|
// confusingly.
|
||||||
|
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
|
||||||
|
expect(r).toEqual(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function aliDownloadsKeys(): Promise<void> {
|
||||||
|
// can't query keys before bob has uploaded them
|
||||||
|
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||||
|
|
||||||
|
const p1 = async () => {
|
||||||
|
await aliTestClient.client.downloadKeys([bobUserId]);
|
||||||
|
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||||
|
expect(devices.length).toEqual(1);
|
||||||
|
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||||
|
};
|
||||||
|
const p2 = expectAliQueryKeys;
|
||||||
|
|
||||||
|
// check that the localStorage is updated as we expect (not sure this is
|
||||||
|
// an integration test, but meh)
|
||||||
|
await Promise.all([p1(), p2()]);
|
||||||
|
await aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||||
|
// @ts-ignore - protected
|
||||||
|
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
|
const devices = data.devices[bobUserId];
|
||||||
|
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||||
|
expect(devices[bobDeviceId].verified).
|
||||||
|
toBe(0); // DeviceVerification.UNVERIFIED
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
|
||||||
|
await client.setRoomEncryption(roomId, {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
});
|
||||||
|
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||||
|
}
|
||||||
|
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
|
||||||
|
const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||||
|
* check the results.
|
||||||
|
*
|
||||||
|
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||||
|
*/
|
||||||
|
async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [_, ciphertext] = await Promise.all([
|
||||||
|
sendMessage(aliTestClient.client),
|
||||||
|
expectAliQueryKeys()
|
||||||
|
.then(expectAliClaimKeys)
|
||||||
|
.then(expectAliSendMessageRequest),
|
||||||
|
]);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||||
|
* and check the results.
|
||||||
|
*
|
||||||
|
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||||
|
*/
|
||||||
|
async function aliSendsMessage(): Promise<OlmPayload> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [_, ciphertext] = await Promise.all([
|
||||||
|
sendMessage(aliTestClient.client),
|
||||||
|
expectAliSendMessageRequest(),
|
||||||
|
]);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||||
|
* expectations and check the results.
|
||||||
|
*
|
||||||
|
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||||
|
*/
|
||||||
|
async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [_, ciphertext] = await Promise.all([
|
||||||
|
sendMessage(bobTestClient.client),
|
||||||
|
expectBobQueryKeys()
|
||||||
|
.then(expectBobSendMessageRequest),
|
||||||
|
]);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an expectation that Ali will send a message, and flush the request
|
||||||
|
*
|
||||||
|
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||||
|
*/
|
||||||
|
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||||
|
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
|
||||||
|
aliMessages.push(content);
|
||||||
|
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||||
|
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||||
|
expect(ciphertext).toBeTruthy();
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an expectation that Bob will send a message, and flush the request
|
||||||
|
*
|
||||||
|
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||||
|
*/
|
||||||
|
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||||
|
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||||
|
bobMessages.push(content);
|
||||||
|
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||||
|
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||||
|
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||||
|
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||||
|
expect(ciphertext).toBeTruthy();
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||||
|
return client.sendMessage(
|
||||||
|
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||||
|
const path = "/send/m.room.encrypted/";
|
||||||
|
const prom = new Promise((resolve) => {
|
||||||
|
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||||
|
resolve(content);
|
||||||
|
return {
|
||||||
|
event_id: "asdfgh",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// it can take a while to process the key query
|
||||||
|
await httpBackend.flush(path, 1);
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aliRecvMessage(): Promise<void> {
|
||||||
|
const message = bobMessages.shift();
|
||||||
|
return recvMessage(
|
||||||
|
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bobRecvMessage(): Promise<void> {
|
||||||
|
const message = aliMessages.shift();
|
||||||
|
return recvMessage(
|
||||||
|
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recvMessage(
|
||||||
|
httpBackend: TestClient["httpBackend"],
|
||||||
|
client: MatrixClient,
|
||||||
|
sender: string,
|
||||||
|
message: IContent,
|
||||||
|
): Promise<void> {
|
||||||
|
const syncData = {
|
||||||
|
next_batch: "x",
|
||||||
|
rooms: {
|
||||||
|
join: {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
syncData.rooms.join[roomId] = {
|
||||||
|
timeline: {
|
||||||
|
events: [
|
||||||
|
testUtils.mkEvent({
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
room: roomId,
|
||||||
|
content: message,
|
||||||
|
sender: sender,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||||
|
|
||||||
|
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||||
|
const onEvent = function(event: MatrixEvent) {
|
||||||
|
// ignore the m.room.member events
|
||||||
|
if (event.getType() == "m.room.member") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.log(client.credentials.userId + " received event",
|
||||||
|
event);
|
||||||
|
|
||||||
|
client.removeListener(ClientEvent.Event, onEvent);
|
||||||
|
resolve(event);
|
||||||
|
};
|
||||||
|
client.on(ClientEvent.Event, onEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
|
||||||
|
const preDecryptionEvent = await eventPromise;
|
||||||
|
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||||
|
// it may still be being decrypted
|
||||||
|
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||||
|
expect(event.getType()).toEqual("m.room.message");
|
||||||
|
expect(event.getContent()).toMatchObject({
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Hello, World",
|
||||||
|
});
|
||||||
|
expect(event.isEncrypted()).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an initial sync response to the client (which just includes the member
|
||||||
|
* list for our test room).
|
||||||
|
*
|
||||||
|
* @param {TestClient} testClient
|
||||||
|
* @returns {Promise} which resolves when the sync has been flushed.
|
||||||
|
*/
|
||||||
|
function firstSync(testClient: TestClient): Promise<void> {
|
||||||
|
// send a sync response including our test room.
|
||||||
|
const syncData = {
|
||||||
|
next_batch: "x",
|
||||||
|
rooms: {
|
||||||
|
join: { },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
syncData.rooms.join[roomId] = {
|
||||||
|
state: {
|
||||||
|
events: [
|
||||||
|
testUtils.mkMembership({
|
||||||
|
mship: "join",
|
||||||
|
user: aliUserId,
|
||||||
|
}),
|
||||||
|
testUtils.mkMembership({
|
||||||
|
mship: "join",
|
||||||
|
user: bobUserId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
events: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||||
|
return testClient.flushSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("MatrixClient crypto", () => {
|
||||||
|
if (!CRYPTO_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||||
|
await aliTestClient.client.initCrypto();
|
||||||
|
|
||||||
|
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||||
|
await bobTestClient.client.initCrypto();
|
||||||
|
|
||||||
|
aliMessages = [];
|
||||||
|
bobMessages = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||||
|
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||||
|
|
||||||
|
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bob uploads device keys", bobUploadsDeviceKeys);
|
||||||
|
|
||||||
|
it("Ali downloads Bobs device keys", async () => {
|
||||||
|
await bobUploadsDeviceKeys();
|
||||||
|
await aliDownloadsKeys();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ali gets keys with an invalid signature", async () => {
|
||||||
|
await bobUploadsDeviceKeys();
|
||||||
|
// tamper bob's keys
|
||||||
|
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||||
|
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||||
|
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||||
|
await Promise.all([
|
||||||
|
aliTestClient.client.downloadKeys([bobUserId]),
|
||||||
|
expectAliQueryKeys(),
|
||||||
|
]);
|
||||||
|
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||||
|
// should get an empty list
|
||||||
|
expect(devices).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ali gets keys with an incorrect userId", async () => {
|
||||||
|
const eveUserId = "@eve:localhost";
|
||||||
|
|
||||||
|
const bobDeviceKeys = {
|
||||||
|
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||||
|
device_id: 'bvcxz',
|
||||||
|
keys: {
|
||||||
|
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||||
|
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||||
|
},
|
||||||
|
user_id: '@eve:localhost',
|
||||||
|
signatures: {
|
||||||
|
'@eve:localhost': {
|
||||||
|
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||||
|
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const bobKeys = {};
|
||||||
|
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||||
|
aliTestClient.httpBackend.when(
|
||||||
|
"POST", "/keys/query",
|
||||||
|
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||||
|
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||||
|
]);
|
||||||
|
const [bobDevices, eveDevices] = await Promise.all([
|
||||||
|
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||||
|
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||||
|
]);
|
||||||
|
// should get an empty list
|
||||||
|
expect(bobDevices).toEqual([]);
|
||||||
|
expect(eveDevices).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ali gets keys with an incorrect deviceId", async () => {
|
||||||
|
const bobDeviceKeys = {
|
||||||
|
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||||
|
device_id: 'bad_device',
|
||||||
|
keys: {
|
||||||
|
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||||
|
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||||
|
},
|
||||||
|
user_id: '@bob:localhost',
|
||||||
|
signatures: {
|
||||||
|
'@bob:localhost': {
|
||||||
|
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||||
|
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const bobKeys = {};
|
||||||
|
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||||
|
aliTestClient.httpBackend.when(
|
||||||
|
"POST", "/keys/query",
|
||||||
|
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
aliTestClient.client.downloadKeys([bobUserId]),
|
||||||
|
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||||
|
]);
|
||||||
|
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||||
|
// should get an empty list
|
||||||
|
expect(devices).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bob starts his client and uploads device keys and one-time keys", async () => {
|
||||||
|
await bobTestClient.start();
|
||||||
|
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||||
|
expect(Object.keys(keys).length).toEqual(5);
|
||||||
|
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ali sends a message", async () => {
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await bobTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
await aliEnablesEncryption();
|
||||||
|
await aliSendsFirstMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bob receives a message", async () => {
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await bobTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
await aliEnablesEncryption();
|
||||||
|
await aliSendsFirstMessage();
|
||||||
|
await bobRecvMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bob receives a message with a bogus sender", async () => {
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await bobTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
await aliEnablesEncryption();
|
||||||
|
await aliSendsFirstMessage();
|
||||||
|
const message = aliMessages.shift();
|
||||||
|
const syncData = {
|
||||||
|
next_batch: "x",
|
||||||
|
rooms: {
|
||||||
|
join: {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
syncData.rooms.join[roomId] = {
|
||||||
|
timeline: {
|
||||||
|
events: [
|
||||||
|
testUtils.mkEvent({
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
room: roomId,
|
||||||
|
content: message,
|
||||||
|
sender: "@bogus:sender",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||||
|
|
||||||
|
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||||
|
const onEvent = function(event: MatrixEvent) {
|
||||||
|
logger.log(bobUserId + " received event", event);
|
||||||
|
resolve(event);
|
||||||
|
};
|
||||||
|
bobTestClient.client.once(ClientEvent.Event, onEvent);
|
||||||
|
});
|
||||||
|
await bobTestClient.httpBackend.flushAllExpected();
|
||||||
|
const preDecryptionEvent = await eventPromise;
|
||||||
|
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||||
|
// it may still be being decrypted
|
||||||
|
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||||
|
expect(event.getType()).toEqual("m.room.message");
|
||||||
|
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ali blocks Bob's device", async () => {
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await bobTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
await aliEnablesEncryption();
|
||||||
|
await aliDownloadsKeys();
|
||||||
|
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||||
|
const p1 = sendMessage(aliTestClient.client);
|
||||||
|
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
|
||||||
|
.then(function(sentContent) {
|
||||||
|
// no unblocked devices, so the ciphertext should be empty
|
||||||
|
expect(sentContent.ciphertext).toEqual({});
|
||||||
|
});
|
||||||
|
await Promise.all([p1, p2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bob receives two pre-key messages", async () => {
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await bobTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
await aliEnablesEncryption();
|
||||||
|
await aliSendsFirstMessage();
|
||||||
|
await bobRecvMessage();
|
||||||
|
await aliSendsMessage();
|
||||||
|
await bobRecvMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bob replies to the message", async () => {
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await bobTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
await firstSync(bobTestClient);
|
||||||
|
await aliEnablesEncryption();
|
||||||
|
await aliSendsFirstMessage();
|
||||||
|
await bobRecvMessage();
|
||||||
|
await bobEnablesEncryption();
|
||||||
|
const ciphertext = await bobSendsReplyMessage();
|
||||||
|
expect(ciphertext.type).toEqual(1);
|
||||||
|
await aliRecvMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ali does a key query when encryption is enabled", async () => {
|
||||||
|
// enabling encryption in the room should make alice download devices
|
||||||
|
// for both members.
|
||||||
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
|
await aliTestClient.start();
|
||||||
|
await firstSync(aliTestClient);
|
||||||
|
const syncData = {
|
||||||
|
next_batch: '2',
|
||||||
|
rooms: {
|
||||||
|
join: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
syncData.rooms.join[roomId] = {
|
||||||
|
state: {
|
||||||
|
events: [
|
||||||
|
testUtils.mkEvent({
|
||||||
|
type: 'm.room.encryption',
|
||||||
|
skey: '',
|
||||||
|
content: {
|
||||||
|
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||||
|
200, syncData);
|
||||||
|
await aliTestClient.httpBackend.flush('/sync', 1);
|
||||||
|
aliTestClient.expectKeyQuery({
|
||||||
|
device_keys: {
|
||||||
|
[bobUserId]: {},
|
||||||
|
},
|
||||||
|
failures: {},
|
||||||
|
});
|
||||||
|
await aliTestClient.httpBackend.flushAllExpected();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
|
||||||
|
// Send a response which causes a key upload
|
||||||
|
const httpBackend = aliTestClient.httpBackend;
|
||||||
|
const syncDataEmpty = {
|
||||||
|
next_batch: "a",
|
||||||
|
device_one_time_keys_count: {
|
||||||
|
signed_curve25519: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// enqueue expectations:
|
||||||
|
// * Sync with empty one_time_keys => upload keys
|
||||||
|
|
||||||
|
logger.log(aliTestClient + ': starting');
|
||||||
|
httpBackend.when("GET", "/versions").respond(200, {});
|
||||||
|
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||||
|
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||||
|
aliTestClient.expectDeviceKeyUpload();
|
||||||
|
|
||||||
|
// we let the client do a very basic initial sync, which it needs before
|
||||||
|
// it will upload one-time keys.
|
||||||
|
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
aliTestClient.client.startClient({}),
|
||||||
|
httpBackend.flushAllExpected(),
|
||||||
|
]);
|
||||||
|
logger.log(aliTestClient + ': started');
|
||||||
|
httpBackend.when("POST", "/keys/upload")
|
||||||
|
.respond(200, (_path, content) => {
|
||||||
|
expect(content.one_time_keys).toBeTruthy();
|
||||||
|
expect(content.one_time_keys).not.toEqual({});
|
||||||
|
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1);
|
||||||
|
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
||||||
|
// cancel futher calls by telling the client
|
||||||
|
// we have more than we need
|
||||||
|
return {
|
||||||
|
one_time_key_counts: {
|
||||||
|
signed_curve25519: 70,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user