Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 27 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b855200
initial implementation of device dehydration
uhoreg Feb 12, 2024
4c6254e
Merge branch 'develop' into dehydration_v2
uhoreg Feb 16, 2024
8e632f3
add dehydrated flag for devices
uhoreg Feb 23, 2024
4f231a6
add missing dehydration.ts file, add test, add function to schedule d…
uhoreg Mar 5, 2024
560511d
add more dehydration utility functions
uhoreg Mar 8, 2024
1def99a
stop scheduled dehydration when crypto stops
uhoreg Mar 12, 2024
71eea91
Merge branch 'develop' into dehydration_v2
uhoreg Mar 22, 2024
4fe5a64
bump matrix-crypto-sdk-wasm version, and fix tests
uhoreg Mar 23, 2024
d6fdce3
Merge branch 'develop' into dehydration_v2
uhoreg Mar 24, 2024
12a934c
adding dehydratedDevices member to mock OlmDevice isn't necessary any…
uhoreg Mar 24, 2024
937fac2
fix yarn lock file
uhoreg Mar 24, 2024
49dedf2
more tests
uhoreg Mar 25, 2024
b0f0703
Merge branch 'develop' into dehydration_v2
uhoreg Mar 25, 2024
7aa13df
fix test
uhoreg Mar 25, 2024
3aa06dd
more tests
uhoreg Mar 26, 2024
7c1e82e
fix typo
uhoreg Mar 26, 2024
663448b
fix logic for checking if dehydration supported
uhoreg Mar 26, 2024
d870915
make changes from review
uhoreg Mar 28, 2024
964333a
add missing file
uhoreg Mar 28, 2024
3a66418
move setup into another function
uhoreg Mar 28, 2024
0947a8a
apply changes from review
uhoreg Apr 4, 2024
f666788
implement simpler API
uhoreg Apr 7, 2024
15c8c5c
Merge branch 'develop' into dehydration_v2
uhoreg Apr 8, 2024
27784d7
fix type and move the code to the right spot
uhoreg Apr 8, 2024
9ad28bb
apply suggestions from review
uhoreg Apr 9, 2024
1525a16
make sure that cross-signing and secret storage are set up
uhoreg Apr 9, 2024
a995c46
Merge branch 'develop' into dehydration_v2
uhoreg Apr 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.6.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.8.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
Expand Down
146 changes: 146 additions & 0 deletions spec/integ/crypto/device-dehydration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
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 { 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 () => {
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");

const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;

// try to rehydrate, but there isn't any dehydrated device yet
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
expect(await crypto.rehydrateDeviceIfAvailable()).toBe(false);

// create a dehydrated device
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 crypto.createAndUploadDehydratedDevice();

// rehydrate the device that we just created
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,
);

expect(await crypto.rehydrateDeviceIfAvailable()).toBe(true);
expect(eventsResponse.mock.calls).toHaveLength(2);
});
});

/** 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 {};
});
richvdh marked this conversation as resolved.
Show resolved Hide resolved

await matrixClient.initRustCrypto();

// 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,
});
}
30 changes: 30 additions & 0 deletions spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
PutDehydratedDeviceRequest,
RoomMessageRequest,
SignatureUploadRequest,
UploadSigningKeysRequest,
Expand Down Expand Up @@ -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();
Expand Down
Loading