You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Merge remote-tracking branch 'origin/develop' into jryans/semi-linting
This commit is contained in:
@@ -69,7 +69,7 @@ export function TestClient(
|
|||||||
|
|
||||||
this.deviceKeys = null;
|
this.deviceKeys = null;
|
||||||
this.oneTimeKeys = {};
|
this.oneTimeKeys = {};
|
||||||
this._callEventHandler = {
|
this.callEventHandler = {
|
||||||
calls: new Map(),
|
calls: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ describe("DeviceList management:", function() {
|
|||||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||||
),
|
),
|
||||||
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
|
aliceTestClient.client.crypto._deviceList.saveIfDirty(),
|
||||||
]);
|
]);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
@@ -202,7 +202,7 @@ describe("DeviceList management:", function() {
|
|||||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||||
}).then((flushed) => {
|
}).then((flushed) => {
|
||||||
expect(flushed).toEqual(0);
|
expect(flushed).toEqual(0);
|
||||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
return aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
@@ -235,7 +235,7 @@ describe("DeviceList management:", function() {
|
|||||||
// wait for the client to stop processing the response
|
// wait for the client to stop processing the response
|
||||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
return aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
@@ -256,7 +256,7 @@ describe("DeviceList management:", function() {
|
|||||||
// wait for the client to stop processing the response
|
// wait for the client to stop processing the response
|
||||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
return aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
@@ -286,7 +286,7 @@ describe("DeviceList management:", function() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
await aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
|
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
@@ -322,7 +322,7 @@ describe("DeviceList management:", function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
await aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
|
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
@@ -358,7 +358,7 @@ describe("DeviceList management:", function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
await aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
|
|
||||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
@@ -379,7 +379,7 @@ describe("DeviceList management:", function() {
|
|||||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||||
200, getSyncResponse([]));
|
200, getSyncResponse([]));
|
||||||
await anotherTestClient.flushSync();
|
await anotherTestClient.flushSync();
|
||||||
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
|
await anotherTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
|
|
||||||
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function aliDownloadsKeys() {
|
|||||||
// check that the localStorage is updated as we expect (not sure this is
|
// check that the localStorage is updated as we expect (not sure this is
|
||||||
// an integration test, but meh)
|
// an integration test, but meh)
|
||||||
return Promise.all([p1, p2]).then(() => {
|
return Promise.all([p1, p2]).then(() => {
|
||||||
return aliTestClient.client._crypto._deviceList.saveIfDirty();
|
return aliTestClient.client.crypto._deviceList.saveIfDirty();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const devices = data.devices[bobUserId];
|
const devices = data.devices[bobUserId];
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ describe("MatrixClient", function() {
|
|||||||
var b = JSON.parse(JSON.stringify(o));
|
var b = JSON.parse(JSON.stringify(o));
|
||||||
delete(b.signatures);
|
delete(b.signatures);
|
||||||
delete(b.unsigned);
|
delete(b.unsigned);
|
||||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
return client.crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("Ed25519: " + ed25519key);
|
logger.log("Ed25519: " + ed25519key);
|
||||||
|
|||||||
@@ -998,7 +998,7 @@ describe("megolm", function() {
|
|||||||
...rawEvent,
|
...rawEvent,
|
||||||
room: ROOM_ID,
|
room: ROOM_ID,
|
||||||
});
|
});
|
||||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||||
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
||||||
});
|
});
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -1013,14 +1013,14 @@ describe("megolm", function() {
|
|||||||
event: true,
|
event: true,
|
||||||
});
|
});
|
||||||
event._senderCurve25519Key = testSenderKey;
|
event._senderCurve25519Key = testSenderKey;
|
||||||
return testClient.client._crypto._onRoomKeyEvent(event);
|
return testClient.client.crypto._onRoomKeyEvent(event);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const event = testUtils.mkEvent({
|
const event = testUtils.mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
...rawEvent,
|
...rawEvent,
|
||||||
room: ROOM_ID,
|
room: ROOM_ID,
|
||||||
});
|
});
|
||||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -357,12 +357,12 @@ export function setHttpResponses(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||||
client._http = [
|
client.http = [
|
||||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||||
"request", "requestWithPrefix", "uploadContent",
|
"request", "requestWithPrefix", "uploadContent",
|
||||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||||
client._http.authedRequest.mockImplementation(httpReq);
|
client.http.authedRequest.mockImplementation(httpReq);
|
||||||
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
|
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||||
client._http.requestWithPrefix.mockImplementation(httpReq);
|
client.http.requestWithPrefix.mockImplementation(httpReq);
|
||||||
client._http.request.mockImplementation(httpReq);
|
client.http.request.mockImplementation(httpReq);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe("Crypto", function() {
|
|||||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||||
device.keys["ed25519:FLIBBLE"] =
|
device.keys["ed25519:FLIBBLE"] =
|
||||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||||
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
|
client.crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
|
||||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||||
@@ -213,7 +213,7 @@ describe("Crypto", function() {
|
|||||||
|
|
||||||
async function keyshareEventForEvent(event, index) {
|
async function keyshareEventForEvent(event, index) {
|
||||||
const eventContent = event.getWireContent();
|
const eventContent = event.getWireContent();
|
||||||
const key = await aliceClient._crypto._olmDevice
|
const key = await aliceClient.crypto._olmDevice
|
||||||
.getInboundGroupSessionKey(
|
.getInboundGroupSessionKey(
|
||||||
roomId, eventContent.sender_key, eventContent.session_id,
|
roomId, eventContent.sender_key, eventContent.session_id,
|
||||||
index,
|
index,
|
||||||
@@ -273,19 +273,19 @@ describe("Crypto", function() {
|
|||||||
await Promise.all(events.map(async (event) => {
|
await Promise.all(events.map(async (event) => {
|
||||||
// alice encrypts each event, and then bob tries to decrypt
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
// them without any keys, so that they'll be in pending
|
// them without any keys, so that they'll be in pending
|
||||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
event._clearEvent = {};
|
event._clearEvent = {};
|
||||||
event._senderCurve25519Key = null;
|
event._senderCurve25519Key = null;
|
||||||
event._claimedEd25519Key = null;
|
event._claimedEd25519Key = null;
|
||||||
try {
|
try {
|
||||||
await bobClient._crypto.decryptEvent(event);
|
await bobClient.crypto.decryptEvent(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// we expect this to fail because we don't have the
|
// we expect this to fail because we don't have the
|
||||||
// decryption keys yet
|
// decryption keys yet
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
|
const bobDecryptor = bobClient.crypto._getRoomDecryptor(
|
||||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ describe("Crypto", function() {
|
|||||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||||
|
|
||||||
const cryptoStore = bobClient._cryptoStore;
|
const cryptoStore = bobClient.cryptoStore;
|
||||||
const eventContent = events[0].getWireContent();
|
const eventContent = events[0].getWireContent();
|
||||||
const senderKey = eventContent.sender_key;
|
const senderKey = eventContent.sender_key;
|
||||||
const sessionId = eventContent.session_id;
|
const sessionId = eventContent.session_id;
|
||||||
@@ -344,7 +344,7 @@ describe("Crypto", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||||
const cryptoStore = aliceClient._cryptoStore;
|
const cryptoStore = aliceClient.cryptoStore;
|
||||||
const roomKeyRequestBody = {
|
const roomKeyRequestBody = {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: "!someroom",
|
room_id: "!someroom",
|
||||||
@@ -377,7 +377,7 @@ describe("Crypto", function() {
|
|||||||
// key requests get queued until the sync has finished, but we don't
|
// key requests get queued until the sync has finished, but we don't
|
||||||
// let the client set up enough for that to happen, so gut-wrench a bit
|
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||||
// to force it to send now.
|
// to force it to send now.
|
||||||
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ describe("MegolmDecryption", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("re-uses sessions for sequential messages", async function() {
|
it("re-uses sessions for sequential messages", async function() {
|
||||||
|
mockCrypto._backupManager = {
|
||||||
|
backupGroupSession: () => {},
|
||||||
|
};
|
||||||
const mockStorage = new MockStorageApi();
|
const mockStorage = new MockStorageApi();
|
||||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||||
|
|
||||||
@@ -362,9 +365,9 @@ describe("MegolmDecryption", function() {
|
|||||||
bobClient1.initCrypto(),
|
bobClient1.initCrypto(),
|
||||||
bobClient2.initCrypto(),
|
bobClient2.initCrypto(),
|
||||||
]);
|
]);
|
||||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
const aliceDevice = aliceClient.crypto._olmDevice;
|
||||||
const bobDevice1 = bobClient1._crypto._olmDevice;
|
const bobDevice1 = bobClient1.crypto._olmDevice;
|
||||||
const bobDevice2 = bobClient2._crypto._olmDevice;
|
const bobDevice2 = bobClient2.crypto._olmDevice;
|
||||||
|
|
||||||
const encryptionCfg = {
|
const encryptionCfg = {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
@@ -401,10 +404,10 @@ describe("MegolmDecryption", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
aliceClient.crypto._deviceList.storeDevicesForUser(
|
||||||
"@bob:example.com", BOB_DEVICES,
|
"@bob:example.com", BOB_DEVICES,
|
||||||
);
|
);
|
||||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
aliceClient.crypto._deviceList.downloadKeys = async function(userIds) {
|
||||||
return this._getDevicesFromStore(userIds);
|
return this._getDevicesFromStore(userIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -445,7 +448,7 @@ describe("MegolmDecryption", function() {
|
|||||||
body: "secret",
|
body: "secret",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await aliceClient._crypto.encryptEvent(event, room);
|
await aliceClient.crypto.encryptEvent(event, room);
|
||||||
|
|
||||||
expect(run).toBe(true);
|
expect(run).toBe(true);
|
||||||
|
|
||||||
@@ -465,8 +468,8 @@ describe("MegolmDecryption", function() {
|
|||||||
aliceClient.initCrypto(),
|
aliceClient.initCrypto(),
|
||||||
bobClient.initCrypto(),
|
bobClient.initCrypto(),
|
||||||
]);
|
]);
|
||||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
const aliceDevice = aliceClient.crypto._olmDevice;
|
||||||
const bobDevice = bobClient._crypto._olmDevice;
|
const bobDevice = bobClient.crypto._olmDevice;
|
||||||
|
|
||||||
const encryptionCfg = {
|
const encryptionCfg = {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
@@ -505,10 +508,10 @@ describe("MegolmDecryption", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
aliceClient.crypto._deviceList.storeDevicesForUser(
|
||||||
"@bob:example.com", BOB_DEVICES,
|
"@bob:example.com", BOB_DEVICES,
|
||||||
);
|
);
|
||||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
aliceClient.crypto._deviceList.downloadKeys = async function(userIds) {
|
||||||
return this._getDevicesFromStore(userIds);
|
return this._getDevicesFromStore(userIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -543,7 +546,7 @@ describe("MegolmDecryption", function() {
|
|||||||
event_id: "$event",
|
event_id: "$event",
|
||||||
content: {},
|
content: {},
|
||||||
});
|
});
|
||||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
await sendPromise;
|
await sendPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -558,11 +561,11 @@ describe("MegolmDecryption", function() {
|
|||||||
aliceClient.initCrypto(),
|
aliceClient.initCrypto(),
|
||||||
bobClient.initCrypto(),
|
bobClient.initCrypto(),
|
||||||
]);
|
]);
|
||||||
const bobDevice = bobClient._crypto._olmDevice;
|
const bobDevice = bobClient.crypto._olmDevice;
|
||||||
|
|
||||||
const roomId = "!someroom";
|
const roomId = "!someroom";
|
||||||
|
|
||||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
|
||||||
type: "org.matrix.room_key.withheld",
|
type: "org.matrix.room_key.withheld",
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
content: {
|
content: {
|
||||||
@@ -575,7 +578,7 @@ describe("MegolmDecryption", function() {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||||
type: "m.room.encrypted",
|
type: "m.room.encrypted",
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
event_id: "$event",
|
event_id: "$event",
|
||||||
@@ -601,14 +604,14 @@ describe("MegolmDecryption", function() {
|
|||||||
aliceClient.initCrypto(),
|
aliceClient.initCrypto(),
|
||||||
bobClient.initCrypto(),
|
bobClient.initCrypto(),
|
||||||
]);
|
]);
|
||||||
aliceClient._crypto.downloadKeys = async () => {};
|
aliceClient.crypto.downloadKeys = async () => {};
|
||||||
const bobDevice = bobClient._crypto._olmDevice;
|
const bobDevice = bobClient.crypto._olmDevice;
|
||||||
|
|
||||||
const roomId = "!someroom";
|
const roomId = "!someroom";
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
|
||||||
type: "org.matrix.room_key.withheld",
|
type: "org.matrix.room_key.withheld",
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
content: {
|
content: {
|
||||||
@@ -625,7 +628,7 @@ describe("MegolmDecryption", function() {
|
|||||||
setTimeout(resolve, 100);
|
setTimeout(resolve, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||||
type: "m.room.encrypted",
|
type: "m.room.encrypted",
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
event_id: "$event",
|
event_id: "$event",
|
||||||
@@ -652,15 +655,15 @@ describe("MegolmDecryption", function() {
|
|||||||
aliceClient.initCrypto(),
|
aliceClient.initCrypto(),
|
||||||
bobClient.initCrypto(),
|
bobClient.initCrypto(),
|
||||||
]);
|
]);
|
||||||
const bobDevice = bobClient._crypto._olmDevice;
|
const bobDevice = bobClient.crypto._olmDevice;
|
||||||
aliceClient._crypto.downloadKeys = async () => {};
|
aliceClient.crypto.downloadKeys = async () => {};
|
||||||
|
|
||||||
const roomId = "!someroom";
|
const roomId = "!someroom";
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// pretend we got an event that we can't decrypt
|
// pretend we got an event that we can't decrypt
|
||||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
|
||||||
type: "m.room.encrypted",
|
type: "m.room.encrypted",
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
content: {
|
content: {
|
||||||
@@ -675,7 +678,7 @@ describe("MegolmDecryption", function() {
|
|||||||
setTimeout(resolve, 100);
|
setTimeout(resolve, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||||
type: "m.room.encrypted",
|
type: "m.room.encrypted",
|
||||||
sender: "@bob:example.com",
|
sender: "@bob:example.com",
|
||||||
event_id: "$event",
|
event_id: "$event",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import * as testUtils from "../../test-utils";
|
|||||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||||
import { Crypto } from "../../../src/crypto";
|
import { Crypto } from "../../../src/crypto";
|
||||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||||
|
import { BackupManager } from "../../../src/crypto/backup";
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ const KEY_BACKUP_DATA = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BACKUP_INFO = {
|
const BACKUP_INFO = {
|
||||||
algorithm: "m.megolm_backup.v1",
|
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
version: 1,
|
version: 1,
|
||||||
auth_data: {
|
auth_data: {
|
||||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||||
@@ -138,6 +139,7 @@ describe("MegolmBackup", function() {
|
|||||||
let megolmDecryption;
|
let megolmDecryption;
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||||
|
mockCrypto._backupManager = testUtils.mock(BackupManager, "BackupManager");
|
||||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||||
mockCrypto.backupKey.set_recipient_key(
|
mockCrypto.backupKey.set_recipient_key(
|
||||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||||
@@ -215,12 +217,14 @@ describe("MegolmBackup", function() {
|
|||||||
};
|
};
|
||||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||||
|
|
||||||
mockCrypto.backupGroupSession = jest.fn();
|
mockCrypto._backupManager = {
|
||||||
|
backupGroupSession: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
return event.attemptDecryption(mockCrypto).then(() => {
|
return event.attemptDecryption(mockCrypto).then(() => {
|
||||||
return megolmDecryption.onRoomKeyEvent(event);
|
return megolmDecryption.onRoomKeyEvent(event);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
|
expect(mockCrypto._backupManager.backupGroupSession).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -264,7 +268,7 @@ describe("MegolmBackup", function() {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
client.enableKeyBackup({
|
client.enableKeyBackup({
|
||||||
algorithm: "m.megolm_backup.v1",
|
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
version: 1,
|
version: 1,
|
||||||
auth_data: {
|
auth_data: {
|
||||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||||
@@ -272,7 +276,7 @@ describe("MegolmBackup", function() {
|
|||||||
});
|
});
|
||||||
let numCalls = 0;
|
let numCalls = 0;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
client._http.authedRequest = function(
|
client.http.authedRequest = function(
|
||||||
callback, method, path, queryParams, data, opts,
|
callback, method, path, queryParams, data, opts,
|
||||||
) {
|
) {
|
||||||
++numCalls;
|
++numCalls;
|
||||||
@@ -292,12 +296,9 @@ describe("MegolmBackup", function() {
|
|||||||
resolve();
|
resolve();
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
};
|
};
|
||||||
client._crypto.backupGroupSession(
|
client.crypto._backupManager.backupGroupSession(
|
||||||
"roomId",
|
|
||||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||||
[],
|
|
||||||
groupSession.session_id(),
|
groupSession.session_id(),
|
||||||
groupSession.session_key(),
|
|
||||||
);
|
);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
expect(numCalls).toBe(1);
|
expect(numCalls).toBe(1);
|
||||||
@@ -335,17 +336,15 @@ describe("MegolmBackup", function() {
|
|||||||
});
|
});
|
||||||
await resetCrossSigningKeys(client);
|
await resetCrossSigningKeys(client);
|
||||||
let numCalls = 0;
|
let numCalls = 0;
|
||||||
await new Promise((resolve, reject) => {
|
await Promise.all([
|
||||||
client._http.authedRequest = function(
|
new Promise((resolve, reject) => {
|
||||||
|
let backupInfo;
|
||||||
|
client.http.authedRequest = function(
|
||||||
callback, method, path, queryParams, data, opts,
|
callback, method, path, queryParams, data, opts,
|
||||||
) {
|
) {
|
||||||
++numCalls;
|
++numCalls;
|
||||||
expect(numCalls).toBeLessThanOrEqual(1);
|
expect(numCalls).toBeLessThanOrEqual(2);
|
||||||
if (numCalls >= 2) {
|
if (numCalls === 1) {
|
||||||
// exit out of retry loop if there's something wrong
|
|
||||||
reject(new Error("authedRequest called too many timmes"));
|
|
||||||
return Promise.resolve({});
|
|
||||||
}
|
|
||||||
expect(method).toBe("POST");
|
expect(method).toBe("POST");
|
||||||
expect(path).toBe("/room_keys/version");
|
expect(path).toBe("/room_keys/version");
|
||||||
try {
|
try {
|
||||||
@@ -357,17 +356,28 @@ describe("MegolmBackup", function() {
|
|||||||
reject(e);
|
reject(e);
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
resolve();
|
backupInfo = data;
|
||||||
return Promise.resolve({});
|
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({
|
client.createKeyBackupVersion({
|
||||||
algorithm: "m.megolm_backup.v1",
|
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
auth_data: {
|
auth_data: {
|
||||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
});
|
]);
|
||||||
expect(numCalls).toBe(1);
|
expect(numCalls).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('retries when a backup fails', function() {
|
it('retries when a backup fails', function() {
|
||||||
@@ -434,7 +444,7 @@ describe("MegolmBackup", function() {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
client.enableKeyBackup({
|
client.enableKeyBackup({
|
||||||
algorithm: "foobar",
|
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
version: 1,
|
version: 1,
|
||||||
auth_data: {
|
auth_data: {
|
||||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||||
@@ -442,7 +452,7 @@ describe("MegolmBackup", function() {
|
|||||||
});
|
});
|
||||||
let numCalls = 0;
|
let numCalls = 0;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
client._http.authedRequest = function(
|
client.http.authedRequest = function(
|
||||||
callback, method, path, queryParams, data, opts,
|
callback, method, path, queryParams, data, opts,
|
||||||
) {
|
) {
|
||||||
++numCalls;
|
++numCalls;
|
||||||
@@ -468,12 +478,9 @@ describe("MegolmBackup", function() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client._crypto.backupGroupSession(
|
client.crypto._backupManager.backupGroupSession(
|
||||||
"roomId",
|
|
||||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||||
[],
|
|
||||||
groupSession.session_id(),
|
groupSession.session_id(),
|
||||||
groupSession.session_key(),
|
|
||||||
);
|
);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
expect(numCalls).toBe(2);
|
expect(numCalls).toBe(2);
|
||||||
@@ -506,7 +513,7 @@ describe("MegolmBackup", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can restore from backup', function() {
|
it('can restore from backup', function() {
|
||||||
client._http.authedRequest = function() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve(KEY_BACKUP_DATA);
|
return Promise.resolve(KEY_BACKUP_DATA);
|
||||||
};
|
};
|
||||||
return client.restoreKeyBackupWithRecoveryKey(
|
return client.restoreKeyBackupWithRecoveryKey(
|
||||||
@@ -523,7 +530,7 @@ describe("MegolmBackup", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can restore backup by room', function() {
|
it('can restore backup by room', function() {
|
||||||
client._http.authedRequest = function() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
rooms: {
|
rooms: {
|
||||||
[ROOM_ID]: {
|
[ROOM_ID]: {
|
||||||
@@ -546,15 +553,15 @@ describe("MegolmBackup", function() {
|
|||||||
|
|
||||||
it('has working cache functions', async function() {
|
it('has working cache functions', async function() {
|
||||||
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||||
await client._crypto.storeSessionBackupPrivateKey(key);
|
await client.crypto.storeSessionBackupPrivateKey(key);
|
||||||
const result = await client._crypto.getSessionBackupPrivateKey();
|
const result = await client.crypto.getSessionBackupPrivateKey();
|
||||||
expect(new Uint8Array(result)).toEqual(key);
|
expect(new Uint8Array(result)).toEqual(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('caches session backup keys as it encounters them', async function() {
|
it('caches session backup keys as it encounters them', async function() {
|
||||||
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
|
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||||
expect(cachedNull).toBeNull();
|
expect(cachedNull).toBeNull();
|
||||||
client._http.authedRequest = function() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve(KEY_BACKUP_DATA);
|
return Promise.resolve(KEY_BACKUP_DATA);
|
||||||
};
|
};
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -566,8 +573,24 @@ describe("MegolmBackup", function() {
|
|||||||
{ cacheCompleteCallback: resolve },
|
{ cacheCompleteCallback: resolve },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||||
expect(cachedKey).not.toBeNull();
|
expect(cachedKey).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails if an known algorithm is used", async function() {
|
||||||
|
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
|
||||||
|
algorithm: "this.algorithm.does.not.exist",
|
||||||
|
});
|
||||||
|
client.http.authedRequest = function() {
|
||||||
|
return Promise.resolve(KEY_BACKUP_DATA);
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||||
|
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||||
|
ROOM_ID,
|
||||||
|
SESSION_ID,
|
||||||
|
BAD_BACKUP_INFO,
|
||||||
|
)).rejects.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ describe("Cross Signing", function() {
|
|||||||
);
|
);
|
||||||
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
||||||
await olmlib.verifySignature(
|
await olmlib.verifySignature(
|
||||||
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
|
alice.crypto._olmDevice, keys.master_key, "@alice:example.com",
|
||||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
alice.uploadKeySignatures = async () => {};
|
alice.uploadKeySignatures = async () => {};
|
||||||
@@ -138,7 +138,7 @@ describe("Cross Signing", function() {
|
|||||||
// set Alice's cross-signing key
|
// set Alice's cross-signing key
|
||||||
await resetCrossSigningKeys(alice);
|
await resetCrossSigningKeys(alice);
|
||||||
// Alice downloads Bob's device key
|
// Alice downloads Bob's device key
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@bob:example.com",
|
user_id: "@bob:example.com",
|
||||||
@@ -201,13 +201,14 @@ describe("Cross Signing", function() {
|
|||||||
|
|
||||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||||
alice.uploadKeySignatures = jest.fn(async (content) => {
|
alice.uploadKeySignatures = jest.fn(async (content) => {
|
||||||
|
try {
|
||||||
await olmlib.verifySignature(
|
await olmlib.verifySignature(
|
||||||
alice._crypto._olmDevice,
|
alice.crypto._olmDevice,
|
||||||
content["@alice:example.com"][
|
content["@alice:example.com"][
|
||||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||||
],
|
],
|
||||||
"@alice:example.com",
|
"@alice:example.com",
|
||||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
|
||||||
);
|
);
|
||||||
olmlib.pkVerify(
|
olmlib.pkVerify(
|
||||||
content["@alice:example.com"]["Osborne2"],
|
content["@alice:example.com"]["Osborne2"],
|
||||||
@@ -215,10 +216,13 @@ describe("Cross Signing", function() {
|
|||||||
"@alice:example.com",
|
"@alice:example.com",
|
||||||
);
|
);
|
||||||
resolve();
|
resolve();
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
|
||||||
.Osborne2;
|
.Osborne2;
|
||||||
const aliceDevice = {
|
const aliceDevice = {
|
||||||
user_id: "@alice:example.com",
|
user_id: "@alice:example.com",
|
||||||
@@ -226,7 +230,7 @@ describe("Cross Signing", function() {
|
|||||||
};
|
};
|
||||||
aliceDevice.keys = deviceInfo.keys;
|
aliceDevice.keys = deviceInfo.keys;
|
||||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||||
await alice._crypto._signObject(aliceDevice);
|
await alice.crypto._signObject(aliceDevice);
|
||||||
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
||||||
|
|
||||||
// feed sync result that includes master key, ssk, device key
|
// feed sync result that includes master key, ssk, device key
|
||||||
@@ -354,7 +358,7 @@ describe("Cross Signing", function() {
|
|||||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@bob:example.com",
|
user_id: "@bob:example.com",
|
||||||
@@ -383,7 +387,7 @@ describe("Cross Signing", function() {
|
|||||||
["ed25519:" + bobPubkey]: sig,
|
["ed25519:" + bobPubkey]: sig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||||
Dynabook: bobDevice,
|
Dynabook: bobDevice,
|
||||||
});
|
});
|
||||||
// Bob's device key should be TOFU
|
// Bob's device key should be TOFU
|
||||||
@@ -417,8 +421,8 @@ describe("Cross Signing", function() {
|
|||||||
null,
|
null,
|
||||||
aliceKeys,
|
aliceKeys,
|
||||||
);
|
);
|
||||||
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
|
alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com");
|
||||||
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
|
alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {};
|
||||||
alice.uploadDeviceSigningKeys = async () => {};
|
alice.uploadDeviceSigningKeys = async () => {};
|
||||||
alice.uploadKeySignatures = async () => {};
|
alice.uploadKeySignatures = async () => {};
|
||||||
|
|
||||||
@@ -433,14 +437,14 @@ describe("Cross Signing", function() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const keyChangePromise = new Promise((resolve, reject) => {
|
const keyChangePromise = new Promise((resolve, reject) => {
|
||||||
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
|
alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||||
if (userId === "@bob:example.com") {
|
if (userId === "@bob:example.com") {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
|
||||||
.Osborne2;
|
.Osborne2;
|
||||||
const aliceDevice = {
|
const aliceDevice = {
|
||||||
user_id: "@alice:example.com",
|
user_id: "@alice:example.com",
|
||||||
@@ -448,7 +452,7 @@ describe("Cross Signing", function() {
|
|||||||
};
|
};
|
||||||
aliceDevice.keys = deviceInfo.keys;
|
aliceDevice.keys = deviceInfo.keys;
|
||||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||||
await alice._crypto._signObject(aliceDevice);
|
await alice.crypto._signObject(aliceDevice);
|
||||||
|
|
||||||
const bobOlmAccount = new global.Olm.Account();
|
const bobOlmAccount = new global.Olm.Account();
|
||||||
bobOlmAccount.create();
|
bobOlmAccount.create();
|
||||||
@@ -602,7 +606,7 @@ describe("Cross Signing", function() {
|
|||||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@bob:example.com",
|
user_id: "@bob:example.com",
|
||||||
@@ -625,7 +629,7 @@ describe("Cross Signing", function() {
|
|||||||
"ed25519:Dynabook": "someOtherPubkey",
|
"ed25519:Dynabook": "someOtherPubkey",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||||
Dynabook: bobDevice,
|
Dynabook: bobDevice,
|
||||||
});
|
});
|
||||||
// Bob's device key should be untrusted
|
// Bob's device key should be untrusted
|
||||||
@@ -669,7 +673,7 @@ describe("Cross Signing", function() {
|
|||||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@bob:example.com",
|
user_id: "@bob:example.com",
|
||||||
@@ -697,7 +701,7 @@ describe("Cross Signing", function() {
|
|||||||
bobDevice.signatures = {};
|
bobDevice.signatures = {};
|
||||||
bobDevice.signatures["@bob:example.com"] = {};
|
bobDevice.signatures["@bob:example.com"] = {};
|
||||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
|
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
|
||||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||||
Dynabook: bobDevice,
|
Dynabook: bobDevice,
|
||||||
});
|
});
|
||||||
// Alice verifies Bob's SSK
|
// Alice verifies Bob's SSK
|
||||||
@@ -729,7 +733,7 @@ describe("Cross Signing", function() {
|
|||||||
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@bob:example.com",
|
user_id: "@bob:example.com",
|
||||||
@@ -766,7 +770,7 @@ describe("Cross Signing", function() {
|
|||||||
// Alice gets new signature for device
|
// Alice gets new signature for device
|
||||||
const sig2 = bobSigning2.sign(bobDeviceString);
|
const sig2 = bobSigning2.sign(bobDeviceString);
|
||||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||||
Dynabook: bobDevice,
|
Dynabook: bobDevice,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -801,20 +805,20 @@ describe("Cross Signing", function() {
|
|||||||
bob.uploadKeySignatures = async () => {};
|
bob.uploadKeySignatures = async () => {};
|
||||||
// set Bob's cross-signing key
|
// set Bob's cross-signing key
|
||||||
await resetCrossSigningKeys(bob);
|
await resetCrossSigningKeys(bob);
|
||||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||||
Dynabook: {
|
Dynabook: {
|
||||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||||
keys: {
|
keys: {
|
||||||
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
|
"curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key,
|
||||||
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
|
"ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key,
|
||||||
},
|
},
|
||||||
verified: 1,
|
verified: 1,
|
||||||
known: true,
|
known: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser(
|
alice.crypto._deviceList.storeCrossSigningForUser(
|
||||||
"@bob:example.com",
|
"@bob:example.com",
|
||||||
bob._crypto._crossSigningInfo.toStorage(),
|
bob.crypto._crossSigningInfo.toStorage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
alice.uploadDeviceSigningKeys = async () => {};
|
alice.uploadDeviceSigningKeys = async () => {};
|
||||||
@@ -834,7 +838,7 @@ describe("Cross Signing", function() {
|
|||||||
expect(bobTrust.isTofu()).toBeTruthy();
|
expect(bobTrust.isTofu()).toBeTruthy();
|
||||||
|
|
||||||
// "forget" that Bob is trusted
|
// "forget" that Bob is trusted
|
||||||
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
delete alice.crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||||
.keys.master.signatures["@alice:example.com"];
|
.keys.master.signatures["@alice:example.com"];
|
||||||
|
|
||||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||||
@@ -844,9 +848,9 @@ describe("Cross Signing", function() {
|
|||||||
upgradePromise = new Promise((resolve) => {
|
upgradePromise = new Promise((resolve) => {
|
||||||
upgradeResolveFunc = resolve;
|
upgradeResolveFunc = resolve;
|
||||||
});
|
});
|
||||||
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
alice._crypto.on("userTrustStatusChanged", resolve);
|
alice.crypto.on("userTrustStatusChanged", resolve);
|
||||||
});
|
});
|
||||||
await upgradePromise;
|
await upgradePromise;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function resetCrossSigningKeys(client, {
|
|||||||
level,
|
level,
|
||||||
authUploadDeviceSigningKeys = async func => await func(),
|
authUploadDeviceSigningKeys = async func => await func(),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const crypto = client._crypto;
|
const crypto = client.crypto;
|
||||||
|
|
||||||
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async function makeTestClient(userInfo, options) {
|
|||||||
await client.initCrypto();
|
await client.initCrypto();
|
||||||
|
|
||||||
// No need to download keys for these tests
|
// No need to download keys for these tests
|
||||||
client._crypto.downloadKeys = async function() {};
|
client.crypto.downloadKeys = async function() {};
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
@@ -99,11 +99,11 @@ describe("Secrets", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
alice._crypto._crossSigningInfo.setKeys({
|
alice.crypto._crossSigningInfo.setKeys({
|
||||||
master: signingkeyInfo,
|
master: signingkeyInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretStorage = alice._crypto._secretStorage;
|
const secretStorage = alice.crypto._secretStorage;
|
||||||
|
|
||||||
alice.setAccountData = async function(eventType, contents, callback) {
|
alice.setAccountData = async function(eventType, contents, callback) {
|
||||||
alice.store.storeAccountDataEvents([
|
alice.store.storeAccountDataEvents([
|
||||||
@@ -120,7 +120,7 @@ describe("Secrets", function() {
|
|||||||
const keyAccountData = {
|
const keyAccountData = {
|
||||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||||
};
|
};
|
||||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
await alice.crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||||
|
|
||||||
alice.store.storeAccountDataEvents([
|
alice.store.storeAccountDataEvents([
|
||||||
new MatrixEvent({
|
new MatrixEvent({
|
||||||
@@ -234,11 +234,11 @@ describe("Secrets", function() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const vaxDevice = vax.client._crypto._olmDevice;
|
const vaxDevice = vax.client.crypto._olmDevice;
|
||||||
const osborne2Device = osborne2.client._crypto._olmDevice;
|
const osborne2Device = osborne2.client.crypto._olmDevice;
|
||||||
const secretStorage = osborne2.client._crypto._secretStorage;
|
const secretStorage = osborne2.client.crypto._secretStorage;
|
||||||
|
|
||||||
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
osborne2.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
"VAX": {
|
"VAX": {
|
||||||
user_id: "@alice:example.com",
|
user_id: "@alice:example.com",
|
||||||
device_id: "VAX",
|
device_id: "VAX",
|
||||||
@@ -249,7 +249,7 @@ describe("Secrets", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
vax.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
"Osborne2": {
|
"Osborne2": {
|
||||||
user_id: "@alice:example.com",
|
user_id: "@alice:example.com",
|
||||||
device_id: "Osborne2",
|
device_id: "Osborne2",
|
||||||
@@ -265,7 +265,7 @@ describe("Secrets", function() {
|
|||||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||||
await osborne2Device.markKeysAsPublished();
|
await osborne2Device.markKeysAsPublished();
|
||||||
|
|
||||||
await vax.client._crypto._olmDevice.createOutboundSession(
|
await vax.client.crypto._olmDevice.createOutboundSession(
|
||||||
osborne2Device.deviceCurve25519Key,
|
osborne2Device.deviceCurve25519Key,
|
||||||
Object.values(otks)[0],
|
Object.values(otks)[0],
|
||||||
);
|
);
|
||||||
@@ -334,8 +334,8 @@ describe("Secrets", function() {
|
|||||||
createSecretStorageKey,
|
createSecretStorageKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const crossSigning = bob._crypto._crossSigningInfo;
|
const crossSigning = bob.crypto._crossSigningInfo;
|
||||||
const secretStorage = bob._crypto._secretStorage;
|
const secretStorage = bob.crypto._secretStorage;
|
||||||
|
|
||||||
expect(crossSigning.getId()).toBeTruthy();
|
expect(crossSigning.getId()).toBeTruthy();
|
||||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||||
@@ -376,10 +376,10 @@ describe("Secrets", function() {
|
|||||||
]);
|
]);
|
||||||
this.emit("accountData", event);
|
this.emit("accountData", event);
|
||||||
};
|
};
|
||||||
bob._crypto.checkKeyBackup = async () => {};
|
bob.crypto._backupManager.checkKeyBackup = async () => {};
|
||||||
|
|
||||||
const crossSigning = bob._crypto._crossSigningInfo;
|
const crossSigning = bob.crypto._crossSigningInfo;
|
||||||
const secretStorage = bob._crypto._secretStorage;
|
const secretStorage = bob.crypto._secretStorage;
|
||||||
|
|
||||||
// Set up cross-signing keys from scratch with specific storage key
|
// Set up cross-signing keys from scratch with specific storage key
|
||||||
await bob.bootstrapCrossSigning({
|
await bob.bootstrapCrossSigning({
|
||||||
@@ -394,7 +394,7 @@ describe("Secrets", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Clear local cross-signing keys and read from secret storage
|
// Clear local cross-signing keys and read from secret storage
|
||||||
bob._crypto._deviceList.storeCrossSigningForUser(
|
bob.crypto._deviceList.storeCrossSigningForUser(
|
||||||
"@bob:example.com",
|
"@bob:example.com",
|
||||||
crossSigning.toStorage(),
|
crossSigning.toStorage(),
|
||||||
);
|
);
|
||||||
@@ -479,7 +479,7 @@ describe("Secrets", function() {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@alice:example.com",
|
user_id: "@alice:example.com",
|
||||||
@@ -619,7 +619,7 @@ describe("Secrets", function() {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||||
keys: {
|
keys: {
|
||||||
master: {
|
master: {
|
||||||
user_id: "@alice:example.com",
|
user_id: "@alice:example.com",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function()
|
|||||||
verificationMethods: [verificationMethods.SAS],
|
verificationMethods: [verificationMethods.SAS],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
alice.client.crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||||
return {
|
return {
|
||||||
Dynabook: {
|
Dynabook: {
|
||||||
keys: {
|
keys: {
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ describe("SAS verification", function() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const aliceDevice = alice.client._crypto._olmDevice;
|
const aliceDevice = alice.client.crypto._olmDevice;
|
||||||
const bobDevice = bob.client._crypto._olmDevice;
|
const bobDevice = bob.client.crypto._olmDevice;
|
||||||
|
|
||||||
ALICE_DEVICES = {
|
ALICE_DEVICES = {
|
||||||
Osborne2: {
|
Osborne2: {
|
||||||
@@ -114,14 +114,14 @@ describe("SAS verification", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
alice.client._crypto._deviceList.storeDevicesForUser(
|
alice.client.crypto._deviceList.storeDevicesForUser(
|
||||||
"@bob:example.com", BOB_DEVICES,
|
"@bob:example.com", BOB_DEVICES,
|
||||||
);
|
);
|
||||||
alice.client.downloadKeys = () => {
|
alice.client.downloadKeys = () => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
bob.client._crypto._deviceList.storeDevicesForUser(
|
bob.client.crypto._deviceList.storeDevicesForUser(
|
||||||
"@alice:example.com", ALICE_DEVICES,
|
"@alice:example.com", ALICE_DEVICES,
|
||||||
);
|
);
|
||||||
bob.client.downloadKeys = () => {
|
bob.client.downloadKeys = () => {
|
||||||
@@ -296,9 +296,9 @@ describe("SAS verification", function() {
|
|||||||
|
|
||||||
await resetCrossSigningKeys(bob.client);
|
await resetCrossSigningKeys(bob.client);
|
||||||
|
|
||||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
bob.client.crypto._deviceList.storeCrossSigningForUser(
|
||||||
"@alice:example.com", {
|
"@alice:example.com", {
|
||||||
keys: alice.client._crypto._crossSigningInfo.keys,
|
keys: alice.client.crypto._crossSigningInfo.keys,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe("self-verifications", () => {
|
|||||||
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
|
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
_crypto: {
|
crypto: {
|
||||||
_crossSigningInfo,
|
_crossSigningInfo,
|
||||||
_secretStorage,
|
_secretStorage,
|
||||||
storeSessionBackupPrivateKey,
|
storeSessionBackupPrivateKey,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export async function makeTestClients(userInfos, options) {
|
|||||||
});
|
});
|
||||||
const client = clientMap[userId][deviceId];
|
const client = clientMap[userId][deviceId];
|
||||||
const decryptionPromise = event.isEncrypted() ?
|
const decryptionPromise = event.isEncrypted() ?
|
||||||
event.attemptDecryption(client._crypto) :
|
event.attemptDecryption(client.crypto) :
|
||||||
Promise.resolve();
|
Promise.resolve();
|
||||||
|
|
||||||
decryptionPromise.then(
|
decryptionPromise.then(
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ describe("MatrixClient", function() {
|
|||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
// FIXME: We shouldn't be yanking _http like this.
|
// FIXME: We shouldn't be yanking http like this.
|
||||||
client._http = [
|
client.http = [
|
||||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||||
client._http.authedRequest.mockImplementation(httpReq);
|
client.http.authedRequest.mockImplementation(httpReq);
|
||||||
client._http.request.mockImplementation(httpReq);
|
client.http.request.mockImplementation(httpReq);
|
||||||
|
|
||||||
// set reasonable working defaults
|
// set reasonable working defaults
|
||||||
acceptKeepalives = true;
|
acceptKeepalives = true;
|
||||||
@@ -166,7 +166,7 @@ describe("MatrixClient", function() {
|
|||||||
// means they may call /events and then fail an expect() which will fail
|
// means they may call /events and then fail an expect() which will fail
|
||||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||||
// promises to stop the client from continuing to run.
|
// promises to stop the client from continuing to run.
|
||||||
client._http.authedRequest.mockImplementation(function() {
|
client.http.authedRequest.mockImplementation(function() {
|
||||||
return new Promise(() => {});
|
return new Promise(() => {});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1314,7 +1314,7 @@ describe("Room", function() {
|
|||||||
isRoomEncrypted: function() {
|
isRoomEncrypted: function() {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
_http: {
|
http: {
|
||||||
serverResponse,
|
serverResponse,
|
||||||
authedRequest: function() {
|
authedRequest: function() {
|
||||||
if (this.serverResponse instanceof Error) {
|
if (this.serverResponse instanceof Error) {
|
||||||
@@ -1384,7 +1384,7 @@ describe("Room", function() {
|
|||||||
}
|
}
|
||||||
expect(hasThrown).toEqual(true);
|
expect(hasThrown).toEqual(true);
|
||||||
|
|
||||||
client._http.serverResponse = [memberEvent];
|
client.http.serverResponse = [memberEvent];
|
||||||
await room.loadMembersIfNeeded();
|
await room.loadMembersIfNeeded();
|
||||||
const memberA = room.getMember("@user_a:bar");
|
const memberA = room.getMember("@user_a:bar");
|
||||||
expect(memberA.name).toEqual("User A");
|
expect(memberA.name).toEqual("User A");
|
||||||
|
|||||||
24
src/@types/IIdentityServerProvider.ts
Normal file
24
src/@types/IIdentityServerProvider.ts
Normal 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
28
src/@types/partials.ts
Normal 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
98
src/@types/requests.ts
Normal 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
21
src/@types/signed.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
2427
src/base-apis.js
2427
src/base-apis.js
File diff suppressed because it is too large
Load Diff
6057
src/client.js
6057
src/client.js
File diff suppressed because it is too large
Load Diff
8391
src/client.ts
Normal file
8391
src/client.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -725,7 +725,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
|||||||
/**
|
/**
|
||||||
* Request cross-signing keys from another device during verification.
|
* Request cross-signing keys from another device during verification.
|
||||||
*
|
*
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface
|
* @param {MatrixClient} baseApis base Matrix API interface
|
||||||
* @param {string} userId The user ID being verified
|
* @param {string} userId The user ID being verified
|
||||||
* @param {string} deviceId The device ID being verified
|
* @param {string} deviceId The device ID being verified
|
||||||
*/
|
*/
|
||||||
@@ -739,7 +739,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
|||||||
// it. We return here in order to test.
|
// it. We return here in order to test.
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const client = baseApis;
|
const client = baseApis;
|
||||||
const original = client._crypto._crossSigningInfo;
|
const original = client.crypto._crossSigningInfo;
|
||||||
|
|
||||||
// We already have all of the infrastructure we need to validate and
|
// We already have all of the infrastructure we need to validate and
|
||||||
// cache cross-signing keys, so instead of replicating that, here we set
|
// cache cross-signing keys, so instead of replicating that, here we set
|
||||||
@@ -775,7 +775,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
|||||||
|
|
||||||
// also request and cache the key backup key
|
// also request and cache the key backup key
|
||||||
const backupKeyPromise = new Promise(async resolve => {
|
const backupKeyPromise = new Promise(async resolve => {
|
||||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||||
if (!cachedKey) {
|
if (!cachedKey) {
|
||||||
logger.info("No cached backup key found. Requesting...");
|
logger.info("No cached backup key found. Requesting...");
|
||||||
const secretReq = client.requestSecret(
|
const secretReq = client.requestSecret(
|
||||||
@@ -785,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
|||||||
logger.info("Got key backup key, decoding...");
|
logger.info("Got key backup key, decoding...");
|
||||||
const decodedKey = decodeBase64(base64Key);
|
const decodedKey = decodeBase64(base64Key);
|
||||||
logger.info("Decoded backup key, storing...");
|
logger.info("Decoded backup key, storing...");
|
||||||
client._crypto.storeSessionBackupPrivateKey(
|
client.crypto.storeSessionBackupPrivateKey(
|
||||||
Uint8Array.from(decodedKey),
|
Uint8Array.from(decodedKey),
|
||||||
);
|
);
|
||||||
logger.info("Backup key stored. Starting backup restore...");
|
logger.info("Backup key stored. Starting backup restore...");
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export class EncryptionSetupOperation {
|
|||||||
// The backup is trusted because the user provided the private key.
|
// The backup is trusted because the user provided the private key.
|
||||||
// Sign the backup with the cross signing key so the key backup can
|
// Sign the backup with the cross signing key so the key backup can
|
||||||
// be trusted via cross-signing.
|
// be trusted via cross-signing.
|
||||||
await baseApis._http.authedRequest(
|
await baseApis.http.authedRequest(
|
||||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||||
undefined, {
|
undefined, {
|
||||||
algorithm: this._keyBackupInfo.algorithm,
|
algorithm: this._keyBackupInfo.algorithm,
|
||||||
@@ -214,7 +214,7 @@ export class EncryptionSetupOperation {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// add new key backup
|
// add new key backup
|
||||||
await baseApis._http.authedRequest(
|
await baseApis.http.authedRequest(
|
||||||
undefined, "POST", "/room_keys/version",
|
undefined, "POST", "/room_keys/version",
|
||||||
undefined, this._keyBackupInfo,
|
undefined, this._keyBackupInfo,
|
||||||
{ prefix: PREFIX_UNSTABLE },
|
{ prefix: PREFIX_UNSTABLE },
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
&& this._incomingRequests[deviceId][content.request_id]) {
|
&& this._incomingRequests[deviceId][content.request_id]) {
|
||||||
logger.info("received request cancellation for secret (" + sender
|
logger.info("received request cancellation for secret (" + sender
|
||||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||||
this.baseApis.emit("crypto.secrets.requestCancelled", {
|
this._baseApis.emit("crypto.secrets.requestCancelled", {
|
||||||
user_id: sender,
|
user_id: sender,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
request_id: content.request_id,
|
request_id: content.request_id,
|
||||||
@@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter {
|
|||||||
};
|
};
|
||||||
const encryptedContent = {
|
const encryptedContent = {
|
||||||
algorithm: olmlib.OLM_ALGORITHM,
|
algorithm: olmlib.OLM_ALGORITHM,
|
||||||
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
|
sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key,
|
||||||
ciphertext: {},
|
ciphertext: {},
|
||||||
};
|
};
|
||||||
await olmlib.ensureOlmSessionsForDevices(
|
await olmlib.ensureOlmSessionsForDevices(
|
||||||
this._baseApis._crypto._olmDevice,
|
this._baseApis.crypto._olmDevice,
|
||||||
this._baseApis,
|
this._baseApis,
|
||||||
{
|
{
|
||||||
[sender]: [
|
[sender]: [
|
||||||
@@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
encryptedContent.ciphertext,
|
encryptedContent.ciphertext,
|
||||||
this._baseApis.getUserId(),
|
this._baseApis.getUserId(),
|
||||||
this._baseApis.deviceId,
|
this._baseApis.deviceId,
|
||||||
this._baseApis._crypto._olmDevice,
|
this._baseApis.crypto._olmDevice,
|
||||||
sender,
|
sender,
|
||||||
this._baseApis.getStoredDevice(sender, deviceId),
|
this._baseApis.getStoredDevice(sender, deviceId),
|
||||||
payload,
|
payload,
|
||||||
@@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
if (requestControl) {
|
if (requestControl) {
|
||||||
// make sure that the device that sent it is one of the devices that
|
// make sure that the device that sent it is one of the devices that
|
||||||
// we requested from
|
// we requested from
|
||||||
const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey(
|
const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey(
|
||||||
olmlib.OLM_ALGORITHM,
|
olmlib.OLM_ALGORITHM,
|
||||||
event.getSenderKey(),
|
event.getSenderKey(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const DECRYPTION_CLASSES = {};
|
|||||||
* @param {string} params.deviceId The identifier for this device.
|
* @param {string} params.deviceId The identifier for this device.
|
||||||
* @param {module:crypto} params.crypto crypto core
|
* @param {module:crypto} params.crypto crypto core
|
||||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
* @param {MatrixClient} baseApis base matrix api interface
|
||||||
* @param {string} params.roomId The ID of the room we will be sending to
|
* @param {string} params.roomId The ID of the room we will be sending to
|
||||||
* @param {object} params.config The body of the m.room.encryption event
|
* @param {object} params.config The body of the m.room.encryption event
|
||||||
*/
|
*/
|
||||||
@@ -102,7 +102,7 @@ export class EncryptionAlgorithm {
|
|||||||
* @param {string} params.userId The UserID for the local user
|
* @param {string} params.userId The UserID for the local user
|
||||||
* @param {module:crypto} params.crypto crypto core
|
* @param {module:crypto} params.crypto crypto core
|
||||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
* @param {MatrixClient} baseApis base matrix api interface
|
||||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||||
* from. Null for to-device events.
|
* from. Null for to-device events.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -413,9 +413,8 @@ MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// don't wait for it to complete
|
// don't wait for it to complete
|
||||||
this._crypto.backupGroupSession(
|
this._crypto._backupManager.backupGroupSession(
|
||||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
this._olmDevice.deviceCurve25519Key, sessionId,
|
||||||
sessionId, key.key,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||||
@@ -1425,11 +1424,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
|||||||
});
|
});
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// don't wait for the keys to be backed up for the server
|
// don't wait for the keys to be backed up for the server
|
||||||
this._crypto.backupGroupSession(
|
this._crypto._backupManager.backupGroupSession(senderKey, content.session_id);
|
||||||
content.room_id, senderKey, forwardingKeyChain,
|
|
||||||
content.session_id, content.session_key, keysClaimed,
|
|
||||||
exportFormat,
|
|
||||||
);
|
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||||
});
|
});
|
||||||
@@ -1645,14 +1640,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
if (opts.source !== "backup") {
|
if (opts.source !== "backup") {
|
||||||
// don't wait for it to complete
|
// don't wait for it to complete
|
||||||
this._crypto.backupGroupSession(
|
this._crypto._backupManager.backupGroupSession(
|
||||||
session.room_id,
|
session.sender_key, session.session_id,
|
||||||
session.sender_key,
|
|
||||||
session.forwarding_curve25519_key_chain,
|
|
||||||
session.session_id,
|
|
||||||
session.session_key,
|
|
||||||
session.sender_claimed_keys,
|
|
||||||
true,
|
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
// This throws if the upload failed, but this is fine
|
// This throws if the upload failed, but this is fine
|
||||||
// since it will have written it to the db and will retry.
|
// since it will have written it to the db and will retry.
|
||||||
|
|||||||
131
src/crypto/api.ts
Normal file
131
src/crypto/api.ts
Normal 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
651
src/crypto/backup.ts
Normal 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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -19,10 +19,23 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
|||||||
import { decryptAES, encryptAES } from './aes';
|
import { decryptAES, encryptAES } from './aes';
|
||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { ISecretStorageKeyInfo } from "../matrix";
|
||||||
|
|
||||||
// FIXME: these types should eventually go in a different file
|
// FIXME: these types should eventually go in a different file
|
||||||
type Signatures = Record<string, Record<string, string>>;
|
type Signatures = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
|
export interface IDehydratedDevice {
|
||||||
|
device_id: string; // eslint-disable-line camelcase
|
||||||
|
device_data: ISecretStorageKeyInfo & { // eslint-disable-line camelcase
|
||||||
|
algorithm: string;
|
||||||
|
account: string; // pickle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDehydratedDeviceKeyInfo {
|
||||||
|
passphrase?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface DeviceKeys {
|
interface DeviceKeys {
|
||||||
algorithms: Array<string>;
|
algorithms: Array<string>;
|
||||||
device_id: string; // eslint-disable-line camelcase
|
device_id: string; // eslint-disable-line camelcase
|
||||||
@@ -192,7 +205,7 @@ export class DehydrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.log("Uploading account to server");
|
logger.log("Uploading account to server");
|
||||||
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
|
const dehydrateResult = await this.crypto._baseApis.http.authedRequest(
|
||||||
undefined,
|
undefined,
|
||||||
"PUT",
|
"PUT",
|
||||||
"/dehydrated_device",
|
"/dehydrated_device",
|
||||||
@@ -255,7 +268,7 @@ export class DehydrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.log("Uploading keys to server");
|
logger.log("Uploading keys to server");
|
||||||
await this.crypto._baseApis._http.authedRequest(
|
await this.crypto._baseApis.http.authedRequest(
|
||||||
undefined,
|
undefined,
|
||||||
"POST",
|
"POST",
|
||||||
"/keys/upload/" + encodeURI(deviceId),
|
"/keys/upload/" + encodeURI(deviceId),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018-2019 New Vector Ltd
|
Copyright 2018-2019 New Vector Ltd
|
||||||
Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -26,7 +26,6 @@ import { EventEmitter } from 'events';
|
|||||||
import { ReEmitter } from '../ReEmitter';
|
import { ReEmitter } from '../ReEmitter';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { sleep } from "../utils";
|
|
||||||
import { OlmDevice } from "./OlmDevice";
|
import { OlmDevice } from "./OlmDevice";
|
||||||
import * as olmlib from "./olmlib";
|
import * as olmlib from "./olmlib";
|
||||||
import { DeviceList } from "./DeviceList";
|
import { DeviceList } from "./DeviceList";
|
||||||
@@ -58,6 +57,7 @@ import { KeySignatureUploadError } from "../errors";
|
|||||||
import { decryptAES, encryptAES } from './aes';
|
import { decryptAES, encryptAES } from './aes';
|
||||||
import { DehydrationManager } from './dehydration';
|
import { DehydrationManager } from './dehydration';
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { BackupManager } from "./backup";
|
||||||
|
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
|
|
||||||
@@ -85,7 +85,6 @@ export function isCryptoAvailable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cryptography bits
|
* Cryptography bits
|
||||||
@@ -97,7 +96,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
|||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*
|
*
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
* @param {MatrixClient} baseApis base matrix api interface
|
||||||
*
|
*
|
||||||
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
|
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
|
||||||
* Store to be used for end-to-end crypto session data
|
* Store to be used for end-to-end crypto session data
|
||||||
@@ -154,13 +153,36 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
} else {
|
} else {
|
||||||
this._verificationMethods = defaultVerificationMethods;
|
this._verificationMethods = defaultVerificationMethods;
|
||||||
}
|
}
|
||||||
// track whether this device's megolm keys are being backed up incrementally
|
|
||||||
// to the server or not.
|
this._backupManager = new BackupManager(baseApis, async (algorithm) => {
|
||||||
// XXX: this should probably have a single source of truth from OlmAccount
|
// try to get key from cache
|
||||||
this.backupInfo = null; // The info dict from /room_keys/version
|
const cachedKey = await this.getSessionBackupPrivateKey();
|
||||||
this.backupKey = null; // The encryption key object
|
if (cachedKey) {
|
||||||
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
|
return cachedKey;
|
||||||
this._sendingBackups = false; // Are we currently sending backups?
|
}
|
||||||
|
|
||||||
|
// try to get key from secret storage
|
||||||
|
const storedKey = await this.getSecret("m.megolm_backup.v1");
|
||||||
|
|
||||||
|
if (storedKey) {
|
||||||
|
// ensure that the key is in the right format. If not, fix the key and
|
||||||
|
// store the fixed version
|
||||||
|
const fixedKey = fixBackupKey(storedKey);
|
||||||
|
if (fixedKey) {
|
||||||
|
const [keyId] = await this._crypto.getSecretStorageKey();
|
||||||
|
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return olmlib.decodeBase64(fixedKey || storedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to get key from app
|
||||||
|
if (this._baseApis._cryptoCallbacks && this._baseApis._cryptoCallbacks.getBackupKey) {
|
||||||
|
return await this._baseApis._cryptoCallbacks.getBackupKey(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unable to get private key");
|
||||||
|
});
|
||||||
|
|
||||||
this._olmDevice = new OlmDevice(cryptoStore);
|
this._olmDevice = new OlmDevice(cryptoStore);
|
||||||
this._deviceList = new DeviceList(
|
this._deviceList = new DeviceList(
|
||||||
@@ -232,7 +254,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
// processing the response.
|
// processing the response.
|
||||||
this._sendKeyRequestsImmediately = false;
|
this._sendKeyRequestsImmediately = false;
|
||||||
|
|
||||||
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
|
const cryptoCallbacks = this._baseApis.cryptoCallbacks || {};
|
||||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice);
|
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this._olmDevice);
|
||||||
|
|
||||||
this._crossSigningInfo = new CrossSigningInfo(
|
this._crossSigningInfo = new CrossSigningInfo(
|
||||||
@@ -331,7 +353,7 @@ Crypto.prototype.init = async function(opts) {
|
|||||||
this._deviceList.startTrackingDeviceList(this._userId);
|
this._deviceList.startTrackingDeviceList(this._userId);
|
||||||
|
|
||||||
logger.log("Crypto: checking for key backup...");
|
logger.log("Crypto: checking for key backup...");
|
||||||
this._checkAndStartKeyBackup();
|
this._backupManager.checkAndStart();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,7 +480,7 @@ Crypto.prototype.isSecretStorageReady = async function() {
|
|||||||
this._secretStorage,
|
this._secretStorage,
|
||||||
);
|
);
|
||||||
const sessionBackupInStorage = (
|
const sessionBackupInStorage = (
|
||||||
!this._baseApis.getKeyBackupEnabled() ||
|
!this._backupManager.getKeyBackupEnabled() ||
|
||||||
this._baseApis.isKeyBackupKeyStored()
|
this._baseApis.isKeyBackupKeyStored()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -495,7 +517,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
|||||||
} = {}) {
|
} = {}) {
|
||||||
logger.log("Bootstrapping cross-signing");
|
logger.log("Bootstrapping cross-signing");
|
||||||
|
|
||||||
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
|
const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks;
|
||||||
const builder = new EncryptionSetupBuilder(
|
const builder = new EncryptionSetupBuilder(
|
||||||
this._baseApis.store.accountData,
|
this._baseApis.store.accountData,
|
||||||
delegateCryptoCallbacks,
|
delegateCryptoCallbacks,
|
||||||
@@ -522,9 +544,11 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
|||||||
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
||||||
|
|
||||||
// Sign message key backup with cross-signing master key
|
// Sign message key backup with cross-signing master key
|
||||||
if (this.backupInfo) {
|
if (this._backupManager.backupInfo) {
|
||||||
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
|
await crossSigningInfo.signObject(
|
||||||
builder.addSessionBackup(this.backupInfo);
|
this._backupManager.backupInfo.auth_data, "master",
|
||||||
|
);
|
||||||
|
builder.addSessionBackup(this._backupManager.backupInfo);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -579,7 +603,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
|||||||
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
|
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
|
||||||
if (
|
if (
|
||||||
crossSigningPrivateKeys.size &&
|
crossSigningPrivateKeys.size &&
|
||||||
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys
|
!this._baseApis.cryptoCallbacks.saveCrossSigningKeys
|
||||||
) {
|
) {
|
||||||
const secretStorage = new SecretStorage(
|
const secretStorage = new SecretStorage(
|
||||||
builder.accountDataClientAdapter,
|
builder.accountDataClientAdapter,
|
||||||
@@ -646,7 +670,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
getKeyBackupPassphrase,
|
getKeyBackupPassphrase,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
logger.log("Bootstrapping Secure Secret Storage");
|
logger.log("Bootstrapping Secure Secret Storage");
|
||||||
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
|
const delegateCryptoCallbacks = this._baseApis.cryptoCallbacks;
|
||||||
const builder = new EncryptionSetupBuilder(
|
const builder = new EncryptionSetupBuilder(
|
||||||
this._baseApis.store.accountData,
|
this._baseApis.store.accountData,
|
||||||
delegateCryptoCallbacks,
|
delegateCryptoCallbacks,
|
||||||
@@ -681,7 +705,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
|
|
||||||
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||||
if (!keyInfo.mac) {
|
if (!keyInfo.mac) {
|
||||||
const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey(
|
const key = await this._baseApis.cryptoCallbacks.getSecretStorageKey(
|
||||||
{ keys: { [keyId]: keyInfo } }, "",
|
{ keys: { [keyId]: keyInfo } }, "",
|
||||||
);
|
);
|
||||||
if (key) {
|
if (key) {
|
||||||
@@ -766,6 +790,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
keyBackupInfo.auth_data.private_key_salt &&
|
keyBackupInfo.auth_data.private_key_salt &&
|
||||||
keyBackupInfo.auth_data.private_key_iterations
|
keyBackupInfo.auth_data.private_key_iterations
|
||||||
) {
|
) {
|
||||||
|
// FIXME: ???
|
||||||
opts.passphrase = {
|
opts.passphrase = {
|
||||||
algorithm: "m.pbkdf2",
|
algorithm: "m.pbkdf2",
|
||||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||||
@@ -801,7 +826,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
|||||||
// If we have cross-signing private keys cached, store them in secret
|
// If we have cross-signing private keys cached, store them in secret
|
||||||
// storage if they are not there already.
|
// storage if they are not there already.
|
||||||
if (
|
if (
|
||||||
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys &&
|
!this._baseApis.cryptoCallbacks.saveCrossSigningKeys &&
|
||||||
await this.isCrossSigningReady() &&
|
await this.isCrossSigningReady() &&
|
||||||
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
|
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
|
||||||
) {
|
) {
|
||||||
@@ -1071,7 +1096,7 @@ Crypto.prototype._afterCrossSigningLocalKeyChange = async function() {
|
|||||||
upload({ shouldEmit: true });
|
upload({ shouldEmit: true });
|
||||||
|
|
||||||
const shouldUpgradeCb = (
|
const shouldUpgradeCb = (
|
||||||
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
|
this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications
|
||||||
);
|
);
|
||||||
if (shouldUpgradeCb) {
|
if (shouldUpgradeCb) {
|
||||||
logger.info("Starting device verification upgrade");
|
logger.info("Starting device verification upgrade");
|
||||||
@@ -1477,7 +1502,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now we may be able to trust our key backup
|
// Now we may be able to trust our key backup
|
||||||
await this.checkKeyBackup();
|
await this._backupManager.checkKeyBackup();
|
||||||
// FIXME: if we previously trusted the backup, should we automatically sign
|
// FIXME: if we previously trusted the backup, should we automatically sign
|
||||||
// the backup with the new key (if not already signed)?
|
// the backup with the new key (if not already signed)?
|
||||||
};
|
};
|
||||||
@@ -1509,7 +1534,7 @@ Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
|
|||||||
*/
|
*/
|
||||||
Crypto.prototype._checkDeviceVerifications = async function(userId) {
|
Crypto.prototype._checkDeviceVerifications = async function(userId) {
|
||||||
const shouldUpgradeCb = (
|
const shouldUpgradeCb = (
|
||||||
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
|
this._baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications
|
||||||
);
|
);
|
||||||
if (!shouldUpgradeCb) {
|
if (!shouldUpgradeCb) {
|
||||||
// Upgrading skipped when callback is not present.
|
// Upgrading skipped when callback is not present.
|
||||||
@@ -1539,206 +1564,11 @@ Crypto.prototype._checkDeviceVerifications = async function(userId) {
|
|||||||
logger.info(`Finished device verification upgrade for ${userId}`);
|
logger.info(`Finished device verification upgrade for ${userId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the server for an active key backup and
|
|
||||||
* if one is present and has a valid signature from
|
|
||||||
* one of the user's verified devices, start backing up
|
|
||||||
* to it.
|
|
||||||
*/
|
|
||||||
Crypto.prototype._checkAndStartKeyBackup = async function() {
|
|
||||||
logger.log("Checking key backup status...");
|
|
||||||
if (this._baseApis.isGuest()) {
|
|
||||||
logger.log("Skipping key backup check since user is guest");
|
|
||||||
this._checkedForBackup = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let backupInfo;
|
|
||||||
try {
|
|
||||||
backupInfo = await this._baseApis.getKeyBackupVersion();
|
|
||||||
} catch (e) {
|
|
||||||
logger.log("Error checking for active key backup", e);
|
|
||||||
if (e.httpStatus === 404) {
|
|
||||||
// 404 is returned when the key backup does not exist, so that
|
|
||||||
// counts as successfully checking.
|
|
||||||
this._checkedForBackup = true;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
this._checkedForBackup = true;
|
|
||||||
|
|
||||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
|
||||||
|
|
||||||
if (trustInfo.usable && !this.backupInfo) {
|
|
||||||
logger.log(
|
|
||||||
"Found usable key backup v" + backupInfo.version +
|
|
||||||
": enabling key backups",
|
|
||||||
);
|
|
||||||
this._baseApis.enableKeyBackup(backupInfo);
|
|
||||||
} else if (!trustInfo.usable && this.backupInfo) {
|
|
||||||
logger.log("No usable key backup: disabling key backup");
|
|
||||||
this._baseApis.disableKeyBackup();
|
|
||||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
|
||||||
logger.log("No usable key backup: not enabling key backup");
|
|
||||||
} else if (trustInfo.usable && this.backupInfo) {
|
|
||||||
// may not be the same version: if not, we should switch
|
|
||||||
if (backupInfo.version !== this.backupInfo.version) {
|
|
||||||
logger.log(
|
|
||||||
"On backup version " + this.backupInfo.version + " but found " +
|
|
||||||
"version " + backupInfo.version + ": switching.",
|
|
||||||
);
|
|
||||||
this._baseApis.disableKeyBackup();
|
|
||||||
this._baseApis.enableKeyBackup(backupInfo);
|
|
||||||
// We're now using a new backup, so schedule all the keys we have to be
|
|
||||||
// uploaded to the new backup. This is a bit of a workaround to upload
|
|
||||||
// keys to a new backup in *most* cases, but it won't cover all cases
|
|
||||||
// because we don't remember what backup version we uploaded keys to:
|
|
||||||
// see https://github.com/vector-im/element-web/issues/14833
|
|
||||||
await this.scheduleAllGroupSessionsForBackup();
|
|
||||||
} else {
|
|
||||||
logger.log("Backup version " + backupInfo.version + " still current");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { backupInfo, trustInfo };
|
|
||||||
};
|
|
||||||
|
|
||||||
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
|
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
|
||||||
// This should be redundant post cross-signing is a thing, so just
|
// This should be redundant post cross-signing is a thing, so just
|
||||||
// plonk it in localStorage for now.
|
// plonk it in localStorage for now.
|
||||||
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
||||||
await this.checkKeyBackup();
|
await this._backupManager.checkKeyBackup();
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces a re-check of the key backup and enables/disables it
|
|
||||||
* as appropriate.
|
|
||||||
*
|
|
||||||
* @return {Object} Object with backup info (as returned by
|
|
||||||
* getKeyBackupVersion) in backupInfo and
|
|
||||||
* trust information (as returned by isKeyBackupTrusted)
|
|
||||||
* in trustInfo.
|
|
||||||
*/
|
|
||||||
Crypto.prototype.checkKeyBackup = async function() {
|
|
||||||
this._checkedForBackup = false;
|
|
||||||
return this._checkAndStartKeyBackup();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
|
||||||
* @return {object} {
|
|
||||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
|
||||||
* sigs: [
|
|
||||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
|
||||||
* deviceId: [string],
|
|
||||||
* device: [DeviceInfo || null],
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
|
||||||
const ret = {
|
|
||||||
usable: false,
|
|
||||||
trusted_locally: false,
|
|
||||||
sigs: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
!backupInfo ||
|
|
||||||
!backupInfo.algorithm ||
|
|
||||||
!backupInfo.auth_data ||
|
|
||||||
!backupInfo.auth_data.public_key ||
|
|
||||||
!backupInfo.auth_data.signatures
|
|
||||||
) {
|
|
||||||
logger.info("Key backup is absent or missing required data");
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey();
|
|
||||||
|
|
||||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
|
||||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
|
||||||
ret.trusted_locally = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
|
|
||||||
|
|
||||||
for (const keyId of Object.keys(mySigs)) {
|
|
||||||
const keyIdParts = keyId.split(':');
|
|
||||||
if (keyIdParts[0] !== 'ed25519') {
|
|
||||||
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Could be a cross-signing master key, but just say this is the device
|
|
||||||
// ID for backwards compat
|
|
||||||
const sigInfo = { deviceId: keyIdParts[1] };
|
|
||||||
|
|
||||||
// first check to see if it's from our cross-signing key
|
|
||||||
const crossSigningId = this._crossSigningInfo.getId();
|
|
||||||
if (crossSigningId === sigInfo.deviceId) {
|
|
||||||
sigInfo.crossSigningId = true;
|
|
||||||
try {
|
|
||||||
await olmlib.verifySignature(
|
|
||||||
this._olmDevice,
|
|
||||||
backupInfo.auth_data,
|
|
||||||
this._userId,
|
|
||||||
sigInfo.deviceId,
|
|
||||||
crossSigningId,
|
|
||||||
);
|
|
||||||
sigInfo.valid = true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.warning(
|
|
||||||
"Bad signature from cross signing key " + crossSigningId, e,
|
|
||||||
);
|
|
||||||
sigInfo.valid = false;
|
|
||||||
}
|
|
||||||
ret.sigs.push(sigInfo);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now look for a sig from a device
|
|
||||||
// At some point this can probably go away and we'll just support
|
|
||||||
// it being signed by the cross-signing master key
|
|
||||||
const device = this._deviceList.getStoredDevice(
|
|
||||||
this._userId, sigInfo.deviceId,
|
|
||||||
);
|
|
||||||
if (device) {
|
|
||||||
sigInfo.device = device;
|
|
||||||
sigInfo.deviceTrust = await this.checkDeviceTrust(
|
|
||||||
this._userId, sigInfo.deviceId,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await olmlib.verifySignature(
|
|
||||||
this._olmDevice,
|
|
||||||
backupInfo.auth_data,
|
|
||||||
this._userId,
|
|
||||||
device.deviceId,
|
|
||||||
device.getFingerprint(),
|
|
||||||
);
|
|
||||||
sigInfo.valid = true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.info(
|
|
||||||
"Bad signature from key ID " + keyId + " userID " + this._userId +
|
|
||||||
" device ID " + device.deviceId + " fingerprint: " +
|
|
||||||
device.getFingerprint(), backupInfo.auth_data, e,
|
|
||||||
);
|
|
||||||
sigInfo.valid = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
|
||||||
logger.info("Ignoring signature from unknown key " + keyId);
|
|
||||||
}
|
|
||||||
ret.sigs.push(sigInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.usable = ret.sigs.some((s) => {
|
|
||||||
return (
|
|
||||||
s.valid && (
|
|
||||||
(s.device && s.deviceTrust.isVerified()) ||
|
|
||||||
(s.crossSigningId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ret.usable |= ret.trusted_locally;
|
|
||||||
return ret;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2785,191 +2615,12 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
|
||||||
* scheduled. Retries if necessary.
|
|
||||||
*
|
|
||||||
* @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay.
|
|
||||||
*/
|
|
||||||
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
|
|
||||||
if (this._sendingBackups) return;
|
|
||||||
|
|
||||||
this._sendingBackups = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
|
||||||
// requests from different clients hitting the server all at
|
|
||||||
// the same time when a new key is sent
|
|
||||||
const delay = Math.random() * maxDelay;
|
|
||||||
await sleep(delay);
|
|
||||||
let numFailures = 0; // number of consecutive failures
|
|
||||||
while (1) {
|
|
||||||
if (!this.backupKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const numBackedUp =
|
|
||||||
await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
|
||||||
if (numBackedUp === 0) {
|
|
||||||
// no sessions left needing backup: we're done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
numFailures = 0;
|
|
||||||
} catch (err) {
|
|
||||||
numFailures++;
|
|
||||||
logger.log("Key backup request failed", err);
|
|
||||||
if (err.data) {
|
|
||||||
if (
|
|
||||||
err.data.errcode == 'M_NOT_FOUND' ||
|
|
||||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
|
||||||
) {
|
|
||||||
// Re-check key backup status on error, so we can be
|
|
||||||
// sure to present the current situation when asked.
|
|
||||||
await this.checkKeyBackup();
|
|
||||||
// Backup version has changed or this backup version
|
|
||||||
// has been deleted
|
|
||||||
this.emit("crypto.keyBackupFailed", err.data.errcode);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (numFailures) {
|
|
||||||
// exponential backoff if we have failures
|
|
||||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this._sendingBackups = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take some e2e keys waiting to be backed up and send them
|
|
||||||
* to the backup.
|
|
||||||
*
|
|
||||||
* @param {integer} limit Maximum number of keys to back up
|
|
||||||
* @returns {integer} Number of sessions backed up
|
|
||||||
*/
|
|
||||||
Crypto.prototype._backupPendingKeys = async function(limit) {
|
|
||||||
const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit);
|
|
||||||
if (!sessions.length) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
|
||||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
|
||||||
|
|
||||||
const data = {};
|
|
||||||
for (const session of sessions) {
|
|
||||||
const roomId = session.sessionData.room_id;
|
|
||||||
if (data[roomId] === undefined) {
|
|
||||||
data[roomId] = { sessions: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionData = await this._olmDevice.exportInboundGroupSession(
|
|
||||||
session.senderKey, session.sessionId, session.sessionData,
|
|
||||||
);
|
|
||||||
sessionData.algorithm = olmlib.MEGOLM_ALGORITHM;
|
|
||||||
delete sessionData.session_id;
|
|
||||||
delete sessionData.room_id;
|
|
||||||
const firstKnownIndex = sessionData.first_known_index;
|
|
||||||
delete sessionData.first_known_index;
|
|
||||||
const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData));
|
|
||||||
|
|
||||||
const forwardedCount =
|
|
||||||
(sessionData.forwarding_curve25519_key_chain || []).length;
|
|
||||||
|
|
||||||
const userId = this._deviceList.getUserByIdentityKey(
|
|
||||||
olmlib.MEGOLM_ALGORITHM, session.senderKey,
|
|
||||||
);
|
|
||||||
const device = this._deviceList.getDeviceByIdentityKey(
|
|
||||||
olmlib.MEGOLM_ALGORITHM, session.senderKey,
|
|
||||||
);
|
|
||||||
const verified = this._checkDeviceInfoTrust(userId, device).isVerified();
|
|
||||||
|
|
||||||
data[roomId]['sessions'][session.sessionId] = {
|
|
||||||
first_message_index: firstKnownIndex,
|
|
||||||
forwarded_count: forwardedCount,
|
|
||||||
is_verified: verified,
|
|
||||||
session_data: encrypted,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._baseApis.sendKeyBackup(
|
|
||||||
undefined, undefined, this.backupInfo.version,
|
|
||||||
{ rooms: data },
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
|
||||||
remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
|
||||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
|
||||||
|
|
||||||
return sessions.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
Crypto.prototype.backupGroupSession = async function(
|
|
||||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
|
||||||
sessionId, sessionKey, keysClaimed,
|
|
||||||
exportFormat,
|
|
||||||
) {
|
|
||||||
await this._cryptoStore.markSessionsNeedingBackup([{
|
|
||||||
senderKey: senderKey,
|
|
||||||
sessionId: sessionId,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
if (this.backupInfo) {
|
|
||||||
// don't wait for this to complete: it will delay so
|
|
||||||
// happens in the background
|
|
||||||
this.scheduleKeyBackupSend();
|
|
||||||
}
|
|
||||||
// if this.backupInfo is not set, then the keys will be backed up when
|
|
||||||
// client.enableKeyBackup is called
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks all group sessions as needing to be backed up and schedules them to
|
|
||||||
* upload in the background as soon as possible.
|
|
||||||
*/
|
|
||||||
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
|
||||||
await this.flagAllGroupSessionsForBackup();
|
|
||||||
|
|
||||||
// Schedule keys to upload in the background as soon as possible.
|
|
||||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks all group sessions as needing to be backed up without scheduling
|
|
||||||
* them to upload in the background.
|
|
||||||
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
|
|
||||||
* (which will be equal to the number of sessions in the store).
|
|
||||||
*/
|
|
||||||
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
|
|
||||||
await this._cryptoStore.doTxn(
|
|
||||||
'readwrite',
|
|
||||||
[
|
|
||||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
|
||||||
IndexedDBCryptoStore.STORE_BACKUP,
|
|
||||||
],
|
|
||||||
(txn) => {
|
|
||||||
this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
|
|
||||||
if (session !== null) {
|
|
||||||
this._cryptoStore.markSessionsNeedingBackup([session], txn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
|
||||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
|
||||||
return remaining;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts the number of end to end session keys that are waiting to be backed up
|
* Counts the number of end to end session keys that are waiting to be backed up
|
||||||
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.countSessionsNeedingBackup = function() {
|
Crypto.prototype.countSessionsNeedingBackup = function() {
|
||||||
return this._cryptoStore.countSessionsNeedingBackup();
|
return this._backupManager.countSessionsNeedingBackup();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3345,10 +2996,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._checkedForBackup) {
|
if (!this._backupManager.checkedForBackup) {
|
||||||
// don't bother awaiting on this - the important thing is that we retry if we
|
// don't bother awaiting on this - the important thing is that we retry if we
|
||||||
// haven't managed to check before
|
// haven't managed to check before
|
||||||
this._checkAndStartKeyBackup();
|
this._backupManager.checkAndStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||||
|
|||||||
70
src/crypto/keybackup.ts
Normal file
70
src/crypto/keybackup.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ export async function encryptMessageForDevice(
|
|||||||
*
|
*
|
||||||
* @param {module:crypto/OlmDevice} olmDevice
|
* @param {module:crypto/OlmDevice} olmDevice
|
||||||
*
|
*
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
* @param {MatrixClient} baseApis
|
||||||
*
|
*
|
||||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||||
* map from userid to list of devices to ensure sessions for
|
* map from userid to list of devices to ensure sessions for
|
||||||
@@ -168,7 +168,7 @@ export async function getExistingOlmSessions(
|
|||||||
*
|
*
|
||||||
* @param {module:crypto/OlmDevice} olmDevice
|
* @param {module:crypto/OlmDevice} olmDevice
|
||||||
*
|
*
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
* @param {MatrixClient} baseApis
|
||||||
*
|
*
|
||||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||||
* map from userid to list of devices to ensure sessions for
|
* map from userid to list of devices to ensure sessions for
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ export class VerificationBase extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
*
|
*
|
||||||
* @param {module:base-apis~Channel} channel the verification channel to send verification messages over.
|
* @param {Object} channel the verification channel to send verification messages over.
|
||||||
|
* TODO: Channel types
|
||||||
*
|
*
|
||||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
* @param {MatrixClient} baseApis base matrix api interface
|
||||||
*
|
*
|
||||||
* @param {string} userId the user ID that is being verified
|
* @param {string} userId the user ID that is being verified
|
||||||
*
|
*
|
||||||
@@ -291,7 +292,7 @@ export class VerificationBase extends EventEmitter {
|
|||||||
await verifier(keyId, device, keyInfo);
|
await verifier(keyId, device, keyInfo);
|
||||||
verifiedDevices.push(deviceId);
|
verifiedDevices.push(deviceId);
|
||||||
} else {
|
} else {
|
||||||
const crossSigningInfo = this._baseApis._crypto._deviceList
|
const crossSigningInfo = this._baseApis.crypto._deviceList
|
||||||
.getStoredCrossSigningForUser(userId);
|
.getStoredCrossSigningForUser(userId);
|
||||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||||
await verifier(keyId, DeviceInfo.fromStorage({
|
await verifier(keyId, DeviceInfo.fromStorage({
|
||||||
|
|||||||
50
src/event-mapper.ts
Normal file
50
src/event-mapper.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -16,17 +14,11 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type Request from "request";
|
|
||||||
|
|
||||||
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
|
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
|
||||||
import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
|
|
||||||
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
|
|
||||||
import { MemoryStore } from "./store/memory";
|
import { MemoryStore } from "./store/memory";
|
||||||
import { StubStore } from "./store/stub";
|
|
||||||
import { LocalIndexedDBStoreBackend } from "./store/indexeddb-local-backend";
|
|
||||||
import { RemoteIndexedDBStoreBackend } from "./store/indexeddb-remote-backend";
|
|
||||||
import { MatrixScheduler } from "./scheduler";
|
import { MatrixScheduler } from "./scheduler";
|
||||||
import { MatrixClient } from "./client";
|
import { MatrixClient } from "./client";
|
||||||
|
import { ICreateClientOpts } from "./client";
|
||||||
|
|
||||||
export * from "./client";
|
export * from "./client";
|
||||||
export * from "./http-api";
|
export * from "./http-api";
|
||||||
@@ -94,11 +86,6 @@ export function wrapRequest(wrapper) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type Store =
|
|
||||||
StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
|
||||||
|
|
||||||
type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
|
|
||||||
|
|
||||||
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,41 +98,6 @@ export function setCryptoStoreFactory(fac) {
|
|||||||
cryptoStoreFactory = fac;
|
cryptoStoreFactory = fac;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateClientOpts {
|
|
||||||
baseUrl: string;
|
|
||||||
idBaseUrl?: string;
|
|
||||||
store?: Store;
|
|
||||||
cryptoStore?: CryptoStore;
|
|
||||||
scheduler?: MatrixScheduler;
|
|
||||||
request?: Request;
|
|
||||||
userId?: string;
|
|
||||||
deviceId?: string;
|
|
||||||
accessToken?: string;
|
|
||||||
identityServer?: any;
|
|
||||||
localTimeoutMs?: number;
|
|
||||||
useAuthorizationHeader?: boolean;
|
|
||||||
timelineSupport?: boolean;
|
|
||||||
queryParams?: Record<string, unknown>;
|
|
||||||
deviceToImport?: {
|
|
||||||
olmDevice: {
|
|
||||||
pickledAccount: string;
|
|
||||||
sessions: Array<Record<string, any>>;
|
|
||||||
pickleKey: string;
|
|
||||||
};
|
|
||||||
userId: string;
|
|
||||||
deviceId: string;
|
|
||||||
};
|
|
||||||
pickleKey?: string;
|
|
||||||
sessionStore?: any;
|
|
||||||
unstableClientRelationAggregation?: boolean;
|
|
||||||
verificationMethods?: Array<any>;
|
|
||||||
forceTURN?: boolean;
|
|
||||||
iceCandidatePoolSize?: number,
|
|
||||||
supportsCallTransfer?: boolean,
|
|
||||||
fallbackICEServerAllowed?: boolean;
|
|
||||||
cryptoCallbacks?: ICryptoCallbacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICryptoCallbacks {
|
export interface ICryptoCallbacks {
|
||||||
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
|
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
|
||||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||||
@@ -166,6 +118,7 @@ export interface ICryptoCallbacks {
|
|||||||
keyInfo: ISecretStorageKeyInfo,
|
keyInfo: ISecretStorageKeyInfo,
|
||||||
checkFunc: (Uint8Array) => void,
|
checkFunc: (Uint8Array) => void,
|
||||||
) => Promise<Uint8Array>;
|
) => Promise<Uint8Array>;
|
||||||
|
getBackupKey?: () => Promise<Uint8Array>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to `SecretStorage` once converted
|
// TODO: Move this to `SecretStorage` once converted
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ export class Relations extends EventEmitter {
|
|||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
if (lastReplacement?.shouldAttemptDecryption()) {
|
if (lastReplacement?.shouldAttemptDecryption()) {
|
||||||
await lastReplacement.attemptDecryption(this._room._client._crypto);
|
await lastReplacement.attemptDecryption(this._room._client.crypto);
|
||||||
} else if (lastReplacement?.isBeingDecrypted()) {
|
} else if (lastReplacement?.isBeingDecrypted()) {
|
||||||
await lastReplacement._decryptionPromise;
|
await lastReplacement._decryptionPromise;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,11 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
|||||||
self._updateMember(member);
|
self._updateMember(member);
|
||||||
self.emit("RoomState.members", event, self, member);
|
self.emit("RoomState.members", event, self, member);
|
||||||
} else if (event.getType() === "m.room.power_levels") {
|
} else if (event.getType() === "m.room.power_levels") {
|
||||||
|
// events with unknown state keys should be ignored
|
||||||
|
// and should not aggregate onto members power levels
|
||||||
|
if (event.getStateKey() !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const members = Object.values(self.members);
|
const members = Object.values(self.members);
|
||||||
members.forEach(function(member) {
|
members.forEach(function(member) {
|
||||||
// We only propagate `RoomState.members` event if the
|
// We only propagate `RoomState.members` event if the
|
||||||
|
|||||||
@@ -192,13 +192,13 @@ export function Room(roomId, client, myUserId, opts) {
|
|||||||
|
|
||||||
if (this._opts.pendingEventOrdering == "detached") {
|
if (this._opts.pendingEventOrdering == "detached") {
|
||||||
this._pendingEventList = [];
|
this._pendingEventList = [];
|
||||||
const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId));
|
const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
|
||||||
if (serializedPendingEventList) {
|
if (serializedPendingEventList) {
|
||||||
JSON.parse(serializedPendingEventList)
|
JSON.parse(serializedPendingEventList)
|
||||||
.forEach(async serializedEvent => {
|
.forEach(async serializedEvent => {
|
||||||
const event = new MatrixEvent(serializedEvent);
|
const event = new MatrixEvent(serializedEvent);
|
||||||
if (event.getType() === "m.room.encrypted") {
|
if (event.getType() === "m.room.encrypted") {
|
||||||
await event.attemptDecryption(this._client._crypto);
|
await event.attemptDecryption(this._client.crypto);
|
||||||
}
|
}
|
||||||
event.setStatus(EventStatus.NOT_SENT);
|
event.setStatus(EventStatus.NOT_SENT);
|
||||||
this.addPendingEvent(event, event.getTxnId());
|
this.addPendingEvent(event, event.getTxnId());
|
||||||
@@ -255,7 +255,7 @@ Room.prototype.decryptCriticalEvents = function() {
|
|||||||
.slice(readReceiptTimelineIndex)
|
.slice(readReceiptTimelineIndex)
|
||||||
.filter(event => event.shouldAttemptDecryption())
|
.filter(event => event.shouldAttemptDecryption())
|
||||||
.reverse()
|
.reverse()
|
||||||
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
|
.map(event => event.attemptDecryption(this._client.crypto, { isRetry: true }));
|
||||||
|
|
||||||
return Promise.allSettled(decryptionPromises);
|
return Promise.allSettled(decryptionPromises);
|
||||||
};
|
};
|
||||||
@@ -272,7 +272,7 @@ Room.prototype.decryptAllEvents = function() {
|
|||||||
.getEvents()
|
.getEvents()
|
||||||
.filter(event => event.shouldAttemptDecryption())
|
.filter(event => event.shouldAttemptDecryption())
|
||||||
.reverse()
|
.reverse()
|
||||||
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
|
.map(event => event.attemptDecryption(this._client.crypto, { isRetry: true }));
|
||||||
|
|
||||||
return Promise.allSettled(decryptionPromises);
|
return Promise.allSettled(decryptionPromises);
|
||||||
};
|
};
|
||||||
@@ -632,7 +632,7 @@ Room.prototype._loadMembersFromServer = async function() {
|
|||||||
});
|
});
|
||||||
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
|
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
|
||||||
{ $roomId: this.roomId });
|
{ $roomId: this.roomId });
|
||||||
const http = this._client._http;
|
const http = this._client.http;
|
||||||
const response = await http.authedRequest(undefined, "GET", path);
|
const response = await http.authedRequest(undefined, "GET", path);
|
||||||
return response.chunk;
|
return response.chunk;
|
||||||
};
|
};
|
||||||
@@ -674,7 +674,7 @@ Room.prototype.loadMembersIfNeeded = function() {
|
|||||||
this.currentState.setOutOfBandMembers(result.memberEvents);
|
this.currentState.setOutOfBandMembers(result.memberEvents);
|
||||||
// now the members are loaded, start to track the e2e devices if needed
|
// now the members are loaded, start to track the e2e devices if needed
|
||||||
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
|
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
|
||||||
this._client._crypto.trackRoomDevices(this.roomId);
|
this._client.crypto.trackRoomDevices(this.roomId);
|
||||||
}
|
}
|
||||||
return result.fromServer;
|
return result.fromServer;
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -1387,7 +1387,7 @@ Room.prototype._savePendingEvents = function() {
|
|||||||
return isEventEncrypted || !isRoomEncrypted;
|
return isEventEncrypted || !isRoomEncrypted;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { store } = this._client._sessionStore;
|
const { store } = this._client.sessionStore;
|
||||||
if (this._pendingEventList.length > 0) {
|
if (this._pendingEventList.length > 0) {
|
||||||
store.setItem(
|
store.setItem(
|
||||||
pendingEventsKey(this.roomId),
|
pendingEventsKey(this.roomId),
|
||||||
|
|||||||
26
src/sync.api.ts
Normal file
26
src/sync.api.ts
Normal 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",
|
||||||
|
}
|
||||||
26
src/sync.js
26
src/sync.js
@@ -21,7 +21,7 @@ limitations under the License.
|
|||||||
* TODO:
|
* TODO:
|
||||||
* This class mainly serves to take all the syncing logic out of client.js and
|
* This class mainly serves to take all the syncing logic out of client.js and
|
||||||
* into a separate file. It's all very fluid, and this class gut wrenches a lot
|
* into a separate file. It's all very fluid, and this class gut wrenches a lot
|
||||||
* of MatrixClient props (e.g. _http). Given we want to support WebSockets as
|
* of MatrixClient props (e.g. http). Given we want to support WebSockets as
|
||||||
* an alternative syncing API, we may want to have a proper syncing interface
|
* an alternative syncing API, we may want to have a proper syncing interface
|
||||||
* for HTTP and WS at some point.
|
* for HTTP and WS at some point.
|
||||||
*/
|
*/
|
||||||
@@ -209,7 +209,7 @@ SyncApi.prototype.syncLeftRooms = function() {
|
|||||||
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
|
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
|
||||||
).then(function(filterId) {
|
).then(function(filterId) {
|
||||||
qps.filter = filterId;
|
qps.filter = filterId;
|
||||||
return client._http.authedRequest(
|
return client.http.authedRequest(
|
||||||
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
|
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
|
||||||
);
|
);
|
||||||
}).then(function(data) {
|
}).then(function(data) {
|
||||||
@@ -349,7 +349,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
|
|||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
// FIXME: gut wrenching; hard-coded timeout values
|
// FIXME: gut wrenching; hard-coded timeout values
|
||||||
this.client._http.authedRequest(undefined, "GET", "/events", {
|
this.client.http.authedRequest(undefined, "GET", "/events", {
|
||||||
room_id: peekRoom.roomId,
|
room_id: peekRoom.roomId,
|
||||||
timeout: 30 * 1000,
|
timeout: 30 * 1000,
|
||||||
from: token,
|
from: token,
|
||||||
@@ -551,7 +551,7 @@ SyncApi.prototype.sync = function() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
debuglog("Storing client options...");
|
debuglog("Storing client options...");
|
||||||
await this.client._storeClientOptions();
|
await this.client.storeClientOptions();
|
||||||
debuglog("Stored client options");
|
debuglog("Stored client options");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Storing client options failed", err);
|
logger.error("Storing client options failed", err);
|
||||||
@@ -815,7 +815,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
|||||||
|
|
||||||
SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) {
|
SyncApi.prototype._doSyncRequest = function(syncOptions, syncToken) {
|
||||||
const qps = this._getSyncParams(syncOptions, syncToken);
|
const qps = this._getSyncParams(syncOptions, syncToken);
|
||||||
return this.client._http.authedRequest(
|
return this.client.http.authedRequest(
|
||||||
undefined, "GET", "/sync", qps, undefined,
|
undefined, "GET", "/sync", qps, undefined,
|
||||||
qps.timeout + BUFFER_PERIOD_MS,
|
qps.timeout + BUFFER_PERIOD_MS,
|
||||||
);
|
);
|
||||||
@@ -1426,7 +1426,7 @@ SyncApi.prototype._pokeKeepAlive = function(connDidFail) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client._http.request(
|
this.client.http.request(
|
||||||
undefined, // callback
|
undefined, // callback
|
||||||
"GET", "/_matrix/client/versions",
|
"GET", "/_matrix/client/versions",
|
||||||
undefined, // queryParams
|
undefined, // queryParams
|
||||||
@@ -1671,19 +1671,9 @@ SyncApi.prototype._processEventsForNotifs = function(room, timelineEventList) {
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
SyncApi.prototype._getGuestFilter = function() {
|
SyncApi.prototype._getGuestFilter = function() {
|
||||||
const guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
|
// Dev note: This used to be conditional to return a filter of 20 events maximum, but
|
||||||
if (!guestRooms) {
|
// the condition never went to the other branch. This is now hardcoded.
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
|
||||||
// we just need to specify the filter inline if we're a guest because guests
|
|
||||||
// can't create filters.
|
|
||||||
return JSON.stringify({
|
|
||||||
room: {
|
|
||||||
timeline: {
|
|
||||||
limit: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ export function ensureNoTrailingSlash(url: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise which resolves with a given value after the given number of ms
|
// Returns a promise which resolves with a given value after the given number of ms
|
||||||
export function sleep<T>(ms: number, value: T): Promise<T> {
|
export function sleep<T>(ms: number, value?: T): Promise<T> {
|
||||||
return new Promise((resolve => {
|
return new Promise((resolve => {
|
||||||
setTimeout(resolve, ms, value);
|
setTimeout(resolve, ms, value);
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
|
|
||||||
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
||||||
// poll and keep the credentials valid so this should be instant.
|
// poll and keep the credentials valid so this should be instant.
|
||||||
const haveTurnCreds = await this.client._checkTurnServers();
|
const haveTurnCreds = await this.client.checkTurnServers();
|
||||||
if (!haveTurnCreds) {
|
if (!haveTurnCreds) {
|
||||||
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
|
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
|
||||||
}
|
}
|
||||||
@@ -846,7 +846,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
},
|
},
|
||||||
} as MCallAnswer;
|
} as MCallAnswer;
|
||||||
|
|
||||||
if (this.client._supportsCallTransfer) {
|
if (this.client.supportsCallTransfer) {
|
||||||
answerContent.capabilities = {
|
answerContent.capabilities = {
|
||||||
'm.call.transferee': true,
|
'm.call.transferee': true,
|
||||||
};
|
};
|
||||||
@@ -1106,21 +1106,8 @@ export class MatrixCall extends EventEmitter {
|
|||||||
await this.peerConn.setRemoteDescription(description);
|
await this.peerConn.setRemoteDescription(description);
|
||||||
|
|
||||||
if (description.type === 'offer') {
|
if (description.type === 'offer') {
|
||||||
// First we sent the direction of the tranciever to what we'd like it to be,
|
|
||||||
// irresepective of whether the other side has us on hold - so just whether we
|
|
||||||
// want the call to be on hold or not. This is necessary because in a few lines,
|
|
||||||
// we'll adjust the direction and unless we do this too, we'll never come off hold.
|
|
||||||
for (const tranceiver of this.peerConn.getTransceivers()) {
|
|
||||||
tranceiver.direction = this.isRemoteOnHold() ? 'inactive' : 'sendrecv';
|
|
||||||
}
|
|
||||||
const localDescription = await this.peerConn.createAnswer();
|
const localDescription = await this.peerConn.createAnswer();
|
||||||
await this.peerConn.setLocalDescription(localDescription);
|
await this.peerConn.setLocalDescription(localDescription);
|
||||||
// Now we've got our answer, set the direction to the outcome of the negotiation.
|
|
||||||
// We need to do this otherwise Firefox will notice that the direction is not the
|
|
||||||
// currentDirection and try to negotiate itself off hold again.
|
|
||||||
for (const tranceiver of this.peerConn.getTransceivers()) {
|
|
||||||
tranceiver.direction = tranceiver.currentDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendVoipEvent(EventType.CallNegotiate, {
|
this.sendVoipEvent(EventType.CallNegotiate, {
|
||||||
description: this.peerConn.localDescription,
|
description: this.peerConn.localDescription,
|
||||||
@@ -1194,7 +1181,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
content.description = this.peerConn.localDescription;
|
content.description = this.peerConn.localDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.client._supportsCallTransfer) {
|
if (this.client.supportsCallTransfer) {
|
||||||
content.capabilities = {
|
content.capabilities = {
|
||||||
'm.call.transferee': true,
|
'm.call.transferee': true,
|
||||||
};
|
};
|
||||||
@@ -1592,14 +1579,14 @@ export class MatrixCall extends EventEmitter {
|
|||||||
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
|
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
|
||||||
logger.log("Getting user media with constraints", constraints);
|
logger.log("Getting user media with constraints", constraints);
|
||||||
// XXX Find a better way to do this
|
// XXX Find a better way to do this
|
||||||
this.client._callEventHandler.calls.set(this.callId, this);
|
this.client.callEventHandler.calls.set(this.callId, this);
|
||||||
this.setState(CallState.WaitLocalMedia);
|
this.setState(CallState.WaitLocalMedia);
|
||||||
this.direction = CallDirection.Outbound;
|
this.direction = CallDirection.Outbound;
|
||||||
this.config = constraints;
|
this.config = constraints;
|
||||||
|
|
||||||
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
// make sure we have valid turn creds. Unless something's gone wrong, it should
|
||||||
// poll and keep the credentials valid so this should be instant.
|
// poll and keep the credentials valid so this should be instant.
|
||||||
const haveTurnCreds = await this.client._checkTurnServers();
|
const haveTurnCreds = await this.client.checkTurnServers();
|
||||||
if (!haveTurnCreds) {
|
if (!haveTurnCreds) {
|
||||||
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
|
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
|
||||||
}
|
}
|
||||||
@@ -1621,7 +1608,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
const pc = new window.RTCPeerConnection({
|
const pc = new window.RTCPeerConnection({
|
||||||
iceTransportPolicy: this.forceTURN ? 'relay' : undefined,
|
iceTransportPolicy: this.forceTURN ? 'relay' : undefined,
|
||||||
iceServers: this.turnServers,
|
iceServers: this.turnServers,
|
||||||
iceCandidatePoolSize: this.client._iceCandidatePoolSize,
|
iceCandidatePoolSize: this.client.iceCandidatePoolSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 'connectionstatechange' would be better, but firefox doesn't implement that.
|
// 'connectionstatechange' would be better, but firefox doesn't implement that.
|
||||||
@@ -1826,7 +1813,7 @@ export function createNewMatrixCall(client: any, roomId: string, options?: CallO
|
|||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
turnServers: client.getTurnServers(),
|
turnServers: client.getTurnServers(),
|
||||||
// call level options
|
// call level options
|
||||||
forceTURN: client._forceTURN || optionsForceTURN,
|
forceTURN: client.forceTURN || optionsForceTURN,
|
||||||
};
|
};
|
||||||
const call = new MatrixCall(opts);
|
const call = new MatrixCall(opts);
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export class CallEventHandler {
|
|||||||
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
|
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
|
||||||
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||||
call = createNewMatrixCall(this.client, event.getRoomId(), {
|
call = createNewMatrixCall(this.client, event.getRoomId(), {
|
||||||
forceTURN: this.client._forceTURN,
|
forceTURN: this.client.forceTURN,
|
||||||
});
|
});
|
||||||
if (!call) {
|
if (!call) {
|
||||||
logger.log(
|
logger.log(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||||
import MatrixClient from "../client";
|
import { MatrixClient } from "../client";
|
||||||
import { RoomMember } from "../models/room-member";
|
import { RoomMember } from "../models/room-member";
|
||||||
|
|
||||||
export enum CallFeedEvent {
|
export enum CallFeedEvent {
|
||||||
|
|||||||
Reference in New Issue
Block a user