commit 882942f9737bf5f96965f3160bd5f2ad4e4cdcbd
parent 990c77891408340e172f391166e443326b393097
Author: MTRNord <mtrnord1@gmail.com>
Date: Fri, 5 May 2023 17:48:40 +0200
Implement E2EE decryption
Diffstat:
5 files changed, 141 insertions(+), 34 deletions(-)
diff --git a/src/app/sdk/api/apiTypes.ts b/src/app/sdk/api/apiTypes.ts
@@ -182,7 +182,26 @@ export interface ISlidingSyncResp {
txn_id: string;
}
-export interface Extensions { }
+export interface Extensions {
+ e2ee?: E2EEExtension;
+ to_device?: ToDeviceExtension;
+}
+
+export interface E2EEExtension {
+ device_one_time_keys_count?: {
+ [key: string]: number;
+ };
+ device_lists?: {
+ changed?: string[];
+ left?: string[];
+ };
+ device_unused_fallback_key_types?: string[];
+}
+
+export interface ToDeviceExtension {
+ next_batch: string;
+ events?: any[];
+}
export interface List {
ops?: (SYNC_OP | INSERT_OP | INVALIDATE_OP | DELETE_OP)[];
diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts
@@ -27,7 +27,7 @@ import {
IDBPDatabase,
openDB
} from "idb";
-import { DeviceId, KeysBackupRequest, KeysUploadRequest, OlmMachine, RequestType, RoomMessageRequest, SignatureUploadRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
+import { DeviceId, DeviceLists, KeysBackupRequest, KeysUploadRequest, OlmMachine, RequestType, RoomMessageRequest, SignatureUploadRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
import { KeysQueryRequest } from "@matrix-org/matrix-sdk-crypto-js";
import { KeysClaimRequest } from "@matrix-org/matrix-sdk-crypto-js";
import { ToDeviceRequest } from "@matrix-org/matrix-sdk-crypto-js";
@@ -91,6 +91,7 @@ interface MatrixDB extends DBSchema {
initialSync: boolean;
lastRanges?: { [key: string]: number[][] }; // [start, end]
lastTxnID?: string;
+ to_device_since?: string;
};
// User ID
key: string;
@@ -116,7 +117,8 @@ export class MatrixClient extends EventEmitter {
private profileInfo?: IProfileInfo;
private lastRanges?: { [key: string]: number[][] };
private lastTxnID?: string;
- private olmMachine?: OlmMachine;
+ private to_device_since?: string;
+ public olmMachine?: OlmMachine;
public get isLoggedIn(): boolean {
return this.access_token !== undefined;
@@ -156,6 +158,8 @@ export class MatrixClient extends EventEmitter {
instance.initialSync = syncInfo.initialSync;
instance.lastRanges = syncInfo.lastRanges;
instance.lastTxnID = syncInfo.lastTxnID;
+ console.log("to_device_since:", syncInfo.to_device_since)
+ instance.to_device_since = syncInfo.to_device_since;
}
// Load rooms
@@ -193,20 +197,22 @@ export class MatrixClient extends EventEmitter {
}
private async createDatabase() {
- this.database = await openDB<MatrixDB>("matrix", 3, {
- upgrade(db) {
- if (db.objectStoreNames.contains("rooms")) {
- db.deleteObjectStore("rooms");
- }
- //if (db.objectStoreNames.contains("loginInfo")) {
- // db.deleteObjectStore("loginInfo");
- //}
- if (db.objectStoreNames.contains("syncInfo")) {
- db.deleteObjectStore("syncInfo");
+ this.database = await openDB<MatrixDB>("matrix", 4, {
+ upgrade(db, oldVersion) {
+ if (oldVersion < 1) {
+ if (db.objectStoreNames.contains("rooms")) {
+ db.deleteObjectStore("rooms");
+ }
+ //if (db.objectStoreNames.contains("loginInfo")) {
+ // db.deleteObjectStore("loginInfo");
+ //}
+ if (db.objectStoreNames.contains("syncInfo")) {
+ db.deleteObjectStore("syncInfo");
+ }
+ db.createObjectStore('rooms', { keyPath: 'roomID' });
+ db.createObjectStore('loginInfo', { keyPath: 'userId' });
+ db.createObjectStore('syncInfo', { keyPath: 'userId' });
}
- db.createObjectStore('rooms', { keyPath: 'roomID' });
- db.createObjectStore('loginInfo', { keyPath: 'userId' });
- db.createObjectStore('syncInfo', { keyPath: 'userId' });
}
});
}
@@ -574,6 +580,7 @@ export class MatrixClient extends EventEmitter {
url = `${this.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=30000&pos=${this.syncPos}`
}
+ console.log("to_device_since:", this.to_device_since)
const resp = await fetch(url, {
method: "POST",
headers: {
@@ -641,6 +648,11 @@ export class MatrixClient extends EventEmitter {
extensions: {
e2ee: {
enabled: true,
+ },
+ to_device: {
+ enabled: true,
+ // TODO: if not initial sync add since token
+ since: this.to_device_since || null
}
}
})
@@ -666,6 +678,26 @@ export class MatrixClient extends EventEmitter {
const json = await resp.json() as ISlidingSyncResp;
this.syncPos = json.pos;
+ if (json.extensions?.to_device) {
+ console.log("Processing to_device events")
+ await this.olmMachine?.receiveSyncChanges(
+ JSON.stringify(json.extensions.to_device.events || []),
+ new DeviceLists(
+ json.extensions.e2ee?.device_lists?.changed?.map(
+ user_id => new UserId(user_id)
+ ),
+ json.extensions.e2ee?.device_lists?.left?.map(
+ user_id => new UserId(user_id)
+ )
+ ),
+ new Map(Object.entries(json.extensions.e2ee?.device_one_time_keys_count || [])),
+ new Set(json.extensions.e2ee?.device_unused_fallback_key_types)
+ );
+ this.to_device_since = json.extensions.to_device.next_batch;
+ }
+
+ await this.sendIdentifyAndOneTimeKeys();
+
const syncInfoTX = this.database?.transaction('syncInfo', 'readwrite');
await syncInfoTX?.store.put({
@@ -674,6 +706,7 @@ export class MatrixClient extends EventEmitter {
initialSync: this.initialSync,
lastRanges: this.lastRanges,
lastTxnID: this.lastTxnID,
+ to_device_since: this.to_device_since,
});
await syncInfoTX?.done;
@@ -888,6 +921,14 @@ export class MatrixClient extends EventEmitter {
if (state_events) {
roomObj.addStateEvents(state_events);
}
+ if (required_state || state_events) {
+ if (roomObj.isEncrypted()) {
+ const joinEvents = [...(required_state || []), ...(state_events || [])]
+ .filter(event => event.type === "m.room.member" && event.content.membership === "join");
+ const memberIds = joinEvents.map(event => new UserId(event.state_key));
+ await this.olmMachine?.updateTrackedUsers(memberIds);
+ }
+ }
if (is_dm) {
roomObj.setDM(is_dm);
}
diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts
@@ -185,4 +185,14 @@ export class Room {
});
return avatarURL;
}
+
+ public isEncrypted(): boolean {
+ let isEncrypted: boolean = false;
+ this.stateEvents.forEach((event) => {
+ if (event.type === "m.room.encryption" && event.content.algorithm === "m.megolm.v1.aes-sha2" && event.state_key === "") {
+ isEncrypted = true;
+ }
+ });
+ return isEncrypted;
+ }
}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
@@ -1,10 +1,15 @@
/// <reference types="./@types/global.d.ts" />
-import { initAsync, start } from "@matrix-org/matrix-sdk-crypto-js";
+import { LoggerLevel, Tracing, initAsync, start } from "@matrix-org/matrix-sdk-crypto-js";
import React from 'react'
import { createRoot } from 'react-dom/client';
import './index.scss';
initAsync().then(() => {
+ if (Tracing.isAvailable()) {
+ console.log("Tracing is available, turning on");
+ let tracing = new Tracing(LoggerLevel.Trace);
+ tracing.turnOn();
+ }
start();
import('./app/sdk/client').then(({ MatrixContext, defaultMatrixClient }) => {
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
@@ -15,6 +15,7 @@ import UnknownEvent from '../components/events/unknownEvent';
import MemberEvent from '../components/events/memberEvent';
import { IRoomEvent, IRoomMemberEvent } from '../app/sdk/api/apiTypes';
import Linkify from 'linkify-react';
+import { RoomId } from '@matrix-org/matrix-sdk-crypto-js';
type ChatViewProps = {
/**
@@ -34,8 +35,56 @@ type ChatViewProps = {
const ChatView: FC<ChatViewProps> = memo(({ roomID, scrollRef }) => {
const room = useRoom(decodeURIComponent(roomID || ""));
+ const client = useContext(MatrixContext);
const events = room?.getEvents();
const { pathname } = useLocation();
+ const [renderedEvents, setRenderedEvents] = useState<JSX.Element[]>([]);
+
+ // Map events to components but also tell components if the previous event was from the same sender and which type it was
+ const renderEvents = (events: IRoomEvent[]) => {
+ const dedupedEvents = events?.filter((event, index, self) => {
+ return self.findIndex(e => e.event_id === event.event_id) === index;
+ });
+
+
+ Promise.all(dedupedEvents?.filter(event => event.type !== "m.reaction").map(async (event, index) => {
+ const previousEvent = dedupedEvents?.filter(event => event.type !== "m.reaction")[index - 1];
+ const previousEventIsFromSameSender = previousEvent?.sender === event.sender;
+ let previousEventType = previousEvent?.type;
+
+ // Make a list of events which are reactions for the current event we want to render
+ const reactions = dedupedEvents?.filter((e) => {
+ return e.type === "m.reaction" && e.content["m.relates_to"].event_id === event.event_id;
+ });
+
+ // Decrypt the event if it is encrypte
+ if (event.type === "m.room.encrypted") {
+ try {
+ const decrypted_event = await client.olmMachine?.decryptRoomEvent(JSON.stringify(event), new RoomId(roomID || ""));
+ if (decrypted_event) {
+ event = JSON.parse(decrypted_event.event) as IRoomEvent;
+ previousEventType = event.type;
+ } else {
+ return (<p>Unable to decrypt event</p>)
+ }
+ } catch (e: any) {
+ console.error(e);
+ }
+ }
+
+ return renderEvent(event, previousEventIsFromSameSender, previousEventType, reactions);
+ })).then((renderedEvents) => {
+ setRenderedEvents(renderedEvents);
+ });
+ }
+
+ useEffect(() => {
+ if (events) {
+ renderEvents(events);
+ }
+ }, [events]);
+
+
useEffect(() => {
scrollRef.current?.scrollTo(0, scrollRef.current?.scrollHeight);
}, [pathname]);
@@ -54,23 +103,6 @@ const ChatView: FC<ChatViewProps> = memo(({ roomID, scrollRef }) => {
}
}
- const dedupedEvents = events?.filter((event, index, self) => {
- return self.findIndex(e => e.event_id === event.event_id) === index;
- });
-
- // Map events to components but also tell components if the previous event was from the same sender and which type it was
- const renderedEvents = dedupedEvents?.filter(event => event.type !== "m.reaction").map((event, index) => {
- const previousEvent = dedupedEvents?.filter(event => event.type !== "m.reaction")[index - 1];
- const previousEventIsFromSameSender = previousEvent?.sender === event.sender;
- const previousEventType = previousEvent?.type;
-
- // Make a list of events which are reactions for the current event we want to render
- const reactions = dedupedEvents?.filter((e) => {
- return e.type === "m.reaction" && e.content["m.relates_to"].event_id === event.event_id;
- });
-
- return renderEvent(event, previousEventIsFromSameSender, previousEventType, reactions);
- });
return <div className='max-w-[130ch] flex flex-col'>{renderedEvents}</div>;
});