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 restoreKeybackup to CryptoApi. #4476

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a1f18cf
First draft of moving out restoreKeyBackup out of MatrixClient
florianduros Oct 29, 2024
61c1940
Deprecate `restoreKeyBackup*` in `MatrixClient`
florianduros Oct 29, 2024
a0dc1e8
Move types
florianduros Oct 29, 2024
d385c72
Handle only the room keys response
florianduros Oct 29, 2024
8ba0416
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/restor…
florianduros Oct 30, 2024
61ba3d2
Renaming and refactor `keysCountInBatch` & `getTotalKeyCount`
florianduros Oct 30, 2024
3b8b4e1
Fix `importRoomKeysAsJson` tsdoc
florianduros Oct 30, 2024
a50b3d5
Fix typo
florianduros Oct 30, 2024
7f35274
Move `backupDecryptor.free()``
florianduros Oct 30, 2024
0192809
Comment and simplify a bit `handleDecryptionOfAFullBackup`
florianduros Oct 30, 2024
f9b5966
Fix decryption crash by moving`backupDecryptor.free`
florianduros Oct 30, 2024
95e55a1
Use new api in `megolm-backup.spec.ts`
florianduros Oct 30, 2024
b02d245
Add tests to get recovery key from secret storage
florianduros Oct 31, 2024
9f7fb5d
Add doc to `KeyBackupRestoreOpts` & `KeyBackupRestoreResult`
florianduros Oct 31, 2024
df83906
Add doc to `restoreKeyBackupWithKey`
florianduros Oct 31, 2024
b057b6e
Add doc to `backup.ts`
florianduros Oct 31, 2024
e9df34b
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/restor…
florianduros Nov 4, 2024
c130e83
Apply comment suggestions
florianduros Nov 4, 2024
7e48a52
- Decryption key is recovered from the cache in `RustCrypto.restoreKe…
florianduros Nov 4, 2024
fbd8d63
Add `CryptoApi.restoreKeyBackup` to `ImportRoomKeyProgressData` doc.
florianduros Nov 4, 2024
d5bc824
Add deprecated symbol to all the `restoreKeyBackup*` overrides.
florianduros Nov 4, 2024
698dd93
Update tests
florianduros Nov 4, 2024
cec2c89
Move `RustBackupManager.getTotalKeyCount` to `backup#calculateKeyCoun…
florianduros Nov 4, 2024
6fd8b1d
Fix `RustBackupManager.restoreKeyBackup` tsdoc
florianduros Nov 4, 2024
9f86663
Move `backupDecryptor.free` in rust crypto.
florianduros Nov 4, 2024
beed963
Move `handleDecryptionOfAFullBackup` in `importKeyBackup`
florianduros Nov 5, 2024
a2582a7
Rename `calculateKeyCountInKeyBackup` to `countKeystInBackup`
florianduros Nov 6, 2024
eeb1dce
Fix `passphrase` typo
florianduros Nov 6, 2024
e0f8913
Rename `backupInfoVersion` to `backupVersion`
florianduros Nov 6, 2024
fe3ea7c
Complete restoreKeyBackup* methods documentation
florianduros Nov 6, 2024
e55aee9
Add `loadSessionBackupPrivateKeyFromSecretStorage`
florianduros Nov 6, 2024
c47066f
Remove useless intermediary result variable.
florianduros Nov 7, 2024
a70ee65
Check that decryption key matchs key backup info in `loadSessionBacku…
florianduros Nov 7, 2024
0112d37
Get backup info from a specific version
florianduros Nov 7, 2024
250b7e9
Fix typo in `countKeysInBackup`
florianduros Nov 7, 2024
37793c5
Improve documentation and naming
florianduros Nov 7, 2024
c063d93
Use `RustSdkCryptoJs.BackupDecryptionKey` as `decryptionKeyMatchesKey…
florianduros Nov 7, 2024
787649a
Call directly `olmMachine.getBackupKeys` in `restoreKeyBackup`
florianduros Nov 7, 2024
b214791
Last review changes
florianduros Nov 13, 2024
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
149 changes: 98 additions & 51 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ function mockUploadEmitter(
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// const newBackendOnly = backend === "libolm" ? test.skip : test;
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend === "libolm" ? test.skip : test;

const isNewBackend = backend === "rust-sdk";

let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
Expand Down Expand Up @@ -247,9 +249,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// On the first decryption attempt, decryption fails.
await awaitDecryption(event);
expect(event.decryptionFailureReason).toEqual(
backend === "libolm"
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
isNewBackend
? DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP
: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);

// Eventually, decryption succeeds.
Expand Down Expand Up @@ -345,24 +347,28 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});

const result = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
isNewBackend
? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58)
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
);

expect(result.imported).toStrictEqual(1);

await awaitKeyCached;
if (!isNewBackend) await awaitKeyCached;

// The key should be now cached
const afterCache = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!),
isNewBackend
? aliceCrypto.restoreKeyBackup(undefined)
: aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!),
);

expect(afterCache.imported).toStrictEqual(1);
Expand Down Expand Up @@ -398,8 +404,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe

it("Should import full backup in chunks", async function () {
const importMockImpl = jest.fn();
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}

// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
Expand All @@ -409,15 +420,19 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const check = await aliceCrypto.checkKeyBackupAndEnable();

const progressCallback = jest.fn();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58, {
progressCallback,
})
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));

expect(result.imported).toStrictEqual(expectedTotal);
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
Expand Down Expand Up @@ -451,8 +466,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});

it("Should continue to process backup if a chunk import fails and report failures", async function () {
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest
const importMockImpl = jest
.fn()
.mockImplementationOnce(() => {
// Fail to import first chunk
Expand All @@ -461,22 +475,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// Ok for other chunks
.mockResolvedValue(undefined);

if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}

const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);

fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);

const check = await aliceCrypto.checkKeyBackupAndEnable();

const progressCallback = jest.fn();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58, { progressCallback })
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));

expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
Expand Down Expand Up @@ -528,19 +552,21 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe

const check = await aliceCrypto.checkKeyBackupAndEnable();

const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58)
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
));

expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
});

it("recover specific session from backup", async function () {
oldBackendOnly("recover specific session from backup", async function () {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
Expand Down Expand Up @@ -576,14 +602,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const check = await aliceCrypto.checkKeyBackupAndEnable();

await expect(
aliceClient.restoreKeyBackupWithRecoveryKey(
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
undefined,
undefined,
check!.backupInfo!,
),
isNewBackend
? aliceCrypto.restoreKeyBackup("EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD")
: aliceClient.restoreKeyBackupWithRecoveryKey(
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
undefined,
undefined,
check!.backupInfo!,
),
).rejects.toThrow();
});

newBackendOnly("Should restore the backup from the secret storage", async () => {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);

const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);

const result = await aliceCrypto.restoreKeyBackup(undefined);
expect(result.imported).toStrictEqual(1);
});
});

describe("backupLoop", () => {
Expand Down
15 changes: 15 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3696,6 +3696,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param opts - Optional params such as callbacks
* @returns Status of restoration with `total` and `imported`
* key counts.
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
public async restoreKeyBackupWithPassword(
password: string,
Expand Down Expand Up @@ -3741,6 +3743,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param opts - Optional params such as callbacks
* @returns Status of restoration with `total` and `imported`
* key counts.
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public async restoreKeyBackupWithSecretStorage(
backupInfo: IKeyBackupInfo,
Expand Down Expand Up @@ -3778,6 +3782,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa

* @returns Status of restoration with `total` and `imported`
* key counts.
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public restoreKeyBackupWithRecoveryKey(
recoveryKey: string,
Expand Down Expand Up @@ -3811,6 +3817,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
}

/**
* Restore from an existing key backup via a private key stored locally
* @param targetRoomId
* @param targetSessionId
* @param backupInfo
* @param opts
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
public async restoreKeyBackupWithCache(
targetRoomId: undefined,
targetSessionId: undefined,
Expand Down
27 changes: 26 additions & 1 deletion src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import { DeviceMap } from "../models/device.ts";
import { UIAuthCallback } from "../interactive-auth.ts";
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage.ts";
import { VerificationRequest } from "./verification.ts";
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./keybackup.ts";
import {
BackupTrustInfo,
KeyBackupCheck,
KeyBackupInfo,
KeyBackupRestoreOpts,
KeyBackupRestoreResult,
} from "./keybackup.ts";
import { ISignatures } from "../@types/signed.ts";
import { MatrixEvent } from "../models/event.ts";

Expand Down Expand Up @@ -539,6 +545,25 @@ export interface CryptoApi {
*/
deleteKeyBackupVersion(version: string): Promise<void>;

/**
* Restores a key backup.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* If the recovery key is not provided, it will try to restore the key backup using the recovery key stored
* in the local cache or in the Secret Storage.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param recoveryKey - The recovery key to use to restore the key backup.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param opts
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
restoreKeyBackup(recoveryKey: string | undefined, opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;

/**
* Restores a key backup using a passphrase.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param phassphrase - The passphrase to use to restore the key backup.
* @param opts
*
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/ shared via 4S.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
restoreKeyBackupWithPassphrase(phassphrase: string, opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;
florianduros marked this conversation as resolved.
Show resolved Hide resolved

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Dehydrated devices
Expand Down
26 changes: 26 additions & 0 deletions src/crypto-api/keybackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

import { ISigned } from "../@types/signed.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { ImportRoomKeyProgressData } from "./index.ts";

export interface Curve25519AuthData {
public_key: string;
Expand Down Expand Up @@ -87,3 +88,28 @@ export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecret
export interface KeyBackupRoomSessions {
[sessionId: string]: KeyBackupSession;
}

/**
* Extra parameters for {@link CryptoApi.restoreKeyBackup} and {@link CryptoApi.restoreKeyBackupWithPassphrase}.
*/
export interface KeyBackupRestoreOpts {
/**
* Reports ongoing progress of the import process.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param progress
*/
progressCallback?: (progress: ImportRoomKeyProgressData) => void;
florianduros marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* The result of {@link CryptoApi.restoreKeyBackup}.
*/
export interface KeyBackupRestoreResult {
/**
* The total number of keys that were found in the backup.
*/
total: number;
/**
* The number of keys that were imported.
*/
imported: number;
}
22 changes: 22 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ import {
OwnDeviceKeys,
CryptoEvent as CryptoApiCryptoEvent,
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
KeyBackupRestoreResult,
KeyBackupRestoreOpts,
} from "../crypto-api/index.ts";
import { Device, DeviceMap } from "../models/device.ts";
import { deviceInfoToDevice } from "./device-converter.ts";
Expand Down Expand Up @@ -4308,6 +4310,26 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public async startDehydration(createNewKey?: boolean): Promise<void> {
throw new Error("Not implemented");
}

/**
* Stub function -- restoreKeyBackup is not implemented here, so throw error
*/
public restoreKeyBackup(
recoveryKey: string | undefined,
opts: KeyBackupRestoreOpts,
): Promise<KeyBackupRestoreResult> {
throw new Error("Not implemented");
}

/**
* Stub function -- restoreKeyBackupWithPassphrase is not implemented here, so throw error
*/
public restoreKeyBackupWithPassphrase(
phassphrase: string,
opts: KeyBackupRestoreOpts,
): Promise<KeyBackupRestoreResult> {
throw new Error("Not implemented");
}
}

/**
Expand Down
Loading