Handle secret pushing for key backups (#5189)
* push backup key to other verified devices when we reset backup * handle receiving pushed backup keys - make sure that backup gets enabled after we receive a pushed key that matches the current, valid backup * apply requested changes from review
This commit is contained in:
+1
-1
@@ -48,7 +48,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
|
||||
Generated
+5
-5
@@ -19,8 +19,8 @@ importers:
|
||||
specifier: ^7.12.5
|
||||
version: 7.29.2
|
||||
'@matrix-org/matrix-sdk-crypto-wasm':
|
||||
specifier: ^18.0.0
|
||||
version: 18.0.0
|
||||
specifier: ^18.1.0
|
||||
version: 18.1.0
|
||||
another-json:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@@ -1071,8 +1071,8 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.0.0':
|
||||
resolution: {integrity: sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==}
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.1.0':
|
||||
resolution: {integrity: sha512-GxXK2U39+2qWNvR3fXJY7nxdikvpiT17RaS0/Dktk6R8FMKDk3vm79Hq65yrCWLBmT7pJZoerfILNZqhrcUHrg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@matrix-org/olm@3.2.15':
|
||||
@@ -5019,7 +5019,7 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.0.0': {}
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.1.0': {}
|
||||
|
||||
'@matrix-org/olm@3.2.15': {}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
encryptMegolmEventRawPlainText,
|
||||
encryptOlmEvent,
|
||||
establishOlmSession,
|
||||
getTestOlmAccountKeys,
|
||||
expectSendRoomKey,
|
||||
@@ -2315,4 +2316,107 @@ describe("crypto", () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
describe("secret pushing", () => {
|
||||
it("should push a new backup key when a new backup key is set", async () => {
|
||||
// setup: alice has another device, DEVICE_ID, which is verified
|
||||
const crypto = aliceClient.getCrypto()!;
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
|
||||
await startClientAndAwaitFirstSync();
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
|
||||
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
|
||||
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
|
||||
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
|
||||
|
||||
// when we set a new backup key
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 200,
|
||||
body: { version: "1" },
|
||||
});
|
||||
const secretPushPromise = new Promise<any>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
await crypto.resetKeyBackup();
|
||||
|
||||
// we expect the other device to get a secret push
|
||||
const content = await secretPushPromise;
|
||||
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
|
||||
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
|
||||
const olmSession = new Olm.Session();
|
||||
olmSession.create_inbound(testOlmAccount, ciphertext.body);
|
||||
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
|
||||
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
|
||||
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
|
||||
});
|
||||
|
||||
it("should receive pushed backup key", async () => {
|
||||
// setup: alice has another device, DEVICE_ID, which is verified,
|
||||
// and has a key backup set up and signed by DEVICE_ID
|
||||
const crypto = aliceClient.getCrypto()!;
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
await startClientAndAwaitFirstSync();
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
|
||||
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
|
||||
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
|
||||
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
|
||||
|
||||
// after we push the backup key to alice...
|
||||
|
||||
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const secretPush = encryptOlmEvent({
|
||||
sender: "@alice:localhost",
|
||||
senderKey: senderIdentityKeys.curve25519,
|
||||
senderSigningKey: senderIdentityKeys.ed25519,
|
||||
p2pSession,
|
||||
recipient: "@alice:localhost",
|
||||
recipientCurve25519Key: aliceDeviceKeys.curve25519,
|
||||
recipientEd25519Key: aliceDeviceKeys.ed25519,
|
||||
plaincontent: {
|
||||
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
|
||||
name: "m.megolm_backup.v1",
|
||||
},
|
||||
plaintype: "io.element.msc4385.secret.push",
|
||||
});
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [secretPush],
|
||||
},
|
||||
};
|
||||
|
||||
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
|
||||
});
|
||||
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// alice should be using backup now
|
||||
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
|
||||
await keyBackupEnabledPromise;
|
||||
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -790,6 +790,7 @@ describe("RustCrypto", () => {
|
||||
undefined,
|
||||
secretStorage,
|
||||
);
|
||||
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
|
||||
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
@@ -837,6 +838,7 @@ describe("RustCrypto", () => {
|
||||
{} as CryptoCallbacks,
|
||||
false,
|
||||
);
|
||||
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
|
||||
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
@@ -2324,6 +2326,7 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
|
||||
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
|
||||
|
||||
// We have a key backup
|
||||
await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull());
|
||||
|
||||
@@ -161,7 +161,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
/**
|
||||
* Handles a backup secret received event and store it if it matches the current backup version.
|
||||
*
|
||||
* @param secret - The secret as received from a `m.secret.send` event for secret `m.megolm_backup.v1`.
|
||||
* @param secret - The secret as received from a `m.secret.send` or `io.element.msc4385.secret.push` event for secret `m.megolm_backup.v1`.
|
||||
* @returns true if the secret is valid and has been stored, false otherwise.
|
||||
*/
|
||||
public async handleBackupSecretReceived(secret: string): Promise<boolean> {
|
||||
@@ -206,6 +206,15 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
`handleBackupSecretReceived: Valid decryption key for the current server-side backup version (${latestBackupInfo.version}) received`,
|
||||
);
|
||||
await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version);
|
||||
// Check if the backup should be enabled (e.g. if it's properly
|
||||
// signed), and enable it if it should
|
||||
if (this.keyBackupCheckInProgress) {
|
||||
await this.keyBackupCheckInProgress;
|
||||
}
|
||||
this.keyBackupCheckInProgress = this.doCheckKeyBackup(latestBackupInfo).finally(() => {
|
||||
this.keyBackupCheckInProgress = null;
|
||||
});
|
||||
await this.keyBackupCheckInProgress;
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.logger.warn("handleBackupSecretReceived: Unable to validate backup decryption key", e);
|
||||
@@ -288,12 +297,17 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
|
||||
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
|
||||
|
||||
/** Helper for `checkKeyBackup` */
|
||||
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> {
|
||||
/** Helper to check the key backup status, and enable/disable it as appropriate
|
||||
*
|
||||
* A KeyBackupInfo can be passed if it was fetched recently, to avoid trying to
|
||||
* re-fetch it from the server.
|
||||
*/
|
||||
private async doCheckKeyBackup(backupInfo?: KeyBackupInfo | null | undefined): Promise<KeyBackupCheck | null> {
|
||||
this.logger.debug("Checking key backup status...");
|
||||
let backupInfo: KeyBackupInfo | null | undefined;
|
||||
try {
|
||||
backupInfo = await this.requestKeyBackupVersion();
|
||||
if (!backupInfo) {
|
||||
backupInfo = await this.requestKeyBackupVersion();
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn("Error checking for active key backup", e);
|
||||
this.serverBackupInfo = undefined;
|
||||
|
||||
@@ -1376,6 +1376,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
public async resetKeyBackup(): Promise<void> {
|
||||
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
|
||||
|
||||
await this.pushSecretToVerifiedDevices("m.megolm_backup.v1");
|
||||
|
||||
// we want to store the private key in 4S
|
||||
// need to check if 4S is set up?
|
||||
if (await this.secretStorageHasAESKey()) {
|
||||
@@ -2104,9 +2106,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/**
|
||||
* Handles secret received from the rust secret inbox.
|
||||
*
|
||||
* The gossipped secrets are received using the `m.secret.send` event type
|
||||
* and are guaranteed to have been received over a 1-to-1 Olm
|
||||
* Session from a verified device.
|
||||
* The gossipped secrets are received using the `m.secret.send` or
|
||||
* `io.element.msc4385.secret.push` event types and are guaranteed to have
|
||||
* been received over a 1-to-1 Olm Session from a verified device.
|
||||
*
|
||||
* The only secret currently handled in this way is `m.megolm_backup.v1`.
|
||||
*
|
||||
@@ -2254,6 +2256,18 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
| undefined;
|
||||
return identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a secret to all of the current user's verified devices.
|
||||
*/
|
||||
public async pushSecretToVerifiedDevices(name: string): Promise<void> {
|
||||
const logger = new LogSpan(this.logger, "pushSecretToVerifiedDevices");
|
||||
await this.keyClaimManager.ensureSessionsForUsers(logger, [new RustSdkCryptoJs.UserId(this.userId)]);
|
||||
await this.olmMachine.pushSecretToVerifiedDevices(name);
|
||||
this.outgoingRequestsManager.doProcessOutgoingRequests().catch((e) => {
|
||||
logger.warn("pushSecretToVerifiedDevices: Error processing outgoing requests", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class EventDecryptor {
|
||||
|
||||
Reference in New Issue
Block a user