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 9 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
12 changes: 8 additions & 4 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
createClient,
Crypto,
CryptoEvent,
encodeBase64,
ICreateClientOpts,
IEvent,
IMegolmSessionData,
Expand Down Expand Up @@ -316,6 +317,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe

beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.get(
`path:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`,
testData.SIGNED_BACKUP_DATA,
);

aliceClient = await initTestClient();
aliceCrypto = aliceClient.getCrypto()!;
Expand Down Expand Up @@ -621,11 +626,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);

const check = await aliceCrypto.checkKeyBackupAndEnable();
const recoveryKey = await aliceCrypto.getSecretStorageBackupPrivateKey();
expect(recoveryKey).not.toBeNull();
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);

await aliceCrypto.storeSessionBackupPrivateKey(recoveryKey!, check!.backupInfo!.version!);
const result = await aliceCrypto.restoreKeyBackup();
expect(result.imported).toStrictEqual(1);
},
Expand Down
24 changes: 12 additions & 12 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,15 +470,6 @@ export interface CryptoApi {
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
* Fetch the backup decryption key we have saved in our secret storage.
*
* This can be used for gossiping the key to other devices.
*
* @returns the key, if any, or null
*/
getSecretStorageBackupPrivateKey(): Promise<Uint8Array | null>;

/**
* Fetch the backup decryption key we have saved in our store.
*
Expand Down Expand Up @@ -511,6 +502,12 @@ export interface CryptoApi {
*/
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;

/**
* Fetch the backup decryption key from the secret storage, fetch the backup info version.
* Store locally the key and the backup info version by calling {@link storeSessionBackupPrivateKey}.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void>;

/**
* Get the current status of key backup.
*
Expand Down Expand Up @@ -555,21 +552,24 @@ export interface CryptoApi {
deleteKeyBackupVersion(version: string): Promise<void>;

/**
* Download the last key backup from the homeserver (endpoint GET /room_keys/keys/).
* Download and restore the last key backup from the homeserver (endpoint GET /room_keys/keys/).
* The key backup is decrypted and imported by using the decryption key stored locally. The decryption key should be stored locally by using {@link CryptoApi#storeSessionBackupPrivateKey}.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* Warning: the full key backup may be quite large, so this operation may take several hours to complete.
* Use of {@link KeyBackupRestoreOpts.progressCallback} is recommended.
* @param opts
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
restoreKeyBackup(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.
* The decoded key (derivated from the passphrase) is store locally by calling {@link CryptoApi#storeSessionBackupPrivateKey}.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param passphrase - The passphrase to use to restore the key backup.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @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.
*/
restoreKeyBackupWithPassphrase(phassphrase: string, opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;
restoreKeyBackupWithPassphrase(passphrase: string, opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;

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

/**
* Implementation of {@link Crypto.loadSessionBackupPrivateKeyFromSecretStorage}.
*/
public loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void> {
throw new Error("Not implmeented");
}

/**
* Get the current status of key backup.
*
Expand Down Expand Up @@ -4322,18 +4329,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* Stub function -- restoreKeyBackupWithPassphrase is not implemented here, so throw error
*/
public restoreKeyBackupWithPassphrase(
phassphrase: string,
passphrase: string,
opts: KeyBackupRestoreOpts,
): Promise<KeyBackupRestoreResult> {
throw new Error("Not implemented");
}

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

/**
Expand Down
66 changes: 46 additions & 20 deletions src/rust-crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { sleep } from "../utils.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { encodeBase64 } from "../base64.ts";

/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];
Expand Down Expand Up @@ -497,16 +498,18 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
*/
private keysCountInBatch(batch: RustSdkCryptoJs.KeysBackupRequest): number {
const parsedBody: KeyBackup = JSON.parse(batch.body);
return calculateKeyCountInKeyBackup(parsedBody);
return countKeysInBackup(parsedBody);
}

/**
* Get information about the current key backup from the server
*
* Get information about a key backup from the server
* - If version is provided, get information about that backup version.
* - If no version is provided, get information about the latest backup.
* @param version - The version of the backup to get information about.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @returns Information object from API or null if there is no active backup.
*/
private async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
return await requestKeyBackupVersion(this.http);
public async requestKeyBackupVersion(version?: string): Promise<KeyBackupInfo | null> {
return await requestKeyBackupVersion(this.http, version);
}

/**
Expand Down Expand Up @@ -593,36 +596,36 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
/**
* Restore a key backup.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param backupInfoVersion - The version of the backup to restore.
* @param backupVersion - The version of the backup to restore.
* @param backupDecryptor - The backup decryptor to use to decrypt the keys.
* @param opts - Options for the restore.
* @returns The total number of keys and the total imported.
*/
public async restoreKeyBackup(
backupInfoVersion: string,
backupVersion: string,
backupDecryptor: BackupDecryptor,
opts?: KeyBackupRestoreOpts,
): Promise<KeyBackupRestoreResult> {
const keyBackup = await this.downloadKeyBackup(backupInfoVersion);
const keyBackup = await this.downloadKeyBackup(backupVersion);
opts?.progressCallback?.({
stage: "load_keys",
});

return this.importKeyBackup(keyBackup, backupInfoVersion, backupDecryptor, opts);
return this.importKeyBackup(keyBackup, backupVersion, backupDecryptor, opts);
}

/**
* Call `/room_keys/keys` to download the key backup (room keys) for the given backup version.
* https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3room_keyskeys
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param backupInfoVersion
* @param backupVersion
* @returns The key backup response.
*/
private downloadKeyBackup(backupInfoVersion: string): Promise<KeyBackup> {
private downloadKeyBackup(backupVersion: string): Promise<KeyBackup> {
return this.http.authedRequest<KeyBackup>(
Method.Get,
"/room_keys/keys",
{ version: backupInfoVersion },
{ version: backupVersion },
undefined,
{
prefix: ClientPrefix.V3,
Expand All @@ -635,7 +638,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
* Call the opts.progressCallback with the progress of the import.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param keyBackup - The response from the server containing the keys to import.
* @param backupInfoVersion - The version of the backup info.
* @param backupVersion - The version of the backup info.
* @param backupDecryptor - The backup decryptor to use to decrypt the keys.
* @param opts - Options for the import.
*
Expand All @@ -645,15 +648,15 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
*/
private async importKeyBackup(
keyBackup: KeyBackup,
backupInfoVersion: string,
backupVersion: string,
backupDecryptor: BackupDecryptor,
opts?: KeyBackupRestoreOpts,
): Promise<KeyBackupRestoreResult> {
// We have a full backup here, it can get quite big, so we need to decrypt and import it in chunks.

const CHUNK_SIZE = 200;
// Get the total count as a first pass
const totalKeyCount = calculateKeyCountInKeyBackup(keyBackup);
const totalKeyCount = countKeysInBackup(keyBackup);
let totalImported = 0;
let totalFailures = 0;

Expand All @@ -677,7 +680,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,

// We have a chunk of decrypted keys: import them
try {
await this.importBackedUpRoomKeys(currentChunk, backupInfoVersion);
await this.importBackedUpRoomKeys(currentChunk, backupVersion);
totalImported += currentChunk.length;
} catch (e) {
totalFailures += currentChunk.length;
Expand Down Expand Up @@ -797,11 +800,23 @@ export class RustBackupDecryptor implements BackupDecryptor {
}
}

/**
* Fetch a key backup info from the server.
* - If `version` is provided call GET /room_keys/version/$version and get the backup info for that version.
* https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3room_keysversionversion
* - If not, call GET /room_keys/version and get the latest backup info.
* https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3room_keysversion
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param http
* @param version - the specific version of the backup info to fetch
* @returns The key backup info or null if there is no backup.
*/
export async function requestKeyBackupVersion(
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
version?: string,
): Promise<KeyBackupInfo | null> {
try {
return await http.authedRequest<KeyBackupInfo>(Method.Get, "/room_keys/version", undefined, undefined, {
const path = version ? encodeUri("/room_keys/version/$version", { $version: version }) : "/room_keys/version";
return await http.authedRequest<KeyBackupInfo>(Method.Get, path, undefined, undefined, {
prefix: ClientPrefix.V3,
});
} catch (e) {
Expand All @@ -814,12 +829,23 @@ export async function requestKeyBackupVersion(
}

/**
* This method calculates the total number of keys present in a key backup
* Checks if the provided decryption key matches the public key of the key backup info.
* @param decryptionKey - The decryption key to check.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param keyBackupInfo - The key backup info to check against.
* @returns `true` if the decryption key matches the key backup info, `false` otherwise.
*/
export function decryptionKeyMatchKeyBackupInfo(decryptionKey: Uint8Array, keyBackupInfo: KeyBackupInfo): boolean {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
florianduros marked this conversation as resolved.
Show resolved Hide resolved
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(decryptionKey));
const authData = <Curve25519AuthData>keyBackupInfo.auth_data;
return authData.public_key === backupDecryptionKey.megolmV1PublicKey.publicKeyBase64;
}

/**
* Counts the total number of keys present in a key backup.
* @param keyBackup - The key backup to count the keys from.
*
* @returns The total number of keys in the backup.
*/
function calculateKeyCountInKeyBackup(keyBackup: KeyBackup): number {
function countKeysInBackup(keyBackup: KeyBackup): number {
let count = 0;
for (const { sessions } of Object.values(keyBackup.rooms)) {
count += Object.keys(sessions).length;
Expand Down
Loading