You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-09 10:22:46 +03:00
Add restoreKeybackup
to CryptoApi
. (#4476)
* First draft of moving out restoreKeyBackup out of MatrixClient * Deprecate `restoreKeyBackup*` in `MatrixClient` * Move types * Handle only the room keys response * Renaming and refactor `keysCountInBatch` & `getTotalKeyCount` * Fix `importRoomKeysAsJson` tsdoc * Fix typo * Move `backupDecryptor.free()`` * Comment and simplify a bit `handleDecryptionOfAFullBackup` * Fix decryption crash by moving`backupDecryptor.free` * Use new api in `megolm-backup.spec.ts` * Add tests to get recovery key from secret storage * Add doc to `KeyBackupRestoreOpts` & `KeyBackupRestoreResult` * Add doc to `restoreKeyBackupWithKey` * Add doc to `backup.ts` * Apply comment suggestions Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * - Decryption key is recovered from the cache in `RustCrypto.restoreKeyBackup` - Add `CryptoApi.getSecretStorageBackupPrivateKey` to get the decryption key from the secret storage. * Add `CryptoApi.restoreKeyBackup` to `ImportRoomKeyProgressData` doc. * Add deprecated symbol to all the `restoreKeyBackup*` overrides. * Update tests * Move `RustBackupManager.getTotalKeyCount` to `backup#calculateKeyCountInKeyBackup` * Fix `RustBackupManager.restoreKeyBackup` tsdoc * Move `backupDecryptor.free` in rust crypto. * Move `handleDecryptionOfAFullBackup` in `importKeyBackup` * Rename `calculateKeyCountInKeyBackup` to `countKeystInBackup` * Fix `passphrase` typo * Rename `backupInfoVersion` to `backupVersion` * Complete restoreKeyBackup* methods documentation * Add `loadSessionBackupPrivateKeyFromSecretStorage` * Remove useless intermediary result variable. * Check that decryption key matchs key backup info in `loadSessionBackupPrivateKeyFromSecretStorage` * Get backup info from a specific version * Fix typo in `countKeysInBackup` * Improve documentation and naming * Use `RustSdkCryptoJs.BackupDecryptionKey` as `decryptionKeyMatchesKeyBackupInfo` parameter. * Call directly `olmMachine.getBackupKeys` in `restoreKeyBackup` * Last review changes --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
|||||||
createClient,
|
createClient,
|
||||||
Crypto,
|
Crypto,
|
||||||
CryptoEvent,
|
CryptoEvent,
|
||||||
|
encodeBase64,
|
||||||
ICreateClientOpts,
|
ICreateClientOpts,
|
||||||
IEvent,
|
IEvent,
|
||||||
IMegolmSessionData,
|
IMegolmSessionData,
|
||||||
@@ -44,7 +45,7 @@ import * as testData from "../../test-utils/test-data";
|
|||||||
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||||
import { flushPromises } from "../../test-utils/flushPromises";
|
import { flushPromises } from "../../test-utils/flushPromises";
|
||||||
import { defer, IDeferred } from "../../../src/utils";
|
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";
|
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||||
|
|
||||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||||
@@ -117,8 +118,10 @@ function mockUploadEmitter(
|
|||||||
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
|
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
|
// 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.
|
// 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 newBackendOnly = backend === "libolm" ? test.skip : test;
|
||||||
|
|
||||||
|
const isNewBackend = backend === "rust-sdk";
|
||||||
|
|
||||||
let aliceClient: MatrixClient;
|
let aliceClient: MatrixClient;
|
||||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||||
@@ -247,9 +250,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
// On the first decryption attempt, decryption fails.
|
// On the first decryption attempt, decryption fails.
|
||||||
await awaitDecryption(event);
|
await awaitDecryption(event);
|
||||||
expect(event.decryptionFailureReason).toEqual(
|
expect(event.decryptionFailureReason).toEqual(
|
||||||
backend === "libolm"
|
isNewBackend
|
||||||
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
|
? DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP
|
||||||
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
|
: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Eventually, decryption succeeds.
|
// Eventually, decryption succeeds.
|
||||||
@@ -314,6 +317,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
|
|
||||||
beforeEach(async () => {
|
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);
|
||||||
|
fetchMock.get(
|
||||||
|
`path:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`,
|
||||||
|
testData.SIGNED_BACKUP_DATA,
|
||||||
|
);
|
||||||
|
|
||||||
aliceClient = await initTestClient();
|
aliceClient = await initTestClient();
|
||||||
aliceCrypto = aliceClient.getCrypto()!;
|
aliceCrypto = aliceClient.getCrypto()!;
|
||||||
@@ -344,20 +351,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
onKeyCached = resolve;
|
onKeyCached = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||||
|
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||||
|
check!.backupInfo!.version!,
|
||||||
|
);
|
||||||
|
|
||||||
const result = await advanceTimersUntil(
|
const result = await advanceTimersUntil(
|
||||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
isNewBackend
|
||||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
? aliceCrypto.restoreKeyBackup()
|
||||||
undefined,
|
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||||
undefined,
|
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||||
check!.backupInfo!,
|
undefined,
|
||||||
{
|
undefined,
|
||||||
cacheCompleteCallback: () => onKeyCached(),
|
check!.backupInfo!,
|
||||||
},
|
{
|
||||||
),
|
cacheCompleteCallback: () => onKeyCached(),
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.imported).toStrictEqual(1);
|
expect(result.imported).toStrictEqual(1);
|
||||||
|
|
||||||
|
if (isNewBackend) return;
|
||||||
|
|
||||||
await awaitKeyCached;
|
await awaitKeyCached;
|
||||||
|
|
||||||
// The key should be now cached
|
// The key should be now cached
|
||||||
@@ -398,8 +414,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
|
|
||||||
it("Should import full backup in chunks", async function () {
|
it("Should import full backup in chunks", async function () {
|
||||||
const importMockImpl = jest.fn();
|
const importMockImpl = jest.fn();
|
||||||
// @ts-ignore - mock a private method for testing purpose
|
if (isNewBackend) {
|
||||||
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
|
// @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
|
// We need several rooms with several sessions to test chunking
|
||||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||||
@@ -408,17 +429,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
|
|
||||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
|
|
||||||
const progressCallback = jest.fn();
|
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
check!.backupInfo!.version!,
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
check!.backupInfo!,
|
|
||||||
{
|
|
||||||
progressCallback,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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);
|
expect(result.imported).toStrictEqual(expectedTotal);
|
||||||
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
|
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
|
||||||
expect(importMockImpl).toHaveBeenCalledTimes(5);
|
expect(importMockImpl).toHaveBeenCalledTimes(5);
|
||||||
@@ -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 () {
|
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
|
const importMockImpl = jest
|
||||||
aliceCrypto.importBackedUpRoomKeys = jest
|
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
// Fail to import first chunk
|
// Fail to import first chunk
|
||||||
@@ -461,22 +490,36 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
// Ok for other chunks
|
// Ok for other chunks
|
||||||
.mockResolvedValue(undefined);
|
.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]);
|
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||||
|
|
||||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||||
|
|
||||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
|
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||||
|
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||||
|
check!.backupInfo!.version!,
|
||||||
|
);
|
||||||
|
|
||||||
const progressCallback = jest.fn();
|
const progressCallback = jest.fn();
|
||||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
const result = await (isNewBackend
|
||||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
? aliceCrypto.restoreKeyBackup({ progressCallback })
|
||||||
undefined,
|
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||||
undefined,
|
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||||
check!.backupInfo!,
|
undefined,
|
||||||
{
|
undefined,
|
||||||
progressCallback,
|
check!.backupInfo!,
|
||||||
},
|
{
|
||||||
);
|
progressCallback,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
expect(result.total).toStrictEqual(expectedTotal);
|
expect(result.total).toStrictEqual(expectedTotal);
|
||||||
// A chunk failed to import
|
// A chunk failed to import
|
||||||
@@ -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);
|
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||||
|
|
||||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||||
|
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
check!.backupInfo!.version!,
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
check!.backupInfo!,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const result = await (isNewBackend
|
||||||
|
? aliceCrypto.restoreKeyBackup()
|
||||||
|
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||||
|
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
check!.backupInfo!,
|
||||||
|
));
|
||||||
|
|
||||||
expect(result.total).toStrictEqual(expectedTotal);
|
expect(result.total).toStrictEqual(expectedTotal);
|
||||||
// A chunk failed to import
|
// A chunk failed to import
|
||||||
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
|
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("recover specific session from backup", async function () {
|
oldBackendOnly("recover specific session from backup", async function () {
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||||
@@ -560,7 +609,33 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
expect(result.imported).toStrictEqual(1);
|
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 = {
|
const fullBackup = {
|
||||||
rooms: {
|
rooms: {
|
||||||
[ROOM_ID]: {
|
[ROOM_ID]: {
|
||||||
@@ -584,6 +659,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
|||||||
),
|
),
|
||||||
).rejects.toThrow();
|
).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", () => {
|
describe("backupLoop", () => {
|
||||||
|
@@ -3703,6 +3703,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param opts - Optional params such as callbacks
|
* @param opts - Optional params such as callbacks
|
||||||
* @returns Status of restoration with `total` and `imported`
|
* @returns Status of restoration with `total` and `imported`
|
||||||
* key counts.
|
* key counts.
|
||||||
|
*
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||||
*/
|
*/
|
||||||
public async restoreKeyBackupWithPassword(
|
public async restoreKeyBackupWithPassword(
|
||||||
password: string,
|
password: string,
|
||||||
@@ -3711,6 +3713,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts: IKeyBackupRestoreOpts,
|
opts: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithPassword(
|
public async restoreKeyBackupWithPassword(
|
||||||
password: string,
|
password: string,
|
||||||
targetRoomId: string,
|
targetRoomId: string,
|
||||||
@@ -3718,6 +3723,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts: IKeyBackupRestoreOpts,
|
opts: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithPassword(
|
public async restoreKeyBackupWithPassword(
|
||||||
password: string,
|
password: string,
|
||||||
targetRoomId: string,
|
targetRoomId: string,
|
||||||
@@ -3725,6 +3733,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts: IKeyBackupRestoreOpts,
|
opts: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithPassword(
|
public async restoreKeyBackupWithPassword(
|
||||||
password: string,
|
password: string,
|
||||||
targetRoomId: string | undefined,
|
targetRoomId: string | undefined,
|
||||||
@@ -3748,6 +3759,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param opts - Optional params such as callbacks
|
* @param opts - Optional params such as callbacks
|
||||||
* @returns Status of restoration with `total` and `imported`
|
* @returns Status of restoration with `total` and `imported`
|
||||||
* key counts.
|
* key counts.
|
||||||
|
*
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
*/
|
*/
|
||||||
public async restoreKeyBackupWithSecretStorage(
|
public async restoreKeyBackupWithSecretStorage(
|
||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
@@ -3785,6 +3798,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
* @returns Status of restoration with `total` and `imported`
|
* @returns Status of restoration with `total` and `imported`
|
||||||
* key counts.
|
* key counts.
|
||||||
|
*
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
*/
|
*/
|
||||||
public restoreKeyBackupWithRecoveryKey(
|
public restoreKeyBackupWithRecoveryKey(
|
||||||
recoveryKey: string,
|
recoveryKey: string,
|
||||||
@@ -3793,6 +3808,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts?: IKeyBackupRestoreOpts,
|
opts?: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public restoreKeyBackupWithRecoveryKey(
|
public restoreKeyBackupWithRecoveryKey(
|
||||||
recoveryKey: string,
|
recoveryKey: string,
|
||||||
targetRoomId: string,
|
targetRoomId: string,
|
||||||
@@ -3800,6 +3818,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts?: IKeyBackupRestoreOpts,
|
opts?: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public restoreKeyBackupWithRecoveryKey(
|
public restoreKeyBackupWithRecoveryKey(
|
||||||
recoveryKey: string,
|
recoveryKey: string,
|
||||||
targetRoomId: string,
|
targetRoomId: string,
|
||||||
@@ -3807,6 +3828,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts?: IKeyBackupRestoreOpts,
|
opts?: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public restoreKeyBackupWithRecoveryKey(
|
public restoreKeyBackupWithRecoveryKey(
|
||||||
recoveryKey: string,
|
recoveryKey: string,
|
||||||
targetRoomId: string | undefined,
|
targetRoomId: string | undefined,
|
||||||
@@ -3818,24 +3842,42 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
|
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore from an existing key backup via a private key stored locally
|
||||||
|
* @param targetRoomId
|
||||||
|
* @param targetSessionId
|
||||||
|
* @param backupInfo
|
||||||
|
* @param opts
|
||||||
|
*
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithCache(
|
public async restoreKeyBackupWithCache(
|
||||||
targetRoomId: undefined,
|
targetRoomId: undefined,
|
||||||
targetSessionId: undefined,
|
targetSessionId: undefined,
|
||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts?: IKeyBackupRestoreOpts,
|
opts?: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithCache(
|
public async restoreKeyBackupWithCache(
|
||||||
targetRoomId: string,
|
targetRoomId: string,
|
||||||
targetSessionId: undefined,
|
targetSessionId: undefined,
|
||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts?: IKeyBackupRestoreOpts,
|
opts?: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithCache(
|
public async restoreKeyBackupWithCache(
|
||||||
targetRoomId: string,
|
targetRoomId: string,
|
||||||
targetSessionId: string,
|
targetSessionId: string,
|
||||||
backupInfo: IKeyBackupInfo,
|
backupInfo: IKeyBackupInfo,
|
||||||
opts?: IKeyBackupRestoreOpts,
|
opts?: IKeyBackupRestoreOpts,
|
||||||
): Promise<IKeyBackupRestoreResult>;
|
): Promise<IKeyBackupRestoreResult>;
|
||||||
|
/**
|
||||||
|
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||||
|
*/
|
||||||
public async restoreKeyBackupWithCache(
|
public async restoreKeyBackupWithCache(
|
||||||
targetRoomId: string | undefined,
|
targetRoomId: string | undefined,
|
||||||
targetSessionId: string | undefined,
|
targetSessionId: string | undefined,
|
||||||
|
@@ -22,7 +22,13 @@ import { DeviceMap } from "../models/device.ts";
|
|||||||
import { UIAuthCallback } from "../interactive-auth.ts";
|
import { UIAuthCallback } from "../interactive-auth.ts";
|
||||||
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage.ts";
|
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage.ts";
|
||||||
import { VerificationRequest } from "./verification.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 { ISignatures } from "../@types/signed.ts";
|
||||||
import { MatrixEvent } from "../models/event.ts";
|
import { MatrixEvent } from "../models/event.ts";
|
||||||
|
|
||||||
@@ -502,6 +508,18 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;
|
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current status of key backup.
|
* Get the current status of key backup.
|
||||||
*
|
*
|
||||||
@@ -545,6 +563,36 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
deleteKeyBackupVersion(version: string): Promise<void>;
|
deleteKeyBackupVersion(version: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<KeyBackupRestoreResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a key backup using a passphrase.
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @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(passphrase: string, opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Dehydrated devices
|
// Dehydrated devices
|
||||||
@@ -886,8 +934,8 @@ export class DeviceVerificationStatus {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Room key import progress report.
|
* Room key import progress report.
|
||||||
* Used when calling {@link CryptoApi#importRoomKeys} or
|
* Used when calling {@link CryptoApi#importRoomKeys},
|
||||||
* {@link CryptoApi#importRoomKeysAsJson} as the parameter of
|
* {@link CryptoApi#importRoomKeysAsJson} or {@link CryptoApi#restoreKeyBackup} as the parameter of
|
||||||
* the progressCallback. Used to display feedback.
|
* the progressCallback. Used to display feedback.
|
||||||
*/
|
*/
|
||||||
export interface ImportRoomKeyProgressData {
|
export interface ImportRoomKeyProgressData {
|
||||||
|
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { ISigned } from "../@types/signed.ts";
|
import { ISigned } from "../@types/signed.ts";
|
||||||
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||||
|
import { ImportRoomKeyProgressData } from "./index.ts";
|
||||||
|
|
||||||
export interface Curve25519AuthData {
|
export interface Curve25519AuthData {
|
||||||
public_key: string;
|
public_key: string;
|
||||||
@@ -87,3 +88,28 @@ export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecret
|
|||||||
export interface KeyBackupRoomSessions {
|
export interface KeyBackupRoomSessions {
|
||||||
[sessionId: string]: KeyBackupSession;
|
[sessionId: string]: KeyBackupSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra parameters for {@link CryptoApi.restoreKeyBackup} and {@link CryptoApi.restoreKeyBackupWithPassphrase}.
|
||||||
|
*/
|
||||||
|
export interface KeyBackupRestoreOpts {
|
||||||
|
/**
|
||||||
|
* A callback which, if defined, will be called periodically to report ongoing progress of the backup restore 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;
|
||||||
|
}
|
||||||
|
@@ -102,6 +102,8 @@ import {
|
|||||||
OwnDeviceKeys,
|
OwnDeviceKeys,
|
||||||
CryptoEvent as CryptoApiCryptoEvent,
|
CryptoEvent as CryptoApiCryptoEvent,
|
||||||
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
|
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
|
||||||
|
KeyBackupRestoreResult,
|
||||||
|
KeyBackupRestoreOpts,
|
||||||
} from "../crypto-api/index.ts";
|
} from "../crypto-api/index.ts";
|
||||||
import { Device, DeviceMap } from "../models/device.ts";
|
import { Device, DeviceMap } from "../models/device.ts";
|
||||||
import { deviceInfoToDevice } from "./device-converter.ts";
|
import { deviceInfoToDevice } from "./device-converter.ts";
|
||||||
@@ -1306,6 +1308,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link Crypto.loadSessionBackupPrivateKeyFromSecretStorage}.
|
||||||
|
*/
|
||||||
|
public loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void> {
|
||||||
|
throw new Error("Not implmeented");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current status of key backup.
|
* Get the current status of key backup.
|
||||||
*
|
*
|
||||||
@@ -4308,6 +4317,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub function -- restoreKeyBackup is not implemented here, so throw error
|
||||||
|
*/
|
||||||
|
public restoreKeyBackup(opts: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult> {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub function -- restoreKeyBackupWithPassphrase is not implemented here, so throw error
|
||||||
|
*/
|
||||||
|
public restoreKeyBackupWithPassphrase(
|
||||||
|
passphrase: string,
|
||||||
|
opts: KeyBackupRestoreOpts,
|
||||||
|
): Promise<KeyBackupRestoreResult> {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -24,6 +24,8 @@ import {
|
|||||||
KeyBackupInfo,
|
KeyBackupInfo,
|
||||||
KeyBackupSession,
|
KeyBackupSession,
|
||||||
Curve25519SessionData,
|
Curve25519SessionData,
|
||||||
|
KeyBackupRestoreOpts,
|
||||||
|
KeyBackupRestoreResult,
|
||||||
KeyBackupRoomSessions,
|
KeyBackupRoomSessions,
|
||||||
} from "../crypto-api/keybackup.ts";
|
} from "../crypto-api/keybackup.ts";
|
||||||
import { logger } from "../logger.ts";
|
import { logger } from "../logger.ts";
|
||||||
@@ -218,7 +220,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|||||||
/**
|
/**
|
||||||
* Import a list of room keys previously exported by exportRoomKeysAsJson
|
* Import a list of room keys previously exported by exportRoomKeysAsJson
|
||||||
*
|
*
|
||||||
* @param keys - a JSON string encoding a list of session export objects,
|
* @param jsonKeys - a JSON string encoding a list of session export objects,
|
||||||
* each of which is an IMegolmSessionData
|
* each of which is an IMegolmSessionData
|
||||||
* @param opts - options object
|
* @param opts - options object
|
||||||
* @returns a promise which resolves once the keys have been imported
|
* @returns a promise which resolves once the keys have been imported
|
||||||
@@ -495,20 +497,19 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|||||||
*/
|
*/
|
||||||
private keysCountInBatch(batch: RustSdkCryptoJs.KeysBackupRequest): number {
|
private keysCountInBatch(batch: RustSdkCryptoJs.KeysBackupRequest): number {
|
||||||
const parsedBody: KeyBackup = JSON.parse(batch.body);
|
const parsedBody: KeyBackup = JSON.parse(batch.body);
|
||||||
let count = 0;
|
return countKeysInBackup(parsedBody);
|
||||||
for (const { sessions } of Object.values(parsedBody.rooms)) {
|
|
||||||
count += Object.keys(sessions).length;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get information about the current key backup from the server
|
* Get information about a key backup from the server
|
||||||
|
* - If version is provided, get information about that backup version.
|
||||||
|
* - If no version is provided, get information about the latest backup.
|
||||||
*
|
*
|
||||||
|
* @param version - The version of the backup to get information about.
|
||||||
* @returns Information object from API or null if there is no active backup.
|
* @returns Information object from API or null if there is no active backup.
|
||||||
*/
|
*/
|
||||||
private async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
|
public async requestKeyBackupVersion(version?: string): Promise<KeyBackupInfo | null> {
|
||||||
return await requestKeyBackupVersion(this.http);
|
return await requestKeyBackupVersion(this.http, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -591,6 +592,148 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|||||||
public createBackupDecryptor(decryptionKey: RustSdkCryptoJs.BackupDecryptionKey): BackupDecryptor {
|
public createBackupDecryptor(decryptionKey: RustSdkCryptoJs.BackupDecryptionKey): BackupDecryptor {
|
||||||
return new RustBackupDecryptor(decryptionKey);
|
return new RustBackupDecryptor(decryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore a key backup.
|
||||||
|
*
|
||||||
|
* @param backupVersion - The version of the backup to restore.
|
||||||
|
* @param backupDecryptor - The backup decryptor to use to decrypt the keys.
|
||||||
|
* @param opts - Options for the restore.
|
||||||
|
* @returns The total number of keys and the total imported.
|
||||||
|
*/
|
||||||
|
public async restoreKeyBackup(
|
||||||
|
backupVersion: string,
|
||||||
|
backupDecryptor: BackupDecryptor,
|
||||||
|
opts?: KeyBackupRestoreOpts,
|
||||||
|
): Promise<KeyBackupRestoreResult> {
|
||||||
|
const keyBackup = await this.downloadKeyBackup(backupVersion);
|
||||||
|
opts?.progressCallback?.({
|
||||||
|
stage: "load_keys",
|
||||||
|
});
|
||||||
|
|
||||||
|
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/v1.12/client-server-api/#get_matrixclientv3room_keyskeys
|
||||||
|
*
|
||||||
|
* @param backupVersion
|
||||||
|
* @returns The key backup response.
|
||||||
|
*/
|
||||||
|
private downloadKeyBackup(backupVersion: string): Promise<KeyBackup> {
|
||||||
|
return this.http.authedRequest<KeyBackup>(
|
||||||
|
Method.Get,
|
||||||
|
"/room_keys/keys",
|
||||||
|
{ version: backupVersion },
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
prefix: ClientPrefix.V3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the room keys from a `/room_keys/keys` call.
|
||||||
|
* Calls `opts.progressCallback` with the progress of the import.
|
||||||
|
*
|
||||||
|
* @param keyBackup - The response from the server containing the keys to import.
|
||||||
|
* @param backupVersion - The version of the backup info.
|
||||||
|
* @param backupDecryptor - The backup decryptor to use to decrypt the keys.
|
||||||
|
* @param opts - Options for the import.
|
||||||
|
*
|
||||||
|
* @returns The total number of keys and the total imported.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async importKeyBackup(
|
||||||
|
keyBackup: KeyBackup,
|
||||||
|
backupVersion: string,
|
||||||
|
backupDecryptor: BackupDecryptor,
|
||||||
|
opts?: KeyBackupRestoreOpts,
|
||||||
|
): Promise<KeyBackupRestoreResult> {
|
||||||
|
// We have a full backup here, it can get quite big, so we need to decrypt and import it in chunks.
|
||||||
|
|
||||||
|
const CHUNK_SIZE = 200;
|
||||||
|
// Get the total count as a first pass
|
||||||
|
const totalKeyCount = countKeysInBackup(keyBackup);
|
||||||
|
let totalImported = 0;
|
||||||
|
let totalFailures = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when we have enough chunks to decrypt.
|
||||||
|
* It will decrypt the chunks and try to import the room keys.
|
||||||
|
* @param roomChunks
|
||||||
|
*/
|
||||||
|
const handleChunkCallback = async (roomChunks: Map<string, KeyBackupRoomSessions>): Promise<void> => {
|
||||||
|
const currentChunk: IMegolmSessionData[] = [];
|
||||||
|
for (const roomId of roomChunks.keys()) {
|
||||||
|
// Decrypt the sessions for the given room
|
||||||
|
const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!);
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a chunk of decrypted keys: import them
|
||||||
|
try {
|
||||||
|
await this.importBackedUpRoomKeys(currentChunk, backupVersion);
|
||||||
|
totalImported += currentChunk.length;
|
||||||
|
} catch (e) {
|
||||||
|
totalFailures += currentChunk.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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let groupChunkCount = 0;
|
||||||
|
let chunkGroupByRoom: Map<string, KeyBackupRoomSessions> = new Map();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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 >= CHUNK_SIZE) {
|
||||||
|
// 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, {});
|
||||||
|
groupChunkCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle remaining chunk if needed
|
||||||
|
if (groupChunkCount > 0) {
|
||||||
|
await handleChunkCallback(chunkGroupByRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total: totalKeyCount, imported: totalImported };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -657,11 +800,26 @@ export class RustBackupDecryptor implements BackupDecryptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a key backup info from the server.
|
||||||
|
*
|
||||||
|
* 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 gets the latest backup info.
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
export async function requestKeyBackupVersion(
|
export async function requestKeyBackupVersion(
|
||||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
|
version?: string,
|
||||||
): Promise<KeyBackupInfo | null> {
|
): Promise<KeyBackupInfo | null> {
|
||||||
try {
|
try {
|
||||||
return await http.authedRequest<KeyBackupInfo>(Method.Get, "/room_keys/version", undefined, undefined, {
|
const path = version ? encodeUri("/room_keys/version/$version", { $version: version }) : "/room_keys/version";
|
||||||
|
return await http.authedRequest<KeyBackupInfo>(Method.Get, path, undefined, undefined, {
|
||||||
prefix: ClientPrefix.V3,
|
prefix: ClientPrefix.V3,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -673,6 +831,34 @@ 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 decryptionKeyMatchesKeyBackupInfo(
|
||||||
|
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey,
|
||||||
|
keyBackupInfo: KeyBackupInfo,
|
||||||
|
): boolean {
|
||||||
|
const authData = <Curve25519AuthData>keyBackupInfo.auth_data;
|
||||||
|
return authData.public_key === decryptionKey.megolmV1PublicKey.publicKeyBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the total number of keys present in a key backup.
|
||||||
|
* @param keyBackup - The key backup to count the keys from.
|
||||||
|
* @returns The total number of keys in the backup.
|
||||||
|
*/
|
||||||
|
function countKeysInBackup(keyBackup: KeyBackup): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const { sessions } of Object.values(keyBackup.rooms)) {
|
||||||
|
count += Object.keys(sessions).length;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
export type RustBackupCryptoEvents =
|
export type RustBackupCryptoEvents =
|
||||||
| CryptoEvent.KeyBackupStatus
|
| CryptoEvent.KeyBackupStatus
|
||||||
| CryptoEvent.KeyBackupSessionsRemaining
|
| CryptoEvent.KeyBackupSessionsRemaining
|
||||||
|
@@ -46,7 +46,6 @@ import {
|
|||||||
CrossSigningStatus,
|
CrossSigningStatus,
|
||||||
CryptoApi,
|
CryptoApi,
|
||||||
CryptoCallbacks,
|
CryptoCallbacks,
|
||||||
Curve25519AuthData,
|
|
||||||
DecryptionFailureCode,
|
DecryptionFailureCode,
|
||||||
DeviceVerificationStatus,
|
DeviceVerificationStatus,
|
||||||
EventEncryptionInfo,
|
EventEncryptionInfo,
|
||||||
@@ -66,6 +65,8 @@ import {
|
|||||||
DeviceIsolationModeKind,
|
DeviceIsolationModeKind,
|
||||||
CryptoEvent,
|
CryptoEvent,
|
||||||
CryptoEventHandlerMap,
|
CryptoEventHandlerMap,
|
||||||
|
KeyBackupRestoreOpts,
|
||||||
|
KeyBackupRestoreResult,
|
||||||
} from "../crypto-api/index.ts";
|
} from "../crypto-api/index.ts";
|
||||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
|
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
|
||||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
|
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
|
||||||
@@ -76,16 +77,17 @@ import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys }
|
|||||||
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
|
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
|
||||||
import { EventType, MsgType } from "../@types/event.ts";
|
import { EventType, MsgType } from "../@types/event.ts";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||||
import { RustBackupManager } from "./backup.ts";
|
import { decryptionKeyMatchesKeyBackupInfo, RustBackupManager } from "./backup.ts";
|
||||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||||
import { randomString } from "../randomstring.ts";
|
import { randomString } from "../randomstring.ts";
|
||||||
import { ClientStoppedError } from "../errors.ts";
|
import { ClientStoppedError } from "../errors.ts";
|
||||||
import { ISignatures } from "../@types/signed.ts";
|
import { ISignatures } from "../@types/signed.ts";
|
||||||
import { encodeBase64 } from "../base64.ts";
|
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||||
import { OutgoingRequestsManager } from "./OutgoingRequestsManager.ts";
|
import { OutgoingRequestsManager } from "./OutgoingRequestsManager.ts";
|
||||||
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.ts";
|
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.ts";
|
||||||
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
|
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
|
||||||
import { VerificationMethod } from "../types.ts";
|
import { VerificationMethod } from "../types.ts";
|
||||||
|
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
|
||||||
|
|
||||||
const ALL_VERIFICATION_METHODS = [
|
const ALL_VERIFICATION_METHODS = [
|
||||||
VerificationMethod.Sas,
|
VerificationMethod.Sas,
|
||||||
@@ -337,9 +339,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
|
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
|
||||||
|
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, backupInfo)) {
|
||||||
const authData = <Curve25519AuthData>backupInfo.auth_data;
|
|
||||||
if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) {
|
|
||||||
throw new Error(`getBackupDecryptor: key backup on server does not match the decryption key`);
|
throw new Error(`getBackupDecryptor: key backup on server does not match the decryption key`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,6 +1202,28 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi#loadSessionBackupPrivateKeyFromSecretStorage}.
|
||||||
|
*/
|
||||||
|
public async loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void> {
|
||||||
|
const backupKey = await this.secretStorage.get("m.megolm_backup.v1");
|
||||||
|
if (!backupKey) {
|
||||||
|
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: missing decryption key in secret storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyBackupInfo = await this.backupManager.getServerBackupInfo();
|
||||||
|
if (!keyBackupInfo || !keyBackupInfo.version) {
|
||||||
|
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: unable to get backup version");
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(backupKey);
|
||||||
|
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, keyBackupInfo)) {
|
||||||
|
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: decryption key does not match backup info");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.backupManager.saveBackupDecryptionKey(backupDecryptionKey, keyBackupInfo.version);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current status of key backup.
|
* Get the current status of key backup.
|
||||||
*
|
*
|
||||||
@@ -1280,6 +1302,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
obj.signatures = Object.fromEntries(sigs.entries());
|
obj.signatures = Object.fromEntries(sigs.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi#restoreKeyBackupWithPassphrase}.
|
||||||
|
*/
|
||||||
|
public async restoreKeyBackupWithPassphrase(
|
||||||
|
passphrase: string,
|
||||||
|
opts?: KeyBackupRestoreOpts,
|
||||||
|
): Promise<KeyBackupRestoreResult> {
|
||||||
|
const backupInfo = await this.backupManager.getServerBackupInfo();
|
||||||
|
if (!backupInfo?.version) {
|
||||||
|
throw new Error("No backup info available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKey = await keyFromAuthData(backupInfo.auth_data, passphrase);
|
||||||
|
|
||||||
|
// Cache the key
|
||||||
|
await this.storeSessionBackupPrivateKey(privateKey, backupInfo.version);
|
||||||
|
return this.restoreKeyBackup(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi#restoreKeyBackup}.
|
||||||
|
*/
|
||||||
|
public async restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult> {
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
const decodedDecryptionKey = decodeBase64(decryptionKey.toBase64());
|
||||||
|
|
||||||
|
const backupInfo = await this.backupManager.requestKeyBackupVersion(backupVersion);
|
||||||
|
if (!backupInfo) throw new Error(`Backup version to restore ${backupVersion} not found on server`);
|
||||||
|
|
||||||
|
const backupDecryptor = await this.getBackupDecryptor(backupInfo, decodedDecryptionKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
opts?.progressCallback?.({
|
||||||
|
stage: "fetch",
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoApi#isDehydrationSupported}.
|
* Implementation of {@link CryptoApi#isDehydrationSupported}.
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user