1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-05 00:42:10 +03:00

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ describe("Crypto", function() {
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto._deviceList.getDeviceByIdentityKey = () => device;
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
@@ -213,7 +213,7 @@ describe("Crypto", function() {
async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent();
const key = await aliceClient.crypto._olmDevice
const key = await aliceClient.crypto.olmDevice
.getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id,
index,
@@ -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);

View File

@@ -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: {

View File

@@ -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(),
);

View File

@@ -64,8 +64,8 @@ describe("Cross Signing", function() {
);
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
await olmlib.verifySignature(
alice.crypto._olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
);
});
alice.uploadKeySignatures = async () => {};
@@ -138,7 +138,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -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);
});

View File

@@ -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", {});

View File

@@ -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",

View File

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

View File

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

View File

@@ -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<Record<string, Record<string, DeviceInfo>>> {
): Promise<Record<string, Record<string, IDevice>>> {
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<module:crypto/deviceinfo?>}
*/
public getEventSenderDeviceInfo(event: MatrixEvent): Promise<DeviceInfo> {
public async getEventSenderDeviceInfo(event: MatrixEvent): Promise<DeviceInfo> {
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<IKeyBackupRestoreResult> {
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(

View File

@@ -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<Uint8Array>;
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
}
export class CrossSigningInfo extends EventEmitter {
public keys: Record<string, any> = {}; // TODO types
public firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverified later for any reason.
private crossSigningVerifiedBefore = false;
/**
* Information about a user's cross-signing keys
*
@@ -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<Record<string, object>> {
// 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<string, Uint8Array>,
secretStorage: SecretStorage,
): Promise<void> {
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<Uint8Array> {
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<boolean> {
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<Map<string, Uint8Array>> {
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<void> {
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<string, Uint8Array> = {};
const keys: Record<string, object> = {};
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<string, any>): void {
const signingKeys: Record<string, object> = {};
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<T extends object>(data: T, type: string): Promise<T> {
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<object> {
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<object> {
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<Uint8Array> {
const key = await new Promise<any>((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<void> {
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<void>(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.");
});
}

View File

@@ -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<string, Record<string, IDevice>>;
/**
* @alias module:crypto/DeviceList
*/
export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
super();
this._cryptoStore = cryptoStore;
// userId -> {
// deviceId -> {
// [device info]
// }
// }
this._devices = {};
private devices: DeviceInfoMap = {};
// userId -> {
// [key info]
// }
this._crossSigningInfo = {};
public crossSigningInfo: Record<string, object> = {};
// map of identity keys to the user who owns it
this._userByIdentityKey = {};
private userByIdentityKey: Record<string, string> = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = {}; // loaded from storage in load()
private deviceTrackingStatus: Record<string, TrackingStatus> = {}; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was writen,
// The 'next_batch' sync token at the point the data was written,
// ie. a token representing the point immediately after the
// moment represented by the snapshot in the db.
this._syncToken = null;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, olmDevice, this,
);
private syncToken: string = null;
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// Maximum number of user IDs per request to prevent server overload (#1619)
this._keyDownloadChunkSize = keyDownloadChunkSize;
private keyDownloadsInProgressByUser: Record<string, Promise<void>> = {};
// Set whenever changes are made other than setting the sync token
this._dirty = false;
private dirty = false;
// Promise resolved when device data is saved
this._savePromise = null;
private savePromise: Promise<boolean> = null;
// Function that resolves the save promise
this._resolveSavePromise = null;
private resolveSavePromise: (saved: boolean) => void = null;
// The time the save is scheduled for
this._savePromiseTime = null;
private savePromiseTime: number = null;
// The timer used to delay the save
this._saveTimer = null;
private saveTimer: NodeJS.Timeout = null;
// True if we have fetched data from the server or loaded a non-empty
// set of device data from the store
this._hasFetched = null;
private hasFetched: boolean = null;
private readonly serialiser: DeviceListUpdateSerialiser;
constructor(
baseApis: MatrixClient,
private readonly cryptoStore: CryptoStore,
olmDevice: OlmDevice,
// Maximum number of user IDs per request to prevent server overload (#1619)
public readonly keyDownloadChunkSize = 250,
) {
super();
this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this);
}
/**
* Load the device tracking state from storage
*/
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<bool>} true if the data was saved, false if
* @return {Promise<boolean>} true if the data was saved, false if
* it was not (eg. because no changes were pending). The promise
* 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<boolean> {
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<DeviceInfoMap> {
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<string, IDevice> {
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<string, IDevice>): 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<void> {
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<string, IDevice>): 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<void> {
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 {
/*
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object
*/
constructor(baseApis, olmDevice, deviceList) {
this._baseApis = baseApis;
this._olmDevice = olmDevice;
this._deviceList = deviceList; // the device list to be updated
this._downloadInProgress = false;
private downloadInProgress = false;
// users which are queued for download
// userId -> true
this._keyDownloadsQueuedByUser = {};
private keyDownloadsQueuedByUser: Record<string, boolean> = {};
// deferred which is resolved when the queued users are downloaded.
//
// non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null;
private queuedQueryDeferred: IDeferred<void> = null;
this._syncToken = null; // The sync token we send with the requests
}
private syncToken: string = null; // The sync token we send with the requests
/*
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object, the device list to be updated
*/
constructor(
private readonly baseApis: MatrixClient,
private readonly olmDevice: OlmDevice,
private readonly deviceList: DeviceList,
) {}
/**
* Make a key query request for the given users
@@ -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<void> {
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<void> {
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<MatrixClient["downloadKeysForUsers"]>[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<void> {
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<string, DeviceInfo> = {};
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<string, DeviceInfo>,
userResult: object,
localUserId: string,
localDeviceId: string,
): Promise<boolean> {
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<string, DeviceInfo>,
deviceResult: any, // TODO types
): Promise<boolean> {
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;
}

View File

@@ -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) {

View File

@@ -1,5 +1,5 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
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)
this._roomEncryption = {};
}
private roomEncryption: Record<string, IRoomEncryption> = {};
async init() {
await this._cryptoStore.doTxn(
constructor(private readonly cryptoStore: CryptoStore) {}
public async init(): Promise<void> {
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<void> {
// 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);
},
);
}

View File

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

View File

@@ -64,16 +64,16 @@ export class DehydrationManager {
this.getDehydrationKeyFromCache();
}
async getDehydrationKeyFromCache(): Promise<void> {
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),

View File

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

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

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

View File

@@ -29,7 +29,7 @@ import { logger } from '../logger';
import { OlmDevice } from "./OlmDevice";
import * 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<string, DeviceInfo>,
devices: Record<string, IDevice>,
): Promise<string[]> {
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<Record<string, Record<string, DeviceInfo>>> {
): Promise<Record<string, Record<string, IDevice>>> {
return this.deviceList.downloadKeys(userIds, forceDownload);
}
@@ -2003,7 +1998,7 @@ export class Crypto extends EventEmitter {
verified?: boolean,
blocked?: boolean,
known?: boolean,
): Promise<DeviceInfo> {
): Promise<DeviceInfo | CrossSigningInfo> {
// 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;
}

View File

@@ -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<Uint8Array> {
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<IKey> {
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<Uint8Array> {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {

View File

@@ -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;

View File

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

View File

@@ -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<Uint8Array>;
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
@@ -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<string>;
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.

View File

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