1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Add support for device dehydration v2 (Element R) (#4062)

* initial implementation of device dehydration

* add dehydrated flag for devices

* add missing dehydration.ts file, add test, add function to schedule dehydration

* add more dehydration utility functions

* stop scheduled dehydration when crypto stops

* bump matrix-crypto-sdk-wasm version, and fix tests

* adding dehydratedDevices member to mock OlmDevice isn't necessary any more

* fix yarn lock file

* more tests

* fix test

* more tests

* fix typo

* fix logic for checking if dehydration supported

* make changes from review

* add missing file

* move setup into another function

* apply changes from review

* implement simpler API

* fix type and move the code to the right spot

* apply suggestions from review

* make sure that cross-signing and secret storage are set up
This commit is contained in:
Hubert Chathi
2024-04-11 00:01:47 -04:00
committed by GitHub
parent 82ed7bd86a
commit 936e7c3072
11 changed files with 643 additions and 17 deletions

View File

@ -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<void>((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<void> {
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<string, object> = 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,
});
}

View File

@ -22,6 +22,7 @@ import {
KeysClaimRequest, KeysClaimRequest,
KeysQueryRequest, KeysQueryRequest,
KeysUploadRequest, KeysUploadRequest,
PutDehydratedDeviceRequest,
RoomMessageRequest, RoomMessageRequest,
SignatureUploadRequest, SignatureUploadRequest,
UploadSigningKeysRequest, UploadSigningKeysRequest,
@ -233,6 +234,35 @@ describe("OutgoingRequestProcessor", () => {
httpBackend.verifyNoOutstandingRequests(); 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 () => { it("does not explode with unknown requests", async () => {
const outgoingRequest = { id: "5678", type: 987 }; const outgoingRequest = { id: "5678", type: 987 };
const markSentCallPromise = awaitCallToMarkAsSent(); const markSentCallPromise = awaitCallToMarkAsSent();

View File

@ -762,8 +762,11 @@ describe("RustCrypto", () => {
}, },
}, },
}; };
} else if (request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) { } else if (
// SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent. 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; return;
} }
if (request.id) { 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 */ /** Build a MatrixHttpApi instance */

View File

@ -496,6 +496,42 @@ export interface CryptoApi {
* @param version - The backup version to delete. * @param version - The backup version to delete.
*/ */
deleteKeyBackupVersion(version: string): Promise<void>; deleteKeyBackupVersion(version: string): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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<boolean>;
/**
* 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<void>;
} }
/** A reason code for a failure to decrypt an event. */ /** A reason code for a failure to decrypt an event. */

View File

@ -4287,6 +4287,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public getRoomEncryption(roomId: string): IRoomEncryption | null { public getRoomEncryption(roomId: string): IRoomEncryption | null {
return this.roomList.getRoomEncryption(roomId); return this.roomList.getRoomEncryption(roomId);
} }
/**
* Returns whether dehydrated devices are supported by the crypto backend
* and by the server.
*/
public async isDehydrationSupported(): Promise<boolean> {
return false;
}
/**
* Stub function -- dehydration is not implemented here, so throw error
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
throw new Error("Not implemented");
}
} }
/** /**

View File

@ -51,6 +51,9 @@ export class Device {
/** display name of the device */ /** display name of the device */
public readonly displayName?: string; public readonly displayName?: string;
/** whether the device is a dehydrated device */
public readonly dehydrated: boolean = false;
public constructor(opts: DeviceParameters) { public constructor(opts: DeviceParameters) {
this.deviceId = opts.deviceId; this.deviceId = opts.deviceId;
this.userId = opts.userId; this.userId = opts.userId;
@ -59,6 +62,7 @@ export class Device {
this.verified = opts.verified || DeviceVerification.Unverified; this.verified = opts.verified || DeviceVerification.Unverified;
this.signatures = opts.signatures || new Map(); this.signatures = opts.signatures || new Map();
this.displayName = opts.displayName; this.displayName = opts.displayName;
this.dehydrated = !!opts.dehydrated;
} }
/** /**

View File

@ -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<typeof setInterval>;
public constructor(
private readonly logger: Logger,
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly secretStorage: ServerSideSecretStorage,
) {}
/**
* Return whether the server supports dehydrated devices.
*/
public async isSupported(): Promise<boolean> {
// 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<DehydratedDeviceResp>(
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<void> {
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<boolean> {
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<void> {
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<Uint8Array | null> {
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<boolean> {
const key = await this.getKey(false);
if (!key) {
return false;
}
let dehydratedDeviceResp;
try {
dehydratedDeviceResp = await this.http.authedRequest<DehydratedDeviceResp>(
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<DehydratedDeviceEventsResp>(
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<void> {
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<void> {
// 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;
}
}
}

View File

@ -20,6 +20,7 @@ import {
KeysQueryRequest, KeysQueryRequest,
KeysUploadRequest, KeysUploadRequest,
OlmMachine, OlmMachine,
PutDehydratedDeviceRequest,
RoomMessageRequest, RoomMessageRequest,
SignatureUploadRequest, SignatureUploadRequest,
ToDeviceRequest, ToDeviceRequest,
@ -32,6 +33,7 @@ import { logDuration, QueryDict, sleep } from "../utils";
import { AuthDict, UIAuthCallback } from "../interactive-auth"; import { AuthDict, UIAuthCallback } from "../interactive-auth";
import { UIAResponse } from "../@types/uia"; import { UIAResponse } from "../@types/uia";
import { ToDeviceMessageId } from "../@types/event"; import { ToDeviceMessageId } from "../@types/event";
import { UnstablePrefix as DehydrationUnstablePrefix } from "./DehydratedDeviceManager";
/** /**
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`. * Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
@ -62,7 +64,7 @@ export class OutgoingRequestProcessor {
) {} ) {}
public async makeOutgoingRequest<T>( public async makeOutgoingRequest<T>(
msg: OutgoingRequest | UploadSigningKeysRequest, msg: OutgoingRequest | UploadSigningKeysRequest | PutDehydratedDeviceRequest,
uiaCallback?: UIAuthCallback<T>, uiaCallback?: UIAuthCallback<T>,
): Promise<void> { ): Promise<void> {
let resp: string; let resp: string;
@ -102,6 +104,11 @@ export class OutgoingRequestProcessor {
); );
// SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent. // SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent.
return; 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 { } else {
logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
resp = ""; resp = "";

View File

@ -80,6 +80,7 @@ export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: Rus
verified, verified,
signatures, signatures,
displayName: device.displayName, displayName: device.displayName,
dehydrated: device.isDehydrated,
}); });
} }

View File

@ -73,6 +73,7 @@ import { ISignatures } from "../@types/signed";
import { encodeBase64 } from "../base64"; import { encodeBase64 } from "../base64";
import { OutgoingRequestsManager } from "./OutgoingRequestsManager"; import { OutgoingRequestsManager } from "./OutgoingRequestsManager";
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader"; import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader";
import { DehydratedDeviceManager } from "./DehydratedDeviceManager";
import { VerificationMethod } from "../types"; import { VerificationMethod } from "../types";
const ALL_VERIFICATION_METHODS = [ const ALL_VERIFICATION_METHODS = [
@ -107,9 +108,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private crossSigningIdentity: CrossSigningIdentity; private crossSigningIdentity: CrossSigningIdentity;
private readonly backupManager: RustBackupManager; private readonly backupManager: RustBackupManager;
private outgoingRequestsManager: OutgoingRequestsManager; private outgoingRequestsManager: OutgoingRequestsManager;
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader; private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader;
private readonly dehydratedDeviceManager: DehydratedDeviceManager;
private readonly reemitter = new TypedReEmitter<RustCryptoEvents, RustCryptoEventMap>(this); private readonly reemitter = new TypedReEmitter<RustCryptoEvents, RustCryptoEventMap>(this);
public constructor( public constructor(
@ -148,14 +148,19 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor); this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
this.backupManager = new RustBackupManager(olmMachine, http, this.outgoingRequestProcessor); this.backupManager = new RustBackupManager(olmMachine, http, this.outgoingRequestProcessor);
this.perSessionBackupDownloader = new PerSessionKeyBackupDownloader( this.perSessionBackupDownloader = new PerSessionKeyBackupDownloader(
this.logger, this.logger,
this.olmMachine, this.olmMachine,
this.http, this.http,
this.backupManager, this.backupManager,
); );
this.dehydratedDeviceManager = new DehydratedDeviceManager(
this.logger,
olmMachine,
http,
this.outgoingRequestProcessor,
secretStorage,
);
this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader); this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader);
this.reemitter.reEmit(this.backupManager, [ this.reemitter.reEmit(this.backupManager, [
@ -212,6 +217,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.backupManager.stop(); this.backupManager.stop();
this.outgoingRequestsManager.stop(); this.outgoingRequestsManager.stop();
this.perSessionBackupDownloader.stop(); this.perSessionBackupDownloader.stop();
this.dehydratedDeviceManager.stop();
// make sure we close() the OlmMachine; doing so means that all the Rust objects will be // make sure we close() the OlmMachine; doing so means that all the Rust objects will be
// cleaned up; in particular, the indexeddb connections will be closed, which means they // cleaned up; in particular, the indexeddb connections will be closed, which means they
@ -1212,6 +1218,23 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return await this.backupManager.importBackedUpRoomKeys(keys, opts); return await this.backupManager.importBackedUpRoomKeys(keys, opts);
} }
/**
* Implementation of {@link CryptoBackend#isDehydrationSupported}.
*/
public async isDehydrationSupported(): Promise<boolean> {
return await this.dehydratedDeviceManager.isSupported();
}
/**
* Implementation of {@link CryptoBackend#startDehydration}.
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
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 // SyncCryptoCallbacks implementation

View File

@ -6535,16 +6535,7 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0": "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==
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:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==