From f29f52347da426cae56823f41152f9dfbcec3dcc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 21 Feb 2022 19:25:59 +0100 Subject: [PATCH] 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 | 21 +++++++--- 6 files changed, 66 insertions(+), 54 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 e139e8c9..70d84491 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 6a5cad04..2891090b 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 618955bb..a1645e57 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 884203a3..6cfb34c3 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 b5f75224..8fcf1475 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 b78c817e..8a0c174d 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[]; - backup: BackupStatus, - source: KeySource } +export type InboundGroupSessionEntryWithKey = InboundGroupSessionEntryBase & { + session: string; + claimedKeys: { [algorithm : string] : string }; + backup: BackupStatus, + 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 };