Skip to content

Commit 61375ef

Browse files
authored
Add CryptoApi.resetEncryption (#4614)
* feat(crypto api): Add `CryptoApi#resetEncryption` * docs(crypto api): Review changes * test(crypto api): Cleaner way to handle key backup removal
1 parent e5fda72 commit 61375ef

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
EventShieldReason,
6262
ImportRoomKeysOpts,
6363
KeyBackupCheck,
64+
KeyBackupInfo,
6465
VerificationRequest,
6566
} from "../../../src/crypto-api";
6667
import * as testData from "../../test-utils/test-data";
@@ -72,6 +73,7 @@ import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
7273
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
7374
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
7475
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
76+
import { RustBackupManager } from "../../../src/rust-crypto/backup.ts";
7577

7678
const TEST_USER = "@alice:example.com";
7779
const TEST_DEVICE_ID = "TEST_DEVICE";
@@ -1879,6 +1881,74 @@ describe("RustCrypto", () => {
18791881
);
18801882
});
18811883
});
1884+
1885+
describe("resetEncryption", () => {
1886+
let secretStorage: ServerSideSecretStorage;
1887+
beforeEach(() => {
1888+
secretStorage = {
1889+
setDefaultKeyId: jest.fn(),
1890+
hasKey: jest.fn().mockResolvedValue(false),
1891+
getKey: jest.fn().mockResolvedValue(null),
1892+
} as unknown as ServerSideSecretStorage;
1893+
1894+
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
1895+
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
1896+
});
1897+
1898+
it("reset should reset 4S, backup and cross-signing", async () => {
1899+
// We don't have a key backup
1900+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {});
1901+
1902+
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
1903+
1904+
const authUploadDeviceSigningKeys = jest.fn();
1905+
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);
1906+
1907+
// The default key id should be deleted
1908+
expect(secretStorage.setDefaultKeyId).toHaveBeenCalledWith(null);
1909+
expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull();
1910+
// The new cross signing keys should be uploaded
1911+
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
1912+
});
1913+
1914+
it("key backup should be re-enabled after reset", async () => {
1915+
// When we will delete the key backup
1916+
let backupIsDeleted = false;
1917+
fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", () => {
1918+
backupIsDeleted = true;
1919+
return {};
1920+
});
1921+
// If the backup is deleted, we will return an empty object
1922+
fetchMock.get("path:/_matrix/client/v3/room_keys/version", () => {
1923+
return backupIsDeleted ? {} : testData.SIGNED_BACKUP_DATA;
1924+
});
1925+
1926+
// We consider the key backup as trusted
1927+
jest.spyOn(RustBackupManager.prototype, "isKeyBackupTrusted").mockResolvedValue({
1928+
trusted: true,
1929+
matchesDecryptionKey: true,
1930+
});
1931+
1932+
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
1933+
// We have a key backup
1934+
expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull();
1935+
1936+
// A new key backup should be created after the reset
1937+
let newKeyBackupInfo!: KeyBackupInfo;
1938+
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => {
1939+
newKeyBackupInfo = JSON.parse(options.body as string);
1940+
return { version: "2" };
1941+
});
1942+
1943+
const authUploadDeviceSigningKeys = jest.fn();
1944+
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);
1945+
1946+
// A new key backup should be created
1947+
expect(newKeyBackupInfo.auth_data).toBeTruthy();
1948+
// The new cross signing keys should be uploaded
1949+
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
1950+
});
1951+
});
18821952
});
18831953

18841954
/** Build a MatrixHttpApi instance */

src/crypto-api/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,19 @@ export interface CryptoApi {
396396
payload: ToDevicePayload,
397397
): Promise<ToDeviceBatch>;
398398

399+
/**
400+
* Reset the encryption of the user by going through the following steps:
401+
* - Disable backing up room keys and delete any existing backups.
402+
* - Remove the default secret storage key from the account data (ie: the recovery key).
403+
* - Reset the cross-signing keys.
404+
* - Re-enable backing up room keys if enabled before.
405+
*
406+
* @param authUploadDeviceSigningKeys - Callback to authenticate the upload of device signing keys.
407+
* Used when resetting the cross signing keys.
408+
* See {@link BootstrapCrossSigningOpts#authUploadDeviceSigningKeys}.
409+
*/
410+
resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void>;
411+
399412
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
400413
//
401414
// Device/User verification

src/crypto/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4340,6 +4340,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
43404340
): Promise<KeyBackupRestoreResult> {
43414341
throw new Error("Not implemented");
43424342
}
4343+
4344+
/**
4345+
* Stub function -- resetEncryption is not implemented here, so throw error
4346+
*/
4347+
public resetEncryption(): Promise<void> {
4348+
throw new Error("Not implemented");
4349+
}
43434350
}
43444351

43454352
/**

src/rust-crypto/rust-crypto.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.t
8888
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
8989
import { VerificationMethod } from "../types.ts";
9090
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
91+
import { UIAuthCallback } from "../interactive-auth.ts";
9192

9293
const ALL_VERIFICATION_METHODS = [
9394
VerificationMethod.Sas,
@@ -1472,6 +1473,30 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
14721473
return batch;
14731474
}
14741475

1476+
/**
1477+
* Implementation of {@link CryptoApi#resetEncryption}.
1478+
*/
1479+
public async resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void> {
1480+
const backupEnabled = (await this.backupManager.getActiveBackupVersion()) !== null;
1481+
1482+
// Disable backup, and delete all the backups from the server
1483+
await this.backupManager.deleteAllKeyBackupVersions();
1484+
1485+
// Disable the recovery key and the secret storage
1486+
await this.secretStorage.setDefaultKeyId(null);
1487+
1488+
// Reset the cross-signing keys
1489+
await this.crossSigningIdentity.bootstrapCrossSigning({
1490+
setupNewCrossSigning: true,
1491+
authUploadDeviceSigningKeys,
1492+
});
1493+
1494+
// If key backup was enabled, we create a new backup
1495+
if (backupEnabled) {
1496+
await this.resetKeyBackup();
1497+
}
1498+
}
1499+
14751500
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
14761501
//
14771502
// SyncCryptoCallbacks implementation

0 commit comments

Comments
 (0)