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 38 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
169 changes: 124 additions & 45 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
createClient,
Crypto,
CryptoEvent,
encodeBase64,
ICreateClientOpts,
IEvent,
IMegolmSessionData,
Expand All @@ -44,7 +45,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;
Expand Down Expand Up @@ -117,8 +118,10 @@ function mockUploadEmitter(
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// const newBackendOnly = backend === "libolm" ? test.skip : test;
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend === "libolm" ? test.skip : test;

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

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

// Eventually, decryption succeeds.
Expand Down Expand Up @@ -314,6 +317,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe

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

aliceClient = await initTestClient();
aliceCrypto = aliceClient.getCrypto()!;
Expand Down Expand Up @@ -344,20 +351,29 @@ 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(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
);

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

if (isNewBackend) return;

await awaitKeyCached;

// The key should be now cached
Expand Down Expand Up @@ -398,8 +414,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe

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

// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
Expand All @@ -408,17 +429,26 @@ 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,
},
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);

const progressCallback = jest.fn();
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({
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
expect(importMockImpl).toHaveBeenCalledTimes(5);
Expand Down Expand Up @@ -451,8 +481,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});

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

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

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

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

const check = await aliceCrypto.checkKeyBackupAndEnable();
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);

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({ progressCallback })
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));

expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
Expand Down Expand Up @@ -527,20 +570,26 @@ 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();

const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);

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

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

it("recover specific session from backup", async function () {
oldBackendOnly("recover specific session from backup", async function () {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
Expand All @@ -560,7 +609,33 @@ 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);

await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);

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

oldBackendOnly("Fails on bad recovery key", async function () {
const fullBackup = {
rooms: {
[ROOM_ID]: {
Expand All @@ -584,6 +659,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
),
).rejects.toThrow();
});

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");
});
});

describe("backupLoop", () => {
Expand Down
Loading