Skip to content

Commit d52c2f2

Browse files
committed
feat(devtools): Add crypto information in devtools
1 parent ad01218 commit d52c2f2

File tree

5 files changed

+331
-0
lines changed

5 files changed

+331
-0
lines changed

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
135135
@import "./views/dialogs/_CreateRoomDialog.pcss";
136136
@import "./views/dialogs/_CreateSubspaceDialog.pcss";
137+
@import "./views/dialogs/_Crypto.pcss";
137138
@import "./views/dialogs/_DeactivateAccountDialog.pcss";
138139
@import "./views/dialogs/_DevtoolsDialog.pcss";
139140
@import "./views/dialogs/_ExportDialog.pcss";

res/css/views/dialogs/_Crypto.pcss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.mx_Crypto {
9+
.mx_KeyStorage,
10+
.mx_CrossSigning {
11+
margin: var(--cpd-space-4x) 0;
12+
text-align: left;
13+
14+
thead {
15+
font: var(--cpd-font-heading-sm-semibold);
16+
}
17+
18+
th {
19+
padding-right: var(--cpd-space-2x);
20+
}
21+
}
22+
}

src/components/views/dialogs/DevtoolsDialog.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
2424
import ServerInfo from "./devtools/ServerInfo";
2525
import CopyableText from "../elements/CopyableText";
2626
import RoomNotifications from "./devtools/RoomNotifications";
27+
import { Crypto } from "./devtools/Crypto";
2728

2829
enum Category {
2930
Room,
@@ -49,6 +50,7 @@ const Tools: Record<Category, [label: TranslationKey, tool: Tool][]> = {
4950
[_td("devtools|explore_account_data"), AccountDataExplorer],
5051
[_td("devtools|settings_explorer"), SettingExplorer],
5152
[_td("devtools|server_info"), ServerInfo],
53+
[_td("devtools|crypto|title"), Crypto],
5254
],
5355
};
5456

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { JSX } from "react";
9+
import { InlineSpinner } from "@vector-im/compound-web";
10+
11+
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
12+
import BaseTool from "./BaseTool";
13+
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
14+
import { _t } from "../../../../languageHandler";
15+
16+
interface KeyBackupProps {
17+
onBack(): void;
18+
}
19+
20+
export function Crypto({ onBack }: KeyBackupProps): JSX.Element {
21+
return (
22+
<BaseTool onBack={onBack} className="mx_Crypto">
23+
<KeyStorage />
24+
<CrossSigning />
25+
</BaseTool>
26+
);
27+
}
28+
29+
/**
30+
* A component that displays information about the key storage.
31+
*/
32+
function KeyStorage(): JSX.Element {
33+
const matrixClient = useMatrixClientContext();
34+
const keyStorageData = useAsyncMemo(async () => {
35+
const crypto = matrixClient.getCrypto();
36+
if (!crypto) return null;
37+
38+
const backupInfo = await crypto.getKeyBackupInfo();
39+
const backupKeyStored = Boolean(await matrixClient.isKeyBackupKeyStored());
40+
const backupKeyFromCache = await crypto.getSessionBackupPrivateKey();
41+
const backupKeyCached = Boolean(backupKeyFromCache);
42+
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
43+
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
44+
const secretStorageKeyInAccount = await matrixClient.secretStorage.hasKey();
45+
const secretStorageReady = await crypto.isSecretStorageReady();
46+
47+
return {
48+
backupInfo,
49+
backupKeyStored,
50+
backupKeyCached,
51+
backupKeyWellFormed,
52+
activeBackupVersion,
53+
secretStorageKeyInAccount,
54+
secretStorageReady,
55+
};
56+
}, [matrixClient]);
57+
58+
if (keyStorageData === undefined) return <InlineSpinner aria-label={_t("common|loading")} />;
59+
if (keyStorageData === null) return <span>{_t("devtools|crypto|crypto_not_available")}</span>;
60+
61+
const {
62+
backupInfo,
63+
backupKeyStored,
64+
backupKeyCached,
65+
backupKeyWellFormed,
66+
activeBackupVersion,
67+
secretStorageKeyInAccount,
68+
secretStorageReady,
69+
} = keyStorageData;
70+
71+
return (
72+
<table className="mx_KeyStorage">
73+
<thead>{_t("devtools|crypto|key_storage")}</thead>
74+
<tbody>
75+
<tr>
76+
<th scope="row">{_t("devtools|crypto|key_backup_latest_version")}</th>
77+
<td>
78+
{backupInfo
79+
? `${backupInfo.version} (${_t("settings|security|key_backup_algorithm")} ${backupInfo.algorithm})`
80+
: _t("devtools|crypto|key_backup_inactive_warning")}
81+
</td>
82+
</tr>
83+
<tr>
84+
<th scope="row">{_t("devtools|crypto|backup_key_stored_status")}</th>
85+
<td>
86+
{backupKeyStored
87+
? _t("devtools|crypto|backup_key_stored")
88+
: _t("devtools|crypto|backup_key_not_stored")}
89+
</td>
90+
</tr>
91+
<tr>
92+
<th scope="row">{_t("devtools|crypto|key_backup_active_version")}</th>
93+
<td>
94+
{activeBackupVersion === null
95+
? _t("devtools|crypto|key_backup_active_version_none")
96+
: activeBackupVersion}
97+
</td>
98+
</tr>
99+
<tr>
100+
<th scope="row">{_t("devtools|crypto|backup_key_cached_status")}</th>
101+
<td>
102+
{`${
103+
backupKeyCached
104+
? _t("devtools|crypto|backup_key_cached")
105+
: _t("devtools|crypto|not_found_locally")
106+
},
107+
${
108+
backupKeyWellFormed
109+
? _t("devtools|crypto|backup_key_well_formed")
110+
: _t("devtools|crypto|backup_key_unexpected_type")
111+
}`}
112+
</td>
113+
</tr>
114+
<tr>
115+
<th scope="row">{_t("devtools|crypto|4s_public_key_status")}</th>
116+
<td>
117+
{secretStorageKeyInAccount
118+
? _t("devtools|crypto|4s_public_key_in_account_data")
119+
: _t("devtools|crypto|4s_public_key_not_in_account_data")}
120+
</td>
121+
</tr>
122+
<tr>
123+
<th scope="row">{_t("devtools|crypto|secret_storage_status")}</th>
124+
<td>
125+
{secretStorageReady
126+
? _t("devtools|crypto|secret_storage_ready")
127+
: _t("devtools|crypto|secret_storage_not_ready")}
128+
</td>
129+
</tr>
130+
</tbody>
131+
</table>
132+
);
133+
}
134+
135+
type CrossSigningData = {
136+
crossSigningPublicKeysOnDevice: boolean;
137+
crossSigningPrivateKeysInStorage: boolean;
138+
masterPrivateKeyCached: boolean;
139+
selfSigningPrivateKeyCached: boolean;
140+
userSigningPrivateKeyCached: boolean;
141+
homeserverSupportsCrossSigning: boolean;
142+
crossSigningReady: boolean;
143+
};
144+
145+
/**
146+
* A component that displays information about cross-signing.
147+
*/
148+
function CrossSigning(): JSX.Element {
149+
const matrixClient = useMatrixClientContext();
150+
const crossSigningData = useAsyncMemo<CrossSigningData | null>(async () => {
151+
const crypto = matrixClient.getCrypto();
152+
if (!crypto) return null;
153+
154+
const crossSigningStatus = await crypto.getCrossSigningStatus();
155+
const crossSigningPublicKeysOnDevice = crossSigningStatus.publicKeysOnDevice;
156+
const crossSigningPrivateKeysInStorage = crossSigningStatus.privateKeysInSecretStorage;
157+
const masterPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.masterKey;
158+
const selfSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.selfSigningKey;
159+
const userSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.userSigningKey;
160+
const homeserverSupportsCrossSigning =
161+
await matrixClient.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
162+
const crossSigningReady = await crypto.isCrossSigningReady();
163+
164+
return {
165+
crossSigningPublicKeysOnDevice,
166+
crossSigningPrivateKeysInStorage,
167+
masterPrivateKeyCached,
168+
selfSigningPrivateKeyCached,
169+
userSigningPrivateKeyCached,
170+
homeserverSupportsCrossSigning,
171+
crossSigningReady,
172+
};
173+
}, [matrixClient]);
174+
175+
if (crossSigningData === undefined) return <InlineSpinner aria-label={_t("common|loading")} />;
176+
if (crossSigningData === null) return <span>{_t("devtools|crypto|crypto_not_available")}</span>;
177+
178+
const {
179+
crossSigningPublicKeysOnDevice,
180+
crossSigningPrivateKeysInStorage,
181+
masterPrivateKeyCached,
182+
selfSigningPrivateKeyCached,
183+
userSigningPrivateKeyCached,
184+
homeserverSupportsCrossSigning,
185+
} = crossSigningData;
186+
187+
return (
188+
<table className="mx_CrossSigning">
189+
<thead>{_t("devtools|crypto|cross_signing")}</thead>
190+
<tbody>
191+
<tr>
192+
<th scope="row">{_t("devtools|crypto|cross_signing_status")}</th>
193+
<td>{getCrossSigningStatus(crossSigningData)}</td>
194+
</tr>
195+
<tr>
196+
<th scope="row">{_t("devtools|crypto|cross_signing_public_keys_on_device_status")}</th>
197+
<td>
198+
{crossSigningPublicKeysOnDevice
199+
? _t("devtools|crypto|cross_signing_public_keys_on_device")
200+
: _t("devtools|crypto|not_found")}
201+
</td>
202+
</tr>
203+
<tr>
204+
<th scope="row">{_t("devtools|crypto|cross_signing_private_keys_in_storage_status")}</th>
205+
<td>
206+
{crossSigningPrivateKeysInStorage
207+
? _t("devtools|crypto|cross_signing_private_keys_in_storage")
208+
: _t("devtools|crypto|cross_signing_private_keys_not_in_storage")}
209+
</td>
210+
</tr>
211+
<tr>
212+
<th scope="row">{_t("devtools|crypto|master_private_key_cached_status")}</th>
213+
<td>
214+
{masterPrivateKeyCached
215+
? _t("devtools|crypto|cross_signing_cached")
216+
: _t("devtools|crypto|not_found_locally")}
217+
</td>
218+
</tr>
219+
<tr>
220+
<th scope="row">{_t("devtools|crypto|self_signing_private_key_cached_status")}</th>
221+
<td>
222+
{selfSigningPrivateKeyCached
223+
? _t("devtools|crypto|cross_signing_cached")
224+
: _t("devtools|crypto|not_found_locally")}
225+
</td>
226+
</tr>
227+
<tr>
228+
<th scope="row">{_t("devtools|crypto|user_signing_private_key_cached_status")}</th>
229+
<td>
230+
{userSigningPrivateKeyCached
231+
? _t("devtools|crypto|cross_signing_cached")
232+
: _t("devtools|crypto|not_found_locally")}
233+
</td>
234+
</tr>
235+
<tr>
236+
<th scope="row">{_t("devtools|crypto|homeserver_supports_cross_signing_status")}</th>
237+
<td>
238+
{homeserverSupportsCrossSigning
239+
? _t("devtools|crypto|homeserver_supports_cross_signing")
240+
: _t("devtools|crypto|not_found")}
241+
</td>
242+
</tr>
243+
</tbody>
244+
</table>
245+
);
246+
}
247+
248+
function getCrossSigningStatus({
249+
homeserverSupportsCrossSigning,
250+
crossSigningReady,
251+
crossSigningPrivateKeysInStorage,
252+
}: CrossSigningData): string {
253+
if (!homeserverSupportsCrossSigning) {
254+
return _t("devtools|crypto|cross_signing_unsupported");
255+
} else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
256+
return _t("devtools|crypto|cross_signing_ready");
257+
} else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
258+
return _t("devtools|crypto|cross_signing_ready_no_backup");
259+
} else if (crossSigningPrivateKeysInStorage) {
260+
return _t("devtools|crypto|cross_signing_untrusted");
261+
} else {
262+
return _t("devtools|crypto|cross_signing_not_ready");
263+
}
264+
}

src/i18n/strings/en_EN.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,48 @@
734734
"category_room": "Room",
735735
"caution_colon": "Caution:",
736736
"client_versions": "Client Versions",
737+
"crypto": {
738+
"4s_public_key_in_account_data": "in account data",
739+
"4s_public_key_not_in_account_data": "not found",
740+
"4s_public_key_status": "Secret storage public key:",
741+
"backup_key_cached": "cached locally",
742+
"backup_key_cached_status": "Backup key cached:",
743+
"backup_key_not_stored": "not stored",
744+
"backup_key_stored": "in secret storage",
745+
"backup_key_stored_status": "Backup key stored:",
746+
"backup_key_unexpected_type": "unexpected type",
747+
"backup_key_well_formed": "well formed",
748+
"cross_signing": "Cross-signing",
749+
"cross_signing_cached": "cached locally",
750+
"cross_signing_not_ready": "Cross-signing is not set up.",
751+
"cross_signing_private_keys_in_storage": "in secret storage",
752+
"cross_signing_private_keys_in_storage_status": "Cross-signing private keys:",
753+
"cross_signing_private_keys_not_in_storage": "not found in storage",
754+
"cross_signing_public_keys_on_device": "in memory",
755+
"cross_signing_public_keys_on_device_status": "Cross-signing public keys:",
756+
"cross_signing_ready": "Cross-signing is ready for use.",
757+
"cross_signing_ready_no_backup": "Cross-signing is ready but keys are not backed up.",
758+
"cross_signing_status": "Cross-signing status:",
759+
"cross_signing_unsupported": "Your homeserver does not support cross-signing.",
760+
"cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
761+
"crypto_not_available": "Cryptographic module is not available",
762+
"homeserver_supports_cross_signing": "exists",
763+
"homeserver_supports_cross_signing_status": "Homeserver feature support:",
764+
"key_backup_active_version": "Active backup version:",
765+
"key_backup_active_version_none": "None",
766+
"key_backup_inactive_warning": "Your keys are not being backed up from this session.",
767+
"key_backup_latest_version": "Latest backup version on server:",
768+
"key_storage": "Key Storage",
769+
"master_private_key_cached_status": "Master private key:",
770+
"not_found": "not found",
771+
"not_found_locally": "not found locally",
772+
"secret_storage_not_ready": "not ready",
773+
"secret_storage_ready": "ready",
774+
"secret_storage_status": "Secret storage:",
775+
"self_signing_private_key_cached_status": "Self signing private key:",
776+
"title": "Crypto",
777+
"user_signing_private_key_cached_status": "User signing private key:"
778+
},
737779
"developer_mode": "Developer mode",
738780
"developer_tools": "Developer Tools",
739781
"edit_setting": "Edit setting",

0 commit comments

Comments
 (0)