diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index cdad8a905..2ca459119 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -165,7 +165,7 @@ describe("DeviceList management:", function() { aliceTestClient.httpBackend.flush('/keys/query', 1).then( () => aliceTestClient.httpBackend.flush('/send/', 1), ), - aliceTestClient.client.crypto._deviceList.saveIfDirty(), + aliceTestClient.client.crypto.deviceList.saveIfDirty(), ]); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { @@ -202,7 +202,7 @@ describe("DeviceList management:", function() { return aliceTestClient.httpBackend.flush('/keys/query', 1); }).then((flushed) => { expect(flushed).toEqual(0); - return aliceTestClient.client.crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -235,7 +235,7 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@bob:xyz']); }).then(() => { - return aliceTestClient.client.crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -256,7 +256,7 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@chris:abc']); }).then(() => { - return aliceTestClient.client.crypto._deviceList.saveIfDirty(); + return aliceTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -286,7 +286,7 @@ describe("DeviceList management:", function() { }, ); await aliceTestClient.httpBackend.flush('/keys/query', 1); - await aliceTestClient.client.crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto.deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -322,7 +322,7 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto.deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -358,7 +358,7 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto._deviceList.saveIfDirty(); + await aliceTestClient.client.crypto.deviceList.saveIfDirty(); aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; @@ -379,7 +379,7 @@ describe("DeviceList management:", function() { anotherTestClient.httpBackend.when('GET', '/sync').respond( 200, getSyncResponse([])); await anotherTestClient.flushSync(); - await anotherTestClient.client.crypto._deviceList.saveIfDirty(); + await anotherTestClient.client.crypto.deviceList.saveIfDirty(); anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js index 6bb1a494b..eb87c5193 100644 --- a/spec/integ/matrix-client-crypto.spec.js +++ b/spec/integ/matrix-client-crypto.spec.js @@ -159,7 +159,7 @@ function aliDownloadsKeys() { // check that the localStorage is updated as we expect (not sure this is // an integration test, but meh) return Promise.all([p1, p2]).then(() => { - return aliTestClient.client.crypto._deviceList.saveIfDirty(); + return aliTestClient.client.crypto.deviceList.saveIfDirty(); }).then(() => { aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const devices = data.devices[bobUserId]; diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index b133b456e..acf353970 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -336,7 +336,7 @@ describe("MatrixClient", function() { var b = JSON.parse(JSON.stringify(o)); delete(b.signatures); delete(b.unsigned); - return client.crypto._olmDevice.sign(anotherjson.stringify(b)); + return client.crypto.olmDevice.sign(anotherjson.stringify(b)); }; logger.log("Ed25519: " + ed25519key); diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index d129590e1..e2bc34c25 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -1013,7 +1013,7 @@ describe("megolm", function() { event: true, }); event.senderCurve25519Key = testSenderKey; - return testClient.client.crypto._onRoomKeyEvent(event); + return testClient.client.crypto.onRoomKeyEvent(event); }).then(() => { const event = testUtils.mkEvent({ event: true, diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index bda03089a..816b952b1 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -65,7 +65,7 @@ describe("Crypto", function() { 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; device.keys["ed25519:FLIBBLE"] = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; - client.crypto._deviceList.getDeviceByIdentityKey = () => device; + client.crypto.deviceList.getDeviceByIdentityKey = () => device; encryptionInfo = client.getEventEncryptionInfo(event); expect(encryptionInfo.encrypted).toBeTruthy(); @@ -213,7 +213,7 @@ describe("Crypto", function() { async function keyshareEventForEvent(event, index) { const eventContent = event.getWireContent(); - const key = await aliceClient.crypto._olmDevice + const key = await aliceClient.crypto.olmDevice .getInboundGroupSessionKey( roomId, eventContent.sender_key, eventContent.session_id, index, @@ -285,7 +285,7 @@ describe("Crypto", function() { } })); - const bobDecryptor = bobClient.crypto._getRoomDecryptor( + const bobDecryptor = bobClient.crypto.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -377,7 +377,7 @@ describe("Crypto", function() { // key requests get queued until the sync has finished, but we don't // let the client set up enough for that to happen, so gut-wrench a bit // to force it to send now. - aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests(); + aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests(); jest.runAllTimers(); await Promise.resolve(); expect(aliceClient.sendToDevice).toBeCalledTimes(1); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 8b56b93b3..b3afc3e6c 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -365,9 +365,9 @@ describe("MegolmDecryption", function() { bobClient1.initCrypto(), bobClient2.initCrypto(), ]); - const aliceDevice = aliceClient.crypto._olmDevice; - const bobDevice1 = bobClient1.crypto._olmDevice; - const bobDevice2 = bobClient2.crypto._olmDevice; + const aliceDevice = aliceClient.crypto.olmDevice; + const bobDevice1 = bobClient1.crypto.olmDevice; + const bobDevice2 = bobClient2.crypto.olmDevice; const encryptionCfg = { "algorithm": "m.megolm.v1.aes-sha2", @@ -404,10 +404,10 @@ describe("MegolmDecryption", function() { }, }; - aliceClient.crypto._deviceList.storeDevicesForUser( + aliceClient.crypto.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); - aliceClient.crypto._deviceList.downloadKeys = async function(userIds) { + aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { return this._getDevicesFromStore(userIds); }; @@ -468,8 +468,8 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const aliceDevice = aliceClient.crypto._olmDevice; - const bobDevice = bobClient.crypto._olmDevice; + const aliceDevice = aliceClient.crypto.olmDevice; + const bobDevice = bobClient.crypto.olmDevice; const encryptionCfg = { "algorithm": "m.megolm.v1.aes-sha2", @@ -508,10 +508,10 @@ describe("MegolmDecryption", function() { }, }; - aliceClient.crypto._deviceList.storeDevicesForUser( + aliceClient.crypto.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); - aliceClient.crypto._deviceList.downloadKeys = async function(userIds) { + aliceClient.crypto.deviceList.downloadKeys = async function(userIds) { return this._getDevicesFromStore(userIds); }; @@ -561,11 +561,11 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const bobDevice = bobClient.crypto._olmDevice; + const bobDevice = bobClient.crypto.olmDevice; const roomId = "!someroom"; - aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ type: "org.matrix.room_key.withheld", sender: "@bob:example.com", content: { @@ -605,13 +605,13 @@ describe("MegolmDecryption", function() { bobClient.initCrypto(), ]); aliceClient.crypto.downloadKeys = async () => {}; - const bobDevice = bobClient.crypto._olmDevice; + const bobDevice = bobClient.crypto.olmDevice; const roomId = "!someroom"; const now = Date.now(); - aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ type: "org.matrix.room_key.withheld", sender: "@bob:example.com", content: { @@ -655,7 +655,7 @@ describe("MegolmDecryption", function() { aliceClient.initCrypto(), bobClient.initCrypto(), ]); - const bobDevice = bobClient.crypto._olmDevice; + const bobDevice = bobClient.crypto.olmDevice; aliceClient.crypto.downloadKeys = async () => {}; const roomId = "!someroom"; @@ -663,7 +663,7 @@ describe("MegolmDecryption", function() { const now = Date.now(); // pretend we got an event that we can't decrypt - aliceClient.crypto._onToDeviceEvent(new MatrixEvent({ + aliceClient.crypto.onToDeviceEvent(new MatrixEvent({ type: "m.room.encrypted", sender: "@bob:example.com", content: { diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 1bb8a39f8..df65475d4 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -296,7 +296,7 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({}); }; - client.crypto._backupManager.backupGroupSession( + client.crypto.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); @@ -478,7 +478,7 @@ describe("MegolmBackup", function() { ); } }; - client.crypto._backupManager.backupGroupSession( + client.crypto.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 56b86b26b..3118e6365 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -64,8 +64,8 @@ describe("Cross Signing", function() { ); alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => { await olmlib.verifySignature( - alice.crypto._olmDevice, keys.master_key, "@alice:example.com", - "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, + alice.crypto.olmDevice, keys.master_key, "@alice:example.com", + "Osborne2", alice.crypto.olmDevice.deviceEd25519Key, ); }); alice.uploadKeySignatures = async () => {}; @@ -138,7 +138,7 @@ describe("Cross Signing", function() { // set Alice's cross-signing key await resetCrossSigningKeys(alice); // Alice downloads Bob's device key - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -203,12 +203,12 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = jest.fn(async (content) => { try { await olmlib.verifySignature( - alice.crypto._olmDevice, + alice.crypto.olmDevice, content["@alice:example.com"][ "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" ], "@alice:example.com", - "Osborne2", alice.crypto._olmDevice.deviceEd25519Key, + "Osborne2", alice.crypto.olmDevice.deviceEd25519Key, ); olmlib.pkVerify( content["@alice:example.com"]["Osborne2"], @@ -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; const aliceDevice = { user_id: "@alice:example.com", @@ -230,7 +230,7 @@ describe("Cross Signing", function() { }; aliceDevice.keys = deviceInfo.keys; aliceDevice.algorithms = deviceInfo.algorithms; - await alice.crypto._signObject(aliceDevice); + await alice.crypto.signObject(aliceDevice); olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com"); // feed sync result that includes master key, ssk, device key @@ -358,7 +358,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -387,7 +387,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobPubkey]: sig, }, }; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be TOFU @@ -421,8 +421,8 @@ describe("Cross Signing", function() { null, aliceKeys, ); - alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com"); - alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {}; + alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com"); + alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {}; alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; @@ -437,14 +437,14 @@ describe("Cross Signing", function() { ]); const keyChangePromise = new Promise((resolve, reject) => { - alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => { + alice.crypto.deviceList.once("userCrossSigningUpdated", (userId) => { if (userId === "@bob:example.com") { resolve(); } }); }); - const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"] + const deviceInfo = alice.crypto.deviceList._devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -452,7 +452,7 @@ describe("Cross Signing", function() { }; aliceDevice.keys = deviceInfo.keys; aliceDevice.algorithms = deviceInfo.algorithms; - await alice.crypto._signObject(aliceDevice); + await alice.crypto.signObject(aliceDevice); const bobOlmAccount = new global.Olm.Account(); bobOlmAccount.create(); @@ -606,7 +606,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -629,7 +629,7 @@ describe("Cross Signing", function() { "ed25519:Dynabook": "someOtherPubkey", }, }; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be untrusted @@ -673,7 +673,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -701,7 +701,7 @@ describe("Cross Signing", function() { bobDevice.signatures = {}; bobDevice.signatures["@bob:example.com"] = {}; bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Alice verifies Bob's SSK @@ -733,7 +733,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey2]: sskSig2, }, }; - alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -770,7 +770,7 @@ describe("Cross Signing", function() { // Alice gets new signature for device const sig2 = bobSigning2.sign(bobDeviceString); bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); @@ -805,20 +805,20 @@ describe("Cross Signing", function() { bob.uploadKeySignatures = async () => {}; // set Bob's cross-signing key await resetCrossSigningKeys(bob); - alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: { algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], keys: { - "curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key, - "ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key, + "curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key, + "ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key, }, verified: 1, known: true, }, }); - alice.crypto._deviceList.storeCrossSigningForUser( + alice.crypto.deviceList.storeCrossSigningForUser( "@bob:example.com", - bob.crypto._crossSigningInfo.toStorage(), + bob.crypto.crossSigningInfo.toStorage(), ); alice.uploadDeviceSigningKeys = async () => {}; @@ -838,7 +838,7 @@ describe("Cross Signing", function() { expect(bobTrust.isTofu()).toBeTruthy(); // "forget" that Bob is trusted - delete alice.crypto._deviceList._crossSigningInfo["@bob:example.com"] + delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"] .keys.master.signatures["@alice:example.com"]; const bobTrust2 = alice.checkUserTrust("@bob:example.com"); @@ -848,7 +848,7 @@ describe("Cross Signing", function() { upgradePromise = new Promise((resolve) => { upgradeResolveFunc = resolve; }); - alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); + alice.crypto.deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); await new Promise((resolve) => { alice.crypto.on("userTrustStatusChanged", resolve); }); diff --git a/spec/unit/crypto/crypto-utils.js b/spec/unit/crypto/crypto-utils.js index dcc9db16a..dbf6cec65 100644 --- a/spec/unit/crypto/crypto-utils.js +++ b/spec/unit/crypto/crypto-utils.js @@ -8,22 +8,22 @@ export async function resetCrossSigningKeys(client, { } = {}) { const crypto = client.crypto; - const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys); + const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys); try { - await crypto._crossSigningInfo.resetKeys(level); - await crypto._signObject(crypto._crossSigningInfo.keys.master); + await crypto.crossSigningInfo.resetKeys(level); + await crypto._signObject(crypto.crossSigningInfo.keys.master); // write a copy locally so we know these are trusted keys await crypto._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { crypto._cryptoStore.storeCrossSigningKeys( - txn, crypto._crossSigningInfo.keys); + txn, crypto.crossSigningInfo.keys); }, ); } catch (e) { // If anything failed here, revert the keys so we know to try again from the start // next time. - crypto._crossSigningInfo.keys = oldKeys; + crypto.crossSigningInfo.keys = oldKeys; throw e; } crypto._baseApis.emit("crossSigning.keysChanged", {}); diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index fc82a3259..2a86dfaa1 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -99,11 +99,11 @@ describe("Secrets", function() { }, }, ); - alice.crypto._crossSigningInfo.setKeys({ + alice.crypto.crossSigningInfo.setKeys({ master: signingkeyInfo, }); - const secretStorage = alice.crypto._secretStorage; + const secretStorage = alice.crypto.secretStorage; alice.setAccountData = async function(eventType, contents, callback) { alice.store.storeAccountDataEvents([ @@ -120,7 +120,7 @@ describe("Secrets", function() { const keyAccountData = { algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, }; - await alice.crypto._crossSigningInfo.signObject(keyAccountData, 'master'); + await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master'); alice.store.storeAccountDataEvents([ new MatrixEvent({ @@ -234,11 +234,11 @@ describe("Secrets", function() { }, ); - const vaxDevice = vax.client.crypto._olmDevice; - const osborne2Device = osborne2.client.crypto._olmDevice; - const secretStorage = osborne2.client.crypto._secretStorage; + const vaxDevice = vax.client.crypto.olmDevice; + const osborne2Device = osborne2.client.crypto.olmDevice; + const secretStorage = osborne2.client.crypto.secretStorage; - osborne2.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", { + osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { "VAX": { user_id: "@alice:example.com", device_id: "VAX", @@ -249,7 +249,7 @@ describe("Secrets", function() { }, }, }); - vax.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", { + vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { "Osborne2": { user_id: "@alice:example.com", device_id: "Osborne2", @@ -265,7 +265,7 @@ describe("Secrets", function() { const otks = (await osborne2Device.getOneTimeKeys()).curve25519; await osborne2Device.markKeysAsPublished(); - await vax.client.crypto._olmDevice.createOutboundSession( + await vax.client.crypto.olmDevice.createOutboundSession( osborne2Device.deviceCurve25519Key, Object.values(otks)[0], ); @@ -334,8 +334,8 @@ describe("Secrets", function() { createSecretStorageKey, }); - const crossSigning = bob.crypto._crossSigningInfo; - const secretStorage = bob.crypto._secretStorage; + const crossSigning = bob.crypto.crossSigningInfo; + const secretStorage = bob.crypto.secretStorage; expect(crossSigning.getId()).toBeTruthy(); expect(await crossSigning.isStoredInSecretStorage(secretStorage)) @@ -376,10 +376,10 @@ describe("Secrets", function() { ]); this.emit("accountData", event); }; - bob.crypto._backupManager.checkKeyBackup = async () => {}; + bob.crypto.backupManager.checkKeyBackup = async () => {}; - const crossSigning = bob.crypto._crossSigningInfo; - const secretStorage = bob.crypto._secretStorage; + const crossSigning = bob.crypto.crossSigningInfo; + const secretStorage = bob.crypto.secretStorage; // Set up cross-signing keys from scratch with specific storage key await bob.bootstrapCrossSigning({ @@ -394,7 +394,7 @@ describe("Secrets", function() { }); // Clear local cross-signing keys and read from secret storage - bob.crypto._deviceList.storeCrossSigningForUser( + bob.crypto.deviceList.storeCrossSigningForUser( "@bob:example.com", crossSigning.toStorage(), ); @@ -479,7 +479,7 @@ describe("Secrets", function() { }, }), ]); - alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", @@ -619,7 +619,7 @@ describe("Secrets", function() { }, }), ]); - alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", diff --git a/spec/unit/crypto/verification/request.spec.js b/spec/unit/crypto/verification/request.spec.js index 11275e6fc..3ad4fe562 100644 --- a/spec/unit/crypto/verification/request.spec.js +++ b/spec/unit/crypto/verification/request.spec.js @@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function() verificationMethods: [verificationMethods.SAS], }, ); - alice.client.crypto._deviceList.getRawStoredDevicesForUser = function() { + alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() { return { Dynabook: { keys: { diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 0a643d318..fcb73de29 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -87,8 +87,8 @@ describe("SAS verification", function() { }, ); - const aliceDevice = alice.client.crypto._olmDevice; - const bobDevice = bob.client.crypto._olmDevice; + const aliceDevice = alice.client.crypto.olmDevice; + const bobDevice = bob.client.crypto.olmDevice; ALICE_DEVICES = { Osborne2: { @@ -114,14 +114,14 @@ describe("SAS verification", function() { }, }; - alice.client.crypto._deviceList.storeDevicesForUser( + alice.client.crypto.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.client.crypto._deviceList.storeDevicesForUser( + bob.client.crypto.deviceList.storeDevicesForUser( "@alice:example.com", ALICE_DEVICES, ); bob.client.downloadKeys = () => { @@ -296,9 +296,9 @@ describe("SAS verification", function() { await resetCrossSigningKeys(bob.client); - bob.client.crypto._deviceList.storeCrossSigningForUser( + bob.client.crypto.deviceList.storeCrossSigningForUser( "@alice:example.com", { - keys: alice.client.crypto._crossSigningInfo.keys, + keys: alice.client.crypto.crossSigningInfo.keys, }, ); diff --git a/src/client.ts b/src/client.ts index d6b4aecda..91beeef39 100644 --- a/src/client.ts +++ b/src/client.ts @@ -48,7 +48,7 @@ import { retryNetworkOperation, } from "./http-api"; import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto'; -import { DeviceInfo } from "./crypto/deviceinfo"; +import { DeviceInfo, IDevice } from "./crypto/deviceinfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { User } from "./models/user"; @@ -64,7 +64,7 @@ import { import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import type Request from "request"; 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 { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; @@ -85,7 +85,7 @@ import { IRecoveryKey, ISecretStorageKey, } from "./crypto/api"; -import { CrossSigningInfo, UserTrustLevel } from "./crypto/CrossSigning"; +import { CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./crypto/CrossSigning"; import { Room } from "./models/room"; import { ICreateRoomOpts, @@ -1265,7 +1265,7 @@ export class MatrixClient extends EventEmitter { public downloadKeys( userIds: string[], forceDownload?: boolean, - ): Promise>> { + ): Promise>> { if (!this.crypto) { 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} 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) { throw new Error("End-to-end encryption disabled"); } @@ -1948,7 +1948,7 @@ export class MatrixClient extends EventEmitter { * * @return {Promise} */ - public getEventSenderDeviceInfo(event: MatrixEvent): Promise { + public async getEventSenderDeviceInfo(event: MatrixEvent): Promise { if (!this.crypto) { return null; } @@ -2488,15 +2488,13 @@ export class MatrixClient extends EventEmitter { targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise { const privKey = await this.crypto.getSessionBackupPrivateKey(); if (!privKey) { throw new Error("Couldn't get key"); } - return this.restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); + return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } private async restoreKeyBackup( diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.ts similarity index 75% rename from src/crypto/CrossSigning.js rename to src/crypto/CrossSigning.ts index e2af6ce3b..2d983c2e5 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.ts @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,22 +19,43 @@ limitations under the License. * @module crypto/CrossSigning */ -import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib'; import { EventEmitter } from 'events'; + +import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib'; import { logger } from '../logger'; import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store'; 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; -function publicKeyFromKeyInfo(keyInfo) { +function publicKeyFromKeyInfo(keyInfo: any): any { // TODO types // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } // We assume only a single key, and we want the bare form without type // prefix, so we select the values. return Object.values(keyInfo.keys)[0]; } +interface ICacheCallbacks { + getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise; + storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise; +} + export class CrossSigningInfo extends EventEmitter { + public keys: Record = {}; // 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 * @@ -46,27 +66,15 @@ export class CrossSigningInfo extends EventEmitter { * Requires getCrossSigningKey and saveCrossSigningKeys * @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(); - - // 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); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { @@ -76,7 +84,7 @@ export class CrossSigningInfo extends EventEmitter { return res; } - toStorage() { + public toStorage(): object { return { keys: this.keys, firstUse: this.firstUse, @@ -92,10 +100,10 @@ export class CrossSigningInfo extends EventEmitter { * the stored public key for the given key type. * @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; - if (!this._callbacks.getCrossSigningKey) { + if (!this.callbacks.getCrossSigningKey) { throw new Error("No getCrossSigningKey callback supplied"); } @@ -103,7 +111,7 @@ export class CrossSigningInfo extends EventEmitter { expectedPubkey = this.getId(type); } - function validateKey(key) { + function validateKey(key: Uint8Array): [string, PkSigning] { if (!key) return; const signing = new global.Olm.PkSigning(); const gotPubkey = signing.init_with_seed(key); @@ -114,9 +122,8 @@ export class CrossSigningInfo extends EventEmitter { } let privkey; - if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) { - privkey = await this._cacheCallbacks - .getCrossSigningKeyCache(type, expectedPubkey); + if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) { + privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); } const cacheresult = validateKey(privkey); @@ -124,11 +131,11 @@ export class CrossSigningInfo extends EventEmitter { return cacheresult; } - privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); + privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey); const result = validateKey(privkey); if (result) { - if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { - await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey); + if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { + await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey); } 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 * key */ - async isStoredInSecretStorage(secretStorage) { + public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise> { // check what SSSS keys have encrypted the master key (if any) - const stored = - await secretStorage.isStored("m.cross_signing.master", false) || {}; + const stored = await secretStorage.isStored("m.cross_signing.master", false) || {}; // then check which of those SSSS keys have also encrypted the SSK and USK function intersect(s) { for (const k of Object.keys(stored)) { @@ -169,9 +175,7 @@ export class CrossSigningInfo extends EventEmitter { } } for (const type of ["self_signing", "user_signing"]) { - intersect( - await secretStorage.isStored(`m.cross_signing.${type}`, false) || {}, - ); + intersect(await secretStorage.isStored(`m.cross_signing.${type}`, false) || {}); } return Object.keys(stored).length ? stored : null; } @@ -184,7 +188,10 @@ export class CrossSigningInfo extends EventEmitter { * @param {Map} keys The keys to store * @param {SecretStorage} secretStorage The secret store using account data */ - static async storeInSecretStorage(keys, secretStorage) { + public static async storeInSecretStorage( + keys: Map, + secretStorage: SecretStorage, + ): Promise { for (const [type, privateKey] of keys) { const encodedKey = encodeBase64(privateKey); 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 * @return {Uint8Array} The private key */ - static async getFromSecretStorage(type, secretStorage) { + public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); if (!encodedKey) { return null; @@ -215,8 +222,8 @@ export class CrossSigningInfo extends EventEmitter { * "self_signing", or "user_signing". Optional, will check all by default. * @returns {boolean} True if all keys are stored in the local cache. */ - async isStoredInKeyCache(type) { - const cacheCallbacks = this._cacheCallbacks; + public async isStoredInKeyCache(type?: string): Promise { + const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return false; const types = type ? [type] : ["master", "self_signing", "user_signing"]; 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) */ - async getCrossSigningKeysFromCache() { + public async getCrossSigningKeysFromCache(): Promise> { const keys = new Map(); - const cacheCallbacks = this._cacheCallbacks; + const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return keys; for (const type of ["master", "self_signing", "user_signing"]) { const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); @@ -255,8 +262,7 @@ export class CrossSigningInfo extends EventEmitter { * * @return {string} the ID */ - getId(type) { - type = type || "master"; + public getId(type = "master"): string { if (!this.keys[type]) return null; const keyInfo = this.keys[type]; return publicKeyFromKeyInfo(keyInfo); @@ -269,8 +275,8 @@ export class CrossSigningInfo extends EventEmitter { * * @param {CrossSigningLevel} level The key types to reset */ - async resetKeys(level) { - if (!this._callbacks.saveCrossSigningKeys) { + public async resetKeys(level?: CrossSigningLevel): Promise { + if (!this.callbacks.saveCrossSigningKeys) { throw new Error("No saveCrossSigningKeys callback supplied"); } @@ -289,8 +295,8 @@ export class CrossSigningInfo extends EventEmitter { return; } - const privateKeys = {}; - const keys = {}; + const privateKeys: Record = {}; + const keys: Record = {}; let masterSigning; let masterPub; @@ -347,7 +353,7 @@ export class CrossSigningInfo extends EventEmitter { } Object.assign(this.keys, keys); - this._callbacks.saveCrossSigningKeys(privateKeys); + this.callbacks.saveCrossSigningKeys(privateKeys); } finally { if (masterSigning) { 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 */ - clearKeys() { + public clearKeys(): void { this.keys = {}; } - setKeys(keys) { - const signingKeys = {}; + public setKeys(keys: Record): void { + const signingKeys: Record = {}; if (keys.master) { if (keys.master.user_id !== this.userId) { 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 // never back to false to avoid a downgrade attack. if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) { @@ -442,7 +448,7 @@ export class CrossSigningInfo extends EventEmitter { } } - async signObject(data, type) { + public async signObject(data: T, type: string): Promise { if (!this.keys[type]) { throw new Error( "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 { if (!this.keys.user_signing) { logger.info("No user signing key: not signing user"); return; @@ -465,7 +471,7 @@ export class CrossSigningInfo extends EventEmitter { return this.signObject(key.keys.master, "user_signing"); } - async signDevice(userId, device) { + public async signDevice(userId: string, device: DeviceInfo): Promise { if (userId !== this.userId) { throw new Error( `Trying to sign ${userId}'s device; can only sign our own device`, @@ -492,7 +498,7 @@ export class CrossSigningInfo extends EventEmitter { * * @returns {UserTrustLevel} */ - checkUserTrust(userCrossSigning) { + public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel { // if we're checking our own key, then it's trusted if the master key // and self-signing key match if (this.userId === userCrossSigning.userId @@ -530,12 +536,17 @@ export class CrossSigningInfo extends EventEmitter { * * @param {CrossSigningInfo} userCrossSigning Cross signing info for user * @param {module:crypto/deviceinfo} device The device to check - * @param {bool} localTrust Whether the device is trusted locally - * @param {bool} trustCrossSignedDevices Whether we trust cross signed devices + * @param {boolean} localTrust Whether the device is trusted locally + * @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices * * @returns {DeviceTrustLevel} */ - checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) { + public checkDeviceTrust( + userCrossSigning: CrossSigningInfo, + device: DeviceInfo, + localTrust: boolean, + trustCrossSignedDevices: boolean, + ): DeviceTrustLevel { const userTrust = this.checkUserTrust(userCrossSigning); 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... pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); // ...and this device's key from their SSK... - pkVerify( - deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId, - ); + pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); // ...then we trust this device as much as far as we trust the user - return DeviceTrustLevel.fromUserTrustLevel( - userTrust, localTrust, trustCrossSignedDevices, - ); + return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices); } catch (e) { - return new DeviceTrustLevel( - false, false, localTrust, trustCrossSignedDevices, - ); + return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); } } /** * @returns {object} Cache callbacks */ - getCacheCallbacks() { - return this._cacheCallbacks; + public getCacheCallbacks(): ICacheCallbacks { + return this.cacheCallbacks; } } -function deviceToObject(device, userId) { +function deviceToObject(device: DeviceInfo, userId: string) { return { algorithms: device.algorithms, keys: device.keys, @@ -584,49 +589,49 @@ function deviceToObject(device, userId) { }; } -export const CrossSigningLevel = { - MASTER: 4, - USER_SIGNING: 2, - SELF_SIGNING: 1, -}; +export enum CrossSigningLevel { + MASTER = 4, + USER_SIGNING = 2, + SELF_SIGNING = 1, +} /** * Represents the ways in which we trust a user */ export class UserTrustLevel { - constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) { - this._crossSigningVerified = crossSigningVerified; - this._crossSigningVerifiedBefore = crossSigningVerifiedBefore; - this._tofu = tofu; - } + constructor( + private readonly crossSigningVerified: boolean, + private readonly crossSigningVerifiedBefore: boolean, + 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(); } /** - * @returns {bool} true if this user is verified via cross signing + * @returns {boolean} true if this user is verified via cross signing */ - isCrossSigningVerified() { - return this._crossSigningVerified; + public isCrossSigningVerified(): boolean { + 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). */ - wasCrossSigningVerified() { - return this._crossSigningVerifiedBefore; + public wasCrossSigningVerified(): boolean { + 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() { - return this._tofu; + public isTofu(): boolean { + return this.tofu; } } @@ -634,58 +639,62 @@ export class UserTrustLevel { * Represents the ways in which we trust a device */ export class DeviceTrustLevel { - constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) { - this._crossSigningVerified = crossSigningVerified; - this._tofu = tofu; - this._localVerified = localVerified; - this._trustCrossSignedDevices = trustCrossSignedDevices; - } + constructor( + public readonly crossSigningVerified: boolean, + public readonly tofu: boolean, + private readonly localVerified: boolean, + private readonly trustCrossSignedDevices: boolean, + ) {} - static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) { + public static fromUserTrustLevel( + userTrustLevel: UserTrustLevel, + localVerified: boolean, + trustCrossSignedDevices: boolean, + ): DeviceTrustLevel { return new DeviceTrustLevel( - userTrustLevel._crossSigningVerified, - userTrustLevel._tofu, + userTrustLevel.isCrossSigningVerified(), + userTrustLevel.isTofu(), localVerified, 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() || ( - 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() { - return this._crossSigningVerified; + public isCrossSigningVerified(): boolean { + return this.crossSigningVerified; } /** - * @returns {bool} true if this device is verified locally + * @returns {boolean} true if this device is verified locally */ - isLocallyVerified() { - return this._localVerified; + public isLocallyVerified(): boolean { + 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 */ - isTofu() { - return this._tofu; + public isTofu(): boolean { + return this.tofu; } } -export function createCryptoStoreCacheCallbacks(store, olmdevice) { +export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks { return { - getCrossSigningKeyCache: async function(type, _expectedPublicKey) { - const key = await new Promise((resolve) => { + getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise { + const key = await new Promise((resolve) => { return store.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -696,20 +705,20 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) { }); if (key && key.ciphertext) { - const pickleKey = Buffer.from(olmdevice._pickleKey); + const pickleKey = Buffer.from(olmDevice._pickleKey); const decrypted = await decryptAES(key, pickleKey, type); return decodeBase64(decrypted); } else { return key; } }, - storeCrossSigningKeyCache: async function(type, key) { + storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise { if (!(key instanceof Uint8Array)) { throw new Error( `storeCrossSigningKeyCache expects Uint8Array, got ${key}`, ); } - const pickleKey = Buffer.from(olmdevice._pickleKey); + const pickleKey = Buffer.from(olmDevice._pickleKey); key = await encryptAES(encodeBase64(key), pickleKey, type); return store.doTxn( 'readwrite', @@ -729,7 +738,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) { * @param {string} userId The user 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 (baseApis.getUserId() !== userId) { return; @@ -739,7 +748,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) // it. We return here in order to test. return new Promise((resolve, reject) => { const client = baseApis; - const original = client.crypto._crossSigningInfo; + const original = client.crypto.crossSigningInfo; // We already have all of the infrastructure we need to validate and // cache cross-signing keys, so instead of replicating that, here we set @@ -748,8 +757,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) const crossSigning = new CrossSigningInfo( original.userId, { getCrossSigningKey: async (type) => { - logger.debug("Cross-signing: requesting secret", - type, deviceId); + logger.debug("Cross-signing: requesting secret", type, deviceId); const { promise } = client.requestSecret( `m.cross_signing.${type}`, [deviceId], ); @@ -757,7 +765,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) const decoded = decodeBase64(result); return Uint8Array.from(decoded); } }, - original._cacheCallbacks, + original.getCacheCallbacks(), ); crossSigning.keys = original.keys; @@ -774,7 +782,8 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId) }); // 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(async resolve => { const cachedKey = await client.crypto.getSessionBackupPrivateKey(); if (!cachedKey) { 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..."); const backupInfo = await client.getKeyBackupVersion(); // no need to await for this - just let it go in the bg - client.restoreKeyBackupWithCache( - undefined, undefined, backupInfo, - ).then(() => { + client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { logger.info("Backup restored."); }); } diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.ts similarity index 62% rename from src/crypto/DeviceList.js rename to src/crypto/DeviceList.ts index 9b74a54db..688e510a9 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -23,12 +21,15 @@ limitations under the License. */ import { EventEmitter } from 'events'; + import { logger } from '../logger'; -import { DeviceInfo } from './deviceinfo'; +import { DeviceInfo, IDevice } from './deviceinfo'; import { CrossSigningInfo } from './CrossSigning'; import * as olmlib from './olmlib'; 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 * @@ -51,91 +52,96 @@ import { chunkPromises, defer, sleep } from '../utils'; */ // constants for DeviceList._deviceTrackingStatus -const TRACKING_STATUS_NOT_TRACKED = 0; -const TRACKING_STATUS_PENDING_DOWNLOAD = 1; -const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2; -const TRACKING_STATUS_UP_TO_DATE = 3; +enum TrackingStatus { + NotTracked, + PendingDownload, + DownloadInProgress, + UpToDate, +} + +type DeviceInfoMap = Record>; /** * @alias module:crypto/DeviceList */ export class DeviceList extends EventEmitter { - constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) { + // userId -> { + // deviceId -> { + // [device info] + // } + // } + private devices: DeviceInfoMap = {}; + + // userId -> { + // [key info] + // } + public crossSigningInfo: Record = {}; + + // map of identity keys to the user who owns it + private userByIdentityKey: Record = {}; + + // which users we are tracking device status for. + // userId -> TRACKING_STATUS_* + private deviceTrackingStatus: Record = {}; // loaded from storage in load() + + // The 'next_batch' sync token at the point the data was written, + // ie. a token representing the point immediately after the + // moment represented by the snapshot in the db. + private syncToken: string = null; + + // userId -> promise + private keyDownloadsInProgressByUser: Record> = {}; + + // Set whenever changes are made other than setting the sync token + private dirty = false; + + // Promise resolved when device data is saved + private savePromise: Promise = null; + // Function that resolves the save promise + private resolveSavePromise: (saved: boolean) => void = null; + // The time the save is scheduled for + private savePromiseTime: number = null; + // The timer used to delay the save + private saveTimer: NodeJS.Timeout = null; + // True if we have fetched data from the server or loaded a non-empty + // set of device data from the store + 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._cryptoStore = cryptoStore; - - // userId -> { - // deviceId -> { - // [device info] - // } - // } - this._devices = {}; - - // userId -> { - // [key info] - // } - this._crossSigningInfo = {}; - - // map of identity keys to the user who owns it - this._userByIdentityKey = {}; - - // which users we are tracking device status for. - // userId -> TRACKING_STATUS_* - this._deviceTrackingStatus = {}; // loaded from storage in load() - - // The 'next_batch' sync token at the point the data was writen, - // ie. a token representing the point immediately after the - // moment represented by the snapshot in the db. - this._syncToken = null; - - this._serialiser = new DeviceListUpdateSerialiser( - baseApis, olmDevice, this, - ); - - // userId -> promise - this._keyDownloadsInProgressByUser = {}; - - // 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 - this._dirty = false; - - // Promise resolved when device data is saved - this._savePromise = null; - // Function that resolves the save promise - this._resolveSavePromise = null; - // The time the save is scheduled for - this._savePromiseTime = null; - // The timer used to delay the save - this._saveTimer = null; - // True if we have fetched data from the server or loaded a non-empty - // set of device data from the store - this._hasFetched = null; + this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this); } /** * Load the device tracking state from storage */ - async load() { - await this._cryptoStore.doTxn( + public async load() { + await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { - this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { - this._hasFetched = Boolean(deviceData && deviceData.devices); - this._devices = deviceData ? deviceData.devices : {}, - this._crossSigningInfo = deviceData ? + this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { + this.hasFetched = Boolean(deviceData && deviceData.devices); + this.devices = deviceData ? deviceData.devices : {}, + this.crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {}; - this._deviceTrackingStatus = deviceData ? + this.deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; - this._syncToken = deviceData ? deviceData.syncToken : null; - this._userByIdentityKey = {}; - for (const user of Object.keys(this._devices)) { - const userDevices = this._devices[user]; + this.syncToken = deviceData ? deviceData.syncToken : null; + this.userByIdentityKey = {}; + for (const user of Object.keys(this.devices)) { + const userDevices = this.devices[user]; for (const device of Object.keys(userDevices)) { const idKey = userDevices[device].keys['curve25519:'+device]; 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 (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { - this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + if (this.deviceTrackingStatus[u] == TrackingStatus.DownloadInProgress) { + this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload; } } } - stop() { - if (this._saveTimer !== null) { - clearTimeout(this._saveTimer); + public stop() { + if (this.saveTimer !== null) { + 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 * 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 * multiple writes, but this behaviour can be disabled by passing 0. * - * @return {Promise} true if the data was saved, false if + * @return {Promise} true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. */ - async saveIfDirty(delay) { - if (!this._dirty) return Promise.resolve(false); + public async saveIfDirty(delay = 500): Promise { + if (!this.dirty) return Promise.resolve(false); // 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) - if (delay === undefined) delay = 500; 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 // it & schedule one for the time we want - clearTimeout(this._saveTimer); - this._saveTimer = null; - this._savePromiseTime = null; + clearTimeout(this.saveTimer); + this.saveTimer = null; + this.savePromiseTime = null; // (but keep the save promise since whatever called save before // will still want to know when the save is done) } - let savePromise = this._savePromise; + let savePromise = this.savePromise; if (savePromise === null) { savePromise = new Promise((resolve, reject) => { - this._resolveSavePromise = resolve; + this.resolveSavePromise = resolve; }); - this._savePromise = savePromise; + this.savePromise = savePromise; } - if (this._saveTimer === null) { - const resolveSavePromise = this._resolveSavePromise; - this._savePromiseTime = targetTime; - this._saveTimer = setTimeout(() => { - logger.log('Saving device tracking data', this._syncToken); + if (this.saveTimer === null) { + const resolveSavePromise = this.resolveSavePromise; + this.savePromiseTime = targetTime; + this.saveTimer = setTimeout(() => { + logger.log('Saving device tracking data', this.syncToken); // null out savePromise now (after the delay but before the write), // otherwise we could return the existing promise when the save has // actually already happened. - this._savePromiseTime = null; - this._saveTimer = null; - this._savePromise = null; - this._resolveSavePromise = null; + this.savePromiseTime = null; + this.saveTimer = null; + this.savePromise = null; + this.resolveSavePromise = null; - this._cryptoStore.doTxn( + this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { - this._cryptoStore.storeEndToEndDeviceData({ - devices: this._devices, - crossSigningInfo: this._crossSigningInfo, - trackingStatus: this._deviceTrackingStatus, - syncToken: this._syncToken, + this.cryptoStore.storeEndToEndDeviceData({ + devices: this.devices, + crossSigningInfo: this.crossSigningInfo, + trackingStatus: this.deviceTrackingStatus, + syncToken: this.syncToken, }, txn); }, ).then(() => { - // The device list is considered dirty until the write - // completes. - this._dirty = false; - resolveSavePromise(); + // The device list is considered dirty until the write completes. + this.dirty = false; + resolveSavePromise(true); }, err => { - logger.error('Failed to save device tracking data', this._syncToken); + logger.error('Failed to save device tracking data', this.syncToken); logger.error(err); }); }, delay); } + return savePromise; } @@ -240,8 +245,8 @@ export class DeviceList extends EventEmitter { * * @return {string} The sync token */ - getSyncToken() { - return this._syncToken; + public getSyncToken(): string { + return this.syncToken; } /** @@ -254,8 +259,8 @@ export class DeviceList extends EventEmitter { * * @param {string} st The sync token */ - setSyncToken(st) { - this._syncToken = st; + public setSyncToken(st: string): void { + this.syncToken = st; } /** @@ -263,33 +268,33 @@ export class DeviceList extends EventEmitter { * downloading and storing them if they're not (or if forceDownload is * true). * @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 * module:crypto/deviceinfo|DeviceInfo}. */ - downloadKeys(userIds, forceDownload) { + public downloadKeys(userIds: string[], forceDownload: boolean): Promise { const usersToDownload = []; const promises = []; userIds.forEach((u) => { - const trackingStatus = this._deviceTrackingStatus[u]; - if (this._keyDownloadsInProgressByUser[u]) { + const trackingStatus = this.deviceTrackingStatus[u]; + if (this.keyDownloadsInProgressByUser[u]) { // already a key download in progress/queued for this user; its results // will be good enough for us. logger.log( `downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`, ); - promises.push(this._keyDownloadsInProgressByUser[u]); - } else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) { + promises.push(this.keyDownloadsInProgressByUser[u]); + } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) { usersToDownload.push(u); } }); if (usersToDownload.length != 0) { logger.log("downloadKeys: downloading for", usersToDownload); - const downloadPromise = this._doKeyDownload(usersToDownload); + const downloadPromise = this.doKeyDownload(usersToDownload); promises.push(downloadPromise); } @@ -298,7 +303,7 @@ export class DeviceList extends EventEmitter { } 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}. */ - _getDevicesFromStore(userIds) { + private getDevicesFromStore(userIds: string[]): DeviceInfoMap { const stored = {}; - const self = this; - userIds.map(function(u) { + userIds.map((u) => { stored[u] = {}; - const devices = self.getStoredDevicesForUser(u) || []; + const devices = this.getStoredDevicesForUser(u) || []; devices.map(function(dev) { stored[u][dev.deviceId] = dev; }); @@ -327,8 +331,8 @@ export class DeviceList extends EventEmitter { * * @return {array} All known user IDs */ - getKnownUserIds() { - return Object.keys(this._devices); + public getKnownUserIds(): string[] { + 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 * managed to get a list of devices for this user yet. */ - getStoredDevicesForUser(userId) { - const devs = this._devices[userId]; + public getStoredDevicesForUser(userId: string): DeviceInfo[] | null { + const devs = this.devices[userId]; if (!devs) { return null; } @@ -361,19 +365,19 @@ export class DeviceList extends EventEmitter { * @return {Object} deviceId->{object} devices, or undefined if * there is no data for this user. */ - getRawStoredDevicesForUser(userId) { - return this._devices[userId]; + public getRawStoredDevicesForUser(userId: string): Record { + return this.devices[userId]; } - getStoredCrossSigningForUser(userId) { - if (!this._crossSigningInfo[userId]) return null; + public getStoredCrossSigningForUser(userId: string): CrossSigningInfo { + if (!this.crossSigningInfo[userId]) return null; - return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId); + return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId); } - storeCrossSigningForUser(userId, info) { - this._crossSigningInfo[userId] = info; - this._dirty = true; + public storeCrossSigningForUser(userId: string, info: CrossSigningInfo): void { + this.crossSigningInfo[userId] = info; + this.dirty = true; } /** @@ -385,8 +389,8 @@ export class DeviceList extends EventEmitter { * @return {module:crypto/deviceinfo?} device, or undefined * if we don't know about this device */ - getStoredDevice(userId, deviceId) { - const devs = this._devices[userId]; + public getStoredDevice(userId: string, deviceId: string): DeviceInfo { + const devs = this.devices[userId]; if (!devs || !devs[deviceId]) { return undefined; } @@ -401,7 +405,7 @@ export class DeviceList extends EventEmitter { * * @return {string} user ID */ - getUserByIdentityKey(algorithm, senderKey) { + public getUserByIdentityKey(algorithm: string, senderKey: string): string { if ( algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM @@ -410,7 +414,7 @@ export class DeviceList extends EventEmitter { return null; } - return this._userByIdentityKey[senderKey]; + return this.userByIdentityKey[senderKey]; } /** @@ -421,13 +425,13 @@ export class DeviceList extends EventEmitter { * * @return {module:crypto/deviceinfo?} */ - getDeviceByIdentityKey(algorithm, senderKey) { + public getDeviceByIdentityKey(algorithm: string, senderKey: string): DeviceInfo | null { const userId = this.getUserByIdentityKey(algorithm, senderKey); if (!userId) { return null; } - const devices = this._devices[userId]; + const devices = this.devices[userId]; if (!devices) { return null; } @@ -462,25 +466,25 @@ export class DeviceList extends EventEmitter { * @param {string} u The user ID * @param {Object} devs New device info for user */ - storeDevicesForUser(u, devs) { - // remove previous devices from _userByIdentityKey - if (this._devices[u] !== undefined) { - for (const [deviceId, dev] of Object.entries(this._devices[u])) { + public storeDevicesForUser(u: string, devs: Record): void { + // remove previous devices from userByIdentityKey + if (this.devices[u] !== undefined) { + for (const [deviceId, dev] of Object.entries(this.devices[u])) { 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 for (const [deviceId, dev] of Object.entries(devs)) { 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 */ - startTrackingDeviceList(userId) { + public startTrackingDeviceList(userId: string): void { // 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 // request and we can never update the device lists again (because @@ -503,12 +507,12 @@ export class DeviceList extends EventEmitter { if (typeof userId !== 'string') { 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); - 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 // 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 */ - stopTrackingDeviceList(userId) { - if (this._deviceTrackingStatus[userId]) { + public stopTrackingDeviceList(userId: string): void { + if (this.deviceTrackingStatus[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 // 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 * update. */ - stopTrackingAllDeviceLists() { - for (const userId of Object.keys(this._deviceTrackingStatus)) { - this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; + public stopTrackingAllDeviceLists(): void { + for (const userId of Object.keys(this.deviceTrackingStatus)) { + this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked; } - this._dirty = true; + this.dirty = true; } /** @@ -556,14 +560,14 @@ export class DeviceList extends EventEmitter { * * @param {String} userId */ - invalidateUserDeviceList(userId) { - if (this._deviceTrackingStatus[userId]) { + public invalidateUserDeviceList(userId: string): void { + if (this.deviceTrackingStatus[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 // 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 * is no need to wait for this (it's mostly for the unit tests). */ - refreshOutdatedDeviceLists() { + public refreshOutdatedDeviceLists(): Promise { this.saveIfDirty(); const usersToDownload = []; - for (const userId of Object.keys(this._deviceTrackingStatus)) { - const stat = this._deviceTrackingStatus[userId]; - if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { + for (const userId of Object.keys(this.deviceTrackingStatus)) { + const stat = this.deviceTrackingStatus[userId]; + if (stat == TrackingStatus.PendingDownload) { 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 */ - _setRawStoredDevicesForUser(userId, devices) { - // remove old devices from _userByIdentityKey - if (this._devices[userId] !== undefined) { - for (const [deviceId, dev] of Object.entries(this._devices[userId])) { + public setRawStoredDevicesForUser(userId: string, devices: Record): void { + // remove old devices from userByIdentityKey + if (this.devices[userId] !== undefined) { + for (const [deviceId, dev] of Object.entries(this.devices[userId])) { 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)) { const identityKey = dev.keys['curve25519:'+deviceId]; - this._userByIdentityKey[identityKey] = userId; + this.userByIdentityKey[identityKey] = userId; } } - setRawStoredCrossSigningForUser(userId, info) { - this._crossSigningInfo[userId] = info; + public setRawStoredCrossSigningForUser(userId: string, info: object): void { + this.crossSigningInfo[userId] = info; } /** * Fire off download update requests for the given users, and update the * device list tracking status for them, and the - * _keyDownloadsInProgressByUser map for them. + * keyDownloadsInProgressByUser map for them. * * @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 * users. */ - _doKeyDownload(users) { + private doKeyDownload(users: string[]): Promise { if (users.length === 0) { // nothing to do return Promise.resolve(); } - const prom = this._serialiser.updateDevicesForUsers( - users, this._syncToken, - ).then(() => { + const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => { finished(true); }, (e) => { logger.error( @@ -649,42 +651,41 @@ export class DeviceList extends EventEmitter { }); users.forEach((u) => { - this._keyDownloadsInProgressByUser[u] = prom; - const stat = this._deviceTrackingStatus[u]; - if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { - this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS; + this.keyDownloadsInProgressByUser[u] = prom; + const stat = this.deviceTrackingStatus[u]; + if (stat == TrackingStatus.PendingDownload) { + this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress; } }); const finished = (success) => { - this.emit("crypto.willUpdateDevices", users, !this._hasFetched); + this.emit("crypto.willUpdateDevices", users, !this.hasFetched); users.forEach((u) => { - this._dirty = true; + this.dirty = true; // we may have queued up another download request for this user // since we started this request. If that happens, we should // ignore the completion of the first one. - if (this._keyDownloadsInProgressByUser[u] !== prom) { - logger.log('Another update in the queue for', u, - '- not marking up-to-date'); + if (this.keyDownloadsInProgressByUser[u] !== prom) { + logger.log('Another update in the queue for', u, '- not marking up-to-date'); return; } - delete this._keyDownloadsInProgressByUser[u]; - const stat = this._deviceTrackingStatus[u]; - if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { + delete this.keyDownloadsInProgressByUser[u]; + const stat = this.deviceTrackingStatus[u]; + if (stat == TrackingStatus.DownloadInProgress) { if (success) { // we didn't get any new invalidations since this download started: // 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"); } else { - this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload; } } }); this.saveIfDirty(); - this.emit("crypto.devicesUpdated", users, !this._hasFetched); - this._hasFetched = true; + this.emit("crypto.devicesUpdated", users, !this.hasFetched); + this.hasFetched = true; }; return prom; @@ -701,29 +702,28 @@ export class DeviceList extends EventEmitter { * time (and queuing other requests up). */ class DeviceListUpdateSerialiser { + private downloadInProgress = false; + + // users which are queued for download + // userId -> true + private keyDownloadsQueuedByUser: Record = {}; + + // deferred which is resolved when the queued users are downloaded. + // non-null indicates that we have users queued for download. + private queuedQueryDeferred: IDeferred = null; + + 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 + * @param {object} deviceList The device list object, the device list to be updated */ - 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 - // userId -> true - this._keyDownloadsQueuedByUser = {}; - - // deferred which is resolved when the queued users are downloaded. - // - // non-null indicates that we have users queued for download. - this._queuedQueryDeferred = null; - - this._syncToken = null; // The sync token we send with the requests - } + constructor( + private readonly baseApis: MatrixClient, + private readonly olmDevice: OlmDevice, + private readonly deviceList: DeviceList, + ) {} /** * 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 * users. */ - updateDevicesForUsers(users, syncToken) { + public updateDevicesForUsers(users: string[], syncToken: string): Promise { users.forEach((u) => { - this._keyDownloadsQueuedByUser[u] = true; + this.keyDownloadsQueuedByUser[u] = true; }); - if (!this._queuedQueryDeferred) { - this._queuedQueryDeferred = defer(); + if (!this.queuedQueryDeferred) { + this.queuedQueryDeferred = defer(); } // 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 // 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 logger.log('Queued key download for', users); - return this._queuedQueryDeferred.promise; + return this.queuedQueryDeferred.promise; } // start a new download. - return this._doQueuedQueries(); + return this.doQueuedQueries(); } - _doQueuedQueries() { - if (this._downloadInProgress) { + private doQueuedQueries(): Promise { + if (this.downloadInProgress) { throw new Error( - "DeviceListUpdateSerialiser._doQueuedQueries called with request active", + "DeviceListUpdateSerialiser.doQueuedQueries called with request active", ); } - const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser); - this._keyDownloadsQueuedByUser = {}; - const deferred = this._queuedQueryDeferred; - this._queuedQueryDeferred = null; + const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser); + this.keyDownloadsQueuedByUser = {}; + const deferred = this.queuedQueryDeferred; + this.queuedQueryDeferred = null; logger.log('Starting key download for', downloadUsers); - this._downloadInProgress = true; + this.downloadInProgress = true; - const opts = {}; - if (this._syncToken) { - opts.token = this._syncToken; + const opts: Parameters[1] = {}; + if (this.syncToken) { + opts.token = this.syncToken; } const factories = []; - for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) { - const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize); - factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts)); + for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) { + const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize); + 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 masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {}))); const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {}))); @@ -802,7 +802,7 @@ class DeviceListUpdateSerialiser { for (const userId of downloadUsers) { await sleep(5); try { - await this._processQueryResponseForUser( + await this.processQueryResponseForUser( userId, dk[userId], { master: masterKeys[userId], self_signing: ssks[userId], @@ -818,32 +818,34 @@ class DeviceListUpdateSerialiser { }).then(() => { logger.log('Completed key download for ' + downloadUsers); - this._downloadInProgress = false; + this.downloadInProgress = false; deferred.resolve(); // if we have queued users, fire off another request. - if (this._queuedQueryDeferred) { - this._doQueuedQueries(); + if (this.queuedQueryDeferred) { + this.doQueuedQueries(); } }, (e) => { logger.warn('Error downloading keys for ' + downloadUsers + ':', e); - this._downloadInProgress = false; + this.downloadInProgress = false; deferred.reject(e); }); return deferred.promise; } - async _processQueryResponseForUser( - userId, dkResponse, crossSigningResponse, - ) { + private async processQueryResponseForUser( + userId: string, + dkResponse: object, + crossSigningResponse: any, // TODO types + ): Promise { logger.log('got device keys for ' + userId + ':', dkResponse); logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); { // map from deviceid -> deviceinfo for this user - const userStore = {}; - const devs = this._deviceList.getRawStoredDevicesForUser(userId); + const userStore: Record = {}; + const devs = this.deviceList.getRawStoredDevicesForUser(userId); if (devs) { Object.keys(devs).forEach((deviceId) => { const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); @@ -851,9 +853,9 @@ class DeviceListUpdateSerialiser { }); } - await _updateStoredDeviceKeysForUser( - this._olmDevice, userId, userStore, dkResponse || {}, - this._baseApis.getUserId(), this._baseApis.deviceId, + await updateStoredDeviceKeysForUser( + this.olmDevice, userId, userStore, dkResponse || {}, + this.baseApis.getUserId(), this.baseApis.deviceId, ); // put the updates into the object that will be returned as our results @@ -862,7 +864,7 @@ class DeviceListUpdateSerialiser { storage[deviceId] = userStore[deviceId].toStorage(); }); - this._deviceList._setRawStoredDevicesForUser(userId, storage); + this.deviceList.setRawStoredDevicesForUser(userId, storage); } // now do the same for the cross-signing keys @@ -873,26 +875,31 @@ class DeviceListUpdateSerialiser { && (crossSigningResponse.master || crossSigningResponse.self_signing || crossSigningResponse.user_signing)) { const crossSigning - = this._deviceList.getStoredCrossSigningForUser(userId) + = this.deviceList.getStoredCrossSigningForUser(userId) || new CrossSigningInfo(userId); crossSigning.setKeys(crossSigningResponse); - this._deviceList.setRawStoredCrossSigningForUser( + this.deviceList.setRawStoredCrossSigningForUser( userId, crossSigning.toStorage(), ); // NB. Unlike most events in the js-sdk, this one is internal to the // js-sdk and is not re-emitted - this._deviceList.emit('userCrossSigningUpdated', userId); + this.deviceList.emit('userCrossSigningUpdated', userId); } } } } -async function _updateStoredDeviceKeysForUser( - _olmDevice, userId, userStore, userResult, localUserId, localDeviceId, -) { +async function updateStoredDeviceKeysForUser( + olmDevice: OlmDevice, + userId: string, + userStore: Record, + userResult: object, + localUserId: string, + localDeviceId: string, +): Promise { let updated = false; // remove any devices in the store which aren't in the response @@ -936,7 +943,7 @@ async function _updateStoredDeviceKeysForUser( continue; } - if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) { + if (await storeDeviceKeys(olmDevice, userStore, deviceResult)) { updated = true; } } @@ -949,7 +956,11 @@ async function _updateStoredDeviceKeysForUser( * * 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, + deviceResult: any, // TODO types +): Promise { if (!deviceResult.keys) { // no keys? return false; @@ -961,8 +972,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { const signKeyId = "ed25519:" + deviceId; const signKey = deviceResult.keys[signKeyId]; if (!signKey) { - logger.warn("Device " + userId + ":" + deviceId + - " has no ed25519 key"); + logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key"); return false; } @@ -970,10 +980,9 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { const signatures = deviceResult.signatures || {}; try { - await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); + await olmlib.verifySignature(olmDevice, deviceResult, userId, deviceId, signKey); } catch (e) { - logger.warn("Unable to verify signature on device " + - userId + ":" + deviceId + ":" + e); + logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e); return false; } diff --git a/src/crypto/EncryptionSetup.js b/src/crypto/EncryptionSetup.js index d7f861a03..8da305824 100644 --- a/src/crypto/EncryptionSetup.js +++ b/src/crypto/EncryptionSetup.js @@ -184,7 +184,7 @@ export class EncryptionSetupOperation { }); // 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 if (this._accountData) { diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.ts similarity index 52% rename from src/crypto/RoomList.js rename to src/crypto/RoomList.ts index b00445247..ab653f456 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.ts @@ -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"); 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 { 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 */ export class RoomList { - constructor(cryptoStore) { - this._cryptoStore = cryptoStore; + // Object of roomId -> room e2e info object (body of the m.room.encryption event) + private roomEncryption: Record = {}; - // Object of roomId -> room e2e info object (body of the m.room.encryption event) - this._roomEncryption = {}; - } + constructor(private readonly cryptoStore: CryptoStore) {} - async init() { - await this._cryptoStore.doTxn( + public async init(): Promise { + await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { - this._cryptoStore.getEndToEndRooms(txn, (result) => { - this._roomEncryption = result; + this.cryptoStore.getEndToEndRooms(txn, (result) => { + this.roomEncryption = result; }); }, ); } - getRoomEncryption(roomId) { - return this._roomEncryption[roomId] || null; + public getRoomEncryption(roomId: string): IRoomEncryption { + return this.roomEncryption[roomId] || null; } - isRoomEncrypted(roomId) { + public isRoomEncrypted(roomId: string): boolean { return Boolean(this.getRoomEncryption(roomId)); } - async setRoomEncryption(roomId, roomInfo) { + public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise { // important that this happens before calling into the store // as it prevents the Crypto::setRoomEncryption from calling // this twice for consecutive m.room.encryption events - this._roomEncryption[roomId] = roomInfo; - await this._cryptoStore.doTxn( + this.roomEncryption[roomId] = roomInfo; + await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { - this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); + this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); }, ); } diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index a1ed647f0..f57c61cb1 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter { }; const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key, + sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key, ciphertext: {}, }; await olmlib.ensureOlmSessionsForDevices( - this._baseApis.crypto._olmDevice, + this._baseApis.crypto.olmDevice, this._baseApis, { [sender]: [ @@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter { encryptedContent.ciphertext, this._baseApis.getUserId(), this._baseApis.deviceId, - this._baseApis.crypto._olmDevice, + this._baseApis.crypto.olmDevice, sender, this._baseApis.getStoredDevice(sender, deviceId), payload, @@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter { if (requestControl) { // make sure that the device that sent it is one of the devices that // we requested from - const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey( + const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey( olmlib.OLM_ALGORITHM, event.getSenderKey(), ); diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index eb003f75b..73c3cd536 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -64,16 +64,16 @@ export class DehydrationManager { this.getDehydrationKeyFromCache(); } async getDehydrationKeyFromCache(): Promise { - return await this.crypto._cryptoStore.doTxn( + return await this.crypto.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.crypto._cryptoStore.getSecretStorePrivateKey( + this.crypto.cryptoStore.getSecretStorePrivateKey( txn, async (result) => { if (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); this.key = decodeBase64(decrypted); this.keyInfo = keyInfo; @@ -114,11 +114,11 @@ export class DehydrationManager { this.timeoutId = undefined; } // clear storage - await this.crypto._cryptoStore.doTxn( + await this.crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.crypto._cryptoStore.storeSecretStorePrivateKey( + this.crypto.cryptoStore.storeSecretStorePrivateKey( txn, "dehydration", null, ); }, @@ -158,15 +158,15 @@ export class DehydrationManager { this.timeoutId = undefined; } try { - const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); + const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey); // update the crypto store with the timestamp const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM); - await this.crypto._cryptoStore.doTxn( + await this.crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.crypto._cryptoStore.storeSecretStorePrivateKey( + this.crypto.cryptoStore.storeSecretStorePrivateKey( txn, "dehydration", { keyInfo: this.keyInfo, @@ -205,7 +205,7 @@ export class DehydrationManager { } logger.log("Uploading account to server"); - const dehydrateResult = await this.crypto._baseApis.http.authedRequest( + const dehydrateResult = await this.crypto.baseApis.http.authedRequest( undefined, "PUT", "/dehydrated_device", @@ -223,9 +223,9 @@ export class DehydrationManager { const deviceId = dehydrateResult.device_id; logger.log("Preparing device keys", deviceId); const deviceKeys: DeviceKeys = { - algorithms: this.crypto._supportedAlgorithms, + algorithms: this.crypto.supportedAlgorithms, device_id: deviceId, - user_id: this.crypto._userId, + user_id: this.crypto.userId, keys: { [`ed25519:${deviceId}`]: e2eKeys.ed25519, [`curve25519:${deviceId}`]: e2eKeys.curve25519, @@ -233,12 +233,12 @@ export class DehydrationManager { }; const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); deviceKeys.signatures = { - [this.crypto._userId]: { + [this.crypto.userId]: { [`ed25519:${deviceId}`]: deviceSignature, }, }; - if (this.crypto._crossSigningInfo.getId("self_signing")) { - await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); + if (this.crypto.crossSigningInfo.getId("self_signing")) { + await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing"); } logger.log("Preparing one-time keys"); @@ -247,7 +247,7 @@ export class DehydrationManager { const k: OneTimeKey = { key }; const signature = account.sign(anotherjson.stringify(k)); k.signatures = { - [this.crypto._userId]: { + [this.crypto.userId]: { [`ed25519:${deviceId}`]: signature, }, }; @@ -260,7 +260,7 @@ export class DehydrationManager { const k: OneTimeKey = { key, fallback: true }; const signature = account.sign(anotherjson.stringify(k)); k.signatures = { - [this.crypto._userId]: { + [this.crypto.userId]: { [`ed25519:${deviceId}`]: signature, }, }; @@ -268,7 +268,7 @@ export class DehydrationManager { } logger.log("Uploading keys to server"); - await this.crypto._baseApis.http.authedRequest( + await this.crypto.baseApis.http.authedRequest( undefined, "POST", "/keys/upload/" + encodeURI(deviceId), diff --git a/src/crypto/deviceinfo.js b/src/crypto/deviceinfo.js deleted file mode 100644 index 379d72c63..000000000 --- a/src/crypto/deviceinfo.js +++ /dev/null @@ -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.} keys a map from - * <key type>:<id> -> <base64-encoded key>> - * - * @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; - diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts new file mode 100644 index 000000000..d723eac4b --- /dev/null +++ b/src/crypto/deviceinfo.ts @@ -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; + algorithms: string[]; + verified: DeviceVerification; + known: boolean; + unsigned?: Record; + signatures?: Record; +} + +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.} keys a map from + * <key type>:<id> -> <base64-encoded key>> + * + * @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 = {}; + public verified = DeviceVerification.Unverified; + public known = false; + public unsigned: Record = {}; + public signatures: Record = {}; + + 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; + } +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 205a21952..038708f86 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -29,7 +29,7 @@ import { logger } from '../logger'; import { OlmDevice } from "./OlmDevice"; import * as olmlib from "./olmlib"; import { DeviceList } from "./DeviceList"; -import { DeviceInfo } from "./deviceinfo"; +import { DeviceInfo, IDevice } from "./deviceinfo"; import * as algorithms from "./algorithms"; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { EncryptionSetupBuilder } from "./EncryptionSetup"; @@ -420,9 +420,7 @@ export class Crypto extends EventEmitter { }; myDevices[this.deviceId] = deviceInfo; - this.deviceList.storeDevicesForUser( - this.userId, myDevices, - ); + this.deviceList.storeDevicesForUser(this.userId, myDevices); this.deviceList.saveIfDirty(); } @@ -922,10 +920,7 @@ export class Crypto extends EventEmitter { await this.crossSigningInfo.getCrossSigningKeysFromCache(); // This is writing to in-memory account data in // builder.accountDataClientAdapter so won't fail - await CrossSigningInfo.storeInSecretStorage( - crossSigningPrivateKeys, - secretStorage, - ); + await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); } if (setupNewKeyBackup && !keyBackupInfo) { @@ -1172,7 +1167,7 @@ export class Crypto extends EventEmitter { // FIXME: do this in batches const users = {}; for (const [userId, crossSigningInfo] - of Object.entries(this.deviceList._crossSigningInfo)) { + of Object.entries(this.deviceList.crossSigningInfo)) { const upgradeInfo = await this.checkForDeviceVerificationUpgrade( userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), ); @@ -1248,7 +1243,7 @@ export class Crypto extends EventEmitter { private async checkForValidDeviceSignature( userId: string, key: any, // TODO types - devices: Record, + devices: Record, ): Promise { const deviceIds: string[] = []; if (devices && key.signatures && key.signatures[userId]) { @@ -1934,7 +1929,7 @@ export class Crypto extends EventEmitter { public downloadKeys( userIds: string[], forceDownload?: boolean, - ): Promise>> { + ): Promise>> { return this.deviceList.downloadKeys(userIds, forceDownload); } @@ -2003,7 +1998,7 @@ export class Crypto extends EventEmitter { verified?: boolean, blocked?: boolean, known?: boolean, - ): Promise { + ): Promise { // get rid of any `undefined`s here so we can just check // for null rather than null or undefined 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 // (we could do local echo to speed things up) } - return device; + return device as any; // TODO types } else { return xsk; } diff --git a/src/crypto/key_passphrase.js b/src/crypto/key_passphrase.ts similarity index 76% rename from src/crypto/key_passphrase.js rename to src/crypto/key_passphrase.ts index 84384cc4e..3631fe30f 100644 --- a/src/crypto/key_passphrase.js +++ b/src/crypto/key_passphrase.ts @@ -1,6 +1,5 @@ /* -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2018 - 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. @@ -21,7 +20,21 @@ const DEFAULT_ITERATIONS = 500000; 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 { if (!global.Olm) { 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 { if (!global.Olm) { throw new Error("Olm is not available"); } @@ -52,7 +65,12 @@ export async function keyFromPassphrase(password) { 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 { const subtleCrypto = global.crypto.subtle; const TextEncoder = global.TextEncoder; if (!subtleCrypto || !TextEncoder) { diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index c2fe3a1ce..8376245cd 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -31,17 +31,22 @@ export interface IKeyBackupRoomSessions { [sessionId: string]: IKeyBackupSession; } +/* eslint-disable camelcase */ export interface IKeyBackupVersion { algorithm: string; - auth_data: { // eslint-disable-line camelcase - public_key: string; // eslint-disable-line camelcase + auth_data: { + public_key: string; signatures: ISignatures; + private_key_salt: string; + private_key_iterations: number; + private_key_bits?: number; }; count: number; etag: string; version: string; // number contained within - recovery_key: string; // eslint-disable-line camelcase + recovery_key: string; } +/* eslint-enable camelcase */ export interface IKeyBackupPrepareOpts { secureSecretStorage: boolean; diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index 2ac17cc88..4ca01bd11 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -292,7 +292,7 @@ export class VerificationBase extends EventEmitter { await verifier(keyId, device, keyInfo); verifiedDevices.push(deviceId); } else { - const crossSigningInfo = this._baseApis.crypto._deviceList + const crossSigningInfo = this._baseApis.crypto.deviceList .getStoredCrossSigningForUser(userId); if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { await verifier(keyId, DeviceInfo.fromStorage({ diff --git a/src/matrix.ts b/src/matrix.ts index c9192f2ea..2cce6867a 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -19,6 +19,7 @@ import { MemoryStore } from "./store/memory"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient } from "./client"; import { ICreateClientOpts } from "./client"; +import { DeviceTrustLevel } from "./crypto/CrossSigning"; export * from "./client"; export * from "./http-api"; @@ -99,7 +100,7 @@ export function setCryptoStoreFactory(fac) { } export interface ICryptoCallbacks { - getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise; + getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; saveCrossSigningKeys?: (keys: Record) => void; shouldUpgradeDeviceVerifications?: ( users: Record @@ -112,7 +113,7 @@ export interface ICryptoCallbacks { ) => void; onSecretRequested?: ( userId: string, deviceId: string, - requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel + requestId: string, secretName: string, deviceTrust: DeviceTrustLevel ) => Promise; getDehydrationKey?: ( keyInfo: ISecretStorageKeyInfo, @@ -132,14 +133,6 @@ export interface ISecretStorageKeyInfo { 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} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. diff --git a/src/utils.ts b/src/utils.ts index 14207ebca..587d9a7f9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -435,12 +435,18 @@ export function isNullOrUndefined(val: any): boolean { return val === null || val === undefined; } +export interface IDeferred { + resolve: (value: T) => void; + reject: (any) => void; + promise: Promise; +} + // Returns a Deferred -export function defer() { +export function defer(): IDeferred { let resolve; let reject; - const promise = new Promise((_resolve, _reject) => { + const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; });