Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CryptoApi.resetEncryption #4614

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
EventShieldReason,
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
VerificationRequest,
} from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
Expand All @@ -72,6 +73,7 @@ import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
import { RustBackupManager } from "../../../src/rust-crypto/backup.ts";

const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
Expand Down Expand Up @@ -1879,6 +1881,79 @@ describe("RustCrypto", () => {
);
});
});

describe("resetEncryption", () => {
let secretStorage: ServerSideSecretStorage;
beforeEach(() => {
secretStorage = {
setDefaultKeyId: jest.fn(),
hasKey: jest.fn().mockResolvedValue(false),
getKey: jest.fn().mockResolvedValue(null),
} as unknown as ServerSideSecretStorage;

fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
});

it("key backup should stay disabled after reset", async () => {
// We don't have a key backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {});

const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);

const authUploadDeviceSigningKeys = jest.fn();
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);

// The default key id should be deleted
expect(secretStorage.setDefaultKeyId).toHaveBeenCalledWith(null);
expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull();
// The new cross signing keys should be uploaded
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
});

it("key backup should be re-enabled after reset", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// When we will delete the key backup
fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", {});

// We consider the key backup as trusted
jest.spyOn(RustBackupManager.prototype, "isKeyBackupTrusted").mockResolvedValue({
trusted: true,
matchesDecryptionKey: true,
});

const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
// We have a key backup
expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull();

let nbCalls = 0;
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
() => {
// First call is when we check if the key backup is enabled.
// Second call is when we get the next backup after we deleted the first key backup,
// we return an empty object because we don't have a key backup anymore.
return ++nbCalls === 1 ? testData.SIGNED_BACKUP_DATA : {};
},
{ overwriteRoutes: true },
);

// A new key backup should be created after the reset
let content!: KeyBackupInfo;
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => {
content = JSON.parse(options.body as string);
return { version: "2" };
});

const authUploadDeviceSigningKeys = jest.fn();
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);

// A new key backup should be created
expect(content.auth_data).toBeTruthy();
// The new cross signing keys should be uploaded
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
});
});
});

/** Build a MatrixHttpApi instance */
Expand Down
13 changes: 13 additions & 0 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,19 @@ export interface CryptoApi {
payload: ToDevicePayload,
): Promise<ToDeviceBatch>;

/**
* Reset the encryption of the user by going through the following steps:
* - Disable backing up room keys and delete any existing backup.
* - Remove the default secret storage key from the account data (ie: the recovery key).
* - Reset the cross-signing keys.
* - Re-enable backing up room keys if enabled before.
*
* @param authUploadDeviceSigningKeys - Callback to authenticate the upload of device signing keys.
* Used when resetting the cross signing keys.
* See {@link BootstrapCrossSigningOpts#authUploadDeviceSigningKeys}.
*/
resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void>;

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Device/User verification
Expand Down
7 changes: 7 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4340,6 +4340,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
): Promise<KeyBackupRestoreResult> {
throw new Error("Not implemented");
}

/**
* Stub function -- resetEncryption is not implemented here, so throw error
*/
public resetEncryption(): Promise<void> {
throw new Error("Not implemented");
}
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.t
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
import { VerificationMethod } from "../types.ts";
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
import { UIAuthCallback } from "../interactive-auth.ts";

const ALL_VERIFICATION_METHODS = [
VerificationMethod.Sas,
Expand Down Expand Up @@ -1472,6 +1473,30 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
return batch;
}

/**
* Implementation of {@link CryptoApi#resetEncryption}.
*/
public async resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void> {
const backupEnabled = (await this.backupManager.getActiveBackupVersion()) !== null;

// Delete all the backup
await this.backupManager.deleteAllKeyBackupVersions();

// Disable the recovery key and the secret storage
await this.secretStorage.setDefaultKeyId(null);

// Reset the cross-signing keys
await this.crossSigningIdentity.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys,
});

// If key backup was enabled, we create a new backup
if (backupEnabled) {
await this.resetKeyBackup();
}
}

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