From 6b12844a35c91d514f1f6d1fc7acfa60951efb39 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 16 Feb 2024 13:28:14 -0500 Subject: [PATCH 01/20] rehydrate/dehydrate device if configured in well-known --- src/stores/SetupEncryptionStore.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 72e463a9c76..6e4dab6dbdb 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -152,6 +152,14 @@ export class SetupEncryptionStore extends EventEmitter { // to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo); } + const wellknown = cli.getClientWellKnown(); + if (wellknown?.["org.matrix.msc3814"]) { + logger.log("Device dehydration enabled in well-known"); + // if we accessed secret storage, we know crypto is available + const crypto = cli.getCrypto()!; + await crypto.rehydrateDeviceIfAvailable(); + await crypto.createAndUploadDehydratedDevice(); + } }).catch(reject); }); From d89e379a41ac6c5bf3a25322785585bc744002f7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 23 Feb 2024 16:40:49 -0500 Subject: [PATCH 02/20] add handling for dehydrated devices --- src/components/views/right_panel/UserInfo.tsx | 14 ++++++++++++++ .../views/settings/devices/useOwnDevices.ts | 13 +++++++++++++ .../views/settings/tabs/user/SessionManagerTab.tsx | 5 +++++ src/stores/SetupEncryptionStore.ts | 3 +++ 4 files changed, 35 insertions(+) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index a339a1ebce0..26e338b50c8 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -289,9 +289,20 @@ function DevicesSection({ let expandHideCaption; let expandIconClasses = "mx_E2EIcon"; + const dehydratedDeviceIds: string[] = []; + for (const device of devices) { + if (device.dehydrated) { + dehydratedDeviceIds.push(device.deviceId); + } + } + const dehydratedDeviceId: string | undefined = dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined; + if (isUserVerified) { for (let i = 0; i < devices.length; ++i) { const device = devices[i]; + if (device.deviceId === dehydratedDeviceId) { + continue; + } const deviceTrust = deviceTrusts[i]; // For your own devices, we use the stricter check of cross-signing // verification to encourage everyone to trust their own devices via @@ -310,6 +321,9 @@ function DevicesSection({ expandHideCaption = _t("user_info|hide_verified_sessions"); expandIconClasses += " mx_E2EIcon_verified"; } else { + if (dehydratedDeviceId) { + devices = devices.filter((device) => device.deviceId !== dehydratedDeviceId); + } expandSectionDevices = devices; expandCountCaption = _t("user_info|count_of_sessions", { count: devices.length }); expandHideCaption = _t("user_info|hide_sessions"); diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index c735b2cbcec..0096e956ab9 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -77,6 +77,7 @@ export enum OwnDevicesError { } export type DevicesState = { devices: DevicesDictionary; + dehydratedDeviceId?: string; pushers: IPusher[]; localNotificationSettings: Map; currentDeviceId: string; @@ -97,6 +98,7 @@ export const useOwnDevices = (): DevicesState => { const userId = matrixClient.getSafeUserId(); const [devices, setDevices] = useState({}); + const [dehydratedDeviceId, setDehydratedDeviceId] = useState(undefined); const [pushers, setPushers] = useState([]); const [localNotificationSettings, setLocalNotificationSettings] = useState< DevicesState["localNotificationSettings"] @@ -131,6 +133,16 @@ export const useOwnDevices = (): DevicesState => { }); setLocalNotificationSettings(notificationSettings); + const userDevices = await matrixClient.getCrypto()?.getUserDevices(matrixClient.getUserId()!); + const dehydratedDeviceIds: string[] = []; + for (const device of userDevices.values()) { + if (device.dehydrated) { + logger.debug("Found dehydrated device", device.deviceId); + dehydratedDeviceIds.push(device.deviceId); + } + } + setDehydratedDeviceId(dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined); + setIsLoadingDeviceList(false); } catch (error) { if ((error as MatrixError).httpStatus == 404) { @@ -228,6 +240,7 @@ export const useOwnDevices = (): DevicesState => { return { devices, + dehydratedDeviceId, pushers, localNotificationSettings, currentDeviceId, diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index bc06103255c..6583948c9ac 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -150,6 +150,7 @@ const useSignOut = ( const SessionManagerTab: React.FC = () => { const { devices, + dehydratedDeviceId, pushers, localNotificationSettings, currentDeviceId, @@ -205,6 +206,9 @@ const SessionManagerTab: React.FC = () => { }; const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; + if (dehydratedDeviceId) { + delete otherDevices[dehydratedDeviceId]; + } const otherSessionsCount = Object.keys(otherDevices).length; const shouldShowOtherSessions = otherSessionsCount > 0; @@ -330,6 +334,7 @@ const SessionManagerTab: React.FC = () => { /> )} + {dehydratedDeviceId &&
Dehydrated device enabled
} { // ignore the dehydrated device if (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id) return false; + if (device.dehydrated) { + return false; + } // ignore devices without an identity key if (!device.getIdentityKey()) return false; From bef714cbbae653e39ee101527575be8b83dc9d7b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 5 Mar 2024 15:07:58 -0500 Subject: [PATCH 03/20] some fixes --- src/components/views/settings/devices/useOwnDevices.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index 0096e956ab9..6d5b15f0e4b 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -98,7 +98,7 @@ export const useOwnDevices = (): DevicesState => { const userId = matrixClient.getSafeUserId(); const [devices, setDevices] = useState({}); - const [dehydratedDeviceId, setDehydratedDeviceId] = useState(undefined); + const [dehydratedDeviceId, setDehydratedDeviceId] = useState(undefined); const [pushers, setPushers] = useState([]); const [localNotificationSettings, setLocalNotificationSettings] = useState< DevicesState["localNotificationSettings"] @@ -133,9 +133,10 @@ export const useOwnDevices = (): DevicesState => { }); setLocalNotificationSettings(notificationSettings); - const userDevices = await matrixClient.getCrypto()?.getUserDevices(matrixClient.getUserId()!); + const ownUserId = matrixClient.getUserId()!; + const userDevices = (await matrixClient.getCrypto()?.getUserDeviceInfo([ownUserId]))?.get(ownUserId); const dehydratedDeviceIds: string[] = []; - for (const device of userDevices.values()) { + for (const device of userDevices?.values() ?? []) { if (device.dehydrated) { logger.debug("Found dehydrated device", device.deviceId); dehydratedDeviceIds.push(device.deviceId); From bdff188ddaef7f82240d6e41aba2acfad1d323f0 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 5 Mar 2024 15:21:09 -0500 Subject: [PATCH 04/20] schedule dehydration --- src/stores/SetupEncryptionStore.ts | 39 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index d070d2edd71..9bc498bb0f9 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 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. @@ -40,6 +40,9 @@ export enum Phase { ConfirmReset = 6, } +// the interval between creating dehydrated devices +const DEHYDRATION_INTERVAL = 7*24*60*60*1000; + export class SetupEncryptionStore extends EventEmitter { private started?: boolean; public phase?: Phase; @@ -155,14 +158,8 @@ export class SetupEncryptionStore extends EventEmitter { // to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo); } - const wellknown = cli.getClientWellKnown(); - if (wellknown?.["org.matrix.msc3814"]) { - logger.log("Device dehydration enabled in well-known"); - // if we accessed secret storage, we know crypto is available - const crypto = cli.getCrypto()!; - await crypto.rehydrateDeviceIfAvailable(); - await crypto.createAndUploadDehydratedDevice(); - } + + await this.initializeDehydration(); }).catch(reject); }); @@ -267,6 +264,9 @@ export class SetupEncryptionStore extends EventEmitter { }, setupNewCrossSigning: true, }); + + await this.initializeDehydration(); + this.phase = Phase.Finished; }, true); } catch (e) { @@ -304,4 +304,25 @@ export class SetupEncryptionStore extends EventEmitter { public lostKeys(): boolean { return !this.hasDevicesToVerifyAgainst && !this.keyInfo; } + + // check if device dehydration is enabled + private async deviceDehydrationEnabled(): Promise { + const wellknown = await MatrixClientPeg.safeGet().waitForClientWellKnown(); + return !!wellknown?.["org.matrix.msc3814"]; + } + + // if dehydration is enabled, rehydrate a device (if available) and create + // a new dehydrated device + private async initializeDehydration(): Promise { + const crypto = MatrixClientPeg.safeGet().getCrypto(); + if (crypto && await this.deviceDehydrationEnabled()) { + logger.log("Device dehydration enabled"); + try { + await crypto.rehydrateDeviceIfAvailable(); + } catch (e) { + logger.error("Error rehydrating device:", e); + } + await crypto.scheduleDeviceDehydration(DEHYDRATION_INTERVAL); + } + } } From 1b79b6e01a6df6e58b1a63dc42bf02ac5aee3b3f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 5 Mar 2024 18:45:36 -0500 Subject: [PATCH 05/20] improve display of own dehydrated device --- .../tabs/user/SecurityUserSettingsTab.tsx | 17 +++++++++++++++++ .../settings/tabs/user/SessionManagerTab.tsx | 1 - src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 74511dfa4ab..0aa4413ee78 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -41,6 +41,7 @@ import type { IServerVersions } from "matrix-js-sdk/src/matrix"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { useOwnDevices } from "../../devices/useOwnDevices"; interface IIgnoredUserProps { userId: string; @@ -48,6 +49,21 @@ interface IIgnoredUserProps { inProgress: boolean; } +const DehydratedDeviceStatus: ReactFC = () => { + const { dehydratedDeviceId } = useOwnDevices(); + + if (dehydratedDeviceId) { + return ( +
+
{_t("settings|security|dehydrated_device_enabled")}
+
{_t("settings|security|dehydrated_device_description")}
+
+ ); + } else { + return null; + } +} + export class IgnoredUser extends React.Component { private onUnignoreClicked = (): void => { this.props.onUnignored(this.props.userId); @@ -358,6 +374,7 @@ export default class SecurityUserSettingsTab extends React.Component {privacySection} diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 6583948c9ac..09389bae9af 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -334,7 +334,6 @@ const SessionManagerTab: React.FC = () => { /> )} - {dehydratedDeviceId &&
Dehydrated device enabled
} Date: Sun, 10 Mar 2024 20:49:52 -0400 Subject: [PATCH 06/20] created dehydrated device when creating or resetting SSSS --- .../security/CreateSecretStorageDialog.tsx | 2 + src/stores/SetupEncryptionStore.ts | 29 ++-------- src/utils/device/dehydration.ts | 54 +++++++++++++++++++ 3 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 src/utils/device/dehydration.ts diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 036fb5038b3..309aad46d09 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -49,6 +49,7 @@ import InteractiveAuthDialog from "../../../../components/views/dialogs/Interact import { IValidationResult } from "../../../../components/views/elements/Validation"; import { Icon as CheckmarkIcon } from "../../../../../res/img/element-icons/check.svg"; import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField"; +import { initializeDehydration } from "../../../../utils/device/dehydration"; // I made a mistake while converting this and it has to be fixed! enum Phase { @@ -398,6 +399,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - const wellknown = await MatrixClientPeg.safeGet().waitForClientWellKnown(); - return !!wellknown?.["org.matrix.msc3814"]; - } - - // if dehydration is enabled, rehydrate a device (if available) and create - // a new dehydrated device - private async initializeDehydration(): Promise { - const crypto = MatrixClientPeg.safeGet().getCrypto(); - if (crypto && await this.deviceDehydrationEnabled()) { - logger.log("Device dehydration enabled"); - try { - await crypto.rehydrateDeviceIfAvailable(); - } catch (e) { - logger.error("Error rehydrating device:", e); - } - await crypto.scheduleDeviceDehydration(DEHYDRATION_INTERVAL); - } - } } diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts new file mode 100644 index 00000000000..ce562163e47 --- /dev/null +++ b/src/utils/device/dehydration.ts @@ -0,0 +1,54 @@ +/* +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 { logger } from "matrix-js-sdk/src/logger"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; + +// the interval between creating dehydrated devices +const DEHYDRATION_INTERVAL = 7*24*60*60*1000; + +// check if device dehydration is enabled +export async function deviceDehydrationEnabled(): Promise { + const crypto = MatrixClientPeg.safeGet().getCrypto(); + if (!(await crypto.isDehydrationSupported())) { + return false; + } + if (await crypto.isDehydrationKeyStored()) { + return true; + } + return true; + const wellknown = await MatrixClientPeg.safeGet().waitForClientWellKnown(); + return !!wellknown?.["org.matrix.msc3814"]; +} + +// if dehydration is enabled, rehydrate a device (if available) and create +// a new dehydrated device +export async function initializeDehydration(reset?: boolean): Promise { + const crypto = MatrixClientPeg.safeGet().getCrypto(); + if (crypto && await deviceDehydrationEnabled()) { + logger.log("Device dehydration enabled"); + if (reset) { + await crypto.resetDehydrationKey(); + } else { + try { + await crypto.rehydrateDeviceIfAvailable(); + } catch (e) { + logger.error("Error rehydrating device:", e); + } + } + await crypto.scheduleDeviceDehydration(DEHYDRATION_INTERVAL); + } +} From 685926d61954768eb7bd10e380015ab9e24d5d68 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Sat, 16 Mar 2024 17:44:31 -0400 Subject: [PATCH 07/20] some UI tweaks --- src/components/views/right_panel/UserInfo.tsx | 15 +++++++++++---- .../tabs/user/SecurityUserSettingsTab.tsx | 2 +- src/i18n/strings/en_EN.json | 1 + 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 26e338b50c8..b8c3f5ec096 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -296,13 +296,11 @@ function DevicesSection({ } } const dehydratedDeviceId: string | undefined = dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined; + let dehydratedDeviceInExpandSection = false; if (isUserVerified) { for (let i = 0; i < devices.length; ++i) { const device = devices[i]; - if (device.deviceId === dehydratedDeviceId) { - continue; - } const deviceTrust = deviceTrusts[i]; // For your own devices, we use the stricter check of cross-signing // verification to encourage everyone to trust their own devices via @@ -312,7 +310,13 @@ function DevicesSection({ const isVerified = deviceTrust && (isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified()); if (isVerified) { - expandSectionDevices.push(device); + // don't show dehydrated device as a normal device, if it's + // verified + if (device.deviceId === dehydratedDeviceId) { + dehydratedDeviceInExpandSection = true; + } else { + expandSectionDevices.push(device); + } } else { unverifiedDevices.push(device); } @@ -360,6 +364,9 @@ function DevicesSection({ ); }), ); + if (dehydratedDeviceInExpandSection) { + deviceList.push( (
{_t("user_info|dehydrated_device_enabled")}
) ); + } } return ( diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 0aa4413ee78..36f15fe449e 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -294,6 +294,7 @@ export default class SecurityUserSettingsTab extends React.Component + ); @@ -374,7 +375,6 @@ export default class SecurityUserSettingsTab extends React.Component {privacySection} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 26d0cad5c46..9e7d78bfdbe 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3695,6 +3695,7 @@ "deactivate_confirm_action": "Deactivate user", "deactivate_confirm_description": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", "deactivate_confirm_title": "Deactivate user?", + "dehydrated_device_enabled": "Offline device enabled", "demote_button": "Demote", "demote_self_confirm_description_space": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.", "demote_self_confirm_room": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", From 91a8d81661f5496633e35934469f675676538b48 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Sat, 16 Mar 2024 17:51:52 -0400 Subject: [PATCH 08/20] reorder strings --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e7d78bfdbe..5f2660fed24 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2689,8 +2689,8 @@ "cross_signing_self_signing_private_key": "Self signing private key:", "cross_signing_user_signing_private_key": "User signing private key:", "cryptography_section": "Cryptography", - "dehydrated_device_enabled": "Offline device enabled", "dehydrated_device_description": "The offline device feature allows you to receive encrypted messages even when you are not logged in to any devices", + "dehydrated_device_enabled": "Offline device enabled", "delete_backup": "Delete Backup", "delete_backup_confirm_description": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", From 861ab6319c9f3097a70c042a41bfae8175ba0e96 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Sat, 16 Mar 2024 18:14:13 -0400 Subject: [PATCH 09/20] lint --- src/components/views/right_panel/UserInfo.tsx | 2 +- .../settings/tabs/user/SecurityUserSettingsTab.tsx | 12 +++++++----- src/utils/device/dehydration.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index b8c3f5ec096..2e81fd62d20 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -365,7 +365,7 @@ function DevicesSection({ }), ); if (dehydratedDeviceInExpandSection) { - deviceList.push( (
{_t("user_info|dehydrated_device_enabled")}
) ); + deviceList.push(
{_t("user_info|dehydrated_device_enabled")}
); } } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 36f15fe449e..3a9c1dfde46 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -49,20 +49,22 @@ interface IIgnoredUserProps { inProgress: boolean; } -const DehydratedDeviceStatus: ReactFC = () => { +const DehydratedDeviceStatus: React.FC = () => { const { dehydratedDeviceId } = useOwnDevices(); if (dehydratedDeviceId) { return ( -
-
{_t("settings|security|dehydrated_device_enabled")}
-
{_t("settings|security|dehydrated_device_description")}
+
+
{_t("settings|security|dehydrated_device_enabled")}
+
+ {_t("settings|security|dehydrated_device_description")} +
); } else { return null; } -} +}; export class IgnoredUser extends React.Component { private onUnignoreClicked = (): void => { diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index ce562163e47..0d063da7bf5 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -15,14 +15,18 @@ limitations under the License. */ import { logger } from "matrix-js-sdk/src/logger"; + import { MatrixClientPeg } from "../../MatrixClientPeg"; // the interval between creating dehydrated devices -const DEHYDRATION_INTERVAL = 7*24*60*60*1000; +const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000; // check if device dehydration is enabled export async function deviceDehydrationEnabled(): Promise { const crypto = MatrixClientPeg.safeGet().getCrypto(); + if (!crypto) { + return false; + } if (!(await crypto.isDehydrationSupported())) { return false; } @@ -38,7 +42,7 @@ export async function deviceDehydrationEnabled(): Promise { // a new dehydrated device export async function initializeDehydration(reset?: boolean): Promise { const crypto = MatrixClientPeg.safeGet().getCrypto(); - if (crypto && await deviceDehydrationEnabled()) { + if (crypto && (await deviceDehydrationEnabled())) { logger.log("Device dehydration enabled"); if (reset) { await crypto.resetDehydrationKey(); From b0b066286e3fb7b33c552ac559d14f69d51fe098 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 26 Mar 2024 16:48:46 -0400 Subject: [PATCH 10/20] remove statement for testing --- src/utils/device/dehydration.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index 0d063da7bf5..f6976a573b5 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -33,7 +33,6 @@ export async function deviceDehydrationEnabled(): Promise { if (await crypto.isDehydrationKeyStored()) { return true; } - return true; const wellknown = await MatrixClientPeg.safeGet().waitForClientWellKnown(); return !!wellknown?.["org.matrix.msc3814"]; } From aeac5c72f787e1406b3b4499a79f51ec5f591b82 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 26 Mar 2024 16:49:43 -0400 Subject: [PATCH 11/20] add playwright test --- playwright/e2e/crypto/dehydration.spec.ts | 113 ++++++++++++++++++ .../synapse/templates/dehydration/README.md | 1 + .../templates/dehydration/homeserver.yaml | 102 ++++++++++++++++ .../synapse/templates/dehydration/log.config | 50 ++++++++ 4 files changed, 266 insertions(+) create mode 100644 playwright/e2e/crypto/dehydration.spec.ts create mode 100644 playwright/plugins/homeserver/synapse/templates/dehydration/README.md create mode 100644 playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml create mode 100644 playwright/plugins/homeserver/synapse/templates/dehydration/log.config diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts new file mode 100644 index 00000000000..213c335aff8 --- /dev/null +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -0,0 +1,113 @@ +/* +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 { test as base, expect } from "../../element-web-test"; +import { viewRoomSummaryByName } from "../right-panel/utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; + +const test = base.extend({ + startHomeserverOpts: async ({}, use) => { + await use("dehydration"); + }, + config: async ({ homeserver, context}, use) => { + const wellKnown = { + "m.homeserver": { + base_url: homeserver.config.baseUrl, + }, + "org.matrix.msc3814": true, + }; + + await context.route("https://localhost/.well-known/matrix/client", async (route) => { + await route.fulfill({ json: wellKnown }); + }); + + await use({ + default_server_config: wellKnown, + }); + }, +}); + +const ROOM_NAME = "Test room"; +const NAME = "Alice"; + +function getMemberTileByName(page: Page, name: string): Locator { + return page.locator(`.mx_EntityTile, [title="${name}"]`); +} + +test.describe("Dehydration", () => { + test.skip(isDendrite, "does not yet support dehydration v2"); + + test.use({ + displayName: NAME, + }); + + test("Create dehydrated device", async ({ page, user, app}, workerInfo) => { + test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto."); + + // Create a backup (which will create SSSS, and dehydrated device) + + const securityTab = await app.settings.openUserSettings("Security & Privacy"); + + await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); + await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible(); + await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); + + const currentDialogLocator = page.locator(".mx_Dialog"); + + // It's the first time and secure storage is not set up, so it will create one + await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible(); + await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible(); + await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click(); + // copy the recovery key to use it later + const securityKey = await app.getClipboard(); + await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); + + await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible(); + await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click(); + + // Open the settings again + await app.settings.openUserSettings("Security & Privacy"); + + // The Security tab should indicate that there is a dehydrated device present + await expect(securityTab.getByText("Offline device enabled")).toBeVisible(); + + await app.settings.closeDialog(); + + // the dehydrated device gets created with the name "Dehydrated + // device". We want to make sure that it is not visible as a normal + // device. + const sessionsTab = await app.settings.openUserSettings("Sessions"); + await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible(); + + await app.settings.closeDialog(); + + // now check that the user info right-panel shows the dehydrated device + // as a feature rather than as a normal device + await app.client.createRoom({ name: ROOM_NAME }); + + await viewRoomSummaryByName(page, app, ROOM_NAME); + + await page.getByRole("menuitem", { name: "People" }).click(); + await expect(page.locator(".mx_MemberList")).toBeVisible(); + + await getMemberTileByName(page, NAME).click(); + await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click(); + + await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible(); + await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible(); + }); +}) diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/README.md b/playwright/plugins/homeserver/synapse/templates/dehydration/README.md new file mode 100644 index 00000000000..18f7923e6d2 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/dehydration/README.md @@ -0,0 +1 @@ +A synapse configured with device dehydration v2 enabled diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml new file mode 100644 index 00000000000..8b5da186977 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml @@ -0,0 +1,102 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +oidc_providers: + - idp_id: test + idp_name: "OAuth test" + issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" + authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" + # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. + token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + client_id: "synapse" + discover: false + scopes: ["profile"] + skip_verification: true + client_auth_method: none + user_mapping_provider: + config: + display_name_template: "{{ user.name }}" + +# Inhibit background updates as this Synapse isn't long-lived +background_updates: + min_batch_size: 100000 + sleep_duration_ms: 100000 + +experimental_features: + msc2697_enabled: false + msc3814_enabled: true diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/log.config b/playwright/plugins/homeserver/synapse/templates/dehydration/log.config new file mode 100644 index 00000000000..b9123d0f5b9 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/dehydration/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false From 18da268d31e2f3b8dcb8d25bfb93622ad7cfe3b4 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 26 Mar 2024 17:28:40 -0400 Subject: [PATCH 12/20] lint and fix broken test --- playwright/e2e/crypto/dehydration.spec.ts | 11 ++++++----- .../synapse/templates/dehydration/homeserver.yaml | 4 ++-- .../security/CreateSecretStorageDialog-test.tsx | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 213c335aff8..13da99ccad4 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -14,15 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Locator, type Page } from "@playwright/test"; + import { test as base, expect } from "../../element-web-test"; import { viewRoomSummaryByName } from "../right-panel/utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; const test = base.extend({ + // eslint-disable-next-line no-empty-pattern startHomeserverOpts: async ({}, use) => { await use("dehydration"); }, - config: async ({ homeserver, context}, use) => { + config: async ({ homeserver, context }, use) => { const wellKnown = { "m.homeserver": { base_url: homeserver.config.baseUrl, @@ -54,7 +57,7 @@ test.describe("Dehydration", () => { displayName: NAME, }); - test("Create dehydrated device", async ({ page, user, app}, workerInfo) => { + test("Create dehydrated device", async ({ page, user, app }, workerInfo) => { test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto."); // Create a backup (which will create SSSS, and dehydrated device) @@ -72,8 +75,6 @@ test.describe("Dehydration", () => { await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible(); await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click(); - // copy the recovery key to use it later - const securityKey = await app.getClipboard(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible(); @@ -110,4 +111,4 @@ test.describe("Dehydration", () => { await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible(); await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible(); }); -}) +}); diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml index 8b5da186977..c3ac5d6536c 100644 --- a/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml @@ -98,5 +98,5 @@ background_updates: sleep_duration_ms: 100000 experimental_features: - msc2697_enabled: false - msc3814_enabled: true + msc2697_enabled: false + msc3814_enabled: true diff --git a/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx index fac786b3c27..1412074ed9d 100644 --- a/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx +++ b/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -50,6 +50,7 @@ describe("CreateSecretStorageDialog", () => { mockCrypto = mocked(mockClient.getCrypto()!); Object.assign(mockCrypto, { isKeyBackupTrusted: jest.fn(), + isDehydrationSupported: jest.fn(() => false), bootstrapCrossSigning: jest.fn(), bootstrapSecretStorage: jest.fn(), }); From 1269c7685af46c1cb06bcd61cd0f98741643e939 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 9 Apr 2024 20:43:08 -0400 Subject: [PATCH 13/20] update to new dehydration API --- src/utils/device/dehydration.ts | 39 +++++++++++++++------------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index f6976a573b5..c8c46205937 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -18,11 +18,15 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -// the interval between creating dehydrated devices -const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000; - -// check if device dehydration is enabled -export async function deviceDehydrationEnabled(): Promise { +/** + * Check if device dehydration is enabled. + * + * Dehydration can only be enabled if encryption is available, and the crypto + * backend supports dehydration. + * + * Dehydration can currently only enabled by setting a flag in the .well-known file. + */ +async function deviceDehydrationEnabled(): Promise { const crypto = MatrixClientPeg.safeGet().getCrypto(); if (!crypto) { return false; @@ -30,28 +34,21 @@ export async function deviceDehydrationEnabled(): Promise { if (!(await crypto.isDehydrationSupported())) { return false; } - if (await crypto.isDehydrationKeyStored()) { - return true; - } const wellknown = await MatrixClientPeg.safeGet().waitForClientWellKnown(); return !!wellknown?.["org.matrix.msc3814"]; } -// if dehydration is enabled, rehydrate a device (if available) and create -// a new dehydrated device -export async function initializeDehydration(reset?: boolean): Promise { +/** + * If dehydration is enabled, rehydrate a device (if available) and create + * a new dehydrated device. + * + * @param createNewKey: force a new dehydration key to be created, even if one + * already exists. This is used when we reset secret storage. + */ +export async function initializeDehydration(createNewKey: boolean = false): Promise { const crypto = MatrixClientPeg.safeGet().getCrypto(); if (crypto && (await deviceDehydrationEnabled())) { logger.log("Device dehydration enabled"); - if (reset) { - await crypto.resetDehydrationKey(); - } else { - try { - await crypto.rehydrateDeviceIfAvailable(); - } catch (e) { - logger.error("Error rehydrating device:", e); - } - } - await crypto.scheduleDeviceDehydration(DEHYDRATION_INTERVAL); + await crypto.startDehydration(createNewKey); } } From 8ff1e8c3cda2c0e6a84e160b42832f4908a549e5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 9 Apr 2024 21:29:03 -0400 Subject: [PATCH 14/20] some fixes from review --- .../views/dialogs/security/CreateSecretStorageDialog.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 5 +++++ src/components/views/settings/devices/useOwnDevices.ts | 5 +++++ src/stores/SetupEncryptionStore.ts | 9 +++++---- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 670bcfb35d1..89ae3c0651c 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -398,7 +398,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { dehydratedDeviceIds.push(device.deviceId); } } + // If the user has exactly one device marked as dehydrated, we consider + // that as the dehydrated device, and hide it as a normal device (but + // indicate that the user is using a dehydrated device). If the user has + // more than one, that is anomalous, and we show all the devices so that + // nothing is hidden. setDehydratedDeviceId(dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined); setIsLoadingDeviceList(false); diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index b653d1e7771..6ad86943118 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -112,11 +112,12 @@ export class SetupEncryptionStore extends EventEmitter { const userDevices: Iterable = (await crypto.getUserDeviceInfo([ownUserId])).get(ownUserId)?.values() ?? []; this.hasDevicesToVerifyAgainst = await asyncSome(userDevices, async (device) => { - // ignore the dehydrated device + // Ignore dehydrated devices. `dehydratedDevice` is set by the + // implementation of MSC2697, whereas MSC3814 proposes that devices + // should set a `dehydrated` flag in the device key. We ignore + // both types of dehydrated devices. if (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id) return false; - if (device.dehydrated) { - return false; - } + if (device.dehydrated) return false; // ignore devices without an identity key if (!device.getIdentityKey()) return false; From 93da280a7b84562e364fe382fa4f60787148272e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Apr 2024 12:44:01 -0400 Subject: [PATCH 15/20] try to fix test error --- .../settings/tabs/user/SecurityUserSettingsTab-test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx index c1848774ee1..e0e1394c5b8 100644 --- a/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -26,6 +26,7 @@ import { mockClientMethodsDevice, mockPlatformPeg, } from "../../../../../test-utils"; +import { SDKContext, SdkContextClass } from "../../../../../../src/contexts/SDKContext"; describe("", () => { const defaultProps = { @@ -44,9 +45,14 @@ describe("", () => { getKeyBackupVersion: jest.fn(), }); + const sdkContext = new SdkContextClass(); + sdkContext.client = mockClient; + const getComponent = () => ( - + + + ); From b456185a6d95e3c1ba783207aaf843c07026772c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Apr 2024 13:17:08 -0400 Subject: [PATCH 16/20] remove unneeded debug line --- src/components/views/settings/devices/useOwnDevices.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index de6f369a43d..f0cec2777a4 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -138,7 +138,6 @@ export const useOwnDevices = (): DevicesState => { const dehydratedDeviceIds: string[] = []; for (const device of userDevices?.values() ?? []) { if (device.dehydrated) { - logger.debug("Found dehydrated device", device.deviceId); dehydratedDeviceIds.push(device.deviceId); } } From ac6bb3ae338a58ea735b645fc33b1907cdcf87ff Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Apr 2024 23:31:14 -0400 Subject: [PATCH 17/20] apply changes from review --- .../dialogs/security/CreateSecretStorageDialog.tsx | 4 ++-- src/stores/SetupEncryptionStore.ts | 6 +++--- src/utils/device/dehydration.ts | 11 +++++++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 89ae3c0651c..0e9a27332ac 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -48,7 +48,7 @@ import InteractiveAuthDialog from "../../../../components/views/dialogs/Interact import { IValidationResult } from "../../../../components/views/elements/Validation"; import { Icon as CheckmarkIcon } from "../../../../../res/img/element-icons/check.svg"; import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField"; -import { initializeDehydration } from "../../../../utils/device/dehydration"; +import { initialiseDehydration } from "../../../../utils/device/dehydration"; // I made a mistake while converting this and it has to be fixed! enum Phase { @@ -398,7 +398,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } /** - * If dehydration is enabled, rehydrate a device (if available) and create + * If dehydration is enabled (i.e., it is supported by the server and enabled in + * the configuration), rehydrate a device (if available) and create * a new dehydrated device. * * @param createNewKey: force a new dehydration key to be created, even if one * already exists. This is used when we reset secret storage. */ -export async function initializeDehydration(createNewKey: boolean = false): Promise { +export async function initialiseDehydration(createNewKey: boolean = false): Promise { const crypto = MatrixClientPeg.safeGet().getCrypto(); if (crypto && (await deviceDehydrationEnabled())) { logger.log("Device dehydration enabled"); From 3d5164646413b88745cf7667a0d3b819ce4119d5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Apr 2024 23:39:07 -0400 Subject: [PATCH 18/20] add Jest tests --- src/components/views/right_panel/UserInfo.tsx | 1 + .../settings/tabs/user/SessionManagerTab.tsx | 2 +- src/stores/SetupEncryptionStore.ts | 14 +- src/utils/device/dehydration.ts | 8 +- .../views/right_panel/UserInfo-test.tsx | 177 ++++++++++++++++++ .../tabs/user/SessionManagerTab-test.tsx | 161 ++++++++++++++++ test/stores/SetupEncryptionStore-test.ts | 59 +++++- test/test-utils/test-utils.ts | 2 + 8 files changed, 413 insertions(+), 11 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 60293586f9a..2e7b9bc9ab6 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -333,6 +333,7 @@ function DevicesSection({ } else { if (dehydratedDeviceId) { devices = devices.filter((device) => device.deviceId !== dehydratedDeviceId); + dehydratedDeviceInExpandSection = true; } expandSectionDevices = devices; expandCountCaption = _t("user_info|count_of_sessions", { count: devices.length }); diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index bb1e8f56f78..61c8e85f8d1 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -209,7 +209,7 @@ const SessionManagerTab: React.FC = () => { }; const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; - if (dehydratedDeviceId) { + if (dehydratedDeviceId && otherDevices[dehydratedDeviceId]?.isVerified) { delete otherDevices[dehydratedDeviceId]; } const otherSessionsCount = Object.keys(otherDevices).length; diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 8706767cf67..e3069d43334 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -149,15 +149,19 @@ export class SetupEncryptionStore extends EventEmitter { await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => { accessSecretStorage(async (): Promise => { await cli.checkOwnCrossSigningTrust(); + + // The remaining tasks (device dehydration and restoring + // key backup) may take some time due to processing many + // to-device messages in the case of device dehydration, or + // having many keys to restore in the case of key backups, + // so we allow the dialog to advance before this. resolve(); + + await initialiseDehydration(); + if (backupInfo) { - // A complete restore can take many minutes for large - // accounts / slow servers, so we allow the dialog - // to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo); } - - await initialiseDehydration(); }).catch(reject); }); diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index 36d72752008..72a69ce5512 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { logger } from "matrix-js-sdk/src/logger"; +import { CryptoApi } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -28,8 +29,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg"; * * Dehydration can currently only enabled by setting a flag in the .well-known file. */ -async function deviceDehydrationEnabled(): Promise { - const crypto = MatrixClientPeg.safeGet().getCrypto(); +async function deviceDehydrationEnabled(crypto: CryptoApi | undefined): Promise { if (!crypto) { return false; } @@ -50,8 +50,8 @@ async function deviceDehydrationEnabled(): Promise { */ export async function initialiseDehydration(createNewKey: boolean = false): Promise { const crypto = MatrixClientPeg.safeGet().getCrypto(); - if (crypto && (await deviceDehydrationEnabled())) { + if (await deviceDehydrationEnabled(crypto)) { logger.log("Device dehydration enabled"); - await crypto.startDehydration(createNewKey); + await crypto!.startDehydration(createNewKey); } } diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx index b40146e609c..7c12efb9d7f 100644 --- a/test/components/views/right_panel/UserInfo-test.tsx +++ b/test/components/views/right_panel/UserInfo-test.tsx @@ -412,6 +412,183 @@ describe("", () => { await waitFor(() => expect(screen.getByRole("button", { name: "Verify" })).toBeInTheDocument()); expect(container).toMatchSnapshot(); }); + + describe("device dehydration", () => { + it("hides a verified dehydrated device (unverified user)", async () => { + const device1 = new Device({ + deviceId: "d1", + userId: defaultUserId, + displayName: "my device", + algorithms: [], + keys: new Map(), + }); + const device2 = new Device({ + deviceId: "d2", + userId: defaultUserId, + displayName: "dehydrated device", + algorithms: [], + keys: new Map(), + dehydrated: true, + }); + const devicesMap = new Map([ + [device1.deviceId, device1], + [device2.deviceId, device2], + ]); + const userDeviceMap = new Map>([[defaultUserId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + + renderComponent({ room: mockRoom }); + await act(flushPromises); + + // check the button exists with the expected text (the dehydrated device shouldn't be counted) + const devicesButton = screen.getByRole("button", { name: "1 session" }); + + // click it + await act(() => { + return userEvent.click(devicesButton); + }); + + // there should now be a button with the non-dehydrated device ID + expect(screen.getByRole("button", { description: "d1" })).toBeInTheDocument(); + + // but not for the dehydrated device ID + expect(screen.queryByRole("button", { description: "d2" })).not.toBeInTheDocument(); + + // there should be a line saying that the user has "Offline device" enabled + expect(screen.getByText("Offline device enabled")).toBeInTheDocument(); + }); + + it("hides a verified dehydrated device (verified user)", async () => { + const device1 = new Device({ + deviceId: "d1", + userId: defaultUserId, + displayName: "my device", + algorithms: [], + keys: new Map(), + }); + const device2 = new Device({ + deviceId: "d2", + userId: defaultUserId, + displayName: "dehydrated device", + algorithms: [], + keys: new Map(), + dehydrated: true, + }); + const devicesMap = new Map([ + [device1.deviceId, device1], + [device2.deviceId, device2], + ]); + const userDeviceMap = new Map>([[defaultUserId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true)); + mockCrypto.getDeviceVerificationStatus.mockResolvedValue({ + isVerified: () => true, + } as DeviceVerificationStatus); + + renderComponent({ room: mockRoom }); + await act(flushPromises); + + // check the button exists with the expected text (the dehydrated device shouldn't be counted) + const devicesButton = screen.getByRole("button", { name: "1 verified session" }); + + // click it + await act(() => { + return userEvent.click(devicesButton); + }); + + // there should now be a button with the non-dehydrated device ID + expect(screen.getByTitle("d1")).toBeInTheDocument(); + + // but not for the dehydrated device ID + expect(screen.queryByTitle("d2")).not.toBeInTheDocument(); + + // there should be a line saying that the user has "Offline device" enabled + expect(screen.getByText("Offline device enabled")).toBeInTheDocument(); + }); + + it("shows an unverified dehydrated device", async () => { + const device1 = new Device({ + deviceId: "d1", + userId: defaultUserId, + displayName: "my device", + algorithms: [], + keys: new Map(), + }); + const device2 = new Device({ + deviceId: "d2", + userId: defaultUserId, + displayName: "dehydrated device", + algorithms: [], + keys: new Map(), + dehydrated: true, + }); + const devicesMap = new Map([ + [device1.deviceId, device1], + [device2.deviceId, device2], + ]); + const userDeviceMap = new Map>([[defaultUserId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, true)); + + renderComponent({ room: mockRoom }); + await act(flushPromises); + + // the dehydrated device should be shown as an unverified device, which means + // there should now be a button with the device id ... + const deviceButton = screen.getByRole("button", { description: "d2" }); + + // ... which should contain the device name + expect(within(deviceButton).getByText("dehydrated device")).toBeInTheDocument(); + }); + + it("shows dehydrated devices if there is more than one", async () => { + const device1 = new Device({ + deviceId: "d1", + userId: defaultUserId, + displayName: "dehydrated device 1", + algorithms: [], + keys: new Map(), + dehydrated: true, + }); + const device2 = new Device({ + deviceId: "d2", + userId: defaultUserId, + displayName: "dehydrated device 2", + algorithms: [], + keys: new Map(), + dehydrated: true, + }); + const devicesMap = new Map([ + [device1.deviceId, device1], + [device2.deviceId, device2], + ]); + const userDeviceMap = new Map>([[defaultUserId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + + renderComponent({ room: mockRoom }); + await act(flushPromises); + + // check the button exists with the expected text (the dehydrated device shouldn't be counted) + const devicesButton = screen.getByRole("button", { name: "2 sessions" }); + + // click it + await act(() => { + return userEvent.click(devicesButton); + }); + + // the dehydrated devices should be shown as an unverified device, which means + // there should now be a button with the first dehydrated device id ... + const device1Button = screen.getByRole("button", { description: "d1" }); + + // ... which should contain the device name + expect(within(device1Button).getByText("dehydrated device 1")).toBeInTheDocument(); + // and a button with the second dehydrated device id ... + const device2Button = screen.getByRole("button", { description: "d2" }); + + // ... which should contain the device name + expect(within(device2Button).getByText("dehydrated device 2")).toBeInTheDocument(); + }); + }); }); describe("with an encrypted room", () => { diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 60b6bade4da..802eb5a0f6d 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -22,6 +22,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { defer, sleep } from "matrix-js-sdk/src/utils"; import { ClientEvent, + Device, IMyDevice, LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixEvent, @@ -61,6 +62,18 @@ mockPlatformPeg(); // to restore later const realWindowLocation = window.location; +function deviceToDeviceObj(userId: string, device: IMyDevice, opts: Partial = {}): Device { + const deviceOpts: Pick & Partial = { + deviceId: device.device_id, + userId, + algorithms: [], + displayName: device.display_name, + keys: new Map(), + }; + Object.assign(deviceOpts, opts); + return new Device(deviceOpts); +} + describe("", () => { const aliceId = "@alice:server.org"; const deviceId = "alices_device"; @@ -69,10 +82,12 @@ describe("", () => { device_id: deviceId, display_name: "Alices device", }; + const alicesDeviceObj = deviceToDeviceObj(aliceId, alicesDevice); const alicesMobileDevice = { device_id: "alices_mobile_device", last_seen_ts: Date.now(), }; + const alicesMobileDeviceObj = deviceToDeviceObj(aliceId, alicesMobileDevice); const alicesOlderMobileDevice = { device_id: "alices_older_mobile_device", @@ -84,6 +99,20 @@ describe("", () => { last_seen_ts: Date.now() - (INACTIVE_DEVICE_AGE_MS + 1000), }; + const alicesDehydratedDevice = { + device_id: "alices_dehydrated_device", + last_seen_ts: Date.now(), + }; + const alicesDehydratedDeviceObj = deviceToDeviceObj(aliceId, alicesDehydratedDevice, { dehydrated: true }); + + const alicesOtherDehydratedDevice = { + device_id: "alices_other_dehydrated_device", + last_seen_ts: Date.now(), + }; + const alicesOtherDehydratedDeviceObj = deviceToDeviceObj(aliceId, alicesOtherDehydratedDevice, { + dehydrated: true, + }); + const mockVerificationRequest = { cancel: jest.fn(), on: jest.fn(), @@ -91,6 +120,7 @@ describe("", () => { const mockCrypto = mocked({ getDeviceVerificationStatus: jest.fn(), + getUserDeviceInfo: jest.fn(), requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest), } as unknown as CryptoApi); @@ -627,6 +657,137 @@ describe("", () => { }); }); + describe("device dehydration", () => { + it("Hides a verified dehydrated device", async () => { + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesMobileDevice, alicesDehydratedDevice], + }); + mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); + + const devicesMap = new Map([ + [alicesDeviceObj.deviceId, alicesDeviceObj], + [alicesMobileDeviceObj.deviceId, alicesMobileDeviceObj], + [alicesDehydratedDeviceObj.deviceId, alicesDehydratedDeviceObj], + ]); + const userDeviceMap = new Map>([[aliceId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { + // alices device is trusted + if (deviceId === alicesDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); + } + // the dehydrated device is trusted + if (deviceId === alicesDehydratedDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); + } + // alices mobile device is not + if (deviceId === alicesMobileDevice.device_id) { + return new DeviceVerificationStatus({}); + } + return null; + }); + + const { queryByTestId } = render(getComponent()); + + await act(async () => { + await flushPromises(); + }); + + expect(queryByTestId(`device-tile-${alicesDevice.device_id}`)).toBeTruthy(); + expect(queryByTestId(`device-tile-${alicesMobileDevice.device_id}`)).toBeTruthy(); + // the dehydrated device should be hidden + expect(queryByTestId(`device-tile-${alicesDehydratedDevice.device_id}`)).toBeFalsy(); + }); + + it("Shows an unverified dehydrated device", async () => { + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesMobileDevice, alicesDehydratedDevice], + }); + mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); + + const devicesMap = new Map([ + [alicesDeviceObj.deviceId, alicesDeviceObj], + [alicesMobileDeviceObj.deviceId, alicesMobileDeviceObj], + [alicesDehydratedDeviceObj.deviceId, alicesDehydratedDeviceObj], + ]); + const userDeviceMap = new Map>([[aliceId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { + // alices device is trusted + if (deviceId === alicesDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); + } + // the dehydrated device is not + if (deviceId === alicesDehydratedDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: false, localVerified: false }); + } + // alices mobile device is not + if (deviceId === alicesMobileDevice.device_id) { + return new DeviceVerificationStatus({}); + } + return null; + }); + + const { queryByTestId } = render(getComponent()); + + await act(async () => { + await flushPromises(); + }); + + expect(queryByTestId(`device-tile-${alicesDevice.device_id}`)).toBeTruthy(); + expect(queryByTestId(`device-tile-${alicesMobileDevice.device_id}`)).toBeTruthy(); + // the dehydrated device should be shown since it is unverified + expect(queryByTestId(`device-tile-${alicesDehydratedDevice.device_id}`)).toBeTruthy(); + }); + + it("Shows the dehydrated devices if there are multiple", async () => { + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesMobileDevice, alicesDehydratedDevice, alicesOtherDehydratedDevice], + }); + mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); + + const devicesMap = new Map([ + [alicesDeviceObj.deviceId, alicesDeviceObj], + [alicesMobileDeviceObj.deviceId, alicesMobileDeviceObj], + [alicesDehydratedDeviceObj.deviceId, alicesDehydratedDeviceObj], + [alicesOtherDehydratedDeviceObj.deviceId, alicesOtherDehydratedDeviceObj], + ]); + const userDeviceMap = new Map>([[aliceId, devicesMap]]); + mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); + mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => { + // alices device is trusted + if (deviceId === alicesDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); + } + // one dehydrated device is trusted + if (deviceId === alicesDehydratedDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }); + } + // the other is not + if (deviceId === alicesOtherDehydratedDevice.device_id) { + return new DeviceVerificationStatus({ crossSigningVerified: false, localVerified: false }); + } + // alices mobile device is not + if (deviceId === alicesMobileDevice.device_id) { + return new DeviceVerificationStatus({}); + } + return null; + }); + + const { queryByTestId } = render(getComponent()); + + await act(async () => { + await flushPromises(); + }); + + expect(queryByTestId(`device-tile-${alicesDevice.device_id}`)).toBeTruthy(); + expect(queryByTestId(`device-tile-${alicesMobileDevice.device_id}`)).toBeTruthy(); + // both the dehydrated devices should be shown, since there are multiple + expect(queryByTestId(`device-tile-${alicesDehydratedDevice.device_id}`)).toBeTruthy(); + expect(queryByTestId(`device-tile-${alicesOtherDehydratedDevice.device_id}`)).toBeTruthy(); + }); + }); + describe("Sign out", () => { it("Signs out of current device", async () => { const modalSpy = jest.spyOn(Modal, "createDialog"); diff --git a/test/stores/SetupEncryptionStore-test.ts b/test/stores/SetupEncryptionStore-test.ts index 26d12f677d3..d220d7db9fa 100644 --- a/test/stores/SetupEncryptionStore-test.ts +++ b/test/stores/SetupEncryptionStore-test.ts @@ -40,9 +40,12 @@ describe("SetupEncryptionStore", () => { client = mocked(stubClient()); mockCrypto = { bootstrapCrossSigning: jest.fn(), + getCrossSigningKeyId: jest.fn(), getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), getUserDeviceInfo: jest.fn(), getDeviceVerificationStatus: jest.fn(), + isDehydrationSupported: jest.fn().mockResolvedValue(false), + startDehydration: jest.fn(), } as unknown as Mocked; client.getCrypto.mockReturnValue(mockCrypto); @@ -101,7 +104,7 @@ describe("SetupEncryptionStore", () => { expect(setupEncryptionStore.hasDevicesToVerifyAgainst).toBe(true); }); - it("should ignore the dehydrated device", async () => { + it("should ignore the MSC2697 dehydrated device", async () => { mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); client.getDehydratedDevice.mockResolvedValue({ device_id: "dehydrated" } as IDehydratedDevice); @@ -123,6 +126,27 @@ describe("SetupEncryptionStore", () => { expect(mockCrypto.getDeviceVerificationStatus).not.toHaveBeenCalled(); }); + it("should ignore the MSC3812 dehydrated device", async () => { + mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); + + const fakeDevice = new Device({ + deviceId: "dehydrated", + userId: "", + algorithms: [], + keys: new Map([["curve25519:dehydrated", "identityKey"]]), + dehydrated: true, + }); + mockCrypto.getUserDeviceInfo.mockResolvedValue( + new Map([[client.getSafeUserId(), new Map([[fakeDevice.deviceId, fakeDevice]])]]), + ); + + setupEncryptionStore.start(); + await emitPromise(setupEncryptionStore, "update"); + + expect(setupEncryptionStore.hasDevicesToVerifyAgainst).toBe(false); + expect(mockCrypto.getDeviceVerificationStatus).not.toHaveBeenCalled(); + }); + it("should correctly handle getUserDeviceInfo() returning an empty map", async () => { mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); mockCrypto.getUserDeviceInfo.mockResolvedValue(new Map()); @@ -133,6 +157,39 @@ describe("SetupEncryptionStore", () => { }); }); + describe("usePassPhrase", () => { + it("should use dehydration when enabled", async () => { + // mocks for cross-signing and secret storage + mockSecretStorage.isStored.mockResolvedValue({ sskeyid: {} as SecretStorageKeyDescriptionAesV1 }); + mockCrypto.getUserDeviceInfo.mockResolvedValue(new Map()); + mockCrypto.getDeviceVerificationStatus.mockResolvedValue( + new DeviceVerificationStatus({ signedByOwner: true }), + ); + mocked(accessSecretStorage).mockImplementation(async (func?: () => Promise) => { + await func!(); + }); + + // mocks for dehydration + mockCrypto.isDehydrationSupported.mockResolvedValue(true); + const dehydrationPromise = new Promise((resolve) => { + // Dehydration gets processed in the background, after + // `usePassPhrase` returns, so we need to use a promise to make + // sure that it is called. + mockCrypto.startDehydration.mockImplementation(async () => { + resolve(); + }); + }); + client.waitForClientWellKnown.mockResolvedValue({ "org.matrix.msc3814": true }); + + setupEncryptionStore.start(); + await emitPromise(setupEncryptionStore, "update"); + + await setupEncryptionStore.usePassPhrase(); + + await dehydrationPromise; + }); + }); + it("resetConfirm should work with a cached account password", async () => { const makeRequest = jest.fn(); mockCrypto.bootstrapCrossSigning.mockImplementation(async (opts: IBootstrapCrossSigningOpts) => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 7a88ea3e03d..b124af4ad11 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -115,6 +115,8 @@ export function createTestClient(): MatrixClient { credentials: { userId: "@userId:matrix.org" }, bootstrapCrossSigning: jest.fn(), hasSecretStorageKey: jest.fn(), + getKeyBackupVersion: jest.fn(), + checkOwnCrossSigningTrust: jest.fn(), secretStorage: { get: jest.fn(), From a974159bc057f62689bdfd5cb9295d5f02d03a3a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 15 Apr 2024 09:46:37 -0400 Subject: [PATCH 19/20] fix typo Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- src/utils/device/dehydration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index 72a69ce5512..83297f42a4e 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -27,7 +27,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg"; * supports it, and the application configuration suggests that it *should* be * initialised on this device. * - * Dehydration can currently only enabled by setting a flag in the .well-known file. + * Dehydration can currently only be enabled by setting a flag in the .well-known file. */ async function deviceDehydrationEnabled(crypto: CryptoApi | undefined): Promise { if (!crypto) { From e1a19d9d01338600bce2efc54060fd66aaca59ca Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 15 Apr 2024 11:26:20 -0400 Subject: [PATCH 20/20] don't need Object.assign Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- .../views/settings/tabs/user/SessionManagerTab-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 802eb5a0f6d..13992b8e155 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -69,8 +69,8 @@ function deviceToDeviceObj(userId: string, device: IMyDevice, opts: Partial