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

Commit b5e7d12

Browse files
author
Kerry
authored
Add config option to autorageshake when key backup is not enabled (#7741)
* report on not enabled Signed-off-by: Kerry Archibald <[email protected]> * add setting Signed-off-by: Kerry Archibald <[email protected]> * check key backup status after crypto init Signed-off-by: Kerry Archibald <[email protected]> * remove log Signed-off-by: Kerry Archibald <[email protected]> * test encryption setup in DeviceListener Signed-off-by: Kerry Archibald <[email protected]> * i18n Signed-off-by: Kerry Archibald <[email protected]> * sendLogs for key backup auto-report event Signed-off-by: Kerry Archibald <[email protected]> * remove reloadOnChagneController Signed-off-by: Kerry Archibald <[email protected]>
1 parent d06ec84 commit b5e7d12

File tree

6 files changed

+290
-1
lines changed

6 files changed

+290
-1
lines changed

Diff for: src/DeviceListener.ts

+19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
3636
import { isSecureBackupRequired } from './utils/WellKnownUtils';
3737
import { isLoggedIn } from './components/structures/MatrixChat';
3838
import { ActionPayload } from "./dispatcher/payloads";
39+
import { Action } from "./dispatcher/actions";
3940

4041
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
4142

@@ -48,6 +49,7 @@ export default class DeviceListener {
4849
// cache of the key backup info
4950
private keyBackupInfo: object = null;
5051
private keyBackupFetchedAt: number = null;
52+
private keyBackupStatusChecked = false;
5153
// We keep a list of our own device IDs so we can batch ones that were already
5254
// there the last time the app launched into a single toast, but display new
5355
// ones in their own toasts.
@@ -92,6 +94,7 @@ export default class DeviceListener {
9294
this.dismissedThisDeviceToast = false;
9395
this.keyBackupInfo = null;
9496
this.keyBackupFetchedAt = null;
97+
this.keyBackupStatusChecked = false;
9598
this.ourDeviceIdsAtStart = null;
9699
this.displayingToastsForDeviceIds = new Set();
97100
}
@@ -227,6 +230,8 @@ export default class DeviceListener {
227230

228231
if (this.dismissedThisDeviceToast || allSystemsReady) {
229232
hideSetupEncryptionToast();
233+
234+
this.checkKeyBackupStatus();
230235
} else if (this.shouldShowSetupEncryptionToast()) {
231236
// make sure our keys are finished downloading
232237
await cli.downloadKeys([cli.getUserId()]);
@@ -238,6 +243,7 @@ export default class DeviceListener {
238243
) {
239244
// Cross-signing on account but this device doesn't trust the master key (verify this session)
240245
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
246+
this.checkKeyBackupStatus();
241247
} else {
242248
const backupInfo = await this.getKeyBackupInfo();
243249
if (backupInfo) {
@@ -312,4 +318,17 @@ export default class DeviceListener {
312318

313319
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
314320
}
321+
322+
private checkKeyBackupStatus = async () => {
323+
if (this.keyBackupStatusChecked) {
324+
return;
325+
}
326+
// returns null when key backup status hasn't finished being checked
327+
const isKeyBackupEnabled = MatrixClientPeg.get().getKeyBackupEnabled();
328+
this.keyBackupStatusChecked = isKeyBackupEnabled !== null;
329+
330+
if (isKeyBackupEnabled === false) {
331+
dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
332+
}
333+
};
315334
}

Diff for: src/dispatcher/actions.ts

+6
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,10 @@ export enum Action {
218218
* Payload: none
219219
*/
220220
AnonymousAnalyticsReject = "anonymous_analytics_reject",
221+
222+
/**
223+
* Fires after crypto is setup if key backup is not enabled
224+
* Used to trigger auto rageshakes when configured
225+
*/
226+
ReportKeyBackupNotEnabled = "report_key_backup_not_enabled",
221227
}

Diff for: src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,7 @@
960960
"Developer mode": "Developer mode",
961961
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
962962
"Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
963+
"Automatically send debug logs when key backup is not functioning": "Automatically send debug logs when key backup is not functioning",
963964
"Collecting app version information": "Collecting app version information",
964965
"Collecting logs": "Collecting logs",
965966
"Uploading logs": "Uploading logs",

Diff for: src/settings/Settings.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
891891
default: false,
892892
controller: new ReloadOnChangeController(),
893893
},
894+
"automaticKeyBackNotEnabledReporting": {
895+
displayName: _td("Automatically send debug logs when key backup is not functioning"),
896+
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
897+
default: false,
898+
},
894899
[UIFeature.RoomHistorySettings]: {
895900
supportedLevels: LEVELS_UI_FEATURE,
896901
default: true,

Diff for: src/stores/AutoRageshakeStore.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import defaultDispatcher from '../dispatcher/dispatcher';
2424
import { AsyncStoreWithClient } from './AsyncStoreWithClient';
2525
import { ActionPayload } from '../dispatcher/payloads';
2626
import SettingsStore from "../settings/SettingsStore";
27+
import { Action } from "../dispatcher/actions";
2728

2829
// Minimum interval of 1 minute between reports
2930
const RAGESHAKE_INTERVAL = 60000;
@@ -62,7 +63,10 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
6263
}
6364

6465
protected async onAction(payload: ActionPayload) {
65-
// we don't actually do anything here
66+
switch (payload.action) {
67+
case Action.ReportKeyBackupNotEnabled:
68+
this.onReportKeyBackupNotEnabled();
69+
}
6670
}
6771

6872
protected async onReady() {
@@ -152,6 +156,16 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
152156
});
153157
}
154158
}
159+
160+
private async onReportKeyBackupNotEnabled(): Promise<void> {
161+
if (!SettingsStore.getValue("automaticKeyBackNotEnabledReporting")) return;
162+
163+
await sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
164+
userText: `Auto-reporting key backup not enabled`,
165+
sendLogs: true,
166+
labels: ["web", Action.ReportKeyBackupNotEnabled],
167+
});
168+
}
155169
}
156170

157171
window.mxAutoRageshakeStore = AutoRageshakeStore.instance;

Diff for: test/DeviceListener-test.ts

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
2+
/*
3+
Copyright 2022 The Matrix.org Foundation C.I.C.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
import { EventEmitter } from "events";
19+
import { mocked } from "jest-mock";
20+
import { Room } from "matrix-js-sdk";
21+
22+
import './skinned-sdk';
23+
import DeviceListener from "../src/DeviceListener";
24+
import { MatrixClientPeg } from "../src/MatrixClientPeg";
25+
import * as SetupEncryptionToast from "../src/toasts/SetupEncryptionToast";
26+
import * as UnverifiedSessionToast from "../src/toasts/UnverifiedSessionToast";
27+
import * as BulkUnverifiedSessionsToast from "../src/toasts/BulkUnverifiedSessionsToast";
28+
import { isSecretStorageBeingAccessed } from "../src/SecurityManager";
29+
import dis from "../src/dispatcher/dispatcher";
30+
import { Action } from "../src/dispatcher/actions";
31+
32+
// don't litter test console with logs
33+
jest.mock("matrix-js-sdk/src/logger");
34+
35+
jest.mock("../src/dispatcher/dispatcher", () => ({
36+
dispatch: jest.fn(),
37+
register: jest.fn(),
38+
}));
39+
40+
jest.mock("../src/SecurityManager", () => ({
41+
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
42+
}));
43+
44+
class MockClient extends EventEmitter {
45+
getUserId = jest.fn();
46+
getKeyBackupVersion = jest.fn().mockResolvedValue(undefined);
47+
getRooms = jest.fn().mockReturnValue([]);
48+
doesServerSupportUnstableFeature = jest.fn().mockResolvedValue(true);
49+
isCrossSigningReady = jest.fn().mockResolvedValue(true);
50+
isSecretStorageReady = jest.fn().mockResolvedValue(true);
51+
isCryptoEnabled = jest.fn().mockReturnValue(true);
52+
isInitialSyncComplete = jest.fn().mockReturnValue(true);
53+
getKeyBackupEnabled = jest.fn();
54+
getStoredDevicesForUser = jest.fn().mockReturnValue([]);
55+
getCrossSigningId = jest.fn();
56+
getStoredCrossSigningForUser = jest.fn();
57+
waitForClientWellKnown = jest.fn();
58+
downloadKeys = jest.fn();
59+
isRoomEncrypted = jest.fn();
60+
getClientWellKnown = jest.fn();
61+
}
62+
const mockDispatcher = mocked(dis);
63+
const flushPromises = async () => await new Promise(process.nextTick);
64+
65+
describe('DeviceListener', () => {
66+
let mockClient;
67+
68+
// spy on various toasts' hide and show functions
69+
// easier than mocking
70+
jest.spyOn(SetupEncryptionToast, 'showToast');
71+
jest.spyOn(SetupEncryptionToast, 'hideToast');
72+
jest.spyOn(BulkUnverifiedSessionsToast, 'showToast');
73+
jest.spyOn(BulkUnverifiedSessionsToast, 'hideToast');
74+
jest.spyOn(UnverifiedSessionToast, 'showToast');
75+
jest.spyOn(UnverifiedSessionToast, 'hideToast');
76+
77+
beforeEach(() => {
78+
jest.resetAllMocks();
79+
mockClient = new MockClient();
80+
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
81+
});
82+
83+
const createAndStart = async (): Promise<DeviceListener> => {
84+
const instance = new DeviceListener();
85+
instance.start();
86+
await flushPromises();
87+
return instance;
88+
};
89+
90+
describe('recheck', () => {
91+
it('does nothing when cross signing feature is not supported', async () => {
92+
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
93+
await createAndStart();
94+
95+
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
96+
});
97+
it('does nothing when crypto is not enabled', async () => {
98+
mockClient.isCryptoEnabled.mockReturnValue(false);
99+
await createAndStart();
100+
101+
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
102+
});
103+
it('does nothing when initial sync is not complete', async () => {
104+
mockClient.isInitialSyncComplete.mockReturnValue(false);
105+
await createAndStart();
106+
107+
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
108+
});
109+
110+
describe('set up encryption', () => {
111+
const rooms = [
112+
{ roomId: '!room1' },
113+
{ roomId: '!room2' },
114+
] as unknown as Room[];
115+
116+
beforeEach(() => {
117+
mockClient.isCrossSigningReady.mockResolvedValue(false);
118+
mockClient.isSecretStorageReady.mockResolvedValue(false);
119+
mockClient.getRooms.mockReturnValue(rooms);
120+
mockClient.isRoomEncrypted.mockReturnValue(true);
121+
});
122+
123+
it('hides setup encryption toast when cross signing and secret storage are ready', async () => {
124+
mockClient.isCrossSigningReady.mockResolvedValue(true);
125+
mockClient.isSecretStorageReady.mockResolvedValue(true);
126+
await createAndStart();
127+
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
128+
});
129+
130+
it('hides setup encryption toast when it is dismissed', async () => {
131+
const instance = await createAndStart();
132+
instance.dismissEncryptionSetup();
133+
await flushPromises();
134+
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
135+
});
136+
137+
it('does not do any checks or show any toasts when secret storage is being accessed', async () => {
138+
mocked(isSecretStorageBeingAccessed).mockReturnValue(true);
139+
await createAndStart();
140+
141+
expect(mockClient.downloadKeys).not.toHaveBeenCalled();
142+
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
143+
});
144+
145+
it('does not do any checks or show any toasts when no rooms are encrypted', async () => {
146+
mockClient.isRoomEncrypted.mockReturnValue(false);
147+
await createAndStart();
148+
149+
expect(mockClient.downloadKeys).not.toHaveBeenCalled();
150+
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
151+
});
152+
153+
describe('when user does not have a cross signing id on this device', () => {
154+
beforeEach(() => {
155+
mockClient.getCrossSigningId.mockReturnValue(undefined);
156+
});
157+
158+
it('shows verify session toast when account has cross signing', async () => {
159+
mockClient.getStoredCrossSigningForUser.mockReturnValue(true);
160+
await createAndStart();
161+
162+
expect(mockClient.downloadKeys).toHaveBeenCalled();
163+
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
164+
SetupEncryptionToast.Kind.VERIFY_THIS_SESSION);
165+
});
166+
167+
it('checks key backup status when when account has cross signing', async () => {
168+
mockClient.getCrossSigningId.mockReturnValue(undefined);
169+
mockClient.getStoredCrossSigningForUser.mockReturnValue(true);
170+
await createAndStart();
171+
172+
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
173+
});
174+
});
175+
176+
describe('when user does have a cross signing id on this device', () => {
177+
beforeEach(() => {
178+
mockClient.getCrossSigningId.mockReturnValue('abc');
179+
});
180+
181+
it('shows upgrade encryption toast when user has a key backup available', async () => {
182+
// non falsy response
183+
mockClient.getKeyBackupVersion.mockResolvedValue({});
184+
await createAndStart();
185+
186+
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
187+
SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION);
188+
});
189+
});
190+
});
191+
192+
describe('key backup status', () => {
193+
it('checks keybackup status when cross signing and secret storage are ready', async () => {
194+
// default mocks set cross signing and secret storage to ready
195+
await createAndStart();
196+
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
197+
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
198+
});
199+
200+
it('checks keybackup status when setup encryption toast has been dismissed', async () => {
201+
mockClient.isCrossSigningReady.mockResolvedValue(false);
202+
const instance = await createAndStart();
203+
204+
instance.dismissEncryptionSetup();
205+
await flushPromises();
206+
207+
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
208+
});
209+
210+
it('does not dispatch keybackup event when key backup check is not finished', async () => {
211+
// returns null when key backup status hasn't finished being checked
212+
mockClient.getKeyBackupEnabled.mockReturnValue(null);
213+
await createAndStart();
214+
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
215+
});
216+
217+
it('dispatches keybackup event when key backup is not enabled', async () => {
218+
mockClient.getKeyBackupEnabled.mockReturnValue(false);
219+
await createAndStart();
220+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ReportKeyBackupNotEnabled });
221+
});
222+
223+
it('does not check key backup status again after check is complete', async () => {
224+
mockClient.getKeyBackupEnabled.mockReturnValue(null);
225+
const instance = await createAndStart();
226+
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
227+
228+
// keyback check now complete
229+
mockClient.getKeyBackupEnabled.mockReturnValue(true);
230+
231+
// trigger a recheck
232+
instance.dismissEncryptionSetup();
233+
await flushPromises();
234+
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
235+
236+
// trigger another recheck
237+
instance.dismissEncryptionSetup();
238+
await flushPromises();
239+
// not called again, check was complete last time
240+
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
241+
});
242+
});
243+
});
244+
});

0 commit comments

Comments
 (0)