Skip to content

Commit e99267d

Browse files
committed
handle secret pushing on for key backups
- push backup key when we reset backup - handle pushed backup key
1 parent 327d2fa commit e99267d

File tree

5 files changed

+140
-5
lines changed

5 files changed

+140
-5
lines changed

spec/integ/crypto/crypto.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
createOlmAccount,
8585
createOlmSession,
8686
encryptGroupSessionKey,
87+
encryptOlmEvent,
8788
encryptMegolmEvent,
8889
encryptMegolmEventRawPlainText,
8990
establishOlmSession,
@@ -2311,4 +2312,107 @@ describe("crypto", () => {
23112312
);
23122313
}
23132314
});
2315+
2316+
describe("secret pushing", () => {
2317+
it("should push a new backup key when a new backup key is set", async () => {
2318+
// setup: alice has another device, DEVICE_ID, which is verified
2319+
const crypto = aliceClient.getCrypto()!;
2320+
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
2321+
await startClientAndAwaitFirstSync();
2322+
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
2323+
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
2324+
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
2325+
2326+
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
2327+
2328+
// when we set a new backup key
2329+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
2330+
status: 404,
2331+
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
2332+
});
2333+
fetchMock.post("path:/_matrix/client/v3/room_keys/version", {
2334+
status: 200,
2335+
body: { version: "1" },
2336+
});
2337+
const secretPushPromise = new Promise<any>((resolve) => {
2338+
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
2339+
const content = JSON.parse(callLog.options.body as string);
2340+
resolve(content);
2341+
return {};
2342+
});
2343+
});
2344+
2345+
await crypto.resetKeyBackup();
2346+
2347+
// we expect the other device to get a secret push
2348+
const content = await secretPushPromise;
2349+
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
2350+
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
2351+
const olmSession = new Olm.Session();
2352+
olmSession.create_inbound(testOlmAccount, ciphertext.body);
2353+
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
2354+
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
2355+
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
2356+
});
2357+
2358+
it("should receive pushed backup key", async () => {
2359+
// setup: alice has another device, DEVICE_ID, which is verified,
2360+
// and has a key backup set up and signed by DEVICE_ID
2361+
const crypto = aliceClient.getCrypto()!;
2362+
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
2363+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
2364+
await startClientAndAwaitFirstSync();
2365+
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
2366+
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
2367+
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
2368+
2369+
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
2370+
2371+
// after we push the backup key to alice...
2372+
2373+
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
2374+
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
2375+
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
2376+
const secretPush = encryptOlmEvent({
2377+
sender: "@alice:localhost",
2378+
senderKey: senderIdentityKeys.curve25519,
2379+
senderSigningKey: senderIdentityKeys.ed25519,
2380+
p2pSession,
2381+
recipient: "@alice:localhost",
2382+
recipientCurve25519Key: aliceDeviceKeys.curve25519,
2383+
recipientEd25519Key: aliceDeviceKeys.ed25519,
2384+
plaincontent: {
2385+
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
2386+
name: "m.megolm_backup.v1",
2387+
},
2388+
plaintype: "io.element.msc4385.secret.push",
2389+
});
2390+
2391+
const syncResponse = {
2392+
next_batch: 1,
2393+
to_device: {
2394+
events: [secretPush],
2395+
},
2396+
};
2397+
2398+
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
2399+
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
2400+
});
2401+
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
2402+
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
2403+
if (enabled) {
2404+
resolve();
2405+
}
2406+
});
2407+
});
2408+
2409+
syncResponder.sendOrQueueSyncResponse(syncResponse);
2410+
await syncPromise(aliceClient);
2411+
2412+
// alice should be using backup now
2413+
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
2414+
await keyBackupEnabledPromise;
2415+
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
2416+
});
2417+
});
23142418
});

spec/unit/rust-crypto/rust-crypto.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,11 @@ describe("initRustCrypto", () => {
9898
return {
9999
registerRoomKeyUpdatedCallback: vi.fn(),
100100
registerUserIdentityUpdatedCallback: vi.fn(),
101-
getSecretsFromInbox: vi.fn().mockResolvedValue([]),
101+
getPushedSecretsFromInbox: vi.fn().mockResolvedValue(new Set()),
102+
getSecretsFromInbox: vi.fn().mockResolvedValue(new Set()),
103+
deletePushedSecretsFromInbox: vi.fn(),
102104
deleteSecretsFromInbox: vi.fn(),
105+
registerReceivePushedSecretCallback: vi.fn(),
103106
registerReceiveSecretCallback: vi.fn(),
104107
registerDevicesUpdatedCallback: vi.fn(),
105108
registerRoomKeysWithheldCallback: vi.fn(),
@@ -788,6 +791,7 @@ describe("RustCrypto", () => {
788791
undefined,
789792
secretStorage,
790793
);
794+
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
791795

792796
async function createSecretStorageKey() {
793797
return {
@@ -835,6 +839,7 @@ describe("RustCrypto", () => {
835839
{} as CryptoCallbacks,
836840
false,
837841
);
842+
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
838843

839844
async function createSecretStorageKey() {
840845
return {
@@ -2320,6 +2325,7 @@ describe("RustCrypto", () => {
23202325
});
23212326

23222327
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
2328+
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
23232329

23242330
// We have a key backup
23252331
await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull());

src/rust-crypto/backup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
161161
/**
162162
* Handles a backup secret received event and store it if it matches the current backup version.
163163
*
164-
* @param secret - The secret as received from a `m.secret.send` event for secret `m.megolm_backup.v1`.
164+
* @param secret - The secret as received from a `m.secret.send` or `io.element.msc4385.secret.push` event for secret `m.megolm_backup.v1`.
165165
* @returns true if the secret is valid and has been stored, false otherwise.
166166
*/
167167
public async handleBackupSecretReceived(secret: string): Promise<boolean> {

src/rust-crypto/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ async function initOlmMachine(
206206
// Once we have all the values, we can safely clear the secret inbox.
207207
rustCrypto.checkSecrets(name),
208208
);
209+
// Register a callback to be notified when a new secret is pushed to us, as for now only the key backup secret is supported (if the cross-signing secrets change, we should re-verify)
210+
await olmMachine.registerReceivePushedSecretCallback(
211+
async (name: string, _value: string) =>
212+
// Instead of directly checking the secret value, we poll the inbox to get all values for that secret type.
213+
// Once we have all the values, we can safely clear the secret inbox.
214+
await rustCrypto.checkSecrets(name),
215+
);
209216

210217
// Tell the OlmMachine to think about its outgoing requests before we hand control back to the application.
211218
//

src/rust-crypto/rust-crypto.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
13681368
public async resetKeyBackup(): Promise<void> {
13691369
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
13701370

1371+
await this.pushSecretToVerifiedDevices("m.megolm_backup.v1");
1372+
13711373
// we want to store the private key in 4S
13721374
// need to check if 4S is set up?
13731375
if (await this.secretStorageHasAESKey()) {
@@ -2035,9 +2037,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
20352037
/**
20362038
* Handles secret received from the rust secret inbox.
20372039
*
2038-
* The gossipped secrets are received using the `m.secret.send` event type
2039-
* and are guaranteed to have been received over a 1-to-1 Olm
2040-
* Session from a verified device.
2040+
* The gossipped secrets are received using the `m.secret.send` or
2041+
* `io.element.msc4385.secret.push` event types and are guaranteed to have
2042+
* been received over a 1-to-1 Olm Session from a verified device.
20412043
*
20422044
* The only secret currently handled in this way is `m.megolm_backup.v1`.
20432045
*
@@ -2065,6 +2067,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
20652067
*/
20662068
public async checkSecrets(name: string): Promise<void> {
20672069
const pendingValues: Set<string> = await this.olmMachine.getSecretsFromInbox(name);
2070+
(await this.olmMachine.getPushedSecretsFromInbox(name)).forEach((value: string) => pendingValues.add(value));
20682071
for (const value of pendingValues) {
20692072
if (await this.handleSecretReceived(name, value)) {
20702073
// If we have a valid secret for that name there is no point of processing the other secrets values.
@@ -2075,6 +2078,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
20752078

20762079
// Important to call this after handling the secrets as good hygiene.
20772080
await this.olmMachine.deleteSecretsFromInbox(name);
2081+
await this.olmMachine.deletePushedSecretsFromInbox(name);
20782082
}
20792083

20802084
/**
@@ -2182,6 +2186,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
21822186
public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> {
21832187
return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
21842188
}
2189+
2190+
/**
2191+
* Push a secret to all of the current user's verified devices.
2192+
*
2193+
* <strong>This method is experimental and may change.</strong>
2194+
*/
2195+
public async pushSecretToVerifiedDevices(name: string): Promise<void> {
2196+
const logger = new LogSpan(this.logger, "pushSecretToVerifiedDevices");
2197+
await this.keyClaimManager.ensureSessionsForUsers(logger, [new RustSdkCryptoJs.UserId(this.userId)]);
2198+
await this.olmMachine.pushSecretToVerifiedDevices(name);
2199+
this.outgoingRequestsManager.doProcessOutgoingRequests().catch((e) => {
2200+
this.logger.warn("onKeyVerificationRequest: Error processing outgoing requests", e);
2201+
});
2202+
}
21852203
}
21862204

21872205
class EventDecryptor {

0 commit comments

Comments
 (0)