Skip to content

Commit cf86e72

Browse files
author
Germain
authored
Merge branch 'develop' into gsouquet/better-getreadupto
2 parents 2eef226 + 896f622 commit cf86e72

File tree

14 files changed

+260
-158
lines changed

14 files changed

+260
-158
lines changed

spec/unit/crypto/verification/setDeviceVerification.spec.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

spec/unit/models/event.spec.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ limitations under the License.
1616

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

2221
describe("MatrixEvent", () => {
@@ -88,22 +87,6 @@ describe("MatrixEvent", () => {
8887
expect(ev.getWireContent().ciphertext).toBeUndefined();
8988
});
9089

91-
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
92-
const ev = new MatrixEvent({
93-
type: EventType.RoomMessageEncrypted,
94-
content: {
95-
body: "Test",
96-
},
97-
event_id: "$event1:server",
98-
});
99-
await ev.attemptDecryption({
100-
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
101-
} as unknown as Crypto);
102-
expect(ev.isEncrypted()).toBeTruthy();
103-
expect(ev.isBeingDecrypted()).toBeFalsy();
104-
expect(ev.isDecryptionFailure()).toBeFalsy();
105-
});
106-
10790
describe("applyVisibilityEvent", () => {
10891
it("should emit VisibilityChange if a change was made", async () => {
10992
const ev = new MatrixEvent({
@@ -134,6 +117,21 @@ describe("MatrixEvent", () => {
134117
});
135118
});
136119

120+
it("should report decryption errors", async () => {
121+
const crypto = {
122+
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
123+
} as unknown as Crypto;
124+
125+
await encryptedEvent.attemptDecryption(crypto);
126+
expect(encryptedEvent.isEncrypted()).toBeTruthy();
127+
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
128+
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
129+
expect(encryptedEvent.getContent()).toEqual({
130+
msgtype: "m.bad.encrypted",
131+
body: "** Unable to decrypt: Error: test error **",
132+
});
133+
});
134+
137135
it("should retry decryption if a retry is queued", async () => {
138136
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
139137

spec/unit/relations.spec.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,21 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { M_POLL_START } from "matrix-events-sdk";
18+
1719
import { EventTimelineSet } from "../../src/models/event-timeline-set";
1820
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
1921
import { Room } from "../../src/models/room";
20-
import { Relations } from "../../src/models/relations";
22+
import { Relations, RelationsEvent } from "../../src/models/relations";
2123
import { TestClient } from "../TestClient";
24+
import { RelationType } from "../../src";
25+
import { logger } from "../../src/logger";
2226

2327
describe("Relations", function () {
28+
afterEach(() => {
29+
jest.spyOn(logger, "error").mockRestore();
30+
});
31+
2432
it("should deduplicate annotations", function () {
2533
const room = new Room("room123", null!, null!);
2634
const relations = new Relations("m.annotation", "m.reaction", room);
@@ -75,6 +83,92 @@ describe("Relations", function () {
7583
}
7684
});
7785

86+
describe("addEvent()", () => {
87+
const relationType = RelationType.Reference;
88+
const eventType = M_POLL_START.stable!;
89+
const altEventTypes = [M_POLL_START.unstable!];
90+
const room = new Room("room123", null!, null!);
91+
92+
it("should not add events without a relation", async () => {
93+
// dont pollute console
94+
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
95+
const relations = new Relations(relationType, eventType, room);
96+
const emitSpy = jest.spyOn(relations, "emit");
97+
const event = new MatrixEvent({ type: eventType });
98+
99+
await relations.addEvent(event);
100+
expect(logSpy).toHaveBeenCalledWith("Event must have relation info");
101+
// event not added
102+
expect(relations.getRelations().length).toBe(0);
103+
expect(emitSpy).not.toHaveBeenCalled();
104+
});
105+
106+
it("should not add events of incorrect event type", async () => {
107+
// dont pollute console
108+
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
109+
const relations = new Relations(relationType, eventType, room);
110+
const emitSpy = jest.spyOn(relations, "emit");
111+
const event = new MatrixEvent({
112+
type: "different-event-type",
113+
content: {
114+
"m.relates_to": {
115+
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
116+
rel_type: relationType,
117+
},
118+
},
119+
});
120+
121+
await relations.addEvent(event);
122+
123+
expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
124+
// event not added
125+
expect(relations.getRelations().length).toBe(0);
126+
expect(emitSpy).not.toHaveBeenCalled();
127+
});
128+
129+
it("adds events that match alt event types", async () => {
130+
const relations = new Relations(relationType, eventType, room, altEventTypes);
131+
const emitSpy = jest.spyOn(relations, "emit");
132+
const event = new MatrixEvent({
133+
type: M_POLL_START.unstable!,
134+
content: {
135+
"m.relates_to": {
136+
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
137+
rel_type: relationType,
138+
},
139+
},
140+
});
141+
142+
await relations.addEvent(event);
143+
144+
// event added
145+
expect(relations.getRelations()).toEqual([event]);
146+
expect(emitSpy).toHaveBeenCalledWith(RelationsEvent.Add, event);
147+
});
148+
149+
it("should not add events of incorrect relation type", async () => {
150+
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
151+
const relations = new Relations(relationType, eventType, room);
152+
const event = new MatrixEvent({
153+
type: eventType,
154+
content: {
155+
"m.relates_to": {
156+
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
157+
rel_type: "m.annotation",
158+
},
159+
},
160+
});
161+
162+
await relations.addEvent(event);
163+
const emitSpy = jest.spyOn(relations, "emit");
164+
165+
expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
166+
// event not added
167+
expect(relations.getRelations().length).toBe(0);
168+
expect(emitSpy).not.toHaveBeenCalled();
169+
});
170+
});
171+
78172
it("should emit created regardless of ordering", async function () {
79173
const targetEvent = new MatrixEvent({
80174
sender: "@bob:example.com",

spec/unit/rust-crypto.spec.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import {
2222
KeysClaimRequest,
2323
KeysQueryRequest,
2424
KeysUploadRequest,
25+
OlmMachine,
2526
SignatureUploadRequest,
2627
} from "@matrix-org/matrix-sdk-crypto-js";
2728
import { Mocked } from "jest-mock";
2829
import MockHttpBackend from "matrix-mock-request";
2930

3031
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
3132
import { initRustCrypto } from "../../src/rust-crypto";
32-
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi } from "../../src";
33+
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
3334
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
3435

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

4950
beforeEach(async () => {
50-
const mockHttpApi = {} as MatrixHttpApi<IHttpOpts>;
51+
const mockHttpApi = {} as MatrixClient["http"];
5152
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
5253
});
5354

@@ -57,6 +58,47 @@ describe("RustCrypto", () => {
5758
});
5859
});
5960

61+
describe("to-device messages", () => {
62+
let rustCrypto: RustCrypto;
63+
64+
beforeEach(async () => {
65+
const mockHttpApi = {} as MatrixClient["http"];
66+
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
67+
});
68+
69+
it("should pass through unencrypted to-device messages", async () => {
70+
const inputs: IToDeviceEvent[] = [
71+
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
72+
];
73+
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
74+
expect(res).toEqual(inputs);
75+
});
76+
77+
it("should pass through bad encrypted messages", async () => {
78+
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
79+
const keys = olmMachine.identityKeys;
80+
const inputs: IToDeviceEvent[] = [
81+
{
82+
type: "m.room.encrypted",
83+
content: {
84+
algorithm: "m.olm.v1.curve25519-aes-sha2",
85+
sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
86+
ciphertext: {
87+
[keys.curve25519.toBase64()]: {
88+
type: 0,
89+
body: "ajyjlghi",
90+
},
91+
},
92+
},
93+
sender: "@alice:example.com",
94+
},
95+
];
96+
97+
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
98+
expect(res).toEqual(inputs);
99+
});
100+
});
101+
60102
describe("outgoing requests", () => {
61103
/** the RustCrypto implementation under test */
62104
let rustCrypto: RustCrypto;
@@ -90,6 +132,7 @@ describe("RustCrypto", () => {
90132
baseUrl: "https://example.com",
91133
prefix: "/_matrix",
92134
fetchFn: httpBackend.fetchFn as typeof global.fetch,
135+
onlyData: true,
93136
});
94137

95138
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that

src/common-crypto/CryptoBackend.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

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

2021
/**
@@ -74,6 +75,20 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
7475

7576
/** The methods which crypto implementations should expose to the Sync api */
7677
export interface SyncCryptoCallbacks {
78+
/**
79+
* Called by the /sync loop whenever there are incoming to-device messages.
80+
*
81+
* The implementation may preprocess the received messages (eg, decrypt them) and return an
82+
* updated list of messages for dispatch to the rest of the system.
83+
*
84+
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
85+
* messages, rather than the results of any decryption attempts.
86+
*
87+
* @param events - the received to-device messages
88+
* @returns A list of preprocessed to-device messages.
89+
*/
90+
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
91+
7792
/**
7893
* Called by the /sync loop after each /sync response is processed.
7994
*

src/crypto/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import { CryptoStore } from "./store/base";
8585
import { IVerificationChannel } from "./verification/request/Channel";
8686
import { TypedEventEmitter } from "../models/typed-event-emitter";
8787
import { IContent } from "../models/event";
88-
import { ISyncResponse } from "../sync-accumulator";
88+
import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
8989
import { ISignatures } from "../@types/signed";
9090
import { IMessage } from "./algorithms/olm";
9191
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
@@ -2225,9 +2225,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
22252225
await upload({ shouldEmit: true });
22262226
// XXX: we'll need to wait for the device list to be updated
22272227
}
2228-
2229-
// redo key requests after verification
2230-
this.cancelAndResendAllOutgoingKeyRequests();
22312228
}
22322229

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

3198+
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
3199+
// all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
3200+
// happens later in decryptEvent, via the EventMapper
3201+
return events.filter((toDevice) => {
3202+
if (
3203+
toDevice.type === EventType.RoomMessageEncrypted &&
3204+
!["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
3205+
) {
3206+
logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
3207+
return false;
3208+
}
3209+
return true;
3210+
});
3211+
}
3212+
32013213
private onToDeviceEvent = (event: MatrixEvent): void => {
32023214
try {
32033215
logger.log(

src/event-mapper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
6060
event.setThread(thread);
6161
}
6262

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

0 commit comments

Comments
 (0)