1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +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.oneTimeKeys = {};
this._callEventHandler = {
this.callEventHandler = {
calls: new Map(),
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import * as testUtils from "../../test-utils";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
const Olm = global.Olm;
@@ -73,7 +74,7 @@ const KEY_BACKUP_DATA = {
};
const BACKUP_INFO = {
algorithm: "m.megolm_backup.v1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -138,6 +139,7 @@ describe("MegolmBackup", function() {
let megolmDecryption;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto._backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -215,12 +217,14 @@ describe("MegolmBackup", function() {
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.backupGroupSession = jest.fn();
mockCrypto._backupManager = {
backupGroupSession: jest.fn(),
};
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
expect(mockCrypto._backupManager.backupGroupSession).toHaveBeenCalled();
});
});
@@ -264,7 +268,7 @@ describe("MegolmBackup", function() {
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -272,7 +276,7 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
@@ -292,12 +296,9 @@ describe("MegolmBackup", function() {
resolve();
return Promise.resolve({});
};
client._crypto.backupGroupSession(
"roomId",
client.crypto._backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(1);
@@ -335,17 +336,15 @@ describe("MegolmBackup", function() {
});
await resetCrossSigningKeys(client);
let numCalls = 0;
await new Promise((resolve, reject) => {
client._http.authedRequest = function(
await Promise.all([
new Promise((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls === 1) {
expect(method).toBe("POST");
expect(path).toBe("/room_keys/version");
try {
@@ -357,17 +356,28 @@ describe("MegolmBackup", function() {
reject(e);
return Promise.resolve({});
}
resolve();
backupInfo = data;
return Promise.resolve({});
} else if (numCalls === 2) {
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({
algorithm: "m.megolm_backup.v1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
});
expect(numCalls).toBe(1);
}),
]);
expect(numCalls).toBe(2);
});
it('retries when a backup fails', function() {
@@ -434,7 +444,7 @@ describe("MegolmBackup", function() {
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -442,7 +452,7 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
@@ -468,12 +478,9 @@ describe("MegolmBackup", function() {
);
}
};
client._crypto.backupGroupSession(
"roomId",
client.crypto._backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(2);
@@ -506,7 +513,7 @@ describe("MegolmBackup", function() {
});
it('can restore from backup', function() {
client._http.authedRequest = function() {
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
@@ -523,7 +530,7 @@ describe("MegolmBackup", function() {
});
it('can restore backup by room', function() {
client._http.authedRequest = function() {
client.http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
@@ -546,15 +553,15 @@ describe("MegolmBackup", function() {
it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client._crypto.storeSessionBackupPrivateKey(key);
const result = await client._crypto.getSessionBackupPrivateKey();
await client.crypto.storeSessionBackupPrivateKey(key);
const result = await client.crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key);
});
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();
client._http.authedRequest = function() {
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
@@ -566,8 +573,24 @@ describe("MegolmBackup", function() {
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
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) => {
await olmlib.verifySignature(
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
alice.crypto._olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
);
});
alice.uploadKeySignatures = async () => {};
@@ -138,7 +138,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -201,13 +201,14 @@ describe("Cross Signing", function() {
const uploadSigsPromise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = jest.fn(async (content) => {
try {
await olmlib.verifySignature(
alice._crypto._olmDevice,
alice.crypto._olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
);
olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"],
@@ -215,10 +216,13 @@ describe("Cross Signing", function() {
"@alice:example.com",
);
resolve();
} catch (e) {
reject(e);
}
});
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -226,7 +230,7 @@ describe("Cross Signing", function() {
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
await alice.crypto._signObject(aliceDevice);
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
// feed sync result that includes master key, ssk, device key
@@ -354,7 +358,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -383,7 +387,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobPubkey]: sig,
},
};
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be TOFU
@@ -417,8 +421,8 @@ describe("Cross Signing", function() {
null,
aliceKeys,
);
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
@@ -433,14 +437,14 @@ describe("Cross Signing", function() {
]);
const keyChangePromise = new Promise((resolve, reject) => {
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
if (userId === "@bob:example.com") {
resolve();
}
});
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -448,7 +452,7 @@ describe("Cross Signing", function() {
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
await alice.crypto._signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create();
@@ -602,7 +606,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -625,7 +629,7 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be untrusted
@@ -669,7 +673,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -697,7 +701,7 @@ describe("Cross Signing", function() {
bobDevice.signatures = {};
bobDevice.signatures["@bob:example.com"] = {};
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
@@ -729,7 +733,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey2]: sskSig2,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -766,7 +770,7 @@ describe("Cross Signing", function() {
// Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
@@ -801,20 +805,20 @@ describe("Cross Signing", function() {
bob.uploadKeySignatures = async () => {};
// set Bob's cross-signing key
await resetCrossSigningKeys(bob);
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
"curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key,
},
verified: 1,
known: true,
},
});
alice._crypto._deviceList.storeCrossSigningForUser(
alice.crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
bob._crypto._crossSigningInfo.toStorage(),
bob.crypto._crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -834,7 +838,7 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy();
// "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"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
@@ -844,9 +848,9 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
await new Promise((resolve) => {
alice._crypto.on("userTrustStatusChanged", resolve);
alice.crypto.on("userTrustStatusChanged", resolve);
});
await upgradePromise;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1314,7 +1314,7 @@ describe("Room", function() {
isRoomEncrypted: function() {
return false;
},
_http: {
http: {
serverResponse,
authedRequest: function() {
if (this.serverResponse instanceof Error) {
@@ -1384,7 +1384,7 @@ describe("Room", function() {
}
expect(hasThrown).toEqual(true);
client._http.serverResponse = [memberEvent];
client.http.serverResponse = [memberEvent];
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
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.
*
* @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} 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.
return new Promise((resolve, reject) => {
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
// 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
const backupKeyPromise = new Promise(async resolve => {
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
if (!cachedKey) {
logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret(
@@ -785,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
client._crypto.storeSessionBackupPrivateKey(
client.crypto.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey),
);
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.
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis._http.authedRequest(
await baseApis.http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
undefined, {
algorithm: this._keyBackupInfo.algorithm,
@@ -214,7 +214,7 @@ export class EncryptionSetupOperation {
);
} else {
// add new key backup
await baseApis._http.authedRequest(
await baseApis.http.authedRequest(
undefined, "POST", "/room_keys/version",
undefined, this._keyBackupInfo,
{ prefix: PREFIX_UNSTABLE },

View File

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

View File

@@ -46,7 +46,7 @@ export const DECRYPTION_CLASSES = {};
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @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 {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 {module:crypto} params.crypto crypto core
* @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
* 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
this._crypto.backupGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [],
sessionId, key.key,
this._crypto._backupManager.backupGroupSession(
this._olmDevice.deviceCurve25519Key, sessionId,
);
return new OutboundSessionInfo(sessionId, sharedHistory);
@@ -1425,11 +1424,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
});
}).then(() => {
// don't wait for the keys to be backed up for the server
this._crypto.backupGroupSession(
content.room_id, senderKey, forwardingKeyChain,
content.session_id, content.session_key, keysClaimed,
exportFormat,
);
this._crypto._backupManager.backupGroupSession(senderKey, content.session_id);
}).catch((e) => {
logger.error(`Error handling m.room_key_event: ${e}`);
});
@@ -1645,14 +1640,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
).then(() => {
if (opts.source !== "backup") {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
this._crypto._backupManager.backupGroupSession(
session.sender_key, session.session_id,
).catch((e) => {
// This throws if the upload failed, but this is fine
// 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");
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 anotherjson from "another-json";
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "../matrix";
// FIXME: these types should eventually go in a different file
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 {
algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase
@@ -192,7 +205,7 @@ export class DehydrationManager {
}
logger.log("Uploading account to server");
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
const dehydrateResult = await this.crypto._baseApis.http.authedRequest(
undefined,
"PUT",
"/dehydrated_device",
@@ -255,7 +268,7 @@ export class DehydrationManager {
}
logger.log("Uploading keys to server");
await this.crypto._baseApis._http.authedRequest(
await this.crypto._baseApis.http.authedRequest(
undefined,
"POST",
"/keys/upload/" + encodeURI(deviceId),

View File

@@ -2,7 +2,7 @@
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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");
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 { logger } from '../logger';
import * as utils from "../utils";
import { sleep } from "../utils";
import { OlmDevice } from "./OlmDevice";
import * as olmlib from "./olmlib";
import { DeviceList } from "./DeviceList";
@@ -58,6 +57,7 @@ import { KeySignatureUploadError } from "../errors";
import { decryptAES, encryptAES } from './aes';
import { DehydrationManager } from './dehydration';
import { MatrixEvent } from "../models/event";
import { BackupManager } from "./backup";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -85,7 +85,6 @@ export function isCryptoAvailable() {
}
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
/**
* Cryptography bits
@@ -97,7 +96,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
*
* @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
* Store to be used for end-to-end crypto session data
@@ -154,13 +153,36 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
} else {
this._verificationMethods = defaultVerificationMethods;
}
// track whether this device's megolm keys are being backed up incrementally
// to the server or not.
// XXX: this should probably have a single source of truth from OlmAccount
this.backupInfo = null; // The info dict from /room_keys/version
this.backupKey = null; // The encryption key object
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
this._sendingBackups = false; // Are we currently sending backups?
this._backupManager = new BackupManager(baseApis, async (algorithm) => {
// try to get key from cache
const cachedKey = await this.getSessionBackupPrivateKey();
if (cachedKey) {
return cachedKey;
}
// 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._deviceList = new DeviceList(
@@ -232,7 +254,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
// processing the response.
this._sendKeyRequestsImmediately = false;
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
const cryptoCallbacks = this._baseApis.cryptoCallbacks || {};
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice);
this._crossSigningInfo = new CrossSigningInfo(
@@ -331,7 +353,7 @@ Crypto.prototype.init = async function(opts) {
this._deviceList.startTrackingDeviceList(this._userId);
logger.log("Crypto: checking for key backup...");
this._checkAndStartKeyBackup();
this._backupManager.checkAndStart();
};
/**
@@ -458,7 +480,7 @@ Crypto.prototype.isSecretStorageReady = async function() {
this._secretStorage,
);
const sessionBackupInStorage = (
!this._baseApis.getKeyBackupEnabled() ||
!this._backupManager.getKeyBackupEnabled() ||
this._baseApis.isKeyBackupKeyStored()
);
@@ -495,7 +517,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
} = {}) {
logger.log("Bootstrapping cross-signing");
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks;
const builder = new EncryptionSetupBuilder(
this._baseApis.store.accountData,
delegateCryptoCallbacks,
@@ -522,9 +544,11 @@ Crypto.prototype.bootstrapCrossSigning = async function({
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
// Sign message key backup with cross-signing master key
if (this.backupInfo) {
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
builder.addSessionBackup(this.backupInfo);
if (this._backupManager.backupInfo) {
await crossSigningInfo.signObject(
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;
if (
crossSigningPrivateKeys.size &&
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys
!this._baseApis.cryptoCallbacks.saveCrossSigningKeys
) {
const secretStorage = new SecretStorage(
builder.accountDataClientAdapter,
@@ -646,7 +670,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
getKeyBackupPassphrase,
} = {}) {
logger.log("Bootstrapping Secure Secret Storage");
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks;
const builder = new EncryptionSetupBuilder(
this._baseApis.store.accountData,
delegateCryptoCallbacks,
@@ -681,7 +705,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
if (!keyInfo.mac) {
const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey(
const key = await this._baseApis.cryptoCallbacks.getSecretStorageKey(
{ keys: { [keyId]: keyInfo } }, "",
);
if (key) {
@@ -766,6 +790,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
// FIXME: ???
opts.passphrase = {
algorithm: "m.pbkdf2",
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
// storage if they are not there already.
if (
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys &&
!this._baseApis.cryptoCallbacks.saveCrossSigningKeys &&
await this.isCrossSigningReady() &&
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
) {
@@ -1071,7 +1096,7 @@ Crypto.prototype._afterCrossSigningLocalKeyChange = async function() {
upload({ shouldEmit: true });
const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications
);
if (shouldUpgradeCb) {
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
await this.checkKeyBackup();
await this._backupManager.checkKeyBackup();
// FIXME: if we previously trusted the backup, should we automatically sign
// 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) {
const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications
);
if (!shouldUpgradeCb) {
// 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}`);
};
/**
* 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) {
// This should be redundant post cross-signing is a thing, so just
// plonk it in localStorage for now.
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
await this.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;
await this._backupManager.checkKeyBackup();
};
/**
@@ -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
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/
Crypto.prototype.countSessionsNeedingBackup = function() {
return this._cryptoStore.countSessionsNeedingBackup();
return this._backupManager.countSessionsNeedingBackup();
};
/**
@@ -3345,10 +2996,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
return;
}
if (!this._checkedForBackup) {
if (!this._backupManager.checkedForBackup) {
// don't bother awaiting on this - the important thing is that we retry if we
// haven't managed to check before
this._checkAndStartKeyBackup();
this._backupManager.checkAndStart();
}
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:base-apis~MatrixBaseApis} baseApis
* @param {MatrixClient} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* 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:base-apis~MatrixBaseApis} baseApis
* @param {MatrixClient} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for

View File

@@ -49,9 +49,10 @@ export class VerificationBase extends EventEmitter {
*
* @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
*
@@ -291,7 +292,7 @@ export class VerificationBase extends EventEmitter {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
} else {
const crossSigningInfo = this._baseApis._crypto._deviceList
const crossSigningInfo = this._baseApis.crypto._deviceList
.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
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 2017 Vector Creations Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2015-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.
@@ -16,17 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type Request from "request";
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
import { MemoryStore } from "./store/memory";
import { StubStore } from "./store/stub";
import { LocalIndexedDBStoreBackend } from "./store/indexeddb-local-backend";
import { RemoteIndexedDBStoreBackend } from "./store/indexeddb-remote-backend";
import { MatrixScheduler } from "./scheduler";
import { MatrixClient } from "./client";
import { ICreateClientOpts } from "./client";
export * from "./client";
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;
/**
@@ -111,41 +98,6 @@ export function setCryptoStoreFactory(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 {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
@@ -166,6 +118,7 @@ export interface ICryptoCallbacks {
keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
// TODO: Move this to `SecretStorage` once converted

View File

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

View File

@@ -349,6 +349,11 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
} 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);
members.forEach(function(member) {
// 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") {
this._pendingEventList = [];
const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId));
const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
if (serializedPendingEventList) {
JSON.parse(serializedPendingEventList)
.forEach(async serializedEvent => {
const event = new MatrixEvent(serializedEvent);
if (event.getType() === "m.room.encrypted") {
await event.attemptDecryption(this._client._crypto);
await event.attemptDecryption(this._client.crypto);
}
event.setStatus(EventStatus.NOT_SENT);
this.addPendingEvent(event, event.getTxnId());
@@ -255,7 +255,7 @@ Room.prototype.decryptCriticalEvents = function() {
.slice(readReceiptTimelineIndex)
.filter(event => event.shouldAttemptDecryption())
.reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
.map(event => event.attemptDecryption(this._client.crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
};
@@ -272,7 +272,7 @@ Room.prototype.decryptAllEvents = function() {
.getEvents()
.filter(event => event.shouldAttemptDecryption())
.reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
.map(event => event.attemptDecryption(this._client.crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
};
@@ -632,7 +632,7 @@ Room.prototype._loadMembersFromServer = async function() {
});
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{ $roomId: this.roomId });
const http = this._client._http;
const http = this._client.http;
const response = await http.authedRequest(undefined, "GET", path);
return response.chunk;
};
@@ -674,7 +674,7 @@ Room.prototype.loadMembersIfNeeded = function() {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
this._client.crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
}).catch((err) => {
@@ -1387,7 +1387,7 @@ Room.prototype._savePendingEvents = function() {
return isEventEncrypted || !isRoomEncrypted;
});
const { store } = this._client._sessionStore;
const { store } = this._client.sessionStore;
if (this._pendingEventList.length > 0) {
store.setItem(
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:
* 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
* 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
* for HTTP and WS at some point.
*/
@@ -209,7 +209,7 @@ SyncApi.prototype.syncLeftRooms = function() {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) {
qps.filter = filterId;
return client._http.authedRequest(
return client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
);
}).then(function(data) {
@@ -349,7 +349,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
const self = this;
// 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,
timeout: 30 * 1000,
from: token,
@@ -551,7 +551,7 @@ SyncApi.prototype.sync = function() {
}
try {
debuglog("Storing client options...");
await this.client._storeClientOptions();
await this.client.storeClientOptions();
debuglog("Stored client options");
} catch (err) {
logger.error("Storing client options failed", err);
@@ -815,7 +815,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) {
const qps = this._getSyncParams(syncOptions, syncToken);
return this.client._http.authedRequest(
return this.client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined,
qps.timeout + BUFFER_PERIOD_MS,
);
@@ -1426,7 +1426,7 @@ SyncApi.prototype._pokeKeepAlive = function(connDidFail) {
}
}
this.client._http.request(
this.client.http.request(
undefined, // callback
"GET", "/_matrix/client/versions",
undefined, // queryParams
@@ -1671,19 +1671,9 @@ SyncApi.prototype._processEventsForNotifs = function(room, timelineEventList) {
* @return {string}
*/
SyncApi.prototype._getGuestFilter = function() {
const guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
if (!guestRooms) {
// Dev note: This used to be conditional to return a filter of 20 events maximum, but
// the condition never went to the other branch. This is now hardcoded.
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
export function sleep<T>(ms: number, value: T): Promise<T> {
export function sleep<T>(ms: number, value?: T): Promise<T> {
return new Promise((resolve => {
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
// 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) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}
@@ -846,7 +846,7 @@ export class MatrixCall extends EventEmitter {
},
} as MCallAnswer;
if (this.client._supportsCallTransfer) {
if (this.client.supportsCallTransfer) {
answerContent.capabilities = {
'm.call.transferee': true,
};
@@ -1106,21 +1106,8 @@ export class MatrixCall extends EventEmitter {
await this.peerConn.setRemoteDescription(description);
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();
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, {
description: this.peerConn.localDescription,
@@ -1194,7 +1181,7 @@ export class MatrixCall extends EventEmitter {
content.description = this.peerConn.localDescription;
}
if (this.client._supportsCallTransfer) {
if (this.client.supportsCallTransfer) {
content.capabilities = {
'm.call.transferee': true,
};
@@ -1592,14 +1579,14 @@ export class MatrixCall extends EventEmitter {
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
logger.log("Getting user media with constraints", constraints);
// 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.direction = CallDirection.Outbound;
this.config = constraints;
// 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.
const haveTurnCreds = await this.client._checkTurnServers();
const haveTurnCreds = await this.client.checkTurnServers();
if (!haveTurnCreds) {
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({
iceTransportPolicy: this.forceTURN ? 'relay' : undefined,
iceServers: this.turnServers,
iceCandidatePoolSize: this.client._iceCandidatePoolSize,
iceCandidatePoolSize: this.client.iceCandidatePoolSize,
});
// '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,
turnServers: client.getTurnServers(),
// call level options
forceTURN: client._forceTURN || optionsForceTURN,
forceTURN: client.forceTURN || optionsForceTURN,
};
const call = new MatrixCall(opts);

View File

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

View File

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