Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert megolm code to Typescript #697

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions src/matrix/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ 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 {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js";
import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption";
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";
Expand Down Expand Up @@ -123,25 +123,25 @@ 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,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,41 @@ limitations under the License.
* see DeviceTracker
*/

import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore";

type DecryptedEvent = {
type?: string,
content?: Record<string, any>
}

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;
Expand All @@ -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;
}
}
4 changes: 2 additions & 2 deletions src/matrix/e2ee/megolm/Decryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, Error> = new Map();
const validEvents: TimelineEvent[] = [];

for (const event of events) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,40 @@ 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(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<string, DecryptionResult>,
private readonly errors: Map<string, Error>,
private readonly replayEntries: ReplayDetectionEntry[]
) {}

/**
* @type MegolmBatchDecryptionResult
* @property {Map<string, DecryptionResult>} results a map of event id to decryption result
* @property {Map<string, Error>} 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: Transaction): Promise<{results: Map<string, DecryptionResult>, errors: Map<string, Error>}> {
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);
this.errors.set(replayEntry.eventId, err);
}
}));
return {
results: this._results,
errors: this._errors
results: this.results,
errors: this.errors
};
}

// need to handle replay attack because
// 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<void> {
const {messageIndex, sessionId, eventId, timestamp} = replayEntry;
const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex);

Expand All @@ -60,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Error>
) {}

async decrypt() {
async decrypt(): Promise<DecryptionChanges> {
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();
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/matrix/e2ee/megolm/decryption/KeyLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export class KeyLoader extends BaseLRUCache<KeyOperation> {
}
}

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();
}
Expand Down Expand Up @@ -98,7 +98,7 @@ export class KeyLoader extends BaseLRUCache<KeyOperation> {
}
}

private releaseOperation(op: KeyOperation) {
private releaseOperation(op: KeyOperation): void {
op.refCount -= 1;
if (op.refCount <= 0 && this.resolveUnusedOperation) {
this.resolveUnusedOperation();
Expand All @@ -116,7 +116,7 @@ export class KeyLoader extends BaseLRUCache<KeyOperation> {
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) {
Expand Down Expand Up @@ -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;
}
Expand Down
Loading