Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 3137339

Browse files
uhoregrichvdh
andauthored
Add support for device dehydration v2 (#12316)
* rehydrate/dehydrate device if configured in well-known * add handling for dehydrated devices * some fixes * schedule dehydration * improve display of own dehydrated device * created dehydrated device when creating or resetting SSSS * some UI tweaks * reorder strings * lint * remove statement for testing * add playwright test * lint and fix broken test * update to new dehydration API * some fixes from review * try to fix test error * remove unneeded debug line * apply changes from review * add Jest tests * fix typo Co-authored-by: Richard van der Hoff <[email protected]> * don't need Object.assign Co-authored-by: Richard van der Hoff <[email protected]> --------- Co-authored-by: Richard van der Hoff <[email protected]>
1 parent 6392759 commit 3137339

File tree

18 files changed

+823
-8
lines changed

18 files changed

+823
-8
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { Locator, type Page } from "@playwright/test";
18+
19+
import { test as base, expect } from "../../element-web-test";
20+
import { viewRoomSummaryByName } from "../right-panel/utils";
21+
import { isDendrite } from "../../plugins/homeserver/dendrite";
22+
23+
const test = base.extend({
24+
// eslint-disable-next-line no-empty-pattern
25+
startHomeserverOpts: async ({}, use) => {
26+
await use("dehydration");
27+
},
28+
config: async ({ homeserver, context }, use) => {
29+
const wellKnown = {
30+
"m.homeserver": {
31+
base_url: homeserver.config.baseUrl,
32+
},
33+
"org.matrix.msc3814": true,
34+
};
35+
36+
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
37+
await route.fulfill({ json: wellKnown });
38+
});
39+
40+
await use({
41+
default_server_config: wellKnown,
42+
});
43+
},
44+
});
45+
46+
const ROOM_NAME = "Test room";
47+
const NAME = "Alice";
48+
49+
function getMemberTileByName(page: Page, name: string): Locator {
50+
return page.locator(`.mx_EntityTile, [title="${name}"]`);
51+
}
52+
53+
test.describe("Dehydration", () => {
54+
test.skip(isDendrite, "does not yet support dehydration v2");
55+
56+
test.use({
57+
displayName: NAME,
58+
});
59+
60+
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
61+
test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
62+
63+
// Create a backup (which will create SSSS, and dehydrated device)
64+
65+
const securityTab = await app.settings.openUserSettings("Security & Privacy");
66+
67+
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
68+
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
69+
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
70+
71+
const currentDialogLocator = page.locator(".mx_Dialog");
72+
73+
// It's the first time and secure storage is not set up, so it will create one
74+
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
75+
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
76+
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
77+
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
78+
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
79+
80+
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
81+
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
82+
83+
// Open the settings again
84+
await app.settings.openUserSettings("Security & Privacy");
85+
86+
// The Security tab should indicate that there is a dehydrated device present
87+
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
88+
89+
await app.settings.closeDialog();
90+
91+
// the dehydrated device gets created with the name "Dehydrated
92+
// device". We want to make sure that it is not visible as a normal
93+
// device.
94+
const sessionsTab = await app.settings.openUserSettings("Sessions");
95+
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
96+
97+
await app.settings.closeDialog();
98+
99+
// now check that the user info right-panel shows the dehydrated device
100+
// as a feature rather than as a normal device
101+
await app.client.createRoom({ name: ROOM_NAME });
102+
103+
await viewRoomSummaryByName(page, app, ROOM_NAME);
104+
105+
await page.getByRole("menuitem", { name: "People" }).click();
106+
await expect(page.locator(".mx_MemberList")).toBeVisible();
107+
108+
await getMemberTileByName(page, NAME).click();
109+
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
110+
111+
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
112+
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
113+
});
114+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A synapse configured with device dehydration v2 enabled
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
server_name: "localhost"
2+
pid_file: /data/homeserver.pid
3+
public_baseurl: "{{PUBLIC_BASEURL}}"
4+
listeners:
5+
- port: 8008
6+
tls: false
7+
bind_addresses: ["::"]
8+
type: http
9+
x_forwarded: true
10+
11+
resources:
12+
- names: [client]
13+
compress: false
14+
15+
database:
16+
name: "sqlite3"
17+
args:
18+
database: ":memory:"
19+
20+
log_config: "/data/log.config"
21+
22+
rc_messages_per_second: 10000
23+
rc_message_burst_count: 10000
24+
rc_registration:
25+
per_second: 10000
26+
burst_count: 10000
27+
rc_joins:
28+
local:
29+
per_second: 9999
30+
burst_count: 9999
31+
remote:
32+
per_second: 9999
33+
burst_count: 9999
34+
rc_joins_per_room:
35+
per_second: 9999
36+
burst_count: 9999
37+
rc_3pid_validation:
38+
per_second: 1000
39+
burst_count: 1000
40+
41+
rc_invites:
42+
per_room:
43+
per_second: 1000
44+
burst_count: 1000
45+
per_user:
46+
per_second: 1000
47+
burst_count: 1000
48+
49+
rc_login:
50+
address:
51+
per_second: 10000
52+
burst_count: 10000
53+
account:
54+
per_second: 10000
55+
burst_count: 10000
56+
failed_attempts:
57+
per_second: 10000
58+
burst_count: 10000
59+
60+
media_store_path: "/data/media_store"
61+
uploads_path: "/data/uploads"
62+
enable_registration: true
63+
enable_registration_without_verification: true
64+
disable_msisdn_registration: false
65+
registration_shared_secret: "{{REGISTRATION_SECRET}}"
66+
report_stats: false
67+
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
68+
form_secret: "{{FORM_SECRET}}"
69+
signing_key_path: "/data/localhost.signing.key"
70+
71+
trusted_key_servers:
72+
- server_name: "matrix.org"
73+
suppress_key_server_warning: true
74+
75+
ui_auth:
76+
session_timeout: "300s"
77+
78+
oidc_providers:
79+
- idp_id: test
80+
idp_name: "OAuth test"
81+
issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth"
82+
authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html"
83+
# the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container.
84+
token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token"
85+
userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo"
86+
client_id: "synapse"
87+
discover: false
88+
scopes: ["profile"]
89+
skip_verification: true
90+
client_auth_method: none
91+
user_mapping_provider:
92+
config:
93+
display_name_template: "{{ user.name }}"
94+
95+
# Inhibit background updates as this Synapse isn't long-lived
96+
background_updates:
97+
min_batch_size: 100000
98+
sleep_duration_ms: 100000
99+
100+
experimental_features:
101+
msc2697_enabled: false
102+
msc3814_enabled: true
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Log configuration for Synapse.
2+
#
3+
# This is a YAML file containing a standard Python logging configuration
4+
# dictionary. See [1] for details on the valid settings.
5+
#
6+
# Synapse also supports structured logging for machine readable logs which can
7+
# be ingested by ELK stacks. See [2] for details.
8+
#
9+
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
10+
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
11+
12+
version: 1
13+
14+
formatters:
15+
precise:
16+
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
17+
18+
handlers:
19+
# A handler that writes logs to stderr. Unused by default, but can be used
20+
# instead of "buffer" and "file" in the logger handlers.
21+
console:
22+
class: logging.StreamHandler
23+
formatter: precise
24+
25+
loggers:
26+
synapse.storage.SQL:
27+
# beware: increasing this to DEBUG will make synapse log sensitive
28+
# information such as access tokens.
29+
level: DEBUG
30+
31+
twisted:
32+
# We send the twisted logging directly to the file handler,
33+
# to work around https://github.com/matrix-org/synapse/issues/3471
34+
# when using "buffer" logger. Use "console" to log to stderr instead.
35+
handlers: [console]
36+
propagate: false
37+
38+
root:
39+
level: DEBUG
40+
41+
# Write logs to the `buffer` handler, which will buffer them together in memory,
42+
# then write them to a file.
43+
#
44+
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
45+
# also need to update the configuration for the `twisted` logger above, in
46+
# this case.)
47+
#
48+
handlers: [console]
49+
50+
disable_existing_loggers: false

src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import InteractiveAuthDialog from "../../../../components/views/dialogs/Interact
4848
import { IValidationResult } from "../../../../components/views/elements/Validation";
4949
import { Icon as CheckmarkIcon } from "../../../../../res/img/element-icons/check.svg";
5050
import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField";
51+
import { initialiseDehydration } from "../../../../utils/device/dehydration";
5152

5253
// I made a mistake while converting this and it has to be fixed!
5354
enum Phase {
@@ -397,6 +398,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
397398
},
398399
});
399400
}
401+
await initialiseDehydration(true);
400402

401403
this.setState({
402404
phase: Phase.Stored,

src/components/views/right_panel/UserInfo.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,20 @@ function DevicesSection({
290290
let expandHideCaption;
291291
let expandIconClasses = "mx_E2EIcon";
292292

293+
const dehydratedDeviceIds: string[] = [];
294+
for (const device of devices) {
295+
if (device.dehydrated) {
296+
dehydratedDeviceIds.push(device.deviceId);
297+
}
298+
}
299+
// If the user has exactly one device marked as dehydrated, we consider
300+
// that as the dehydrated device, and hide it as a normal device (but
301+
// indicate that the user is using a dehydrated device). If the user has
302+
// more than one, that is anomalous, and we show all the devices so that
303+
// nothing is hidden.
304+
const dehydratedDeviceId: string | undefined = dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined;
305+
let dehydratedDeviceInExpandSection = false;
306+
293307
if (isUserVerified) {
294308
for (let i = 0; i < devices.length; ++i) {
295309
const device = devices[i];
@@ -302,7 +316,13 @@ function DevicesSection({
302316
const isVerified = deviceTrust && (isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified());
303317

304318
if (isVerified) {
305-
expandSectionDevices.push(device);
319+
// don't show dehydrated device as a normal device, if it's
320+
// verified
321+
if (device.deviceId === dehydratedDeviceId) {
322+
dehydratedDeviceInExpandSection = true;
323+
} else {
324+
expandSectionDevices.push(device);
325+
}
306326
} else {
307327
unverifiedDevices.push(device);
308328
}
@@ -311,6 +331,10 @@ function DevicesSection({
311331
expandHideCaption = _t("user_info|hide_verified_sessions");
312332
expandIconClasses += " mx_E2EIcon_verified";
313333
} else {
334+
if (dehydratedDeviceId) {
335+
devices = devices.filter((device) => device.deviceId !== dehydratedDeviceId);
336+
dehydratedDeviceInExpandSection = true;
337+
}
314338
expandSectionDevices = devices;
315339
expandCountCaption = _t("user_info|count_of_sessions", { count: devices.length });
316340
expandHideCaption = _t("user_info|hide_sessions");
@@ -347,6 +371,9 @@ function DevicesSection({
347371
);
348372
}),
349373
);
374+
if (dehydratedDeviceInExpandSection) {
375+
deviceList.push(<div>{_t("user_info|dehydrated_device_enabled")}</div>);
376+
}
350377
}
351378

352379
return (

src/components/views/settings/devices/useOwnDevices.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export enum OwnDevicesError {
7777
}
7878
export type DevicesState = {
7979
devices: DevicesDictionary;
80+
dehydratedDeviceId?: string;
8081
pushers: IPusher[];
8182
localNotificationSettings: Map<string, LocalNotificationSettings>;
8283
currentDeviceId: string;
@@ -97,6 +98,7 @@ export const useOwnDevices = (): DevicesState => {
9798
const userId = matrixClient.getSafeUserId();
9899

99100
const [devices, setDevices] = useState<DevicesState["devices"]>({});
101+
const [dehydratedDeviceId, setDehydratedDeviceId] = useState<DevicesState["dehydratedDeviceId"]>(undefined);
100102
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
101103
const [localNotificationSettings, setLocalNotificationSettings] = useState<
102104
DevicesState["localNotificationSettings"]
@@ -131,6 +133,21 @@ export const useOwnDevices = (): DevicesState => {
131133
});
132134
setLocalNotificationSettings(notificationSettings);
133135

136+
const ownUserId = matrixClient.getUserId()!;
137+
const userDevices = (await matrixClient.getCrypto()?.getUserDeviceInfo([ownUserId]))?.get(ownUserId);
138+
const dehydratedDeviceIds: string[] = [];
139+
for (const device of userDevices?.values() ?? []) {
140+
if (device.dehydrated) {
141+
dehydratedDeviceIds.push(device.deviceId);
142+
}
143+
}
144+
// If the user has exactly one device marked as dehydrated, we consider
145+
// that as the dehydrated device, and hide it as a normal device (but
146+
// indicate that the user is using a dehydrated device). If the user has
147+
// more than one, that is anomalous, and we show all the devices so that
148+
// nothing is hidden.
149+
setDehydratedDeviceId(dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined);
150+
134151
setIsLoadingDeviceList(false);
135152
} catch (error) {
136153
if ((error as MatrixError).httpStatus == 404) {
@@ -228,6 +245,7 @@ export const useOwnDevices = (): DevicesState => {
228245

229246
return {
230247
devices,
248+
dehydratedDeviceId,
231249
pushers,
232250
localNotificationSettings,
233251
currentDeviceId,

0 commit comments

Comments
 (0)