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

Crypto: fix display of device key #86

Merged
merged 3 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions src/components/views/settings/CryptographyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import React from "react";
import { logger } from "matrix-js-sdk/src/logger";

import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
import type ImportE2eKeysDialog from "../../../async-components/views/dialogs/security/ImportE2eKeysDialog";
Expand All @@ -22,25 +23,53 @@

interface IProps {}

interface IState {}
interface IState {
/** The device's base64-encoded Ed25519 identity key, or:
*
* * `undefined`: not yet loaded
* * `null`: encryption is not supported (or the crypto stack was not correctly initialized)
*/
deviceIdentityKey: string | undefined | null;
}

export default class CryptographyPanel extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);

const client = MatrixClientPeg.safeGet();
const crypto = client.getCrypto();
if (!crypto) {
this.state = { deviceIdentityKey: null };
} else {
this.state = { deviceIdentityKey: undefined };
crypto
.getOwnDeviceKeys()

Check failure on line 46 in src/components/views/settings/CryptographyPanel.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<UserSettingsDialog /> › renders with security tab selected

TypeError: Cannot read properties of undefined (reading 'then') at new CryptographyPanel (src/components/views/settings/CryptographyPanel.tsx:46:36) at constructClassInstance (node_modules/react-dom/cjs/react-dom.development.js:12716:18) at updateClassComponent (node_modules/react-dom/cjs/react-dom.development.js:17425:5) at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19073:16) at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14) at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30) at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25) at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3) at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9) at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17) at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34) at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16) at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31) at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23964:7) at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22779:12) at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22707:5) at renderRootSync (node_modules/react-dom/cjs/react-dom.development.js:22670:7) at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:22293:18) at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:21881:7) at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:25482:3) at node_modules/react-dom/cjs/react-dom.development.js:26021:7 at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:22431:12) at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:26020:5) at Object.render (node_modules/react-dom/cjs/react-dom.development.js:26103:10) at node_modules/@testing-library/react/dist/pure.js:101:25 at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:22380:12) at act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1042:14) at render (node_modules/@testing-library/react/dist/pure.js:97:26) at Object.<anonymous> (test/components/views/dialogs/UserSettingsDialog-test.tsx:176:37)

Check failure on line 46 in src/components/views/settings/CryptographyPanel.tsx

View workflow job for this annotation

GitHub Actions / Jest (2)

<SecurityUserSettingsTab /> › renders security section

TypeError: Cannot read properties of undefined (reading 'then') at new CryptographyPanel (src/components/views/settings/CryptographyPanel.tsx:46:36) at constructClassInstance (node_modules/react-dom/cjs/react-dom.development.js:12716:18) at updateClassComponent (node_modules/react-dom/cjs/react-dom.development.js:17425:5) at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19073:16) at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14) at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30) at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25) at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3) at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9) at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17) at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34) at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16) at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31) at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23964:7) at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22779:12) at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22707:5) at renderRootSync (node_modules/react-dom/cjs/react-dom.development.js:22670:7) at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:22293:18) at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:21881:7) at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:25482:3) at node_modules/react-dom/cjs/react-dom.development.js:26021:7 at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:22431:12) at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:26020:5) at Object.render (node_modules/react-dom/cjs/react-dom.development.js:26103:10) at node_modules/@testing-library/react/dist/pure.js:101:25 at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:22380:12) at act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1042:14) at render (node_modules/@testing-library/react/dist/pure.js:97:26) at Object.<anonymous> (test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx:57:37)
.then((keys) => {
this.setState({ deviceIdentityKey: keys.ed25519 });
})
.catch((e) => {
logger.error(`CryptographyPanel: Error fetching own device keys: ${e}`);
this.setState({ deviceIdentityKey: null });
});
}
}

public render(): React.ReactNode {
const client = MatrixClientPeg.safeGet();
const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
let identityKey = this.state.deviceIdentityKey;
if (identityKey === undefined) {
// Should show a spinner here really, but since this will be very transitional, I can't be doing with the
// necessary styling.
identityKey = "...";
} else if (identityKey === null) {
identityKey = _t("encryption|not_supported");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}

let importExportButtons: JSX.Element | undefined;
if (client.isCryptoEnabled()) {
if (client.getCrypto()) {
importExportButtons = (
<div className="mx_CryptographyPanel_importExportButtons">
<AccessibleButton kind="primary_outline" onClick={this.onExportE2eKeysClicked}>
Expand Down Expand Up @@ -68,20 +97,22 @@
<SettingsSubsection heading={_t("settings|security|cryptography_section")}>
<SettingsSubsectionText>
<table className="mx_CryptographyPanel_sessionInfo">
<tr>
<th scope="row">{_t("settings|security|session_id")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|session_key")}</th>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
<tbody>
<tr>
<th scope="row">{_t("settings|security|session_id")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|session_key")}</th>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
</tbody>
</table>
</SettingsSubsectionText>
{importExportButtons}
Expand Down
36 changes: 34 additions & 2 deletions test/components/views/settings/CryptographyPanel-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@
import React from "react";
import { render } from "@testing-library/react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";

import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import * as TestUtils from "../../../test-utils";
import CryptographyPanel from "../../../../src/components/views/settings/CryptographyPanel";
import { flushPromises } from "../../../test-utils";

describe("CryptographyPanel", () => {
it("shows the session ID and key", () => {
it("shows the session ID and key", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
const sessionKeyFormatted = "<b>AbCD eFgh IJK7 L/m4 nOPq RSTU VW4x yzaB CDef 6gHI Jkl</b>";

TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
client.getDeviceEd25519Key = () => sessionKey;

mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });

// When we render the CryptographyPanel
const rendered = render(<CryptographyPanel />);
Expand All @@ -32,6 +35,35 @@
const codes = rendered.container.querySelectorAll("code");
expect(codes.length).toEqual(2);
expect(codes[0].innerHTML).toEqual(sessionId);

// Initially a placeholder
expect(codes[1].innerHTML).toEqual("<b>...</b>");

// Then the actual key
await flushPromises();
expect(codes[1].innerHTML).toEqual(sessionKeyFormatted);
});

it("handles errors fetching session key", async () => {
const sessionId = "ABCDEFGHIJ";

TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;

mocked(client.getCrypto()!.getOwnDeviceKeys).mockRejectedValue(new Error("bleh"));

// When we render the CryptographyPanel
const rendered = render(<CryptographyPanel />);

// Then it displays info about the user's session
const codes = rendered.container.querySelectorAll("code");

// Initially a placeholder
expect(codes[1].innerHTML).toEqual("<b>...</b>");

// Then "not supported key
await flushPromises();
expect(codes[1].innerHTML).toEqual("<b>&lt;Not supported&gt;</b>");

Check failure on line 67 in test/components/views/settings/CryptographyPanel-test.tsx

View workflow job for this annotation

GitHub Actions / Jest (1)

CryptographyPanel › handles errors fetching session key

expect(received).toEqual(expected) // deep equality Expected: "<b>&lt;Not supported&gt;</b>" Received: "<b>&lt;not supported&gt;</b>" at Object.toEqual (test/components/views/settings/CryptographyPanel-test.tsx:67:36)
});
});
2 changes: 0 additions & 2 deletions test/test-utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export const mockClientMethodsDevice = (
deviceId = "test-device-id",
): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getDeviceId: jest.fn().mockReturnValue(deviceId),
getDeviceEd25519Key: jest.fn(),
getDevices: jest.fn().mockResolvedValue({ devices: [] }),
});

Expand Down Expand Up @@ -167,7 +166,6 @@ export const mockClientMethodsCrypto = (): Partial<
getOwnDeviceKeys: jest.fn(),
getCrossSigningKeyId: jest.fn(),
}),
getDeviceEd25519Key: jest.fn(),
});

export const mockClientMethodsRooms = (rooms: Room[] = []): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
Expand Down
1 change: 1 addition & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export function createTestClient(): MatrixClient {
},
},
getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn(),
getUserDeviceInfo: jest.fn(),
getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
Expand Down
Loading