mirror of https://github.com/vector-im/riot-web
448 lines
16 KiB
TypeScript
448 lines
16 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 { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
|
|
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
|
|
|
import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker";
|
|
|
|
async function createFailedDecryptionEvent(code?: DecryptionFailureCode) {
|
|
return await mkDecryptionFailureMatrixEvent({
|
|
roomId: "!room:id",
|
|
sender: "@alice:example.com",
|
|
code: code ?? DecryptionFailureCode.UNKNOWN_ERROR,
|
|
msg: ":(",
|
|
});
|
|
}
|
|
|
|
describe("DecryptionFailureTracker", function () {
|
|
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(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
tracker.eventDecrypted(failedDecryptionEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Immediately track the newest failures
|
|
tracker.trackFailures();
|
|
|
|
// 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(
|
|
DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
|
);
|
|
|
|
let count = 0;
|
|
let reportedRawCode = "";
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(total: number, _errCode: string, rawCode: string) => {
|
|
count += total;
|
|
reportedRawCode = rawCode;
|
|
},
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
tracker.eventDecrypted(failedDecryptionEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Immediately track the newest failures
|
|
tracker.trackFailures();
|
|
|
|
// 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(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.eventDecrypted(failedDecryptionEvent);
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Immediately track the newest failures
|
|
tracker.trackFailures();
|
|
|
|
// should track a failure for an event that failed decryption
|
|
expect(count).not.toBe(0);
|
|
});
|
|
|
|
it("does not track a failed decryption for an event that never becomes visible", async function () {
|
|
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.eventDecrypted(failedDecryptionEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Immediately track the newest failures
|
|
tracker.trackFailures();
|
|
|
|
// should not track a failure for an event that never became visible
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
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(
|
|
(_total: number) => {
|
|
// should not track an event that has since been decrypted correctly
|
|
expect(true).toBe(false);
|
|
},
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
// Indicate successful decryption.
|
|
await decryptExistingEvent(decryptedEvent, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
tracker.eventDecrypted(decryptedEvent, null);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Immediately track the newest failures
|
|
tracker.trackFailures();
|
|
});
|
|
|
|
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(
|
|
(_total: number) => {
|
|
// should not track an event that has since been decrypted correctly
|
|
expect(true).toBe(false);
|
|
},
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
// Indicate successful decryption.
|
|
await decryptExistingEvent(decryptedEvent, {
|
|
plainType: "m.room.message",
|
|
plainContent: { body: "success" },
|
|
});
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Immediately track the newest failures
|
|
tracker.trackFailures();
|
|
},
|
|
);
|
|
|
|
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(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Arbitrary number of failed decryptions for both events
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent2);
|
|
tracker.eventDecrypted(decryptedEvent2);
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
tracker.eventDecrypted(decryptedEvent2);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
// Simulated polling of `trackFailures`, an arbitrary number ( > 2 ) times
|
|
tracker.trackFailures();
|
|
tracker.trackFailures();
|
|
tracker.trackFailures();
|
|
tracker.trackFailures();
|
|
|
|
// 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(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Indicate decryption
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
tracker.trackFailures();
|
|
|
|
// Indicate a second decryption, after having tracked the failure
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
tracker.trackFailures();
|
|
|
|
// should only track a single failure per event
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it.skip("should not track a failure for an event that was tracked in a previous session", async () => {
|
|
// This test uses localStorage, clear it beforehand
|
|
localStorage.clear();
|
|
|
|
const decryptedEvent = await createFailedDecryptionEvent();
|
|
|
|
let count = 0;
|
|
// @ts-ignore access to private constructor
|
|
const tracker = new DecryptionFailureTracker(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
// Indicate decryption
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
// NB: This saves to localStorage specific to DFT
|
|
tracker.checkFailures(Infinity);
|
|
|
|
tracker.trackFailures();
|
|
|
|
// Simulate the browser refreshing by destroying tracker and creating a new tracker
|
|
// @ts-ignore access to private constructor
|
|
const secondTracker = new DecryptionFailureTracker(
|
|
(total: number) => (count += total),
|
|
() => "UnknownError",
|
|
);
|
|
|
|
secondTracker.addVisibleEvent(decryptedEvent);
|
|
|
|
//secondTracker.loadTrackedEvents();
|
|
|
|
secondTracker.eventDecrypted(decryptedEvent);
|
|
secondTracker.checkFailures(Infinity);
|
|
secondTracker.trackFailures();
|
|
|
|
// should only track a single failure per event
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
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(
|
|
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
|
(error: DecryptionFailureCode) =>
|
|
error === DecryptionFailureCode.UNKNOWN_ERROR ? "UnknownError" : "OlmKeysNotSentError",
|
|
);
|
|
|
|
const decryptedEvent1 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
|
const decryptedEvent2 = await createFailedDecryptionEvent(
|
|
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
);
|
|
const decryptedEvent3 = await createFailedDecryptionEvent(
|
|
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent1);
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
tracker.addVisibleEvent(decryptedEvent3);
|
|
|
|
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
|
|
tracker.eventDecrypted(decryptedEvent1);
|
|
tracker.eventDecrypted(decryptedEvent2);
|
|
tracker.eventDecrypted(decryptedEvent2);
|
|
tracker.eventDecrypted(decryptedEvent3);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
tracker.trackFailures();
|
|
|
|
//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(
|
|
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
|
(_errorCode: string) => "OlmUnspecifiedError",
|
|
);
|
|
|
|
const decryptedEvent1 = await createFailedDecryptionEvent(
|
|
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
|
);
|
|
const decryptedEvent2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
|
const decryptedEvent3 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent1);
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
tracker.addVisibleEvent(decryptedEvent3);
|
|
|
|
tracker.eventDecrypted(decryptedEvent1);
|
|
tracker.eventDecrypted(decryptedEvent2);
|
|
tracker.eventDecrypted(decryptedEvent3);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
tracker.trackFailures();
|
|
|
|
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(
|
|
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
|
(errorCode: string) => Array.from(errorCode).reverse().join(""),
|
|
);
|
|
|
|
const decryptedEvent = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
tracker.eventDecrypted(decryptedEvent);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
tracker.trackFailures();
|
|
|
|
// 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(
|
|
(total: number, errorCode: string) => {
|
|
errorCodes.push(errorCode);
|
|
},
|
|
// @ts-ignore access to private member
|
|
DecryptionFailureTracker.instance.errorCodeMapFn,
|
|
);
|
|
|
|
const event1 = await createFailedDecryptionEvent(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
|
|
tracker.addVisibleEvent(event1);
|
|
tracker.eventDecrypted(event1);
|
|
|
|
const event2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
|
tracker.addVisibleEvent(event2);
|
|
tracker.eventDecrypted(event2);
|
|
|
|
const event3 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
|
tracker.addVisibleEvent(event3);
|
|
tracker.eventDecrypted(event3);
|
|
|
|
const event4 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED);
|
|
tracker.addVisibleEvent(event4);
|
|
tracker.eventDecrypted(event4);
|
|
|
|
const event5 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
|
|
tracker.addVisibleEvent(event5);
|
|
tracker.eventDecrypted(event5);
|
|
|
|
const event6 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
|
|
tracker.addVisibleEvent(event6);
|
|
tracker.eventDecrypted(event6);
|
|
|
|
const event7 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
|
tracker.addVisibleEvent(event7);
|
|
tracker.eventDecrypted(event7);
|
|
|
|
// Pretend "now" is Infinity
|
|
tracker.checkFailures(Infinity);
|
|
|
|
tracker.trackFailures();
|
|
|
|
expect(errorCodes).toEqual([
|
|
"OlmKeysNotSentError",
|
|
"OlmIndexError",
|
|
"HistoricalMessage",
|
|
"HistoricalMessage",
|
|
"HistoricalMessage",
|
|
"ExpectedDueToMembership",
|
|
"UnknownError",
|
|
]);
|
|
});
|
|
});
|