From 3eb5130cda6340454ea912c7193e2561cefc7866 Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Thu, 13 Jan 2022 10:55:25 -0500 Subject: [PATCH] Add labs flag to automatically rageshake on decryption errors (#7307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also sends a to-device message to the sender, prompting them to auto-rageshake too if they have this lab enabled as well. Co-authored-by: Šimon Brandner Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/@types/global.d.ts | 2 + src/components/structures/MatrixChat.tsx | 1 + .../tabs/user/LabsUserSettingsTab.tsx | 5 + src/i18n/strings/en_EN.json | 1 + src/rageshake/submit-rageshake.ts | 22 ++- src/settings/Settings.tsx | 6 + src/stores/AutoRageshakeStore.ts | 136 ++++++++++++++++++ 7 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 src/stores/AutoRageshakeStore.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7b1a95ac55..9c2e7677ed 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -52,6 +52,7 @@ import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import { Skinner } from "../Skinner"; +import AutoRageshakeStore from "../stores/AutoRageshakeStore"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -111,6 +112,7 @@ declare global { electron?: Electron; mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise; mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise; + mxAutoRageshakeStore?: AutoRageshakeStore; } interface DesktopCapturerSource { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 57cb860d95..2843ed04a1 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -44,6 +44,7 @@ import * as Rooms from '../../Rooms'; import * as Lifecycle from '../../Lifecycle'; // LifecycleStore is not used but does listen to and dispatch actions import '../../stores/LifecycleStore'; +import '../../stores/AutoRageshakeStore'; import PageType from '../../PageTypes'; import createRoom, { IOpts } from "../../createRoom"; import { _t, _td, getCurrentLanguage } from '../../languageHandler'; diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 0a20bcabcf..2e9ffc9fb9 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -129,6 +129,11 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> { name="automaticErrorReporting" level={SettingLevel.DEVICE} />, + , ); if (this.state.showHiddenReadReceipts) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6b0be665e1..e1ce9479a4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -948,6 +948,7 @@ "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.", "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", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 58adbf8726..5fdca27fa0 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -31,7 +31,8 @@ interface IOpts { label?: string; userText?: string; sendLogs?: boolean; - progressCallback?: (string) => void; + progressCallback?: (s: string) => void; + customFields?: Record; } async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { @@ -72,6 +73,12 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append('installed_pwa', installedPWA); body.append('touch_input', touchInput); + if (opts.customFields) { + for (const key in opts.customFields) { + body.append(key, opts.customFields[key]); + } + } + if (client) { body.append('user_id', client.credentials.userId); body.append('device_id', client.deviceId); @@ -191,9 +198,9 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { * * @param {function(string)} opts.progressCallback Callback to call with progress updates * - * @return {Promise} Resolved when the bug report is sent. + * @return {Promise} URL returned by the rageshake server */ -export default async function sendBugReport(bugReportEndpoint: string, opts: IOpts = {}) { +export default async function sendBugReport(bugReportEndpoint: string, opts: IOpts = {}): Promise { if (!bugReportEndpoint) { throw new Error("No bug report endpoint has been set."); } @@ -202,7 +209,7 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp const body = await collectBugReport(opts); progressCallback(_t("Uploading logs")); - await submitReport(bugReportEndpoint, body, progressCallback); + return await submitReport(bugReportEndpoint, body, progressCallback); } /** @@ -291,10 +298,11 @@ export async function submitFeedback( await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {}); } -function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void) { - return new Promise((resolve, reject) => { +function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void): Promise { + return new Promise((resolve, reject) => { const req = new XMLHttpRequest(); req.open("POST", endpoint); + req.responseType = "json"; req.timeout = 5 * 60 * 1000; req.onreadystatechange = function() { if (req.readyState === XMLHttpRequest.LOADING) { @@ -305,7 +313,7 @@ function submitReport(endpoint: string, body: FormData, progressCallback: (str: reject(new Error(`HTTP ${req.status}`)); return; } - resolve(); + resolve(req.response.report_url || ""); } }; req.send(body); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index eb18eed35f..de2ddb7245 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -880,6 +880,12 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: false, controller: new ReloadOnChangeController(), }, + "automaticDecryptionErrorReporting": { + displayName: _td("Automatically send debug logs on decryption errors"), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + controller: new ReloadOnChangeController(), + }, [UIFeature.RoomHistorySettings]: { supportedLevels: LEVELS_UI_FEATURE, default: true, diff --git a/src/stores/AutoRageshakeStore.ts b/src/stores/AutoRageshakeStore.ts new file mode 100644 index 0000000000..958c1e4635 --- /dev/null +++ b/src/stores/AutoRageshakeStore.ts @@ -0,0 +1,136 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src"; + +import SdkConfig from '../SdkConfig'; +import sendBugReport from '../rageshake/submit-rageshake'; +import defaultDispatcher from '../dispatcher/dispatcher'; +import { AsyncStoreWithClient } from './AsyncStoreWithClient'; +import { ActionPayload } from '../dispatcher/payloads'; +import SettingsStore from "../settings/SettingsStore"; + +// Minimum interval of 5 minutes between reports, especially important when we're doing an initial sync with a lot of decryption errors +const RAGESHAKE_INTERVAL = 5*60*1000; +// Event type for to-device messages requesting sender auto-rageshakes +const AUTO_RS_REQUEST = "im.vector.auto_rs_request"; + +interface IState { + reportedSessionIds: Set; + lastRageshakeTime: number; +} + +/** + * Watches for decryption errors to auto-report if the relevant lab is + * enabled, and keeps track of session IDs that have already been + * reported. + */ +export default class AutoRageshakeStore extends AsyncStoreWithClient { + private static internalInstance = new AutoRageshakeStore(); + + private constructor() { + super(defaultDispatcher, { + reportedSessionIds: new Set(), + lastRageshakeTime: 0, + }); + this.onDecryptionAttempt = this.onDecryptionAttempt.bind(this); + this.onDeviceMessage = this.onDeviceMessage.bind(this); + } + + public static get instance(): AutoRageshakeStore { + return AutoRageshakeStore.internalInstance; + } + + protected async onAction(payload: ActionPayload) { + // we don't actually do anything here + } + + protected async onReady() { + if (!SettingsStore.getValue("automaticDecryptionErrorReporting")) return; + + if (this.matrixClient) { + this.matrixClient.on('Event.decrypted', this.onDecryptionAttempt); + this.matrixClient.on('toDeviceEvent', this.onDeviceMessage); + } + } + + protected async onNotReady() { + if (this.matrixClient) { + this.matrixClient.removeListener('toDeviceEvent', this.onDeviceMessage); + this.matrixClient.removeListener('Event.decrypted', this.onDecryptionAttempt); + } + } + + private async onDecryptionAttempt(ev: MatrixEvent): Promise { + const wireContent = ev.getWireContent(); + const sessionId = wireContent.session_id; + if (ev.isDecryptionFailure() && !this.state.reportedSessionIds.has(sessionId)) { + const newReportedSessionIds = new Set(this.state.reportedSessionIds); + await this.updateState({ reportedSessionIds: newReportedSessionIds.add(sessionId) }); + + const now = new Date().getTime(); + if (now - this.state.lastRageshakeTime < RAGESHAKE_INTERVAL) { return; } + + await this.updateState({ lastRageshakeTime: now }); + + const eventInfo = { + "event_id": ev.getId(), + "room_id": ev.getRoomId(), + "session_id": sessionId, + "device_id": wireContent.device_id, + "user_id": ev.getSender(), + "sender_key": wireContent.sender_key, + }; + + const rageshakeURL = await sendBugReport(SdkConfig.get().bug_report_endpoint_url, { + userText: "Auto-reporting decryption error (recipient)", + sendLogs: true, + label: "Z-UISI", + customFields: { "auto_uisi": JSON.stringify(eventInfo) }, + }); + + const messageContent = { + ...eventInfo, + "recipient_rageshake": rageshakeURL, + }; + this.matrixClient.sendToDevice( + AUTO_RS_REQUEST, + { [messageContent.user_id]: { [messageContent.device_id]: messageContent } }, + ); + } + } + + private async onDeviceMessage(ev: MatrixEvent): Promise { + if (ev.getType() !== AUTO_RS_REQUEST) return; + const messageContent = ev.getContent(); + const recipientRageshake = messageContent["recipient_rageshake"] || ""; + const now = new Date().getTime(); + if (now - this.state.lastRageshakeTime > RAGESHAKE_INTERVAL) { + await this.updateState({ lastRageshakeTime: now }); + await sendBugReport(SdkConfig.get().bug_report_endpoint_url, { + userText: `Auto-reporting decryption error (sender)\nRecipient rageshake: ${recipientRageshake}`, + sendLogs: true, + label: "Z-UISI", + customFields: { + "recipient_rageshake": recipientRageshake, + "auto_uisi": JSON.stringify(messageContent), + }, + }); + } + } +} + +window.mxAutoRageshakeStore = AutoRageshakeStore.instance;