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

Commit 5ed68ef

Browse files
authored
New context for local device verification (#12417)
* New context for local device verification * Fix up tests * Use PropsWithChildren
1 parent 3137339 commit 5ed68ef

File tree

6 files changed

+256
-3
lines changed

6 files changed

+256
-3
lines changed

src/components/structures/LoggedInView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import SettingsStore from "../../settings/SettingsStore";
3838
import { SettingLevel } from "../../settings/SettingLevel";
3939
import ResizeHandle from "../views/elements/ResizeHandle";
4040
import { CollapseDistributor, Resizer } from "../../resizer";
41-
import MatrixClientContext from "../../contexts/MatrixClientContext";
4241
import ResizeNotifier from "../../utils/ResizeNotifier";
4342
import PlatformPeg from "../../PlatformPeg";
4443
import { DefaultTagID } from "../../stores/room-list/models";
@@ -75,6 +74,7 @@ import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage"
7574
import { PipContainer } from "./PipContainer";
7675
import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushRules";
7776
import { ConfigOptions } from "../../SdkConfig";
77+
import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
7878

7979
// We need to fetch each pinned message individually (if we don't already have it)
8080
// so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -672,7 +672,7 @@ class LoggedInView extends React.Component<IProps, IState> {
672672
});
673673

674674
return (
675-
<MatrixClientContext.Provider value={this._matrixClient}>
675+
<MatrixClientContextProvider client={this._matrixClient}>
676676
<div
677677
onPaste={this.onPaste}
678678
onKeyDown={this.onReactKeyDown}
@@ -707,7 +707,7 @@ class LoggedInView extends React.Component<IProps, IState> {
707707
<PipContainer />
708708
<NonUrgentToastContainer />
709709
{audioFeedArraysForCalls}
710-
</MatrixClientContext.Provider>
710+
</MatrixClientContextProvider>
711711
);
712712
}
713713
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { PropsWithChildren, useEffect, useState } from "react";
18+
import { CryptoEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
19+
import { logger } from "matrix-js-sdk/src/logger";
20+
21+
import MatrixClientContext from "../../contexts/MatrixClientContext";
22+
import { useEventEmitter } from "../../hooks/useEventEmitter";
23+
import { LocalDeviceVerificationStateContext } from "../../contexts/LocalDeviceVerificationStateContext";
24+
25+
/**
26+
* A React hook whose value is whether the local device has been "verified".
27+
*
28+
* Figuring out if we are verified is an async operation, so on the first render this always returns `false`, but
29+
* fires off a background job to update a state variable. It also registers an event listener to update the state
30+
* variable changes.
31+
*
32+
* @param client - Matrix client.
33+
* @returns A boolean which is `true` if the local device has been verified.
34+
*
35+
* @remarks
36+
*
37+
* Some notes on implementation.
38+
*
39+
* It turns out "is this device verified?" isn't a question that is easy to answer as you might think.
40+
*
41+
* Roughly speaking, it normally means "do we believe this device actually belongs to the person it claims to belong
42+
* to", and that data is available via `getDeviceVerificationStatus().isVerified()`. However, the problem is that for
43+
* the local device, that "do we believe..." question is trivially true, and `isVerified()` always returns true.
44+
*
45+
* Instead, when we're talking about the local device, what we really mean is one of:
46+
* * "have we completed a verification dance (either interactive verification with a device with access to the
47+
* cross-signing secrets, or typing in the 4S key)?", or
48+
* * "will other devices consider this one to be verified?"
49+
*
50+
* (The first is generally required but not sufficient for the second to be true.)
51+
*
52+
* The second question basically amounts to "has this device been signed by our cross-signing key". So one option here
53+
* is to use `getDeviceVerificationStatus().isCrossSigningVerified()`. That might work, but it's a bit annoying because
54+
* it needs a `/keys/query` request to complete after the actual verification process completes.
55+
*
56+
* A slightly less rigorous check is just to find out if we have validated our own public cross-signing keys. If we
57+
* have, it's a good indication that we've at least completed a verification dance -- and hopefully, during that dance,
58+
* a cross-signature of our own device was published. And it's also easy to monitor via `UserTrustStatusChanged` events.
59+
*
60+
* Sooo: TL;DR: `getUserVerificationStatus()` is a good proxy for "is the local device verified?".
61+
*/
62+
function useLocalVerificationState(client: MatrixClient): boolean {
63+
const [value, setValue] = useState(false);
64+
65+
// On the first render, initialise the state variable
66+
useEffect(() => {
67+
const userId = client.getUserId();
68+
if (!userId) return;
69+
const crypto = client.getCrypto();
70+
crypto?.getUserVerificationStatus(userId).then(
71+
(verificationStatus) => setValue(verificationStatus.isCrossSigningVerified()),
72+
(error) => logger.error("Error fetching verification status", error),
73+
);
74+
}, [client]);
75+
76+
// Update the value whenever our own trust status changes.
77+
useEventEmitter(client, CryptoEvent.UserTrustStatusChanged, (userId, verificationStatus) => {
78+
if (userId === client.getUserId()) {
79+
setValue(verificationStatus.isCrossSigningVerified());
80+
}
81+
});
82+
83+
return value;
84+
}
85+
86+
interface Props {
87+
/** Matrix client, which is exposed to all child components via {@link MatrixClientContext}. */
88+
client: MatrixClient;
89+
}
90+
91+
/**
92+
* A React component which exposes a {@link MatrixClientContext} and a {@link LocalDeviceVerificationStateContext}
93+
* to its children.
94+
*/
95+
export function MatrixClientContextProvider(props: PropsWithChildren<Props>): React.JSX.Element {
96+
const verificationState = useLocalVerificationState(props.client);
97+
return (
98+
<MatrixClientContext.Provider value={props.client}>
99+
<LocalDeviceVerificationStateContext.Provider value={verificationState}>
100+
{props.children}
101+
</LocalDeviceVerificationStateContext.Provider>
102+
</MatrixClientContext.Provider>
103+
);
104+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { createContext } from "react";
18+
19+
/**
20+
* React context whose value is whether the local device has been verified.
21+
*
22+
* (Specifically, this is true if we have done enough verification to confirm that the published public cross-signing
23+
* keys are genuine -- which normally means that we or another device will have published a signature of this device.)
24+
*
25+
* This context is available to all components under {@link LoggedInView}, via {@link MatrixClientContextProvider}.
26+
*/
27+
export const LocalDeviceVerificationStateContext = createContext(false);

test/components/structures/LoggedInView-test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe("<LoggedInView />", () => {
3838
getMediaHandler: jest.fn(),
3939
setPushRuleEnabled: jest.fn(),
4040
setPushRuleActions: jest.fn(),
41+
getCrypto: jest.fn().mockReturnValue(undefined),
4142
});
4243
const mediaHandler = new MediaHandler(mockClient);
4344
const mockSdkContext = new TestSdkContext();

test/components/structures/MatrixChat-test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { logger } from "matrix-js-sdk/src/logger";
2626
import { OidcError } from "matrix-js-sdk/src/oidc/error";
2727
import { BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
2828
import { defer, sleep } from "matrix-js-sdk/src/utils";
29+
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
2930

3031
import MatrixChat from "../../../src/components/structures/MatrixChat";
3132
import * as StorageManager from "../../../src/utils/StorageManager";
@@ -948,6 +949,9 @@ describe("<MatrixChat />", () => {
948949
const mockCrypto = {
949950
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
950951
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
952+
getUserVerificationStatus: jest
953+
.fn()
954+
.mockResolvedValue(new UserVerificationStatus(false, false, false)),
951955
};
952956
loginClient.isCryptoEnabled.mockReturnValue(true);
953957
loginClient.getCrypto.mockReturnValue(mockCrypto as any);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { act, render } from "@testing-library/react";
18+
import React, { useContext } from "react";
19+
import { CryptoEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
20+
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
21+
22+
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
23+
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
24+
import { LocalDeviceVerificationStateContext } from "../../../src/contexts/LocalDeviceVerificationStateContext";
25+
import {
26+
flushPromises,
27+
getMockClientWithEventEmitter,
28+
mockClientMethodsCrypto,
29+
mockClientMethodsUser,
30+
} from "../../test-utils";
31+
32+
describe("MatrixClientContextProvider", () => {
33+
it("Should expose a matrix client context", () => {
34+
const mockClient = getMockClientWithEventEmitter({
35+
...mockClientMethodsUser(),
36+
getCrypto: () => null,
37+
});
38+
39+
let receivedClient: MatrixClient | undefined;
40+
function ContextReceiver() {
41+
receivedClient = useContext(MatrixClientContext);
42+
return <></>;
43+
}
44+
45+
render(
46+
<MatrixClientContextProvider client={mockClient}>
47+
<ContextReceiver />
48+
</MatrixClientContextProvider>,
49+
);
50+
51+
expect(receivedClient).toBe(mockClient);
52+
});
53+
54+
describe("Should expose a verification status context", () => {
55+
/** The most recent verification status received by our `ContextReceiver` */
56+
let receivedState: boolean | undefined;
57+
58+
/** The mock client for use in the tests */
59+
let mockClient: MatrixClient;
60+
61+
function ContextReceiver() {
62+
receivedState = useContext(LocalDeviceVerificationStateContext);
63+
return <></>;
64+
}
65+
66+
function getComponent(mockClient: MatrixClient) {
67+
return render(
68+
<MatrixClientContextProvider client={mockClient}>
69+
<ContextReceiver />
70+
</MatrixClientContextProvider>,
71+
);
72+
}
73+
74+
beforeEach(() => {
75+
receivedState = undefined;
76+
mockClient = getMockClientWithEventEmitter({
77+
...mockClientMethodsUser(),
78+
...mockClientMethodsCrypto(),
79+
});
80+
});
81+
82+
it("returns false if device is unverified", async () => {
83+
mockClient.getCrypto()!.getUserVerificationStatus = jest
84+
.fn()
85+
.mockResolvedValue(new UserVerificationStatus(false, false, false));
86+
getComponent(mockClient);
87+
expect(receivedState).toBe(false);
88+
});
89+
90+
it("returns true if device is verified", async () => {
91+
mockClient.getCrypto()!.getUserVerificationStatus = jest
92+
.fn()
93+
.mockResolvedValue(new UserVerificationStatus(true, false, false));
94+
getComponent(mockClient);
95+
await act(() => flushPromises());
96+
expect(receivedState).toBe(true);
97+
});
98+
99+
it("updates when the trust status updates", async () => {
100+
const getVerificationStatus = jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false));
101+
mockClient.getCrypto()!.getUserVerificationStatus = getVerificationStatus;
102+
getComponent(mockClient);
103+
104+
// starts out false
105+
await act(() => flushPromises());
106+
expect(receivedState).toBe(false);
107+
108+
// Now the state is updated
109+
const verifiedStatus = new UserVerificationStatus(true, false, false);
110+
getVerificationStatus.mockResolvedValue(verifiedStatus);
111+
act(() => {
112+
mockClient.emit(CryptoEvent.UserTrustStatusChanged, mockClient.getSafeUserId(), verifiedStatus);
113+
});
114+
expect(receivedState).toBe(true);
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)