mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Add config option to autorageshake when key backup is not enabled (#7741)
* report on not enabled Signed-off-by: Kerry Archibald <kerrya@element.io> * add setting Signed-off-by: Kerry Archibald <kerrya@element.io> * check key backup status after crypto init Signed-off-by: Kerry Archibald <kerrya@element.io> * remove log Signed-off-by: Kerry Archibald <kerrya@element.io> * test encryption setup in DeviceListener Signed-off-by: Kerry Archibald <kerrya@element.io> * i18n Signed-off-by: Kerry Archibald <kerrya@element.io> * sendLogs for key backup auto-report event Signed-off-by: Kerry Archibald <kerrya@element.io> * remove reloadOnChagneController Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
d06ec845ee
commit
b5e7d12f76
@ -36,6 +36,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
|
||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||
import { isLoggedIn } from './components/structures/MatrixChat';
|
||||
import { ActionPayload } from "./dispatcher/payloads";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
@ -48,6 +49,7 @@ export default class DeviceListener {
|
||||
// cache of the key backup info
|
||||
private keyBackupInfo: object = null;
|
||||
private keyBackupFetchedAt: number = null;
|
||||
private keyBackupStatusChecked = false;
|
||||
// We keep a list of our own device IDs so we can batch ones that were already
|
||||
// there the last time the app launched into a single toast, but display new
|
||||
// ones in their own toasts.
|
||||
@ -92,6 +94,7 @@ export default class DeviceListener {
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.keyBackupInfo = null;
|
||||
this.keyBackupFetchedAt = null;
|
||||
this.keyBackupStatusChecked = false;
|
||||
this.ourDeviceIdsAtStart = null;
|
||||
this.displayingToastsForDeviceIds = new Set();
|
||||
}
|
||||
@ -227,6 +230,8 @@ export default class DeviceListener {
|
||||
|
||||
if (this.dismissedThisDeviceToast || allSystemsReady) {
|
||||
hideSetupEncryptionToast();
|
||||
|
||||
this.checkKeyBackupStatus();
|
||||
} else if (this.shouldShowSetupEncryptionToast()) {
|
||||
// make sure our keys are finished downloading
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
@ -238,6 +243,7 @@ export default class DeviceListener {
|
||||
) {
|
||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
this.checkKeyBackupStatus();
|
||||
} else {
|
||||
const backupInfo = await this.getKeyBackupInfo();
|
||||
if (backupInfo) {
|
||||
@ -312,4 +318,17 @@ export default class DeviceListener {
|
||||
|
||||
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
}
|
||||
|
||||
private checkKeyBackupStatus = async () => {
|
||||
if (this.keyBackupStatusChecked) {
|
||||
return;
|
||||
}
|
||||
// returns null when key backup status hasn't finished being checked
|
||||
const isKeyBackupEnabled = MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
this.keyBackupStatusChecked = isKeyBackupEnabled !== null;
|
||||
|
||||
if (isKeyBackupEnabled === false) {
|
||||
dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -218,4 +218,10 @@ export enum Action {
|
||||
* Payload: none
|
||||
*/
|
||||
AnonymousAnalyticsReject = "anonymous_analytics_reject",
|
||||
|
||||
/**
|
||||
* Fires after crypto is setup if key backup is not enabled
|
||||
* Used to trigger auto rageshakes when configured
|
||||
*/
|
||||
ReportKeyBackupNotEnabled = "report_key_backup_not_enabled",
|
||||
}
|
||||
|
@ -960,6 +960,7 @@
|
||||
"Developer mode": "Developer mode",
|
||||
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
|
||||
"Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
|
||||
"Automatically send debug logs when key backup is not functioning": "Automatically send debug logs when key backup is not functioning",
|
||||
"Collecting app version information": "Collecting app version information",
|
||||
"Collecting logs": "Collecting logs",
|
||||
"Uploading logs": "Uploading logs",
|
||||
|
@ -891,6 +891,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||
default: false,
|
||||
controller: new ReloadOnChangeController(),
|
||||
},
|
||||
"automaticKeyBackNotEnabledReporting": {
|
||||
displayName: _td("Automatically send debug logs when key backup is not functioning"),
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: false,
|
||||
},
|
||||
[UIFeature.RoomHistorySettings]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
|
@ -24,6 +24,7 @@ import defaultDispatcher from '../dispatcher/dispatcher';
|
||||
import { AsyncStoreWithClient } from './AsyncStoreWithClient';
|
||||
import { ActionPayload } from '../dispatcher/payloads';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
|
||||
// Minimum interval of 1 minute between reports
|
||||
const RAGESHAKE_INTERVAL = 60000;
|
||||
@ -62,7 +63,10 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
// we don't actually do anything here
|
||||
switch (payload.action) {
|
||||
case Action.ReportKeyBackupNotEnabled:
|
||||
this.onReportKeyBackupNotEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
@ -152,6 +156,16 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async onReportKeyBackupNotEnabled(): Promise<void> {
|
||||
if (!SettingsStore.getValue("automaticKeyBackNotEnabledReporting")) return;
|
||||
|
||||
await sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText: `Auto-reporting key backup not enabled`,
|
||||
sendLogs: true,
|
||||
labels: ["web", Action.ReportKeyBackupNotEnabled],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.mxAutoRageshakeStore = AutoRageshakeStore.instance;
|
||||
|
244
test/DeviceListener-test.ts
Normal file
244
test/DeviceListener-test.ts
Normal file
@ -0,0 +1,244 @@
|
||||
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room } from "matrix-js-sdk";
|
||||
|
||||
import './skinned-sdk';
|
||||
import DeviceListener from "../src/DeviceListener";
|
||||
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||
import * as SetupEncryptionToast from "../src/toasts/SetupEncryptionToast";
|
||||
import * as UnverifiedSessionToast from "../src/toasts/UnverifiedSessionToast";
|
||||
import * as BulkUnverifiedSessionsToast from "../src/toasts/BulkUnverifiedSessionsToast";
|
||||
import { isSecretStorageBeingAccessed } from "../src/SecurityManager";
|
||||
import dis from "../src/dispatcher/dispatcher";
|
||||
import { Action } from "../src/dispatcher/actions";
|
||||
|
||||
// don't litter test console with logs
|
||||
jest.mock("matrix-js-sdk/src/logger");
|
||||
|
||||
jest.mock("../src/dispatcher/dispatcher", () => ({
|
||||
dispatch: jest.fn(),
|
||||
register: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../src/SecurityManager", () => ({
|
||||
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
class MockClient extends EventEmitter {
|
||||
getUserId = jest.fn();
|
||||
getKeyBackupVersion = jest.fn().mockResolvedValue(undefined);
|
||||
getRooms = jest.fn().mockReturnValue([]);
|
||||
doesServerSupportUnstableFeature = jest.fn().mockResolvedValue(true);
|
||||
isCrossSigningReady = jest.fn().mockResolvedValue(true);
|
||||
isSecretStorageReady = jest.fn().mockResolvedValue(true);
|
||||
isCryptoEnabled = jest.fn().mockReturnValue(true);
|
||||
isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
||||
getKeyBackupEnabled = jest.fn();
|
||||
getStoredDevicesForUser = jest.fn().mockReturnValue([]);
|
||||
getCrossSigningId = jest.fn();
|
||||
getStoredCrossSigningForUser = jest.fn();
|
||||
waitForClientWellKnown = jest.fn();
|
||||
downloadKeys = jest.fn();
|
||||
isRoomEncrypted = jest.fn();
|
||||
getClientWellKnown = jest.fn();
|
||||
}
|
||||
const mockDispatcher = mocked(dis);
|
||||
const flushPromises = async () => await new Promise(process.nextTick);
|
||||
|
||||
describe('DeviceListener', () => {
|
||||
let mockClient;
|
||||
|
||||
// spy on various toasts' hide and show functions
|
||||
// easier than mocking
|
||||
jest.spyOn(SetupEncryptionToast, 'showToast');
|
||||
jest.spyOn(SetupEncryptionToast, 'hideToast');
|
||||
jest.spyOn(BulkUnverifiedSessionsToast, 'showToast');
|
||||
jest.spyOn(BulkUnverifiedSessionsToast, 'hideToast');
|
||||
jest.spyOn(UnverifiedSessionToast, 'showToast');
|
||||
jest.spyOn(UnverifiedSessionToast, 'hideToast');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockClient = new MockClient();
|
||||
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
const createAndStart = async (): Promise<DeviceListener> => {
|
||||
const instance = new DeviceListener();
|
||||
instance.start();
|
||||
await flushPromises();
|
||||
return instance;
|
||||
};
|
||||
|
||||
describe('recheck', () => {
|
||||
it('does nothing when cross signing feature is not supported', async () => {
|
||||
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does nothing when crypto is not enabled', async () => {
|
||||
mockClient.isCryptoEnabled.mockReturnValue(false);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does nothing when initial sync is not complete', async () => {
|
||||
mockClient.isInitialSyncComplete.mockReturnValue(false);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('set up encryption', () => {
|
||||
const rooms = [
|
||||
{ roomId: '!room1' },
|
||||
{ roomId: '!room2' },
|
||||
] as unknown as Room[];
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.isCrossSigningReady.mockResolvedValue(false);
|
||||
mockClient.isSecretStorageReady.mockResolvedValue(false);
|
||||
mockClient.getRooms.mockReturnValue(rooms);
|
||||
mockClient.isRoomEncrypted.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('hides setup encryption toast when cross signing and secret storage are ready', async () => {
|
||||
mockClient.isCrossSigningReady.mockResolvedValue(true);
|
||||
mockClient.isSecretStorageReady.mockResolvedValue(true);
|
||||
await createAndStart();
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides setup encryption toast when it is dismissed', async () => {
|
||||
const instance = await createAndStart();
|
||||
instance.dismissEncryptionSetup();
|
||||
await flushPromises();
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not do any checks or show any toasts when secret storage is being accessed', async () => {
|
||||
mocked(isSecretStorageBeingAccessed).mockReturnValue(true);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.downloadKeys).not.toHaveBeenCalled();
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not do any checks or show any toasts when no rooms are encrypted', async () => {
|
||||
mockClient.isRoomEncrypted.mockReturnValue(false);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.downloadKeys).not.toHaveBeenCalled();
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when user does not have a cross signing id on this device', () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getCrossSigningId.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it('shows verify session toast when account has cross signing', async () => {
|
||||
mockClient.getStoredCrossSigningForUser.mockReturnValue(true);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.downloadKeys).toHaveBeenCalled();
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.VERIFY_THIS_SESSION);
|
||||
});
|
||||
|
||||
it('checks key backup status when when account has cross signing', async () => {
|
||||
mockClient.getCrossSigningId.mockReturnValue(undefined);
|
||||
mockClient.getStoredCrossSigningForUser.mockReturnValue(true);
|
||||
await createAndStart();
|
||||
|
||||
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user does have a cross signing id on this device', () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getCrossSigningId.mockReturnValue('abc');
|
||||
});
|
||||
|
||||
it('shows upgrade encryption toast when user has a key backup available', async () => {
|
||||
// non falsy response
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({});
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('key backup status', () => {
|
||||
it('checks keybackup status when cross signing and secret storage are ready', async () => {
|
||||
// default mocks set cross signing and secret storage to ready
|
||||
await createAndStart();
|
||||
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
|
||||
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checks keybackup status when setup encryption toast has been dismissed', async () => {
|
||||
mockClient.isCrossSigningReady.mockResolvedValue(false);
|
||||
const instance = await createAndStart();
|
||||
|
||||
instance.dismissEncryptionSetup();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not dispatch keybackup event when key backup check is not finished', async () => {
|
||||
// returns null when key backup status hasn't finished being checked
|
||||
mockClient.getKeyBackupEnabled.mockReturnValue(null);
|
||||
await createAndStart();
|
||||
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches keybackup event when key backup is not enabled', async () => {
|
||||
mockClient.getKeyBackupEnabled.mockReturnValue(false);
|
||||
await createAndStart();
|
||||
expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ReportKeyBackupNotEnabled });
|
||||
});
|
||||
|
||||
it('does not check key backup status again after check is complete', async () => {
|
||||
mockClient.getKeyBackupEnabled.mockReturnValue(null);
|
||||
const instance = await createAndStart();
|
||||
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
|
||||
|
||||
// keyback check now complete
|
||||
mockClient.getKeyBackupEnabled.mockReturnValue(true);
|
||||
|
||||
// trigger a recheck
|
||||
instance.dismissEncryptionSetup();
|
||||
await flushPromises();
|
||||
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
|
||||
|
||||
// trigger another recheck
|
||||
instance.dismissEncryptionSetup();
|
||||
await flushPromises();
|
||||
// not called again, check was complete last time
|
||||
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user