Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add restoreKeybackup to CryptoApi. #4476

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a1f18cf
First draft of moving out restoreKeyBackup out of MatrixClient
florianduros Oct 29, 2024
61c1940
Deprecate `restoreKeyBackup*` in `MatrixClient`
florianduros Oct 29, 2024
a0dc1e8
Move types
florianduros Oct 29, 2024
d385c72
Handle only the room keys response
florianduros Oct 29, 2024
8ba0416
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/restor…
florianduros Oct 30, 2024
61ba3d2
Renaming and refactor `keysCountInBatch` & `getTotalKeyCount`
florianduros Oct 30, 2024
3b8b4e1
Fix `importRoomKeysAsJson` tsdoc
florianduros Oct 30, 2024
a50b3d5
Fix typo
florianduros Oct 30, 2024
7f35274
Move `backupDecryptor.free()``
florianduros Oct 30, 2024
0192809
Comment and simplify a bit `handleDecryptionOfAFullBackup`
florianduros Oct 30, 2024
f9b5966
Fix decryption crash by moving`backupDecryptor.free`
florianduros Oct 30, 2024
95e55a1
Use new api in `megolm-backup.spec.ts`
florianduros Oct 30, 2024
b02d245
Add tests to get recovery key from secret storage
florianduros Oct 31, 2024
9f7fb5d
Add doc to `KeyBackupRestoreOpts` & `KeyBackupRestoreResult`
florianduros Oct 31, 2024
df83906
Add doc to `restoreKeyBackupWithKey`
florianduros Oct 31, 2024
b057b6e
Add doc to `backup.ts`
florianduros Oct 31, 2024
e9df34b
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/restor…
florianduros Nov 4, 2024
c130e83
Apply comment suggestions
florianduros Nov 4, 2024
7e48a52
- Decryption key is recovered from the cache in `RustCrypto.restoreKe…
florianduros Nov 4, 2024
fbd8d63
Add `CryptoApi.restoreKeyBackup` to `ImportRoomKeyProgressData` doc.
florianduros Nov 4, 2024
d5bc824
Add deprecated symbol to all the `restoreKeyBackup*` overrides.
florianduros Nov 4, 2024
698dd93
Update tests
florianduros Nov 4, 2024
cec2c89
Move `RustBackupManager.getTotalKeyCount` to `backup#calculateKeyCoun…
florianduros Nov 4, 2024
6fd8b1d
Fix `RustBackupManager.restoreKeyBackup` tsdoc
florianduros Nov 4, 2024
9f86663
Move `backupDecryptor.free` in rust crypto.
florianduros Nov 4, 2024
beed963
Move `handleDecryptionOfAFullBackup` in `importKeyBackup`
florianduros Nov 5, 2024
a2582a7
Rename `calculateKeyCountInKeyBackup` to `countKeystInBackup`
florianduros Nov 6, 2024
eeb1dce
Fix `passphrase` typo
florianduros Nov 6, 2024
e0f8913
Rename `backupInfoVersion` to `backupVersion`
florianduros Nov 6, 2024
fe3ea7c
Complete restoreKeyBackup* methods documentation
florianduros Nov 6, 2024
e55aee9
Add `loadSessionBackupPrivateKeyFromSecretStorage`
florianduros Nov 6, 2024
c47066f
Remove useless intermediary result variable.
florianduros Nov 7, 2024
a70ee65
Check that decryption key matchs key backup info in `loadSessionBacku…
florianduros Nov 7, 2024
0112d37
Get backup info from a specific version
florianduros Nov 7, 2024
250b7e9
Fix typo in `countKeysInBackup`
florianduros Nov 7, 2024
37793c5
Improve documentation and naming
florianduros Nov 7, 2024
c063d93
Use `RustSdkCryptoJs.BackupDecryptionKey` as `decryptionKeyMatchesKey…
florianduros Nov 7, 2024
787649a
Call directly `olmMachine.getBackupKeys` in `restoreKeyBackup`
florianduros Nov 7, 2024
b214791
Last review changes
florianduros Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down
26 changes: 21 additions & 5 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,14 @@ export interface CryptoApi {
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;

/**
* Fetch the backup decryption key from the secret storage, fetch the backup info version.
* Store locally the key and the backup info version by calling {@link storeSessionBackupPrivateKey}.
* 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
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* does not match, throws an exception.
*/
loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void>;

Expand Down Expand Up @@ -552,18 +558,28 @@ export interface CryptoApi {
deleteKeyBackupVersion(version: string): Promise<void>;

/**
* 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.
*
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param opts
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;

/**
* Restores a key backup using a passphrase.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* 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.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param opts
*
Expand Down
26 changes: 16 additions & 10 deletions src/rust-crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -505,6 +504,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
* Get information about a key backup from the server
* - If version is provided, get information about that backup version.
* - If no version is provided, get information about the latest backup.
*
* @param version - The version of the backup to get information about.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @returns Information object from API or null if there is no active backup.
*/
Expand Down Expand Up @@ -616,7 +616,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,

/**
* 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
* https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3room_keyskeys
*
* @param backupVersion
* @returns The key backup response.
Expand All @@ -635,7 +635,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,

/**
* Import the room keys from a `/room_keys/keys` call.
* Call the opts.progressCallback with the progress of the import.
* Calls the `opts.progressCallback` with the progress of the import.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param keyBackup - The response from the server containing the keys to import.
* @param backupVersion - The version of the backup info.
Expand Down Expand Up @@ -802,10 +802,13 @@ 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
*
* If `version` is provided, calls `GET /room_keys/version/$version` and gets the backup info for that version.
* See https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3room_keysversionversion.
*
* If not, calls `GET /room_keys/version` and get the latest backup info.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* See https://spec.matrix.org/v1.12/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.
Expand All @@ -830,14 +833,17 @@ 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.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
* @param keyBackupInfo - The key backup info to check against.
* @returns `true` if the decryption key matches the key backup info, `false` otherwise.
*/
export function decryptionKeyMatchKeyBackupInfo(decryptionKey: Uint8Array, keyBackupInfo: KeyBackupInfo): boolean {
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(decryptionKey));
export function decryptionKeyMatchesKeyBackupInfo(
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey,
keyBackupInfo: KeyBackupInfo,
): boolean {
const authData = <Curve25519AuthData>keyBackupInfo.auth_data;
return authData.public_key === backupDecryptionKey.megolmV1PublicKey.publicKeyBase64;
return authData.public_key === decryptionKey.megolmV1PublicKey.publicKeyBase64;
}

/**
Expand Down
35 changes: 13 additions & 22 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -338,11 +338,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
throw new Error(`getBackupDecryptor: Unsupported algorithm ${backupInfo.algorithm}`);
}

if (!decryptionKeyMatchKeyBackupInfo(privKey, backupInfo)) {
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, 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);
}

Expand Down Expand Up @@ -1181,15 +1181,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
return Buffer.from(backupKeys.decryptionKey.toBase64(), "base64");
}

/**
* Fetch the backup version we have saved in our store.
* @returns the backup version, if any, or null
*/
private async getSessionBackupVersion(): Promise<string | null> {
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
return backupKeys.backupVersion || null;
}

/**
* Store the backup decryption key.
*
Expand Down Expand Up @@ -1226,7 +1217,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: unable to get backup version");
}

if (!decryptionKeyMatchKeyBackupInfo(decodedKey, keyBackupInfo)) {
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(backupKey);
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, keyBackupInfo)) {
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: decryption key does not match backup info");
}

Expand Down Expand Up @@ -1334,25 +1326,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#restoreKeyBackup}.
*/
public async restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult> {
// 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();
Expand Down