You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Device Dehydration | js-sdk: store/load dehydration key (#4599)
* feat(dehydrated): Use the dehydrated key cache API * feat(dehydrated): Add signalling to device dehydration manager * feat(dehydrated): fix unneeded call getCachedKey * Upgrade to `matrix-sdk-crypto-wasm` v13.0.0 * review: quick fix and doc * apply changes from review * apply changes from review * fix comment * add some tests and emit an event on rehydration failure * factor out event counter into a test util, since it may be useful elsewhere * adjust test to cover a few more lines * fix documentation * Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * fix missing bracket * add test for getting the dehydration key from SSSS --------- Co-authored-by: Hubert Chathi <hubertc@matrix.org> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
@ -50,7 +50,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "^12.1.0",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
|
||||||
"@matrix-org/olm": "3.2.15",
|
"@matrix-org/olm": "3.2.15",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
|
@ -17,11 +17,13 @@ limitations under the License.
|
|||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
|
||||||
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
|
import { ClientEvent, createClient, MatrixClient, MatrixEvent } from "../../../src";
|
||||||
|
import { CryptoEvent } from "../../../src/crypto-api/index";
|
||||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||||
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||||
|
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
||||||
|
|
||||||
describe("Device dehydration", () => {
|
describe("Device dehydration", () => {
|
||||||
it("should rehydrate and dehydrate a device", async () => {
|
it("should rehydrate and dehydrate a device", async () => {
|
||||||
@ -40,6 +42,12 @@ describe("Device dehydration", () => {
|
|||||||
|
|
||||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||||
|
|
||||||
|
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
|
||||||
|
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
|
||||||
|
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
|
||||||
|
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
|
||||||
|
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
|
||||||
|
|
||||||
// count the number of times the dehydration key gets set
|
// count the number of times the dehydration key gets set
|
||||||
let setDehydrationCount = 0;
|
let setDehydrationCount = 0;
|
||||||
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
||||||
@ -74,6 +82,8 @@ describe("Device dehydration", () => {
|
|||||||
await crypto.startDehydration();
|
await crypto.startDehydration();
|
||||||
|
|
||||||
expect(dehydrationCount).toEqual(1);
|
expect(dehydrationCount).toEqual(1);
|
||||||
|
expect(creationEventCounter.counter).toEqual(1);
|
||||||
|
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||||
|
|
||||||
// a week later, we should have created another dehydrated device
|
// a week later, we should have created another dehydrated device
|
||||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||||
@ -81,7 +91,10 @@ describe("Device dehydration", () => {
|
|||||||
});
|
});
|
||||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||||
await dehydrationPromise;
|
await dehydrationPromise;
|
||||||
|
|
||||||
|
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||||
expect(dehydrationCount).toEqual(2);
|
expect(dehydrationCount).toEqual(2);
|
||||||
|
expect(creationEventCounter.counter).toEqual(2);
|
||||||
|
|
||||||
// restart dehydration -- rehydrate the device that we created above,
|
// restart dehydration -- rehydrate the device that we created above,
|
||||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||||
@ -113,6 +126,39 @@ describe("Device dehydration", () => {
|
|||||||
expect(setDehydrationCount).toEqual(2);
|
expect(setDehydrationCount).toEqual(2);
|
||||||
expect(eventsResponse.mock.calls).toHaveLength(2);
|
expect(eventsResponse.mock.calls).toHaveLength(2);
|
||||||
|
|
||||||
|
expect(rehydrationStartedCounter.counter).toEqual(1);
|
||||||
|
expect(rehydrationCompletedCounter.counter).toEqual(1);
|
||||||
|
expect(creationEventCounter.counter).toEqual(3);
|
||||||
|
expect(rehydrationProgressCounter.counter).toEqual(1);
|
||||||
|
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||||
|
|
||||||
|
// test that if we get an error when we try to rotate, it emits an event
|
||||||
|
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
errcode: "M_UNKNOWN",
|
||||||
|
error: "Unknown error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||||
|
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||||
|
await rotationErrorEventPromise;
|
||||||
|
|
||||||
|
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||||
|
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
errcode: "M_UNKNOWN",
|
||||||
|
error: "Unknown error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||||
|
await crypto.startDehydration(true);
|
||||||
|
await rehydrationErrorEventPromise;
|
||||||
|
|
||||||
matrixClient.stopClient();
|
matrixClient.stopClient();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -566,6 +566,19 @@ if (globalThis.Olm) {
|
|||||||
|
|
||||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of times that an event was emitted.
|
||||||
|
*/
|
||||||
|
export class EventCounter {
|
||||||
|
public counter;
|
||||||
|
constructor(emitter: EventEmitter, event: string) {
|
||||||
|
this.counter = 0;
|
||||||
|
emitter.on(event, () => {
|
||||||
|
this.counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advance the fake timers in a loop until the given promise resolves or rejects.
|
* Advance the fake timers in a loop until the given promise resolves or rejects.
|
||||||
*
|
*
|
||||||
|
@ -44,7 +44,7 @@ import {
|
|||||||
MemoryCryptoStore,
|
MemoryCryptoStore,
|
||||||
TypedEventEmitter,
|
TypedEventEmitter,
|
||||||
} from "../../../src";
|
} from "../../../src";
|
||||||
import { mkEvent } from "../../test-utils/test-utils";
|
import { emitPromise, mkEvent } from "../../test-utils/test-utils";
|
||||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||||
import { IEventDecryptionResult, IMegolmSessionData } from "../../../src/@types/crypto";
|
import { IEventDecryptionResult, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||||
@ -1739,6 +1739,110 @@ describe("RustCrypto", () => {
|
|||||||
});
|
});
|
||||||
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
|
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should load the dehydration key from SSSS if available", async () => {
|
||||||
|
fetchMock.config.overwriteRoutes = true;
|
||||||
|
|
||||||
|
const secretStorageCallbacks = {
|
||||||
|
getSecretStorageKey: async (keys: any, name: string) => {
|
||||||
|
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||||
|
},
|
||||||
|
} as SecretStorageCallbacks;
|
||||||
|
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
|
||||||
|
|
||||||
|
// Create a RustCrypto to set up device dehydration.
|
||||||
|
const e2eKeyReceiver1 = new E2EKeyReceiver("http://server");
|
||||||
|
const e2eKeyResponder1 = new E2EKeyResponder("http://server");
|
||||||
|
e2eKeyResponder1.addKeyReceiver(TEST_USER, e2eKeyReceiver1);
|
||||||
|
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: {},
|
||||||
|
});
|
||||||
|
const rustCrypto1 = 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 rustCrypto1.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||||
|
await rustCrypto1.bootstrapSecretStorage({
|
||||||
|
createSecretStorageKey,
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
setupNewKeyBackup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// we need to process a sync so that the OlmMachine will upload keys
|
||||||
|
await rustCrypto1.preprocessToDeviceMessages([]);
|
||||||
|
await rustCrypto1.onSyncCompleted({});
|
||||||
|
|
||||||
|
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||||
|
status: 404,
|
||||||
|
body: {
|
||||||
|
errcode: "M_NOT_FOUND",
|
||||||
|
error: "Not found",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let dehydratedDeviceBody: any;
|
||||||
|
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||||
|
dehydratedDeviceBody = JSON.parse(opts.body as string);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
await rustCrypto1.startDehydration();
|
||||||
|
await rustCrypto1.stop();
|
||||||
|
|
||||||
|
// Create another RustCrypto, using the same SecretStorage, to
|
||||||
|
// rehydrate the device.
|
||||||
|
const e2eKeyReceiver2 = new E2EKeyReceiver("http://server");
|
||||||
|
const e2eKeyResponder2 = new E2EKeyResponder("http://server");
|
||||||
|
e2eKeyResponder2.addKeyReceiver(TEST_USER, e2eKeyReceiver2);
|
||||||
|
|
||||||
|
const rustCrypto2 = await makeTestRustCrypto(
|
||||||
|
makeMatrixHttpApi(),
|
||||||
|
TEST_USER,
|
||||||
|
"ANOTHERDEVICE",
|
||||||
|
secretStorage,
|
||||||
|
);
|
||||||
|
|
||||||
|
// dehydration requires secret storage and cross signing
|
||||||
|
await rustCrypto2.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||||
|
|
||||||
|
// we need to process a sync so that the OlmMachine will upload keys
|
||||||
|
await rustCrypto2.preprocessToDeviceMessages([]);
|
||||||
|
await rustCrypto2.onSyncCompleted({});
|
||||||
|
|
||||||
|
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||||
|
device_id: dehydratedDeviceBody.device_id,
|
||||||
|
device_data: dehydratedDeviceBody.device_data,
|
||||||
|
});
|
||||||
|
fetchMock.post(
|
||||||
|
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
|
||||||
|
{
|
||||||
|
events: [],
|
||||||
|
next_batch: "token",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// We check that a RehydrationCompleted event gets emitted, which
|
||||||
|
// means that the device was successfully rehydrated.
|
||||||
|
const rehydrationCompletedPromise = emitPromise(rustCrypto2, CryptoEvent.RehydrationCompleted);
|
||||||
|
await rustCrypto2.startDehydration();
|
||||||
|
await rehydrationCompletedPromise;
|
||||||
|
await rustCrypto2.stop();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("import & export secrets bundle", () => {
|
describe("import & export secrets bundle", () => {
|
||||||
|
@ -2311,6 +2311,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
CryptoEvent.KeysChanged,
|
CryptoEvent.KeysChanged,
|
||||||
CryptoEvent.DevicesUpdated,
|
CryptoEvent.DevicesUpdated,
|
||||||
CryptoEvent.WillUpdateDevices,
|
CryptoEvent.WillUpdateDevices,
|
||||||
|
CryptoEvent.DehydratedDeviceCreated,
|
||||||
|
CryptoEvent.DehydratedDeviceUploaded,
|
||||||
|
CryptoEvent.RehydrationStarted,
|
||||||
|
CryptoEvent.RehydrationProgress,
|
||||||
|
CryptoEvent.RehydrationCompleted,
|
||||||
|
CryptoEvent.RehydrationError,
|
||||||
|
CryptoEvent.DehydrationKeyCached,
|
||||||
|
CryptoEvent.DehydratedDeviceRotationError,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,4 +90,63 @@ export enum CryptoEvent {
|
|||||||
* `progress === total === -1`.
|
* `progress === total === -1`.
|
||||||
*/
|
*/
|
||||||
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
|
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when a new dehydrated device is created locally.
|
||||||
|
*
|
||||||
|
* After the client calls {@link CryptoApi.startDehydration}, this event
|
||||||
|
* will be fired every time a new dehydrated device is created. It may fire
|
||||||
|
* before `startDehydration` returns.
|
||||||
|
*/
|
||||||
|
DehydratedDeviceCreated = "dehydration.DehydratedDeviceCreated",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when a new dehydrated device is successfully uploaded to the server.
|
||||||
|
*
|
||||||
|
* This should fire shortly after {@link DehydratedDeviceCreated} fires. If
|
||||||
|
* upload is unsuccessful, this will be reported either by an error thrown
|
||||||
|
* by {@link CryptoApi.startDehydration} (for errors that happen before
|
||||||
|
* `startDehydration` returns), or by firing {@link DehydratedDeviceRotationError}
|
||||||
|
* (for errors that happen during regular rotation of the dehydrated device)
|
||||||
|
*/
|
||||||
|
DehydratedDeviceUploaded = "dehydration.DehydratedDeviceUploaded",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when rehydration has started.
|
||||||
|
*
|
||||||
|
* After the client calls {@link CryptoApi.startDehydration}, this event will
|
||||||
|
* fire if a dehydrated device is found and we attempt to rehydrate it.
|
||||||
|
*/
|
||||||
|
RehydrationStarted = "dehydration.RehydrationStarted",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires during rehydration, to inform the application of rehydration progress.
|
||||||
|
*
|
||||||
|
* The payload is a pair `[roomKeyCount: number, toDeviceCount: number]`,
|
||||||
|
* where `roomKeyCount` is the number of room keys that have been received
|
||||||
|
* so far, and `toDeviceCount` is the number of to-device messages received
|
||||||
|
* so far (including the messages containing room keys).
|
||||||
|
*/
|
||||||
|
RehydrationProgress = "dehydration.RehydrationProgress",
|
||||||
|
|
||||||
|
/** Fires when rehydration has completed successfully. */
|
||||||
|
RehydrationCompleted = "dehydration.RehydrationCompleted",
|
||||||
|
|
||||||
|
/** Fires when there was an error in rehydration.
|
||||||
|
*
|
||||||
|
* The payload is an error message as a string.
|
||||||
|
*/
|
||||||
|
RehydrationError = "dehydration.RehydrationError",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when a dehydrated device key has been cached in the local database.
|
||||||
|
*/
|
||||||
|
DehydrationKeyCached = "dehydration.DehydrationKeyCached",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when an error occurs during periodic rotation of the dehydrated device.
|
||||||
|
*
|
||||||
|
* The payload is an error message as a string.
|
||||||
|
*/
|
||||||
|
DehydratedDeviceRotationError = "dehydration.DehydratedDeviceRotationError",
|
||||||
}
|
}
|
||||||
|
@ -30,4 +30,12 @@ export type CryptoEventHandlerMap = {
|
|||||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||||
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
|
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
|
||||||
|
[CryptoEvent.DehydratedDeviceCreated]: () => void;
|
||||||
|
[CryptoEvent.DehydratedDeviceUploaded]: () => void;
|
||||||
|
[CryptoEvent.RehydrationStarted]: () => void;
|
||||||
|
[CryptoEvent.RehydrationProgress]: (roomKeyCount: number, toDeviceCount: number) => void;
|
||||||
|
[CryptoEvent.RehydrationCompleted]: () => void;
|
||||||
|
[CryptoEvent.RehydrationError]: (msg: string) => void;
|
||||||
|
[CryptoEvent.DehydrationKeyCached]: () => void;
|
||||||
|
[CryptoEvent.DehydratedDeviceRotationError]: (msg: string) => void;
|
||||||
} & RustBackupCryptoEventMap;
|
} & RustBackupCryptoEventMap;
|
||||||
|
@ -21,8 +21,10 @@ import { encodeUri } from "../utils.ts";
|
|||||||
import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
|
import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
|
||||||
import { IToDeviceEvent } from "../sync-accumulator.ts";
|
import { IToDeviceEvent } from "../sync-accumulator.ts";
|
||||||
import { ServerSideSecretStorage } from "../secret-storage.ts";
|
import { ServerSideSecretStorage } from "../secret-storage.ts";
|
||||||
import { decodeBase64, encodeUnpaddedBase64 } 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 { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`.
|
* The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`.
|
||||||
@ -67,9 +69,7 @@ const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000;
|
|||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class DehydratedDeviceManager {
|
export class DehydratedDeviceManager extends TypedEventEmitter<DehydratedDevicesEvents, DehydratedDevicesEventMap> {
|
||||||
/** the secret key used for dehydrating and rehydrating */
|
|
||||||
private key?: Uint8Array;
|
|
||||||
/** the ID of the interval for periodically replacing the dehydrated device */
|
/** the ID of the interval for periodically replacing the dehydrated device */
|
||||||
private intervalId?: ReturnType<typeof setInterval>;
|
private intervalId?: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
@ -79,7 +79,14 @@ export class DehydratedDeviceManager {
|
|||||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||||
private readonly secretStorage: ServerSideSecretStorage,
|
private readonly secretStorage: ServerSideSecretStorage,
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cacheKey(key: RustSdkCryptoJs.DehydratedDeviceKey): Promise<void> {
|
||||||
|
await this.olmMachine.dehydratedDevices().saveDehydratedDeviceKey(key);
|
||||||
|
this.emit(CryptoEvent.DehydrationKeyCached);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether the server supports dehydrated devices.
|
* Return whether the server supports dehydrated devices.
|
||||||
@ -133,6 +140,7 @@ export class DehydratedDeviceManager {
|
|||||||
// If rehydration fails, there isn't much we can do about it. Log
|
// If rehydration fails, there isn't much we can do about it. Log
|
||||||
// the error, and create a new device.
|
// the error, and create a new device.
|
||||||
this.logger.info("dehydration: Error rehydrating device:", e);
|
this.logger.info("dehydration: Error rehydrating device:", e);
|
||||||
|
this.emit(CryptoEvent.RehydrationError, (e as Error).message);
|
||||||
}
|
}
|
||||||
if (createNewKey) {
|
if (createNewKey) {
|
||||||
await this.resetKey();
|
await this.resetKey();
|
||||||
@ -151,12 +159,15 @@ export class DehydratedDeviceManager {
|
|||||||
* Reset the dehydration key.
|
* Reset the dehydration key.
|
||||||
*
|
*
|
||||||
* Creates a new key and stores it in secret storage.
|
* Creates a new key and stores it in secret storage.
|
||||||
|
*
|
||||||
|
* @returns The newly-generated key.
|
||||||
*/
|
*/
|
||||||
public async resetKey(): Promise<void> {
|
public async resetKey(): Promise<RustSdkCryptoJs.DehydratedDeviceKey> {
|
||||||
const key = new Uint8Array(32);
|
const key = RustSdkCryptoJs.DehydratedDeviceKey.createRandomKey();
|
||||||
globalThis.crypto.getRandomValues(key);
|
await this.secretStorage.store(SECRET_STORAGE_NAME, key.toBase64());
|
||||||
await this.secretStorage.store(SECRET_STORAGE_NAME, encodeUnpaddedBase64(key));
|
// Also cache it in the rust SDK's crypto store.
|
||||||
this.key = key;
|
await this.cacheKey(key);
|
||||||
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,19 +177,27 @@ export class DehydratedDeviceManager {
|
|||||||
*
|
*
|
||||||
* @returns the key, if available, or `null` if no key is available
|
* @returns the key, if available, or `null` if no key is available
|
||||||
*/
|
*/
|
||||||
private async getKey(create: boolean): Promise<Uint8Array | null> {
|
private async getKey(create: boolean): Promise<RustSdkCryptoJs.DehydratedDeviceKey | null> {
|
||||||
if (this.key === undefined) {
|
const cachedKey = await this.olmMachine.dehydratedDevices().getDehydratedDeviceKey();
|
||||||
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
|
if (cachedKey) return cachedKey;
|
||||||
if (keyB64 === undefined) {
|
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
|
||||||
if (!create) {
|
if (keyB64 === undefined) {
|
||||||
return null;
|
if (!create) {
|
||||||
}
|
return null;
|
||||||
await this.resetKey();
|
|
||||||
} else {
|
|
||||||
this.key = decodeBase64(keyB64);
|
|
||||||
}
|
}
|
||||||
|
return await this.resetKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We successfully found the key in secret storage: decode it, and cache it in
|
||||||
|
// the rust SDK's crypto store.
|
||||||
|
const bytes = decodeBase64(keyB64);
|
||||||
|
try {
|
||||||
|
const key = RustSdkCryptoJs.DehydratedDeviceKey.createKeyFromArray(bytes);
|
||||||
|
await this.cacheKey(key);
|
||||||
|
return key;
|
||||||
|
} finally {
|
||||||
|
bytes.fill(0);
|
||||||
}
|
}
|
||||||
return this.key!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,6 +238,7 @@ export class DehydratedDeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info("dehydration: dehydrated device found");
|
this.logger.info("dehydration: dehydrated device found");
|
||||||
|
this.emit(CryptoEvent.RehydrationStarted);
|
||||||
|
|
||||||
const rehydratedDevice = await this.olmMachine
|
const rehydratedDevice = await this.olmMachine
|
||||||
.dehydratedDevices()
|
.dehydratedDevices()
|
||||||
@ -255,8 +275,11 @@ export class DehydratedDeviceManager {
|
|||||||
nextBatch = eventResp.next_batch;
|
nextBatch = eventResp.next_batch;
|
||||||
const roomKeyInfos = await rehydratedDevice.receiveEvents(JSON.stringify(eventResp.events));
|
const roomKeyInfos = await rehydratedDevice.receiveEvents(JSON.stringify(eventResp.events));
|
||||||
roomKeyCount += roomKeyInfos.length;
|
roomKeyCount += roomKeyInfos.length;
|
||||||
|
|
||||||
|
this.emit(CryptoEvent.RehydrationProgress, roomKeyCount, toDeviceCount);
|
||||||
}
|
}
|
||||||
this.logger.info(`dehydration: received ${roomKeyCount} room keys from ${toDeviceCount} to-device events`);
|
this.logger.info(`dehydration: received ${roomKeyCount} room keys from ${toDeviceCount} to-device events`);
|
||||||
|
this.emit(CryptoEvent.RehydrationCompleted);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -270,9 +293,11 @@ export class DehydratedDeviceManager {
|
|||||||
const key = (await this.getKey(true))!;
|
const key = (await this.getKey(true))!;
|
||||||
|
|
||||||
const dehydratedDevice = await this.olmMachine.dehydratedDevices().create();
|
const dehydratedDevice = await this.olmMachine.dehydratedDevices().create();
|
||||||
|
this.emit(CryptoEvent.DehydratedDeviceCreated);
|
||||||
const request = await dehydratedDevice.keysForUpload("Dehydrated device", key);
|
const request = await dehydratedDevice.keysForUpload("Dehydrated device", key);
|
||||||
|
|
||||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||||
|
this.emit(CryptoEvent.DehydratedDeviceUploaded);
|
||||||
|
|
||||||
this.logger.info("dehydration: uploaded device");
|
this.logger.info("dehydration: uploaded device");
|
||||||
}
|
}
|
||||||
@ -287,6 +312,7 @@ export class DehydratedDeviceManager {
|
|||||||
await this.createAndUploadDehydratedDevice();
|
await this.createAndUploadDehydratedDevice();
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
this.createAndUploadDehydratedDevice().catch((error) => {
|
this.createAndUploadDehydratedDevice().catch((error) => {
|
||||||
|
this.emit(CryptoEvent.DehydratedDeviceRotationError, error.message);
|
||||||
this.logger.error("Error creating dehydrated device:", error);
|
this.logger.error("Error creating dehydrated device:", error);
|
||||||
});
|
});
|
||||||
}, DEHYDRATION_INTERVAL);
|
}, DEHYDRATION_INTERVAL);
|
||||||
@ -304,3 +330,23 @@ export class DehydratedDeviceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events fired by the DehydratedDeviceManager
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
type DehydratedDevicesEvents =
|
||||||
|
| CryptoEvent.DehydratedDeviceCreated
|
||||||
|
| CryptoEvent.DehydratedDeviceUploaded
|
||||||
|
| CryptoEvent.RehydrationStarted
|
||||||
|
| CryptoEvent.RehydrationProgress
|
||||||
|
| CryptoEvent.RehydrationCompleted
|
||||||
|
| CryptoEvent.RehydrationError
|
||||||
|
| CryptoEvent.DehydrationKeyCached
|
||||||
|
| CryptoEvent.DehydratedDeviceRotationError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of the {@link DehydratedDeviceEvents} fired by the {@link DehydratedDeviceManager} and their payloads.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
type DehydratedDevicesEventMap = Pick<CryptoEventHandlerMap, DehydratedDevicesEvents>;
|
||||||
|
@ -183,12 +183,23 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
);
|
);
|
||||||
this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader);
|
this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader);
|
||||||
|
|
||||||
|
// re-emit the events emitted by managers
|
||||||
this.reemitter.reEmit(this.backupManager, [
|
this.reemitter.reEmit(this.backupManager, [
|
||||||
CryptoEvent.KeyBackupStatus,
|
CryptoEvent.KeyBackupStatus,
|
||||||
CryptoEvent.KeyBackupSessionsRemaining,
|
CryptoEvent.KeyBackupSessionsRemaining,
|
||||||
CryptoEvent.KeyBackupFailed,
|
CryptoEvent.KeyBackupFailed,
|
||||||
CryptoEvent.KeyBackupDecryptionKeyCached,
|
CryptoEvent.KeyBackupDecryptionKeyCached,
|
||||||
]);
|
]);
|
||||||
|
this.reemitter.reEmit(this.dehydratedDeviceManager, [
|
||||||
|
CryptoEvent.DehydratedDeviceCreated,
|
||||||
|
CryptoEvent.DehydratedDeviceUploaded,
|
||||||
|
CryptoEvent.RehydrationStarted,
|
||||||
|
CryptoEvent.RehydrationProgress,
|
||||||
|
CryptoEvent.RehydrationCompleted,
|
||||||
|
CryptoEvent.RehydrationError,
|
||||||
|
CryptoEvent.DehydrationKeyCached,
|
||||||
|
CryptoEvent.DehydratedDeviceRotationError,
|
||||||
|
]);
|
||||||
|
|
||||||
this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor, secretStorage);
|
this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor, secretStorage);
|
||||||
|
|
||||||
|
@ -1477,10 +1477,10 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm@^12.1.0":
|
"@matrix-org/matrix-sdk-crypto-wasm@^13.0.0":
|
||||||
version "12.1.0"
|
version "13.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.1.0.tgz#2aef64eab2d30c0a1ace9c0fe876f53aa2949f14"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-13.0.0.tgz#658bed951e4c8a06a6dd545575a79cf32022d4ba"
|
||||||
integrity sha512-NhJFu/8FOGjnW7mDssRUzaMSwXrYOcCqgAjZyAw9KQ9unNADKEi7KoIKe7GtrG2PWtm36y2bUf+hB8vhSY6Wdw==
|
integrity sha512-2gtpjnxL42sdJAgkwitpMMI4cw7Gcjf5sW0MXoe+OAlXPlxIzyM+06F5JJ8ENvBeHkuV2RqtFIRrh8i90HLsMw==
|
||||||
|
|
||||||
"@matrix-org/olm@3.2.15":
|
"@matrix-org/olm@3.2.15":
|
||||||
version "3.2.15"
|
version "3.2.15"
|
||||||
|
Reference in New Issue
Block a user