This commit is contained in:
Bruno Windels 2022-02-21 19:25:59 +01:00
parent c114eab26d
commit f29f52347d
6 changed files with 66 additions and 54 deletions

View File

@ -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<string, Error> = new Map();
const validEvents: TimelineEvent[] = [];
for (const event of events) {

View File

@ -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<string, DecryptionResult>,
private readonly errors: Map<string, Error> | undefined,
private readonly errors: Map<string, Error>,
private readonly replayEntries: ReplayDetectionEntry[]
) {}
/**
* Handle replay attack detection, and return result
*/
async write(txn): Promise<{results: Map<string, DecryptionResult>, errors: Map<string, Error>}> {
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);
}
@ -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 didnt 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);
@ -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,

View File

@ -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();
}
}

View File

@ -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();
}
@ -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();
@ -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) {
@ -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;
}

View File

@ -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<boolean> {
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?

View File

@ -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 };