From 7aeda70ff66614c74e69a4345202ebcbbfde8769 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 18:19:49 +0100 Subject: [PATCH 1/7] convert DecryptionResult --- ...ecryptionResult.js => DecryptionResult.ts} | 42 +++++++++++-------- .../megolm/decryption/SessionDecryption.ts | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) rename src/matrix/e2ee/{DecryptionResult.js => DecryptionResult.ts} (66%) diff --git a/src/matrix/e2ee/DecryptionResult.js b/src/matrix/e2ee/DecryptionResult.ts similarity index 66% rename from src/matrix/e2ee/DecryptionResult.js rename to src/matrix/e2ee/DecryptionResult.ts index e1c2bcc44f..67c242bc5a 100644 --- a/src/matrix/e2ee/DecryptionResult.js +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -26,35 +26,41 @@ limitations under the License. * see DeviceTracker */ +import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore"; +type DecryptedEvent = { + type?: string, + content?: Record +} export class DecryptionResult { - constructor(event, senderCurve25519Key, claimedEd25519Key) { - this.event = event; - this.senderCurve25519Key = senderCurve25519Key; - this.claimedEd25519Key = claimedEd25519Key; - this._device = null; - this._roomTracked = true; - } + private device?: DeviceIdentity; + private roomTracked: boolean = true; + + constructor( + public readonly event: DecryptedEvent, + public readonly senderCurve25519Key: string, + public readonly claimedEd25519Key: string + ) {} - setDevice(device) { - this._device = device; + setDevice(device: DeviceIdentity) { + this.device = device; } - setRoomNotTrackedYet() { - this._roomTracked = false; + setRoomNotTrackedYet(): void { + this.roomTracked = false; } - get isVerified() { - if (this._device) { - const comesFromDevice = this._device.ed25519Key === this.claimedEd25519Key; + get isVerified(): boolean { + if (this.device) { + const comesFromDevice = this.device.ed25519Key === this.claimedEd25519Key; return comesFromDevice; } return false; } - get isUnverified() { - if (this._device) { + get isUnverified(): boolean { + if (this.device) { return !this.isVerified; } else if (this.isVerificationUnknown) { return false; @@ -63,8 +69,8 @@ export class DecryptionResult { } } - get isVerificationUnknown() { + get isVerificationUnknown(): boolean { // verification is unknown if we haven't yet fetched the devices for the room - return !this._device && !this._roomTracked; + return !this.device && !this.roomTracked; } } diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index f56feb4718..57ef9a96ff 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {DecryptionResult} from "../../DecryptionResult.js"; +import {DecryptionResult} from "../../DecryptionResult"; import {DecryptionError} from "../../common.js"; import {ReplayDetectionEntry} from "./ReplayDetectionEntry"; import type {RoomKey} from "./RoomKey"; From 74c640f9375d59f84ca450d4635c77b2829be94b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 18:20:49 +0100 Subject: [PATCH 2/7] convert Session --- src/matrix/e2ee/olm/Encryption.js | 2 +- .../e2ee/olm/{Session.js => Session.ts} | 35 +++++++++++-------- .../storage/idb/stores/OlmSessionStore.ts | 18 +++++----- 3 files changed, 31 insertions(+), 24 deletions(-) rename src/matrix/e2ee/olm/{Session.js => Session.ts} (53%) diff --git a/src/matrix/e2ee/olm/Encryption.js b/src/matrix/e2ee/olm/Encryption.js index 652c657c12..3e78470d36 100644 --- a/src/matrix/e2ee/olm/Encryption.js +++ b/src/matrix/e2ee/olm/Encryption.js @@ -16,7 +16,7 @@ limitations under the License. import {groupByWithCreator} from "../../../utils/groupBy"; import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js"; -import {createSessionEntry} from "./Session.js"; +import {createSessionEntry} from "./Session"; function findFirstSessionId(sessionIds) { return sessionIds.reduce((first, sessionId) => { diff --git a/src/matrix/e2ee/olm/Session.js b/src/matrix/e2ee/olm/Session.ts similarity index 53% rename from src/matrix/e2ee/olm/Session.js rename to src/matrix/e2ee/olm/Session.ts index 9b5f4db052..f97c847804 100644 --- a/src/matrix/e2ee/olm/Session.js +++ b/src/matrix/e2ee/olm/Session.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function createSessionEntry(olmSession, senderKey, timestamp, pickleKey) { +import type {OlmSessionEntry} from "../../storage/idb/stores/OlmSessionStore"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +export function createSessionEntry(olmSession: Olm.Session, senderKey: string, timestamp: number, pickleKey: string): OlmSessionEntry { return { session: olmSession.pickle(pickleKey), sessionId: olmSession.session_id(), @@ -24,35 +28,38 @@ export function createSessionEntry(olmSession, senderKey, timestamp, pickleKey) } export class Session { - constructor(data, pickleKey, olm, isNew = false) { - this.data = data; - this._olm = olm; - this._pickleKey = pickleKey; - this.isNew = isNew; + public isModified: boolean; + + constructor( + public readonly data: OlmSessionEntry, + private readonly pickleKey: string, + private readonly olm: Olm, + public isNew: boolean = false + ) { this.isModified = isNew; } - static create(senderKey, olmSession, olm, pickleKey, timestamp) { + static create(senderKey: string, olmSession: Olm.Session, olm: Olm, pickleKey: string, timestamp: number): Session { const data = createSessionEntry(olmSession, senderKey, timestamp, pickleKey); return new Session(data, pickleKey, olm, true); } - get id() { + get id(): string { return this.data.sessionId; } - load() { - const session = new this._olm.Session(); - session.unpickle(this._pickleKey, this.data.session); + load(): Olm.Session { + const session = new this.olm.Session(); + session.unpickle(this.pickleKey, this.data.session); return session; } - unload(olmSession) { + unload(olmSession: Olm.Session): void { olmSession.free(); } - save(olmSession) { - this.data.session = olmSession.pickle(this._pickleKey); + save(olmSession: Olm.Session): void { + this.data.session = olmSession.pickle(this.pickleKey); this.isModified = true; } } diff --git a/src/matrix/storage/idb/stores/OlmSessionStore.ts b/src/matrix/storage/idb/stores/OlmSessionStore.ts index d5a79de2f9..1263a6495f 100644 --- a/src/matrix/storage/idb/stores/OlmSessionStore.ts +++ b/src/matrix/storage/idb/stores/OlmSessionStore.ts @@ -24,19 +24,19 @@ function decodeKey(key: string): { senderKey: string, sessionId: string } { return {senderKey, sessionId}; } -interface OlmSession { +export type OlmSessionEntry = { session: string; sessionId: string; senderKey: string; lastUsed: number; } -type OlmSessionEntry = OlmSession & { key: string }; +type OlmSessionStoredEntry = OlmSessionEntry & { key: string }; export class OlmSessionStore { - private _store: Store; + private _store: Store; - constructor(store: Store) { + constructor(store: Store) { this._store = store; } @@ -55,20 +55,20 @@ export class OlmSessionStore { return sessionIds; } - getAll(senderKey: string): Promise { + getAll(senderKey: string): Promise { const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, "")); return this._store.selectWhile(range, session => { return session.senderKey === senderKey; }); } - get(senderKey: string, sessionId: string): Promise { + get(senderKey: string, sessionId: string): Promise { return this._store.get(encodeKey(senderKey, sessionId)); } - set(session: OlmSession): void { - (session as OlmSessionEntry).key = encodeKey(session.senderKey, session.sessionId); - this._store.put(session as OlmSessionEntry); + set(session: OlmSessionEntry): void { + (session as OlmSessionStoredEntry).key = encodeKey(session.senderKey, session.sessionId); + this._store.put(session as OlmSessionStoredEntry); } remove(senderKey: string, sessionId: string): void { From a4fd1615ddbc0472e02825eb9a3fe3185a225c0c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 18:21:29 +0100 Subject: [PATCH 3/7] convert decryption --- src/matrix/Session.js | 18 +- .../e2ee/olm/{Decryption.js => Decryption.ts} | 159 ++++++++++-------- src/matrix/e2ee/olm/types.ts | 43 +++++ src/utils/Lock.ts | 8 +- 4 files changed, 149 insertions(+), 79 deletions(-) rename src/matrix/e2ee/olm/{Decryption.js => Decryption.ts} (70%) create mode 100644 src/matrix/e2ee/olm/types.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 83a2df0266..72d8a3133d 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -26,7 +26,7 @@ import {User} from "./User.js"; import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {Account as E2EEAccount} from "./e2ee/Account.js"; import {uploadAccountAsDehydratedDevice} from "./e2ee/Dehydration.js"; -import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; +import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; @@ -123,15 +123,15 @@ export class Session { // TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account // and can create RoomEncryption objects and handle encrypted to_device messages and device list changes. const senderKeyLock = new LockMap(); - const olmDecryption = new OlmDecryption({ - account: this._e2eeAccount, - pickleKey: PICKLE_KEY, - olm: this._olm, - storage: this._storage, - now: this._platform.clock.now, - ownUserId: this._user.id, + const olmDecryption = new OlmDecryption( + this._e2eeAccount, + PICKLE_KEY, + this._olm, + this._storage, + this._platform.clock.now, + this._user.id, senderKeyLock - }); + ); this._olmEncryption = new OlmEncryption({ account: this._e2eeAccount, pickleKey: PICKLE_KEY, diff --git a/src/matrix/e2ee/olm/Decryption.js b/src/matrix/e2ee/olm/Decryption.ts similarity index 70% rename from src/matrix/e2ee/olm/Decryption.js rename to src/matrix/e2ee/olm/Decryption.ts index 16e617a511..0fd4f0f9b1 100644 --- a/src/matrix/e2ee/olm/Decryption.js +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -16,32 +16,52 @@ limitations under the License. import {DecryptionError} from "../common.js"; import {groupBy} from "../../../utils/groupBy"; -import {MultiLock} from "../../../utils/Lock"; +import {MultiLock, ILock} from "../../../utils/Lock"; import {Session} from "./Session.js"; -import {DecryptionResult} from "../DecryptionResult.js"; +import {DecryptionResult} from "../DecryptionResult"; + +import type {OlmMessage, OlmPayload} from "./types"; +import type {Account} from "../Account"; +import type {LockMap} from "../../../utils/LockMap"; +import type {Storage} from "../../storage/idb/Storage"; +import type {Transaction} from "../../storage/idb/Transaction"; +import type {OlmEncryptedEvent} from "./types"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; const SESSION_LIMIT_PER_SENDER_KEY = 4; -function isPreKeyMessage(message) { +type DecryptionResults = { + results: DecryptionResult[], + errors: DecryptionError[], + senderKeyDecryption: SenderKeyDecryption +}; + +type CreateAndDecryptResult = { + session: Session, + plaintext: string +}; + +function isPreKeyMessage(message: OlmMessage): boolean { return message.type === 0; } -function sortSessions(sessions) { +function sortSessions(sessions: Session[]) { sessions.sort((a, b) => { return b.data.lastUsed - a.data.lastUsed; }); } export class Decryption { - constructor({account, pickleKey, now, ownUserId, storage, olm, senderKeyLock}) { - this._account = account; - this._pickleKey = pickleKey; - this._now = now; - this._ownUserId = ownUserId; - this._storage = storage; - this._olm = olm; - this._senderKeyLock = senderKeyLock; - } + constructor( + private readonly account: Account, + private readonly pickleKey: string, + private readonly now: () => number, + private readonly ownUserId: string, + private readonly storage: Storage, + private readonly olm: Olm, + private readonly senderKeyLock: LockMap + ) {} // we need to lock because both encryption and decryption can't be done in one txn, // so for them not to step on each other toes, we need to lock. @@ -50,8 +70,8 @@ export class Decryption { // - decryptAll below fails (to release the lock as early as we can) // - DecryptionChanges.write succeeds // - Sync finishes the writeSync phase (or an error was thrown, in case we never get to DecryptionChanges.write) - async obtainDecryptionLock(events) { - const senderKeys = new Set(); + async obtainDecryptionLock(events: OlmEncryptedEvent[]): Promise { + const senderKeys = new Set(); for (const event of events) { const senderKey = event.content?.["sender_key"]; if (senderKey) { @@ -61,7 +81,7 @@ export class Decryption { // take a lock on all senderKeys so encryption or other calls to decryptAll (should not happen) // don't modify the sessions at the same time const locks = await Promise.all(Array.from(senderKeys).map(senderKey => { - return this._senderKeyLock.takeLock(senderKey); + return this.senderKeyLock.takeLock(senderKey); })); return new MultiLock(locks); } @@ -83,18 +103,18 @@ export class Decryption { * @param {[type]} events * @return {Promise} [description] */ - async decryptAll(events, lock, txn) { + async decryptAll(events: OlmEncryptedEvent[], lock: ILock, txn: Transaction): Promise { try { - const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]); - const timestamp = this._now(); + const eventsPerSenderKey = groupBy(events, (event: OlmEncryptedEvent) => event.content?.["sender_key"]); + const timestamp = this.now(); // decrypt events for different sender keys in parallel const senderKeyOperations = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => { - return this._decryptAllForSenderKey(senderKey, events, timestamp, txn); + return this._decryptAllForSenderKey(senderKey!, events, timestamp, txn); })); - const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []); - const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []); + const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), [] as DecryptionResult[]); + const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), [] as DecryptionError[]); const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption); - return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, lock); + return new DecryptionChanges(senderKeyDecryptions, results, errors, this.account, lock); } catch (err) { // make sure the locks are release if something throws // otherwise they will be released in DecryptionChanges after having written @@ -104,11 +124,11 @@ export class Decryption { } } - async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) { + async _decryptAllForSenderKey(senderKey: string, events: OlmEncryptedEvent[], timestamp: number, readSessionsTxn: Transaction): Promise { const sessions = await this._getSessions(senderKey, readSessionsTxn); - const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp); - const results = []; - const errors = []; + const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this.olm, timestamp); + const results: DecryptionResult[] = []; + const errors: DecryptionError[] = []; // events for a single senderKey need to be decrypted one by one for (const event of events) { try { @@ -121,10 +141,10 @@ export class Decryption { return {results, errors, senderKeyDecryption}; } - _decryptForSenderKey(senderKeyDecryption, event, timestamp) { + _decryptForSenderKey(senderKeyDecryption: SenderKeyDecryption, event: OlmEncryptedEvent, timestamp: number): DecryptionResult { const senderKey = senderKeyDecryption.senderKey; const message = this._getMessageAndValidateEvent(event); - let plaintext; + let plaintext: string | undefined; try { plaintext = senderKeyDecryption.decrypt(message); } catch (err) { @@ -133,7 +153,7 @@ export class Decryption { } // could not decrypt with any existing session if (typeof plaintext !== "string" && isPreKeyMessage(message)) { - let createResult; + let createResult: CreateAndDecryptResult; try { createResult = this._createSessionAndDecrypt(senderKey, message, timestamp); } catch (error) { @@ -143,14 +163,14 @@ export class Decryption { plaintext = createResult.plaintext; } if (typeof plaintext === "string") { - let payload; + let payload: OlmPayload; try { payload = JSON.parse(plaintext); } catch (error) { throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, error}); } this._validatePayload(payload, event); - return new DecryptionResult(payload, senderKey, payload.keys.ed25519); + return new DecryptionResult(payload, senderKey, payload.keys!.ed25519!); } else { throw new DecryptionError("OLM_NO_MATCHING_SESSION", event, {knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)}); @@ -158,16 +178,16 @@ export class Decryption { } // only for pre-key messages after having attempted decryption with existing sessions - _createSessionAndDecrypt(senderKey, message, timestamp) { + _createSessionAndDecrypt(senderKey: string, message: OlmMessage, timestamp: number): CreateAndDecryptResult { let plaintext; // if we have multiple messages encrypted with the same new session, // this could create multiple sessions as the OTK isn't removed yet // (this only happens in DecryptionChanges.write) // This should be ok though as we'll first try to decrypt with the new session - const olmSession = this._account.createInboundOlmSession(senderKey, message.body); + const olmSession = this.account.createInboundOlmSession(senderKey, message.body); try { plaintext = olmSession.decrypt(message.type, message.body); - const session = Session.create(senderKey, olmSession, this._olm, this._pickleKey, timestamp); + const session = Session.create(senderKey, olmSession, this.olm, this.pickleKey, timestamp); session.unload(olmSession); return {session, plaintext}; } catch (err) { @@ -176,12 +196,12 @@ export class Decryption { } } - _getMessageAndValidateEvent(event) { + _getMessageAndValidateEvent(event: OlmEncryptedEvent): OlmMessage { const ciphertext = event.content?.ciphertext; if (!ciphertext) { throw new DecryptionError("OLM_MISSING_CIPHERTEXT", event); } - const message = ciphertext?.[this._account.identityKeys.curve25519]; + const message = ciphertext?.[this.account.identityKeys.curve25519]; if (!message) { throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", event); } @@ -189,22 +209,22 @@ export class Decryption { return message; } - async _getSessions(senderKey, txn) { + async _getSessions(senderKey: string, txn: Transaction): Promise { const sessionEntries = await txn.olmSessions.getAll(senderKey); // sort most recent used sessions first - const sessions = sessionEntries.map(s => new Session(s, this._pickleKey, this._olm)); + const sessions = sessionEntries.map(s => new Session(s, this.pickleKey, this.olm)); sortSessions(sessions); return sessions; } - _validatePayload(payload, event) { + _validatePayload(payload: OlmPayload, event: OlmEncryptedEvent): void { if (payload.sender !== event.sender) { throw new DecryptionError("OLM_FORWARDED_MESSAGE", event, {sentBy: event.sender, encryptedBy: payload.sender}); } - if (payload.recipient !== this._ownUserId) { + if (payload.recipient !== this.ownUserId) { throw new DecryptionError("OLM_BAD_RECIPIENT", event, {recipient: payload.recipient}); } - if (payload.recipient_keys?.ed25519 !== this._account.identityKeys.ed25519) { + if (payload.recipient_keys?.ed25519 !== this.account.identityKeys.ed25519) { throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", event, {key: payload.recipient_keys?.ed25519}); } // TODO: check room_id @@ -219,21 +239,21 @@ export class Decryption { // decryption helper for a single senderKey class SenderKeyDecryption { - constructor(senderKey, sessions, olm, timestamp) { - this.senderKey = senderKey; - this.sessions = sessions; - this._olm = olm; - this._timestamp = timestamp; - } + constructor( + public readonly senderKey: string, + public readonly sessions: Session[], + private readonly olm: Olm, + private readonly timestamp: number + ) {} - addNewSession(session) { + addNewSession(session: Session) { // add at top as it is most recent this.sessions.unshift(session); } - decrypt(message) { + decrypt(message: OlmMessage): string | undefined { for (const session of this.sessions) { - const plaintext = this._decryptWithSession(session, message); + const plaintext = this.decryptWithSession(session, message); if (typeof plaintext === "string") { // keep them sorted so will try the same session first for other messages // and so we can assume the excess ones are at the end @@ -244,11 +264,11 @@ class SenderKeyDecryption { } } - getModifiedSessions() { + getModifiedSessions(): Session[] { return this.sessions.filter(session => session.isModified); } - get hasNewSessions() { + get hasNewSessions(): boolean { return this.sessions.some(session => session.isNew); } @@ -257,7 +277,10 @@ class SenderKeyDecryption { // if this turns out to be a real cost for IE11, // we could look into adding a less expensive serialization mechanism // for olm sessions to libolm - _decryptWithSession(session, message) { + private decryptWithSession(session: Session, message: OlmMessage): string | undefined { + if (message.type === undefined || message.body === undefined) { + throw new Error("Invalid message without type or body"); + } const olmSession = session.load(); try { if (isPreKeyMessage(message) && !olmSession.matches_inbound(message.body)) { @@ -266,7 +289,7 @@ class SenderKeyDecryption { try { const plaintext = olmSession.decrypt(message.type, message.body); session.save(olmSession); - session.lastUsed = this._timestamp; + session.data.lastUsed = this.timestamp; return plaintext; } catch (err) { if (isPreKeyMessage(message)) { @@ -286,27 +309,27 @@ class SenderKeyDecryption { * @property {Array} errors see DecryptionError.event to retrieve the event that failed to decrypt. */ class DecryptionChanges { - constructor(senderKeyDecryptions, results, errors, account, lock) { - this._senderKeyDecryptions = senderKeyDecryptions; - this._account = account; - this.results = results; - this.errors = errors; - this._lock = lock; - } + constructor( + private readonly senderKeyDecryptions: SenderKeyDecryption[], + private readonly results: DecryptionResult[], + private readonly errors: DecryptionError[], + private readonly account: Account, + private readonly lock: ILock + ) {} - get hasNewSessions() { - return this._senderKeyDecryptions.some(skd => skd.hasNewSessions); + get hasNewSessions(): boolean { + return this.senderKeyDecryptions.some(skd => skd.hasNewSessions); } - write(txn) { + write(txn: Transaction): void { try { - for (const senderKeyDecryption of this._senderKeyDecryptions) { + for (const senderKeyDecryption of this.senderKeyDecryptions) { for (const session of senderKeyDecryption.getModifiedSessions()) { txn.olmSessions.set(session.data); if (session.isNew) { const olmSession = session.load(); try { - this._account.writeRemoveOneTimeKey(olmSession, txn); + this.account.writeRemoveOneTimeKey(olmSession, txn); } finally { session.unload(olmSession); } @@ -322,7 +345,7 @@ class DecryptionChanges { } } } finally { - this._lock.release(); + this.lock.release(); } } } diff --git a/src/matrix/e2ee/olm/types.ts b/src/matrix/e2ee/olm/types.ts new file mode 100644 index 0000000000..b9e394d5af --- /dev/null +++ b/src/matrix/e2ee/olm/types.ts @@ -0,0 +1,43 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +export type OlmMessage = { + type?: 0 | 1, + body?: string +} + +export type OlmEncryptedMessageContent = { + algorithm?: "m.olm.v1.curve25519-aes-sha2" + sender_key?: string, + ciphertext?: { + [deviceCurve25519Key: string]: OlmMessage + } +} + +export type OlmEncryptedEvent = { + type?: "m.room.encrypted", + content?: OlmEncryptedMessageContent + sender?: string +} + +export type OlmPayload = { + type?: string; + content?: Record; + sender?: string; + recipient?: string; + recipient_keys?: {ed25519?: string}; + keys?: {ed25519?: string}; +} diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts index 238d88f9b3..ff623ebabc 100644 --- a/src/utils/Lock.ts +++ b/src/utils/Lock.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export class Lock { +export interface ILock { + release(): void; +} + +export class Lock implements ILock { private _promise?: Promise; private _resolve?: (() => void); @@ -52,7 +56,7 @@ export class Lock { } } -export class MultiLock { +export class MultiLock implements ILock { constructor(public readonly locks: Lock[]) { } From eb5ca200f2559a29545e1c3390960bc723317baf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 16 Feb 2022 18:00:03 +0100 Subject: [PATCH 4/7] missed rename here --- src/matrix/e2ee/olm/Decryption.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 0fd4f0f9b1..7d9be4a33f 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -17,7 +17,7 @@ limitations under the License. import {DecryptionError} from "../common.js"; import {groupBy} from "../../../utils/groupBy"; import {MultiLock, ILock} from "../../../utils/Lock"; -import {Session} from "./Session.js"; +import {Session} from "./Session"; import {DecryptionResult} from "../DecryptionResult"; import type {OlmMessage, OlmPayload} from "./types"; @@ -246,7 +246,7 @@ class SenderKeyDecryption { private readonly timestamp: number ) {} - addNewSession(session: Session) { + addNewSession(session: Session): void { // add at top as it is most recent this.sessions.unshift(session); } From e3e90ed1671c247f623ceba95d1ff58d7bcf6f01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 16 Feb 2022 18:00:13 +0100 Subject: [PATCH 5/7] convert olm/Encryption to TS --- src/matrix/Session.js | 20 +-- .../e2ee/olm/{Encryption.js => Encryption.ts} | 141 +++++++++++------- 2 files changed, 94 insertions(+), 67 deletions(-) rename src/matrix/e2ee/olm/{Encryption.js => Encryption.ts} (64%) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 72d8a3133d..8652a1d745 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -27,7 +27,7 @@ import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {Account as E2EEAccount} from "./e2ee/Account.js"; import {uploadAccountAsDehydratedDevice} from "./e2ee/Dehydration.js"; import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption"; -import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; +import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup"; @@ -132,16 +132,16 @@ export class Session { this._user.id, senderKeyLock ); - this._olmEncryption = new OlmEncryption({ - account: this._e2eeAccount, - pickleKey: PICKLE_KEY, - olm: this._olm, - storage: this._storage, - now: this._platform.clock.now, - ownUserId: this._user.id, - olmUtil: this._olmUtil, + this._olmEncryption = new OlmEncryption( + this._e2eeAccount, + PICKLE_KEY, + this._olm, + this._storage, + this._platform.clock.now, + this._user.id, + this._olmUtil, senderKeyLock - }); + ); this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); this._megolmEncryption = new MegOlmEncryption({ account: this._e2eeAccount, diff --git a/src/matrix/e2ee/olm/Encryption.js b/src/matrix/e2ee/olm/Encryption.ts similarity index 64% rename from src/matrix/e2ee/olm/Encryption.js rename to src/matrix/e2ee/olm/Encryption.ts index 3e78470d36..ebc3817021 100644 --- a/src/matrix/e2ee/olm/Encryption.js +++ b/src/matrix/e2ee/olm/Encryption.ts @@ -18,6 +18,32 @@ import {groupByWithCreator} from "../../../utils/groupBy"; import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js"; import {createSessionEntry} from "./Session"; +import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types"; +import type {Account} from "../Account"; +import type {LockMap} from "../../../utils/LockMap"; +import type {Storage} from "../../storage/idb/Storage"; +import type {Transaction} from "../../storage/idb/Transaction"; +import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; +import type {HomeServerApi} from "../../net/HomeServerApi"; +import type {ILogItem} from "../../../logging/types"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +type ClaimedOTKResponse = { + [userId: string]: { + [deviceId: string]: { + [algorithmAndOtk: string]: { + key: string, + signatures: { + [userId: string]: { + [algorithmAndDevice: string]: string + } + } + } + } + } +}; + function findFirstSessionId(sessionIds) { return sessionIds.reduce((first, sessionId) => { if (!first || sessionId < first) { @@ -36,19 +62,19 @@ const OTK_ALGORITHM = "signed_curve25519"; const MAX_BATCH_SIZE = 20; export class Encryption { - constructor({account, olm, olmUtil, ownUserId, storage, now, pickleKey, senderKeyLock}) { - this._account = account; - this._olm = olm; - this._olmUtil = olmUtil; - this._ownUserId = ownUserId; - this._storage = storage; - this._now = now; - this._pickleKey = pickleKey; - this._senderKeyLock = senderKeyLock; - } + constructor( + private readonly account: Account, + private readonly olm: Olm, + private readonly olmUtil: Olm.Utility, + private readonly ownUserId: string, + private readonly storage: Storage, + private readonly now: () => number, + private readonly pickleKey: string, + private readonly senderKeyLock: LockMap + ) {} - async encrypt(type, content, devices, hsApi, log) { - let messages = []; + async encrypt(type: string, content: Record, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise { + let messages: EncryptedMessage[] = []; for (let i = 0; i < devices.length ; i += MAX_BATCH_SIZE) { const batchDevices = devices.slice(i, i + MAX_BATCH_SIZE); const batchMessages = await this._encryptForMaxDevices(type, content, batchDevices, hsApi, log); @@ -57,12 +83,12 @@ export class Encryption { return messages; } - async _encryptForMaxDevices(type, content, devices, hsApi, log) { + async _encryptForMaxDevices(type: string, content: Record, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise { // TODO: see if we can only hold some of the locks until after the /keys/claim call (if needed) // take a lock on all senderKeys so decryption and other calls to encrypt (should not happen) // don't modify the sessions at the same time const locks = await Promise.all(devices.map(device => { - return this._senderKeyLock.takeLock(device.curve25519Key); + return this.senderKeyLock.takeLock(device.curve25519Key); })); try { const { @@ -70,9 +96,9 @@ export class Encryption { existingEncryptionTargets, } = await this._findExistingSessions(devices); - const timestamp = this._now(); + const timestamp = this.now(); - let encryptionTargets = []; + let encryptionTargets: EncryptionTarget[] = []; try { if (devicesWithoutSession.length) { const newEncryptionTargets = await log.wrap("create sessions", log => this._createNewSessions( @@ -100,8 +126,8 @@ export class Encryption { } } - async _findExistingSessions(devices) { - const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); + async _findExistingSessions(devices: DeviceIdentity[]): Promise<{devicesWithoutSession: DeviceIdentity[], existingEncryptionTargets: EncryptionTarget[]}> { + const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]); const sessionIdsForDevice = await Promise.all(devices.map(async device => { return await txn.olmSessions.getSessionIds(device.curve25519Key); })); @@ -116,18 +142,18 @@ export class Encryption { const sessionId = findFirstSessionId(sessionIds); return EncryptionTarget.fromSessionId(device, sessionId); } - }).filter(target => !!target); + }).filter(target => !!target) as EncryptionTarget[]; return {devicesWithoutSession, existingEncryptionTargets}; } - _encryptForDevice(type, content, target) { + _encryptForDevice(type: string, content: Record, target: EncryptionTarget): OlmEncryptedMessageContent { const {session, device} = target; const plaintext = JSON.stringify(this._buildPlainTextMessageForDevice(type, content, device)); - const message = session.encrypt(plaintext); + const message = session!.encrypt(plaintext); const encryptedContent = { algorithm: OLM_ALGORITHM, - sender_key: this._account.identityKeys.curve25519, + sender_key: this.account.identityKeys.curve25519, ciphertext: { [device.curve25519Key]: message } @@ -135,27 +161,27 @@ export class Encryption { return encryptedContent; } - _buildPlainTextMessageForDevice(type, content, device) { + _buildPlainTextMessageForDevice(type: string, content: Record, device: DeviceIdentity): OlmPayload { return { keys: { - "ed25519": this._account.identityKeys.ed25519 + "ed25519": this.account.identityKeys.ed25519 }, recipient_keys: { "ed25519": device.ed25519Key }, recipient: device.userId, - sender: this._ownUserId, + sender: this.ownUserId, content, type } } - async _createNewSessions(devicesWithoutSession, hsApi, timestamp, log) { + async _createNewSessions(devicesWithoutSession: DeviceIdentity[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise { const newEncryptionTargets = await log.wrap("claim", log => this._claimOneTimeKeys(hsApi, devicesWithoutSession, log)); try { for (const target of newEncryptionTargets) { const {device, oneTimeKey} = target; - target.session = await this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); + target.session = await this.account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); } await this._storeSessions(newEncryptionTargets, timestamp); } catch (err) { @@ -167,12 +193,12 @@ export class Encryption { return newEncryptionTargets; } - async _claimOneTimeKeys(hsApi, deviceIdentities, log) { + async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceIdentity[], log: ILogItem): Promise { // create a Map> const devicesByUser = groupByWithCreator(deviceIdentities, - device => device.userId, - () => new Map(), - (deviceMap, device) => deviceMap.set(device.deviceId, device) + (device: DeviceIdentity) => device.userId, + (): Map => new Map(), + (deviceMap: Map, device: DeviceIdentity) => deviceMap.set(device.deviceId, device) ); const oneTimeKeys = Array.from(devicesByUser.entries()).reduce((usersObj, [userId, deviceMap]) => { usersObj[userId] = Array.from(deviceMap.values()).reduce((devicesObj, device) => { @@ -188,12 +214,12 @@ export class Encryption { if (Object.keys(claimResponse.failures).length) { log.log({l: "failures", servers: Object.keys(claimResponse.failures)}, log.level.Warn); } - const userKeyMap = claimResponse?.["one_time_keys"]; + const userKeyMap = claimResponse?.["one_time_keys"] as ClaimedOTKResponse; return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log); } - _verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log) { - const verifiedEncryptionTargets = []; + _verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map>, log: ILogItem): EncryptionTarget[] { + const verifiedEncryptionTargets: EncryptionTarget[] = []; for (const [userId, userSection] of Object.entries(userKeyMap)) { for (const [deviceId, deviceSection] of Object.entries(userSection)) { const [firstPropName, keySection] = Object.entries(deviceSection)[0]; @@ -202,7 +228,7 @@ export class Encryption { const device = devicesByUser.get(userId)?.get(deviceId); if (device) { const isValidSignature = verifyEd25519Signature( - this._olmUtil, userId, deviceId, device.ed25519Key, keySection, log); + this.olmUtil, userId, deviceId, device.ed25519Key, keySection, log); if (isValidSignature) { const target = EncryptionTarget.fromOTK(device, keySection.key); verifiedEncryptionTargets.push(target); @@ -214,8 +240,8 @@ export class Encryption { return verifiedEncryptionTargets; } - async _loadSessions(encryptionTargets) { - const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); + async _loadSessions(encryptionTargets: EncryptionTarget[]): Promise { + const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]); // given we run loading in parallel, there might still be some // storage requests that will finish later once one has failed. // those should not allocate a session anymore. @@ -223,10 +249,10 @@ export class Encryption { try { await Promise.all(encryptionTargets.map(async encryptionTarget => { const sessionEntry = await txn.olmSessions.get( - encryptionTarget.device.curve25519Key, encryptionTarget.sessionId); + encryptionTarget.device.curve25519Key, encryptionTarget.sessionId!); if (sessionEntry && !failed) { - const olmSession = new this._olm.Session(); - olmSession.unpickle(this._pickleKey, sessionEntry.session); + const olmSession = new this.olm.Session(); + olmSession.unpickle(this.pickleKey, sessionEntry.session); encryptionTarget.session = olmSession; } })); @@ -240,12 +266,12 @@ export class Encryption { } } - async _storeSessions(encryptionTargets, timestamp) { - const txn = await this._storage.readWriteTxn([this._storage.storeNames.olmSessions]); + async _storeSessions(encryptionTargets: EncryptionTarget[], timestamp: number): Promise { + const txn = await this.storage.readWriteTxn([this.storage.storeNames.olmSessions]); try { for (const target of encryptionTargets) { const sessionEntry = createSessionEntry( - target.session, target.device.curve25519Key, timestamp, this._pickleKey); + target.session!, target.device.curve25519Key, timestamp, this.pickleKey); txn.olmSessions.set(sessionEntry); } } catch (err) { @@ -261,23 +287,24 @@ export class Encryption { // (and later converted to a session) in case of a new session // or an existing session class EncryptionTarget { - constructor(device, oneTimeKey, sessionId) { - this.device = device; - this.oneTimeKey = oneTimeKey; - this.sessionId = sessionId; - // an olmSession, should probably be called olmSession - this.session = null; - } + + public session: Olm.Session | null = null; - static fromOTK(device, oneTimeKey) { + constructor( + public readonly device: DeviceIdentity, + public readonly oneTimeKey: string | null, + public readonly sessionId: string | null + ) {} + + static fromOTK(device: DeviceIdentity, oneTimeKey: string): EncryptionTarget { return new EncryptionTarget(device, oneTimeKey, null); } - static fromSessionId(device, sessionId) { + static fromSessionId(device: DeviceIdentity, sessionId: string): EncryptionTarget { return new EncryptionTarget(device, null, sessionId); } - dispose() { + dispose(): void { if (this.session) { this.session.free(); } @@ -285,8 +312,8 @@ class EncryptionTarget { } class EncryptedMessage { - constructor(content, device) { - this.content = content; - this.device = device; - } + constructor( + public readonly content: OlmEncryptedMessageContent, + public readonly device: DeviceIdentity + ) {} } From c114eab26deaeab92417ceee29d2d234eea571e1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Feb 2022 17:55:51 +0100 Subject: [PATCH 6/7] WIP --- ...ryptionChanges.js => DecryptionChanges.ts} | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) rename src/matrix/e2ee/megolm/decryption/{DecryptionChanges.js => DecryptionChanges.ts} (72%) diff --git a/src/matrix/e2ee/megolm/decryption/DecryptionChanges.js b/src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts similarity index 72% rename from src/matrix/e2ee/megolm/decryption/DecryptionChanges.js rename to src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts index b45ab6dd94..6a5cad0485 100644 --- a/src/matrix/e2ee/megolm/decryption/DecryptionChanges.js +++ b/src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts @@ -15,35 +15,31 @@ limitations under the License. */ import {DecryptionError} from "../../common.js"; +import type {DecryptionResult} from "../../DecryptionResult"; +import type {ReplayDetectionEntry} from "./ReplayDetectionEntry"; export class DecryptionChanges { - constructor(roomId, results, errors, replayEntries) { - this._roomId = roomId; - this._results = results; - this._errors = errors; - this._replayEntries = replayEntries; - } + constructor( + private readonly roomId: string, + private readonly results: Map, + private readonly errors: Map | undefined, + private readonly replayEntries: ReplayDetectionEntry[] + ) {} /** - * @type MegolmBatchDecryptionResult - * @property {Map} results a map of event id to decryption result - * @property {Map} errors event id -> errors - * * Handle replay attack detection, and return result - * @param {[type]} txn [description] - * @return {MegolmBatchDecryptionResult} */ - async write(txn) { - await Promise.all(this._replayEntries.map(async replayEntry => { + async write(txn): Promise<{results: Map, errors: Map}> { + await Promise.all(this.replayEntries.map(async replayEntry => { try { - this._handleReplayAttack(this._roomId, replayEntry, txn); + this._handleReplayAttack(this.roomId, replayEntry, txn); } catch (err) { - this._errors.set(replayEntry.eventId, err); + this.errors.set(replayEntry.eventId, err); } })); return { - results: this._results, - errors: this._errors + results: this.results, + errors: this.errors }; } From f29f52347da426cae56823f41152f9dfbcec3dcc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 21 Feb 2022 19:25:59 +0100 Subject: [PATCH 7/7] WIP2 --- src/matrix/e2ee/megolm/Decryption.ts | 4 +- .../megolm/decryption/DecryptionChanges.ts | 11 ++--- ...reparation.js => DecryptionPreparation.ts} | 28 +++++++------ .../e2ee/megolm/decryption/KeyLoader.ts | 14 +++---- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 42 +++++++++---------- .../idb/stores/InboundGroupSessionStore.ts | 19 ++++++--- 6 files changed, 65 insertions(+), 53 deletions(-) rename src/matrix/e2ee/megolm/decryption/{DecryptionPreparation.js => DecryptionPreparation.ts} (61%) diff --git a/src/matrix/e2ee/megolm/Decryption.ts b/src/matrix/e2ee/megolm/Decryption.ts index e139e8c9a0..70d8449108 100644 --- a/src/matrix/e2ee/megolm/Decryption.ts +++ b/src/matrix/e2ee/megolm/Decryption.ts @@ -37,7 +37,7 @@ export class Decryption { this.olmWorker = olmWorker; } - async addMissingKeyEventIds(roomId, senderKey, sessionId, eventIds, txn) { + async addMissingKeyEventIds(roomId: string, senderKey: string, sessionId: string, eventIds: string[], txn: Transaction) { let sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId); // we never want to overwrite an existing key if (sessionEntry?.session) { @@ -79,7 +79,7 @@ export class Decryption { * @return {DecryptionPreparation} */ async prepareDecryptAll(roomId: string, events: TimelineEvent[], newKeys: IncomingRoomKey[] | undefined, txn: Transaction) { - const errors = new Map(); + const errors: Map = new Map(); const validEvents: TimelineEvent[] = []; for (const event of events) { diff --git a/src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts b/src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts index 6a5cad0485..2891090bc0 100644 --- a/src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts +++ b/src/matrix/e2ee/megolm/decryption/DecryptionChanges.ts @@ -16,23 +16,24 @@ limitations under the License. import {DecryptionError} from "../../common.js"; import type {DecryptionResult} from "../../DecryptionResult"; +import type {Transaction} from "../../../storage/idb/Transaction"; import type {ReplayDetectionEntry} from "./ReplayDetectionEntry"; export class DecryptionChanges { constructor( private readonly roomId: string, private readonly results: Map, - private readonly errors: Map | undefined, + private readonly errors: Map, private readonly replayEntries: ReplayDetectionEntry[] ) {} /** * Handle replay attack detection, and return result */ - async write(txn): Promise<{results: Map, errors: Map}> { + async write(txn: Transaction): Promise<{results: Map, errors: Map}> { await Promise.all(this.replayEntries.map(async replayEntry => { try { - this._handleReplayAttack(this.roomId, replayEntry, txn); + await this._handleReplayAttack(this.roomId, replayEntry, txn); } catch (err) { this.errors.set(replayEntry.eventId, err); } @@ -47,7 +48,7 @@ export class DecryptionChanges { // if we redecrypted the same message twice and showed it again // then it could be a malicious server admin replaying the word “yes” // to make you respond to a msg you didn’t say “yes” to, or something - async _handleReplayAttack(roomId, replayEntry, txn) { + async _handleReplayAttack(roomId: string, replayEntry: ReplayDetectionEntry, txn: Transaction): Promise { const {messageIndex, sessionId, eventId, timestamp} = replayEntry; const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex); @@ -56,7 +57,7 @@ export class DecryptionChanges { const decryptedEventIsBad = decryption.timestamp < timestamp; const badEventId = decryptedEventIsBad ? eventId : decryption.eventId; // discard result - this._results.delete(eventId); + this.results.delete(eventId); throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, { messageIndex, diff --git a/src/matrix/e2ee/megolm/decryption/DecryptionPreparation.js b/src/matrix/e2ee/megolm/decryption/DecryptionPreparation.ts similarity index 61% rename from src/matrix/e2ee/megolm/decryption/DecryptionPreparation.js rename to src/matrix/e2ee/megolm/decryption/DecryptionPreparation.ts index 618955bb36..a1645e57ba 100644 --- a/src/matrix/e2ee/megolm/decryption/DecryptionPreparation.js +++ b/src/matrix/e2ee/megolm/decryption/DecryptionPreparation.ts @@ -14,38 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {DecryptionChanges} from "./DecryptionChanges.js"; +import {DecryptionChanges} from "./DecryptionChanges"; import {mergeMap} from "../../../../utils/mergeMap"; +import type {SessionDecryption} from "./SessionDecryption"; +import type {ReplayDetectionEntry} from "./ReplayDetectionEntry"; /** * Class that contains all the state loaded from storage to decrypt the given events */ export class DecryptionPreparation { - constructor(roomId, sessionDecryptions, errors) { - this._roomId = roomId; - this._sessionDecryptions = sessionDecryptions; - this._initialErrors = errors; - } + constructor( + private readonly roomId: string, + private readonly sessionDecryptions: SessionDecryption[], + private errors: Map + ) {} - async decrypt() { + async decrypt(): Promise { try { - const errors = this._initialErrors; + const errors = this.errors; const results = new Map(); - const replayEntries = []; - await Promise.all(this._sessionDecryptions.map(async sessionDecryption => { + const replayEntries: ReplayDetectionEntry[] = []; + await Promise.all(this.sessionDecryptions.map(async sessionDecryption => { const sessionResult = await sessionDecryption.decryptAll(); mergeMap(sessionResult.errors, errors); mergeMap(sessionResult.results, results); replayEntries.push(...sessionResult.replayEntries); })); - return new DecryptionChanges(this._roomId, results, errors, replayEntries); + return new DecryptionChanges(this.roomId, results, errors, replayEntries); } finally { this.dispose(); } } - dispose() { - for (const sd of this._sessionDecryptions) { + dispose(): void { + for (const sd of this.sessionDecryptions) { sd.dispose(); } } diff --git a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts index 884203a34a..6cfb34c3b5 100644 --- a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts +++ b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts @@ -58,11 +58,11 @@ export class KeyLoader extends BaseLRUCache { } } - get running() { + get running(): boolean { return this._entries.some(op => op.refCount !== 0); } - dispose() { + dispose(): void { for (let i = 0; i < this._entries.length; i += 1) { this._entries[i].dispose(); } @@ -98,7 +98,7 @@ export class KeyLoader extends BaseLRUCache { } } - private releaseOperation(op: KeyOperation) { + private releaseOperation(op: KeyOperation): void { op.refCount -= 1; if (op.refCount <= 0 && this.resolveUnusedOperation) { this.resolveUnusedOperation(); @@ -116,7 +116,7 @@ export class KeyLoader extends BaseLRUCache { return this.operationBecomesUnusedPromise; } - private findIndexForAllocation(key: RoomKey) { + private findIndexForAllocation(key: RoomKey): number { let idx = this.findIndexSameKey(key); // cache hit if (idx === -1) { if (this.size < this.limit) { @@ -190,16 +190,16 @@ class KeyOperation { } // assumes isForSameSession is true - isBetter(other: KeyOperation) { + isBetter(other: KeyOperation): boolean { return isBetterThan(this.session, other.session); } - isForKey(key: RoomKey) { + isForKey(key: RoomKey): boolean { return this.key.serializationKey === key.serializationKey && this.key.serializationType === key.serializationType; } - dispose() { + dispose(): void { this.session.free(); this.session = undefined as any; } diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index b5f75224ff..8fcf1475ad 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -47,7 +47,7 @@ export abstract class RoomKey { set isBetter(value: boolean | undefined) { this._isBetter = value; } } -export function isBetterThan(newSession: Olm.InboundGroupSession, existingSession: Olm.InboundGroupSession) { +export function isBetterThan(newSession: Olm.InboundGroupSession, existingSession: Olm.InboundGroupSession): boolean { return newSession.first_known_index() < existingSession.first_known_index(); } @@ -90,7 +90,7 @@ export abstract class IncomingRoomKey extends RoomKey { return true; } - get eventIds() { return this._eventIds; } + get eventIds(): string[] | undefined { return this._eventIds; } private async _checkBetterThanKeyInStorage(loader: KeyLoader, callback: (((session: Olm.InboundGroupSession, pickleKey: string) => void) | undefined), txn: Transaction): Promise { if (this.isBetter !== undefined) { @@ -144,15 +144,15 @@ class DeviceMessageRoomKey extends IncomingRoomKey { this._decryptionResult = decryptionResult; } - get roomId() { return this._decryptionResult.event.content?.["room_id"]; } - get senderKey() { return this._decryptionResult.senderCurve25519Key; } - get sessionId() { return this._decryptionResult.event.content?.["session_id"]; } - get claimedEd25519Key() { return this._decryptionResult.claimedEd25519Key; } + get roomId(): string { return this._decryptionResult.event.content?.["room_id"]; } + get senderKey(): string { return this._decryptionResult.senderCurve25519Key; } + get sessionId(): string { return this._decryptionResult.event.content?.["session_id"]; } + get claimedEd25519Key(): string { return this._decryptionResult.claimedEd25519Key; } get serializationKey(): string { return this._decryptionResult.event.content?.["session_key"]; } get serializationType(): string { return "create"; } protected get keySource(): KeySource { return KeySource.DeviceMessage; } - loadInto(session) { + loadInto(session): void { session.create(this.serializationKey); } } @@ -184,7 +184,7 @@ export class OutboundRoomKey extends IncomingRoomKey { get serializationType(): string { return "create"; } protected get keySource(): KeySource { return KeySource.Outbound; } - loadInto(session: Olm.InboundGroupSession) { + loadInto(session: Olm.InboundGroupSession): void { session.create(this.serializationKey); } } @@ -194,15 +194,15 @@ class BackupRoomKey extends IncomingRoomKey { super(); } - get roomId() { return this._roomId; } - get senderKey() { return this._backupInfo["sender_key"]; } - get sessionId() { return this._sessionId; } - get claimedEd25519Key() { return this._backupInfo["sender_claimed_keys"]?.["ed25519"]; } + get roomId(): void { return this._roomId; } + get senderKey(): void { return this._backupInfo["sender_key"]; } + get sessionId(): void { return this._sessionId; } + get claimedEd25519Key(): void { return this._backupInfo["sender_claimed_keys"]?.["ed25519"]; } get serializationKey(): string { return this._backupInfo["session_key"]; } get serializationType(): string { return "import_session"; } protected get keySource(): KeySource { return KeySource.Backup; } - loadInto(session) { + loadInto(session): void { session.import_session(this.serializationKey); } @@ -220,19 +220,19 @@ export class StoredRoomKey extends RoomKey { this.storageEntry = storageEntry; } - get roomId() { return this.storageEntry.roomId; } - get senderKey() { return this.storageEntry.senderKey; } - get sessionId() { return this.storageEntry.sessionId; } - get claimedEd25519Key() { return this.storageEntry.claimedKeys!["ed25519"]; } - get eventIds() { return this.storageEntry.eventIds; } + get roomId(): string { return this.storageEntry.roomId; } + get senderKey(): string { return this.storageEntry.senderKey; } + get sessionId(): string { return this.storageEntry.sessionId; } + get claimedEd25519Key(): string { return this.storageEntry.claimedKeys!["ed25519"]; } + get eventIds(): string[] | undefined { return this.storageEntry.eventIds; } get serializationKey(): string { return this.storageEntry.session || ""; } get serializationType(): string { return "unpickle"; } - loadInto(session, pickleKey) { + loadInto(session, pickleKey): void { session.unpickle(pickleKey, this.serializationKey); } - get hasSession() { + get hasSession(): boolean { // sessions are stored before they are received // to keep track of events that need it to be decrypted. // This is used to retry decryption of those events once the session is received. @@ -261,7 +261,7 @@ sessionInfo is a response from key backup and has the following keys: sender_key session_key */ -export function keyFromBackup(roomId, sessionId, backupInfo): BackupRoomKey | undefined { +export function keyFromBackup(roomId: string, sessionId: string, backupInfo: object): BackupRoomKey | undefined { const sessionKey = backupInfo["session_key"]; const senderKey = backupInfo["sender_key"]; // TODO: can we just trust this? diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index b78c817e81..8a0c174dc9 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -28,17 +28,26 @@ export enum KeySource { Outbound } -export interface InboundGroupSessionEntry { +type InboundGroupSessionEntryBase = { roomId: string; senderKey: string; sessionId: string; - session?: string; - claimedKeys?: { [algorithm : string] : string }; - eventIds?: string[]; +} + +export type InboundGroupSessionEntryWithKey = InboundGroupSessionEntryBase & { + session: string; + claimedKeys: { [algorithm : string] : string }; backup: BackupStatus, - source: KeySource + source: KeySource, } +// used to keep track of which event ids can be decrypted with this key as we encounter them before the key is received +export type InboundGroupSessionEntryWithEventIds = InboundGroupSessionEntryBase & { + eventIds: string[]; +} + +type InboundGroupSessionEntry = InboundGroupSessionEntryWithKey | InboundGroupSessionEntryWithEventIds; + type InboundGroupSessionStorageEntry = InboundGroupSessionEntry & { key: string };