1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Merge remote-tracking branch 'origin/develop' into jryans/semi-linting

This commit is contained in:
J. Ryan Stinnett
2021-06-04 10:22:06 +01:00
48 changed files with 9824 additions and 9199 deletions

View File

@@ -69,7 +69,7 @@ export function TestClient(
this.deviceKeys = null; this.deviceKeys = null;
this.oneTimeKeys = {}; this.oneTimeKeys = {};
this._callEventHandler = { this.callEventHandler = {
calls: new Map(), calls: new Map(),
}; };
} }

View File

@@ -165,7 +165,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1).then( aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1), () => aliceTestClient.httpBackend.flush('/send/', 1),
), ),
aliceTestClient.client._crypto._deviceList.saveIfDirty(), aliceTestClient.client.crypto._deviceList.saveIfDirty(),
]); ]);
}).then(() => { }).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
@@ -202,7 +202,7 @@ describe("DeviceList management:", function() {
return aliceTestClient.httpBackend.flush('/keys/query', 1); return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => { }).then((flushed) => {
expect(flushed).toEqual(0); expect(flushed).toEqual(0);
return aliceTestClient.client._crypto._deviceList.saveIfDirty(); return aliceTestClient.client.crypto._deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
@@ -235,7 +235,7 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response // wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']); return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => { }).then(() => {
return aliceTestClient.client._crypto._deviceList.saveIfDirty(); return aliceTestClient.client.crypto._deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
@@ -256,7 +256,7 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response // wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']); return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => { }).then(() => {
return aliceTestClient.client._crypto._deviceList.saveIfDirty(); return aliceTestClient.client.crypto._deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
@@ -286,7 +286,7 @@ describe("DeviceList management:", function() {
}, },
); );
await aliceTestClient.httpBackend.flush('/keys/query', 1); await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client._crypto._deviceList.saveIfDirty(); await aliceTestClient.client.crypto._deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
@@ -322,7 +322,7 @@ describe("DeviceList management:", function() {
); );
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty(); await aliceTestClient.client.crypto._deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
@@ -358,7 +358,7 @@ describe("DeviceList management:", function() {
); );
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty(); await aliceTestClient.client.crypto._deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
@@ -379,7 +379,7 @@ describe("DeviceList management:", function() {
anotherTestClient.httpBackend.when('GET', '/sync').respond( anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([])); 200, getSyncResponse([]));
await anotherTestClient.flushSync(); await anotherTestClient.flushSync();
await anotherTestClient.client._crypto._deviceList.saveIfDirty(); await anotherTestClient.client.crypto._deviceList.saveIfDirty();
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];

View File

@@ -159,7 +159,7 @@ function aliDownloadsKeys() {
// check that the localStorage is updated as we expect (not sure this is // check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh) // an integration test, but meh)
return Promise.all([p1, p2]).then(() => { return Promise.all([p1, p2]).then(() => {
return aliTestClient.client._crypto._deviceList.saveIfDirty(); return aliTestClient.client.crypto._deviceList.saveIfDirty();
}).then(() => { }).then(() => {
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId]; const devices = data.devices[bobUserId];

View File

@@ -336,7 +336,7 @@ describe("MatrixClient", function() {
var b = JSON.parse(JSON.stringify(o)); var b = JSON.parse(JSON.stringify(o));
delete(b.signatures); delete(b.signatures);
delete(b.unsigned); delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b)); return client.crypto._olmDevice.sign(anotherjson.stringify(b));
}; };
logger.log("Ed25519: " + ed25519key); logger.log("Ed25519: " + ed25519key);

View File

@@ -998,7 +998,7 @@ describe("megolm", function() {
...rawEvent, ...rawEvent,
room: ROOM_ID, room: ROOM_ID,
}); });
return event.attemptDecryption(testClient.client._crypto, true).then(() => { return event.attemptDecryption(testClient.client.crypto, true).then(() => {
expect(event.isKeySourceUntrusted()).toBeTruthy(); expect(event.isKeySourceUntrusted()).toBeTruthy();
}); });
}).then(() => { }).then(() => {
@@ -1013,14 +1013,14 @@ describe("megolm", function() {
event: true, event: true,
}); });
event._senderCurve25519Key = testSenderKey; event._senderCurve25519Key = testSenderKey;
return testClient.client._crypto._onRoomKeyEvent(event); return testClient.client.crypto._onRoomKeyEvent(event);
}).then(() => { }).then(() => {
const event = testUtils.mkEvent({ const event = testUtils.mkEvent({
event: true, event: true,
...rawEvent, ...rawEvent,
room: ROOM_ID, room: ROOM_ID,
}); });
return event.attemptDecryption(testClient.client._crypto, true).then(() => { return event.attemptDecryption(testClient.client.crypto, true).then(() => {
expect(event.isKeySourceUntrusted()).toBeFalsy(); expect(event.isKeySourceUntrusted()).toBeFalsy();
}); });
}); });

View File

@@ -357,12 +357,12 @@ export function setHttpResponses(
); );
const httpReq = httpResponseObj.request.bind(httpResponseObj); const httpReq = httpResponseObj.request.bind(httpResponseObj);
client._http = [ client.http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri", "authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent", "request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}); ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
client._http.authedRequest.mockImplementation(httpReq); client.http.authedRequest.mockImplementation(httpReq);
client._http.authedRequestWithPrefix.mockImplementation(httpReq); client.http.authedRequestWithPrefix.mockImplementation(httpReq);
client._http.requestWithPrefix.mockImplementation(httpReq); client.http.requestWithPrefix.mockImplementation(httpReq);
client._http.request.mockImplementation(httpReq); client.http.request.mockImplementation(httpReq);
} }

View File

@@ -65,7 +65,7 @@ describe("Crypto", function() {
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] = device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client._crypto._deviceList.getDeviceByIdentityKey = () => device; client.crypto._deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event); encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy(); expect(encryptionInfo.encrypted).toBeTruthy();
@@ -213,7 +213,7 @@ describe("Crypto", function() {
async function keyshareEventForEvent(event, index) { async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent(); const eventContent = event.getWireContent();
const key = await aliceClient._crypto._olmDevice const key = await aliceClient.crypto._olmDevice
.getInboundGroupSessionKey( .getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id, roomId, eventContent.sender_key, eventContent.session_id,
index, index,
@@ -273,19 +273,19 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => { await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt // alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending // them without any keys, so that they'll be in pending
await aliceClient._crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto.encryptEvent(event, aliceRoom);
event._clearEvent = {}; event._clearEvent = {};
event._senderCurve25519Key = null; event._senderCurve25519Key = null;
event._claimedEd25519Key = null; event._claimedEd25519Key = null;
try { try {
await bobClient._crypto.decryptEvent(event); await bobClient.crypto.decryptEvent(event);
} catch (e) { } catch (e) {
// we expect this to fail because we don't have the // we expect this to fail because we don't have the
// decryption keys yet // decryption keys yet
} }
})); }));
const bobDecryptor = bobClient._crypto._getRoomDecryptor( const bobDecryptor = bobClient.crypto._getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@@ -302,7 +302,7 @@ describe("Crypto", function() {
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
const cryptoStore = bobClient._cryptoStore; const cryptoStore = bobClient.cryptoStore;
const eventContent = events[0].getWireContent(); const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key; const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id; const sessionId = eventContent.session_id;
@@ -344,7 +344,7 @@ describe("Crypto", function() {
}, },
}); });
await aliceClient.cancelAndResendEventRoomKeyRequest(event); await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient._cryptoStore; const cryptoStore = aliceClient.cryptoStore;
const roomKeyRequestBody = { const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM, algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom", room_id: "!someroom",
@@ -377,7 +377,7 @@ describe("Crypto", function() {
// key requests get queued until the sync has finished, but we don't // 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 // let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now. // to force it to send now.
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests(); aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers(); jest.runAllTimers();
await Promise.resolve(); await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1); expect(aliceClient.sendToDevice).toBeCalledTimes(1);

View File

@@ -257,6 +257,9 @@ describe("MegolmDecryption", function() {
}); });
it("re-uses sessions for sequential messages", async function() { it("re-uses sessions for sequential messages", async function() {
mockCrypto._backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi(); const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage); const cryptoStore = new MemoryCryptoStore(mockStorage);
@@ -362,9 +365,9 @@ describe("MegolmDecryption", function() {
bobClient1.initCrypto(), bobClient1.initCrypto(),
bobClient2.initCrypto(), bobClient2.initCrypto(),
]); ]);
const aliceDevice = aliceClient._crypto._olmDevice; const aliceDevice = aliceClient.crypto._olmDevice;
const bobDevice1 = bobClient1._crypto._olmDevice; const bobDevice1 = bobClient1.crypto._olmDevice;
const bobDevice2 = bobClient2._crypto._olmDevice; const bobDevice2 = bobClient2.crypto._olmDevice;
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -401,10 +404,10 @@ describe("MegolmDecryption", function() {
}, },
}; };
aliceClient._crypto._deviceList.storeDevicesForUser( aliceClient.crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES, "@bob:example.com", BOB_DEVICES,
); );
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) { aliceClient.crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds); return this._getDevicesFromStore(userIds);
}; };
@@ -445,7 +448,7 @@ describe("MegolmDecryption", function() {
body: "secret", body: "secret",
}, },
}); });
await aliceClient._crypto.encryptEvent(event, room); await aliceClient.crypto.encryptEvent(event, room);
expect(run).toBe(true); expect(run).toBe(true);
@@ -465,8 +468,8 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(), aliceClient.initCrypto(),
bobClient.initCrypto(), bobClient.initCrypto(),
]); ]);
const aliceDevice = aliceClient._crypto._olmDevice; const aliceDevice = aliceClient.crypto._olmDevice;
const bobDevice = bobClient._crypto._olmDevice; const bobDevice = bobClient.crypto._olmDevice;
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -505,10 +508,10 @@ describe("MegolmDecryption", function() {
}, },
}; };
aliceClient._crypto._deviceList.storeDevicesForUser( aliceClient.crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES, "@bob:example.com", BOB_DEVICES,
); );
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) { aliceClient.crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds); return this._getDevicesFromStore(userIds);
}; };
@@ -543,7 +546,7 @@ describe("MegolmDecryption", function() {
event_id: "$event", event_id: "$event",
content: {}, content: {},
}); });
await aliceClient._crypto.encryptEvent(event, aliceRoom); await aliceClient.crypto.encryptEvent(event, aliceRoom);
await sendPromise; await sendPromise;
}); });
@@ -558,11 +561,11 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(), aliceClient.initCrypto(),
bobClient.initCrypto(), bobClient.initCrypto(),
]); ]);
const bobDevice = bobClient._crypto._olmDevice; const bobDevice = bobClient.crypto._olmDevice;
const roomId = "!someroom"; const roomId = "!someroom";
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld", type: "org.matrix.room_key.withheld",
sender: "@bob:example.com", sender: "@bob:example.com",
content: { content: {
@@ -575,7 +578,7 @@ describe("MegolmDecryption", function() {
}, },
})); }));
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted", type: "m.room.encrypted",
sender: "@bob:example.com", sender: "@bob:example.com",
event_id: "$event", event_id: "$event",
@@ -601,14 +604,14 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(), aliceClient.initCrypto(),
bobClient.initCrypto(), bobClient.initCrypto(),
]); ]);
aliceClient._crypto.downloadKeys = async () => {}; aliceClient.crypto.downloadKeys = async () => {};
const bobDevice = bobClient._crypto._olmDevice; const bobDevice = bobClient.crypto._olmDevice;
const roomId = "!someroom"; const roomId = "!someroom";
const now = Date.now(); const now = Date.now();
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld", type: "org.matrix.room_key.withheld",
sender: "@bob:example.com", sender: "@bob:example.com",
content: { content: {
@@ -625,7 +628,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100); setTimeout(resolve, 100);
}); });
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted", type: "m.room.encrypted",
sender: "@bob:example.com", sender: "@bob:example.com",
event_id: "$event", event_id: "$event",
@@ -652,15 +655,15 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(), aliceClient.initCrypto(),
bobClient.initCrypto(), bobClient.initCrypto(),
]); ]);
const bobDevice = bobClient._crypto._olmDevice; const bobDevice = bobClient.crypto._olmDevice;
aliceClient._crypto.downloadKeys = async () => {}; aliceClient.crypto.downloadKeys = async () => {};
const roomId = "!someroom"; const roomId = "!someroom";
const now = Date.now(); const now = Date.now();
// pretend we got an event that we can't decrypt // pretend we got an event that we can't decrypt
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
type: "m.room.encrypted", type: "m.room.encrypted",
sender: "@bob:example.com", sender: "@bob:example.com",
content: { content: {
@@ -675,7 +678,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100); setTimeout(resolve, 100);
}); });
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted", type: "m.room.encrypted",
sender: "@bob:example.com", sender: "@bob:example.com",
event_id: "$event", event_id: "$event",

View File

@@ -28,6 +28,7 @@ import * as testUtils from "../../test-utils";
import { OlmDevice } from "../../../src/crypto/OlmDevice"; import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { Crypto } from "../../../src/crypto"; import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils"; import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
const Olm = global.Olm; const Olm = global.Olm;
@@ -73,7 +74,7 @@ const KEY_BACKUP_DATA = {
}; };
const BACKUP_INFO = { const BACKUP_INFO = {
algorithm: "m.megolm_backup.v1", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1, version: 1,
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -138,6 +139,7 @@ describe("MegolmBackup", function() {
let megolmDecryption; let megolmDecryption;
beforeEach(async function() { beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto._backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption(); mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key( mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -215,12 +217,14 @@ describe("MegolmBackup", function() {
}; };
mockCrypto.cancelRoomKeyRequest = function() {}; mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.backupGroupSession = jest.fn(); mockCrypto._backupManager = {
backupGroupSession: jest.fn(),
};
return event.attemptDecryption(mockCrypto).then(() => { return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event); return megolmDecryption.onRoomKeyEvent(event);
}).then(() => { }).then(() => {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); expect(mockCrypto._backupManager.backupGroupSession).toHaveBeenCalled();
}); });
}); });
@@ -264,7 +268,7 @@ describe("MegolmBackup", function() {
}) })
.then(() => { .then(() => {
client.enableKeyBackup({ client.enableKeyBackup({
algorithm: "m.megolm_backup.v1", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1, version: 1,
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -272,7 +276,7 @@ describe("MegolmBackup", function() {
}); });
let numCalls = 0; let numCalls = 0;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
client._http.authedRequest = function( client.http.authedRequest = function(
callback, method, path, queryParams, data, opts, callback, method, path, queryParams, data, opts,
) { ) {
++numCalls; ++numCalls;
@@ -292,12 +296,9 @@ describe("MegolmBackup", function() {
resolve(); resolve();
return Promise.resolve({}); return Promise.resolve({});
}; };
client._crypto.backupGroupSession( client.crypto._backupManager.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(), groupSession.session_id(),
groupSession.session_key(),
); );
}).then(() => { }).then(() => {
expect(numCalls).toBe(1); expect(numCalls).toBe(1);
@@ -335,39 +336,48 @@ describe("MegolmBackup", function() {
}); });
await resetCrossSigningKeys(client); await resetCrossSigningKeys(client);
let numCalls = 0; let numCalls = 0;
await new Promise((resolve, reject) => { await Promise.all([
client._http.authedRequest = function( new Promise((resolve, reject) => {
callback, method, path, queryParams, data, opts, let backupInfo;
) { client.http.authedRequest = function(
++numCalls; callback, method, path, queryParams, data, opts,
expect(numCalls).toBeLessThanOrEqual(1); ) {
if (numCalls >= 2) { ++numCalls;
// exit out of retry loop if there's something wrong expect(numCalls).toBeLessThanOrEqual(2);
reject(new Error("authedRequest called too many timmes")); if (numCalls === 1) {
return Promise.resolve({}); expect(method).toBe("POST");
} expect(path).toBe("/room_keys/version");
expect(method).toBe("POST"); try {
expect(path).toBe("/room_keys/version"); // make sure auth_data is signed by the master key
try { olmlib.pkVerify(
// make sure auth_data is signed by the master key data.auth_data, client.getCrossSigningId(), "@alice:bar",
olmlib.pkVerify( );
data.auth_data, client.getCrossSigningId(), "@alice:bar", } catch (e) {
); reject(e);
} catch (e) { return Promise.resolve({});
reject(e); }
return Promise.resolve({}); backupInfo = data;
} return Promise.resolve({});
resolve(); } else if (numCalls === 2) {
return Promise.resolve({}); expect(method).toBe("GET");
}; expect(path).toBe("/room_keys/version");
resolve();
return Promise.resolve(backupInfo);
} else {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many times"));
return Promise.resolve({});
}
};
}),
client.createKeyBackupVersion({ client.createKeyBackupVersion({
algorithm: "m.megolm_backup.v1", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
}, },
}); }),
}); ]);
expect(numCalls).toBe(1); expect(numCalls).toBe(2);
}); });
it('retries when a backup fails', function() { it('retries when a backup fails', function() {
@@ -434,7 +444,7 @@ describe("MegolmBackup", function() {
}) })
.then(() => { .then(() => {
client.enableKeyBackup({ client.enableKeyBackup({
algorithm: "foobar", algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1, version: 1,
auth_data: { auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -442,7 +452,7 @@ describe("MegolmBackup", function() {
}); });
let numCalls = 0; let numCalls = 0;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
client._http.authedRequest = function( client.http.authedRequest = function(
callback, method, path, queryParams, data, opts, callback, method, path, queryParams, data, opts,
) { ) {
++numCalls; ++numCalls;
@@ -468,12 +478,9 @@ describe("MegolmBackup", function() {
); );
} }
}; };
client._crypto.backupGroupSession( client.crypto._backupManager.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(), groupSession.session_id(),
groupSession.session_key(),
); );
}).then(() => { }).then(() => {
expect(numCalls).toBe(2); expect(numCalls).toBe(2);
@@ -506,7 +513,7 @@ describe("MegolmBackup", function() {
}); });
it('can restore from backup', function() { it('can restore from backup', function() {
client._http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA); return Promise.resolve(KEY_BACKUP_DATA);
}; };
return client.restoreKeyBackupWithRecoveryKey( return client.restoreKeyBackupWithRecoveryKey(
@@ -523,7 +530,7 @@ describe("MegolmBackup", function() {
}); });
it('can restore backup by room', function() { it('can restore backup by room', function() {
client._http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve({ return Promise.resolve({
rooms: { rooms: {
[ROOM_ID]: { [ROOM_ID]: {
@@ -546,15 +553,15 @@ describe("MegolmBackup", function() {
it('has working cache functions', async function() { it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client._crypto.storeSessionBackupPrivateKey(key); await client.crypto.storeSessionBackupPrivateKey(key);
const result = await client._crypto.getSessionBackupPrivateKey(); const result = await client.crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key); expect(new Uint8Array(result)).toEqual(key);
}); });
it('caches session backup keys as it encounters them', async function() { it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client._crypto.getSessionBackupPrivateKey(); const cachedNull = await client.crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull(); expect(cachedNull).toBeNull();
client._http.authedRequest = function() { client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA); return Promise.resolve(KEY_BACKUP_DATA);
}; };
await new Promise((resolve) => { await new Promise((resolve) => {
@@ -566,8 +573,24 @@ describe("MegolmBackup", function() {
{ cacheCompleteCallback: resolve }, { cacheCompleteCallback: resolve },
); );
}); });
const cachedKey = await client._crypto.getSessionBackupPrivateKey(); const cachedKey = await client.crypto.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull(); expect(cachedKey).not.toBeNull();
}); });
it("fails if an known algorithm is used", async function() {
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
await expect(client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BAD_BACKUP_INFO,
)).rejects.toThrow();
});
}); });
}); });

View File

@@ -64,8 +64,8 @@ describe("Cross Signing", function() {
); );
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => { alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
await olmlib.verifySignature( await olmlib.verifySignature(
alice._crypto._olmDevice, keys.master_key, "@alice:example.com", alice.crypto._olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key, "Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
); );
}); });
alice.uploadKeySignatures = async () => {}; alice.uploadKeySignatures = async () => {};
@@ -138,7 +138,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key // set Alice's cross-signing key
await resetCrossSigningKeys(alice); await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key // Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@@ -201,24 +201,28 @@ describe("Cross Signing", function() {
const uploadSigsPromise = new Promise((resolve, reject) => { const uploadSigsPromise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = jest.fn(async (content) => { alice.uploadKeySignatures = jest.fn(async (content) => {
await olmlib.verifySignature( try {
alice._crypto._olmDevice, await olmlib.verifySignature(
content["@alice:example.com"][ alice.crypto._olmDevice,
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" content["@alice:example.com"][
], "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
"@alice:example.com", ],
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key, "@alice:example.com",
); "Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
olmlib.pkVerify( );
content["@alice:example.com"]["Osborne2"], olmlib.pkVerify(
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", content["@alice:example.com"]["Osborne2"],
"@alice:example.com", "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
); "@alice:example.com",
resolve(); );
resolve();
} catch (e) {
reject(e);
}
}); });
}); });
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
.Osborne2; .Osborne2;
const aliceDevice = { const aliceDevice = {
user_id: "@alice:example.com", user_id: "@alice:example.com",
@@ -226,7 +230,7 @@ describe("Cross Signing", function() {
}; };
aliceDevice.keys = deviceInfo.keys; aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms; aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice); await alice.crypto._signObject(aliceDevice);
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com"); olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
// feed sync result that includes master key, ssk, device key // feed sync result that includes master key, ssk, device key
@@ -354,7 +358,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig, ["ed25519:" + bobMasterPubkey]: sskSig,
}, },
}; };
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@@ -383,7 +387,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobPubkey]: sig, ["ed25519:" + bobPubkey]: sig,
}, },
}; };
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
// Bob's device key should be TOFU // Bob's device key should be TOFU
@@ -417,8 +421,8 @@ describe("Cross Signing", function() {
null, null,
aliceKeys, aliceKeys,
); );
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com");
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => {}; alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {}; alice.uploadKeySignatures = async () => {};
@@ -433,14 +437,14 @@ describe("Cross Signing", function() {
]); ]);
const keyChangePromise = new Promise((resolve, reject) => { const keyChangePromise = new Promise((resolve, reject) => {
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => { alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
if (userId === "@bob:example.com") { if (userId === "@bob:example.com") {
resolve(); resolve();
} }
}); });
}); });
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
.Osborne2; .Osborne2;
const aliceDevice = { const aliceDevice = {
user_id: "@alice:example.com", user_id: "@alice:example.com",
@@ -448,7 +452,7 @@ describe("Cross Signing", function() {
}; };
aliceDevice.keys = deviceInfo.keys; aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms; aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice); await alice.crypto._signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account(); const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create(); bobOlmAccount.create();
@@ -602,7 +606,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig, ["ed25519:" + bobMasterPubkey]: sskSig,
}, },
}; };
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@@ -625,7 +629,7 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey", "ed25519:Dynabook": "someOtherPubkey",
}, },
}; };
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
// Bob's device key should be untrusted // Bob's device key should be untrusted
@@ -669,7 +673,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig, ["ed25519:" + bobMasterPubkey]: sskSig,
}, },
}; };
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@@ -697,7 +701,7 @@ describe("Cross Signing", function() {
bobDevice.signatures = {}; bobDevice.signatures = {};
bobDevice.signatures["@bob:example.com"] = {}; bobDevice.signatures["@bob:example.com"] = {};
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
// Alice verifies Bob's SSK // Alice verifies Bob's SSK
@@ -729,7 +733,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey2]: sskSig2, ["ed25519:" + bobMasterPubkey2]: sskSig2,
}, },
}; };
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@@ -766,7 +770,7 @@ describe("Cross Signing", function() {
// Alice gets new signature for device // Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString); const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice, Dynabook: bobDevice,
}); });
@@ -801,20 +805,20 @@ describe("Cross Signing", function() {
bob.uploadKeySignatures = async () => {}; bob.uploadKeySignatures = async () => {};
// set Bob's cross-signing key // set Bob's cross-signing key
await resetCrossSigningKeys(bob); await resetCrossSigningKeys(bob);
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: { Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: { keys: {
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key, "curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key, "ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key,
}, },
verified: 1, verified: 1,
known: true, known: true,
}, },
}); });
alice._crypto._deviceList.storeCrossSigningForUser( alice.crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com", "@bob:example.com",
bob._crypto._crossSigningInfo.toStorage(), bob.crypto._crossSigningInfo.toStorage(),
); );
alice.uploadDeviceSigningKeys = async () => {}; alice.uploadDeviceSigningKeys = async () => {};
@@ -834,7 +838,7 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy(); expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted // "forget" that Bob is trusted
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"] delete alice.crypto._deviceList._crossSigningInfo["@bob:example.com"]
.keys.master.signatures["@alice:example.com"]; .keys.master.signatures["@alice:example.com"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com"); const bobTrust2 = alice.checkUserTrust("@bob:example.com");
@@ -844,9 +848,9 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => { upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve; upgradeResolveFunc = resolve;
}); });
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
await new Promise((resolve) => { await new Promise((resolve) => {
alice._crypto.on("userTrustStatusChanged", resolve); alice.crypto.on("userTrustStatusChanged", resolve);
}); });
await upgradePromise; await upgradePromise;

View File

@@ -6,7 +6,7 @@ export async function resetCrossSigningKeys(client, {
level, level,
authUploadDeviceSigningKeys = async func => await func(), authUploadDeviceSigningKeys = async func => await func(),
} = {}) { } = {}) {
const crypto = client._crypto; const crypto = client.crypto;
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys); const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
try { try {

View File

@@ -47,7 +47,7 @@ async function makeTestClient(userInfo, options) {
await client.initCrypto(); await client.initCrypto();
// No need to download keys for these tests // No need to download keys for these tests
client._crypto.downloadKeys = async function() {}; client.crypto.downloadKeys = async function() {};
return client; return client;
} }
@@ -99,11 +99,11 @@ describe("Secrets", function() {
}, },
}, },
); );
alice._crypto._crossSigningInfo.setKeys({ alice.crypto._crossSigningInfo.setKeys({
master: signingkeyInfo, master: signingkeyInfo,
}); });
const secretStorage = alice._crypto._secretStorage; const secretStorage = alice.crypto._secretStorage;
alice.setAccountData = async function(eventType, contents, callback) { alice.setAccountData = async function(eventType, contents, callback) {
alice.store.storeAccountDataEvents([ alice.store.storeAccountDataEvents([
@@ -120,7 +120,7 @@ describe("Secrets", function() {
const keyAccountData = { const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
}; };
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master'); await alice.crypto._crossSigningInfo.signObject(keyAccountData, 'master');
alice.store.storeAccountDataEvents([ alice.store.storeAccountDataEvents([
new MatrixEvent({ new MatrixEvent({
@@ -234,11 +234,11 @@ describe("Secrets", function() {
}, },
); );
const vaxDevice = vax.client._crypto._olmDevice; const vaxDevice = vax.client.crypto._olmDevice;
const osborne2Device = osborne2.client._crypto._olmDevice; const osborne2Device = osborne2.client.crypto._olmDevice;
const secretStorage = osborne2.client._crypto._secretStorage; const secretStorage = osborne2.client.crypto._secretStorage;
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { osborne2.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": { "VAX": {
user_id: "@alice:example.com", user_id: "@alice:example.com",
device_id: "VAX", device_id: "VAX",
@@ -249,7 +249,7 @@ describe("Secrets", function() {
}, },
}, },
}); });
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { vax.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": { "Osborne2": {
user_id: "@alice:example.com", user_id: "@alice:example.com",
device_id: "Osborne2", device_id: "Osborne2",
@@ -265,7 +265,7 @@ describe("Secrets", function() {
const otks = (await osborne2Device.getOneTimeKeys()).curve25519; const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
await osborne2Device.markKeysAsPublished(); await osborne2Device.markKeysAsPublished();
await vax.client._crypto._olmDevice.createOutboundSession( await vax.client.crypto._olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key, osborne2Device.deviceCurve25519Key,
Object.values(otks)[0], Object.values(otks)[0],
); );
@@ -334,8 +334,8 @@ describe("Secrets", function() {
createSecretStorageKey, createSecretStorageKey,
}); });
const crossSigning = bob._crypto._crossSigningInfo; const crossSigning = bob.crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage; const secretStorage = bob.crypto._secretStorage;
expect(crossSigning.getId()).toBeTruthy(); expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)) expect(await crossSigning.isStoredInSecretStorage(secretStorage))
@@ -376,10 +376,10 @@ describe("Secrets", function() {
]); ]);
this.emit("accountData", event); this.emit("accountData", event);
}; };
bob._crypto.checkKeyBackup = async () => {}; bob.crypto._backupManager.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo; const crossSigning = bob.crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage; const secretStorage = bob.crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key // Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
@@ -394,7 +394,7 @@ describe("Secrets", function() {
}); });
// Clear local cross-signing keys and read from secret storage // Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser( bob.crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com", "@bob:example.com",
crossSigning.toStorage(), crossSigning.toStorage(),
); );
@@ -479,7 +479,7 @@ describe("Secrets", function() {
}, },
}), }),
]); ]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: { keys: {
master: { master: {
user_id: "@alice:example.com", user_id: "@alice:example.com",
@@ -619,7 +619,7 @@ describe("Secrets", function() {
}, },
}), }),
]); ]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: { keys: {
master: { master: {
user_id: "@alice:example.com", user_id: "@alice:example.com",

View File

@@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function()
verificationMethods: [verificationMethods.SAS], verificationMethods: [verificationMethods.SAS],
}, },
); );
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() { alice.client.crypto._deviceList.getRawStoredDevicesForUser = function() {
return { return {
Dynabook: { Dynabook: {
keys: { keys: {

View File

@@ -87,8 +87,8 @@ describe("SAS verification", function() {
}, },
); );
const aliceDevice = alice.client._crypto._olmDevice; const aliceDevice = alice.client.crypto._olmDevice;
const bobDevice = bob.client._crypto._olmDevice; const bobDevice = bob.client.crypto._olmDevice;
ALICE_DEVICES = { ALICE_DEVICES = {
Osborne2: { Osborne2: {
@@ -114,14 +114,14 @@ describe("SAS verification", function() {
}, },
}; };
alice.client._crypto._deviceList.storeDevicesForUser( alice.client.crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES, "@bob:example.com", BOB_DEVICES,
); );
alice.client.downloadKeys = () => { alice.client.downloadKeys = () => {
return Promise.resolve(); return Promise.resolve();
}; };
bob.client._crypto._deviceList.storeDevicesForUser( bob.client.crypto._deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES, "@alice:example.com", ALICE_DEVICES,
); );
bob.client.downloadKeys = () => { bob.client.downloadKeys = () => {
@@ -296,9 +296,9 @@ describe("SAS verification", function() {
await resetCrossSigningKeys(bob.client); await resetCrossSigningKeys(bob.client);
bob.client._crypto._deviceList.storeCrossSigningForUser( bob.client.crypto._deviceList.storeCrossSigningForUser(
"@alice:example.com", { "@alice:example.com", {
keys: alice.client._crypto._crossSigningInfo.keys, keys: alice.client.crypto._crossSigningInfo.keys,
}, },
); );

View File

@@ -69,7 +69,7 @@ describe("self-verifications", () => {
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve()); const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
const client = { const client = {
_crypto: { crypto: {
_crossSigningInfo, _crossSigningInfo,
_secretStorage, _secretStorage,
storeSessionBackupPrivateKey, storeSessionBackupPrivateKey,

View File

@@ -36,7 +36,7 @@ export async function makeTestClients(userInfos, options) {
}); });
const client = clientMap[userId][deviceId]; const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted() ? const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client._crypto) : event.attemptDecryption(client.crypto) :
Promise.resolve(); Promise.resolve();
decryptionPromise.then( decryptionPromise.then(

View File

@@ -144,12 +144,12 @@ describe("MatrixClient", function() {
scheduler: scheduler, scheduler: scheduler,
userId: userId, userId: userId,
}); });
// FIXME: We shouldn't be yanking _http like this. // FIXME: We shouldn't be yanking http like this.
client._http = [ client.http = [
"authedRequest", "getContentUri", "request", "uploadContent", "authedRequest", "getContentUri", "request", "uploadContent",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
client._http.authedRequest.mockImplementation(httpReq); client.http.authedRequest.mockImplementation(httpReq);
client._http.request.mockImplementation(httpReq); client.http.request.mockImplementation(httpReq);
// set reasonable working defaults // set reasonable working defaults
acceptKeepalives = true; acceptKeepalives = true;
@@ -166,7 +166,7 @@ describe("MatrixClient", function() {
// means they may call /events and then fail an expect() which will fail // means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved // a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run. // promises to stop the client from continuing to run.
client._http.authedRequest.mockImplementation(function() { client.http.authedRequest.mockImplementation(function() {
return new Promise(() => {}); return new Promise(() => {});
}); });
}); });

View File

@@ -1314,7 +1314,7 @@ describe("Room", function() {
isRoomEncrypted: function() { isRoomEncrypted: function() {
return false; return false;
}, },
_http: { http: {
serverResponse, serverResponse,
authedRequest: function() { authedRequest: function() {
if (this.serverResponse instanceof Error) { if (this.serverResponse instanceof Error) {
@@ -1384,7 +1384,7 @@ describe("Room", function() {
} }
expect(hasThrown).toEqual(true); expect(hasThrown).toEqual(true);
client._http.serverResponse = [memberEvent]; client.http.serverResponse = [memberEvent];
await room.loadMembersIfNeeded(); await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar"); const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A"); expect(memberA.name).toEqual("User A");

View File

@@ -0,0 +1,24 @@
/*
Copyright 2021 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 interface IIdentityServerProvider {
/**
* Gets an access token for use against the identity server,
* for the associated client.
* @returns {Promise<string>} Resolves to the access token.
*/
getAccessToken(): Promise<string>;
}

28
src/@types/partials.ts Normal file
View File

@@ -0,0 +1,28 @@
/*
Copyright 2021 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 interface IImageInfo {
size?: number;
mimetype?: string;
thumbnail_info?: { // eslint-disable-line camelcase
w?: number;
h?: number;
size?: number;
mimetype?: string;
};
w?: number;
h?: number;
}

98
src/@types/requests.ts Normal file
View File

@@ -0,0 +1,98 @@
/*
Copyright 2021 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.
*/
import { Callback } from "../client";
export interface IJoinRoomOpts {
/**
* True to do a room initial sync on the resulting
* room. If false, the <strong>returned Room object will have no current state.
* </strong> Default: true.
*/
syncRoom?: boolean;
/**
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
*/
inviteSignUrl?: string;
/**
* The server names to try and join through in addition to those that are automatically chosen.
*/
viaServers?: string[];
}
export interface IRedactOpts {
reason?: string;
}
export interface ISendEventResponse {
event_id: string; // eslint-disable-line camelcase
}
export interface IPresenceOpts {
presence: "online" | "offline" | "unavailable";
status_msg?: string; // eslint-disable-line camelcase
}
export interface IPaginateOpts {
backwards?: boolean;
limit?: number;
}
export interface IGuestAccessOpts {
allowJoin: boolean;
allowRead: boolean;
}
export interface ISearchOpts {
keys?: string[];
query: string;
}
export interface IEventSearchOpts {
filter: any; // TODO: Types
term: string;
}
export interface ICreateRoomOpts {
room_alias_name?: string; // eslint-disable-line camelcase
visibility?: "public" | "private";
name?: string;
topic?: string;
preset?: string;
// TODO: Types (next line)
invite_3pid?: any[]; // eslint-disable-line camelcase
}
export interface IRoomDirectoryOptions {
server?: string;
limit?: number;
since?: string;
// TODO: Proper types
filter?: any & {generic_search_term: string}; // eslint-disable-line camelcase
}
export interface IUploadOpts {
name?: string;
includeFilename?: boolean;
type?: string;
rawResponse?: boolean;
onlyContentUri?: boolean;
callback?: Callback;
progressHandler?: (state: {loaded: number, total: number}) => void;
}

21
src/@types/signed.ts Normal file
View File

@@ -0,0 +1,21 @@
/*
Copyright 2021 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 interface ISignatures {
[entity: string]: {
[keyId: string]: string;
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

8391
src/client.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -725,7 +725,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
/** /**
* Request cross-signing keys from another device during verification. * Request cross-signing keys from another device during verification.
* *
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface * @param {MatrixClient} baseApis base Matrix API interface
* @param {string} userId The user ID being verified * @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified * @param {string} deviceId The device ID being verified
*/ */
@@ -739,7 +739,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
// it. We return here in order to test. // it. We return here in order to test.
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const client = baseApis; const client = baseApis;
const original = client._crypto._crossSigningInfo; const original = client.crypto._crossSigningInfo;
// We already have all of the infrastructure we need to validate and // We already have all of the infrastructure we need to validate and
// cache cross-signing keys, so instead of replicating that, here we set // cache cross-signing keys, so instead of replicating that, here we set
@@ -775,7 +775,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
// also request and cache the key backup key // also request and cache the key backup key
const backupKeyPromise = new Promise(async resolve => { const backupKeyPromise = new Promise(async resolve => {
const cachedKey = await client._crypto.getSessionBackupPrivateKey(); const cachedKey = await client.crypto.getSessionBackupPrivateKey();
if (!cachedKey) { if (!cachedKey) {
logger.info("No cached backup key found. Requesting..."); logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret( const secretReq = client.requestSecret(
@@ -785,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
logger.info("Got key backup key, decoding..."); logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key); const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing..."); logger.info("Decoded backup key, storing...");
client._crypto.storeSessionBackupPrivateKey( client.crypto.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey), Uint8Array.from(decodedKey),
); );
logger.info("Backup key stored. Starting backup restore..."); logger.info("Backup key stored. Starting backup restore...");

View File

@@ -204,7 +204,7 @@ export class EncryptionSetupOperation {
// The backup is trusted because the user provided the private key. // The backup is trusted because the user provided the private key.
// Sign the backup with the cross signing key so the key backup can // Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing. // be trusted via cross-signing.
await baseApis._http.authedRequest( await baseApis.http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version, undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
undefined, { undefined, {
algorithm: this._keyBackupInfo.algorithm, algorithm: this._keyBackupInfo.algorithm,
@@ -214,7 +214,7 @@ export class EncryptionSetupOperation {
); );
} else { } else {
// add new key backup // add new key backup
await baseApis._http.authedRequest( await baseApis.http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, "POST", "/room_keys/version",
undefined, this._keyBackupInfo, undefined, this._keyBackupInfo,
{ prefix: PREFIX_UNSTABLE }, { prefix: PREFIX_UNSTABLE },

View File

@@ -444,7 +444,7 @@ export class SecretStorage extends EventEmitter {
&& this._incomingRequests[deviceId][content.request_id]) { && this._incomingRequests[deviceId][content.request_id]) {
logger.info("received request cancellation for secret (" + sender logger.info("received request cancellation for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")"); + ", " + deviceId + ", " + content.request_id + ")");
this.baseApis.emit("crypto.secrets.requestCancelled", { this._baseApis.emit("crypto.secrets.requestCancelled", {
user_id: sender, user_id: sender,
device_id: deviceId, device_id: deviceId,
request_id: content.request_id, request_id: content.request_id,
@@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter {
}; };
const encryptedContent = { const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key,
ciphertext: {}, ciphertext: {},
}; };
await olmlib.ensureOlmSessionsForDevices( await olmlib.ensureOlmSessionsForDevices(
this._baseApis._crypto._olmDevice, this._baseApis.crypto._olmDevice,
this._baseApis, this._baseApis,
{ {
[sender]: [ [sender]: [
@@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter {
encryptedContent.ciphertext, encryptedContent.ciphertext,
this._baseApis.getUserId(), this._baseApis.getUserId(),
this._baseApis.deviceId, this._baseApis.deviceId,
this._baseApis._crypto._olmDevice, this._baseApis.crypto._olmDevice,
sender, sender,
this._baseApis.getStoredDevice(sender, deviceId), this._baseApis.getStoredDevice(sender, deviceId),
payload, payload,
@@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter {
if (requestControl) { if (requestControl) {
// make sure that the device that sent it is one of the devices that // make sure that the device that sent it is one of the devices that
// we requested from // we requested from
const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey( const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM, olmlib.OLM_ALGORITHM,
event.getSenderKey(), event.getSenderKey(),
); );

View File

@@ -46,7 +46,7 @@ export const DECRYPTION_CLASSES = {};
* @param {string} params.deviceId The identifier for this device. * @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core * @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface * @param {MatrixClient} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to * @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event * @param {object} params.config The body of the m.room.encryption event
*/ */
@@ -102,7 +102,7 @@ export class EncryptionAlgorithm {
* @param {string} params.userId The UserID for the local user * @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core * @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface * @param {MatrixClient} baseApis base matrix api interface
* @param {string=} params.roomId The ID of the room we will be receiving * @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events. * from. Null for to-device events.
*/ */

View File

@@ -413,9 +413,8 @@ MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
); );
// don't wait for it to complete // don't wait for it to complete
this._crypto.backupGroupSession( this._crypto._backupManager.backupGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [], this._olmDevice.deviceCurve25519Key, sessionId,
sessionId, key.key,
); );
return new OutboundSessionInfo(sessionId, sharedHistory); return new OutboundSessionInfo(sessionId, sharedHistory);
@@ -1425,11 +1424,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
}); });
}).then(() => { }).then(() => {
// don't wait for the keys to be backed up for the server // don't wait for the keys to be backed up for the server
this._crypto.backupGroupSession( this._crypto._backupManager.backupGroupSession(senderKey, content.session_id);
content.room_id, senderKey, forwardingKeyChain,
content.session_id, content.session_key, keysClaimed,
exportFormat,
);
}).catch((e) => { }).catch((e) => {
logger.error(`Error handling m.room_key_event: ${e}`); logger.error(`Error handling m.room_key_event: ${e}`);
}); });
@@ -1645,14 +1640,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
).then(() => { ).then(() => {
if (opts.source !== "backup") { if (opts.source !== "backup") {
// don't wait for it to complete // don't wait for it to complete
this._crypto.backupGroupSession( this._crypto._backupManager.backupGroupSession(
session.room_id, session.sender_key, session.session_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).catch((e) => { ).catch((e) => {
// This throws if the upload failed, but this is fine // This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry. // since it will have written it to the db and will retry.

131
src/crypto/api.ts Normal file
View File

@@ -0,0 +1,131 @@
/*
Copyright 2021 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.
*/
import { DeviceInfo } from "./deviceinfo";
import { IKeyBackupVersion } from "./keybackup";
import { ISecretStorageKeyInfo } from "../matrix";
// TODO: Merge this with crypto.js once converted
export enum CrossSigningKey {
Master = "master",
SelfSigning = "self_signing",
UserSigning = "user_signing",
}
export interface IEncryptedEventInfo {
/**
* whether the event is encrypted (if not encrypted, some of the other properties may not be set)
*/
encrypted: boolean;
/**
* the sender's key
*/
senderKey: string;
/**
* the algorithm used to encrypt the event
*/
algorithm: string;
/**
* whether we can be sure that the owner of the senderKey sent the event
*/
authenticated: boolean;
/**
* the sender's device information, if available
*/
sender?: DeviceInfo;
/**
* if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set)
*/
mismatchedSender: boolean;
}
export interface IRecoveryKey {
keyInfo: {
pubkey: Uint8Array;
passphrase?: {
algorithm: string;
iterations: number;
salt: string;
};
};
privateKey: Uint8Array;
encodedPrivateKey: string;
}
export interface ICreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* Returns:
* {Promise<Object>} Object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<IRecoveryKey>;
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
*/
keyBackupInfo?: IKeyBackupVersion;
/**
* If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
*/
setupNewKeyBackup?: boolean;
/**
* Reset even if keys already exist.
*/
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
export interface ISecretStorageKey {
keyId: string;
keyInfo: ISecretStorageKeyInfo;
}
export interface IAddSecretStorageKeyOpts {
// depends on algorithm
// TODO: Types
}
export interface IImportOpts {
stage: string; // TODO: Enum
successes: number;
failures: number;
total: number;
}
export interface IImportRoomKeysOpts {
progressCallback: (stage: IImportOpts) => void;
untrusted?: boolean;
source?: string; // TODO: Enum
}

651
src/crypto/backup.ts Normal file
View File

@@ -0,0 +1,651 @@
/*
Copyright 2021 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.
*/
/**
* @module crypto/backup
*
* Classes for dealing with key backup.
*/
import { MatrixClient } from "../client";
import { logger } from "../logger";
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo"
import { DeviceTrustLevel } from './CrossSigning';
import { keyFromPassphrase } from './key_passphrase';
import { sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
type AuthData = Record<string, any>;
type BackupInfo = {
algorithm: string,
auth_data: AuthData, // eslint-disable-line camelcase
[properties: string]: any,
};
type SigInfo = {
deviceId: string,
valid?: boolean | null, // true: valid, false: invalid, null: cannot attempt validation
device?: DeviceInfo | null,
crossSigningId?: boolean,
deviceTrust?: DeviceTrustLevel,
};
type TrustInfo = {
usable: boolean, // is the backup trusted, true iff there is a sig that is valid & from a trusted device
sigs: SigInfo[],
};
/** A function used to get the secret key for a backup.
*/
type GetKey = () => Promise<Uint8Array>;
interface BackupAlgorithmClass {
algorithmName: string;
// initialize from an existing backup
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
// prepare a brand new backup
prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]>;
}
interface BackupAlgorithm {
encryptSession(data: Record<string, any>): Promise<any>;
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
authData: AuthData;
keyMatches(key: Uint8Array): Promise<boolean>;
free(): void;
}
/**
* Manages the key backup.
*/
export class BackupManager {
private algorithm: BackupAlgorithm | undefined;
private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
private sendingBackups: boolean; // Are we currently sending backups?
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
this.checkedForBackup = false;
this.sendingBackups = false;
}
public get version(): string | undefined {
return this.backupInfo && this.backupInfo.version;
}
public static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
const Algorithm = algorithmsByName[info.algorithm];
if (!Algorithm) {
throw new Error("Unknown backup algorithm");
}
return await Algorithm.init(info.auth_data, getKey);
}
public async enableKeyBackup(info: BackupInfo): Promise<void> {
this.backupInfo = info;
if (this.algorithm) {
this.algorithm.free();
}
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
this.baseApis.emit('crypto.keyBackupStatus', true);
// There may be keys left over from a partially completed backup, so
// schedule a send to check.
this.scheduleKeyBackupSend();
}
/**
* Disable backing up of keys.
*/
public disableKeyBackup(): void {
if (this.algorithm) {
this.algorithm.free();
}
this.algorithm = undefined;
this.backupInfo = undefined;
this.baseApis.emit('crypto.keyBackupStatus', false);
}
public getKeyBackupEnabled(): boolean | null {
if (!this.checkedForBackup) {
return null;
}
return Boolean(this.algorithm);
}
public async prepareKeyBackupVersion(
key?: string | Uint8Array | null,
algorithm?: string | undefined,
): Promise<BackupInfo> {
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
if (!Algorithm) {
throw new Error("Unknown backup algorithm");
}
const [privateKey, authData] = await Algorithm.prepare(key);
const recoveryKey = encodeRecoveryKey(privateKey);
return {
algorithm: Algorithm.algorithmName,
auth_data: authData,
recovery_key: recoveryKey,
privateKey,
};
}
public async createKeyBackupVersion(info: BackupInfo): Promise<void> {
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
}
/**
* Check the server for an active key backup and
* if one is present and has a valid signature from
* one of the user's verified devices, start backing up
* to it.
*/
public async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> {
logger.log("Checking key backup status...");
if (this.baseApis.isGuest()) {
logger.log("Skipping key backup check since user is guest");
this.checkedForBackup = true;
return null;
}
let backupInfo: BackupInfo;
try {
backupInfo = await this.baseApis.getKeyBackupVersion();
} catch (e) {
logger.log("Error checking for active key backup", e);
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;
}
this.checkedForBackup = true;
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
logger.log(
"Found usable key backup v" + backupInfo.version +
": enabling key backups",
);
await this.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) {
logger.log("No usable key backup: disabling key backup");
this.disableKeyBackup();
} else if (!trustInfo.usable && !this.backupInfo) {
logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) {
logger.log(
"On backup version " + this.backupInfo.version + " but found " +
"version " + backupInfo.version + ": switching.",
);
this.disableKeyBackup();
await this.enableKeyBackup(backupInfo);
// We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases
// because we don't remember what backup version we uploaded keys to:
// see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup();
} else {
logger.log("Backup version " + backupInfo.version + " still current");
}
}
return { backupInfo, trustInfo };
}
/**
* Forces a re-check of the key backup and enables/disables it
* as appropriate.
*
* @return {Object} Object with backup info (as returned by
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*/
public async checkKeyBackup(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> {
this.checkedForBackup = false;
return this.checkAndStart();
}
/**
* Check if the given backup info is trusted.
*
* @param {object} backupInfo key backup info dict from /room_keys/version
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
* deviceId: [string],
* device: [DeviceInfo || null],
* ]
* }
*/
public async isKeyBackupTrusted(backupInfo: BackupInfo): Promise<TrustInfo> {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
};
if (
!backupInfo ||
!backupInfo.algorithm ||
!backupInfo.auth_data ||
!backupInfo.auth_data.public_key ||
!backupInfo.auth_data.signatures
) {
logger.info("Key backup is absent or missing required data");
return ret;
}
const trustedPubkey = this.baseApis.crypto._sessionStore.getLocalTrustedBackupPubKey();
if (backupInfo.auth_data.public_key === trustedPubkey) {
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
ret.trusted_locally = true;
}
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
if (keyIdParts[0] !== 'ed25519') {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
continue;
}
// Could be a cross-signing master key, but just say this is the device
// ID for backwards compat
const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
// first check to see if it's from our cross-signing key
const crossSigningId = this.baseApis.crypto._crossSigningInfo.getId();
if (crossSigningId === sigInfo.deviceId) {
sigInfo.crossSigningId = true;
try {
await verifySignature(
this.baseApis.crypto._olmDevice,
backupInfo.auth_data,
this.baseApis.getUserId(),
sigInfo.deviceId,
crossSigningId,
);
sigInfo.valid = true;
} catch (e) {
logger.warn(
"Bad signature from cross signing key " + crossSigningId, e,
);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
continue;
}
// Now look for a sig from a device
// At some point this can probably go away and we'll just support
// it being signed by the cross-signing master key
const device = this.baseApis.crypto._deviceList.getStoredDevice(
this.baseApis.getUserId(), sigInfo.deviceId,
);
if (device) {
sigInfo.device = device;
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
this.baseApis.getUserId(), sigInfo.deviceId,
);
try {
await verifySignature(
this.baseApis.crypto._olmDevice,
backupInfo.auth_data,
this.baseApis.getUserId(),
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
logger.info(
"Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() +
" device ID " + device.deviceId + " fingerprint: " +
device.getFingerprint(), backupInfo.auth_data, e,
);
sigInfo.valid = false;
}
} else {
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
logger.info("Ignoring signature from unknown key " + keyId);
}
ret.sigs.push(sigInfo);
}
ret.usable = ret.sigs.some((s) => {
return (
s.valid && (
(s.device && s.deviceTrust.isVerified()) ||
(s.crossSigningId)
)
);
});
ret.usable = ret.usable || ret.trusted_locally;
return ret;
}
/**
* Schedules sending all keys waiting to be sent to the backup, if not already
* scheduled. Retries if necessary.
*
* @param maxDelay Maximum delay to wait in ms. 0 means no delay.
*/
public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
if (this.sendingBackups) return;
this.sendingBackups = true;
try {
// wait between 0 and `maxDelay` seconds, to avoid backup
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * maxDelay;
await sleep(delay, undefined);
let numFailures = 0; // number of consecutive failures
for (;;) {
if (!this.algorithm) {
return;
}
try {
const numBackedUp =
await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
if (numBackedUp === 0) {
// no sessions left needing backup: we're done
return;
}
numFailures = 0;
} catch (err) {
numFailures++;
logger.log("Key backup request failed", err);
if (err.data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) {
// Re-check key backup status on error, so we can be
// sure to present the current situation when asked.
await this.checkKeyBackup();
// Backup version has changed or this backup version
// has been deleted
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
throw err;
}
}
}
if (numFailures) {
// exponential backoff if we have failures
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined);
}
}
} finally {
this.sendingBackups = false;
}
}
/**
* Take some e2e keys waiting to be backed up and send them
* to the backup.
*
* @param {integer} limit Maximum number of keys to back up
* @returns {integer} Number of sessions backed up
*/
private async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto._cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) {
return 0;
}
let remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
const data = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
if (data[roomId] === undefined) {
data[roomId] = { sessions: {} };
}
const sessionData = await this.baseApis.crypto._olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData,
);
sessionData.algorithm = MEGOLM_ALGORITHM;
const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length;
const userId = this.baseApis.crypto._deviceList.getUserByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey,
);
const device = this.baseApis.crypto._deviceList.getDeviceByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey,
);
const verified = this.baseApis.crypto._checkDeviceInfoTrust(userId, device).isVerified();
data[roomId]['sessions'][session.sessionId] = {
first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount,
is_verified: verified,
session_data: await this.algorithm.encryptSession(sessionData),
};
}
await this.baseApis.sendKeyBackup(
undefined, undefined, this.backupInfo.version,
{ rooms: data },
);
await this.baseApis.crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
return sessions.length;
}
public async backupGroupSession(
senderKey: string, sessionId: string,
): Promise<void> {
await this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey,
sessionId: sessionId,
}]);
if (this.backupInfo) {
// don't wait for this to complete: it will delay so
// happens in the background
this.scheduleKeyBackupSend();
}
// if this.backupInfo is not set, then the keys will be backed up when
// this.enableKeyBackup is called
}
/**
* Marks all group sessions as needing to be backed up and schedules them to
* upload in the background as soon as possible.
*/
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
}
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
* (which will be equal to the number of sessions in the store).
*/
public async flagAllGroupSessionsForBackup(): Promise<number> {
await this.baseApis.crypto._cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_BACKUP,
],
(txn) => {
this.baseApis.crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (session !== null) {
this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([session], txn);
}
});
},
);
const remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
return remaining;
}
/**
* Counts the number of end to end session keys that are waiting to be backed up
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/
public countSessionsNeedingBackup(): Promise<number> {
return this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
}
}
export class Curve25519 implements BackupAlgorithm {
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
constructor(
public authData: AuthData,
private publicKey: any, // FIXME: PkEncryption
private getKey: () => Promise<Uint8Array>,
) {}
public static async init(
authData: AuthData,
getKey: () => Promise<Uint8Array>,
): Promise<Curve25519> {
if (!authData || !authData.public_key) {
throw new Error("auth_data missing required information");
}
const publicKey = new global.Olm.PkEncryption();
publicKey.set_recipient_key(authData.public_key);
return new Curve25519(authData, publicKey, getKey);
}
public static async prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> {
const decryption = new global.Olm.PkDecryption();
try {
const authData: AuthData = {};
if (!key) {
authData.public_key = decryption.generate_key();
} else if (key instanceof Uint8Array) {
authData.public_key = decryption.init_with_private_key(key);
} else {
const derivation = await keyFromPassphrase(key);
authData.private_key_salt = derivation.salt;
authData.private_key_iterations = derivation.iterations;
authData.public_key = decryption.init_with_private_key(derivation.key);
}
const publicKey = new global.Olm.PkEncryption();
publicKey.set_recipient_key(authData.public_key);
return [
decryption.get_private_key(),
authData,
]
} finally {
decryption.free();
}
}
public async encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
delete plainText.first_known_index;
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
const backupPubKey = decryption.init_with_private_key(privKey);
if (backupPubKey !== this.authData.public_key) {
// eslint-disable-next-line no-throw-literal
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
}
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(decryption.decrypt(
sessionData.session_data.ephemeral,
sessionData.session_data.mac,
sessionData.session_data.ciphertext,
));
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
}
}
return keys;
} finally {
decryption.free();
}
}
public async keyMatches(key: Uint8Array): Promise<boolean> {
const decryption = new global.Olm.PkDecryption();
let pubKey;
try {
pubKey = decryption.init_with_private_key(key);
} finally {
decryption.free();
}
return pubKey === this.authData.public_key;
}
public free(): void {
this.publicKey.free();
}
}
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
[Curve25519.algorithmName]: Curve25519,
};
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -19,10 +19,23 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes'; import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json"; import anotherjson from "another-json";
import { logger } from '../logger'; import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "../matrix";
// FIXME: these types should eventually go in a different file // FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>; type Signatures = Record<string, Record<string, string>>;
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
device_data: ISecretStorageKeyInfo & { // eslint-disable-line camelcase
algorithm: string;
account: string; // pickle
};
}
export interface IDehydratedDeviceKeyInfo {
passphrase?: string;
}
interface DeviceKeys { interface DeviceKeys {
algorithms: Array<string>; algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase device_id: string; // eslint-disable-line camelcase
@@ -192,7 +205,7 @@ export class DehydrationManager {
} }
logger.log("Uploading account to server"); logger.log("Uploading account to server");
const dehydrateResult = await this.crypto._baseApis._http.authedRequest( const dehydrateResult = await this.crypto._baseApis.http.authedRequest(
undefined, undefined,
"PUT", "PUT",
"/dehydrated_device", "/dehydrated_device",
@@ -255,7 +268,7 @@ export class DehydrationManager {
} }
logger.log("Uploading keys to server"); logger.log("Uploading keys to server");
await this.crypto._baseApis._http.authedRequest( await this.crypto._baseApis.http.authedRequest(
undefined, undefined,
"POST", "POST",
"/keys/upload/" + encodeURI(deviceId), "/keys/upload/" + encodeURI(deviceId),

View File

@@ -2,7 +2,7 @@
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd Copyright 2018-2019 New Vector Ltd
Copyright 2019-2020 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -26,7 +26,6 @@ import { EventEmitter } from 'events';
import { ReEmitter } from '../ReEmitter'; import { ReEmitter } from '../ReEmitter';
import { logger } from '../logger'; import { logger } from '../logger';
import * as utils from "../utils"; import * as utils from "../utils";
import { sleep } from "../utils";
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
import * as olmlib from "./olmlib"; import * as olmlib from "./olmlib";
import { DeviceList } from "./DeviceList"; import { DeviceList } from "./DeviceList";
@@ -58,6 +57,7 @@ import { KeySignatureUploadError } from "../errors";
import { decryptAES, encryptAES } from './aes'; import { decryptAES, encryptAES } from './aes';
import { DehydrationManager } from './dehydration'; import { DehydrationManager } from './dehydration';
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { BackupManager } from "./backup";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -85,7 +85,6 @@ export function isCryptoAvailable() {
} }
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
/** /**
* Cryptography bits * Cryptography bits
@@ -97,7 +96,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
* *
* @internal * @internal
* *
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface * @param {MatrixClient} baseApis base matrix api interface
* *
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore * @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
* Store to be used for end-to-end crypto session data * Store to be used for end-to-end crypto session data
@@ -154,13 +153,36 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
} else { } else {
this._verificationMethods = defaultVerificationMethods; this._verificationMethods = defaultVerificationMethods;
} }
// track whether this device's megolm keys are being backed up incrementally
// to the server or not. this._backupManager = new BackupManager(baseApis, async (algorithm) => {
// XXX: this should probably have a single source of truth from OlmAccount // try to get key from cache
this.backupInfo = null; // The info dict from /room_keys/version const cachedKey = await this.getSessionBackupPrivateKey();
this.backupKey = null; // The encryption key object if (cachedKey) {
this._checkedForBackup = false; // Have we checked the server for a backup we can use? return cachedKey;
this._sendingBackups = false; // Are we currently sending backups? }
// try to get key from secret storage
const storedKey = await this.getSecret("m.megolm_backup.v1");
if (storedKey) {
// ensure that the key is in the right format. If not, fix the key and
// store the fixed version
const fixedKey = fixBackupKey(storedKey);
if (fixedKey) {
const [keyId] = await this._crypto.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]);
}
return olmlib.decodeBase64(fixedKey || storedKey);
}
// try to get key from app
if (this._baseApis._cryptoCallbacks && this._baseApis._cryptoCallbacks.getBackupKey) {
return await this._baseApis._cryptoCallbacks.getBackupKey(algorithm);
}
throw new Error("Unable to get private key");
});
this._olmDevice = new OlmDevice(cryptoStore); this._olmDevice = new OlmDevice(cryptoStore);
this._deviceList = new DeviceList( this._deviceList = new DeviceList(
@@ -232,7 +254,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
// processing the response. // processing the response.
this._sendKeyRequestsImmediately = false; this._sendKeyRequestsImmediately = false;
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {}; const cryptoCallbacks = this._baseApis.cryptoCallbacks || {};
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice); const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice);
this._crossSigningInfo = new CrossSigningInfo( this._crossSigningInfo = new CrossSigningInfo(
@@ -331,7 +353,7 @@ Crypto.prototype.init = async function(opts) {
this._deviceList.startTrackingDeviceList(this._userId); this._deviceList.startTrackingDeviceList(this._userId);
logger.log("Crypto: checking for key backup..."); logger.log("Crypto: checking for key backup...");
this._checkAndStartKeyBackup(); this._backupManager.checkAndStart();
}; };
/** /**
@@ -458,7 +480,7 @@ Crypto.prototype.isSecretStorageReady = async function() {
this._secretStorage, this._secretStorage,
); );
const sessionBackupInStorage = ( const sessionBackupInStorage = (
!this._baseApis.getKeyBackupEnabled() || !this._backupManager.getKeyBackupEnabled() ||
this._baseApis.isKeyBackupKeyStored() this._baseApis.isKeyBackupKeyStored()
); );
@@ -495,7 +517,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
} = {}) { } = {}) {
logger.log("Bootstrapping cross-signing"); logger.log("Bootstrapping cross-signing");
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks;
const builder = new EncryptionSetupBuilder( const builder = new EncryptionSetupBuilder(
this._baseApis.store.accountData, this._baseApis.store.accountData,
delegateCryptoCallbacks, delegateCryptoCallbacks,
@@ -522,9 +544,11 @@ Crypto.prototype.bootstrapCrossSigning = async function({
builder.addKeySignature(this._userId, this._deviceId, deviceSignature); builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
// Sign message key backup with cross-signing master key // Sign message key backup with cross-signing master key
if (this.backupInfo) { if (this._backupManager.backupInfo) {
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master"); await crossSigningInfo.signObject(
builder.addSessionBackup(this.backupInfo); this._backupManager.backupInfo.auth_data, "master",
);
builder.addSessionBackup(this._backupManager.backupInfo);
} }
}; };
@@ -579,7 +603,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
if ( if (
crossSigningPrivateKeys.size && crossSigningPrivateKeys.size &&
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys !this._baseApis.cryptoCallbacks.saveCrossSigningKeys
) { ) {
const secretStorage = new SecretStorage( const secretStorage = new SecretStorage(
builder.accountDataClientAdapter, builder.accountDataClientAdapter,
@@ -646,7 +670,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
getKeyBackupPassphrase, getKeyBackupPassphrase,
} = {}) { } = {}) {
logger.log("Bootstrapping Secure Secret Storage"); logger.log("Bootstrapping Secure Secret Storage");
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks;
const builder = new EncryptionSetupBuilder( const builder = new EncryptionSetupBuilder(
this._baseApis.store.accountData, this._baseApis.store.accountData,
delegateCryptoCallbacks, delegateCryptoCallbacks,
@@ -681,7 +705,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
const ensureCanCheckPassphrase = async (keyId, keyInfo) => { const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
if (!keyInfo.mac) { if (!keyInfo.mac) {
const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey( const key = await this._baseApis.cryptoCallbacks.getSecretStorageKey(
{ keys: { [keyId]: keyInfo } }, "", { keys: { [keyId]: keyInfo } }, "",
); );
if (key) { if (key) {
@@ -766,6 +790,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations keyBackupInfo.auth_data.private_key_iterations
) { ) {
// FIXME: ???
opts.passphrase = { opts.passphrase = {
algorithm: "m.pbkdf2", algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations, iterations: keyBackupInfo.auth_data.private_key_iterations,
@@ -801,7 +826,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
// If we have cross-signing private keys cached, store them in secret // If we have cross-signing private keys cached, store them in secret
// storage if they are not there already. // storage if they are not there already.
if ( if (
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys && !this._baseApis.cryptoCallbacks.saveCrossSigningKeys &&
await this.isCrossSigningReady() && await this.isCrossSigningReady() &&
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage)) (newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
) { ) {
@@ -1071,7 +1096,7 @@ Crypto.prototype._afterCrossSigningLocalKeyChange = async function() {
upload({ shouldEmit: true }); upload({ shouldEmit: true });
const shouldUpgradeCb = ( const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications
); );
if (shouldUpgradeCb) { if (shouldUpgradeCb) {
logger.info("Starting device verification upgrade"); logger.info("Starting device verification upgrade");
@@ -1477,7 +1502,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function({
} }
// Now we may be able to trust our key backup // Now we may be able to trust our key backup
await this.checkKeyBackup(); await this._backupManager.checkKeyBackup();
// FIXME: if we previously trusted the backup, should we automatically sign // FIXME: if we previously trusted the backup, should we automatically sign
// the backup with the new key (if not already signed)? // the backup with the new key (if not already signed)?
}; };
@@ -1509,7 +1534,7 @@ Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
*/ */
Crypto.prototype._checkDeviceVerifications = async function(userId) { Crypto.prototype._checkDeviceVerifications = async function(userId) {
const shouldUpgradeCb = ( const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications
); );
if (!shouldUpgradeCb) { if (!shouldUpgradeCb) {
// Upgrading skipped when callback is not present. // Upgrading skipped when callback is not present.
@@ -1539,206 +1564,11 @@ Crypto.prototype._checkDeviceVerifications = async function(userId) {
logger.info(`Finished device verification upgrade for ${userId}`); logger.info(`Finished device verification upgrade for ${userId}`);
}; };
/**
* Check the server for an active key backup and
* if one is present and has a valid signature from
* one of the user's verified devices, start backing up
* to it.
*/
Crypto.prototype._checkAndStartKeyBackup = async function() {
logger.log("Checking key backup status...");
if (this._baseApis.isGuest()) {
logger.log("Skipping key backup check since user is guest");
this._checkedForBackup = true;
return null;
}
let backupInfo;
try {
backupInfo = await this._baseApis.getKeyBackupVersion();
} catch (e) {
logger.log("Error checking for active key backup", e);
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;
}
this._checkedForBackup = true;
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
logger.log(
"Found usable key backup v" + backupInfo.version +
": enabling key backups",
);
this._baseApis.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) {
logger.log("No usable key backup: disabling key backup");
this._baseApis.disableKeyBackup();
} else if (!trustInfo.usable && !this.backupInfo) {
logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) {
logger.log(
"On backup version " + this.backupInfo.version + " but found " +
"version " + backupInfo.version + ": switching.",
);
this._baseApis.disableKeyBackup();
this._baseApis.enableKeyBackup(backupInfo);
// We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases
// because we don't remember what backup version we uploaded keys to:
// see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup();
} else {
logger.log("Backup version " + backupInfo.version + " still current");
}
}
return { backupInfo, trustInfo };
};
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) { Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
// This should be redundant post cross-signing is a thing, so just // This should be redundant post cross-signing is a thing, so just
// plonk it in localStorage for now. // plonk it in localStorage for now.
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey); this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
await this.checkKeyBackup(); await this._backupManager.checkKeyBackup();
};
/**
* Forces a re-check of the key backup and enables/disables it
* as appropriate.
*
* @return {Object} Object with backup info (as returned by
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*/
Crypto.prototype.checkKeyBackup = async function() {
this._checkedForBackup = false;
return this._checkAndStartKeyBackup();
};
/**
* @param {object} backupInfo key backup info dict from /room_keys/version
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
* deviceId: [string],
* device: [DeviceInfo || null],
* ]
* }
*/
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
};
if (
!backupInfo ||
!backupInfo.algorithm ||
!backupInfo.auth_data ||
!backupInfo.auth_data.public_key ||
!backupInfo.auth_data.signatures
) {
logger.info("Key backup is absent or missing required data");
return ret;
}
const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey();
if (backupInfo.auth_data.public_key === trustedPubkey) {
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
ret.trusted_locally = true;
}
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
if (keyIdParts[0] !== 'ed25519') {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
continue;
}
// Could be a cross-signing master key, but just say this is the device
// ID for backwards compat
const sigInfo = { deviceId: keyIdParts[1] };
// first check to see if it's from our cross-signing key
const crossSigningId = this._crossSigningInfo.getId();
if (crossSigningId === sigInfo.deviceId) {
sigInfo.crossSigningId = true;
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
this._userId,
sigInfo.deviceId,
crossSigningId,
);
sigInfo.valid = true;
} catch (e) {
logger.warning(
"Bad signature from cross signing key " + crossSigningId, e,
);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
continue;
}
// Now look for a sig from a device
// At some point this can probably go away and we'll just support
// it being signed by the cross-signing master key
const device = this._deviceList.getStoredDevice(
this._userId, sigInfo.deviceId,
);
if (device) {
sigInfo.device = device;
sigInfo.deviceTrust = await this.checkDeviceTrust(
this._userId, sigInfo.deviceId,
);
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
this._userId,
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
logger.info(
"Bad signature from key ID " + keyId + " userID " + this._userId +
" device ID " + device.deviceId + " fingerprint: " +
device.getFingerprint(), backupInfo.auth_data, e,
);
sigInfo.valid = false;
}
} else {
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
logger.info("Ignoring signature from unknown key " + keyId);
}
ret.sigs.push(sigInfo);
}
ret.usable = ret.sigs.some((s) => {
return (
s.valid && (
(s.device && s.deviceTrust.isVerified()) ||
(s.crossSigningId)
)
);
});
ret.usable |= ret.trusted_locally;
return ret;
}; };
/** /**
@@ -2785,191 +2615,12 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
})); }));
}; };
/**
* Schedules sending all keys waiting to be sent to the backup, if not already
* scheduled. Retries if necessary.
*
* @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay.
*/
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
if (this._sendingBackups) return;
this._sendingBackups = true;
try {
// wait between 0 and `maxDelay` seconds, to avoid backup
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * maxDelay;
await sleep(delay);
let numFailures = 0; // number of consecutive failures
while (1) {
if (!this.backupKey) {
return;
}
try {
const numBackedUp =
await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
if (numBackedUp === 0) {
// no sessions left needing backup: we're done
return;
}
numFailures = 0;
} catch (err) {
numFailures++;
logger.log("Key backup request failed", err);
if (err.data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) {
// Re-check key backup status on error, so we can be
// sure to present the current situation when asked.
await this.checkKeyBackup();
// Backup version has changed or this backup version
// has been deleted
this.emit("crypto.keyBackupFailed", err.data.errcode);
throw err;
}
}
}
if (numFailures) {
// exponential backoff if we have failures
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
}
}
} finally {
this._sendingBackups = false;
}
};
/**
* Take some e2e keys waiting to be backed up and send them
* to the backup.
*
* @param {integer} limit Maximum number of keys to back up
* @returns {integer} Number of sessions backed up
*/
Crypto.prototype._backupPendingKeys = async function(limit) {
const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) {
return 0;
}
let remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
const data = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
if (data[roomId] === undefined) {
data[roomId] = { sessions: {} };
}
const sessionData = await this._olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData,
);
sessionData.algorithm = olmlib.MEGOLM_ALGORITHM;
delete sessionData.session_id;
delete sessionData.room_id;
const firstKnownIndex = sessionData.first_known_index;
delete sessionData.first_known_index;
const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData));
const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length;
const userId = this._deviceList.getUserByIdentityKey(
olmlib.MEGOLM_ALGORITHM, session.senderKey,
);
const device = this._deviceList.getDeviceByIdentityKey(
olmlib.MEGOLM_ALGORITHM, session.senderKey,
);
const verified = this._checkDeviceInfoTrust(userId, device).isVerified();
data[roomId]['sessions'][session.sessionId] = {
first_message_index: firstKnownIndex,
forwarded_count: forwardedCount,
is_verified: verified,
session_data: encrypted,
};
}
await this._baseApis.sendKeyBackup(
undefined, undefined, this.backupInfo.version,
{ rooms: data },
);
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
return sessions.length;
};
Crypto.prototype.backupGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed,
exportFormat,
) {
await this._cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey,
sessionId: sessionId,
}]);
if (this.backupInfo) {
// don't wait for this to complete: it will delay so
// happens in the background
this.scheduleKeyBackupSend();
}
// if this.backupInfo is not set, then the keys will be backed up when
// client.enableKeyBackup is called
};
/**
* Marks all group sessions as needing to be backed up and schedules them to
* upload in the background as soon as possible.
*/
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
};
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
* (which will be equal to the number of sessions in the store).
*/
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
await this._cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_BACKUP,
],
(txn) => {
this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (session !== null) {
this._cryptoStore.markSessionsNeedingBackup([session], txn);
}
});
},
);
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
return remaining;
};
/** /**
* Counts the number of end to end session keys that are waiting to be backed up * Counts the number of end to end session keys that are waiting to be backed up
* @returns {Promise<int>} Resolves to the number of sessions requiring backup * @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/ */
Crypto.prototype.countSessionsNeedingBackup = function() { Crypto.prototype.countSessionsNeedingBackup = function() {
return this._cryptoStore.countSessionsNeedingBackup(); return this._backupManager.countSessionsNeedingBackup();
}; };
/** /**
@@ -3345,10 +2996,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
return; return;
} }
if (!this._checkedForBackup) { if (!this._backupManager.checkedForBackup) {
// don't bother awaiting on this - the important thing is that we retry if we // don't bother awaiting on this - the important thing is that we retry if we
// haven't managed to check before // haven't managed to check before
this._checkAndStartKeyBackup(); this._backupManager.checkAndStart();
} }
const alg = this._getRoomDecryptor(content.room_id, content.algorithm); const alg = this._getRoomDecryptor(content.room_id, content.algorithm);

70
src/crypto/keybackup.ts Normal file
View File

@@ -0,0 +1,70 @@
/*
Copyright 2021 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.
*/
import { ISignatures } from "../@types/signed";
import { DeviceInfo } from "./deviceinfo";
export interface IKeyBackupSession {
first_message_index: number; // eslint-disable-line camelcase
forwarded_count: number; // eslint-disable-line camelcase
is_verified: boolean; // eslint-disable-line camelcase
session_data: { // eslint-disable-line camelcase
ciphertext: string;
ephemeral: string;
mac: string;
};
}
export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
export interface IKeyBackupVersion {
algorithm: string;
auth_data: { // eslint-disable-line camelcase
public_key: string; // eslint-disable-line camelcase
signatures: ISignatures;
};
count: number;
etag: string;
version: string; // number contained within
}
// TODO: Verify types
export interface IKeyBackupTrustInfo {
/**
* is the backup trusted, true if there is a sig that is valid & from a trusted device
*/
usable: boolean[];
sigs: {
valid: boolean[];
device: DeviceInfo[];
}[];
}
export interface IKeyBackupPrepareOpts {
secureSecretStorage: boolean;
}
export interface IKeyBackupRestoreResult {
total: number;
imported: number;
}
export interface IKeyBackupRestoreOpts {
cacheCompleteCallback?: () => void;
progressCallback?: ({ stage: string }) => void;
}

View File

@@ -118,7 +118,7 @@ export async function encryptMessageForDevice(
* *
* @param {module:crypto/OlmDevice} olmDevice * @param {module:crypto/OlmDevice} olmDevice
* *
* @param {module:base-apis~MatrixBaseApis} baseApis * @param {MatrixClient} baseApis
* *
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser * @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for * map from userid to list of devices to ensure sessions for
@@ -168,7 +168,7 @@ export async function getExistingOlmSessions(
* *
* @param {module:crypto/OlmDevice} olmDevice * @param {module:crypto/OlmDevice} olmDevice
* *
* @param {module:base-apis~MatrixBaseApis} baseApis * @param {MatrixClient} baseApis
* *
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser * @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for * map from userid to list of devices to ensure sessions for

View File

@@ -49,9 +49,10 @@ export class VerificationBase extends EventEmitter {
* *
* @class * @class
* *
* @param {module:base-apis~Channel} channel the verification channel to send verification messages over. * @param {Object} channel the verification channel to send verification messages over.
* TODO: Channel types
* *
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface * @param {MatrixClient} baseApis base matrix api interface
* *
* @param {string} userId the user ID that is being verified * @param {string} userId the user ID that is being verified
* *
@@ -291,7 +292,7 @@ export class VerificationBase extends EventEmitter {
await verifier(keyId, device, keyInfo); await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId); verifiedDevices.push(deviceId);
} else { } else {
const crossSigningInfo = this._baseApis._crypto._deviceList const crossSigningInfo = this._baseApis.crypto._deviceList
.getStoredCrossSigningForUser(userId); .getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
await verifier(keyId, DeviceInfo.fromStorage({ await verifier(keyId, DeviceInfo.fromStorage({

50
src/event-mapper.ts Normal file
View File

@@ -0,0 +1,50 @@
/*
Copyright 2021 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.
*/
import { MatrixClient } from "./client";
import { MatrixEvent } from "./models/event";
export type EventMapper = (obj: any) => MatrixEvent;
export interface MapperOpts {
preventReEmit?: boolean;
decrypt?: boolean;
}
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
const preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
}
if (decrypt) {
client.decryptEventIfNeeded(event);
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]);
}
return event;
}
return mapper;
}

View File

@@ -1,7 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -16,17 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import type Request from "request";
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
import { MemoryStore } from "./store/memory"; import { MemoryStore } from "./store/memory";
import { StubStore } from "./store/stub";
import { LocalIndexedDBStoreBackend } from "./store/indexeddb-local-backend";
import { RemoteIndexedDBStoreBackend } from "./store/indexeddb-remote-backend";
import { MatrixScheduler } from "./scheduler"; import { MatrixScheduler } from "./scheduler";
import { MatrixClient } from "./client"; import { MatrixClient } from "./client";
import { ICreateClientOpts } from "./client";
export * from "./client"; export * from "./client";
export * from "./http-api"; export * from "./http-api";
@@ -94,11 +86,6 @@ export function wrapRequest(wrapper) {
}; };
} }
type Store =
StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
let cryptoStoreFactory = () => new MemoryCryptoStore; let cryptoStoreFactory = () => new MemoryCryptoStore;
/** /**
@@ -111,41 +98,6 @@ export function setCryptoStoreFactory(fac) {
cryptoStoreFactory = fac; cryptoStoreFactory = fac;
} }
export interface ICreateClientOpts {
baseUrl: string;
idBaseUrl?: string;
store?: Store;
cryptoStore?: CryptoStore;
scheduler?: MatrixScheduler;
request?: Request;
userId?: string;
deviceId?: string;
accessToken?: string;
identityServer?: any;
localTimeoutMs?: number;
useAuthorizationHeader?: boolean;
timelineSupport?: boolean;
queryParams?: Record<string, unknown>;
deviceToImport?: {
olmDevice: {
pickledAccount: string;
sessions: Array<Record<string, any>>;
pickleKey: string;
};
userId: string;
deviceId: string;
};
pickleKey?: string;
sessionStore?: any;
unstableClientRelationAggregation?: boolean;
verificationMethods?: Array<any>;
forceTURN?: boolean;
iceCandidatePoolSize?: number,
supportsCallTransfer?: boolean,
fallbackICEServerAllowed?: boolean;
cryptoCallbacks?: ICryptoCallbacks;
}
export interface ICryptoCallbacks { export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>; getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void; saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
@@ -166,6 +118,7 @@ export interface ICryptoCallbacks {
keyInfo: ISecretStorageKeyInfo, keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void, checkFunc: (Uint8Array) => void,
) => Promise<Uint8Array>; ) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
} }
// TODO: Move this to `SecretStorage` once converted // TODO: Move this to `SecretStorage` once converted

View File

@@ -332,7 +332,7 @@ export class Relations extends EventEmitter {
}, null); }, null);
if (lastReplacement?.shouldAttemptDecryption()) { if (lastReplacement?.shouldAttemptDecryption()) {
await lastReplacement.attemptDecryption(this._room._client._crypto); await lastReplacement.attemptDecryption(this._room._client.crypto);
} else if (lastReplacement?.isBeingDecrypted()) { } else if (lastReplacement?.isBeingDecrypted()) {
await lastReplacement._decryptionPromise; await lastReplacement._decryptionPromise;
} }

View File

@@ -349,6 +349,11 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self._updateMember(member); self._updateMember(member);
self.emit("RoomState.members", event, self, member); self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") { } else if (event.getType() === "m.room.power_levels") {
// events with unknown state keys should be ignored
// and should not aggregate onto members power levels
if (event.getStateKey() !== "") {
return;
}
const members = Object.values(self.members); const members = Object.values(self.members);
members.forEach(function(member) { members.forEach(function(member) {
// We only propagate `RoomState.members` event if the // We only propagate `RoomState.members` event if the

View File

@@ -192,13 +192,13 @@ export function Room(roomId, client, myUserId, opts) {
if (this._opts.pendingEventOrdering == "detached") { if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList = []; this._pendingEventList = [];
const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId)); const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
if (serializedPendingEventList) { if (serializedPendingEventList) {
JSON.parse(serializedPendingEventList) JSON.parse(serializedPendingEventList)
.forEach(async serializedEvent => { .forEach(async serializedEvent => {
const event = new MatrixEvent(serializedEvent); const event = new MatrixEvent(serializedEvent);
if (event.getType() === "m.room.encrypted") { if (event.getType() === "m.room.encrypted") {
await event.attemptDecryption(this._client._crypto); await event.attemptDecryption(this._client.crypto);
} }
event.setStatus(EventStatus.NOT_SENT); event.setStatus(EventStatus.NOT_SENT);
this.addPendingEvent(event, event.getTxnId()); this.addPendingEvent(event, event.getTxnId());
@@ -255,7 +255,7 @@ Room.prototype.decryptCriticalEvents = function() {
.slice(readReceiptTimelineIndex) .slice(readReceiptTimelineIndex)
.filter(event => event.shouldAttemptDecryption()) .filter(event => event.shouldAttemptDecryption())
.reverse() .reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true })); .map(event => event.attemptDecryption(this._client.crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises); return Promise.allSettled(decryptionPromises);
}; };
@@ -272,7 +272,7 @@ Room.prototype.decryptAllEvents = function() {
.getEvents() .getEvents()
.filter(event => event.shouldAttemptDecryption()) .filter(event => event.shouldAttemptDecryption())
.reverse() .reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true })); .map(event => event.attemptDecryption(this._client.crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises); return Promise.allSettled(decryptionPromises);
}; };
@@ -632,7 +632,7 @@ Room.prototype._loadMembersFromServer = async function() {
}); });
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{ $roomId: this.roomId }); { $roomId: this.roomId });
const http = this._client._http; const http = this._client.http;
const response = await http.authedRequest(undefined, "GET", path); const response = await http.authedRequest(undefined, "GET", path);
return response.chunk; return response.chunk;
}; };
@@ -674,7 +674,7 @@ Room.prototype.loadMembersIfNeeded = function() {
this.currentState.setOutOfBandMembers(result.memberEvents); this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed // now the members are loaded, start to track the e2e devices if needed
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) { if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId); this._client.crypto.trackRoomDevices(this.roomId);
} }
return result.fromServer; return result.fromServer;
}).catch((err) => { }).catch((err) => {
@@ -1387,7 +1387,7 @@ Room.prototype._savePendingEvents = function() {
return isEventEncrypted || !isRoomEncrypted; return isEventEncrypted || !isRoomEncrypted;
}); });
const { store } = this._client._sessionStore; const { store } = this._client.sessionStore;
if (this._pendingEventList.length > 0) { if (this._pendingEventList.length > 0) {
store.setItem( store.setItem(
pendingEventsKey(this.roomId), pendingEventsKey(this.roomId),

26
src/sync.api.ts Normal file
View File

@@ -0,0 +1,26 @@
/*
Copyright 2021 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.
*/
// TODO: Merge this with sync.js once converted
export enum SyncState {
Error = "ERROR",
Prepared = "PREPARED",
Stopped = "STOPPED",
Syncing = "SYNCING",
Catchup = "CATCHUP",
Reconnecting = "RECONNECTING",
}

View File

@@ -21,7 +21,7 @@ limitations under the License.
* TODO: * TODO:
* This class mainly serves to take all the syncing logic out of client.js and * This class mainly serves to take all the syncing logic out of client.js and
* into a separate file. It's all very fluid, and this class gut wrenches a lot * into a separate file. It's all very fluid, and this class gut wrenches a lot
* of MatrixClient props (e.g. _http). Given we want to support WebSockets as * of MatrixClient props (e.g. http). Given we want to support WebSockets as
* an alternative syncing API, we may want to have a proper syncing interface * an alternative syncing API, we may want to have a proper syncing interface
* for HTTP and WS at some point. * for HTTP and WS at some point.
*/ */
@@ -209,7 +209,7 @@ SyncApi.prototype.syncLeftRooms = function() {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter, getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) { ).then(function(filterId) {
qps.filter = filterId; qps.filter = filterId;
return client._http.authedRequest( return client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined, localTimeoutMs, undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
); );
}).then(function(data) { }).then(function(data) {
@@ -349,7 +349,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
const self = this; const self = this;
// FIXME: gut wrenching; hard-coded timeout values // FIXME: gut wrenching; hard-coded timeout values
this.client._http.authedRequest(undefined, "GET", "/events", { this.client.http.authedRequest(undefined, "GET", "/events", {
room_id: peekRoom.roomId, room_id: peekRoom.roomId,
timeout: 30 * 1000, timeout: 30 * 1000,
from: token, from: token,
@@ -551,7 +551,7 @@ SyncApi.prototype.sync = function() {
} }
try { try {
debuglog("Storing client options..."); debuglog("Storing client options...");
await this.client._storeClientOptions(); await this.client.storeClientOptions();
debuglog("Stored client options"); debuglog("Stored client options");
} catch (err) { } catch (err) {
logger.error("Storing client options failed", err); logger.error("Storing client options failed", err);
@@ -815,7 +815,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) { SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) {
const qps = this._getSyncParams(syncOptions, syncToken); const qps = this._getSyncParams(syncOptions, syncToken);
return this.client._http.authedRequest( return this.client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined, undefined, "GET", "/sync", qps, undefined,
qps.timeout + BUFFER_PERIOD_MS, qps.timeout + BUFFER_PERIOD_MS,
); );
@@ -1426,7 +1426,7 @@ SyncApi.prototype._pokeKeepAlive = function(connDidFail) {
} }
} }
this.client._http.request( this.client.http.request(
undefined, // callback undefined, // callback
"GET", "/_matrix/client/versions", "GET", "/_matrix/client/versions",
undefined, // queryParams undefined, // queryParams
@@ -1671,19 +1671,9 @@ SyncApi.prototype._processEventsForNotifs = function(room, timelineEventList) {
* @return {string} * @return {string}
*/ */
SyncApi.prototype._getGuestFilter = function() { SyncApi.prototype._getGuestFilter = function() {
const guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching // Dev note: This used to be conditional to return a filter of 20 events maximum, but
if (!guestRooms) { // the condition never went to the other branch. This is now hardcoded.
return "{}"; return "{}";
}
// we just need to specify the filter inline if we're a guest because guests
// can't create filters.
return JSON.stringify({
room: {
timeline: {
limit: 20,
},
},
});
}; };
/** /**

View File

@@ -398,7 +398,7 @@ export function ensureNoTrailingSlash(url: string): string {
} }
// Returns a promise which resolves with a given value after the given number of ms // Returns a promise which resolves with a given value after the given number of ms
export function sleep<T>(ms: number, value: T): Promise<T> { export function sleep<T>(ms: number, value?: T): Promise<T> {
return new Promise((resolve => { return new Promise((resolve => {
setTimeout(resolve, ms, value); setTimeout(resolve, ms, value);
})); }));

View File

@@ -511,7 +511,7 @@ export class MatrixCall extends EventEmitter {
// make sure we have valid turn creds. Unless something's gone wrong, it should // make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant. // poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers(); const haveTurnCreds = await this.client.checkTurnServers();
if (!haveTurnCreds) { if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
} }
@@ -846,7 +846,7 @@ export class MatrixCall extends EventEmitter {
}, },
} as MCallAnswer; } as MCallAnswer;
if (this.client._supportsCallTransfer) { if (this.client.supportsCallTransfer) {
answerContent.capabilities = { answerContent.capabilities = {
'm.call.transferee': true, 'm.call.transferee': true,
}; };
@@ -1106,21 +1106,8 @@ export class MatrixCall extends EventEmitter {
await this.peerConn.setRemoteDescription(description); await this.peerConn.setRemoteDescription(description);
if (description.type === 'offer') { if (description.type === 'offer') {
// First we sent the direction of the tranciever to what we'd like it to be,
// irresepective of whether the other side has us on hold - so just whether we
// want the call to be on hold or not. This is necessary because in a few lines,
// we'll adjust the direction and unless we do this too, we'll never come off hold.
for (const tranceiver of this.peerConn.getTransceivers()) {
tranceiver.direction = this.isRemoteOnHold() ? 'inactive' : 'sendrecv';
}
const localDescription = await this.peerConn.createAnswer(); const localDescription = await this.peerConn.createAnswer();
await this.peerConn.setLocalDescription(localDescription); await this.peerConn.setLocalDescription(localDescription);
// Now we've got our answer, set the direction to the outcome of the negotiation.
// We need to do this otherwise Firefox will notice that the direction is not the
// currentDirection and try to negotiate itself off hold again.
for (const tranceiver of this.peerConn.getTransceivers()) {
tranceiver.direction = tranceiver.currentDirection;
}
this.sendVoipEvent(EventType.CallNegotiate, { this.sendVoipEvent(EventType.CallNegotiate, {
description: this.peerConn.localDescription, description: this.peerConn.localDescription,
@@ -1194,7 +1181,7 @@ export class MatrixCall extends EventEmitter {
content.description = this.peerConn.localDescription; content.description = this.peerConn.localDescription;
} }
if (this.client._supportsCallTransfer) { if (this.client.supportsCallTransfer) {
content.capabilities = { content.capabilities = {
'm.call.transferee': true, 'm.call.transferee': true,
}; };
@@ -1592,14 +1579,14 @@ export class MatrixCall extends EventEmitter {
private async placeCallWithConstraints(constraints: MediaStreamConstraints) { private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
logger.log("Getting user media with constraints", constraints); logger.log("Getting user media with constraints", constraints);
// XXX Find a better way to do this // XXX Find a better way to do this
this.client._callEventHandler.calls.set(this.callId, this); this.client.callEventHandler.calls.set(this.callId, this);
this.setState(CallState.WaitLocalMedia); this.setState(CallState.WaitLocalMedia);
this.direction = CallDirection.Outbound; this.direction = CallDirection.Outbound;
this.config = constraints; this.config = constraints;
// make sure we have valid turn creds. Unless something's gone wrong, it should // make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant. // poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers(); const haveTurnCreds = await this.client.checkTurnServers();
if (!haveTurnCreds) { if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
} }
@@ -1621,7 +1608,7 @@ export class MatrixCall extends EventEmitter {
const pc = new window.RTCPeerConnection({ const pc = new window.RTCPeerConnection({
iceTransportPolicy: this.forceTURN ? 'relay' : undefined, iceTransportPolicy: this.forceTURN ? 'relay' : undefined,
iceServers: this.turnServers, iceServers: this.turnServers,
iceCandidatePoolSize: this.client._iceCandidatePoolSize, iceCandidatePoolSize: this.client.iceCandidatePoolSize,
}); });
// 'connectionstatechange' would be better, but firefox doesn't implement that. // 'connectionstatechange' would be better, but firefox doesn't implement that.
@@ -1826,7 +1813,7 @@ export function createNewMatrixCall(client: any, roomId: string, options?: CallO
roomId: roomId, roomId: roomId,
turnServers: client.getTurnServers(), turnServers: client.getTurnServers(),
// call level options // call level options
forceTURN: client._forceTURN || optionsForceTURN, forceTURN: client.forceTURN || optionsForceTURN,
}; };
const call = new MatrixCall(opts); const call = new MatrixCall(opts);

View File

@@ -155,7 +155,7 @@ export class CallEventHandler {
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now(); const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = createNewMatrixCall(this.client, event.getRoomId(), { call = createNewMatrixCall(this.client, event.getRoomId(), {
forceTURN: this.client._forceTURN, forceTURN: this.client.forceTURN,
}); });
if (!call) { if (!call) {
logger.log( logger.log(

View File

@@ -16,7 +16,7 @@ limitations under the License.
import EventEmitter from "events"; import EventEmitter from "events";
import { SDPStreamMetadataPurpose } from "./callEventTypes"; import { SDPStreamMetadataPurpose } from "./callEventTypes";
import MatrixClient from "../client"; import { MatrixClient } from "../client";
import { RoomMember } from "../models/room-member"; import { RoomMember } from "../models/room-member";
export enum CallFeedEvent { export enum CallFeedEvent {