You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Provide more options for starting dehydration (#4664)
* provide more options for starting dehydration * improve doc comments and tests
This commit is contained in:
@@ -1894,6 +1894,193 @@ describe("RustCrypto", () => {
|
|||||||
await rehydrationCompletedPromise;
|
await rehydrationCompletedPromise;
|
||||||
await rustCrypto2.stop();
|
await rustCrypto2.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("start dehydration options", () => {
|
||||||
|
let rustCrypto: RustCrypto;
|
||||||
|
let secretStorage: ServerSideSecretStorageImpl;
|
||||||
|
let dehydratedDeviceInfo: Record<string, any> | undefined;
|
||||||
|
|
||||||
|
// Function that is called when `GET /dehydrated_device` is called
|
||||||
|
// (i.e. when we try to rehydrate a device)
|
||||||
|
const getDehydratedDeviceMock = jest.fn(() => {
|
||||||
|
if (dehydratedDeviceInfo) {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: dehydratedDeviceInfo,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: 404,
|
||||||
|
body: {
|
||||||
|
errcode: "M_NOT_FOUND",
|
||||||
|
error: "Not found",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Function that is called when `PUT /dehydrated_device` is called
|
||||||
|
// (i.e. when we create a new dehydrated device)
|
||||||
|
const putDehydratedDeviceMock = jest.fn((path, opts) => {
|
||||||
|
const content = JSON.parse(opts.body as string);
|
||||||
|
dehydratedDeviceInfo = {
|
||||||
|
device_id: content.device_id,
|
||||||
|
device_data: content.device_data,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
device_id: content.device_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Set up a RustCrypto object with secret storage and cross-signing.
|
||||||
|
const secretStorageCallbacks = {
|
||||||
|
getSecretStorageKey: async (keys: any, name: string) => {
|
||||||
|
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||||
|
},
|
||||||
|
} as SecretStorageCallbacks;
|
||||||
|
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
|
||||||
|
|
||||||
|
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
|
||||||
|
const e2eKeyResponder = new E2EKeyResponder("http://server");
|
||||||
|
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
|
||||||
|
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||||
|
status: 404,
|
||||||
|
body: {
|
||||||
|
errcode: "M_NOT_FOUND",
|
||||||
|
error: "Not found",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
|
||||||
|
status: 200,
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
|
||||||
|
status: 200,
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
|
||||||
|
|
||||||
|
// dehydration requires secret storage and cross signing
|
||||||
|
async function createSecretStorageKey() {
|
||||||
|
return {
|
||||||
|
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||||
|
privateKey: new Uint8Array(32),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||||
|
await rustCrypto.bootstrapSecretStorage({
|
||||||
|
createSecretStorageKey,
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
setupNewKeyBackup: false,
|
||||||
|
});
|
||||||
|
// we need to process a sync so that the OlmMachine will upload keys
|
||||||
|
await rustCrypto.preprocessToDeviceMessages([]);
|
||||||
|
await rustCrypto.onSyncCompleted({});
|
||||||
|
|
||||||
|
// set up mocks needed for device dehydration
|
||||||
|
dehydratedDeviceInfo = undefined;
|
||||||
|
fetchMock.get(
|
||||||
|
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||||
|
getDehydratedDeviceMock,
|
||||||
|
);
|
||||||
|
fetchMock.put(
|
||||||
|
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||||
|
putDehydratedDeviceMock,
|
||||||
|
);
|
||||||
|
fetchMock.post(/_matrix\/client\/unstable\/org.matrix.msc3814.v1\/dehydrated_device\/.*\/events/, {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
events: [],
|
||||||
|
next_batch: "foo",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
getDehydratedDeviceMock.mockClear();
|
||||||
|
putDehydratedDeviceMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rustCrypto.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Several tests require a dehydrated device and dehydration key
|
||||||
|
// already set up.
|
||||||
|
async function setUpInitialDehydratedDevice() {
|
||||||
|
await rustCrypto.startDehydration();
|
||||||
|
getDehydratedDeviceMock.mockClear();
|
||||||
|
putDehydratedDeviceMock.mockClear();
|
||||||
|
return await secretStorage.get("org.matrix.msc3814");
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should create a new key and dehydrate a device when no options given", async () => {
|
||||||
|
// With the default options, when we don't have an existing key ...
|
||||||
|
await rustCrypto.startDehydration();
|
||||||
|
// ... we create a new dehydration key ...
|
||||||
|
expect(await secretStorage.get("org.matrix.msc3814")).toBeTruthy();
|
||||||
|
// ... and create a new dehydrated device.
|
||||||
|
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should rehydrate a device if available and keep existing key when no options given", async () => {
|
||||||
|
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||||
|
|
||||||
|
// If we already have a dehydration key and dehydrated device...
|
||||||
|
await rustCrypto.startDehydration();
|
||||||
|
// ... we should fetch the device to rehydrate it ...
|
||||||
|
expect(getDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
// ... create a new dehydrated device ...
|
||||||
|
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
// ... and keep the same dehydration key.
|
||||||
|
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should do nothing if onlyIfKeyCached is true and we have no key cached", async () => {
|
||||||
|
// Since there is no key cached, so should do nothing. i.e. it
|
||||||
|
// should not make any HTTP requests and should not create a new key.
|
||||||
|
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
|
||||||
|
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
|
||||||
|
expect(putDehydratedDeviceMock).not.toHaveBeenCalled();
|
||||||
|
expect(await secretStorage.get("org.matrix.msc3814")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should start dehydration when onlyIfKeyCached is true, and we have a cached key", async () => {
|
||||||
|
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||||
|
|
||||||
|
// If `onlyIfKeyCached` is `true`, and we already have have a
|
||||||
|
// key, we should behave the same as if no options were given.
|
||||||
|
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
|
||||||
|
expect(getDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not rehydrate if rehydrate is set to false", async () => {
|
||||||
|
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||||
|
|
||||||
|
// If `rehydrate` is set to `false` ...
|
||||||
|
await rustCrypto.startDehydration({ rehydrate: false });
|
||||||
|
// ... we should not try to rehydrate ...
|
||||||
|
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
|
||||||
|
// ... but we should still create a new dehydrated device ...
|
||||||
|
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
// ... and we should keep the same dehydration key.
|
||||||
|
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a new key if createNewKey is set to true", async () => {
|
||||||
|
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||||
|
|
||||||
|
// If `createNewKey` is set to `true` ...
|
||||||
|
await rustCrypto.startDehydration({ createNewKey: true });
|
||||||
|
// ... we should rehydrate and dehydrate as normal ...
|
||||||
|
expect(getDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||||
|
// ... and we should create a new dehydration key.
|
||||||
|
expect(await secretStorage.get("org.matrix.msc3814")).not.toEqual(origDehydrationKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("import & export secrets bundle", () => {
|
describe("import & export secrets bundle", () => {
|
||||||
|
@@ -41,6 +41,37 @@ import { MatrixEvent } from "../models/event.ts";
|
|||||||
* @packageDocumentation
|
* @packageDocumentation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options to start device dehydration.
|
||||||
|
*/
|
||||||
|
export interface StartDehydrationOpts {
|
||||||
|
/**
|
||||||
|
* Force creation of a new dehydration key, even if there is already an
|
||||||
|
* existing dehydration key. If `false`, and `onlyIfKeyCached` is `false`, a
|
||||||
|
* new key will be created if there is no existing dehydration key, whether
|
||||||
|
* already cached in our local storage or stored in Secret Storage.
|
||||||
|
*
|
||||||
|
* Checking for the presence of the key in Secret Storage may result in the
|
||||||
|
* `getSecretStorageKey` callback being called.
|
||||||
|
*
|
||||||
|
* Defaults to `false`.
|
||||||
|
*/
|
||||||
|
createNewKey?: boolean;
|
||||||
|
/**
|
||||||
|
* Only start dehydration if we have a dehydration key cached in our local
|
||||||
|
* storage. If `true`, Secret Storage will not be checked. Defaults to
|
||||||
|
* `false`.
|
||||||
|
*/
|
||||||
|
onlyIfKeyCached?: boolean;
|
||||||
|
/**
|
||||||
|
* Try to rehydrate a device before creating a new dehydrated device.
|
||||||
|
* Setting this to `false` may be useful for situations where the client is
|
||||||
|
* known to pre-date the dehydrated device, and so rehydration is
|
||||||
|
* unnecessary. Defaults to `true`.
|
||||||
|
*/
|
||||||
|
rehydrate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public interface to the cryptography parts of the js-sdk
|
* Public interface to the cryptography parts of the js-sdk
|
||||||
*
|
*
|
||||||
@@ -649,10 +680,11 @@ export interface CryptoApi {
|
|||||||
/**
|
/**
|
||||||
* Start using device dehydration.
|
* Start using device dehydration.
|
||||||
*
|
*
|
||||||
* - Rehydrates a dehydrated device, if one is available.
|
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
|
||||||
|
* is `true`.
|
||||||
* - Creates a new dehydration key, if necessary, and stores it in Secret
|
* - Creates a new dehydration key, if necessary, and stores it in Secret
|
||||||
* Storage.
|
* Storage.
|
||||||
* - If `createNewKey` is set to true, always creates a new key.
|
* - If `opts.createNewKey` is set to true, always creates a new key.
|
||||||
* - If a dehydration key is not available, creates a new one.
|
* - If a dehydration key is not available, creates a new one.
|
||||||
* - Creates a new dehydrated device, and schedules periodically creating
|
* - Creates a new dehydrated device, and schedules periodically creating
|
||||||
* new dehydrated devices.
|
* new dehydrated devices.
|
||||||
@@ -661,11 +693,11 @@ export interface CryptoApi {
|
|||||||
* `true`, and must not be called until after cross-signing and secret
|
* `true`, and must not be called until after cross-signing and secret
|
||||||
* storage have been set up.
|
* storage have been set up.
|
||||||
*
|
*
|
||||||
* @param createNewKey - whether to force creation of a new dehydration key.
|
* @param opts - options for device dehydration. For backwards compatibility
|
||||||
* This can be used, for example, if Secret Storage is being reset. Defaults
|
* with old code, a boolean can be given here, which will be treated as
|
||||||
* to false.
|
* the `createNewKey` option. However, this is deprecated.
|
||||||
*/
|
*/
|
||||||
startDehydration(createNewKey?: boolean): Promise<void>;
|
startDehydration(opts?: StartDehydrationOpts | boolean): Promise<void>;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
|
@@ -105,6 +105,7 @@ import {
|
|||||||
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
|
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
|
||||||
KeyBackupRestoreResult,
|
KeyBackupRestoreResult,
|
||||||
KeyBackupRestoreOpts,
|
KeyBackupRestoreOpts,
|
||||||
|
StartDehydrationOpts,
|
||||||
} 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";
|
||||||
@@ -4327,7 +4328,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
/**
|
/**
|
||||||
* Stub function -- dehydration is not implemented here, so throw error
|
* Stub function -- dehydration is not implemented here, so throw error
|
||||||
*/
|
*/
|
||||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
public async startDehydration(createNewKey?: StartDehydrationOpts | boolean): Promise<void> {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ import { IToDeviceEvent } from "../sync-accumulator.ts";
|
|||||||
import { ServerSideSecretStorage } from "../secret-storage.ts";
|
import { ServerSideSecretStorage } from "../secret-storage.ts";
|
||||||
import { decodeBase64 } from "../base64.ts";
|
import { decodeBase64 } from "../base64.ts";
|
||||||
import { Logger } from "../logger.ts";
|
import { Logger } from "../logger.ts";
|
||||||
import { CryptoEvent, CryptoEventHandlerMap } from "../crypto-api/index.ts";
|
import { CryptoEvent, CryptoEventHandlerMap, StartDehydrationOpts } from "../crypto-api/index.ts";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,19 +121,29 @@ export class DehydratedDeviceManager extends TypedEventEmitter<DehydratedDevices
|
|||||||
/**
|
/**
|
||||||
* Start using device dehydration.
|
* Start using device dehydration.
|
||||||
*
|
*
|
||||||
* - Rehydrates a dehydrated device, if one is available.
|
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
|
||||||
|
* is `true`.
|
||||||
* - Creates a new dehydration key, if necessary, and stores it in Secret
|
* - Creates a new dehydration key, if necessary, and stores it in Secret
|
||||||
* Storage.
|
* Storage.
|
||||||
* - If `createNewKey` is set to true, always creates a new key.
|
* - If `opts.createNewKey` is set to true, always creates a new key.
|
||||||
* - If a dehydration key is not available, creates a new one.
|
* - If a dehydration key is not available, creates a new one.
|
||||||
* - Creates a new dehydrated device, and schedules periodically creating
|
* - Creates a new dehydrated device, and schedules periodically creating
|
||||||
* new dehydrated devices.
|
* new dehydrated devices.
|
||||||
*
|
*
|
||||||
* @param createNewKey - whether to force creation of a new dehydration key.
|
* @param opts - options for device dehydration. For backwards compatibility
|
||||||
* This can be used, for example, if Secret Storage is being reset.
|
* with old code, a boolean can be given here, which will be treated as
|
||||||
|
* the `createNewKey` option. However, this is deprecated.
|
||||||
*/
|
*/
|
||||||
public async start(createNewKey?: boolean): Promise<void> {
|
public async start(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
|
||||||
|
if (typeof opts === "boolean") {
|
||||||
|
opts = { createNewKey: opts };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.onlyIfKeyCached && !(await this.olmMachine.dehydratedDevices().getDehydratedDeviceKey())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.stop();
|
this.stop();
|
||||||
|
if (opts.rehydrate !== false) {
|
||||||
try {
|
try {
|
||||||
await this.rehydrateDeviceIfAvailable();
|
await this.rehydrateDeviceIfAvailable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -142,7 +152,8 @@ export class DehydratedDeviceManager extends TypedEventEmitter<DehydratedDevices
|
|||||||
this.logger.info("dehydration: Error rehydrating device:", e);
|
this.logger.info("dehydration: Error rehydrating device:", e);
|
||||||
this.emit(CryptoEvent.RehydrationError, (e as Error).message);
|
this.emit(CryptoEvent.RehydrationError, (e as Error).message);
|
||||||
}
|
}
|
||||||
if (createNewKey) {
|
}
|
||||||
|
if (opts.createNewKey) {
|
||||||
await this.resetKey();
|
await this.resetKey();
|
||||||
}
|
}
|
||||||
await this.scheduleDeviceDehydration();
|
await this.scheduleDeviceDehydration();
|
||||||
|
@@ -67,6 +67,7 @@ import {
|
|||||||
CryptoEventHandlerMap,
|
CryptoEventHandlerMap,
|
||||||
KeyBackupRestoreOpts,
|
KeyBackupRestoreOpts,
|
||||||
KeyBackupRestoreResult,
|
KeyBackupRestoreResult,
|
||||||
|
StartDehydrationOpts,
|
||||||
} 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";
|
||||||
@@ -1425,11 +1426,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoApi#startDehydration}.
|
* Implementation of {@link CryptoApi#startDehydration}.
|
||||||
*/
|
*/
|
||||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
public async startDehydration(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
|
||||||
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
|
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
|
||||||
throw new Error("Device dehydration requires cross-signing and secret storage to be set up");
|
throw new Error("Device dehydration requires cross-signing and secret storage to be set up");
|
||||||
}
|
}
|
||||||
return await this.dehydratedDeviceManager.start(createNewKey);
|
return await this.dehydratedDeviceManager.start(opts || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user