Skip to content

Commit

Permalink
Merge branch 'develop' into gsouquet/better-getreadupto
Browse files Browse the repository at this point in the history
  • Loading branch information
Germain authored and germain-gg committed Jan 6, 2023
2 parents 2eef226 + 896f622 commit 1779fbb
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 154 deletions.
56 changes: 0 additions & 56 deletions spec/unit/crypto/verification/setDeviceVerification.spec.ts

This file was deleted.

32 changes: 15 additions & 17 deletions spec/unit/models/event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.

import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { EventType } from "../../../src";
import { Crypto } from "../../../src/crypto";

describe("MatrixEvent", () => {
Expand Down Expand Up @@ -88,22 +87,6 @@ describe("MatrixEvent", () => {
expect(ev.getWireContent().ciphertext).toBeUndefined();
});

it("should abort decryption if fails with an error other than a DecryptionError", async () => {
const ev = new MatrixEvent({
type: EventType.RoomMessageEncrypted,
content: {
body: "Test",
},
event_id: "$event1:server",
});
await ev.attemptDecryption({
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
} as unknown as Crypto);
expect(ev.isEncrypted()).toBeTruthy();
expect(ev.isBeingDecrypted()).toBeFalsy();
expect(ev.isDecryptionFailure()).toBeFalsy();
});

describe("applyVisibilityEvent", () => {
it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({
Expand Down Expand Up @@ -134,6 +117,21 @@ describe("MatrixEvent", () => {
});
});

it("should report decryption errors", async () => {
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
} as unknown as Crypto;

await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: Error: test error **",
});
});

it("should retry decryption if a retry is queued", async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");

Expand Down
96 changes: 95 additions & 1 deletion spec/unit/relations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { M_POLL_START } from "matrix-events-sdk";

import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations";
import { Relations, RelationsEvent } from "../../src/models/relations";
import { TestClient } from "../TestClient";
import { RelationType } from "../../src";
import { logger } from "../../src/logger";

describe("Relations", function () {
afterEach(() => {
jest.spyOn(logger, "error").mockRestore();
});

it("should deduplicate annotations", function () {
const room = new Room("room123", null!, null!);
const relations = new Relations("m.annotation", "m.reaction", room);
Expand Down Expand Up @@ -75,6 +83,92 @@ describe("Relations", function () {
}
});

describe("addEvent()", () => {
const relationType = RelationType.Reference;
const eventType = M_POLL_START.stable!;
const altEventTypes = [M_POLL_START.unstable!];
const room = new Room("room123", null!, null!);

it("should not add events without a relation", async () => {
// dont pollute console
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
const relations = new Relations(relationType, eventType, room);
const emitSpy = jest.spyOn(relations, "emit");
const event = new MatrixEvent({ type: eventType });

await relations.addEvent(event);
expect(logSpy).toHaveBeenCalledWith("Event must have relation info");
// event not added
expect(relations.getRelations().length).toBe(0);
expect(emitSpy).not.toHaveBeenCalled();
});

it("should not add events of incorrect event type", async () => {
// dont pollute console
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
const relations = new Relations(relationType, eventType, room);
const emitSpy = jest.spyOn(relations, "emit");
const event = new MatrixEvent({
type: "different-event-type",
content: {
"m.relates_to": {
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
rel_type: relationType,
},
},
});

await relations.addEvent(event);

expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
// event not added
expect(relations.getRelations().length).toBe(0);
expect(emitSpy).not.toHaveBeenCalled();
});

it("adds events that match alt event types", async () => {
const relations = new Relations(relationType, eventType, room, altEventTypes);
const emitSpy = jest.spyOn(relations, "emit");
const event = new MatrixEvent({
type: M_POLL_START.unstable!,
content: {
"m.relates_to": {
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
rel_type: relationType,
},
},
});

await relations.addEvent(event);

// event added
expect(relations.getRelations()).toEqual([event]);
expect(emitSpy).toHaveBeenCalledWith(RelationsEvent.Add, event);
});

it("should not add events of incorrect relation type", async () => {
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
const relations = new Relations(relationType, eventType, room);
const event = new MatrixEvent({
type: eventType,
content: {
"m.relates_to": {
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
rel_type: "m.annotation",
},
},
});

await relations.addEvent(event);
const emitSpy = jest.spyOn(relations, "emit");

expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
// event not added
expect(relations.getRelations().length).toBe(0);
expect(emitSpy).not.toHaveBeenCalled();
});
});

it("should emit created regardless of ordering", async function () {
const targetEvent = new MatrixEvent({
sender: "@bob:example.com",
Expand Down
47 changes: 45 additions & 2 deletions spec/unit/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ import {
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
OlmMachine,
SignatureUploadRequest,
} from "@matrix-org/matrix-sdk-crypto-js";
import { Mocked } from "jest-mock";
import MockHttpBackend from "matrix-mock-request";

import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../src/rust-crypto";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi } from "../../src";
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";

afterEach(() => {
Expand All @@ -47,7 +48,7 @@ describe("RustCrypto", () => {
let rustCrypto: RustCrypto;

beforeEach(async () => {
const mockHttpApi = {} as MatrixHttpApi<IHttpOpts>;
const mockHttpApi = {} as MatrixClient["http"];
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
});

Expand All @@ -57,6 +58,47 @@ describe("RustCrypto", () => {
});
});

describe("to-device messages", () => {
let rustCrypto: RustCrypto;

beforeEach(async () => {
const mockHttpApi = {} as MatrixClient["http"];
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
});

it("should pass through unencrypted to-device messages", async () => {
const inputs: IToDeviceEvent[] = [
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
];
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
expect(res).toEqual(inputs);
});

it("should pass through bad encrypted messages", async () => {
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
const keys = olmMachine.identityKeys;
const inputs: IToDeviceEvent[] = [
{
type: "m.room.encrypted",
content: {
algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
ciphertext: {
[keys.curve25519.toBase64()]: {
type: 0,
body: "ajyjlghi",
},
},
},
sender: "@alice:example.com",
},
];

const res = await rustCrypto.preprocessToDeviceMessages(inputs);
expect(res).toEqual(inputs);
});
});

describe("outgoing requests", () => {
/** the RustCrypto implementation under test */
let rustCrypto: RustCrypto;
Expand Down Expand Up @@ -90,6 +132,7 @@ describe("RustCrypto", () => {
baseUrl: "https://example.com",
prefix: "/_matrix",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
onlyData: true,
});

// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
Expand Down
15 changes: 15 additions & 0 deletions src/common-crypto/CryptoBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import type { IToDeviceEvent } from "../sync-accumulator";
import { MatrixEvent } from "../models/event";

/**
Expand Down Expand Up @@ -74,6 +75,20 @@ export interface CryptoBackend extends SyncCryptoCallbacks {

/** The methods which crypto implementations should expose to the Sync api */
export interface SyncCryptoCallbacks {
/**
* Called by the /sync loop whenever there are incoming to-device messages.
*
* The implementation may preprocess the received messages (eg, decrypt them) and return an
* updated list of messages for dispatch to the rest of the system.
*
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
* messages, rather than the results of any decryption attempts.
*
* @param events - the received to-device messages
* @returns A list of preprocessed to-device messages.
*/
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;

/**
* Called by the /sync loop after each /sync response is processed.
*
Expand Down
20 changes: 16 additions & 4 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IContent } from "../models/event";
import { ISyncResponse } from "../sync-accumulator";
import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm";
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
Expand Down Expand Up @@ -2225,9 +2225,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await upload({ shouldEmit: true });
// XXX: we'll need to wait for the device list to be updated
}

// redo key requests after verification
this.cancelAndResendAllOutgoingKeyRequests();
}

const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
Expand Down Expand Up @@ -3198,6 +3195,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
};

public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
// all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
// happens later in decryptEvent, via the EventMapper
return events.filter((toDevice) => {
if (
toDevice.type === EventType.RoomMessageEncrypted &&
!["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
) {
logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
return false;
}
return true;
});
}

private onToDeviceEvent = (event: MatrixEvent): void => {
try {
logger.log(
Expand Down
3 changes: 3 additions & 0 deletions src/event-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
event.setThread(thread);
}

// TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than
// to-device events), because the rust implementation decrypts to-device messages at a higher level.
// Generally we probably want to use a different eventMapper implementation for to-device events because
if (event.isEncrypted()) {
if (!preventReEmit) {
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
Expand Down
Loading

0 comments on commit 1779fbb

Please sign in to comment.