1
0
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:
Valere
2025-01-27 23:05:23 +01:00
committed by GitHub
parent 44158bc843
commit 7d8cbd6ef0
10 changed files with 323 additions and 28 deletions

View File

@ -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",

View File

@ -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();
}); });
}); });

View File

@ -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.
* *

View File

@ -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", () => {

View File

@ -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,
]); ]);
} }

View File

@ -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",
} }

View File

@ -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;

View File

@ -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>;

View File

@ -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);

View File

@ -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"