1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Convert more of js-sdk crypto and fix underscored field accesses

This commit is contained in:
Michael Telatynski
2021-06-23 14:47:25 +01:00
parent 6017fead19
commit 5a8299f1a5
27 changed files with 789 additions and 744 deletions

View File

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

View File

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

View File

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

View File

@@ -1013,7 +1013,7 @@ 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,

View File

@@ -65,7 +65,7 @@ describe("Crypto", function() {
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] = device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto._deviceList.getDeviceByIdentityKey = () => device; client.crypto.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event); encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy(); expect(encryptionInfo.encrypted).toBeTruthy();
@@ -213,7 +213,7 @@ describe("Crypto", function() {
async function keyshareEventForEvent(event, index) { async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent(); const eventContent = event.getWireContent();
const key = await aliceClient.crypto._olmDevice const key = await aliceClient.crypto.olmDevice
.getInboundGroupSessionKey( .getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id, roomId, eventContent.sender_key, eventContent.session_id,
index, index,
@@ -285,7 +285,7 @@ describe("Crypto", function() {
} }
})); }));
const bobDecryptor = bobClient.crypto._getRoomDecryptor( const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@@ -377,7 +377,7 @@ describe("Crypto", function() {
// key requests get queued until the sync has finished, but we don't // key requests get queued until the sync has finished, but we don't
// let the client set up enough for that to happen, so gut-wrench a bit // let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now. // to force it to send now.
aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests(); aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers(); jest.runAllTimers();
await Promise.resolve(); await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1); expect(aliceClient.sendToDevice).toBeCalledTimes(1);

View File

@@ -365,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",
@@ -404,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);
}; };
@@ -468,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",
@@ -508,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);
}; };
@@ -561,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: {
@@ -605,13 +605,13 @@ describe("MegolmDecryption", function() {
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: {
@@ -655,7 +655,7 @@ 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";
@@ -663,7 +663,7 @@ describe("MegolmDecryption", function() {
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: {

View File

@@ -296,7 +296,7 @@ describe("MegolmBackup", function() {
resolve(); resolve();
return Promise.resolve({}); return Promise.resolve({});
}; };
client.crypto._backupManager.backupGroupSession( client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
); );
@@ -478,7 +478,7 @@ describe("MegolmBackup", function() {
); );
} }
}; };
client.crypto._backupManager.backupGroupSession( client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(), groupSession.session_id(),
); );

View File

@@ -64,8 +64,8 @@ describe("Cross Signing", function() {
); );
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => { alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
await olmlib.verifySignature( await olmlib.verifySignature(
alice.crypto._olmDevice, keys.master_key, "@alice:example.com", alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key, "Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
); );
}); });
alice.uploadKeySignatures = async () => {}; alice.uploadKeySignatures = async () => {};
@@ -138,7 +138,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key // set Alice's cross-signing key
await resetCrossSigningKeys(alice); await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key // Alice downloads Bob's device key
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: { keys: {
master: { master: {
user_id: "@bob:example.com", user_id: "@bob:example.com",
@@ -203,12 +203,12 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = jest.fn(async (content) => { alice.uploadKeySignatures = jest.fn(async (content) => {
try { 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"],
@@ -222,7 +222,7 @@ describe("Cross Signing", function() {
}); });
}); });
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",
@@ -230,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
@@ -358,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",
@@ -387,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
@@ -421,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 () => {};
@@ -437,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",
@@ -452,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();
@@ -606,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",
@@ -629,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
@@ -673,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",
@@ -701,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
@@ -733,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",
@@ -770,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,
}); });
@@ -805,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 () => {};
@@ -838,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");
@@ -848,7 +848,7 @@ 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);
}); });

View File

@@ -8,22 +8,22 @@ export async function resetCrossSigningKeys(client, {
} = {}) { } = {}) {
const crypto = client.crypto; const crypto = client.crypto;
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys); const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
try { try {
await crypto._crossSigningInfo.resetKeys(level); await crypto.crossSigningInfo.resetKeys(level);
await crypto._signObject(crypto._crossSigningInfo.keys.master); await crypto._signObject(crypto.crossSigningInfo.keys.master);
// write a copy locally so we know these are trusted keys // write a copy locally so we know these are trusted keys
await crypto._cryptoStore.doTxn( await crypto._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
crypto._cryptoStore.storeCrossSigningKeys( crypto._cryptoStore.storeCrossSigningKeys(
txn, crypto._crossSigningInfo.keys); txn, crypto.crossSigningInfo.keys);
}, },
); );
} catch (e) { } catch (e) {
// If anything failed here, revert the keys so we know to try again from the start // If anything failed here, revert the keys so we know to try again from the start
// next time. // next time.
crypto._crossSigningInfo.keys = oldKeys; crypto.crossSigningInfo.keys = oldKeys;
throw e; throw e;
} }
crypto._baseApis.emit("crossSigning.keysChanged", {}); crypto._baseApis.emit("crossSigning.keysChanged", {});

View File

@@ -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._backupManager.checkKeyBackup = async () => {}; bob.crypto.backupManager.checkKeyBackup = async () => {};
const crossSigning = bob.crypto._crossSigningInfo; const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto._secretStorage; const secretStorage = bob.crypto.secretStorage;
// Set up cross-signing keys from scratch with specific storage key // Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({ await bob.bootstrapCrossSigning({
@@ -394,7 +394,7 @@ describe("Secrets", function() {
}); });
// Clear local cross-signing keys and read from secret storage // Clear local cross-signing keys and read from secret storage
bob.crypto._deviceList.storeCrossSigningForUser( bob.crypto.deviceList.storeCrossSigningForUser(
"@bob:example.com", "@bob:example.com",
crossSigning.toStorage(), crossSigning.toStorage(),
); );
@@ -479,7 +479,7 @@ describe("Secrets", function() {
}, },
}), }),
]); ]);
alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: { keys: {
master: { master: {
user_id: "@alice:example.com", user_id: "@alice:example.com",
@@ -619,7 +619,7 @@ describe("Secrets", function() {
}, },
}), }),
]); ]);
alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: { keys: {
master: { master: {
user_id: "@alice:example.com", user_id: "@alice:example.com",

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ import {
retryNetworkOperation, retryNetworkOperation,
} from "./http-api"; } from "./http-api";
import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto'; import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto';
import { DeviceInfo } from "./crypto/deviceinfo"; import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from './crypto/recoverykey'; import { decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromAuthData } from './crypto/key_passphrase'; import { keyFromAuthData } from './crypto/key_passphrase';
import { User } from "./models/user"; import { User } from "./models/user";
@@ -64,7 +64,7 @@ import {
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import type Request from "request"; import type Request from "request";
import { MatrixScheduler } from "./scheduler"; import { MatrixScheduler } from "./scheduler";
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo, NotificationCountType } from "./matrix"; import { ICryptoCallbacks, ISecretStorageKeyInfo, NotificationCountType } from "./matrix";
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 { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
@@ -85,7 +85,7 @@ import {
IRecoveryKey, IRecoveryKey,
ISecretStorageKey, ISecretStorageKey,
} from "./crypto/api"; } from "./crypto/api";
import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; import { CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./crypto/CrossSigning";
import { Room } from "./models/room"; import { Room } from "./models/room";
import { import {
ICreateRoomOpts, ICreateRoomOpts,
@@ -1265,7 +1265,7 @@ export class MatrixClient extends EventEmitter {
public downloadKeys( public downloadKeys(
userIds: string[], userIds: string[],
forceDownload?: boolean, forceDownload?: boolean,
): Promise<Record<string, Record<string, DeviceInfo>>> { ): Promise<Record<string, Record<string, IDevice>>> {
if (!this.crypto) { if (!this.crypto) {
return Promise.reject(new Error("End-to-end encryption disabled")); return Promise.reject(new Error("End-to-end encryption disabled"));
} }
@@ -1571,9 +1571,9 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId The ID of the user whose devices is to be checked. * @param {string} userId The ID of the user whose devices is to be checked.
* @param {string} deviceId The ID of the device to check * @param {string} deviceId The ID of the device to check
* *
* @returns {IDeviceTrustLevel} * @returns {DeviceTrustLevel}
*/ */
public checkDeviceTrust(userId: string, deviceId: string): IDeviceTrustLevel { public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
if (!this.crypto) { if (!this.crypto) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
@@ -1948,7 +1948,7 @@ export class MatrixClient extends EventEmitter {
* *
* @return {Promise<module:crypto/deviceinfo?>} * @return {Promise<module:crypto/deviceinfo?>}
*/ */
public getEventSenderDeviceInfo(event: MatrixEvent): Promise<DeviceInfo> { public async getEventSenderDeviceInfo(event: MatrixEvent): Promise<DeviceInfo> {
if (!this.crypto) { if (!this.crypto) {
return null; return null;
} }
@@ -2488,15 +2488,13 @@ export class MatrixClient extends EventEmitter {
targetRoomId: string, targetRoomId: string,
targetSessionId: string, targetSessionId: string,
backupInfo: IKeyBackupVersion, backupInfo: IKeyBackupVersion,
opts: IKeyBackupRestoreOpts, opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> { ): Promise<IKeyBackupRestoreResult> {
const privKey = await this.crypto.getSessionBackupPrivateKey(); const privKey = await this.crypto.getSessionBackupPrivateKey();
if (!privKey) { if (!privKey) {
throw new Error("Couldn't get key"); throw new Error("Couldn't get key");
} }
return this.restoreKeyBackup( return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
} }
private async restoreKeyBackup( private async restoreKeyBackup(

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 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.
@@ -20,22 +19,43 @@ limitations under the License.
* @module crypto/CrossSigning * @module crypto/CrossSigning
*/ */
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { logger } from '../logger'; import { logger } from '../logger';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes'; import { decryptAES, encryptAES } from './aes';
import { PkSigning } from "@matrix-org/olm";
import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { CryptoStore, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
function publicKeyFromKeyInfo(keyInfo) { function publicKeyFromKeyInfo(keyInfo: any): any { // TODO types
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
// We assume only a single key, and we want the bare form without type // We assume only a single key, and we want the bare form without type
// prefix, so we select the values. // prefix, so we select the values.
return Object.values(keyInfo.keys)[0]; return Object.values(keyInfo.keys)[0];
} }
interface ICacheCallbacks {
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array>;
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
}
export class CrossSigningInfo extends EventEmitter { export class CrossSigningInfo extends EventEmitter {
public keys: Record<string, any> = {}; // TODO types
public firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverified later for any reason.
private crossSigningVerifiedBefore = false;
/** /**
* Information about a user's cross-signing keys * Information about a user's cross-signing keys
* *
@@ -46,27 +66,15 @@ export class CrossSigningInfo extends EventEmitter {
* Requires getCrossSigningKey and saveCrossSigningKeys * Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache * @param {object} cacheCallbacks Callbacks used to interact with the cache
*/ */
constructor(userId, callbacks, cacheCallbacks) { constructor(
public readonly userId: string,
private callbacks: ICryptoCallbacks = {},
private cacheCallbacks: ICacheCallbacks = {},
) {
super(); super();
// you can't change the userId
Object.defineProperty(this, 'userId', {
enumerable: true,
value: userId,
});
this._callbacks = callbacks || {};
this._cacheCallbacks = cacheCallbacks || {};
this.keys = {};
this.firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverifed later for any reason.
this.crossSigningVerifiedBefore = false;
} }
static fromStorage(obj, userId) { public static fromStorage(obj: object, userId: string): CrossSigningInfo {
const res = new CrossSigningInfo(userId); const res = new CrossSigningInfo(userId);
for (const prop in obj) { for (const prop in obj) {
if (obj.hasOwnProperty(prop)) { if (obj.hasOwnProperty(prop)) {
@@ -76,7 +84,7 @@ export class CrossSigningInfo extends EventEmitter {
return res; return res;
} }
toStorage() { public toStorage(): object {
return { return {
keys: this.keys, keys: this.keys,
firstUse: this.firstUse, firstUse: this.firstUse,
@@ -92,10 +100,10 @@ export class CrossSigningInfo extends EventEmitter {
* the stored public key for the given key type. * the stored public key for the given key type.
* @returns {Array} An array with [ public key, Olm.PkSigning ] * @returns {Array} An array with [ public key, Olm.PkSigning ]
*/ */
async getCrossSigningKey(type, expectedPubkey) { public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> {
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
if (!this._callbacks.getCrossSigningKey) { if (!this.callbacks.getCrossSigningKey) {
throw new Error("No getCrossSigningKey callback supplied"); throw new Error("No getCrossSigningKey callback supplied");
} }
@@ -103,7 +111,7 @@ export class CrossSigningInfo extends EventEmitter {
expectedPubkey = this.getId(type); expectedPubkey = this.getId(type);
} }
function validateKey(key) { function validateKey(key: Uint8Array): [string, PkSigning] {
if (!key) return; if (!key) return;
const signing = new global.Olm.PkSigning(); const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(key); const gotPubkey = signing.init_with_seed(key);
@@ -114,9 +122,8 @@ export class CrossSigningInfo extends EventEmitter {
} }
let privkey; let privkey;
if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) { if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this._cacheCallbacks privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
.getCrossSigningKeyCache(type, expectedPubkey);
} }
const cacheresult = validateKey(privkey); const cacheresult = validateKey(privkey);
@@ -124,11 +131,11 @@ export class CrossSigningInfo extends EventEmitter {
return cacheresult; return cacheresult;
} }
privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey);
const result = validateKey(privkey); const result = validateKey(privkey);
if (result) { if (result) {
if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey); await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
} }
return result; return result;
} }
@@ -156,10 +163,9 @@ export class CrossSigningInfo extends EventEmitter {
* with, or null if it is not present or not encrypted with a trusted * with, or null if it is not present or not encrypted with a trusted
* key * key
*/ */
async isStoredInSecretStorage(secretStorage) { public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
// check what SSSS keys have encrypted the master key (if any) // check what SSSS keys have encrypted the master key (if any)
const stored = const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK // then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) { function intersect(s) {
for (const k of Object.keys(stored)) { for (const k of Object.keys(stored)) {
@@ -169,9 +175,7 @@ export class CrossSigningInfo extends EventEmitter {
} }
} }
for (const type of ["self_signing", "user_signing"]) { for (const type of ["self_signing", "user_signing"]) {
intersect( intersect(await secretStorage.isStored(`m.cross_signing.${type}`, false) || {});
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
);
} }
return Object.keys(stored).length ? stored : null; return Object.keys(stored).length ? stored : null;
} }
@@ -184,7 +188,10 @@ export class CrossSigningInfo extends EventEmitter {
* @param {Map} keys The keys to store * @param {Map} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data * @param {SecretStorage} secretStorage The secret store using account data
*/ */
static async storeInSecretStorage(keys, secretStorage) { public static async storeInSecretStorage(
keys: Map<string, Uint8Array>,
secretStorage: SecretStorage,
): Promise<void> {
for (const [type, privateKey] of keys) { for (const [type, privateKey] of keys) {
const encodedKey = encodeBase64(privateKey); const encodedKey = encodeBase64(privateKey);
await secretStorage.store(`m.cross_signing.${type}`, encodedKey); await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
@@ -200,7 +207,7 @@ export class CrossSigningInfo extends EventEmitter {
* @param {SecretStorage} secretStorage The secret store using account data * @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key * @return {Uint8Array} The private key
*/ */
static async getFromSecretStorage(type, secretStorage) { public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array> {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
if (!encodedKey) { if (!encodedKey) {
return null; return null;
@@ -215,8 +222,8 @@ export class CrossSigningInfo extends EventEmitter {
* "self_signing", or "user_signing". Optional, will check all by default. * "self_signing", or "user_signing". Optional, will check all by default.
* @returns {boolean} True if all keys are stored in the local cache. * @returns {boolean} True if all keys are stored in the local cache.
*/ */
async isStoredInKeyCache(type) { public async isStoredInKeyCache(type?: string): Promise<boolean> {
const cacheCallbacks = this._cacheCallbacks; const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return false; if (!cacheCallbacks) return false;
const types = type ? [type] : ["master", "self_signing", "user_signing"]; const types = type ? [type] : ["master", "self_signing", "user_signing"];
for (const t of types) { for (const t of types) {
@@ -232,9 +239,9 @@ export class CrossSigningInfo extends EventEmitter {
* *
* @returns {Map} A map from key type (string) to private key (Uint8Array) * @returns {Map} A map from key type (string) to private key (Uint8Array)
*/ */
async getCrossSigningKeysFromCache() { public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
const keys = new Map(); const keys = new Map();
const cacheCallbacks = this._cacheCallbacks; const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return keys; if (!cacheCallbacks) return keys;
for (const type of ["master", "self_signing", "user_signing"]) { for (const type of ["master", "self_signing", "user_signing"]) {
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
@@ -255,8 +262,7 @@ export class CrossSigningInfo extends EventEmitter {
* *
* @return {string} the ID * @return {string} the ID
*/ */
getId(type) { public getId(type = "master"): string {
type = type || "master";
if (!this.keys[type]) return null; if (!this.keys[type]) return null;
const keyInfo = this.keys[type]; const keyInfo = this.keys[type];
return publicKeyFromKeyInfo(keyInfo); return publicKeyFromKeyInfo(keyInfo);
@@ -269,8 +275,8 @@ export class CrossSigningInfo extends EventEmitter {
* *
* @param {CrossSigningLevel} level The key types to reset * @param {CrossSigningLevel} level The key types to reset
*/ */
async resetKeys(level) { public async resetKeys(level?: CrossSigningLevel): Promise<void> {
if (!this._callbacks.saveCrossSigningKeys) { if (!this.callbacks.saveCrossSigningKeys) {
throw new Error("No saveCrossSigningKeys callback supplied"); throw new Error("No saveCrossSigningKeys callback supplied");
} }
@@ -289,8 +295,8 @@ export class CrossSigningInfo extends EventEmitter {
return; return;
} }
const privateKeys = {}; const privateKeys: Record<string, Uint8Array> = {};
const keys = {}; const keys: Record<string, object> = {};
let masterSigning; let masterSigning;
let masterPub; let masterPub;
@@ -347,7 +353,7 @@ export class CrossSigningInfo extends EventEmitter {
} }
Object.assign(this.keys, keys); Object.assign(this.keys, keys);
this._callbacks.saveCrossSigningKeys(privateKeys); this.callbacks.saveCrossSigningKeys(privateKeys);
} finally { } finally {
if (masterSigning) { if (masterSigning) {
masterSigning.free(); masterSigning.free();
@@ -358,12 +364,12 @@ export class CrossSigningInfo extends EventEmitter {
/** /**
* unsets the keys, used when another session has reset the keys, to disable cross-signing * unsets the keys, used when another session has reset the keys, to disable cross-signing
*/ */
clearKeys() { public clearKeys(): void {
this.keys = {}; this.keys = {};
} }
setKeys(keys) { public setKeys(keys: Record<string, any>): void {
const signingKeys = {}; const signingKeys: Record<string, object> = {};
if (keys.master) { if (keys.master) {
if (keys.master.user_id !== this.userId) { if (keys.master.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id + const error = "Mismatched user ID " + keys.master.user_id +
@@ -434,7 +440,7 @@ export class CrossSigningInfo extends EventEmitter {
} }
} }
updateCrossSigningVerifiedBefore(isCrossSigningVerified) { public updateCrossSigningVerifiedBefore(isCrossSigningVerified: boolean): void {
// It is critical that this value latches forward from false to true but // It is critical that this value latches forward from false to true but
// never back to false to avoid a downgrade attack. // never back to false to avoid a downgrade attack.
if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) { if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
@@ -442,7 +448,7 @@ export class CrossSigningInfo extends EventEmitter {
} }
} }
async signObject(data, type) { public async signObject<T extends object>(data: T, type: string): Promise<T> {
if (!this.keys[type]) { if (!this.keys[type]) {
throw new Error( throw new Error(
"Attempted to sign with " + type + " key but no such key present", "Attempted to sign with " + type + " key but no such key present",
@@ -457,7 +463,7 @@ export class CrossSigningInfo extends EventEmitter {
} }
} }
async signUser(key) { public async signUser(key: CrossSigningInfo): Promise<object> {
if (!this.keys.user_signing) { if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user"); logger.info("No user signing key: not signing user");
return; return;
@@ -465,7 +471,7 @@ export class CrossSigningInfo extends EventEmitter {
return this.signObject(key.keys.master, "user_signing"); return this.signObject(key.keys.master, "user_signing");
} }
async signDevice(userId, device) { public async signDevice(userId: string, device: DeviceInfo): Promise<object> {
if (userId !== this.userId) { if (userId !== this.userId) {
throw new Error( throw new Error(
`Trying to sign ${userId}'s device; can only sign our own device`, `Trying to sign ${userId}'s device; can only sign our own device`,
@@ -492,7 +498,7 @@ export class CrossSigningInfo extends EventEmitter {
* *
* @returns {UserTrustLevel} * @returns {UserTrustLevel}
*/ */
checkUserTrust(userCrossSigning) { public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel {
// if we're checking our own key, then it's trusted if the master key // if we're checking our own key, then it's trusted if the master key
// and self-signing key match // and self-signing key match
if (this.userId === userCrossSigning.userId if (this.userId === userCrossSigning.userId
@@ -530,12 +536,17 @@ export class CrossSigningInfo extends EventEmitter {
* *
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user * @param {CrossSigningInfo} userCrossSigning Cross signing info for user
* @param {module:crypto/deviceinfo} device The device to check * @param {module:crypto/deviceinfo} device The device to check
* @param {bool} localTrust Whether the device is trusted locally * @param {boolean} localTrust Whether the device is trusted locally
* @param {bool} trustCrossSignedDevices Whether we trust cross signed devices * @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices
* *
* @returns {DeviceTrustLevel} * @returns {DeviceTrustLevel}
*/ */
checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) { public checkDeviceTrust(
userCrossSigning: CrossSigningInfo,
device: DeviceInfo,
localTrust: boolean,
trustCrossSignedDevices: boolean,
): DeviceTrustLevel {
const userTrust = this.checkUserTrust(userCrossSigning); const userTrust = this.checkUserTrust(userCrossSigning);
const userSSK = userCrossSigning.keys.self_signing; const userSSK = userCrossSigning.keys.self_signing;
@@ -552,29 +563,23 @@ export class CrossSigningInfo extends EventEmitter {
// if we can verify the user's SSK from their master key... // if we can verify the user's SSK from their master key...
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
// ...and this device's key from their SSK... // ...and this device's key from their SSK...
pkVerify( pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId,
);
// ...then we trust this device as much as far as we trust the user // ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel( return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices);
userTrust, localTrust, trustCrossSignedDevices,
);
} catch (e) { } catch (e) {
return new DeviceTrustLevel( return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
false, false, localTrust, trustCrossSignedDevices,
);
} }
} }
/** /**
* @returns {object} Cache callbacks * @returns {object} Cache callbacks
*/ */
getCacheCallbacks() { public getCacheCallbacks(): ICacheCallbacks {
return this._cacheCallbacks; return this.cacheCallbacks;
} }
} }
function deviceToObject(device, userId) { function deviceToObject(device: DeviceInfo, userId: string) {
return { return {
algorithms: device.algorithms, algorithms: device.algorithms,
keys: device.keys, keys: device.keys,
@@ -584,49 +589,49 @@ function deviceToObject(device, userId) {
}; };
} }
export const CrossSigningLevel = { export enum CrossSigningLevel {
MASTER: 4, MASTER = 4,
USER_SIGNING: 2, USER_SIGNING = 2,
SELF_SIGNING: 1, SELF_SIGNING = 1,
}; }
/** /**
* Represents the ways in which we trust a user * Represents the ways in which we trust a user
*/ */
export class UserTrustLevel { export class UserTrustLevel {
constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) { constructor(
this._crossSigningVerified = crossSigningVerified; private readonly crossSigningVerified: boolean,
this._crossSigningVerifiedBefore = crossSigningVerifiedBefore; private readonly crossSigningVerifiedBefore: boolean,
this._tofu = tofu; private readonly tofu: boolean,
} ) {}
/** /**
* @returns {bool} true if this user is verified via any means * @returns {boolean} true if this user is verified via any means
*/ */
isVerified() { public isVerified(): boolean {
return this.isCrossSigningVerified(); return this.isCrossSigningVerified();
} }
/** /**
* @returns {bool} true if this user is verified via cross signing * @returns {boolean} true if this user is verified via cross signing
*/ */
isCrossSigningVerified() { public isCrossSigningVerified(): boolean {
return this._crossSigningVerified; return this.crossSigningVerified;
} }
/** /**
* @returns {bool} true if we ever verified this user before (at least for * @returns {boolean} true if we ever verified this user before (at least for
* the history of verifications observed by this device). * the history of verifications observed by this device).
*/ */
wasCrossSigningVerified() { public wasCrossSigningVerified(): boolean {
return this._crossSigningVerifiedBefore; return this.crossSigningVerifiedBefore;
} }
/** /**
* @returns {bool} true if this user's key is trusted on first use * @returns {boolean} true if this user's key is trusted on first use
*/ */
isTofu() { public isTofu(): boolean {
return this._tofu; return this.tofu;
} }
} }
@@ -634,58 +639,62 @@ export class UserTrustLevel {
* Represents the ways in which we trust a device * Represents the ways in which we trust a device
*/ */
export class DeviceTrustLevel { export class DeviceTrustLevel {
constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) { constructor(
this._crossSigningVerified = crossSigningVerified; public readonly crossSigningVerified: boolean,
this._tofu = tofu; public readonly tofu: boolean,
this._localVerified = localVerified; private readonly localVerified: boolean,
this._trustCrossSignedDevices = trustCrossSignedDevices; private readonly trustCrossSignedDevices: boolean,
} ) {}
static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) { public static fromUserTrustLevel(
userTrustLevel: UserTrustLevel,
localVerified: boolean,
trustCrossSignedDevices: boolean,
): DeviceTrustLevel {
return new DeviceTrustLevel( return new DeviceTrustLevel(
userTrustLevel._crossSigningVerified, userTrustLevel.isCrossSigningVerified(),
userTrustLevel._tofu, userTrustLevel.isTofu(),
localVerified, localVerified,
trustCrossSignedDevices, trustCrossSignedDevices,
); );
} }
/** /**
* @returns {bool} true if this device is verified via any means * @returns {boolean} true if this device is verified via any means
*/ */
isVerified() { public isVerified(): boolean {
return Boolean(this.isLocallyVerified() || ( return Boolean(this.isLocallyVerified() || (
this._trustCrossSignedDevices && this.isCrossSigningVerified() this.trustCrossSignedDevices && this.isCrossSigningVerified()
)); ));
} }
/** /**
* @returns {bool} true if this device is verified via cross signing * @returns {boolean} true if this device is verified via cross signing
*/ */
isCrossSigningVerified() { public isCrossSigningVerified(): boolean {
return this._crossSigningVerified; return this.crossSigningVerified;
} }
/** /**
* @returns {bool} true if this device is verified locally * @returns {boolean} true if this device is verified locally
*/ */
isLocallyVerified() { public isLocallyVerified(): boolean {
return this._localVerified; return this.localVerified;
} }
/** /**
* @returns {bool} true if this device is trusted from a user's key * @returns {boolean} true if this device is trusted from a user's key
* that is trusted on first use * that is trusted on first use
*/ */
isTofu() { public isTofu(): boolean {
return this._tofu; return this.tofu;
} }
} }
export function createCryptoStoreCacheCallbacks(store, olmdevice) { export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
return { return {
getCrossSigningKeyCache: async function(type, _expectedPublicKey) { getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise<Uint8Array> {
const key = await new Promise((resolve) => { const key = await new Promise<any>((resolve) => {
return store.doTxn( return store.doTxn(
'readonly', 'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT], [IndexedDBCryptoStore.STORE_ACCOUNT],
@@ -696,20 +705,20 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
}); });
if (key && key.ciphertext) { if (key && key.ciphertext) {
const pickleKey = Buffer.from(olmdevice._pickleKey); const pickleKey = Buffer.from(olmDevice._pickleKey);
const decrypted = await decryptAES(key, pickleKey, type); const decrypted = await decryptAES(key, pickleKey, type);
return decodeBase64(decrypted); return decodeBase64(decrypted);
} else { } else {
return key; return key;
} }
}, },
storeCrossSigningKeyCache: async function(type, key) { storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise<void> {
if (!(key instanceof Uint8Array)) { if (!(key instanceof Uint8Array)) {
throw new Error( throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`, `storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
); );
} }
const pickleKey = Buffer.from(olmdevice._pickleKey); const pickleKey = Buffer.from(olmDevice._pickleKey);
key = await encryptAES(encodeBase64(key), pickleKey, type); key = await encryptAES(encodeBase64(key), pickleKey, type);
return store.doTxn( return store.doTxn(
'readwrite', 'readwrite',
@@ -729,7 +738,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
* @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
*/ */
export async function requestKeysDuringVerification(baseApis, userId, deviceId) { export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) {
// If this is a self-verification, ask the other party for keys // If this is a self-verification, ask the other party for keys
if (baseApis.getUserId() !== userId) { if (baseApis.getUserId() !== userId) {
return; return;
@@ -739,7 +748,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
@@ -748,8 +757,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
const crossSigning = new CrossSigningInfo( const crossSigning = new CrossSigningInfo(
original.userId, original.userId,
{ getCrossSigningKey: async (type) => { { getCrossSigningKey: async (type) => {
logger.debug("Cross-signing: requesting secret", logger.debug("Cross-signing: requesting secret", type, deviceId);
type, deviceId);
const { promise } = client.requestSecret( const { promise } = client.requestSecret(
`m.cross_signing.${type}`, [deviceId], `m.cross_signing.${type}`, [deviceId],
); );
@@ -757,7 +765,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
const decoded = decodeBase64(result); const decoded = decodeBase64(result);
return Uint8Array.from(decoded); return Uint8Array.from(decoded);
} }, } },
original._cacheCallbacks, original.getCacheCallbacks(),
); );
crossSigning.keys = original.keys; crossSigning.keys = original.keys;
@@ -774,7 +782,8 @@ 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 => { // eslint-disable-next-line no-async-promise-executor
const backupKeyPromise = new Promise<void>(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...");
@@ -791,9 +800,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
logger.info("Backup key stored. Starting backup restore..."); logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion(); const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg // no need to await for this - just let it go in the bg
client.restoreKeyBackupWithCache( client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => {
undefined, undefined, backupInfo,
).then(() => {
logger.info("Backup restored."); logger.info("Backup restored.");
}); });
} }

View File

@@ -1,7 +1,5 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 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.
@@ -23,12 +21,15 @@ limitations under the License.
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { logger } from '../logger'; import { logger } from '../logger';
import { DeviceInfo } from './deviceinfo'; import { DeviceInfo, IDevice } from './deviceinfo';
import { CrossSigningInfo } from './CrossSigning'; import { CrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib'; import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, sleep } from '../utils'; import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { MatrixClient, CryptoStore } from "../client";
import { OlmDevice } from "./OlmDevice";
/* State transition diagram for DeviceList._deviceTrackingStatus /* State transition diagram for DeviceList._deviceTrackingStatus
* *
@@ -51,91 +52,96 @@ import { chunkPromises, defer, sleep } from '../utils';
*/ */
// constants for DeviceList._deviceTrackingStatus // constants for DeviceList._deviceTrackingStatus
const TRACKING_STATUS_NOT_TRACKED = 0; enum TrackingStatus {
const TRACKING_STATUS_PENDING_DOWNLOAD = 1; NotTracked,
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2; PendingDownload,
const TRACKING_STATUS_UP_TO_DATE = 3; DownloadInProgress,
UpToDate,
}
type DeviceInfoMap = Record<string, Record<string, IDevice>>;
/** /**
* @alias module:crypto/DeviceList * @alias module:crypto/DeviceList
*/ */
export class DeviceList extends EventEmitter { export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
super();
this._cryptoStore = cryptoStore;
// userId -> { // userId -> {
// deviceId -> { // deviceId -> {
// [device info] // [device info]
// } // }
// } // }
this._devices = {}; private devices: DeviceInfoMap = {};
// userId -> { // userId -> {
// [key info] // [key info]
// } // }
this._crossSigningInfo = {}; public crossSigningInfo: Record<string, object> = {};
// map of identity keys to the user who owns it // map of identity keys to the user who owns it
this._userByIdentityKey = {}; private userByIdentityKey: Record<string, string> = {};
// which users we are tracking device status for. // which users we are tracking device status for.
// userId -> TRACKING_STATUS_* // userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = {}; // loaded from storage in load() private deviceTrackingStatus: Record<string, TrackingStatus> = {}; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was writen, // The 'next_batch' sync token at the point the data was written,
// ie. a token representing the point immediately after the // ie. a token representing the point immediately after the
// moment represented by the snapshot in the db. // moment represented by the snapshot in the db.
this._syncToken = null; private syncToken: string = null;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, olmDevice, this,
);
// userId -> promise // userId -> promise
this._keyDownloadsInProgressByUser = {}; private keyDownloadsInProgressByUser: Record<string, Promise<void>> = {};
// Maximum number of user IDs per request to prevent server overload (#1619)
this._keyDownloadChunkSize = keyDownloadChunkSize;
// Set whenever changes are made other than setting the sync token // Set whenever changes are made other than setting the sync token
this._dirty = false; private dirty = false;
// Promise resolved when device data is saved // Promise resolved when device data is saved
this._savePromise = null; private savePromise: Promise<boolean> = null;
// Function that resolves the save promise // Function that resolves the save promise
this._resolveSavePromise = null; private resolveSavePromise: (saved: boolean) => void = null;
// The time the save is scheduled for // The time the save is scheduled for
this._savePromiseTime = null; private savePromiseTime: number = null;
// The timer used to delay the save // The timer used to delay the save
this._saveTimer = null; private saveTimer: NodeJS.Timeout = null;
// True if we have fetched data from the server or loaded a non-empty // True if we have fetched data from the server or loaded a non-empty
// set of device data from the store // set of device data from the store
this._hasFetched = null; private hasFetched: boolean = null;
private readonly serialiser: DeviceListUpdateSerialiser;
constructor(
baseApis: MatrixClient,
private readonly cryptoStore: CryptoStore,
olmDevice: OlmDevice,
// Maximum number of user IDs per request to prevent server overload (#1619)
public readonly keyDownloadChunkSize = 250,
) {
super();
this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this);
} }
/** /**
* Load the device tracking state from storage * Load the device tracking state from storage
*/ */
async load() { public async load() {
await this._cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { 'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._hasFetched = Boolean(deviceData && deviceData.devices); this.hasFetched = Boolean(deviceData && deviceData.devices);
this._devices = deviceData ? deviceData.devices : {}, this.devices = deviceData ? deviceData.devices : {},
this._crossSigningInfo = deviceData ? this.crossSigningInfo = deviceData ?
deviceData.crossSigningInfo || {} : {}; deviceData.crossSigningInfo || {} : {};
this._deviceTrackingStatus = deviceData ? this.deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {}; deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null; this.syncToken = deviceData ? deviceData.syncToken : null;
this._userByIdentityKey = {}; this.userByIdentityKey = {};
for (const user of Object.keys(this._devices)) { for (const user of Object.keys(this.devices)) {
const userDevices = this._devices[user]; const userDevices = this.devices[user];
for (const device of Object.keys(userDevices)) { for (const device of Object.keys(userDevices)) {
const idKey = userDevices[device].keys['curve25519:'+device]; const idKey = userDevices[device].keys['curve25519:'+device];
if (idKey !== undefined) { if (idKey !== undefined) {
this._userByIdentityKey[idKey] = user; this.userByIdentityKey[idKey] = user;
} }
} }
} }
@@ -143,17 +149,17 @@ export class DeviceList extends EventEmitter {
}, },
); );
for (const u of Object.keys(this._deviceTrackingStatus)) { for (const u of Object.keys(this.deviceTrackingStatus)) {
// if a download was in progress when we got shut down, it isn't any more. // if a download was in progress when we got shut down, it isn't any more.
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { if (this.deviceTrackingStatus[u] == TrackingStatus.DownloadInProgress) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
} }
} }
} }
stop() { public stop() {
if (this._saveTimer !== null) { if (this.saveTimer !== null) {
clearTimeout(this._saveTimer); clearTimeout(this.saveTimer);
} }
} }
@@ -164,74 +170,73 @@ export class DeviceList extends EventEmitter {
* The actual save will be delayed by a short amount of time to * The actual save will be delayed by a short amount of time to
* aggregate multiple writes to the database. * aggregate multiple writes to the database.
* *
* @param {integer} delay Time in ms before which the save actually happens. * @param {number} delay Time in ms before which the save actually happens.
* By default, the save is delayed for a short period in order to batch * By default, the save is delayed for a short period in order to batch
* multiple writes, but this behaviour can be disabled by passing 0. * multiple writes, but this behaviour can be disabled by passing 0.
* *
* @return {Promise<bool>} true if the data was saved, false if * @return {Promise<boolean>} true if the data was saved, false if
* it was not (eg. because no changes were pending). The promise * it was not (eg. because no changes were pending). The promise
* will only resolve once the data is saved, so may take some time * will only resolve once the data is saved, so may take some time
* to resolve. * to resolve.
*/ */
async saveIfDirty(delay) { public async saveIfDirty(delay = 500): Promise<boolean> {
if (!this._dirty) return Promise.resolve(false); if (!this.dirty) return Promise.resolve(false);
// Delay saves for a bit so we can aggregate multiple saves that happen // Delay saves for a bit so we can aggregate multiple saves that happen
// in quick succession (eg. when a whole room's devices are marked as known) // in quick succession (eg. when a whole room's devices are marked as known)
if (delay === undefined) delay = 500;
const targetTime = Date.now + delay; const targetTime = Date.now + delay;
if (this._savePromiseTime && targetTime < this._savePromiseTime) { if (this.savePromiseTime && targetTime < this.savePromiseTime) {
// There's a save scheduled but for after we would like: cancel // There's a save scheduled but for after we would like: cancel
// it & schedule one for the time we want // it & schedule one for the time we want
clearTimeout(this._saveTimer); clearTimeout(this.saveTimer);
this._saveTimer = null; this.saveTimer = null;
this._savePromiseTime = null; this.savePromiseTime = null;
// (but keep the save promise since whatever called save before // (but keep the save promise since whatever called save before
// will still want to know when the save is done) // will still want to know when the save is done)
} }
let savePromise = this._savePromise; let savePromise = this.savePromise;
if (savePromise === null) { if (savePromise === null) {
savePromise = new Promise((resolve, reject) => { savePromise = new Promise((resolve, reject) => {
this._resolveSavePromise = resolve; this.resolveSavePromise = resolve;
}); });
this._savePromise = savePromise; this.savePromise = savePromise;
} }
if (this._saveTimer === null) { if (this.saveTimer === null) {
const resolveSavePromise = this._resolveSavePromise; const resolveSavePromise = this.resolveSavePromise;
this._savePromiseTime = targetTime; this.savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => { this.saveTimer = setTimeout(() => {
logger.log('Saving device tracking data', this._syncToken); logger.log('Saving device tracking data', this.syncToken);
// null out savePromise now (after the delay but before the write), // null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has // otherwise we could return the existing promise when the save has
// actually already happened. // actually already happened.
this._savePromiseTime = null; this.savePromiseTime = null;
this._saveTimer = null; this.saveTimer = null;
this._savePromise = null; this.savePromise = null;
this._resolveSavePromise = null; this.resolveSavePromise = null;
this._cryptoStore.doTxn( this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({ this.cryptoStore.storeEndToEndDeviceData({
devices: this._devices, devices: this.devices,
crossSigningInfo: this._crossSigningInfo, crossSigningInfo: this.crossSigningInfo,
trackingStatus: this._deviceTrackingStatus, trackingStatus: this.deviceTrackingStatus,
syncToken: this._syncToken, syncToken: this.syncToken,
}, txn); }, txn);
}, },
).then(() => { ).then(() => {
// The device list is considered dirty until the write // The device list is considered dirty until the write completes.
// completes. this.dirty = false;
this._dirty = false; resolveSavePromise(true);
resolveSavePromise();
}, err => { }, err => {
logger.error('Failed to save device tracking data', this._syncToken); logger.error('Failed to save device tracking data', this.syncToken);
logger.error(err); logger.error(err);
}); });
}, delay); }, delay);
} }
return savePromise; return savePromise;
} }
@@ -240,8 +245,8 @@ export class DeviceList extends EventEmitter {
* *
* @return {string} The sync token * @return {string} The sync token
*/ */
getSyncToken() { public getSyncToken(): string {
return this._syncToken; return this.syncToken;
} }
/** /**
@@ -254,8 +259,8 @@ export class DeviceList extends EventEmitter {
* *
* @param {string} st The sync token * @param {string} st The sync token
*/ */
setSyncToken(st) { public setSyncToken(st: string): void {
this._syncToken = st; this.syncToken = st;
} }
/** /**
@@ -263,33 +268,33 @@ export class DeviceList extends EventEmitter {
* downloading and storing them if they're not (or if forceDownload is * downloading and storing them if they're not (or if forceDownload is
* true). * true).
* @param {Array} userIds The users to fetch. * @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached. * @param {boolean} forceDownload Always download the keys even if cached.
* *
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link * @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto/deviceinfo|DeviceInfo}. * module:crypto/deviceinfo|DeviceInfo}.
*/ */
downloadKeys(userIds, forceDownload) { public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
const usersToDownload = []; const usersToDownload = [];
const promises = []; const promises = [];
userIds.forEach((u) => { userIds.forEach((u) => {
const trackingStatus = this._deviceTrackingStatus[u]; const trackingStatus = this.deviceTrackingStatus[u];
if (this._keyDownloadsInProgressByUser[u]) { if (this.keyDownloadsInProgressByUser[u]) {
// already a key download in progress/queued for this user; its results // already a key download in progress/queued for this user; its results
// will be good enough for us. // will be good enough for us.
logger.log( logger.log(
`downloadKeys: already have a download in progress for ` + `downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`, `${u}: awaiting its result`,
); );
promises.push(this._keyDownloadsInProgressByUser[u]); promises.push(this.keyDownloadsInProgressByUser[u]);
} else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) { } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) {
usersToDownload.push(u); usersToDownload.push(u);
} }
}); });
if (usersToDownload.length != 0) { if (usersToDownload.length != 0) {
logger.log("downloadKeys: downloading for", usersToDownload); logger.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload); const downloadPromise = this.doKeyDownload(usersToDownload);
promises.push(downloadPromise); promises.push(downloadPromise);
} }
@@ -298,7 +303,7 @@ export class DeviceList extends EventEmitter {
} }
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
return this._getDevicesFromStore(userIds); return this.getDevicesFromStore(userIds);
}); });
} }
@@ -309,12 +314,11 @@ export class DeviceList extends EventEmitter {
* *
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}. * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
*/ */
_getDevicesFromStore(userIds) { private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
const stored = {}; const stored = {};
const self = this; userIds.map((u) => {
userIds.map(function(u) {
stored[u] = {}; stored[u] = {};
const devices = self.getStoredDevicesForUser(u) || []; const devices = this.getStoredDevicesForUser(u) || [];
devices.map(function(dev) { devices.map(function(dev) {
stored[u][dev.deviceId] = dev; stored[u][dev.deviceId] = dev;
}); });
@@ -327,8 +331,8 @@ export class DeviceList extends EventEmitter {
* *
* @return {array} All known user IDs * @return {array} All known user IDs
*/ */
getKnownUserIds() { public getKnownUserIds(): string[] {
return Object.keys(this._devices); return Object.keys(this.devices);
} }
/** /**
@@ -339,8 +343,8 @@ export class DeviceList extends EventEmitter {
* @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
* managed to get a list of devices for this user yet. * managed to get a list of devices for this user yet.
*/ */
getStoredDevicesForUser(userId) { public getStoredDevicesForUser(userId: string): DeviceInfo[] | null {
const devs = this._devices[userId]; const devs = this.devices[userId];
if (!devs) { if (!devs) {
return null; return null;
} }
@@ -361,19 +365,19 @@ export class DeviceList extends EventEmitter {
* @return {Object} deviceId->{object} devices, or undefined if * @return {Object} deviceId->{object} devices, or undefined if
* there is no data for this user. * there is no data for this user.
*/ */
getRawStoredDevicesForUser(userId) { public getRawStoredDevicesForUser(userId: string): Record<string, IDevice> {
return this._devices[userId]; return this.devices[userId];
} }
getStoredCrossSigningForUser(userId) { public getStoredCrossSigningForUser(userId: string): CrossSigningInfo {
if (!this._crossSigningInfo[userId]) return null; if (!this.crossSigningInfo[userId]) return null;
return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId); return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
} }
storeCrossSigningForUser(userId, info) { public storeCrossSigningForUser(userId: string, info: CrossSigningInfo): void {
this._crossSigningInfo[userId] = info; this.crossSigningInfo[userId] = info;
this._dirty = true; this.dirty = true;
} }
/** /**
@@ -385,8 +389,8 @@ export class DeviceList extends EventEmitter {
* @return {module:crypto/deviceinfo?} device, or undefined * @return {module:crypto/deviceinfo?} device, or undefined
* if we don't know about this device * if we don't know about this device
*/ */
getStoredDevice(userId, deviceId) { public getStoredDevice(userId: string, deviceId: string): DeviceInfo {
const devs = this._devices[userId]; const devs = this.devices[userId];
if (!devs || !devs[deviceId]) { if (!devs || !devs[deviceId]) {
return undefined; return undefined;
} }
@@ -401,7 +405,7 @@ export class DeviceList extends EventEmitter {
* *
* @return {string} user ID * @return {string} user ID
*/ */
getUserByIdentityKey(algorithm, senderKey) { public getUserByIdentityKey(algorithm: string, senderKey: string): string {
if ( if (
algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM algorithm !== olmlib.MEGOLM_ALGORITHM
@@ -410,7 +414,7 @@ export class DeviceList extends EventEmitter {
return null; return null;
} }
return this._userByIdentityKey[senderKey]; return this.userByIdentityKey[senderKey];
} }
/** /**
@@ -421,13 +425,13 @@ export class DeviceList extends EventEmitter {
* *
* @return {module:crypto/deviceinfo?} * @return {module:crypto/deviceinfo?}
*/ */
getDeviceByIdentityKey(algorithm, senderKey) { public getDeviceByIdentityKey(algorithm: string, senderKey: string): DeviceInfo | null {
const userId = this.getUserByIdentityKey(algorithm, senderKey); const userId = this.getUserByIdentityKey(algorithm, senderKey);
if (!userId) { if (!userId) {
return null; return null;
} }
const devices = this._devices[userId]; const devices = this.devices[userId];
if (!devices) { if (!devices) {
return null; return null;
} }
@@ -462,25 +466,25 @@ export class DeviceList extends EventEmitter {
* @param {string} u The user ID * @param {string} u The user ID
* @param {Object} devs New device info for user * @param {Object} devs New device info for user
*/ */
storeDevicesForUser(u, devs) { public storeDevicesForUser(u: string, devs: Record<string, IDevice>): void {
// remove previous devices from _userByIdentityKey // remove previous devices from userByIdentityKey
if (this._devices[u] !== undefined) { if (this.devices[u] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[u])) { for (const [deviceId, dev] of Object.entries(this.devices[u])) {
const identityKey = dev.keys['curve25519:'+deviceId]; const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey]; delete this.userByIdentityKey[identityKey];
} }
} }
this._devices[u] = devs; this.devices[u] = devs;
// add new ones // add new ones
for (const [deviceId, dev] of Object.entries(devs)) { for (const [deviceId, dev] of Object.entries(devs)) {
const identityKey = dev.keys['curve25519:'+deviceId]; const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = u; this.userByIdentityKey[identityKey] = u;
} }
this._dirty = true; this.dirty = true;
} }
/** /**
@@ -492,7 +496,7 @@ export class DeviceList extends EventEmitter {
* *
* @param {String} userId * @param {String} userId
*/ */
startTrackingDeviceList(userId) { public startTrackingDeviceList(userId: string): void {
// sanity-check the userId. This is mostly paranoia, but if synapse // sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole // can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because // request and we can never update the device lists again (because
@@ -503,12 +507,12 @@ export class DeviceList extends EventEmitter {
if (typeof userId !== 'string') { if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId); throw new Error('userId must be a string; was '+userId);
} }
if (!this._deviceTrackingStatus[userId]) { if (!this.deviceTrackingStatus[userId]) {
logger.log('Now tracking device list for ' + userId); logger.log('Now tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload;
// we don't yet persist the tracking status, since there may be a lot // we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done // of calls; we save all data together once the sync is done
this._dirty = true; this.dirty = true;
} }
} }
@@ -521,14 +525,14 @@ export class DeviceList extends EventEmitter {
* *
* @param {String} userId * @param {String} userId
*/ */
stopTrackingDeviceList(userId) { public stopTrackingDeviceList(userId: string): void {
if (this._deviceTrackingStatus[userId]) { if (this.deviceTrackingStatus[userId]) {
logger.log('No longer tracking device list for ' + userId); logger.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked;
// we don't yet persist the tracking status, since there may be a lot // we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done // of calls; we save all data together once the sync is done
this._dirty = true; this.dirty = true;
} }
} }
@@ -538,11 +542,11 @@ export class DeviceList extends EventEmitter {
* This will flag each user whose devices we are tracking as in need of an * This will flag each user whose devices we are tracking as in need of an
* update. * update.
*/ */
stopTrackingAllDeviceLists() { public stopTrackingAllDeviceLists(): void {
for (const userId of Object.keys(this._deviceTrackingStatus)) { for (const userId of Object.keys(this.deviceTrackingStatus)) {
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked;
} }
this._dirty = true; this.dirty = true;
} }
/** /**
@@ -556,14 +560,14 @@ export class DeviceList extends EventEmitter {
* *
* @param {String} userId * @param {String} userId
*/ */
invalidateUserDeviceList(userId) { public invalidateUserDeviceList(userId: string): void {
if (this._deviceTrackingStatus[userId]) { if (this.deviceTrackingStatus[userId]) {
logger.log("Marking device list outdated for", userId); logger.log("Marking device list outdated for", userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload;
// we don't yet persist the tracking status, since there may be a lot // we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done // of calls; we save all data together once the sync is done
this._dirty = true; this.dirty = true;
} }
} }
@@ -573,18 +577,18 @@ export class DeviceList extends EventEmitter {
* @returns {Promise} which completes when the download completes; normally there * @returns {Promise} which completes when the download completes; normally there
* is no need to wait for this (it's mostly for the unit tests). * is no need to wait for this (it's mostly for the unit tests).
*/ */
refreshOutdatedDeviceLists() { public refreshOutdatedDeviceLists(): Promise<void> {
this.saveIfDirty(); this.saveIfDirty();
const usersToDownload = []; const usersToDownload = [];
for (const userId of Object.keys(this._deviceTrackingStatus)) { for (const userId of Object.keys(this.deviceTrackingStatus)) {
const stat = this._deviceTrackingStatus[userId]; const stat = this.deviceTrackingStatus[userId];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { if (stat == TrackingStatus.PendingDownload) {
usersToDownload.push(userId); usersToDownload.push(userId);
} }
} }
return this._doKeyDownload(usersToDownload); return this.doKeyDownload(usersToDownload);
} }
/** /**
@@ -595,34 +599,34 @@ export class DeviceList extends EventEmitter {
* *
* @param {Object} devices deviceId->{object} the new devices * @param {Object} devices deviceId->{object} the new devices
*/ */
_setRawStoredDevicesForUser(userId, devices) { public setRawStoredDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
// remove old devices from _userByIdentityKey // remove old devices from userByIdentityKey
if (this._devices[userId] !== undefined) { if (this.devices[userId] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[userId])) { for (const [deviceId, dev] of Object.entries(this.devices[userId])) {
const identityKey = dev.keys['curve25519:'+deviceId]; const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey]; delete this.userByIdentityKey[identityKey];
} }
} }
this._devices[userId] = devices; this.devices[userId] = devices;
// add new devices into _userByIdentityKey // add new devices into userByIdentityKey
for (const [deviceId, dev] of Object.entries(devices)) { for (const [deviceId, dev] of Object.entries(devices)) {
const identityKey = dev.keys['curve25519:'+deviceId]; const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = userId; this.userByIdentityKey[identityKey] = userId;
} }
} }
setRawStoredCrossSigningForUser(userId, info) { public setRawStoredCrossSigningForUser(userId: string, info: object): void {
this._crossSigningInfo[userId] = info; this.crossSigningInfo[userId] = info;
} }
/** /**
* Fire off download update requests for the given users, and update the * Fire off download update requests for the given users, and update the
* device list tracking status for them, and the * device list tracking status for them, and the
* _keyDownloadsInProgressByUser map for them. * keyDownloadsInProgressByUser map for them.
* *
* @param {String[]} users list of userIds * @param {String[]} users list of userIds
* *
@@ -630,15 +634,13 @@ export class DeviceList extends EventEmitter {
* been updated. rejects if there was a problem updating any of the * been updated. rejects if there was a problem updating any of the
* users. * users.
*/ */
_doKeyDownload(users) { private doKeyDownload(users: string[]): Promise<void> {
if (users.length === 0) { if (users.length === 0) {
// nothing to do // nothing to do
return Promise.resolve(); return Promise.resolve();
} }
const prom = this._serialiser.updateDevicesForUsers( const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => {
users, this._syncToken,
).then(() => {
finished(true); finished(true);
}, (e) => { }, (e) => {
logger.error( logger.error(
@@ -649,42 +651,41 @@ export class DeviceList extends EventEmitter {
}); });
users.forEach((u) => { users.forEach((u) => {
this._keyDownloadsInProgressByUser[u] = prom; this.keyDownloadsInProgressByUser[u] = prom;
const stat = this._deviceTrackingStatus[u]; const stat = this.deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { if (stat == TrackingStatus.PendingDownload) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS; this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress;
} }
}); });
const finished = (success) => { const finished = (success) => {
this.emit("crypto.willUpdateDevices", users, !this._hasFetched); this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
users.forEach((u) => { users.forEach((u) => {
this._dirty = true; this.dirty = true;
// we may have queued up another download request for this user // we may have queued up another download request for this user
// since we started this request. If that happens, we should // since we started this request. If that happens, we should
// ignore the completion of the first one. // ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) { if (this.keyDownloadsInProgressByUser[u] !== prom) {
logger.log('Another update in the queue for', u, logger.log('Another update in the queue for', u, '- not marking up-to-date');
'- not marking up-to-date');
return; return;
} }
delete this._keyDownloadsInProgressByUser[u]; delete this.keyDownloadsInProgressByUser[u];
const stat = this._deviceTrackingStatus[u]; const stat = this.deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { if (stat == TrackingStatus.DownloadInProgress) {
if (success) { if (success) {
// we didn't get any new invalidations since this download started: // we didn't get any new invalidations since this download started:
// this user's device list is now up to date. // this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE; this.deviceTrackingStatus[u] = TrackingStatus.UpToDate;
logger.log("Device list for", u, "now up to date"); logger.log("Device list for", u, "now up to date");
} else { } else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
} }
} }
}); });
this.saveIfDirty(); this.saveIfDirty();
this.emit("crypto.devicesUpdated", users, !this._hasFetched); this.emit("crypto.devicesUpdated", users, !this.hasFetched);
this._hasFetched = true; this.hasFetched = true;
}; };
return prom; return prom;
@@ -701,29 +702,28 @@ export class DeviceList extends EventEmitter {
* time (and queuing other requests up). * time (and queuing other requests up).
*/ */
class DeviceListUpdateSerialiser { class DeviceListUpdateSerialiser {
/* private downloadInProgress = false;
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object
*/
constructor(baseApis, olmDevice, deviceList) {
this._baseApis = baseApis;
this._olmDevice = olmDevice;
this._deviceList = deviceList; // the device list to be updated
this._downloadInProgress = false;
// users which are queued for download // users which are queued for download
// userId -> true // userId -> true
this._keyDownloadsQueuedByUser = {}; private keyDownloadsQueuedByUser: Record<string, boolean> = {};
// deferred which is resolved when the queued users are downloaded. // deferred which is resolved when the queued users are downloaded.
//
// non-null indicates that we have users queued for download. // non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null; private queuedQueryDeferred: IDeferred<void> = null;
this._syncToken = null; // The sync token we send with the requests private syncToken: string = null; // The sync token we send with the requests
}
/*
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object, the device list to be updated
*/
constructor(
private readonly baseApis: MatrixClient,
private readonly olmDevice: OlmDevice,
private readonly deviceList: DeviceList,
) {}
/** /**
* Make a key query request for the given users * Make a key query request for the given users
@@ -737,57 +737,57 @@ class DeviceListUpdateSerialiser {
* been updated. rejects if there was a problem updating any of the * been updated. rejects if there was a problem updating any of the
* users. * users.
*/ */
updateDevicesForUsers(users, syncToken) { public updateDevicesForUsers(users: string[], syncToken: string): Promise<void> {
users.forEach((u) => { users.forEach((u) => {
this._keyDownloadsQueuedByUser[u] = true; this.keyDownloadsQueuedByUser[u] = true;
}); });
if (!this._queuedQueryDeferred) { if (!this.queuedQueryDeferred) {
this._queuedQueryDeferred = defer(); this.queuedQueryDeferred = defer();
} }
// We always take the new sync token and just use the latest one we've // We always take the new sync token and just use the latest one we've
// been given, since it just needs to be at least as recent as the // been given, since it just needs to be at least as recent as the
// sync response the device invalidation message arrived in // sync response the device invalidation message arrived in
this._syncToken = syncToken; this.syncToken = syncToken;
if (this._downloadInProgress) { if (this.downloadInProgress) {
// just queue up these users // just queue up these users
logger.log('Queued key download for', users); logger.log('Queued key download for', users);
return this._queuedQueryDeferred.promise; return this.queuedQueryDeferred.promise;
} }
// start a new download. // start a new download.
return this._doQueuedQueries(); return this.doQueuedQueries();
} }
_doQueuedQueries() { private doQueuedQueries(): Promise<void> {
if (this._downloadInProgress) { if (this.downloadInProgress) {
throw new Error( throw new Error(
"DeviceListUpdateSerialiser._doQueuedQueries called with request active", "DeviceListUpdateSerialiser.doQueuedQueries called with request active",
); );
} }
const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser); const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser);
this._keyDownloadsQueuedByUser = {}; this.keyDownloadsQueuedByUser = {};
const deferred = this._queuedQueryDeferred; const deferred = this.queuedQueryDeferred;
this._queuedQueryDeferred = null; this.queuedQueryDeferred = null;
logger.log('Starting key download for', downloadUsers); logger.log('Starting key download for', downloadUsers);
this._downloadInProgress = true; this.downloadInProgress = true;
const opts = {}; const opts: Parameters<MatrixClient["downloadKeysForUsers"]>[1] = {};
if (this._syncToken) { if (this.syncToken) {
opts.token = this._syncToken; opts.token = this.syncToken;
} }
const factories = []; const factories = [];
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) { for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize); const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts)); factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
} }
chunkPromises(factories, 3).then(async (responses) => { chunkPromises(factories, 3).then(async (responses: any[]) => {
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {}))); const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {}))); const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {}))); const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
@@ -802,7 +802,7 @@ class DeviceListUpdateSerialiser {
for (const userId of downloadUsers) { for (const userId of downloadUsers) {
await sleep(5); await sleep(5);
try { try {
await this._processQueryResponseForUser( await this.processQueryResponseForUser(
userId, dk[userId], { userId, dk[userId], {
master: masterKeys[userId], master: masterKeys[userId],
self_signing: ssks[userId], self_signing: ssks[userId],
@@ -818,32 +818,34 @@ class DeviceListUpdateSerialiser {
}).then(() => { }).then(() => {
logger.log('Completed key download for ' + downloadUsers); logger.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false; this.downloadInProgress = false;
deferred.resolve(); deferred.resolve();
// if we have queued users, fire off another request. // if we have queued users, fire off another request.
if (this._queuedQueryDeferred) { if (this.queuedQueryDeferred) {
this._doQueuedQueries(); this.doQueuedQueries();
} }
}, (e) => { }, (e) => {
logger.warn('Error downloading keys for ' + downloadUsers + ':', e); logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgress = false; this.downloadInProgress = false;
deferred.reject(e); deferred.reject(e);
}); });
return deferred.promise; return deferred.promise;
} }
async _processQueryResponseForUser( private async processQueryResponseForUser(
userId, dkResponse, crossSigningResponse, userId: string,
) { dkResponse: object,
crossSigningResponse: any, // TODO types
): Promise<void> {
logger.log('got device keys for ' + userId + ':', dkResponse); logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
{ {
// map from deviceid -> deviceinfo for this user // map from deviceid -> deviceinfo for this user
const userStore = {}; const userStore: Record<string, DeviceInfo> = {};
const devs = this._deviceList.getRawStoredDevicesForUser(userId); const devs = this.deviceList.getRawStoredDevicesForUser(userId);
if (devs) { if (devs) {
Object.keys(devs).forEach((deviceId) => { Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
@@ -851,9 +853,9 @@ class DeviceListUpdateSerialiser {
}); });
} }
await _updateStoredDeviceKeysForUser( await updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, dkResponse || {}, this.olmDevice, userId, userStore, dkResponse || {},
this._baseApis.getUserId(), this._baseApis.deviceId, this.baseApis.getUserId(), this.baseApis.deviceId,
); );
// put the updates into the object that will be returned as our results // put the updates into the object that will be returned as our results
@@ -862,7 +864,7 @@ class DeviceListUpdateSerialiser {
storage[deviceId] = userStore[deviceId].toStorage(); storage[deviceId] = userStore[deviceId].toStorage();
}); });
this._deviceList._setRawStoredDevicesForUser(userId, storage); this.deviceList.setRawStoredDevicesForUser(userId, storage);
} }
// now do the same for the cross-signing keys // now do the same for the cross-signing keys
@@ -873,26 +875,31 @@ class DeviceListUpdateSerialiser {
&& (crossSigningResponse.master || crossSigningResponse.self_signing && (crossSigningResponse.master || crossSigningResponse.self_signing
|| crossSigningResponse.user_signing)) { || crossSigningResponse.user_signing)) {
const crossSigning const crossSigning
= this._deviceList.getStoredCrossSigningForUser(userId) = this.deviceList.getStoredCrossSigningForUser(userId)
|| new CrossSigningInfo(userId); || new CrossSigningInfo(userId);
crossSigning.setKeys(crossSigningResponse); crossSigning.setKeys(crossSigningResponse);
this._deviceList.setRawStoredCrossSigningForUser( this.deviceList.setRawStoredCrossSigningForUser(
userId, crossSigning.toStorage(), userId, crossSigning.toStorage(),
); );
// NB. Unlike most events in the js-sdk, this one is internal to the // NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted // js-sdk and is not re-emitted
this._deviceList.emit('userCrossSigningUpdated', userId); this.deviceList.emit('userCrossSigningUpdated', userId);
} }
} }
} }
} }
async function _updateStoredDeviceKeysForUser( async function updateStoredDeviceKeysForUser(
_olmDevice, userId, userStore, userResult, localUserId, localDeviceId, olmDevice: OlmDevice,
) { userId: string,
userStore: Record<string, DeviceInfo>,
userResult: object,
localUserId: string,
localDeviceId: string,
): Promise<boolean> {
let updated = false; let updated = false;
// remove any devices in the store which aren't in the response // remove any devices in the store which aren't in the response
@@ -936,7 +943,7 @@ async function _updateStoredDeviceKeysForUser(
continue; continue;
} }
if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) { if (await storeDeviceKeys(olmDevice, userStore, deviceResult)) {
updated = true; updated = true;
} }
} }
@@ -949,7 +956,11 @@ async function _updateStoredDeviceKeysForUser(
* *
* returns (a promise for) true if a change was made, else false * returns (a promise for) true if a change was made, else false
*/ */
async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { async function storeDeviceKeys(
olmDevice: OlmDevice,
userStore: Record<string, DeviceInfo>,
deviceResult: any, // TODO types
): Promise<boolean> {
if (!deviceResult.keys) { if (!deviceResult.keys) {
// no keys? // no keys?
return false; return false;
@@ -961,8 +972,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
const signKeyId = "ed25519:" + deviceId; const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId]; const signKey = deviceResult.keys[signKeyId];
if (!signKey) { if (!signKey) {
logger.warn("Device " + userId + ":" + deviceId + logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key");
" has no ed25519 key");
return false; return false;
} }
@@ -970,10 +980,9 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
const signatures = deviceResult.signatures || {}; const signatures = deviceResult.signatures || {};
try { try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); await olmlib.verifySignature(olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) { } catch (e) {
logger.warn("Unable to verify signature on device " + logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e);
userId + ":" + deviceId + ":" + e);
return false; return false;
} }

View File

@@ -184,7 +184,7 @@ export class EncryptionSetupOperation {
}); });
// pass the new keys to the main instance of our own CrossSigningInfo. // pass the new keys to the main instance of our own CrossSigningInfo.
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys); crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys);
} }
// set account data // set account data
if (this._accountData) { if (this._accountData) {

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2018, 2019 New Vector Ltd Copyright 2018 - 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.
@@ -21,44 +21,51 @@ limitations under the License.
*/ */
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { CryptoStore } from "../client";
/* eslint-disable camelcase */
interface IRoomEncryption {
algorithm: string;
rotation_period_ms: number;
rotation_period_msgs: number;
}
/* eslint-enable camelcase */
/** /**
* @alias module:crypto/RoomList * @alias module:crypto/RoomList
*/ */
export class RoomList { export class RoomList {
constructor(cryptoStore) {
this._cryptoStore = cryptoStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event) // Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {}; private roomEncryption: Record<string, IRoomEncryption> = {};
}
async init() { constructor(private readonly cryptoStore: CryptoStore) {}
await this._cryptoStore.doTxn(
public async init(): Promise<void> {
await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => { this.cryptoStore.getEndToEndRooms(txn, (result) => {
this._roomEncryption = result; this.roomEncryption = result;
}); });
}, },
); );
} }
getRoomEncryption(roomId) { public getRoomEncryption(roomId: string): IRoomEncryption {
return this._roomEncryption[roomId] || null; return this.roomEncryption[roomId] || null;
} }
isRoomEncrypted(roomId) { public isRoomEncrypted(roomId: string): boolean {
return Boolean(this.getRoomEncryption(roomId)); return Boolean(this.getRoomEncryption(roomId));
} }
async setRoomEncryption(roomId, roomInfo) { public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise<void> {
// important that this happens before calling into the store // important that this happens before calling into the store
// as it prevents the Crypto::setRoomEncryption from calling // as it prevents the Crypto::setRoomEncryption from calling
// this twice for consecutive m.room.encryption events // this twice for consecutive m.room.encryption events
this._roomEncryption[roomId] = roomInfo; this.roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
}, },
); );
} }

View File

@@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter {
}; };
const encryptedContent = { const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM, algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key, sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key,
ciphertext: {}, ciphertext: {},
}; };
await olmlib.ensureOlmSessionsForDevices( await olmlib.ensureOlmSessionsForDevices(
this._baseApis.crypto._olmDevice, this._baseApis.crypto.olmDevice,
this._baseApis, this._baseApis,
{ {
[sender]: [ [sender]: [
@@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter {
encryptedContent.ciphertext, encryptedContent.ciphertext,
this._baseApis.getUserId(), this._baseApis.getUserId(),
this._baseApis.deviceId, this._baseApis.deviceId,
this._baseApis.crypto._olmDevice, this._baseApis.crypto.olmDevice,
sender, sender,
this._baseApis.getStoredDevice(sender, deviceId), this._baseApis.getStoredDevice(sender, deviceId),
payload, payload,
@@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter {
if (requestControl) { if (requestControl) {
// make sure that the device that sent it is one of the devices that // make sure that the device that sent it is one of the devices that
// we requested from // we requested from
const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey( const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM, olmlib.OLM_ALGORITHM,
event.getSenderKey(), event.getSenderKey(),
); );

View File

@@ -64,16 +64,16 @@ export class DehydrationManager {
this.getDehydrationKeyFromCache(); this.getDehydrationKeyFromCache();
} }
async getDehydrationKeyFromCache(): Promise<void> { async getDehydrationKeyFromCache(): Promise<void> {
return await this.crypto._cryptoStore.doTxn( return await this.crypto.cryptoStore.doTxn(
'readonly', 'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT], [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
this.crypto._cryptoStore.getSecretStorePrivateKey( this.crypto.cryptoStore.getSecretStorePrivateKey(
txn, txn,
async (result) => { async (result) => {
if (result) { if (result) {
const { key, keyInfo, deviceDisplayName, time } = result; const { key, keyInfo, deviceDisplayName, time } = result;
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM); const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
this.key = decodeBase64(decrypted); this.key = decodeBase64(decrypted);
this.keyInfo = keyInfo; this.keyInfo = keyInfo;
@@ -114,11 +114,11 @@ export class DehydrationManager {
this.timeoutId = undefined; this.timeoutId = undefined;
} }
// clear storage // clear storage
await this.crypto._cryptoStore.doTxn( await this.crypto.cryptoStore.doTxn(
'readwrite', 'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT], [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
this.crypto._cryptoStore.storeSecretStorePrivateKey( this.crypto.cryptoStore.storeSecretStorePrivateKey(
txn, "dehydration", null, txn, "dehydration", null,
); );
}, },
@@ -158,15 +158,15 @@ export class DehydrationManager {
this.timeoutId = undefined; this.timeoutId = undefined;
} }
try { try {
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
// update the crypto store with the timestamp // update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM); const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto._cryptoStore.doTxn( await this.crypto.cryptoStore.doTxn(
'readwrite', 'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT], [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
this.crypto._cryptoStore.storeSecretStorePrivateKey( this.crypto.cryptoStore.storeSecretStorePrivateKey(
txn, "dehydration", txn, "dehydration",
{ {
keyInfo: this.keyInfo, keyInfo: this.keyInfo,
@@ -205,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",
@@ -223,9 +223,9 @@ export class DehydrationManager {
const deviceId = dehydrateResult.device_id; const deviceId = dehydrateResult.device_id;
logger.log("Preparing device keys", deviceId); logger.log("Preparing device keys", deviceId);
const deviceKeys: DeviceKeys = { const deviceKeys: DeviceKeys = {
algorithms: this.crypto._supportedAlgorithms, algorithms: this.crypto.supportedAlgorithms,
device_id: deviceId, device_id: deviceId,
user_id: this.crypto._userId, user_id: this.crypto.userId,
keys: { keys: {
[`ed25519:${deviceId}`]: e2eKeys.ed25519, [`ed25519:${deviceId}`]: e2eKeys.ed25519,
[`curve25519:${deviceId}`]: e2eKeys.curve25519, [`curve25519:${deviceId}`]: e2eKeys.curve25519,
@@ -233,12 +233,12 @@ export class DehydrationManager {
}; };
const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
deviceKeys.signatures = { deviceKeys.signatures = {
[this.crypto._userId]: { [this.crypto.userId]: {
[`ed25519:${deviceId}`]: deviceSignature, [`ed25519:${deviceId}`]: deviceSignature,
}, },
}; };
if (this.crypto._crossSigningInfo.getId("self_signing")) { if (this.crypto.crossSigningInfo.getId("self_signing")) {
await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing");
} }
logger.log("Preparing one-time keys"); logger.log("Preparing one-time keys");
@@ -247,7 +247,7 @@ export class DehydrationManager {
const k: OneTimeKey = { key }; const k: OneTimeKey = { key };
const signature = account.sign(anotherjson.stringify(k)); const signature = account.sign(anotherjson.stringify(k));
k.signatures = { k.signatures = {
[this.crypto._userId]: { [this.crypto.userId]: {
[`ed25519:${deviceId}`]: signature, [`ed25519:${deviceId}`]: signature,
}, },
}; };
@@ -260,7 +260,7 @@ export class DehydrationManager {
const k: OneTimeKey = { key, fallback: true }; const k: OneTimeKey = { key, fallback: true };
const signature = account.sign(anotherjson.stringify(k)); const signature = account.sign(anotherjson.stringify(k));
k.signatures = { k.signatures = {
[this.crypto._userId]: { [this.crypto.userId]: {
[`ed25519:${deviceId}`]: signature, [`ed25519:${deviceId}`]: signature,
}, },
}; };
@@ -268,7 +268,7 @@ export class DehydrationManager {
} }
logger.log("Uploading keys to server"); logger.log("Uploading keys to server");
await this.crypto._baseApis.http.authedRequest( await this.crypto.baseApis.http.authedRequest(
undefined, undefined,
"POST", "POST",
"/keys/upload/" + encodeURI(deviceId), "/keys/upload/" + encodeURI(deviceId),

View File

@@ -1,168 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module crypto/deviceinfo
*/
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
export function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
value: deviceId,
});
this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.known = false;
this.unsigned = {};
this.signatures = {};
}
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
};
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
DeviceInfo.prototype.toStorage = function() {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
signatures: this.signatures,
};
};
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
DeviceInfo.prototype.getFingerprint = function() {
return this.keys["ed25519:" + this.deviceId];
};
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
DeviceInfo.prototype.getIdentityKey = function() {
return this.keys["curve25519:" + this.deviceId];
};
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
DeviceInfo.prototype.getDisplayName = function() {
return this.unsigned.device_display_name || null;
};
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
*/
DeviceInfo.prototype.isBlocked = function() {
return this.verified == DeviceVerification.BLOCKED;
};
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
*/
DeviceInfo.prototype.isVerified = function() {
return this.verified == DeviceVerification.VERIFIED;
};
/**
* Returns true if this device is unverified
*
* @return {Boolean} true if unverified
*/
DeviceInfo.prototype.isUnverified = function() {
return this.verified == DeviceVerification.UNVERIFIED;
};
/**
* Returns true if the user knows about this device's existence
*
* @return {Boolean} true if known
*/
DeviceInfo.prototype.isKnown = function() {
return this.known == true;
};
/**
* @enum
*/
DeviceInfo.DeviceVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
BLOCKED: -1,
};
const DeviceVerification = DeviceInfo.DeviceVerification;

175
src/crypto/deviceinfo.ts Normal file
View File

@@ -0,0 +1,175 @@
/*
Copyright 2016 - 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/deviceinfo
*/
export interface IDevice {
keys: Record<string, string>;
algorithms: string[];
verified: DeviceVerification;
known: boolean;
unsigned?: Record<string, any>;
signatures?: Record<string, string>;
}
enum DeviceVerification {
Blocked = -1,
Unverified = 0,
Verified = 1,
}
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
export class DeviceInfo {
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
}
/**
* @enum
*/
public static DeviceVerification = {
VERIFIED: DeviceVerification.Verified,
UNVERIFIED: DeviceVerification.Unverified,
BLOCKED: DeviceVerification.Blocked,
};
public algorithms: string[];
public keys: Record<string, string> = {};
public verified = DeviceVerification.Unverified;
public known = false;
public unsigned: Record<string, any> = {};
public signatures: Record<string, string> = {};
constructor(public readonly deviceId: string) {}
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
public toStorage(): IDevice {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
signatures: this.signatures,
};
}
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
public getFingerprint(): string {
return this.keys["ed25519:" + this.deviceId];
}
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
public getIdentityKey(): string {
return this.keys["curve25519:" + this.deviceId];
}
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
public getDisplayName(): string | null {
return this.unsigned.device_display_name || null;
}
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
*/
public isBlocked(): boolean {
return this.verified == DeviceVerification.Blocked;
}
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
*/
public isVerified(): boolean {
return this.verified == DeviceVerification.Verified;
}
/**
* Returns true if this device is unverified
*
* @return {Boolean} true if unverified
*/
public isUnverified(): boolean {
return this.verified == DeviceVerification.Unverified;
}
/**
* Returns true if the user knows about this device's existence
*
* @return {Boolean} true if known
*/
public isKnown(): boolean {
return this.known === true;
}
}

View File

@@ -29,7 +29,7 @@ import { logger } from '../logger';
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";
import { DeviceInfo } from "./deviceinfo"; import { DeviceInfo, IDevice } from "./deviceinfo";
import * as algorithms from "./algorithms"; import * as algorithms from "./algorithms";
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
import { EncryptionSetupBuilder } from "./EncryptionSetup"; import { EncryptionSetupBuilder } from "./EncryptionSetup";
@@ -420,9 +420,7 @@ export class Crypto extends EventEmitter {
}; };
myDevices[this.deviceId] = deviceInfo; myDevices[this.deviceId] = deviceInfo;
this.deviceList.storeDevicesForUser( this.deviceList.storeDevicesForUser(this.userId, myDevices);
this.userId, myDevices,
);
this.deviceList.saveIfDirty(); this.deviceList.saveIfDirty();
} }
@@ -922,10 +920,7 @@ export class Crypto extends EventEmitter {
await this.crossSigningInfo.getCrossSigningKeysFromCache(); await this.crossSigningInfo.getCrossSigningKeysFromCache();
// This is writing to in-memory account data in // This is writing to in-memory account data in
// builder.accountDataClientAdapter so won't fail // builder.accountDataClientAdapter so won't fail
await CrossSigningInfo.storeInSecretStorage( await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
crossSigningPrivateKeys,
secretStorage,
);
} }
if (setupNewKeyBackup && !keyBackupInfo) { if (setupNewKeyBackup && !keyBackupInfo) {
@@ -1172,7 +1167,7 @@ export class Crypto extends EventEmitter {
// FIXME: do this in batches // FIXME: do this in batches
const users = {}; const users = {};
for (const [userId, crossSigningInfo] for (const [userId, crossSigningInfo]
of Object.entries(this.deviceList._crossSigningInfo)) { of Object.entries(this.deviceList.crossSigningInfo)) {
const upgradeInfo = await this.checkForDeviceVerificationUpgrade( const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId),
); );
@@ -1248,7 +1243,7 @@ export class Crypto extends EventEmitter {
private async checkForValidDeviceSignature( private async checkForValidDeviceSignature(
userId: string, userId: string,
key: any, // TODO types key: any, // TODO types
devices: Record<string, DeviceInfo>, devices: Record<string, IDevice>,
): Promise<string[]> { ): Promise<string[]> {
const deviceIds: string[] = []; const deviceIds: string[] = [];
if (devices && key.signatures && key.signatures[userId]) { if (devices && key.signatures && key.signatures[userId]) {
@@ -1934,7 +1929,7 @@ export class Crypto extends EventEmitter {
public downloadKeys( public downloadKeys(
userIds: string[], userIds: string[],
forceDownload?: boolean, forceDownload?: boolean,
): Promise<Record<string, Record<string, DeviceInfo>>> { ): Promise<Record<string, Record<string, IDevice>>> {
return this.deviceList.downloadKeys(userIds, forceDownload); return this.deviceList.downloadKeys(userIds, forceDownload);
} }
@@ -2003,7 +1998,7 @@ export class Crypto extends EventEmitter {
verified?: boolean, verified?: boolean,
blocked?: boolean, blocked?: boolean,
known?: boolean, known?: boolean,
): Promise<DeviceInfo> { ): Promise<DeviceInfo | CrossSigningInfo> {
// get rid of any `undefined`s here so we can just check // get rid of any `undefined`s here so we can just check
// for null rather than null or undefined // for null rather than null or undefined
if (verified === undefined) verified = null; if (verified === undefined) verified = null;
@@ -2068,7 +2063,7 @@ export class Crypto extends EventEmitter {
// This will emit events when it comes back down the sync // This will emit events when it comes back down the sync
// (we could do local echo to speed things up) // (we could do local echo to speed things up)
} }
return device; return device as any; // TODO types
} else { } else {
return xsk; return xsk;
} }

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 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.
@@ -21,7 +20,21 @@ const DEFAULT_ITERATIONS = 500000;
const DEFAULT_BITSIZE = 256; const DEFAULT_BITSIZE = 256;
export async function keyFromAuthData(authData, password) { /* eslint-disable camelcase */
interface IAuthData {
private_key_salt: string;
private_key_iterations: number;
private_key_bits?: number;
}
/* eslint-enable camelcase */
interface IKey {
key: Uint8Array;
salt: string;
iterations: number
}
export async function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
if (!global.Olm) { if (!global.Olm) {
throw new Error("Olm is not available"); throw new Error("Olm is not available");
} }
@@ -40,7 +53,7 @@ export async function keyFromAuthData(authData, password) {
); );
} }
export async function keyFromPassphrase(password) { export async function keyFromPassphrase(password: string): Promise<IKey> {
if (!global.Olm) { if (!global.Olm) {
throw new Error("Olm is not available"); throw new Error("Olm is not available");
} }
@@ -52,7 +65,12 @@ export async function keyFromPassphrase(password) {
return { key, salt, iterations: DEFAULT_ITERATIONS }; return { key, salt, iterations: DEFAULT_ITERATIONS };
} }
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) { export async function deriveKey(
password: string,
salt: string,
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
const subtleCrypto = global.crypto.subtle; const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder; const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) { if (!subtleCrypto || !TextEncoder) {

View File

@@ -31,17 +31,22 @@ export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession; [sessionId: string]: IKeyBackupSession;
} }
/* eslint-disable camelcase */
export interface IKeyBackupVersion { export interface IKeyBackupVersion {
algorithm: string; algorithm: string;
auth_data: { // eslint-disable-line camelcase auth_data: {
public_key: string; // eslint-disable-line camelcase public_key: string;
signatures: ISignatures; signatures: ISignatures;
private_key_salt: string;
private_key_iterations: number;
private_key_bits?: number;
}; };
count: number; count: number;
etag: string; etag: string;
version: string; // number contained within version: string; // number contained within
recovery_key: string; // eslint-disable-line camelcase recovery_key: string;
} }
/* eslint-enable camelcase */
export interface IKeyBackupPrepareOpts { export interface IKeyBackupPrepareOpts {
secureSecretStorage: boolean; secureSecretStorage: boolean;

View File

@@ -292,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({

View File

@@ -19,6 +19,7 @@ import { MemoryStore } from "./store/memory";
import { MatrixScheduler } from "./scheduler"; import { MatrixScheduler } from "./scheduler";
import { MatrixClient } from "./client"; import { MatrixClient } from "./client";
import { ICreateClientOpts } from "./client"; import { ICreateClientOpts } from "./client";
import { DeviceTrustLevel } from "./crypto/CrossSigning";
export * from "./client"; export * from "./client";
export * from "./http-api"; export * from "./http-api";
@@ -99,7 +100,7 @@ export function setCryptoStoreFactory(fac) {
} }
export interface ICryptoCallbacks { export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>; getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void; saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: ( shouldUpgradeDeviceVerifications?: (
users: Record<string, any> users: Record<string, any>
@@ -112,7 +113,7 @@ export interface ICryptoCallbacks {
) => void; ) => void;
onSecretRequested?: ( onSecretRequested?: (
userId: string, deviceId: string, userId: string, deviceId: string,
requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel requestId: string, secretName: string, deviceTrust: DeviceTrustLevel
) => Promise<string>; ) => Promise<string>;
getDehydrationKey?: ( getDehydrationKey?: (
keyInfo: ISecretStorageKeyInfo, keyInfo: ISecretStorageKeyInfo,
@@ -132,14 +133,6 @@ export interface ISecretStorageKeyInfo {
mac?: string; mac?: string;
} }
// TODO: Move this to `CrossSigning` once converted
export interface IDeviceTrustLevel {
isVerified(): boolean;
isCrossSigningVerified(): boolean;
isLocallyVerified(): boolean;
isTofu(): boolean;
}
/** /**
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient} * Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied. * except that the 'request', 'store' and 'scheduler' dependencies are satisfied.

View File

@@ -435,12 +435,18 @@ export function isNullOrUndefined(val: any): boolean {
return val === null || val === undefined; return val === null || val === undefined;
} }
export interface IDeferred<T> {
resolve: (value: T) => void;
reject: (any) => void;
promise: Promise<T>;
}
// Returns a Deferred // Returns a Deferred
export function defer() { export function defer<T>(): IDeferred<T> {
let resolve; let resolve;
let reject; let reject;
const promise = new Promise((_resolve, _reject) => { const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve; resolve = _resolve;
reject = _reject; reject = _reject;
}); });