diff --git a/spec/integ/crypto/device-dehydration.spec.ts b/spec/integ/crypto/device-dehydration.spec.ts new file mode 100644 index 00000000000..cf319a9878c --- /dev/null +++ b/spec/integ/crypto/device-dehydration.spec.ts @@ -0,0 +1,181 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import "fake-indexeddb/auto"; +import fetchMock from "fetch-mock-jest"; + +import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src"; +import { RustCrypto } from "../../../src/rust-crypto/rust-crypto"; +import { AddSecretStorageKeyOpts } from "../../../src/secret-storage"; +import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; +import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; + +describe("Device dehydration", () => { + it("should rehydrate and dehydrate a device", async () => { + jest.useFakeTimers({ doNotFake: ["queueMicrotask"] }); + + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + cryptoCallbacks: { + getSecretStorageKey: async (keys: any, name: string) => { + return [[...Object.keys(keys.keys)][0], new Uint8Array(32)]; + }, + }, + }); + + await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server"); + + // count the number of times the dehydration key gets set + let setDehydrationCount = 0; + matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => { + if (event.getType() === "org.matrix.msc3814") { + setDehydrationCount++; + } + }); + + const crypto = matrixClient.getCrypto()!; + fetchMock.config.overwriteRoutes = true; + + // start dehydration -- we start with no dehydrated device, and we + // store the dehydrated device that we create + 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; + let dehydrationCount = 0; + let resolveDehydrationPromise: () => void; + fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => { + dehydratedDeviceBody = JSON.parse(opts.body as string); + dehydrationCount++; + if (resolveDehydrationPromise) { + resolveDehydrationPromise(); + } + return {}; + }); + await crypto.startDehydration(); + + expect(dehydrationCount).toEqual(1); + + // a week later, we should have created another dehydrated device + const dehydrationPromise = new Promise((resolve, reject) => { + resolveDehydrationPromise = resolve; + }); + jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000); + await dehydrationPromise; + expect(dehydrationCount).toEqual(2); + + // restart dehydration -- rehydrate the device that we created above, + // and create a new dehydrated device. We also set `createNewKey`, so + // a new dehydration key will be set + fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", { + device_id: dehydratedDeviceBody.device_id, + device_data: dehydratedDeviceBody.device_data, + }); + const eventsResponse = jest.fn((url, opts) => { + // rehydrating should make two calls to the /events endpoint. + // The first time will return a single event, and the second + // time will return no events (which will signal to the + // rehydration function that it can stop) + const body = JSON.parse(opts.body as string); + const nextBatch = body.next_batch ?? "0"; + const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : []; + return { + events, + next_batch: nextBatch + "1", + }; + }); + fetchMock.post( + `path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`, + eventsResponse, + ); + await crypto.startDehydration(true); + expect(dehydrationCount).toEqual(3); + + expect(setDehydrationCount).toEqual(2); + expect(eventsResponse.mock.calls).toHaveLength(2); + + matrixClient.stopClient(); + }); +}); + +/** create a new secret storage and cross-signing keys */ +async function initializeSecretStorage( + matrixClient: MatrixClient, + userId: string, + homeserverUrl: string, +): Promise { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", { + status: 404, + body: { + errcode: "M_NOT_FOUND", + error: "Not found", + }, + }); + const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl); + const e2eKeyResponder = new E2EKeyResponder(homeserverUrl); + e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver); + fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {}); + fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {}); + const accountData: Map = new Map(); + fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => { + const name = url.split("/").pop()!; + const value = accountData.get(name); + if (value) { + return value; + } else { + return { + status: 404, + body: { + errcode: "M_NOT_FOUND", + error: "Not found", + }, + }; + } + }); + fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => { + const name = url.split("/").pop()!; + const value = JSON.parse(opts.body as string); + accountData.set(name, value); + matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value })); + return {}; + }); + + await matrixClient.initRustCrypto(); + const crypto = matrixClient.getCrypto()! as RustCrypto; + // we need to process a sync so that the OlmMachine will upload keys + await crypto.preprocessToDeviceMessages([]); + await crypto.onSyncCompleted({}); + + // create initial secret storage + async function createSecretStorageKey() { + return { + keyInfo: {} as AddSecretStorageKeyOpts, + privateKey: new Uint8Array(32), + }; + } + await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true }); + await matrixClient.bootstrapSecretStorage({ + createSecretStorageKey, + setupNewSecretStorage: true, + setupNewKeyBackup: false, + }); +} diff --git a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts index e63243d2913..4074e1c9a32 100644 --- a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts +++ b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts @@ -22,6 +22,7 @@ import { KeysClaimRequest, KeysQueryRequest, KeysUploadRequest, + PutDehydratedDeviceRequest, RoomMessageRequest, SignatureUploadRequest, UploadSigningKeysRequest, @@ -233,6 +234,35 @@ describe("OutgoingRequestProcessor", () => { httpBackend.verifyNoOutstandingRequests(); }); + it("should handle PutDehydratedDeviceRequest", async () => { + // first, mock up a request as we might expect to receive it from the Rust layer ... + const testReq = { foo: "bar" }; + const outgoingRequest = new PutDehydratedDeviceRequest(JSON.stringify(testReq)); + + // ... then poke the request into the OutgoingRequestProcessor under test + const reqProm = processor.makeOutgoingRequest(outgoingRequest); + + // Now: check that it makes a matching HTTP request. + const testResponse = '{"result":1}'; + httpBackend + .when("PUT", "/_matrix") + .check((req) => { + expect(req.path).toEqual( + "https://example.com/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", + ); + expect(JSON.parse(req.rawData)).toEqual(testReq); + expect(req.headers["Accept"]).toEqual("application/json"); + expect(req.headers["Content-Type"]).toEqual("application/json"); + }) + .respond(200, testResponse, true); + + // PutDehydratedDeviceRequest does not need to be marked as sent, so no call to OlmMachine.markAsSent is expected. + + await httpBackend.flushAllExpected(); + await reqProm; + httpBackend.verifyNoOutstandingRequests(); + }); + it("does not explode with unknown requests", async () => { const outgoingRequest = { id: "5678", type: 987 }; const markSentCallPromise = awaitCallToMarkAsSent(); diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index b9c76c9fb27..97d31d29ad2 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -762,8 +762,11 @@ describe("RustCrypto", () => { }, }, }; - } else if (request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) { - // SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent. + } else if ( + request instanceof RustSdkCryptoJs.UploadSigningKeysRequest || + request instanceof RustSdkCryptoJs.PutDehydratedDeviceRequest + ) { + // These request types do not implement OutgoingRequest and do not need to be marked as sent. return; } if (request.id) { @@ -1395,6 +1398,34 @@ describe("RustCrypto", () => { }); }); }); + + describe("device dehydration", () => { + it("should detect if dehydration is supported", async () => { + const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi()); + fetchMock.config.overwriteRoutes = true; + fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", { + status: 404, + body: { + errcode: "M_UNRECOGNIZED", + error: "Unknown endpoint", + }, + }); + expect(await rustCrypto.isDehydrationSupported()).toBe(false); + fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", { + status: 404, + body: { + errcode: "M_NOT_FOUND", + error: "Not found", + }, + }); + expect(await rustCrypto.isDehydrationSupported()).toBe(true); + fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", { + device_id: "DEVICE_ID", + device_data: "data", + }); + expect(await rustCrypto.isDehydrationSupported()).toBe(true); + }); + }); }); /** Build a MatrixHttpApi instance */ diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 7d7eba937ba..0f78bd76a1a 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -496,6 +496,42 @@ export interface CryptoApi { * @param version - The backup version to delete. */ deleteKeyBackupVersion(version: string): Promise; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Dehydrated devices + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Returns whether MSC3814 dehydrated devices are supported by the crypto + * backend and by the server. + * + * This should be called before calling `startDehydration`, and if this + * returns `false`, `startDehydration` should not be called. + */ + isDehydrationSupported(): Promise; + + /** + * Start using device dehydration. + * + * - Rehydrates a dehydrated device, if one is available. + * - Creates a new dehydration key, if necessary, and stores it in Secret + * Storage. + * - If `createNewKey` is set to true, always creates a new key. + * - If a dehydration key is not available, creates a new one. + * - Creates a new dehydrated device, and schedules periodically creating + * new dehydrated devices. + * + * This function must not be called unless `isDehydrationSupported` returns + * `true`, and must not be called until after cross-signing and secret + * storage have been set up. + * + * @param createNewKey - whether to force creation of a new dehydration key. + * This can be used, for example, if Secret Storage is being reset. Defaults + * to false. + */ + startDehydration(createNewKey?: boolean): Promise; } /** A reason code for a failure to decrypt an event. */ diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 83b45a82c05..10751ca5632 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -4287,6 +4287,21 @@ export class Crypto extends TypedEventEmitter { + return false; + } + + /** + * Stub function -- dehydration is not implemented here, so throw error + */ + public async startDehydration(createNewKey?: boolean): Promise { + throw new Error("Not implemented"); + } } /** diff --git a/src/models/device.ts b/src/models/device.ts index 0a451fd5a8d..8498b55157f 100644 --- a/src/models/device.ts +++ b/src/models/device.ts @@ -51,6 +51,9 @@ export class Device { /** display name of the device */ public readonly displayName?: string; + /** whether the device is a dehydrated device */ + public readonly dehydrated: boolean = false; + public constructor(opts: DeviceParameters) { this.deviceId = opts.deviceId; this.userId = opts.userId; @@ -59,6 +62,7 @@ export class Device { this.verified = opts.verified || DeviceVerification.Unverified; this.signatures = opts.signatures || new Map(); this.displayName = opts.displayName; + this.dehydrated = !!opts.dehydrated; } /** diff --git a/src/rust-crypto/DehydratedDeviceManager.ts b/src/rust-crypto/DehydratedDeviceManager.ts new file mode 100644 index 00000000000..56f6052b724 --- /dev/null +++ b/src/rust-crypto/DehydratedDeviceManager.ts @@ -0,0 +1,307 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; + +import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; +import { encodeUri } from "../utils"; +import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api"; +import { IToDeviceEvent } from "../sync-accumulator"; +import { ServerSideSecretStorage } from "../secret-storage"; +import { crypto } from "../crypto/crypto"; +import { decodeBase64, encodeUnpaddedBase64 } from "../base64"; +import { Logger } from "../logger"; + +/** + * The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`. + */ +interface DehydratedDeviceResp { + device_id: string; + device_data: { + algorithm: string; + }; +} +/** + * The response body of `POST /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/events`. + */ +interface DehydratedDeviceEventsResp { + events: IToDeviceEvent[]; + next_batch: string; +} + +/** + * The unstable URL prefix for dehydrated device endpoints + */ +export const UnstablePrefix = "/_matrix/client/unstable/org.matrix.msc3814.v1"; +/** + * The name used for the dehydration key in Secret Storage + */ +const SECRET_STORAGE_NAME = "org.matrix.msc3814"; + +/** + * The interval between creating dehydrated devices. (one week) + */ +const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000; + +/** + * Manages dehydrated devices + * + * We have one of these per `RustCrypto`. It's responsible for + * + * * determining server support for dehydrated devices + * * creating new dehydrated devices when requested, including periodically + * replacing the dehydrated device with a new one + * * rehydrating a device when requested, and when present + * + * @internal + */ +export class DehydratedDeviceManager { + /** the secret key used for dehydrating and rehydrating */ + private key?: Uint8Array; + /** the ID of the interval for periodically replacing the dehydrated device */ + private intervalId?: ReturnType; + + public constructor( + private readonly logger: Logger, + private readonly olmMachine: RustSdkCryptoJs.OlmMachine, + private readonly http: MatrixHttpApi, + private readonly outgoingRequestProcessor: OutgoingRequestProcessor, + private readonly secretStorage: ServerSideSecretStorage, + ) {} + + /** + * Return whether the server supports dehydrated devices. + */ + public async isSupported(): Promise { + // call the endpoint to get a dehydrated device. If it returns an + // M_UNRECOGNIZED error, then dehydration is unsupported. If it returns + // a successful response, or an M_NOT_FOUND, then dehydration is supported. + // Any other exceptions are passed through. + try { + await this.http.authedRequest( + Method.Get, + "/dehydrated_device", + undefined, + undefined, + { + prefix: UnstablePrefix, + }, + ); + } catch (error) { + const err = error as MatrixError; + if (err.errcode === "M_UNRECOGNIZED") { + return false; + } else if (err.errcode === "M_NOT_FOUND") { + return true; + } + throw error; + } + return true; + } + + /** + * Start using device dehydration. + * + * - Rehydrates a dehydrated device, if one is available. + * - Creates a new dehydration key, if necessary, and stores it in Secret + * Storage. + * - If `createNewKey` is set to true, always creates a new key. + * - If a dehydration key is not available, creates a new one. + * - Creates a new dehydrated device, and schedules periodically creating + * new dehydrated devices. + * + * @param createNewKey - whether to force creation of a new dehydration key. + * This can be used, for example, if Secret Storage is being reset. + */ + public async start(createNewKey?: boolean): Promise { + this.stop(); + try { + await this.rehydrateDeviceIfAvailable(); + } catch (e) { + // If rehydration fails, there isn't much we can do about it. Log + // the error, and create a new device. + this.logger.info("dehydration: Error rehydrating device:", e); + } + if (createNewKey) { + await this.resetKey(); + } + await this.scheduleDeviceDehydration(); + } + + /** + * Return whether the dehydration key is stored in Secret Storage. + */ + public async isKeyStored(): Promise { + return Boolean(await this.secretStorage.isStored(SECRET_STORAGE_NAME)); + } + + /** + * Reset the dehydration key. + * + * Creates a new key and stores it in secret storage. + */ + public async resetKey(): Promise { + const key = new Uint8Array(32); + crypto.getRandomValues(key); + await this.secretStorage.store(SECRET_STORAGE_NAME, encodeUnpaddedBase64(key)); + this.key = key; + } + + /** + * Get and cache the encryption key from secret storage. + * + * If `create` is `true`, creates a new key if no existing key is present. + * + * @returns the key, if available, or `null` if no key is available + */ + private async getKey(create: boolean): Promise { + if (this.key === undefined) { + const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME); + if (keyB64 === undefined) { + if (!create) { + return null; + } + await this.resetKey(); + } else { + this.key = decodeBase64(keyB64); + } + } + return this.key!; + } + + /** + * Rehydrate the dehydrated device stored on the server. + * + * Checks if there is a dehydrated device on the server. If so, rehydrates + * the device and processes the to-device events. + * + * Returns whether or not a dehydrated device was found. + */ + public async rehydrateDeviceIfAvailable(): Promise { + const key = await this.getKey(false); + if (!key) { + return false; + } + + let dehydratedDeviceResp; + try { + dehydratedDeviceResp = await this.http.authedRequest( + Method.Get, + "/dehydrated_device", + undefined, + undefined, + { + prefix: UnstablePrefix, + }, + ); + } catch (error) { + const err = error as MatrixError; + // We ignore M_NOT_FOUND (there is no dehydrated device, so nothing + // us to do) and M_UNRECOGNIZED (the server does not understand the + // endpoint). We pass through any other errors. + if (err.errcode === "M_NOT_FOUND" || err.errcode === "M_UNRECOGNIZED") { + this.logger.info("dehydration: No dehydrated device"); + return false; + } + throw err; + } + + this.logger.info("dehydration: dehydrated device found"); + + const rehydratedDevice = await this.olmMachine + .dehydratedDevices() + .rehydrate( + key, + new RustSdkCryptoJs.DeviceId(dehydratedDeviceResp.device_id), + JSON.stringify(dehydratedDeviceResp.device_data), + ); + + this.logger.info("dehydration: device rehydrated"); + + let nextBatch: string | undefined = undefined; + let toDeviceCount = 0; + let roomKeyCount = 0; + const path = encodeUri("/dehydrated_device/$device_id/events", { + $device_id: dehydratedDeviceResp.device_id, + }); + // eslint-disable-next-line no-constant-condition + while (true) { + const eventResp: DehydratedDeviceEventsResp = await this.http.authedRequest( + Method.Post, + path, + undefined, + nextBatch ? { next_batch: nextBatch } : {}, + { + prefix: UnstablePrefix, + }, + ); + + if (eventResp.events.length === 0) { + break; + } + toDeviceCount += eventResp.events.length; + nextBatch = eventResp.next_batch; + const roomKeyInfos = await rehydratedDevice.receiveEvents(JSON.stringify(eventResp.events)); + roomKeyCount += roomKeyInfos.length; + } + this.logger.info(`dehydration: received ${roomKeyCount} room keys from ${toDeviceCount} to-device events`); + + return true; + } + + /** + * Creates and uploads a new dehydrated device. + * + * Creates and stores a new key in secret storage if none is available. + */ + public async createAndUploadDehydratedDevice(): Promise { + const key = (await this.getKey(true))!; + + const dehydratedDevice = await this.olmMachine.dehydratedDevices().create(); + const request = await dehydratedDevice.keysForUpload("Dehydrated device", key); + + await this.outgoingRequestProcessor.makeOutgoingRequest(request); + + this.logger.info("dehydration: uploaded device"); + } + + /** + * Schedule periodic creation of dehydrated devices. + */ + public async scheduleDeviceDehydration(): Promise { + // cancel any previously-scheduled tasks + this.stop(); + + await this.createAndUploadDehydratedDevice(); + this.intervalId = setInterval(() => { + this.createAndUploadDehydratedDevice().catch((error) => { + this.logger.error("Error creating dehydrated device:", error); + }); + }, DEHYDRATION_INTERVAL); + } + + /** + * Stop the dehydrated device manager. + * + * Cancels any scheduled dehydration tasks. + */ + public stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = undefined; + } + } +} diff --git a/src/rust-crypto/OutgoingRequestProcessor.ts b/src/rust-crypto/OutgoingRequestProcessor.ts index 26845015417..d3ae4d9c4a3 100644 --- a/src/rust-crypto/OutgoingRequestProcessor.ts +++ b/src/rust-crypto/OutgoingRequestProcessor.ts @@ -20,6 +20,7 @@ import { KeysQueryRequest, KeysUploadRequest, OlmMachine, + PutDehydratedDeviceRequest, RoomMessageRequest, SignatureUploadRequest, ToDeviceRequest, @@ -32,6 +33,7 @@ import { logDuration, QueryDict, sleep } from "../utils"; import { AuthDict, UIAuthCallback } from "../interactive-auth"; import { UIAResponse } from "../@types/uia"; import { ToDeviceMessageId } from "../@types/event"; +import { UnstablePrefix as DehydrationUnstablePrefix } from "./DehydratedDeviceManager"; /** * Common interface for all the request types returned by `OlmMachine.outgoingRequests`. @@ -62,7 +64,7 @@ export class OutgoingRequestProcessor { ) {} public async makeOutgoingRequest( - msg: OutgoingRequest | UploadSigningKeysRequest, + msg: OutgoingRequest | UploadSigningKeysRequest | PutDehydratedDeviceRequest, uiaCallback?: UIAuthCallback, ): Promise { let resp: string; @@ -102,6 +104,11 @@ export class OutgoingRequestProcessor { ); // SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent. return; + } else if (msg instanceof PutDehydratedDeviceRequest) { + const path = DehydrationUnstablePrefix + "/dehydrated_device"; + await this.rawJsonRequest(Method.Put, path, {}, msg.body); + // PutDehydratedDeviceRequest does not implement OutgoingRequest and does not need to be marked as sent. + return; } else { logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); resp = ""; diff --git a/src/rust-crypto/device-converter.ts b/src/rust-crypto/device-converter.ts index b37a63dcca0..587640cb0e9 100644 --- a/src/rust-crypto/device-converter.ts +++ b/src/rust-crypto/device-converter.ts @@ -80,6 +80,7 @@ export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: Rus verified, signatures, displayName: device.displayName, + dehydrated: device.isDehydrated, }); } diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 512b0822fb5..713c113866a 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -73,6 +73,7 @@ import { ISignatures } from "../@types/signed"; import { encodeBase64 } from "../base64"; import { OutgoingRequestsManager } from "./OutgoingRequestsManager"; import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader"; +import { DehydratedDeviceManager } from "./DehydratedDeviceManager"; import { VerificationMethod } from "../types"; const ALL_VERIFICATION_METHODS = [ @@ -107,9 +108,8 @@ export class RustCrypto extends TypedEventEmitter(this); public constructor( @@ -148,14 +148,19 @@ export class RustCrypto extends TypedEventEmitter { + return await this.dehydratedDeviceManager.isSupported(); + } + + /** + * Implementation of {@link CryptoBackend#startDehydration}. + */ + public async startDehydration(createNewKey?: boolean): Promise { + if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) { + throw new Error("Device dehydration requires cross-signing and secret storage to be set up"); + } + return await this.dehydratedDeviceManager.start(createNewKey); + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation diff --git a/yarn.lock b/yarn.lock index e6207c8cfd6..c0fff20a6d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6535,16 +6535,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==