From a1f18cfff28fa4b66d684a356470207c4bcc7174 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 29 Oct 2024 10:42:38 +0100 Subject: [PATCH 01/37] First draft of moving out restoreKeyBackup out of MatrixClient --- src/crypto-api/index.ts | 27 +++- src/crypto-api/keybackup.ts | 18 +++ src/crypto/index.ts | 22 +++ src/rust-crypto/backup.ts | 240 ++++++++++++++++++++++++++++++++- src/rust-crypto/rust-crypto.ts | 90 ++++++++++++- 5 files changed, 394 insertions(+), 3 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index ea6177f7f8..162681103e 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -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"; @@ -539,6 +545,25 @@ export interface CryptoApi { */ deleteKeyBackupVersion(version: string): Promise; + /** + * Restores a key backup. + * 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. + * + * @param recoveryKey - The recovery key to use to restore the key backup. + * @param opts + */ + restoreKeyBackup(recoveryKey: string | undefined, opts?: KeyBackupRestoreOpts): Promise; + + /** + * Restores a key backup using a passphrase. + * @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. + */ + restoreKeyBackupWithPassphrase(phassphrase: string, opts?: KeyBackupRestoreOpts): Promise; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Dehydrated devices diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index efae30d0ec..a7b8d58941 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -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; @@ -87,3 +88,20 @@ export interface KeyBackupSession; +} + +export interface KeyBackupRestoreOpts { + progressCallback?: (progress: ImportRoomKeyProgressData) => void; +} + +export interface KeyBackupRestoreResult { + total: number; + imported: number; +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts index c252ba15a4..99d9f2ba41 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -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"; @@ -4308,6 +4310,26 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); } + + /** + * Stub function -- restoreKeyBackup is not implemented here, so throw error + */ + public restoreKeyBackup( + recoveryKey: string | undefined, + opts: KeyBackupRestoreOpts, + ): Promise { + throw new Error("Not implemented"); + } + + /** + * Stub function -- restoreBackupWithPassphrase is not implemented here, so throw error + */ + public restoreKeyBackupWithPassphrase( + phassphrase: string, + opts: KeyBackupRestoreOpts, + ): Promise { + throw new Error("Not implemented"); + } } /** diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index d4a91bd0fa..1c860464c5 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -24,6 +24,10 @@ import { KeyBackupInfo, KeyBackupSession, Curve25519SessionData, + RoomKeysResponse, + RoomsKeysResponse, + KeyBackupRestoreOpts, + KeyBackupRestoreResult, } from "../crypto-api/keybackup.ts"; import { logger } from "../logger.ts"; import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts"; @@ -34,7 +38,7 @@ import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts"; import { sleep } from "../utils.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts"; -import { IKeyBackupInfo } from "../crypto/keybackup.ts"; +import { IKeyBackupInfo, IKeyBackupRoomSessions } from "../crypto/keybackup.ts"; import { IKeyBackup } from "../crypto/backup.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; @@ -585,6 +589,240 @@ export class RustBackupManager extends TypedEventEmitter { + try { + const roomKeysResponse = await this.downloadRoomKeys(backupInfoVersion); + + opts?.progressCallback?.({ + stage: "load_keys", + }); + + if ((roomKeysResponse as RoomsKeysResponse).rooms) { + return this.handleRoomsKeysResponse( + roomKeysResponse as RoomsKeysResponse, + backupInfoVersion, + backupDecryptor, + opts, + ); + } else if ((roomKeysResponse as RoomKeysResponse).sessions) { + return this.handleRoomKeysResponse( + roomKeysResponse as RoomKeysResponse, + backupInfoVersion, + backupDecryptor, + opts, + ); + } else { + return this.handleKeyBackupSessionResponse( + roomKeysResponse as KeyBackupSession, + backupInfoVersion, + backupDecryptor, + opts, + ); + } + } finally { + backupDecryptor.free(); + } + } + + /** + * Call `/room_keys/keys` to download the room keys for the given backup version. + * @param backupInfoVersion + */ + private downloadRoomKeys( + backupInfoVersion: string, + ): Promise { + return this.http.authedRequest( + Method.Get, + "/room_keys/keys", + { version: backupInfoVersion }, + undefined, + { + prefix: ClientPrefix.V3, + }, + ); + } + + /** + * Decrypt a key backup session and import the keys. + * @param session + * @param backupInfoVersion + * @param backupDecryptor + * @param opts + */ + private async handleKeyBackupSessionResponse( + session: KeyBackupSession, + backupInfoVersion: string, + backupDecryptor: BackupDecryptor, + opts?: KeyBackupRestoreOpts, + ): Promise { + let imported = 0; + try { + const [key] = await backupDecryptor.decryptSessions({ + [undefined!]: session, + }); + + await this.importBackedUpRoomKeys([key], backupInfoVersion, { + progressCallback: opts?.progressCallback, + }); + imported = 1; + } catch (e) { + logger.debug("Failed to decrypt megolm session from backup", e); + } + + return { total: 1, imported }; + } + + /** + * Decrypt and import + * @param response + * @param backupInfoVersion + * @param backupDecryptor + * @param opts + */ + private async handleRoomKeysResponse( + response: RoomKeysResponse, + backupInfoVersion: string, + backupDecryptor: BackupDecryptor, + opts?: KeyBackupRestoreOpts, + ): Promise { + // For now we don't chunk for a single room backup, but we could in the future. + // Currently it is not used by the application. + const { sessions } = response; + const keys = await backupDecryptor.decryptSessions(sessions); + for (const k of keys) { + k.room_id = undefined!; + } + await this.importBackedUpRoomKeys(keys, backupInfoVersion, { + progressCallback: opts?.progressCallback, + }); + + return { total: Object.keys(sessions).length, imported: keys.length }; + } + + private async handleRoomsKeysResponse( + response: RoomsKeysResponse, + backupInfoVersion: string, + backupDecryptor: BackupDecryptor, + opts?: KeyBackupRestoreOpts, + ): Promise { + // We have a full backup here, it can get quite big, so we need to decrypt and import it in chunks. + + // Get the total count as a first pass + const totalKeyCount = this.getTotalKeyCount(response); + let totalImported = 0; + let totalFailures = 0; + // Now decrypt and import the keys in chunks + await this.handleDecryptionOfAFullBackup(response, backupDecryptor, 200, async (chunk) => { + // We have a chunk of decrypted keys: import them + try { + await this.importBackedUpRoomKeys(chunk, backupInfoVersion); + totalImported += chunk.length; + } catch (e) { + totalFailures += chunk.length; + // We failed to import some keys, but we should still try to import the rest? + // Log the error and continue + logger.error("Error importing keys from backup", e); + } + + opts?.progressCallback?.({ + total: totalKeyCount, + successes: totalImported, + stage: "load_keys", + failures: totalFailures, + }); + }); + + return { total: totalKeyCount, imported: totalImported }; + } + + /** + * This method calculates the total number of keys present in the response of a `/room_keys/keys` call. + * + * @param res - The response from the server containing the keys to be counted. + * + * @returns The total number of keys in the backup. + */ + private getTotalKeyCount(res: RoomsKeysResponse): number { + const rooms = res.rooms; + let totalKeyCount = 0; + for (const roomData of Object.values(rooms)) { + if (!roomData.sessions) continue; + totalKeyCount += Object.keys(roomData.sessions).length; + } + return totalKeyCount; + } + + /** + * This method handles the decryption of a full backup, i.e a call to `/room_keys/keys`. + * It will decrypt the keys in chunks and call the `block` callback for each chunk. + * + * @param res - The response from the server containing the keys to be decrypted. + * @param backupDecryptor - An instance of the BackupDecryptor class used to decrypt the keys. + * @param chunkSize - The size of the chunks to be processed at a time. + * @param block - A callback function that is called for each chunk of keys. + * + * @returns A promise that resolves when the decryption is complete. + */ + private async handleDecryptionOfAFullBackup( + res: RoomsKeysResponse, + backupDecryptor: BackupDecryptor, + chunkSize: number, + block: (chunk: IMegolmSessionData[]) => Promise, + ): Promise { + const { rooms } = res; + + let groupChunkCount = 0; + let chunkGroupByRoom: Map = new Map(); + + const handleChunkCallback = async (roomChunks: Map): Promise => { + const currentChunk: IMegolmSessionData[] = []; + for (const roomId of roomChunks.keys()) { + const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!); + for (const sessionId in decryptedSessions) { + const k = decryptedSessions[sessionId]; + k.room_id = roomId; + currentChunk.push(k); + } + } + await block(currentChunk); + }; + + for (const [roomId, roomData] of Object.entries(rooms)) { + if (!roomData.sessions) continue; + + chunkGroupByRoom.set(roomId, {}); + + for (const [sessionId, session] of Object.entries(roomData.sessions)) { + const sessionsForRoom = chunkGroupByRoom.get(roomId)!; + sessionsForRoom[sessionId] = session; + groupChunkCount += 1; + if (groupChunkCount >= chunkSize) { + // We have enough chunks to decrypt + await handleChunkCallback(chunkGroupByRoom); + chunkGroupByRoom = new Map(); + // There might be remaining keys for that room, so add back an entry for the current room. + chunkGroupByRoom.set(roomId, {}); + groupChunkCount = 0; + } + } + } + + // Handle remaining chunk if needed + if (groupChunkCount > 0) { + await handleChunkCallback(chunkGroupByRoom); + } + } } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 2d65e7f5c5..d4617953b8 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -66,6 +66,9 @@ import { DeviceIsolationModeKind, CryptoEvent, CryptoEventHandlerMap, + KeyBackupRestoreOpts, + KeyBackupRestoreResult, + decodeRecoveryKey, } from "../crypto-api/index.ts"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts"; @@ -81,11 +84,12 @@ import { TypedReEmitter } from "../ReEmitter.ts"; import { randomString } from "../randomstring.ts"; import { ClientStoppedError } from "../errors.ts"; import { ISignatures } from "../@types/signed.ts"; -import { encodeBase64 } from "../base64.ts"; +import { decodeBase64, encodeBase64 } from "../base64.ts"; import { OutgoingRequestsManager } from "./OutgoingRequestsManager.ts"; import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.ts"; import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts"; import { VerificationMethod } from "../types.ts"; +import { keyFromAuthData } from "../common-crypto/key-passphrase.ts"; const ALL_VERIFICATION_METHODS = [ VerificationMethod.Sas, @@ -1280,6 +1284,90 @@ export class RustCrypto extends TypedEventEmitter { + const backupInfo = await this.backupManager.getServerBackupInfo(); + if (!backupInfo?.version) { + throw new Error("No backup info available"); + } + + const privateKey = await keyFromAuthData(backupInfo.auth_data, phassphrase); + return this.restoreKeyBackupWithKey(privateKey, backupInfo, opts); + } + + /** + * Implementation of {@link CryptoBackend#restoreKeyBackup}. + */ + public async restoreKeyBackup( + recoveryKey: string | undefined, + opts?: KeyBackupRestoreOpts, + ): Promise { + // If the recovery key is not provided, we try to get it from the local cache or the secret storage + const privateKey = recoveryKey + ? decodeRecoveryKey(recoveryKey) + : await this.getPrivateKeyFromCacheOrSecretStorage(); + + if (!privateKey) { + throw new Error("No recovery key available"); + } + + const backupInfo = await this.backupManager.getServerBackupInfo(); + if (!backupInfo) throw new Error("No backup info available"); + + return this.restoreKeyBackupWithKey(privateKey, backupInfo, opts); + } + + /** + * + * @param recoveryKey + * @param backupInfo + * @param opts + * @private + */ + private async restoreKeyBackupWithKey( + recoveryKey: Uint8Array, + backupInfo: KeyBackupInfo, + opts?: KeyBackupRestoreOpts, + ): Promise { + if (!backupInfo.version) throw new Error("Missing version in backup info"); + + // Cache the key, if possible. + // This is async. + this.storeSessionBackupPrivateKey(recoveryKey, backupInfo.version).catch((e) => { + this.logger.warn("Error caching session backup key:", e); + }); + + const backupDecryptor = await this.getBackupDecryptor(backupInfo, recoveryKey); + + opts?.progressCallback?.({ + stage: "fetch", + }); + + return this.backupManager.restoreKeyBackup(backupInfo.version, backupDecryptor, opts); + } + + /** + * Get the private key from the cache or the secret storage. + * If the key is not found, returns null. + * @private + */ + private async getPrivateKeyFromCacheOrSecretStorage(): Promise { + // First we try to git from our local store + const privateKeyFromCache = await this.getSessionBackupPrivateKey(); + if (privateKeyFromCache) return privateKeyFromCache; + + // If not found, we try to get it from the secret storage + const encodedKey = await this.secretStorage.get("m.megolm_backup.v1"); + if (!encodedKey) return null; + + return decodeBase64(encodedKey); + } + /** * Implementation of {@link CryptoApi#isDehydrationSupported}. */ From 61c194092d95666dc7d10fab472d7e4c7e35311a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 29 Oct 2024 10:54:13 +0100 Subject: [PATCH 02/37] Deprecate `restoreKeyBackup*` in `MatrixClient` --- src/client.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/client.ts b/src/client.ts index 023231532e..ebe6d0634f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3696,6 +3696,8 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 29 Oct 2024 16:32:18 +0100 Subject: [PATCH 03/37] Move types --- src/crypto-api/keybackup.ts | 8 -------- src/rust-crypto/backup.ts | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index a7b8d58941..0fc9cdd753 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -89,14 +89,6 @@ export interface KeyBackupRoomSessions { [sessionId: string]: KeyBackupSession; } -export interface RoomKeysResponse { - sessions: KeyBackupRoomSessions; -} - -export interface RoomsKeysResponse { - rooms: Record; -} - export interface KeyBackupRestoreOpts { progressCallback?: (progress: ImportRoomKeyProgressData) => void; } diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 1c860464c5..24793b5762 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -24,10 +24,9 @@ import { KeyBackupInfo, KeyBackupSession, Curve25519SessionData, - RoomKeysResponse, - RoomsKeysResponse, KeyBackupRestoreOpts, KeyBackupRestoreResult, + KeyBackupRoomSessions, } from "../crypto-api/keybackup.ts"; import { logger } from "../logger.ts"; import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts"; @@ -38,7 +37,7 @@ import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts"; import { sleep } from "../utils.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts"; -import { IKeyBackupInfo, IKeyBackupRoomSessions } from "../crypto/keybackup.ts"; +import { IKeyBackupInfo } from "../crypto/keybackup.ts"; import { IKeyBackup } from "../crypto/backup.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; @@ -783,9 +782,9 @@ export class RustBackupManager extends TypedEventEmitter = new Map(); + let chunkGroupByRoom: Map = new Map(); - const handleChunkCallback = async (roomChunks: Map): Promise => { + const handleChunkCallback = async (roomChunks: Map): Promise => { const currentChunk: IMegolmSessionData[] = []; for (const roomId of roomChunks.keys()) { const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!); @@ -917,3 +916,11 @@ export type RustBackupCryptoEventMap = { [CryptoEvent.KeyBackupFailed]: (errCode: string) => void; [CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void; }; + +interface RoomKeysResponse { + sessions: KeyBackupRoomSessions; +} + +interface RoomsKeysResponse { + rooms: Record; +} From d385c723c4b4336e1ab4a5e222872d3cca813148 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 29 Oct 2024 17:45:29 +0100 Subject: [PATCH 04/37] Handle only the room keys response --- src/rust-crypto/backup.ts | 109 ++++++++------------------------------ 1 file changed, 22 insertions(+), 87 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 24793b5762..3d08d4d33c 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -602,33 +602,11 @@ export class RustBackupManager extends TypedEventEmitter { try { const roomKeysResponse = await this.downloadRoomKeys(backupInfoVersion); - opts?.progressCallback?.({ stage: "load_keys", }); - if ((roomKeysResponse as RoomsKeysResponse).rooms) { - return this.handleRoomsKeysResponse( - roomKeysResponse as RoomsKeysResponse, - backupInfoVersion, - backupDecryptor, - opts, - ); - } else if ((roomKeysResponse as RoomKeysResponse).sessions) { - return this.handleRoomKeysResponse( - roomKeysResponse as RoomKeysResponse, - backupInfoVersion, - backupDecryptor, - opts, - ); - } else { - return this.handleKeyBackupSessionResponse( - roomKeysResponse as KeyBackupSession, - backupInfoVersion, - backupDecryptor, - opts, - ); - } + return this.handleRoomsKeysResponse(roomKeysResponse, backupInfoVersion, backupDecryptor, opts); } finally { backupDecryptor.free(); } @@ -636,12 +614,13 @@ export class RustBackupManager extends TypedEventEmitter { - return this.http.authedRequest( + private downloadRoomKeys(backupInfoVersion: string): Promise { + return this.http.authedRequest( Method.Get, "/room_keys/keys", { version: backupInfoVersion }, @@ -653,62 +632,18 @@ export class RustBackupManager extends TypedEventEmitter { - let imported = 0; - try { - const [key] = await backupDecryptor.decryptSessions({ - [undefined!]: session, - }); - - await this.importBackedUpRoomKeys([key], backupInfoVersion, { - progressCallback: opts?.progressCallback, - }); - imported = 1; - } catch (e) { - logger.debug("Failed to decrypt megolm session from backup", e); - } - - return { total: 1, imported }; - } - - /** - * Decrypt and import - * @param response - * @param backupInfoVersion - * @param backupDecryptor - * @param opts + * Import the room keys from a `/room_keys/keys` call. + * Call the opts.progressCallback with the progress of the import. + * + * @param response - The response from the server containing the keys to import. + * @param backupInfoVersion - The version of the backup info. + * @param backupDecryptor - The backup decryptor to use to decrypt the keys. + * @param opts - Options for the import. + * + * @return The total number of keys and the total imported. + * + * @private */ - private async handleRoomKeysResponse( - response: RoomKeysResponse, - backupInfoVersion: string, - backupDecryptor: BackupDecryptor, - opts?: KeyBackupRestoreOpts, - ): Promise { - // For now we don't chunk for a single room backup, but we could in the future. - // Currently it is not used by the application. - const { sessions } = response; - const keys = await backupDecryptor.decryptSessions(sessions); - for (const k of keys) { - k.room_id = undefined!; - } - await this.importBackedUpRoomKeys(keys, backupInfoVersion, { - progressCallback: opts?.progressCallback, - }); - - return { total: Object.keys(sessions).length, imported: keys.length }; - } - private async handleRoomsKeysResponse( response: RoomsKeysResponse, backupInfoVersion: string, @@ -917,10 +852,10 @@ export type RustBackupCryptoEventMap = { [CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void; }; -interface RoomKeysResponse { - sessions: KeyBackupRoomSessions; -} - +/** + * Response from GET `/room_keys/keys` endpoint. + * See https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3room_keyskeys + */ interface RoomsKeysResponse { - rooms: Record; + rooms: Record; } From 61ba3d2b5c6601763c428f8e395adc168561aa7d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 30 Oct 2024 15:01:33 +0100 Subject: [PATCH 05/37] Renaming and refactor `keysCountInBatch` & `getTotalKeyCount` --- src/rust-crypto/backup.ts | 57 +++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 421d2a4aba..12cb34a642 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -490,8 +490,18 @@ export class RustBackupManager extends TypedEventEmitter { try { - const roomKeysResponse = await this.downloadRoomKeys(backupInfoVersion); + const keyBackup = await this.downloadKeyBackup(backupInfoVersion); opts?.progressCallback?.({ stage: "load_keys", }); - return this.handleRoomsKeysResponse(roomKeysResponse, backupInfoVersion, backupDecryptor, opts); + return this.importKeyBackup(keyBackup, backupInfoVersion, backupDecryptor, opts); } finally { backupDecryptor.free(); } } /** - * Call `/room_keys/keys` to download the room keys for the given backup version. + * 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 * * @param backupInfoVersion - * @returns The response from the server containing the keys to import. + * @returns The key backup response. */ - private downloadRoomKeys(backupInfoVersion: string): Promise { - return this.http.authedRequest( + private downloadKeyBackup(backupInfoVersion: string): Promise { + return this.http.authedRequest( Method.Get, "/room_keys/keys", { version: backupInfoVersion }, @@ -633,7 +643,7 @@ export class RustBackupManager extends TypedEventEmitter { + await this.handleDecryptionOfAFullBackup(keyBackup, backupDecryptor, 200, async (chunk) => { // We have a chunk of decrypted keys: import them try { await this.importBackedUpRoomKeys(chunk, backupInfoVersion); @@ -678,28 +688,11 @@ export class RustBackupManager extends TypedEventEmitter Promise, ): Promise { - const { rooms } = res; + const { rooms } = keyBackup; let groupChunkCount = 0; let chunkGroupByRoom: Map = new Map(); From 3b8b4e109c891f61932ec4a59fa291575beb23c8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 30 Oct 2024 15:02:08 +0100 Subject: [PATCH 06/37] Fix `importRoomKeysAsJson` tsdoc --- src/rust-crypto/backup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 12cb34a642..c6876cf360 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -220,7 +220,7 @@ export class RustBackupManager extends TypedEventEmitter Date: Wed, 30 Oct 2024 15:17:30 +0100 Subject: [PATCH 07/37] Fix typo --- src/crypto/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 99d9f2ba41..8ce9c99017 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -4322,7 +4322,7 @@ export class Crypto extends TypedEventEmitter Date: Wed, 30 Oct 2024 15:38:17 +0100 Subject: [PATCH 08/37] Move `backupDecryptor.free()`` --- src/rust-crypto/backup.ts | 14 +++++--------- src/rust-crypto/rust-crypto.ts | 6 +++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index c6876cf360..8b09d61076 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -608,16 +608,12 @@ export class RustBackupManager extends TypedEventEmitter { - try { - const keyBackup = await this.downloadKeyBackup(backupInfoVersion); - opts?.progressCallback?.({ - stage: "load_keys", - }); + const keyBackup = await this.downloadKeyBackup(backupInfoVersion); + opts?.progressCallback?.({ + stage: "load_keys", + }); - return this.importKeyBackup(keyBackup, backupInfoVersion, backupDecryptor, opts); - } finally { - backupDecryptor.free(); - } + return this.importKeyBackup(keyBackup, backupInfoVersion, backupDecryptor, opts); } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index d4617953b8..7f328bc8ad 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1348,7 +1348,11 @@ export class RustCrypto extends TypedEventEmitter Date: Wed, 30 Oct 2024 16:19:31 +0100 Subject: [PATCH 09/37] Comment and simplify a bit `handleDecryptionOfAFullBackup` --- src/rust-crypto/backup.ts | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 8b09d61076..345e84e1ef 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -703,34 +703,48 @@ export class RustBackupManager extends TypedEventEmitter { const { rooms } = keyBackup; - let groupChunkCount = 0; - let chunkGroupByRoom: Map = new Map(); - + /** + * This method is called when we have enough chunks to decrypt. + * It will decrypt the chunks and call the `block` callback. + * @param roomChunks + */ const handleChunkCallback = async (roomChunks: Map): Promise => { const currentChunk: IMegolmSessionData[] = []; + // const decryptedSessions: IMegolmSessionData[] = []; for (const roomId of roomChunks.keys()) { + // Decrypt the sessions for the given room const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!); - for (const sessionId in decryptedSessions) { - const k = decryptedSessions[sessionId]; - k.room_id = roomId; - currentChunk.push(k); - } + // Add the decrypted sessions to the current chunk + decryptedSessions.forEach((session) => { + // We set the room_id for each session + session.room_id = roomId; + currentChunk.push(session); + }); } + await block(currentChunk); }; + let groupChunkCount = 0; + let chunkGroupByRoom: Map = new Map(); + for (const [roomId, roomData] of Object.entries(rooms)) { + // If there are no sessions for the room, skip it if (!roomData.sessions) continue; + // Initialize a new chunk group for the current room chunkGroupByRoom.set(roomId, {}); for (const [sessionId, session] of Object.entries(roomData.sessions)) { + // We set previously the chunk group for the current room, so we can safely get it const sessionsForRoom = chunkGroupByRoom.get(roomId)!; sessionsForRoom[sessionId] = session; groupChunkCount += 1; + // If we have enough chunks to decrypt, call the block callback if (groupChunkCount >= chunkSize) { // We have enough chunks to decrypt await handleChunkCallback(chunkGroupByRoom); + // Reset the chunk group chunkGroupByRoom = new Map(); // There might be remaining keys for that room, so add back an entry for the current room. chunkGroupByRoom.set(roomId, {}); From f9b5966954d10ea455112e18d494ac3f72e305d0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 30 Oct 2024 18:31:12 +0100 Subject: [PATCH 10/37] Fix decryption crash by moving`backupDecryptor.free` --- src/rust-crypto/backup.ts | 42 +++++++++++++++++++--------------- src/rust-crypto/rust-crypto.ts | 6 +---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 345e84e1ef..402042ba1c 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -660,26 +660,31 @@ export class RustBackupManager extends TypedEventEmitter { - // We have a chunk of decrypted keys: import them - try { - await this.importBackedUpRoomKeys(chunk, backupInfoVersion); - totalImported += chunk.length; - } catch (e) { - totalFailures += chunk.length; - // We failed to import some keys, but we should still try to import the rest? - // Log the error and continue - logger.error("Error importing keys from backup", e); - } - opts?.progressCallback?.({ - total: totalKeyCount, - successes: totalImported, - stage: "load_keys", - failures: totalFailures, + try { + // Now decrypt and import the keys in chunks + await this.handleDecryptionOfAFullBackup(keyBackup, backupDecryptor, 200, async (chunk) => { + // We have a chunk of decrypted keys: import them + try { + await this.importBackedUpRoomKeys(chunk, backupInfoVersion); + totalImported += chunk.length; + } catch (e) { + totalFailures += chunk.length; + // We failed to import some keys, but we should still try to import the rest? + // Log the error and continue + logger.error("Error importing keys from backup", e); + } + + opts?.progressCallback?.({ + total: totalKeyCount, + successes: totalImported, + stage: "load_keys", + failures: totalFailures, + }); }); - }); + } finally { + backupDecryptor.free(); + } return { total: totalKeyCount, imported: totalImported }; } @@ -710,7 +715,6 @@ export class RustBackupManager extends TypedEventEmitter): Promise => { const currentChunk: IMegolmSessionData[] = []; - // const decryptedSessions: IMegolmSessionData[] = []; for (const roomId of roomChunks.keys()) { // Decrypt the sessions for the given room const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 7f328bc8ad..d4617953b8 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1348,11 +1348,7 @@ export class RustCrypto extends TypedEventEmitter Date: Wed, 30 Oct 2024 18:31:44 +0100 Subject: [PATCH 11/37] Use new api in `megolm-backup.spec.ts` --- spec/integ/crypto/megolm-backup.spec.ts | 132 ++++++++++++++---------- 1 file changed, 80 insertions(+), 52 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index bc36f940ff..67fdfabbed 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -42,10 +42,10 @@ import { } from "../../test-utils/test-utils"; import * as testData from "../../test-utils/test-data"; import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup"; -import { IKeyBackup } from "../../../src/crypto/backup"; import { flushPromises } from "../../test-utils/flushPromises"; import { defer, IDeferred } from "../../../src/utils"; import { DecryptionFailureCode } from "../../../src/crypto-api"; +import { KeyBackup } from "../../../src/rust-crypto/backup.ts"; const ROOM_ID = testData.TEST_ROOM_ID; @@ -91,7 +91,7 @@ function mockUploadEmitter( }, }; } - const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}"); + const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}"); let count = 0; for (const [roomId, value] of Object.entries(uploadPayload.rooms)) { for (const sessionId of Object.keys(value.sessions)) { @@ -117,9 +117,11 @@ 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 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 */ let syncResponder: SyncResponder; @@ -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. @@ -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); @@ -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]); @@ -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 @@ -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 @@ -461,6 +475,14 @@ 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); @@ -468,15 +490,17 @@ 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.total).toStrictEqual(expectedTotal); // A chunk failed to import @@ -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, @@ -576,12 +602,14 @@ 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(); }); }); From b02d24554b53597502f20fbb5c05e0cc354b3a1c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 31 Oct 2024 10:04:48 +0100 Subject: [PATCH 12/37] Add tests to get recovery key from secret storage --- spec/integ/crypto/megolm-backup.spec.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 67fdfabbed..8e7877dff6 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -118,7 +118,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe // 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 newBackendOnly = backend === "libolm" ? test.skip : test; const isNewBackend = backend === "rust-sdk"; @@ -612,6 +612,25 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe ), ).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", () => { From 9f7fb5d64a35408392febc3a25dbf668f827676e Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 31 Oct 2024 10:25:10 +0100 Subject: [PATCH 13/37] Add doc to `KeyBackupRestoreOpts` & `KeyBackupRestoreResult` --- src/crypto-api/keybackup.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index 0fc9cdd753..abb8727127 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -89,11 +89,27 @@ 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. + * @param progress + */ progressCallback?: (progress: ImportRoomKeyProgressData) => void; } +/** + * 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; } From df8390648e034d23fa243b01b40d73cdc8a89bf7 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 31 Oct 2024 10:39:10 +0100 Subject: [PATCH 14/37] Add doc to `restoreKeyBackupWithKey` --- src/rust-crypto/rust-crypto.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index d4617953b8..f3ff719c01 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1323,10 +1323,10 @@ export class RustCrypto extends TypedEventEmitter Date: Thu, 31 Oct 2024 10:46:16 +0100 Subject: [PATCH 15/37] Add doc to `backup.ts` --- src/rust-crypto/backup.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 402042ba1c..5e240f0ffb 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -599,9 +599,10 @@ export class RustBackupManager extends TypedEventEmitter Date: Mon, 4 Nov 2024 17:08:49 +0100 Subject: [PATCH 16/37] Apply comment suggestions Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- src/crypto-api/index.ts | 2 +- src/crypto-api/keybackup.ts | 2 +- src/rust-crypto/backup.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 162681103e..799a183038 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -560,7 +560,7 @@ export interface CryptoApi { * @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. + * @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; diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index abb8727127..9840159dae 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -94,7 +94,7 @@ export interface KeyBackupRoomSessions { */ export interface KeyBackupRestoreOpts { /** - * Reports ongoing progress of the import process. + * A callback which, if defined, will be called periodically to report ongoing progress of the backup restore process. * @param progress */ progressCallback?: (progress: ImportRoomKeyProgressData) => void; diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 117eb37983..d24bc35b2d 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -606,7 +606,8 @@ export class RustBackupManager extends TypedEventEmitter Date: Mon, 4 Nov 2024 17:13:29 +0100 Subject: [PATCH 17/37] - Decryption key is recovered from the cache in `RustCrypto.restoreKeyBackup` - Add `CryptoApi.getSecretStorageBackupPrivateKey` to get the decryption key from the secret storage. --- src/crypto-api/index.ts | 17 +++++--- src/crypto/index.ts | 12 ++++-- src/rust-crypto/rust-crypto.ts | 76 +++++++++------------------------- 3 files changed, 39 insertions(+), 66 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 799a183038..b20fb09197 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -470,6 +470,15 @@ 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; + /** * Fetch the backup decryption key we have saved in our store. * @@ -546,14 +555,12 @@ export interface CryptoApi { deleteKeyBackupVersion(version: string): Promise; /** - * Restores a key backup. - * 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. + * Download 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}. * - * @param recoveryKey - The recovery key to use to restore the key backup. * @param opts */ - restoreKeyBackup(recoveryKey: string | undefined, opts?: KeyBackupRestoreOpts): Promise; + restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise; /** * Restores a key backup using a passphrase. diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 0b23bdcf05..0baee7ac7b 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -4314,10 +4314,7 @@ export class Crypto extends TypedEventEmitter { + public restoreKeyBackup(opts: KeyBackupRestoreOpts): Promise { throw new Error("Not implemented"); } @@ -4330,6 +4327,13 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); } + + /** + * Stub function -- getSecretStorageBackupPrivateKey is not implemented here, so throw error + */ + public getSecretStorageBackupPrivateKey(): Promise { + throw new Error("Not implemented"); + } } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index f3ff719c01..8d36601ee2 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -68,7 +68,6 @@ import { CryptoEventHandlerMap, KeyBackupRestoreOpts, KeyBackupRestoreResult, - decodeRecoveryKey, } from "../crypto-api/index.ts"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts"; @@ -1172,6 +1171,15 @@ export class RustCrypto extends TypedEventEmitter { + const backupKey = await this.secretStorage.get("m.megolm_backup.v1"); + if (!backupKey) return null; + return decodeBase64(backupKey); + } + /** * Fetch the backup decryption key we have saved in our store. * @@ -1297,52 +1305,23 @@ export class RustCrypto extends TypedEventEmitter { - // If the recovery key is not provided, we try to get it from the local cache or the secret storage - const privateKey = recoveryKey - ? decodeRecoveryKey(recoveryKey) - : await this.getPrivateKeyFromCacheOrSecretStorage(); - - if (!privateKey) { - throw new Error("No recovery key available"); - } - - const backupInfo = await this.backupManager.getServerBackupInfo(); - if (!backupInfo) throw new Error("No backup info available"); - return this.restoreKeyBackupWithKey(privateKey, backupInfo, opts); + // Cache the key + await this.storeSessionBackupPrivateKey(privateKey, backupInfo.version); + return this.restoreKeyBackup(opts); } /** - * Restore a key backup with the given key. - * @param recoveryKey - the key to use for decryption - * @param backupInfo - the backup info - * @param opts - additional options - * @private + * Implementation of {@link CryptoApi#restoreKeyBackup}. */ - private async restoreKeyBackupWithKey( - recoveryKey: Uint8Array, - backupInfo: KeyBackupInfo, - opts?: KeyBackupRestoreOpts, - ): Promise { - if (!backupInfo.version) throw new Error("Missing version in backup info"); + public async restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise { + const privateKeyFromCache = await this.getSessionBackupPrivateKey(); + if (!privateKeyFromCache) throw new Error("No decryption key found in cache"); - // Cache the key, if possible. - // This is async. - this.storeSessionBackupPrivateKey(recoveryKey, backupInfo.version).catch((e) => { - this.logger.warn("Error caching session backup key:", e); - }); + const backupInfo = await this.backupManager.getServerBackupInfo(); + if (!backupInfo?.version) throw new Error("Missing version in backup info"); - const backupDecryptor = await this.getBackupDecryptor(backupInfo, recoveryKey); + const backupDecryptor = await this.getBackupDecryptor(backupInfo, privateKeyFromCache); opts?.progressCallback?.({ stage: "fetch", @@ -1351,23 +1330,6 @@ export class RustCrypto extends TypedEventEmitter { - // First we try to git from our local store - const privateKeyFromCache = await this.getSessionBackupPrivateKey(); - if (privateKeyFromCache) return privateKeyFromCache; - - // If not found, we try to get it from the secret storage - const encodedKey = await this.secretStorage.get("m.megolm_backup.v1"); - if (!encodedKey) return null; - - return decodeBase64(encodedKey); - } - /** * Implementation of {@link CryptoApi#isDehydrationSupported}. */ From fbd8d631fa475b4da116671aaf1b712aa718aeb2 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 4 Nov 2024 17:22:27 +0100 Subject: [PATCH 18/37] Add `CryptoApi.restoreKeyBackup` to `ImportRoomKeyProgressData` doc. --- src/crypto-api/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index b20fb09197..bf41b180fd 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -912,8 +912,8 @@ export class DeviceVerificationStatus { /** * Room key import progress report. - * Used when calling {@link CryptoApi#importRoomKeys} or - * {@link CryptoApi#importRoomKeysAsJson} as the parameter of + * Used when calling {@link CryptoApi#importRoomKeys}, + * {@link CryptoApi#importRoomKeysAsJson} or {@link CryptoApi#restoreKeyBackup} as the parameter of * the progressCallback. Used to display feedback. */ export interface ImportRoomKeyProgressData { From d5bc8249ecf425c92265e36d1fc4a336d3071123 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 4 Nov 2024 17:28:08 +0100 Subject: [PATCH 19/37] Add deprecated symbol to all the `restoreKeyBackup*` overrides. --- src/client.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/client.ts b/src/client.ts index ea0550c317..46f0ff9c13 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3706,6 +3706,9 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. + */ public async restoreKeyBackupWithPassword( password: string, targetRoomId: string, @@ -3713,6 +3716,9 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. + */ public async restoreKeyBackupWithPassword( password: string, targetRoomId: string, @@ -3720,6 +3726,9 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}. + */ public async restoreKeyBackupWithPassword( password: string, targetRoomId: string | undefined, @@ -3792,6 +3801,9 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + */ public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string, @@ -3799,6 +3811,9 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + */ public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string, @@ -3806,6 +3821,9 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + */ public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string | undefined, @@ -3832,18 +3850,27 @@ export class MatrixClient extends TypedEventEmitter; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + */ public async restoreKeyBackupWithCache( targetRoomId: string, targetSessionId: undefined, backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + */ public async restoreKeyBackupWithCache( targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise; + /** + * @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}. + */ public async restoreKeyBackupWithCache( targetRoomId: string | undefined, targetSessionId: string | undefined, From 698dd9300f20a12e1d295aecce25c1608d5fb3f0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 4 Nov 2024 18:11:13 +0100 Subject: [PATCH 20/37] Update tests --- spec/integ/crypto/megolm-backup.spec.ts | 98 ++++++++++++++++--------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 8e7877dff6..de693c4c8a 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -44,7 +44,7 @@ import * as testData from "../../test-utils/test-data"; import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup"; import { flushPromises } from "../../test-utils/flushPromises"; import { defer, IDeferred } from "../../../src/utils"; -import { DecryptionFailureCode } from "../../../src/crypto-api"; +import { decodeRecoveryKey, DecryptionFailureCode } from "../../../src/crypto-api"; import { KeyBackup } from "../../../src/rust-crypto/backup.ts"; const ROOM_ID = testData.TEST_ROOM_ID; @@ -346,9 +346,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe onKeyCached = resolve; }); + await aliceCrypto.storeSessionBackupPrivateKey( + decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58), + check!.backupInfo!.version!, + ); + const result = await advanceTimersUntil( isNewBackend - ? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58) + ? aliceCrypto.restoreKeyBackup() : aliceClient.restoreKeyBackupWithRecoveryKey( testData.BACKUP_DECRYPTION_KEY_BASE58, undefined, @@ -362,13 +367,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe expect(result.imported).toStrictEqual(1); - if (!isNewBackend) await awaitKeyCached; + if (isNewBackend) return; + + await awaitKeyCached; // The key should be now cached const afterCache = await advanceTimersUntil( - isNewBackend - ? aliceCrypto.restoreKeyBackup(undefined) - : aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!), + aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!), ); expect(afterCache.imported).toStrictEqual(1); @@ -419,9 +424,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe const check = await aliceCrypto.checkKeyBackupAndEnable(); + await aliceCrypto.storeSessionBackupPrivateKey( + decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58), + check!.backupInfo!.version!, + ); + const progressCallback = jest.fn(); const result = await (isNewBackend - ? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58, { + ? aliceCrypto.restoreKeyBackup({ progressCallback, }) : aliceClient.restoreKeyBackupWithRecoveryKey( @@ -488,10 +498,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response); const check = await aliceCrypto.checkKeyBackupAndEnable(); + await aliceCrypto.storeSessionBackupPrivateKey( + decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58), + check!.backupInfo!.version!, + ); const progressCallback = jest.fn(); const result = await (isNewBackend - ? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58, { progressCallback }) + ? aliceCrypto.restoreKeyBackup({ progressCallback }) : aliceClient.restoreKeyBackupWithRecoveryKey( testData.BACKUP_DECRYPTION_KEY_BASE58, undefined, @@ -551,9 +565,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response); const check = await aliceCrypto.checkKeyBackupAndEnable(); + await aliceCrypto.storeSessionBackupPrivateKey( + decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58), + check!.backupInfo!.version!, + ); const result = await (isNewBackend - ? aliceCrypto.restoreKeyBackup(testData.BACKUP_DECRYPTION_KEY_BASE58) + ? aliceCrypto.restoreKeyBackup() : aliceClient.restoreKeyBackupWithRecoveryKey( testData.BACKUP_DECRYPTION_KEY_BASE58, undefined, @@ -586,7 +604,34 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe expect(result.imported).toStrictEqual(1); }); - it("Fails on bad recovery key", async function () { + newBackendOnly( + "Should get the decryption key from the secret storage and restore the key backup", + async function () { + // @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 check = await aliceCrypto.checkKeyBackupAndEnable(); + const recoveryKey = await aliceCrypto.getSecretStorageBackupPrivateKey(); + expect(recoveryKey).not.toBeNull(); + + await aliceCrypto.storeSessionBackupPrivateKey(recoveryKey!, check!.backupInfo!.version!); + const result = await aliceCrypto.restoreKeyBackup(); + expect(result.imported).toStrictEqual(1); + }, + ); + + oldBackendOnly("Fails on bad recovery key", async function () { const fullBackup = { rooms: { [ROOM_ID]: { @@ -602,34 +647,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe const check = await aliceCrypto.checkKeyBackupAndEnable(); await expect( - 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!, - ), + 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); + newBackendOnly("Should throw an error if the decryption key is found in cache", async () => { + await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in cache"); }); }); From cec2c89b81b495e7c76d64136d93aba81efd39b0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 4 Nov 2024 18:23:46 +0100 Subject: [PATCH 21/37] Move `RustBackupManager.getTotalKeyCount` to `backup#calculateKeyCountInKeyBackup` --- src/rust-crypto/backup.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index d24bc35b2d..63bb28e8f2 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -497,21 +497,7 @@ export class RustBackupManager extends TypedEventEmitter Date: Mon, 4 Nov 2024 18:25:32 +0100 Subject: [PATCH 22/37] Fix `RustBackupManager.restoreKeyBackup` tsdoc --- src/rust-crypto/backup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 63bb28e8f2..65d08992b5 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -593,7 +593,7 @@ export class RustBackupManager extends TypedEventEmitter Date: Mon, 4 Nov 2024 18:33:49 +0100 Subject: [PATCH 23/37] Move `backupDecryptor.free` in rust crypto. --- src/rust-crypto/backup.ts | 40 +++++++++++++++------------------- src/rust-crypto/rust-crypto.ts | 10 ++++++++- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 65d08992b5..c40ccde7b3 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -656,30 +656,26 @@ export class RustBackupManager extends TypedEventEmitter { - // We have a chunk of decrypted keys: import them - try { - await this.importBackedUpRoomKeys(chunk, backupInfoVersion); - totalImported += chunk.length; - } catch (e) { - totalFailures += chunk.length; - // We failed to import some keys, but we should still try to import the rest? - // Log the error and continue - logger.error("Error importing keys from backup", e); - } + // Now decrypt and import the keys in chunks + await this.handleDecryptionOfAFullBackup(keyBackup, backupDecryptor, 200, async (chunk) => { + // We have a chunk of decrypted keys: import them + try { + await this.importBackedUpRoomKeys(chunk, backupInfoVersion); + totalImported += chunk.length; + } catch (e) { + totalFailures += chunk.length; + // We failed to import some keys, but we should still try to import the rest? + // Log the error and continue + logger.error("Error importing keys from backup", e); + } - opts?.progressCallback?.({ - total: totalKeyCount, - successes: totalImported, - stage: "load_keys", - failures: totalFailures, - }); + opts?.progressCallback?.({ + total: totalKeyCount, + successes: totalImported, + stage: "load_keys", + failures: totalFailures, }); - } finally { - backupDecryptor.free(); - } + }); return { total: totalKeyCount, imported: totalImported }; } diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 8d36601ee2..bef77146ab 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1327,7 +1327,15 @@ export class RustCrypto extends TypedEventEmitter Date: Tue, 5 Nov 2024 10:50:13 +0100 Subject: [PATCH 24/37] Move `handleDecryptionOfAFullBackup` in `importKeyBackup` --- src/rust-crypto/backup.ts | 72 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index c40ccde7b3..ca6d271bf9 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -651,57 +651,15 @@ export class RustBackupManager extends TypedEventEmitter { // 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); let totalImported = 0; let totalFailures = 0; - // Now decrypt and import the keys in chunks - await this.handleDecryptionOfAFullBackup(keyBackup, backupDecryptor, 200, async (chunk) => { - // We have a chunk of decrypted keys: import them - try { - await this.importBackedUpRoomKeys(chunk, backupInfoVersion); - totalImported += chunk.length; - } catch (e) { - totalFailures += chunk.length; - // We failed to import some keys, but we should still try to import the rest? - // Log the error and continue - logger.error("Error importing keys from backup", e); - } - - opts?.progressCallback?.({ - total: totalKeyCount, - successes: totalImported, - stage: "load_keys", - failures: totalFailures, - }); - }); - - return { total: totalKeyCount, imported: totalImported }; - } - - /** - * This method handles the decryption of a full backup, i.e a call to `/room_keys/keys`. - * It will decrypt the keys in chunks and call the `block` callback for each chunk. - * - * @param keyBackup - The key backup from the server containing the keys to be decrypted. - * @param backupDecryptor - An instance of the BackupDecryptor class used to decrypt the keys. - * @param chunkSize - The size of the chunks to be processed at a time. - * @param block - A callback function that is called for each chunk of keys. - * - * @returns A promise that resolves when the decryption is complete. - */ - private async handleDecryptionOfAFullBackup( - keyBackup: KeyBackup, - backupDecryptor: BackupDecryptor, - chunkSize: number, - block: (chunk: IMegolmSessionData[]) => Promise, - ): Promise { - const { rooms } = keyBackup; - /** * This method is called when we have enough chunks to decrypt. - * It will decrypt the chunks and call the `block` callback. + * It will decrypt the chunks and try to import the room keys. * @param roomChunks */ const handleChunkCallback = async (roomChunks: Map): Promise => { @@ -717,13 +675,31 @@ export class RustBackupManager extends TypedEventEmitter = new Map(); - for (const [roomId, roomData] of Object.entries(rooms)) { + // Iterate over the rooms and sessions to group them in chunks + // And we call the handleChunkCallback when we have enough chunks to decrypt + for (const [roomId, roomData] of Object.entries(keyBackup.rooms)) { // If there are no sessions for the room, skip it if (!roomData.sessions) continue; @@ -736,7 +712,7 @@ export class RustBackupManager extends TypedEventEmitter= chunkSize) { + if (groupChunkCount >= CHUNK_SIZE) { // We have enough chunks to decrypt await handleChunkCallback(chunkGroupByRoom); // Reset the chunk group @@ -752,6 +728,8 @@ export class RustBackupManager extends TypedEventEmitter 0) { await handleChunkCallback(chunkGroupByRoom); } + + return { total: totalKeyCount, imported: totalImported }; } } From a2582a7875bc2ed9b5134a54380fb446cea9c594 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 6 Nov 2024 17:42:06 +0100 Subject: [PATCH 25/37] Rename `calculateKeyCountInKeyBackup` to `countKeystInBackup` --- src/rust-crypto/backup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index ca6d271bf9..16fc73440f 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -497,7 +497,7 @@ export class RustBackupManager extends TypedEventEmitter Date: Wed, 6 Nov 2024 17:43:20 +0100 Subject: [PATCH 26/37] Fix `passphrase` typo --- src/crypto-api/index.ts | 4 ++-- src/crypto/index.ts | 2 +- src/rust-crypto/rust-crypto.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index bf41b180fd..b624eeaaec 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -564,12 +564,12 @@ export interface CryptoApi { /** * Restores a key backup using a passphrase. - * @param phassphrase - The passphrase to use to restore the key backup. + * @param passphrase - 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. */ - restoreKeyBackupWithPassphrase(phassphrase: string, opts?: KeyBackupRestoreOpts): Promise; + restoreKeyBackupWithPassphrase(passphrase: string, opts?: KeyBackupRestoreOpts): Promise; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 0baee7ac7b..ab0245eb55 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -4322,7 +4322,7 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index bef77146ab..7fa360b60f 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1296,7 +1296,7 @@ export class RustCrypto extends TypedEventEmitter { const backupInfo = await this.backupManager.getServerBackupInfo(); @@ -1304,7 +1304,7 @@ export class RustCrypto extends TypedEventEmitter Date: Wed, 6 Nov 2024 17:52:30 +0100 Subject: [PATCH 27/37] Rename `backupInfoVersion` to `backupVersion` --- src/rust-crypto/backup.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 16fc73440f..a16901afb5 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -593,36 +593,36 @@ export class RustBackupManager extends TypedEventEmitter { - 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 * - * @param backupInfoVersion + * @param backupVersion * @returns The key backup response. */ - private downloadKeyBackup(backupInfoVersion: string): Promise { + private downloadKeyBackup(backupVersion: string): Promise { return this.http.authedRequest( Method.Get, "/room_keys/keys", - { version: backupInfoVersion }, + { version: backupVersion }, undefined, { prefix: ClientPrefix.V3, @@ -635,7 +635,7 @@ export class RustBackupManager extends TypedEventEmitter { @@ -677,7 +677,7 @@ export class RustBackupManager extends TypedEventEmitter Date: Wed, 6 Nov 2024 17:58:38 +0100 Subject: [PATCH 28/37] Complete restoreKeyBackup* methods documentation --- src/crypto-api/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index b624eeaaec..d62daba23e 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -555,15 +555,18 @@ export interface CryptoApi { deleteKeyBackupVersion(version: string): Promise; /** - * 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}. * + * 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 */ restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise; /** * Restores a key backup using a passphrase. + * The decoded key (derivated from the passphrase) is store locally by calling {@link CryptoApi#storeSessionBackupPrivateKey}. * @param passphrase - The passphrase to use to restore the key backup. * @param opts * From e55aee9617cced9771e1d6c28a76e45e596fce6e Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 6 Nov 2024 18:37:56 +0100 Subject: [PATCH 29/37] Add `loadSessionBackupPrivateKeyFromSecretStorage` --- spec/integ/crypto/megolm-backup.spec.ts | 8 ++--- src/crypto-api/index.ts | 15 ++++----- src/crypto/index.ts | 14 ++++----- src/rust-crypto/rust-crypto.ts | 41 +++++++++++++++---------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index de693c4c8a..94d98b4cef 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -23,6 +23,7 @@ import { createClient, Crypto, CryptoEvent, + encodeBase64, ICreateClientOpts, IEvent, IMegolmSessionData, @@ -621,11 +622,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); }, diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index d62daba23e..d9ab886c26 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -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; - /** * Fetch the backup decryption key we have saved in our store. * @@ -511,6 +502,12 @@ export interface CryptoApi { */ storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise; + /** + * 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}. + */ + loadSessionBackupPrivateKeyFromSecretStorage(): Promise; + /** * Get the current status of key backup. * diff --git a/src/crypto/index.ts b/src/crypto/index.ts index ab0245eb55..c75589813e 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1308,6 +1308,13 @@ export class Crypto extends TypedEventEmitter { + throw new Error("Not implmeented"); + } + /** * Get the current status of key backup. * @@ -4327,13 +4334,6 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); } - - /** - * Stub function -- getSecretStorageBackupPrivateKey is not implemented here, so throw error - */ - public getSecretStorageBackupPrivateKey(): Promise { - throw new Error("Not implemented"); - } } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 7fa360b60f..95f2667449 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1171,15 +1171,6 @@ export class RustCrypto extends TypedEventEmitter { - const backupKey = await this.secretStorage.get("m.megolm_backup.v1"); - if (!backupKey) return null; - return decodeBase64(backupKey); - } - /** * Fetch the backup decryption key we have saved in our store. * @@ -1214,6 +1205,24 @@ export class RustCrypto extends TypedEventEmitter { + const backupKey = await this.secretStorage.get("m.megolm_backup.v1"); + if (!backupKey) { + throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: missing decryption key in secret storage"); + } + + const decodedKey = decodeBase64(backupKey); + const keyBackupInfo = await this.backupManager.getServerBackupInfo(); + if (!keyBackupInfo) { + throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: unable to get backup version"); + } + + await this.storeSessionBackupPrivateKey(decodedKey, keyBackupInfo.version); + } + /** * Get the current status of key backup. * @@ -1323,19 +1332,17 @@ export class RustCrypto extends TypedEventEmitter Date: Thu, 7 Nov 2024 09:57:09 +0100 Subject: [PATCH 30/37] Remove useless intermediary result variable. --- src/rust-crypto/rust-crypto.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 95f2667449..8cc338b8e0 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1337,8 +1337,7 @@ export class RustCrypto extends TypedEventEmitter Date: Thu, 7 Nov 2024 10:26:11 +0100 Subject: [PATCH 31/37] Check that decryption key matchs key backup info in `loadSessionBackupPrivateKeyFromSecretStorage` --- src/rust-crypto/backup.ts | 13 +++++++++++++ src/rust-crypto/rust-crypto.ts | 13 +++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index a16901afb5..35b6d491ab 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -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"]; @@ -813,6 +814,18 @@ export async function requestKeyBackupVersion( } } +/** + * Checks if the provided decryption key matches the public key of the key backup info. + * @param decryptionKey - The decryption key to check. + * @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 { + const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(decryptionKey)); + const authData = 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. diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 8cc338b8e0..6725e44feb 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -46,7 +46,6 @@ import { CrossSigningStatus, CryptoApi, CryptoCallbacks, - Curve25519AuthData, DecryptionFailureCode, DeviceVerificationStatus, EventEncryptionInfo, @@ -78,7 +77,7 @@ import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts"; import { EventType, MsgType } from "../@types/event.ts"; import { TypedEventEmitter } from "../models/typed-event-emitter.ts"; -import { RustBackupManager } from "./backup.ts"; +import { decryptionKeyMatchKeyBackupInfo, RustBackupManager } from "./backup.ts"; import { TypedReEmitter } from "../ReEmitter.ts"; import { randomString } from "../randomstring.ts"; import { ClientStoppedError } from "../errors.ts"; @@ -339,13 +338,11 @@ export class RustCrypto extends TypedEventEmitterbackupInfo.auth_data; - if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) { + if (!decryptionKeyMatchKeyBackupInfo(privKey, backupInfo)) { throw new Error(`getBackupDecryptor: key backup on server does not match the decryption key`); } + const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey)); return this.backupManager.createBackupDecryptor(backupDecryptionKey); } @@ -1220,6 +1217,10 @@ export class RustCrypto extends TypedEventEmitter Date: Thu, 7 Nov 2024 15:11:10 +0100 Subject: [PATCH 32/37] Get backup info from a specific version --- spec/integ/crypto/megolm-backup.spec.ts | 4 ++++ src/rust-crypto/backup.ts | 24 +++++++++++++++++++----- src/rust-crypto/rust-crypto.ts | 16 +++++++++++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 94d98b4cef..e0a69e94bc 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -317,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()!; diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 35b6d491ab..b8a7781e0f 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -502,12 +502,14 @@ export class RustBackupManager extends TypedEventEmitter { - return await requestKeyBackupVersion(this.http); + public async requestKeyBackupVersion(version?: string): Promise { + return await requestKeyBackupVersion(this.http, version); } /** @@ -798,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 + * @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, + version?: string, ): Promise { try { - return await http.authedRequest(Method.Get, "/room_keys/version", undefined, undefined, { + const path = version ? encodeUri("/room_keys/version/$version", { $version: version }) : "/room_keys/version"; + return await http.authedRequest(Method.Get, path, undefined, undefined, { prefix: ClientPrefix.V3, }); } catch (e) { diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 6725e44feb..aadd7f9397 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1181,6 +1181,15 @@ export class RustCrypto extends TypedEventEmitter { + const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys(); + return backupKeys.backupVersion || null; + } + /** * Store the backup decryption key. * @@ -1325,10 +1334,15 @@ export class RustCrypto extends TypedEventEmitter { + // Get the decryption key from the cache const privateKeyFromCache = await this.getSessionBackupPrivateKey(); if (!privateKeyFromCache) throw new Error("No decryption key found in cache"); - const backupInfo = await this.backupManager.getServerBackupInfo(); + // Get the backup version from the cache + const backupVersion = await this.getSessionBackupVersion(); + if (!backupVersion) throw new Error("No backup version found in cache"); + + const backupInfo = await this.backupManager.requestKeyBackupVersion(backupVersion); if (!backupInfo?.version) throw new Error("Missing version in backup info"); const backupDecryptor = await this.getBackupDecryptor(backupInfo, privateKeyFromCache); From 250b7e95f3cfbcda1832b6c88dc291c76189fc78 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 7 Nov 2024 15:18:20 +0100 Subject: [PATCH 33/37] Fix typo in `countKeysInBackup` --- src/rust-crypto/backup.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index b8a7781e0f..a18cf931be 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -498,7 +498,7 @@ export class RustBackupManager extends TypedEventEmitter Date: Thu, 7 Nov 2024 18:25:32 +0100 Subject: [PATCH 34/37] Improve documentation and naming --- src/crypto-api/index.ts | 26 +++++++++++++++++++++----- src/rust-crypto/backup.ts | 19 ++++++++++++------- src/rust-crypto/rust-crypto.ts | 6 +++--- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index d9ab886c26..86be823f71 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -503,8 +503,14 @@ export interface CryptoApi { storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise; /** - * 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}. + * Attempt to fetch the backup decryption key from secret storage. + * + * If the key is found in secret storage, checks it against the latest backup on the server; + * if they match, stores the key in the crypto store by calling {@link storeSessionBackupPrivateKey}, + * which enables automatic restore of individual keys when an Unable-to-decrypt error is encountered. + * + * if we are unable to fetch the key from secret storage, there is no backup on the server, or the key + * does not match, throws an exception. */ loadSessionBackupPrivateKeyFromSecretStorage(): Promise; @@ -552,18 +558,28 @@ export interface CryptoApi { deleteKeyBackupVersion(version: string): Promise; /** - * 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}. + * Download and restore the full key backup from the homeserver. + * + * Before calling this method, a decryption key, and the backup version to restore, + * must have been saved in the crypto store. This happens in one of the following ways: + * + * - When a new backup version is created with {@link CryptoApi.resetKeyBackup}, a new key is created and cached. + * - The key can be loaded from secret storage with {@link CryptoApi.loadSessionBackupPrivateKeyFromSecretStorage}. + * - The key can be received from another device via secret sharing, typically as part of the interactive verification flow. + * - The key and backup version can also be set explicitly via {@link CryptoApi.storeSessionBackupPrivateKey}, + * though this is not expected to be a common operation. * * 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 */ restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise; /** * Restores a key backup using a passphrase. - * The decoded key (derivated from the passphrase) is store locally by calling {@link CryptoApi#storeSessionBackupPrivateKey}. + * The decoded key (derived from the passphrase) is stored locally by calling {@link CryptoApi#storeSessionBackupPrivateKey}. + * * @param passphrase - The passphrase to use to restore the key backup. * @param opts * diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index a18cf931be..e1e59bc312 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -505,6 +505,7 @@ export class RustBackupManager extends TypedEventEmitterkeyBackupInfo.auth_data; return authData.public_key === backupDecryptionKey.megolmV1PublicKey.publicKeyBase64; diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index aadd7f9397..19795fc7cb 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -77,7 +77,7 @@ import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts"; import { EventType, MsgType } from "../@types/event.ts"; import { TypedEventEmitter } from "../models/typed-event-emitter.ts"; -import { decryptionKeyMatchKeyBackupInfo, RustBackupManager } from "./backup.ts"; +import { decryptionKeyMatchesKeyBackupInfo, RustBackupManager } from "./backup.ts"; import { TypedReEmitter } from "../ReEmitter.ts"; import { randomString } from "../randomstring.ts"; import { ClientStoppedError } from "../errors.ts"; @@ -338,7 +338,7 @@ export class RustCrypto extends TypedEventEmitter Date: Thu, 7 Nov 2024 18:33:52 +0100 Subject: [PATCH 35/37] Use `RustSdkCryptoJs.BackupDecryptionKey` as `decryptionKeyMatchesKeyBackupInfo` parameter. --- src/rust-crypto/backup.ts | 9 +++++---- src/rust-crypto/rust-crypto.ts | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index e1e59bc312..87e293d918 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -38,7 +38,6 @@ 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"]; @@ -839,10 +838,12 @@ export async function requestKeyBackupVersion( * @param keyBackupInfo - The key backup info to check against. * @returns `true` if the decryption key matches the key backup info, `false` otherwise. */ -export function decryptionKeyMatchesKeyBackupInfo(decryptionKey: Uint8Array, keyBackupInfo: KeyBackupInfo): boolean { - const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(decryptionKey)); +export function decryptionKeyMatchesKeyBackupInfo( + decryptionKey: RustSdkCryptoJs.BackupDecryptionKey, + keyBackupInfo: KeyBackupInfo, +): boolean { const authData = keyBackupInfo.auth_data; - return authData.public_key === backupDecryptionKey.megolmV1PublicKey.publicKeyBase64; + return authData.public_key === decryptionKey.megolmV1PublicKey.publicKeyBase64; } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 19795fc7cb..2af7d013d7 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -338,11 +338,11 @@ export class RustCrypto extends TypedEventEmitter Date: Thu, 7 Nov 2024 18:39:55 +0100 Subject: [PATCH 36/37] Call directly `olmMachine.getBackupKeys` in `restoreKeyBackup` --- spec/integ/crypto/megolm-backup.spec.ts | 4 ++-- src/rust-crypto/rust-crypto.ts | 26 ++++++++----------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index e0a69e94bc..adc04678d7 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -660,8 +660,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe ).rejects.toThrow(); }); - newBackendOnly("Should throw an error if the decryption key is found in cache", async () => { - await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in cache"); + newBackendOnly("Should throw an error if the decryption key is not found in cache", async () => { + await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store"); }); }); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 2af7d013d7..d998ddbb17 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1181,15 +1181,6 @@ export class RustCrypto extends TypedEventEmitter { - const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys(); - return backupKeys.backupVersion || null; - } - /** * Store the backup decryption key. * @@ -1335,25 +1326,24 @@ export class RustCrypto extends TypedEventEmitter { - // Get the decryption key from the cache - const privateKeyFromCache = await this.getSessionBackupPrivateKey(); - if (!privateKeyFromCache) throw new Error("No decryption key found in cache"); + // Get the decryption key from the crypto store + const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys(); + const { decryptionKey, backupVersion } = backupKeys; + if (!decryptionKey || !backupVersion) throw new Error("No decryption key found in crypto store"); - // Get the backup version from the cache - const backupVersion = await this.getSessionBackupVersion(); - if (!backupVersion) throw new Error("No backup version found in cache"); + const decodedDecryptionKey = decodeBase64(decryptionKey.toBase64()); const backupInfo = await this.backupManager.requestKeyBackupVersion(backupVersion); - if (!backupInfo?.version) throw new Error("Missing version in backup info"); + if (!backupInfo) throw new Error(`Backup version to restore ${backupVersion} not found on server`); - const backupDecryptor = await this.getBackupDecryptor(backupInfo, privateKeyFromCache); + const backupDecryptor = await this.getBackupDecryptor(backupInfo, decodedDecryptionKey); try { opts?.progressCallback?.({ stage: "fetch", }); - return await this.backupManager.restoreKeyBackup(backupInfo.version, backupDecryptor, opts); + return await this.backupManager.restoreKeyBackup(backupVersion, backupDecryptor, opts); } finally { // Free to avoid to keep in memory the decryption key stored in it. To avoid to exposing it to an attacker. backupDecryptor.free(); From b2147918e9184651633e855d7eb4776afc4fc465 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 13 Nov 2024 10:13:06 +0100 Subject: [PATCH 37/37] Last review changes --- src/crypto-api/index.ts | 2 +- src/rust-crypto/backup.ts | 4 ++-- src/rust-crypto/rust-crypto.ts | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 86be823f71..8e288e9a4d 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -509,7 +509,7 @@ export interface CryptoApi { * if they match, stores the key in the crypto store by calling {@link storeSessionBackupPrivateKey}, * which enables automatic restore of individual keys when an Unable-to-decrypt error is encountered. * - * if we are unable to fetch the key from secret storage, there is no backup on the server, or the key + * If we are unable to fetch the key from secret storage, there is no backup on the server, or the key * does not match, throws an exception. */ loadSessionBackupPrivateKeyFromSecretStorage(): Promise; diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 87e293d918..18a3f7c679 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -635,7 +635,7 @@ export class RustBackupManager extends TypedEventEmitter