Skip to content

Commit

Permalink
initial attempt at scheduling device dehydration on restart
Browse files Browse the repository at this point in the history
  • Loading branch information
uhoreg committed Jan 15, 2025
1 parent c916a0c commit 34d7d34
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 22 deletions.
101 changes: 101 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import {
VerificationRequest,
} from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { defer } from "../../../src/utils";
import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
Expand Down Expand Up @@ -1722,6 +1724,105 @@ describe("RustCrypto", () => {
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
});

it("should handle dehydration start options", async () => {
let secretStorageKey: SecretStorageKeyObject | undefined;
const secrets: Map<string, string> = new Map();
const secretStorage = {
addKey: jest.fn(async(algorithm, opts, keyId) => {
const keyInfo = { algorithm } as SecretStorageKeyDescriptionAesV1;

if (opts.name) {
keyInfo.name = opts.name;
}

if (!keyId) {
keyId = "foo";
}
secretStorageKey = {
keyId,
keyInfo,
};
return secretStorageKey;
}),
getDefaultKeyId: jest.fn(async () => secretStorageKey?.keyId),
get: jest.fn(async (name: string) => secrets.get(name)),
getKey: jest.fn(async (keyId?: string | null) => secretStorageKey),
hasKey: jest.fn(async (keyId: string) => keyId in [secretStorageKey?.keyId, undefined]),
isStored: jest.fn(async (name: string) => secrets.has(name)),
setDefaultKeyId: jest.fn(),
store: jest.fn(async (name: string, value: string) => secrets.set(name, value)),
} as SecretStorage;
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
const e2eKeyResponder = new E2EKeyResponder("http://server");
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
const rustCrypto = await makeTestRustCrypto(
makeMatrixHttpApi(),
TEST_USER,
TEST_DEVICE_ID,
secretStorage,
);
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto.preprocessToDeviceMessages([]);
await rustCrypto.onSyncCompleted({});

async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
console.log("*** bootstrap cross signing");
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
console.log("*** bootstrap secret storage");
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
console.log("*** done bootstrap");

fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});

// When "onlyIfKeyCached" is set to true and no key is available,
// should do nothing
console.log("*** onlyIfKeyCached: true");
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
expect(fetchMock).not.toHaveFetched();

fetchMock.mockClear();

// With default options and no key is available, should create new
// key and create new dehydrated device
console.log("*** default");
await rustCrypto.startDehydration();
expect(fetchMock).toHaveFetched();

fetchMock.mockClear();

console.log("*** done");
});
});

describe("import & export secrets bundle", () => {
Expand Down
27 changes: 20 additions & 7 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
* @packageDocumentation
*/

/**
* The options to start device dehydration.
*/
export interface StartDehydrationOpts {
/** Force creation of a new dehydration key. Defaults to `false`. */
createNewKey?: boolean;
/** Only start dehydration if we have a cached key. Defaults to `false`. */
onlyIfKeyCached?: boolean;
/** Try to rehydrate a device before creating a new dehydrated device. Defaults to `true`. */
rehydrate?: boolean;
}

/**
* Public interface to the cryptography parts of the js-sdk
*
Expand Down Expand Up @@ -634,10 +646,11 @@ export interface CryptoApi {
/**
* Start using device dehydration.
*
* - Rehydrates a dehydrated device, if one is available.
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
* is `true`.
* - Creates a new dehydration key, if necessary, and stores it in Secret
* Storage.
* - If `createNewKey` is set to true, always creates a new key.
* - If `opts.createNewKey` is set to true, always creates a new key.
* - If a dehydration key is not available, creates a new one.
* - Creates a new dehydrated device, and schedules periodically creating
* new dehydrated devices.
Expand All @@ -646,11 +659,11 @@ export interface CryptoApi {
* `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.
* @param opts - options for device dehydration. For backwards compatibility
* with old code, a boolean can be given here, which will be treated as
* the `createNewKey` option. However, this is deprecated.
*/
startDehydration(createNewKey?: boolean): Promise<void>;
startDehydration(opts: StartDehydrationOpts | boolean): Promise<void>;

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
Expand Down Expand Up @@ -1286,7 +1299,7 @@ export abstract class DehydratedDevicesAPI extends TypedEventEmitter<
}

public abstract isSupported(): Promise<boolean>;
public abstract start(createNewKey?: boolean): Promise<void>;
public abstract start(opts: StartDehydrationOpts | boolean): Promise<void>;
}

export * from "./verification.ts";
Expand Down
5 changes: 5 additions & 0 deletions src/rust-crypto/CrossSigningIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export class CrossSigningIdentity {
* Initialise our cross-signing keys by creating new keys if they do not exist, and uploading to the server
*/
public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> {
console.log("*** bootstrapCrossSigning", opts);
if (opts.setupNewCrossSigning) {
console.log("*** calling resetCrossSigning");
await this.resetCrossSigning(opts.authUploadDeviceSigningKeys);
return;
}
Expand Down Expand Up @@ -151,9 +153,12 @@ export class CrossSigningIdentity {
outgoingRequests.uploadSignaturesRequest,
]) {
if (req) {
console.log("*** before makeOutgoingRequest", req, this.outgoingRequestProcessor);
await this.outgoingRequestProcessor.makeOutgoingRequest(req, authUploadDeviceSigningKeys);
console.log("*** after makeOutgoingRequest", req);
}
}
logger.log("*** done reset");
}

/**
Expand Down
37 changes: 24 additions & 13 deletions src/rust-crypto/DehydratedDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { IToDeviceEvent } from "../sync-accumulator.ts";
import { ServerSideSecretStorage } from "../secret-storage.ts";
import { decodeBase64 } from "../base64.ts";
import { Logger } from "../logger.ts";
import { DehydratedDevicesEvents, DehydratedDevicesAPI } from "../crypto-api/index.ts";
import { DehydratedDevicesEvents, DehydratedDevicesAPI, StartDehydrationOpts } from "../crypto-api/index.ts";

/**
* The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`.
Expand Down Expand Up @@ -123,27 +123,38 @@ export class DehydratedDeviceManager extends DehydratedDevicesAPI {
/**
* Start using device dehydration.
*
* - Rehydrates a dehydrated device, if one is available.
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
* is `true`.
* - Creates a new dehydration key, if necessary, and stores it in Secret
* Storage.
* - If `createNewKey` is set to true, always creates a new key.
* - If `opts.createNewKey` is set to true, always creates a new key.
* - If a dehydration key is not available, creates a new one.
* - 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.
* @param opts - options for device dehydration. For backwards compatibility
* with old code, a boolean can be given here, which will be treated as
* the `createNewKey` option. However, this is deprecated.
*/
public async start(createNewKey?: boolean): Promise<void> {
public async start(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
if (typeof opts === "boolean") {
opts = { createNewKey: opts };
}

if (opts.onlyIfKeyCached && !(await this.getCachedKey())) {
return;
}
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 (opts.rehydrate !== false) {
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) {
if (opts.createNewKey) {
await this.resetKey();
}
await this.scheduleDeviceDehydration();
Expand Down
2 changes: 2 additions & 0 deletions src/rust-crypto/OutgoingRequestProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class OutgoingRequestProcessor {
/* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
* for the complete list of request types
*/
console.log(msg);
if (msg instanceof KeysUploadRequest) {
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
} else if (msg instanceof KeysQueryRequest) {
Expand Down Expand Up @@ -134,6 +135,7 @@ export class OutgoingRequestProcessor {
} else {
logger.trace(`Outgoing request type:${msg.type} does not have an ID`);
}
console.log("*** done make request", msg);
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
KeyBackupRestoreOpts,
KeyBackupRestoreResult,
DehydratedDevicesAPI,
StartDehydrationOpts,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
Expand Down Expand Up @@ -1400,11 +1401,12 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/**
* Implementation of {@link CryptoApi#startDehydration}.
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
public async startDehydration(opts: StartDehydrationOpts | boolean): Promise<void> {
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
console.log(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);
return await this.dehydratedDeviceManager.start(opts || {});
}

/**
Expand Down

0 comments on commit 34d7d34

Please sign in to comment.