mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-25 09:58:11 +08:00
c4c1faff97
* Add flag for tests to avoid double-reporting check Some of the tests were flaking. Our best guess is that it's failing to track some events due to false positives in the Bloom filter used to guard against double-reporting. So we add a flag to disable using that in tests (except for tests that test that functionality). * invert logic to avoid double negative
767 lines
29 KiB
TypeScript
767 lines
29 KiB
TypeScript
/*
|
|
Copyright 2018 New Vector Ltd
|
|
|
|
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 { mocked, Mocked, MockedObject } from "jest-mock";
|
|
import { CryptoEvent, HttpApiEvent, MatrixClient, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
|
import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
|
|
import { CryptoApi, DecryptionFailureCode, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
|
import { sleep } from "matrix-js-sdk/src/utils";
|
|
|
|
import { DecryptionFailureTracker, ErrorProperties } from "../src/DecryptionFailureTracker";
|
|
import { stubClient } from "./test-utils";
|
|
import * as Lifecycle from "../src/Lifecycle";
|
|
|
|
async function createFailedDecryptionEvent(opts: { sender?: string; code?: DecryptionFailureCode } = {}) {
|
|
return await mkDecryptionFailureMatrixEvent({
|
|
roomId: "!room:id",
|
|
sender: opts.sender ?? "@alice:example.com",
|
|
code: opts.code ?? DecryptionFailureCode.UNKNOWN_ERROR,
|
|
msg: ":(",
|
|
});
|
|
}
|
|
|
|
// wrap tracker.eventDecrypted so that we don't need to have so many `ts-ignore`s
|
|
function eventDecrypted(tracker: DecryptionFailureTracker, e: MatrixEvent, nowTs: number): void {
|
|
// @ts-ignore access to private member
|
|
return tracker.eventDecrypted(e, nowTs);
|
|
}
|
|
|
|
describe("DecryptionFailureTracker", function () {
|
|
afterEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
it("tracks a failed decryption for a visible event", async function () {
|
|
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// should track a failure for an event that failed decryption
|
|
expect(count).not.toBe(0);
|
|
});
|
|
|
|
it("tracks a failed decryption with expected raw error for a visible event", async function () {
|
|
const failedDecryptionEvent = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
|
});
|
|
|
|
let count = 0;
|
|
let reportedRawCode = "";
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(_errCode: string, rawCode: string) => {
|
|
count++;
|
|
reportedRawCode = rawCode;
|
|
},
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// should track a failure for an event that failed decryption
|
|
expect(count).not.toBe(0);
|
|
|
|
// Should add the rawCode to the event context
|
|
expect(reportedRawCode).toBe("OLM_UNKNOWN_MESSAGE_INDEX");
|
|
});
|
|
|
|
it("tracks a failed decryption for an event that becomes visible later", async function () {
|
|
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// should track a failure for an event that failed decryption
|
|
expect(count).not.toBe(0);
|
|
});
|
|
|
|
it("tracks visible vs. not visible events", async () => {
|
|
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
|
propertiesByErrorCode[errorCode] = properties;
|
|
},
|
|
(error: string) => error,
|
|
false,
|
|
);
|
|
|
|
// use three different errors so that we can distinguish the reports
|
|
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
|
|
const error2 = DecryptionFailureCode.MEGOLM_BAD_ROOM;
|
|
const error3 = DecryptionFailureCode.MEGOLM_MISSING_FIELDS;
|
|
|
|
// event that will be marked as visible before it's marked as undecryptable
|
|
const markedVisibleFirst = await createFailedDecryptionEvent({ code: error1 });
|
|
// event that will be marked as undecryptable before it's marked as visible
|
|
const markedUndecryptableFirst = await createFailedDecryptionEvent({ code: error2 });
|
|
// event that is never marked as visible
|
|
const neverVisible = await createFailedDecryptionEvent({ code: error3 });
|
|
|
|
tracker.addVisibleEvent(markedVisibleFirst);
|
|
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, markedVisibleFirst, now);
|
|
eventDecrypted(tracker, markedUndecryptableFirst, now);
|
|
eventDecrypted(tracker, neverVisible, now);
|
|
|
|
tracker.addVisibleEvent(markedUndecryptableFirst);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(propertiesByErrorCode[error1].wasVisibleToUser).toBe(true);
|
|
expect(propertiesByErrorCode[error2].wasVisibleToUser).toBe(true);
|
|
expect(propertiesByErrorCode[error3].wasVisibleToUser).toBe(false);
|
|
});
|
|
|
|
it("does not track a failed decryption where the event is subsequently successfully decrypted", async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => {
|
|
// should not track an event that has since been decrypted correctly
|
|
expect(true).toBe(false);
|
|
},
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// Indicate successful decryption.
|
|
await decryptExistingEvent(decryptedEvent, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
});
|
|
|
|
it(
|
|
"does not track a failed decryption where the event is subsequently successfully decrypted " +
|
|
"and later becomes visible",
|
|
async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => {
|
|
// should not track an event that has since been decrypted correctly
|
|
expect(true).toBe(false);
|
|
},
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// Indicate successful decryption.
|
|
await decryptExistingEvent(decryptedEvent, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
},
|
|
);
|
|
|
|
it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
const decryptedEvent2 = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Arbitrary number of failed decryptions for both events
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, decryptedEvent, now);
|
|
eventDecrypted(tracker, decryptedEvent, now);
|
|
eventDecrypted(tracker, decryptedEvent, now);
|
|
eventDecrypted(tracker, decryptedEvent, now);
|
|
eventDecrypted(tracker, decryptedEvent, now);
|
|
eventDecrypted(tracker, decryptedEvent2, now);
|
|
eventDecrypted(tracker, decryptedEvent2, now);
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
eventDecrypted(tracker, decryptedEvent2, now);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Simulated polling of `checkFailures`, an arbitrary number ( > 2 ) times
|
|
tracker.checkFailures(Infinity);
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// should only track a single failure per event
|
|
expect(count).toBe(2);
|
|
});
|
|
|
|
it("should not track a failure for an event that was tracked previously", async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
);
|
|
await tracker.start(mockClient());
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Indicate decryption
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Indicate a second decryption, after having tracked the failure
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// should only track a single failure per event
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it("should not report a failure for an event that was reported in a previous session", async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
);
|
|
await tracker.start(mockClient());
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Indicate decryption
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// Pretend "now" is Infinity
|
|
// NB: This saves to localStorage specific to DFT
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Simulate the browser refreshing by destroying tracker and creating a new tracker
|
|
// @ts-ignore access to private constructor
|
|
const secondTracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
);
|
|
await secondTracker.start(mockClient());
|
|
|
|
secondTracker.addVisibleEvent(decryptedEvent);
|
|
|
|
eventDecrypted(secondTracker, decryptedEvent, Date.now());
|
|
secondTracker.checkFailures(Infinity);
|
|
|
|
// should only track a single failure per event
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it("should report a failure for an event that was tracked but not reported in a previous session", async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
);
|
|
await tracker.start(mockClient());
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Indicate decryption
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// we do *not* call `checkFailures` here
|
|
expect(count).toBe(0);
|
|
|
|
// Simulate the browser refreshing by destroying tracker and creating a new tracker
|
|
// @ts-ignore access to private constructor
|
|
const secondTracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
);
|
|
await secondTracker.start(mockClient());
|
|
|
|
secondTracker.addVisibleEvent(decryptedEvent);
|
|
|
|
eventDecrypted(secondTracker, decryptedEvent, Date.now());
|
|
secondTracker.checkFailures(Infinity);
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it("should report a failure for an event that was reported before a logout/login cycle", async () => {
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
() => count++,
|
|
() => "UnknownError",
|
|
);
|
|
await tracker.start(mockClient());
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Indicate decryption
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
tracker.checkFailures(Infinity);
|
|
expect(count).toBe(1);
|
|
|
|
// Simulate a logout/login cycle
|
|
await Lifecycle.onLoggedOut();
|
|
await tracker.start(mockClient());
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
tracker.checkFailures(Infinity);
|
|
expect(count).toBe(2);
|
|
});
|
|
|
|
it("should count different error codes separately for multiple failures with different error codes", async () => {
|
|
const counts: Record<string, number> = {};
|
|
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
|
|
(error: DecryptionFailureCode) =>
|
|
error === DecryptionFailureCode.UNKNOWN_ERROR ? "UnknownError" : "OlmKeysNotSentError",
|
|
false,
|
|
);
|
|
|
|
const decryptedEvent1 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.UNKNOWN_ERROR,
|
|
});
|
|
const decryptedEvent2 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
});
|
|
const decryptedEvent3 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
});
|
|
|
|
tracker.addVisibleEvent(decryptedEvent1);
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
tracker.addVisibleEvent(decryptedEvent3);
|
|
|
|
// One failure of UNKNOWN_ERROR, and effectively two for MEGOLM_UNKNOWN_INBOUND_SESSION_ID
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, decryptedEvent1, now);
|
|
eventDecrypted(tracker, decryptedEvent2, now);
|
|
eventDecrypted(tracker, decryptedEvent2, now);
|
|
eventDecrypted(tracker, decryptedEvent3, now);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
//expect(counts['UnknownError']).toBe(1, 'should track one UnknownError');
|
|
expect(counts["OlmKeysNotSentError"]).toBe(2);
|
|
});
|
|
|
|
it("should aggregate error codes correctly", async () => {
|
|
const counts: Record<string, number> = {};
|
|
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
|
|
(_errorCode: string) => "OlmUnspecifiedError",
|
|
false,
|
|
);
|
|
|
|
const decryptedEvent1 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
});
|
|
const decryptedEvent2 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
|
});
|
|
const decryptedEvent3 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.UNKNOWN_ERROR,
|
|
});
|
|
|
|
tracker.addVisibleEvent(decryptedEvent1);
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
tracker.addVisibleEvent(decryptedEvent3);
|
|
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, decryptedEvent1, now);
|
|
eventDecrypted(tracker, decryptedEvent2, now);
|
|
eventDecrypted(tracker, decryptedEvent3, now);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(counts["OlmUnspecifiedError"]).toBe(3);
|
|
});
|
|
|
|
it("should remap error codes correctly", async () => {
|
|
const counts: Record<string, number> = {};
|
|
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
|
|
(errorCode: string) => Array.from(errorCode).reverse().join(""),
|
|
false,
|
|
);
|
|
|
|
const decryptedEvent = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
|
});
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
eventDecrypted(tracker, decryptedEvent, Date.now());
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// should track remapped error code
|
|
expect(counts["XEDNI_EGASSEM_NWONKNU_MLO"]).toBe(1);
|
|
});
|
|
|
|
it("default error code mapper maps error codes correctly", async () => {
|
|
const errorCodes: string[] = [];
|
|
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string) => {
|
|
errorCodes.push(errorCode);
|
|
},
|
|
// @ts-ignore access to private member
|
|
DecryptionFailureTracker.instance.errorCodeMapFn,
|
|
false,
|
|
);
|
|
|
|
const now = Date.now();
|
|
|
|
const event1 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
});
|
|
tracker.addVisibleEvent(event1);
|
|
eventDecrypted(tracker, event1, now);
|
|
|
|
const event2 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
|
});
|
|
tracker.addVisibleEvent(event2);
|
|
eventDecrypted(tracker, event2, now);
|
|
|
|
const event3 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP,
|
|
});
|
|
tracker.addVisibleEvent(event3);
|
|
eventDecrypted(tracker, event3, now);
|
|
|
|
const event4 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
|
|
});
|
|
tracker.addVisibleEvent(event4);
|
|
eventDecrypted(tracker, event4, now);
|
|
|
|
const event5 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
|
|
});
|
|
tracker.addVisibleEvent(event5);
|
|
eventDecrypted(tracker, event5, now);
|
|
|
|
const event6 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
|
|
});
|
|
tracker.addVisibleEvent(event6);
|
|
eventDecrypted(tracker, event6, now);
|
|
|
|
const event7 = await createFailedDecryptionEvent({
|
|
code: DecryptionFailureCode.UNKNOWN_ERROR,
|
|
});
|
|
tracker.addVisibleEvent(event7);
|
|
eventDecrypted(tracker, event7, now);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(errorCodes).toEqual([
|
|
"OlmKeysNotSentError",
|
|
"OlmIndexError",
|
|
"HistoricalMessage",
|
|
"HistoricalMessage",
|
|
"HistoricalMessage",
|
|
"ExpectedDueToMembership",
|
|
"UnknownError",
|
|
]);
|
|
});
|
|
|
|
it("tracks late decryptions vs. undecryptable", async () => {
|
|
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
|
propertiesByErrorCode[errorCode] = properties;
|
|
},
|
|
(error: string) => error,
|
|
false,
|
|
);
|
|
|
|
// use three different errors so that we can distinguish the reports
|
|
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
|
|
const error2 = DecryptionFailureCode.MEGOLM_BAD_ROOM;
|
|
const error3 = DecryptionFailureCode.MEGOLM_MISSING_FIELDS;
|
|
|
|
// event that will be slow to decrypt
|
|
const lateDecryption = await createFailedDecryptionEvent({ code: error1 });
|
|
// event that will be so slow to decrypt, it gets counted as undecryptable
|
|
const veryLateDecryption = await createFailedDecryptionEvent({ code: error2 });
|
|
// event that never gets decrypted
|
|
const neverDecrypted = await createFailedDecryptionEvent({ code: error3 });
|
|
|
|
tracker.addVisibleEvent(lateDecryption);
|
|
tracker.addVisibleEvent(veryLateDecryption);
|
|
tracker.addVisibleEvent(neverDecrypted);
|
|
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, lateDecryption, now);
|
|
eventDecrypted(tracker, veryLateDecryption, now);
|
|
eventDecrypted(tracker, neverDecrypted, now);
|
|
|
|
await decryptExistingEvent(lateDecryption, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
await decryptExistingEvent(veryLateDecryption, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
eventDecrypted(tracker, lateDecryption, now + 40000);
|
|
eventDecrypted(tracker, veryLateDecryption, now + 100000);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(propertiesByErrorCode[error1].timeToDecryptMillis).toEqual(40000);
|
|
expect(propertiesByErrorCode[error2].timeToDecryptMillis).toEqual(-1);
|
|
expect(propertiesByErrorCode[error3].timeToDecryptMillis).toEqual(-1);
|
|
});
|
|
|
|
it("listens for client events", async () => {
|
|
// Test that the decryption failure tracker registers the right event
|
|
// handlers on start, and unregisters them when the client logs out.
|
|
const client = mockClient();
|
|
|
|
let errorCount: number = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
|
errorCount++;
|
|
},
|
|
(error: string) => error,
|
|
false,
|
|
);
|
|
|
|
// Calling .start will start some intervals. This test shouldn't run
|
|
// long enough for the timers to fire, but we'll use fake timers just
|
|
// to be safe.
|
|
jest.useFakeTimers();
|
|
await tracker.start(client);
|
|
|
|
// If the client fails to decrypt, it should get tracked
|
|
const failedDecryption = await createFailedDecryptionEvent();
|
|
client.emit(MatrixEventEvent.Decrypted, failedDecryption);
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(errorCount).toEqual(1);
|
|
|
|
client.emit(HttpApiEvent.SessionLoggedOut, {} as any);
|
|
|
|
// After the client has logged out, we shouldn't be listening to events
|
|
// any more, so even if the client emits an event regarding a failed
|
|
// decryption, we won't track it.
|
|
const anotherFailedDecryption = await createFailedDecryptionEvent();
|
|
client.emit(MatrixEventEvent.Decrypted, anotherFailedDecryption);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(errorCount).toEqual(1);
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it("tracks client information", async () => {
|
|
const client = mockClient();
|
|
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
|
propertiesByErrorCode[errorCode] = properties;
|
|
},
|
|
(error: string) => error,
|
|
false,
|
|
);
|
|
|
|
// @ts-ignore access to private method
|
|
await tracker.calculateClientProperties(client);
|
|
// @ts-ignore access to private method
|
|
await tracker.registerHandlers(client);
|
|
|
|
// use three different errors so that we can distinguish the reports
|
|
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
|
|
const error2 = DecryptionFailureCode.MEGOLM_BAD_ROOM;
|
|
const error3 = DecryptionFailureCode.MEGOLM_MISSING_FIELDS;
|
|
|
|
// event from a federated user (@alice:example.com)
|
|
const federatedDecryption = await createFailedDecryptionEvent({
|
|
code: error1,
|
|
});
|
|
// event from a local user
|
|
const localDecryption = await createFailedDecryptionEvent({
|
|
sender: "@bob:matrix.org",
|
|
code: error2,
|
|
});
|
|
|
|
tracker.addVisibleEvent(federatedDecryption);
|
|
tracker.addVisibleEvent(localDecryption);
|
|
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, federatedDecryption, now);
|
|
|
|
mocked(client.getCrypto()!.getUserVerificationStatus).mockResolvedValue(
|
|
new UserVerificationStatus(true, true, false),
|
|
);
|
|
client.emit(CryptoEvent.KeysChanged, {});
|
|
await sleep(100);
|
|
eventDecrypted(tracker, localDecryption, now);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
expect(propertiesByErrorCode[error1].isMatrixDotOrg).toBe(true);
|
|
expect(propertiesByErrorCode[error1].cryptoSDK).toEqual("Rust");
|
|
|
|
expect(propertiesByErrorCode[error1].isFederated).toBe(true);
|
|
expect(propertiesByErrorCode[error1].userTrustsOwnIdentity).toEqual(false);
|
|
expect(propertiesByErrorCode[error2].isFederated).toBe(false);
|
|
expect(propertiesByErrorCode[error2].userTrustsOwnIdentity).toEqual(true);
|
|
|
|
// change client params, and make sure the reports the right values
|
|
client.getDomain.mockReturnValue("example.com");
|
|
mocked(client.getCrypto()!.getVersion).mockReturnValue("Olm 0.0.0");
|
|
// @ts-ignore access to private method
|
|
await tracker.calculateClientProperties(client);
|
|
|
|
const anotherFailure = await createFailedDecryptionEvent({
|
|
code: error3,
|
|
});
|
|
tracker.addVisibleEvent(anotherFailure);
|
|
eventDecrypted(tracker, anotherFailure, now);
|
|
tracker.checkFailures(Infinity);
|
|
expect(propertiesByErrorCode[error3].isMatrixDotOrg).toBe(false);
|
|
expect(propertiesByErrorCode[error3].cryptoSDK).toEqual("Legacy");
|
|
});
|
|
|
|
it("keeps the original timestamp after repeated decryption failures", async () => {
|
|
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
|
|
|
let failure: ErrorProperties | undefined;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
|
failure = properties;
|
|
},
|
|
() => "UnknownError",
|
|
false,
|
|
);
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
|
|
const now = Date.now();
|
|
eventDecrypted(tracker, failedDecryptionEvent, now);
|
|
eventDecrypted(tracker, failedDecryptionEvent, now + 20000);
|
|
await decryptExistingEvent(failedDecryptionEvent, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
eventDecrypted(tracker, failedDecryptionEvent, now + 50000);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// the time to decrypt should be relative to the first time we failed
|
|
// to decrypt, not the second
|
|
expect(failure?.timeToDecryptMillis).toEqual(50000);
|
|
});
|
|
});
|
|
|
|
function mockClient(): MockedObject<MatrixClient> {
|
|
const client = mocked(stubClient());
|
|
const mockCrypto = {
|
|
getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"),
|
|
getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)),
|
|
} as unknown as Mocked<CryptoApi>;
|
|
client.getCrypto.mockReturnValue(mockCrypto);
|
|
|
|
// @ts-ignore
|
|
client.stopClient = jest.fn(() => {});
|
|
// @ts-ignore
|
|
client.removeAllListeners = jest.fn(() => {});
|
|
|
|
client.store = { destroy: jest.fn(() => {}) } as any;
|
|
|
|
return client;
|
|
}
|