Skip to content

Commit d96a873

Browse files
committed
Encrypt and decrypt by default when possible
1 parent 028635c commit d96a873

File tree

6 files changed

+95
-90
lines changed

6 files changed

+95
-90
lines changed

examples/encryption_bot.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {
2-
EncryptedRoomEvent,
32
EncryptionAlgorithm,
43
LogLevel,
54
LogService,
65
MatrixClient, MessageEvent,
7-
RichConsoleLogger, RichReply,
6+
RichConsoleLogger,
87
SimpleFsStorageProvider
98
} from "../src";
109
import { SqliteCryptoStorageProvider } from "../src/storage/SqliteCryptoStorageProvider";
@@ -52,23 +51,19 @@ const client = new MatrixClient(homeserverUrl, accessToken, storage, crypto);
5251
});
5352
}
5453

55-
client.on("room.event", async (roomId: string, event: any) => {
56-
if (roomId !== encryptedRoomId || event['type'] !== "m.room.encrypted") return;
54+
client.on("room.message", async (roomId: string, event: any) => {
55+
if (roomId !== encryptedRoomId) return;
5756

58-
try {
59-
const decrypted = await client.crypto.decryptRoomEvent(new EncryptedRoomEvent(event), roomId);
60-
if (decrypted.type === "m.room.message") {
61-
const message = new MessageEvent(decrypted.raw);
62-
if (message.messageType !== "m.text") return;
63-
if (message.textBody.startsWith("!ping")) {
64-
const reply = RichReply.createFor(roomId, message.raw, "Pong", "Pong");
65-
reply['msgtype'] = "m.notice";
66-
const encrypted = await client.crypto.encryptRoomEvent(roomId, "m.room.message", reply);
67-
await client.sendEvent(roomId, "m.room.encrypted", encrypted);
68-
}
69-
}
70-
} catch (e) {
71-
LogService.error("index", e);
57+
const message = new MessageEvent(event);
58+
59+
if (message.sender === (await client.getUserId())) {
60+
// yay, we decrypted our own message. Communicate that back for testing purposes.
61+
return await client.unstableApis.addReactionToEvent(roomId, message.eventId, '🔐');
62+
}
63+
64+
if (message.messageType !== "m.text") return;
65+
if (message.textBody.startsWith("!ping")) {
66+
await client.replyNotice(roomId, event, "Pong");
7267
}
7368
});
7469

src/MatrixClient.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from "./models/Crypto";
3838
import { requiresCrypto } from "./e2ee/decorators";
3939
import { ICryptoStorageProvider } from "./storage/ICryptoStorageProvider";
40+
import { EncryptedRoomEvent } from "./models/events/EncryptedRoomEvent";
4041

4142
/**
4243
* A client that is capable of interacting with a matrix homeserver.
@@ -799,6 +800,17 @@ export class MatrixClient extends EventEmitter {
799800

800801
for (let event of room['timeline']['events']) {
801802
event = await this.processEvent(event);
803+
if (event['type'] === 'm.room.encrypted' && await this.crypto?.isRoomEncrypted(roomId)) {
804+
await emitFn("room.encrypted_event", roomId, event);
805+
try {
806+
event = (await this.crypto.decryptRoomEvent(new EncryptedRoomEvent(event), roomId)).raw;
807+
event = await this.processEvent(event);
808+
await emitFn("room.decrypted_event", roomId, event);
809+
} catch (e) {
810+
LogService.error("MatrixClientLite", `Decryption error on ${roomId} ${event['event_id']}`, e);
811+
await emitFn("room.failed_decryption", roomId, event);
812+
}
813+
}
802814
if (event['type'] === 'm.room.message') {
803815
await emitFn("room.message", roomId, event);
804816
}
@@ -814,14 +826,29 @@ export class MatrixClient extends EventEmitter {
814826
}
815827
}
816828

829+
/**
830+
* Gets an event for a room. If the event is encrypted, and the client supports encryption,
831+
* and the room is encrypted, then this will return a decrypted event.
832+
* @param {string} roomId the room ID to get the event in
833+
* @param {string} eventId the event ID to look up
834+
* @returns {Promise<any>} resolves to the found event
835+
*/
836+
@timedMatrixClientFunctionCall()
837+
public async getEvent(roomId: string, eventId: string): Promise<any> {
838+
const event = await this.getRawEvent(roomId, eventId);
839+
if (event['type'] === 'm.room.encrypted' && await this.crypto?.isRoomEncrypted(roomId)) {
840+
return this.processEvent((await this.crypto.decryptRoomEvent(new EncryptedRoomEvent(event), roomId)).raw);
841+
}
842+
}
843+
817844
/**
818845
* Gets an event for a room. Returned as a raw event.
819846
* @param {string} roomId the room ID to get the event in
820847
* @param {string} eventId the event ID to look up
821848
* @returns {Promise<any>} resolves to the found event
822849
*/
823850
@timedMatrixClientFunctionCall()
824-
public getEvent(roomId: string, eventId: string): Promise<any> {
851+
public getRawEvent(roomId: string, eventId: string): Promise<any> {
825852
return this.doRequest("GET", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/event/" + encodeURIComponent(eventId))
826853
.then(ev => this.processEvent(ev));
827854
}
@@ -1030,6 +1057,7 @@ export class MatrixClient extends EventEmitter {
10301057

10311058
/**
10321059
* Replies to a given event with the given text. The event is sent with a msgtype of m.text.
1060+
* The message will be encrypted if the client supports encryption and the room is encrypted.
10331061
* @param {string} roomId the room ID to reply in
10341062
* @param {any} event the event to reply to
10351063
* @param {string} text the text to reply with
@@ -1046,6 +1074,7 @@ export class MatrixClient extends EventEmitter {
10461074

10471075
/**
10481076
* Replies to a given event with the given HTML. The event is sent with a msgtype of m.text.
1077+
* The message will be encrypted if the client supports encryption and the room is encrypted.
10491078
* @param {string} roomId the room ID to reply in
10501079
* @param {any} event the event to reply to
10511080
* @param {string} html the HTML to reply with.
@@ -1060,6 +1089,7 @@ export class MatrixClient extends EventEmitter {
10601089

10611090
/**
10621091
* Replies to a given event with the given text. The event is sent with a msgtype of m.notice.
1092+
* The message will be encrypted if the client supports encryption and the room is encrypted.
10631093
* @param {string} roomId the room ID to reply in
10641094
* @param {any} event the event to reply to
10651095
* @param {string} text the text to reply with
@@ -1077,6 +1107,7 @@ export class MatrixClient extends EventEmitter {
10771107

10781108
/**
10791109
* Replies to a given event with the given HTML. The event is sent with a msgtype of m.notice.
1110+
* The message will be encrypted if the client supports encryption and the room is encrypted.
10801111
* @param {string} roomId the room ID to reply in
10811112
* @param {any} event the event to reply to
10821113
* @param {string} html the HTML to reply with.
@@ -1091,7 +1122,8 @@ export class MatrixClient extends EventEmitter {
10911122
}
10921123

10931124
/**
1094-
* Sends a notice to the given room
1125+
* Sends a notice to the given room. The message will be encrypted if the client supports
1126+
* encryption and the room is encrypted.
10951127
* @param {string} roomId the room ID to send the notice to
10961128
* @param {string} text the text to send
10971129
* @returns {Promise<string>} resolves to the event ID that represents the message
@@ -1105,7 +1137,8 @@ export class MatrixClient extends EventEmitter {
11051137
}
11061138

11071139
/**
1108-
* Sends a notice to the given room with HTML content
1140+
* Sends a notice to the given room with HTML content. The message will be encrypted if the client supports
1141+
* encryption and the room is encrypted.
11091142
* @param {string} roomId the room ID to send the notice to
11101143
* @param {string} html the HTML to send
11111144
* @returns {Promise<string>} resolves to the event ID that represents the message
@@ -1121,7 +1154,8 @@ export class MatrixClient extends EventEmitter {
11211154
}
11221155

11231156
/**
1124-
* Sends a text message to the given room
1157+
* Sends a text message to the given room. The message will be encrypted if the client supports
1158+
* encryption and the room is encrypted.
11251159
* @param {string} roomId the room ID to send the text to
11261160
* @param {string} text the text to send
11271161
* @returns {Promise<string>} resolves to the event ID that represents the message
@@ -1135,7 +1169,8 @@ export class MatrixClient extends EventEmitter {
11351169
}
11361170

11371171
/**
1138-
* Sends a text message to the given room with HTML content
1172+
* Sends a text message to the given room with HTML content. The message will be encrypted if the client supports
1173+
* encryption and the room is encrypted.
11391174
* @param {string} roomId the room ID to send the text to
11401175
* @param {string} html the HTML to send
11411176
* @returns {Promise<string>} resolves to the event ID that represents the message
@@ -1151,7 +1186,8 @@ export class MatrixClient extends EventEmitter {
11511186
}
11521187

11531188
/**
1154-
* Sends a message to the given room
1189+
* Sends a message to the given room. The message will be encrypted if the client supports
1190+
* encryption and the room is encrypted.
11551191
* @param {string} roomId the room ID to send the message to
11561192
* @param {object} content the event content to send
11571193
* @returns {Promise<string>} resolves to the event ID that represents the message
@@ -1162,14 +1198,31 @@ export class MatrixClient extends EventEmitter {
11621198
}
11631199

11641200
/**
1165-
* Sends an event to the given room
1201+
* Sends an event to the given room. This will encrypt the event before sending if the room is
1202+
* encrypted and the client supports encryption. Use sendRawEvent() to avoid this behaviour.
11661203
* @param {string} roomId the room ID to send the event to
11671204
* @param {string} eventType the type of event to send
11681205
* @param {string} content the event body to send
11691206
* @returns {Promise<string>} resolves to the event ID that represents the event
11701207
*/
11711208
@timedMatrixClientFunctionCall()
11721209
public async sendEvent(roomId: string, eventType: string, content: any): Promise<string> {
1210+
if (await this.crypto?.isRoomEncrypted(roomId)) {
1211+
content = await this.crypto.encryptRoomEvent(roomId, eventType, content);
1212+
eventType = "m.room.encrypted";
1213+
}
1214+
return this.sendRawEvent(roomId, eventType, content);
1215+
}
1216+
1217+
/**
1218+
* Sends an event to the given room.
1219+
* @param {string} roomId the room ID to send the event to
1220+
* @param {string} eventType the type of event to send
1221+
* @param {string} content the event body to send
1222+
* @returns {Promise<string>} resolves to the event ID that represents the event
1223+
*/
1224+
@timedMatrixClientFunctionCall()
1225+
public async sendRawEvent(roomId: string, eventType: string, content: any): Promise<string> {
11731226
const txnId = (new Date().getTime()) + "__inc" + (++this.requestId);
11741227
return this.doRequest("PUT", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/send/" + encodeURIComponent(eventType) + "/" + encodeURIComponent(txnId), null, content).then(response => {
11751228
return response['event_id'];

src/e2ee/CryptoClient.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,9 @@ export class CryptoClient {
422422
throw new Error("Room is not encrypted");
423423
}
424424

425+
const relatesTo = JSON.parse(JSON.stringify(content['m.relates_to']));
426+
delete content['m.relates_to'];
427+
425428
const now = (new Date()).getTime();
426429

427430
let currentSession = await this.client.cryptoStore.getCurrentOutboundGroupSession(roomId);
@@ -448,7 +451,10 @@ export class CryptoClient {
448451
usesLeft: roomConfig.rotationPeriodMessages,
449452
expiresTs: now + roomConfig.rotationPeriodMs,
450453
};
451-
await this.client.cryptoStore.storeOutboundGroupSession(currentSession);
454+
455+
// Store the session as an inbound session up front. This is to ensure that we have the
456+
// earliest possible ratchet available to our own decryption functions. We don't store
457+
// the outbound session here as it is stored earlier on.
452458
await this.storeInboundGroupSession({
453459
room_id: roomId,
454460
session_id: newSession.session_id(),
@@ -496,13 +502,23 @@ export class CryptoClient {
496502
room_id: roomId,
497503
}));
498504

499-
return {
500-
algorithm: EncryptionAlgorithm.MegolmV1AesSha2,
505+
currentSession.pickled = session.pickle(this.pickleKey);
506+
currentSession.usesLeft--;
507+
await this.client.cryptoStore.storeOutboundGroupSession(currentSession);
508+
509+
const body = {
501510
sender_key: this.deviceCurve25519,
502511
ciphertext: encrypted,
503512
session_id: session.session_id(),
504513
device_id: this.clientDeviceId,
505514
};
515+
if (relatesTo) {
516+
body['m.relates_to'] = relatesTo;
517+
}
518+
return {
519+
...body,
520+
algorithm: EncryptionAlgorithm.MegolmV1AesSha2,
521+
};
506522
} finally {
507523
session.free();
508524
}
@@ -549,6 +565,9 @@ export class CryptoClient {
549565

550566
await this.client.cryptoStore.setMessageIndexForEvent(roomId, event.eventId, storedSession.sessionId, messageIndex);
551567

568+
storedSession.pickled = session.pickle(this.pickleKey);
569+
await this.client.cryptoStore.storeInboundGroupSession(storedSession);
570+
552571
return new RoomEvent<unknown>({
553572
...event.raw,
554573
type: eventBody.type || "io.t2bot.unknown",

src/storage/ICryptoStorageProvider.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,6 @@ export interface ICryptoStorageProvider {
133133
*/
134134
getCurrentOutboundGroupSession(roomId: string): Promise<IOutboundGroupSession>;
135135

136-
/**
137-
* Decrements the available usages for an outbound group session.
138-
* @param {string} sessionId The session ID.
139-
* @param {string} roomId The room ID.
140-
* @returns {Promise<void>} Resolves when complete.
141-
*/
142-
useOutboundGroupSession(sessionId: string, roomId: string): Promise<void>;
143-
144136
/**
145137
* Stores a session as sent to a user's device.
146138
* @param {IOutboundGroupSession} session The session that was sent.

src/storage/SqliteCryptoStorageProvider.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export class SqliteCryptoStorageProvider implements ICryptoStorageProvider {
2323
private obGroupSessionUpsert: Database.Statement;
2424
private obGroupSessionSelect: Database.Statement;
2525
private obGroupCurrentSessionSelect: Database.Statement;
26-
private obGroupSessionMarkUsage: Database.Statement;
2726
private obGroupSessionMarkAllInactive: Database.Statement;
2827
private obSentGroupSessionUpsert: Database.Statement;
2928
private obSentSelectLastSent: Database.Statement;
@@ -71,7 +70,6 @@ export class SqliteCryptoStorageProvider implements ICryptoStorageProvider {
7170
this.obGroupSessionUpsert = this.db.prepare("INSERT INTO outbound_group_sessions (session_id, room_id, current, pickled, uses_left, expires_ts) VALUES (@sessionId, @roomId, @current, @pickled, @usesLeft, @expiresTs) ON CONFLICT (session_id, room_id) DO UPDATE SET pickled = @pickled, current = @current, uses_left = @usesLeft, expires_ts = @expiresTs");
7271
this.obGroupSessionSelect = this.db.prepare("SELECT session_id, room_id, current, pickled, uses_left, expires_ts FROM outbound_group_sessions WHERE session_id = @sessionId AND room_id = @roomId");
7372
this.obGroupCurrentSessionSelect = this.db.prepare("SELECT session_id, room_id, current, pickled, uses_left, expires_ts FROM outbound_group_sessions WHERE room_id = @roomId AND current = 1");
74-
this.obGroupSessionMarkUsage = this.db.prepare("UPDATE outbound_group_sessions SET uses_left = uses_left - 1 WHERE session_id = @sessionId and room_id = @roomId");
7573
this.obGroupSessionMarkAllInactive = this.db.prepare("UPDATE outbound_group_sessions SET current = 0 WHERE room_id = @roomId");
7674

7775
this.obSentGroupSessionUpsert = this.db.prepare("INSERT INTO sent_outbound_group_sessions (session_id, room_id, session_index, user_id, device_id) VALUES (@sessionId, @roomId, @sessionIndex, @userId, @deviceId) ON CONFLICT (session_id, room_id, user_id, device_id, session_index) DO NOTHING");
@@ -220,10 +218,6 @@ export class SqliteCryptoStorageProvider implements ICryptoStorageProvider {
220218
return null;
221219
}
222220

223-
public async useOutboundGroupSession(sessionId: string, roomId: string): Promise<void> {
224-
this.obGroupSessionMarkUsage.run({sessionId: sessionId, roomId: roomId});
225-
}
226-
227221
public async storeSentOutboundGroupSession(session: IOutboundGroupSession, index: number, device: UserDevice): Promise<void> {
228222
this.obSentGroupSessionUpsert.run({
229223
sessionId: session.sessionId,

test/storage/SqliteCryptoStorageProvider.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -341,54 +341,6 @@ describe('SqliteCryptoStorageProvider', () => {
341341
await store.close();
342342
});
343343

344-
it('should count usages of outbound sessions', async () => {
345-
const sessionId = "session";
346-
const roomId = "!room:example.org";
347-
const usesLeft = 100;
348-
const expiresTs = Date.now();
349-
const pickle = "pickled";
350-
351-
const name = tmp.fileSync().name;
352-
let store = new SqliteCryptoStorageProvider(name);
353-
354-
await store.storeOutboundGroupSession({
355-
sessionId: sessionId,
356-
roomId: roomId,
357-
pickled: pickle,
358-
expiresTs: expiresTs,
359-
usesLeft: usesLeft,
360-
isCurrent: true,
361-
});
362-
expect(await store.getOutboundGroupSession(sessionId, roomId)).toMatchObject({
363-
sessionId: sessionId,
364-
roomId: roomId,
365-
pickled: pickle,
366-
expiresTs: expiresTs,
367-
usesLeft: usesLeft,
368-
isCurrent: true,
369-
});
370-
await store.useOutboundGroupSession(sessionId, roomId);
371-
expect(await store.getOutboundGroupSession(sessionId, roomId)).toMatchObject({
372-
sessionId: sessionId,
373-
roomId: roomId,
374-
pickled: pickle,
375-
expiresTs: expiresTs,
376-
usesLeft: usesLeft - 1,
377-
isCurrent: true,
378-
});
379-
await store.close();
380-
store = new SqliteCryptoStorageProvider(name);
381-
expect(await store.getOutboundGroupSession(sessionId, roomId)).toMatchObject({
382-
sessionId: sessionId,
383-
roomId: roomId,
384-
pickled: pickle,
385-
expiresTs: expiresTs,
386-
usesLeft: usesLeft - 1,
387-
isCurrent: true,
388-
});
389-
await store.close();
390-
});
391-
392344
it('should track sent outbound sessions', async () => {
393345
const sessionId = "session";
394346
const roomId = "!room:example.org";

0 commit comments

Comments
 (0)