Skip to content

Commit

Permalink
Add CryptoApi.getActiveSessionBackupVersion() (#3555)
Browse files Browse the repository at this point in the history
* stub backupmanager

* Implement `CryptoApi.getActiveSessionBackupVersion`

* Revert unnecessary change

we can do this later, once we have better test coverage

* more test coverage

---------

Co-authored-by: Richard van der Hoff <[email protected]>
  • Loading branch information
BillCarsonFr and richvdh committed Jul 28, 2023
1 parent 83d447a commit 6d28154
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 6 deletions.
76 changes: 70 additions & 6 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";

import { IKeyBackupSession } from "../../../src/crypto/keybackup";
import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
Expand Down Expand Up @@ -151,13 +151,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// start after saving the private key
await aliceClient.startClient();

// Persuade alice to fetch the device list. Completing the initial sync will make the device list download
// outdated device lists (of which our own user will be one).
syncResponder.sendOrQueueSyncResponse({});
await jest.advanceTimersByTimeAsync(10); // DeviceList has a sleep(5) which we need to make happen

// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.checkKeyBackup();

Expand All @@ -170,4 +166,72 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual("testytest");
});

oldBackendOnly("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);

aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();

// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.checkKeyBackup();

// At this point there is no backup
let backupStatus: string | null;
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();

// Serve a backup with no trusted signature
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});

const checked = await aliceClient.checkKeyBackup();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
expect(checked?.trustInfo?.usable).toBeFalsy();

backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();

// Add a valid signature to the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});

// check that signaling is working
const backupPromise = new Promise<void>((resolve, reject) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});

const validCheck = await aliceClient.checkKeyBackup();
expect(validCheck?.trustInfo?.usable).toStrictEqual(true);

await backupPromise;

backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
});

/** make sure that the client knows about the dummy device */
async function waitForDeviceList(): Promise<void> {
// Completing the initial sync will make the device list download outdated device lists (of which our own
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);

// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
}
});
7 changes: 7 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,13 @@ describe("RustCrypto", () => {
expect(new TextDecoder().decode(fetched!)).toEqual(key);
});
});

describe("getActiveSessionBackupVersion", () => {
it("returns null", async () => {
const rustCrypto = await makeTestRustCrypto();
expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull();
});
});
});

/** build a basic RustCrypto instance for testing
Expand Down
7 changes: 7 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3294,6 +3294,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns true if the client is configured to back up keys to
* the server, otherwise false. If we haven't completed a successful check
* of key backup status yet, returns null.
*
* @deprecated Prefer direct access to {@link CryptoApi.getActiveSessionBackupVersion}:
*
* ```javascript
* let enabled = (await client.getCrypto().getActiveSessionBackupVersion()) !== null;
* ```
*/
public getKeyBackupEnabled(): boolean | null {
if (!this.crypto) {
Expand Down Expand Up @@ -3436,6 +3442,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeyBackupVersion
// so this is symmetrical).
// TODO: convert this to use crypto.getActiveSessionBackupVersion. And actually check the version.
if (this.crypto.backupManager.version) {
this.crypto.backupManager.disableKeyBackup();
}
Expand Down
7 changes: 7 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,13 @@ export interface CryptoApi {
* @param key - the backup decryption key
*/
storeSessionBackupPrivateKey(key: Uint8Array): Promise<void>;

/**
* Get the current status of key backup.
*
* @returns If automatic key backups are enabled, the `version` of the active backup. Otherwise, `null`.
*/
getActiveSessionBackupVersion(): Promise<string | null>;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,18 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
});
}

/**
* Get the current status of key backup.
*
* Implementation of {@link CryptoApi.getActiveSessionBackupVersion}.
*/
public async getActiveSessionBackupVersion(): Promise<string | null> {
if (this.backupManager.getKeyBackupEnabled()) {
return this.backupManager.version ?? null;
}
return null;
}

/**
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the
Expand Down
25 changes: 25 additions & 0 deletions src/rust-crypto/backup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2023 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.
*/

export class RustBackupManager {
/**
* Get the backup version we are currently backing up to, if any
*/
public async getActiveBackupVersion(): Promise<string | null> {
// TODO stub
return null;
}
}
13 changes: 13 additions & 0 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { RustVerificationRequest, verificationMethodIdentifierToMethod } from ".
import { EventType } from "../@types/event";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { RustBackupManager } from "./backup";

const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];

Expand All @@ -82,6 +83,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private outgoingRequestProcessor: OutgoingRequestProcessor;
private crossSigningIdentity: CrossSigningIdentity;

public readonly backupManager: RustBackupManager;

public constructor(
/** The `OlmMachine` from the underlying rust crypto sdk. */
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
Expand Down Expand Up @@ -109,6 +112,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
this.eventDecryptor = new EventDecryptor(olmMachine);
this.backupManager = new RustBackupManager();

// Fire if the cross signing keys are imported from the secret storage
const onCrossSigningKeysImport = (): void => {
Expand Down Expand Up @@ -780,6 +784,15 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), "");
}

/**
* Get the current status of key backup.
*
* Implementation of {@link CryptoApi#getActiveSessionBackupVersion}.
*/
public async getActiveSessionBackupVersion(): Promise<string | null> {
return await this.backupManager.getActiveBackupVersion();
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
Expand Down

0 comments on commit 6d28154

Please sign in to comment.