From 80ed52c21ffd2cfff6e07dbb685250a254e2cb27 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Wed, 2 Sep 2026 10:52:27 +0200 Subject: [PATCH 1/2] fix(console): stop the paste-scam warning firing on every mobile keyboard --- src/app/utils/consolePasteScamWarning.test.ts | 44 +++++++++++++++++++ src/app/utils/consolePasteScamWarning.ts | 6 +++ 2 files changed, 50 insertions(+) create mode 100644 src/app/utils/consolePasteScamWarning.test.ts diff --git a/src/app/utils/consolePasteScamWarning.test.ts b/src/app/utils/consolePasteScamWarning.test.ts new file mode 100644 index 0000000000..19af68472d --- /dev/null +++ b/src/app/utils/consolePasteScamWarning.test.ts @@ -0,0 +1,44 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { installConsolePasteScamWarning } from './consolePasteScamWarning'; + +const isMobileTauri = vi.hoisted(() => vi.fn<() => boolean>()); + +vi.mock('./platform', () => ({ isMobileTauri })); + +describe('installConsolePasteScamWarning', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.clearAllMocks(); + window.innerWidth = 800; + window.outerWidth = 800; + window.innerHeight = 400; + // A soft keyboard shrinks the viewport by far more than the 160px threshold. + window.outerHeight = 900; + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('stays silent on a phone, where the keyboard looks like docked devtools', () => { + isMobileTauri.mockReturnValue(true); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + installConsolePasteScamWarning(); + vi.advanceTimersByTime(2000); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('still warns on desktop when devtools look docked', () => { + isMobileTauri.mockReturnValue(false); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + installConsolePasteScamWarning(); + vi.advanceTimersByTime(2000); + + expect(warn).toHaveBeenCalled(); + warn.mockRestore(); + }); +}); diff --git a/src/app/utils/consolePasteScamWarning.ts b/src/app/utils/consolePasteScamWarning.ts index 72d750e28f..2ec701092f 100644 --- a/src/app/utils/consolePasteScamWarning.ts +++ b/src/app/utils/consolePasteScamWarning.ts @@ -1,3 +1,5 @@ +import { isMobileTauri } from './platform'; + // This is probably not very accurate, but doesn't really matter I suppose function isDockedDevtoolsLikely(): boolean { const gapW = window.outerWidth - window.innerWidth; @@ -7,6 +9,10 @@ function isDockedDevtoolsLikely(): boolean { } export function installConsolePasteScamWarning(): void { + // A phone has no docked devtools, and the soft keyboard moves the viewport far enough + // to look exactly like one opening. + if (isMobileTauri()) return; + const BANNER_STYLE = 'font-size:56px;font-weight:900;color:#ff0033;background:#1a0006;padding:16px 24px;border:6px solid #ff0033;line-height:1.1;'; const BODY_STYLE = From 9688c696c4b46afd5c76891ed4750d0810224444 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Wed, 2 Sep 2026 11:05:27 +0200 Subject: [PATCH 2/2] fix(crypto): report to-device events the engine could not read --- src/app/crypto/engineCrypto/EngineCrypto.ts | 11 +++++-- .../incomingVerificationRequest.test.ts | 33 ++++++++++++++++++- src/app/utils/verificationTrace.ts | 10 ++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/app/crypto/engineCrypto/EngineCrypto.ts b/src/app/crypto/engineCrypto/EngineCrypto.ts index a072222a67..7ede813a8f 100644 --- a/src/app/crypto/engineCrypto/EngineCrypto.ts +++ b/src/app/crypto/engineCrypto/EngineCrypto.ts @@ -37,7 +37,7 @@ import { secretStorageCanAccessSecrets } from './secretStorageAccess'; import { PerSessionBackupDownloader } from './perSessionBackupDownload'; import type { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto-api/CryptoEventHandlerMap'; import { createDebugLogger } from '$utils/debugLogger'; -import { traceVerification, warnVerification } from '$utils/verificationTrace'; +import { traceVerification, warnToDevice, warnVerification } from '$utils/verificationTrace'; import { EngineVerificationRequest } from '../verification/request'; import { EnginePhase, @@ -921,8 +921,15 @@ export class EngineCrypto }); } else if (event.type === ProcessedToDeviceEventType.PlainText) { received.push({ message, encryptionInfo: null }); + } else { + // Dropped like js-sdk's backend does, but an unreadable one carries no type, so a + // verification request lost here is invisible everywhere else. + warnToDevice('Dropped a to-device event the engine could not read', { + sender: message.sender ?? 'unknown', + type: message.type ?? 'unknown', + processed: event.type, + }); } - // Undecryptable and invalid events are dropped, as js-sdk's own backend does. } return received; diff --git a/src/app/crypto/engineCrypto/incomingVerificationRequest.test.ts b/src/app/crypto/engineCrypto/incomingVerificationRequest.test.ts index ddaf69b072..8215940ec9 100644 --- a/src/app/crypto/engineCrypto/incomingVerificationRequest.test.ts +++ b/src/app/crypto/engineCrypto/incomingVerificationRequest.test.ts @@ -1,11 +1,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CryptoEvent, EventType, type MatrixClient } from '$types/matrix-sdk'; import { engineInvoke } from '../olmMachine/engineInvoke'; -import { traceVerification, warnVerification } from '$utils/verificationTrace'; +import { traceVerification, warnToDevice, warnVerification } from '$utils/verificationTrace'; import { EngineCrypto } from './EngineCrypto'; vi.mock('$utils/verificationTrace', () => ({ traceVerification: vi.fn<(message: string, data?: unknown) => void>(), + warnToDevice: vi.fn<(message: string, data?: unknown) => void>(), warnVerification: vi.fn<(message: string, data?: unknown) => void>(), })); @@ -33,6 +34,36 @@ const requestState = { isSelfVerification: true, }; +describe('unreadable to-device events', () => { + beforeEach(() => { + mockInvoke.mockReset(); + vi.mocked(warnToDevice).mockClear(); + }); + + it('reports one the engine could not decrypt instead of dropping it silently', async () => { + const { mx } = clientSpy(); + mockInvoke.mockImplementation(async (_identity, method) => { + if (method === 'receiveSyncChanges') { + return [ + { + type: 1, + rawEvent: JSON.stringify({ type: 'm.room.encrypted', sender: '@me:e.org' }), + }, + ]; + } + return []; + }); + + const crypto = new EngineCrypto(mx, { userId: '@me:e.org', deviceId: 'D' }); + await crypto.preprocessToDeviceMessages([REQUEST_EVENT as never]); + + expect(warnToDevice).toHaveBeenCalledWith( + 'Dropped a to-device event the engine could not read', + expect.objectContaining({ sender: '@me:e.org', type: 'm.room.encrypted' }) + ); + }); +}); + describe('sending a verification request', () => { beforeEach(() => { mockInvoke.mockReset(); diff --git a/src/app/utils/verificationTrace.ts b/src/app/utils/verificationTrace.ts index 3a881330f2..41d17fbc59 100644 --- a/src/app/utils/verificationTrace.ts +++ b/src/app/utils/verificationTrace.ts @@ -22,6 +22,16 @@ export const traceVerification = (message: string, data: TraceData = {}): void = Sentry.logger.info(`[crypto:verification] ${message}`, attributes(data)); }; +export const warnToDevice = (message: string, data: TraceData = {}): void => { + Sentry.addBreadcrumb({ + category: 'crypto.to-device', + message, + level: 'warning', + data: attributes(data), + }); + Sentry.logger.warn(`[crypto:to-device] ${message}`, attributes(data)); +}; + export const warnVerification = (message: string, data: TraceData = {}): void => { Sentry.addBreadcrumb({ category: 'crypto.verification',