Skip to content

Commit

Permalink
Add cryptography information in devtools (#29073)
Browse files Browse the repository at this point in the history
* feat(devtools): add crypto information in devtools

* ci: add crypto devtools file to crypto code owners

* test(dev tools): update test to add new crypto button

* test(dev tools): add tests for crypto component
  • Loading branch information
florianduros authored Jan 24, 2025
1 parent 197afd6 commit a73eb37
Show file tree
Hide file tree
Showing 10 changed files with 708 additions and 2 deletions.
5 changes: 3 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers

# Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings
Expand Down
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
@import "./views/dialogs/_CreateRoomDialog.pcss";
@import "./views/dialogs/_CreateSubspaceDialog.pcss";
@import "./views/dialogs/_Crypto.pcss";
@import "./views/dialogs/_DeactivateAccountDialog.pcss";
@import "./views/dialogs/_DevtoolsDialog.pcss";
@import "./views/dialogs/_ExportDialog.pcss";
Expand Down
18 changes: 18 additions & 0 deletions res/css/views/dialogs/_Crypto.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

.mx_Crypto {
table {
margin: var(--cpd-space-4x) 0;
text-align: left;
border-spacing: var(--cpd-space-2x) 0;

thead {
font: var(--cpd-font-heading-sm-semibold);
}
}
}
2 changes: 2 additions & 0 deletions src/components/views/dialogs/DevtoolsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import ServerInfo from "./devtools/ServerInfo";
import CopyableText from "../elements/CopyableText";
import RoomNotifications from "./devtools/RoomNotifications";
import { Crypto } from "./devtools/Crypto";

enum Category {
Room,
Expand All @@ -49,6 +50,7 @@ const Tools: Record<Category, [label: TranslationKey, tool: Tool][]> = {
[_td("devtools|explore_account_data"), AccountDataExplorer],
[_td("devtools|settings_explorer"), SettingExplorer],
[_td("devtools|server_info"), ServerInfo],
[_td("devtools|crypto|title"), Crypto],
],
};

Expand Down
256 changes: 256 additions & 0 deletions src/components/views/dialogs/devtools/Crypto.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import React, { JSX } from "react";
import { InlineSpinner } from "@vector-im/compound-web";

import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import BaseTool from "./BaseTool";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
import { _t } from "../../../../languageHandler";

interface KeyBackupProps {
/**
* Callback to invoke when the back button is clicked.
*/
onBack(): void;
}

/**
* A component that displays information about the key storage and cross-signing.
*/
export function Crypto({ onBack }: KeyBackupProps): JSX.Element {
const matrixClient = useMatrixClientContext();
return (
<BaseTool onBack={onBack} className="mx_Crypto">
{matrixClient.getCrypto() ? (
<>
<KeyStorage />
<CrossSigning />
</>
) : (
<span>{_t("devtools|crypto|crypto_not_available")}</span>
)}
</BaseTool>
);
}

/**
* A component that displays information about the key storage.
*/
function KeyStorage(): JSX.Element {
const matrixClient = useMatrixClientContext();
const keyStorageData = useAsyncMemo(async () => {
const crypto = matrixClient.getCrypto()!;

// Get all the key storage data that we will display
const backupInfo = await crypto.getKeyBackupInfo();
const backupKeyStored = Boolean(await matrixClient.isKeyBackupKeyStored());
const backupKeyFromCache = await crypto.getSessionBackupPrivateKey();
const backupKeyCached = Boolean(backupKeyFromCache);
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
const secretStorageKeyInAccount = await matrixClient.secretStorage.hasKey();
const secretStorageReady = await crypto.isSecretStorageReady();

return {
backupInfo,
backupKeyStored,
backupKeyCached,
backupKeyWellFormed,
activeBackupVersion,
secretStorageKeyInAccount,
secretStorageReady,
};
}, [matrixClient]);

// Show a spinner while loading
if (keyStorageData === undefined) return <InlineSpinner aria-label={_t("common|loading")} />;

const {
backupInfo,
backupKeyStored,
backupKeyCached,
backupKeyWellFormed,
activeBackupVersion,
secretStorageKeyInAccount,
secretStorageReady,
} = keyStorageData;

return (
<table aria-label={_t("devtools|crypto|key_storage")}>
<thead>{_t("devtools|crypto|key_storage")}</thead>
<tbody>
<tr>
<th scope="row">{_t("devtools|crypto|key_backup_latest_version")}</th>
<td>
{backupInfo
? `${backupInfo.version} (${_t("settings|security|key_backup_algorithm")} ${backupInfo.algorithm})`
: _t("devtools|crypto|key_backup_inactive_warning")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|backup_key_stored_status")}</th>
<td>
{backupKeyStored
? _t("devtools|crypto|backup_key_stored")
: _t("devtools|crypto|backup_key_not_stored")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|key_backup_active_version")}</th>
<td>
{activeBackupVersion === null
? _t("devtools|crypto|key_backup_active_version_none")
: activeBackupVersion}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|backup_key_cached_status")}</th>
<td>
{`${
backupKeyCached
? _t("devtools|crypto|backup_key_cached")
: _t("devtools|crypto|not_found_locally")
}, ${
backupKeyWellFormed
? _t("devtools|crypto|backup_key_well_formed")
: _t("devtools|crypto|backup_key_unexpected_type")
}`}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|4s_public_key_status")}</th>
<td>
{secretStorageKeyInAccount
? _t("devtools|crypto|4s_public_key_in_account_data")
: _t("devtools|crypto|4s_public_key_not_in_account_data")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|secret_storage_status")}</th>
<td>
{secretStorageReady
? _t("devtools|crypto|secret_storage_ready")
: _t("devtools|crypto|secret_storage_not_ready")}
</td>
</tr>
</tbody>
</table>
);
}

/**
* A component that displays information about cross-signing.
*/
function CrossSigning(): JSX.Element {
const matrixClient = useMatrixClientContext();
const crossSigningData = useAsyncMemo(async () => {
const crypto = matrixClient.getCrypto()!;

// Get all the cross-signing data that we will display
const crossSigningStatus = await crypto.getCrossSigningStatus();
const crossSigningPublicKeysOnDevice = crossSigningStatus.publicKeysOnDevice;
const crossSigningPrivateKeysInStorage = crossSigningStatus.privateKeysInSecretStorage;
const masterPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.masterKey;
const selfSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.selfSigningKey;
const userSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.userSigningKey;
const crossSigningReady = await crypto.isCrossSigningReady();

return {
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
crossSigningReady,
};
}, [matrixClient]);

// Show a spinner while loading
if (crossSigningData === undefined) return <InlineSpinner aria-label={_t("common|loading")} />;

const {
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
crossSigningReady,
} = crossSigningData;

return (
<table aria-label={_t("devtools|crypto|cross_signing")}>
<thead>{_t("devtools|crypto|cross_signing")}</thead>
<tbody>
<tr>
<th scope="row">{_t("devtools|crypto|cross_signing_status")}</th>
<td>{getCrossSigningStatus(crossSigningReady, crossSigningPrivateKeysInStorage)}</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|cross_signing_public_keys_on_device_status")}</th>
<td>
{crossSigningPublicKeysOnDevice
? _t("devtools|crypto|cross_signing_public_keys_on_device")
: _t("devtools|crypto|not_found")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|cross_signing_private_keys_in_storage_status")}</th>
<td>
{crossSigningPrivateKeysInStorage
? _t("devtools|crypto|cross_signing_private_keys_in_storage")
: _t("devtools|crypto|cross_signing_private_keys_not_in_storage")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|master_private_key_cached_status")}</th>
<td>
{masterPrivateKeyCached
? _t("devtools|crypto|cross_signing_cached")
: _t("devtools|crypto|not_found_locally")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|self_signing_private_key_cached_status")}</th>
<td>
{selfSigningPrivateKeyCached
? _t("devtools|crypto|cross_signing_cached")
: _t("devtools|crypto|not_found_locally")}
</td>
</tr>
<tr>
<th scope="row">{_t("devtools|crypto|user_signing_private_key_cached_status")}</th>
<td>
{userSigningPrivateKeyCached
? _t("devtools|crypto|cross_signing_cached")
: _t("devtools|crypto|not_found_locally")}
</td>
</tr>
</tbody>
</table>
);
}

/**
* Get the cross-signing status.
* @param crossSigningReady Whether cross-signing is ready.
* @param crossSigningPrivateKeysInStorage Whether cross-signing private keys are in secret storage.
*/
function getCrossSigningStatus(crossSigningReady: boolean, crossSigningPrivateKeysInStorage: boolean): string {
if (crossSigningReady) {
return crossSigningPrivateKeysInStorage
? _t("devtools|crypto|cross_signing_ready")
: _t("devtools|crypto|cross_signing_untrusted");
}

if (crossSigningPrivateKeysInStorage) {
return _t("devtools|crypto|cross_signing_not_ready");
}

return _t("devtools|crypto|cross_signing_not_ready");
}
38 changes: 38 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,44 @@
"category_room": "Room",
"caution_colon": "Caution:",
"client_versions": "Client Versions",
"crypto": {
"4s_public_key_in_account_data": "in account data",
"4s_public_key_not_in_account_data": "not found",
"4s_public_key_status": "Secret storage public key:",
"backup_key_cached": "cached locally",
"backup_key_cached_status": "Backup key cached:",
"backup_key_not_stored": "not stored",
"backup_key_stored": "in secret storage",
"backup_key_stored_status": "Backup key stored:",
"backup_key_unexpected_type": "unexpected type",
"backup_key_well_formed": "well formed",
"cross_signing": "Cross-signing",
"cross_signing_cached": "cached locally",
"cross_signing_not_ready": "Cross-signing is not set up.",
"cross_signing_private_keys_in_storage": "in secret storage",
"cross_signing_private_keys_in_storage_status": "Cross-signing private keys:",
"cross_signing_private_keys_not_in_storage": "not found in storage",
"cross_signing_public_keys_on_device": "in memory",
"cross_signing_public_keys_on_device_status": "Cross-signing public keys:",
"cross_signing_ready": "Cross-signing is ready for use.",
"cross_signing_status": "Cross-signing status:",
"cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
"crypto_not_available": "Cryptographic module is not available",
"key_backup_active_version": "Active backup version:",
"key_backup_active_version_none": "None",
"key_backup_inactive_warning": "Your keys are not being backed up from this session.",
"key_backup_latest_version": "Latest backup version on server:",
"key_storage": "Key Storage",
"master_private_key_cached_status": "Master private key:",
"not_found": "not found",
"not_found_locally": "not found locally",
"secret_storage_not_ready": "not ready",
"secret_storage_ready": "ready",
"secret_storage_status": "Secret storage:",
"self_signing_private_key_cached_status": "Self signing private key:",
"title": "End-to-end encryption",
"user_signing_private_key_cached_status": "User signing private key:"
},
"developer_mode": "Developer mode",
"developer_tools": "Developer Tools",
"edit_setting": "Edit setting",
Expand Down
2 changes: 2 additions & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export function createTestClient(): MatrixClient {
}),
isCrossSigningReady: jest.fn().mockResolvedValue(false),
resetEncryption: jest.fn(),
getSessionBackupPrivateKey: jest.fn().mockResolvedValue(null),
isSecretStorageReady: jest.fn().mockResolvedValue(false),
}),

getPushActionsForEvent: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
>
Server info
</button>
<button
class="mx_DevTools_button"
>
End-to-end encryption
</button>
</div>
<div>
<h3>
Expand Down
Loading

0 comments on commit a73eb37

Please sign in to comment.