Compare commits

...

2 commits

Author SHA1 Message Date
Bruno Windels f29f52347d WIP2 2022-02-21 19:25:59 +01:00
Bruno Windels c114eab26d WIP 2022-02-17 17:55:51 +01:00
6 changed files with 77 additions and 69 deletions

View file

@ -37,7 +37,7 @@ export class Decryption {
this.olmWorker = olmWorker; 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); let sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId);
// we never want to overwrite an existing key // we never want to overwrite an existing key
if (sessionEntry?.session) { if (sessionEntry?.session) {
@ -79,7 +79,7 @@ export class Decryption {
* @return {DecryptionPreparation} * @return {DecryptionPreparation}
*/ */
async prepareDecryptAll(roomId: string, events: TimelineEvent[], newKeys: IncomingRoomKey[] | undefined, txn: Transaction) { 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[] = []; const validEvents: TimelineEvent[] = [];
for (const event of events) { for (const event of events) {

View file

@ -15,35 +15,32 @@ limitations under the License.
*/ */
import {DecryptionError} from "../../common.js"; 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 { export class DecryptionChanges {
constructor(roomId, results, errors, replayEntries) { constructor(
this._roomId = roomId; private readonly roomId: string,
this._results = results; private readonly results: Map<string, DecryptionResult>,
this._errors = errors; private readonly errors: Map<string, Error>,
this._replayEntries = replayEntries; 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 * Handle replay attack detection, and return result
* @param {[type]} txn [description]
* @return {MegolmBatchDecryptionResult}
*/ */
async write(txn) { async write(txn: Transaction): Promise<{results: Map<string, DecryptionResult>, errors: Map<string, Error>}> {
await Promise.all(this._replayEntries.map(async replayEntry => { await Promise.all(this.replayEntries.map(async replayEntry => {
try { try {
this._handleReplayAttack(this._roomId, replayEntry, txn); await this._handleReplayAttack(this.roomId, replayEntry, txn);
} catch (err) { } catch (err) {
this._errors.set(replayEntry.eventId, err); this.errors.set(replayEntry.eventId, err);
} }
})); }));
return { return {
results: this._results, results: this.results,
errors: this._errors errors: this.errors
}; };
} }
@ -51,7 +48,7 @@ export class DecryptionChanges {
// if we redecrypted the same message twice and showed it again // if we redecrypted the same message twice and showed it again
// then it could be a malicious server admin replaying the word “yes” // 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 // 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 {messageIndex, sessionId, eventId, timestamp} = replayEntry;
const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex); const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex);
@ -60,7 +57,7 @@ export class DecryptionChanges {
const decryptedEventIsBad = decryption.timestamp < timestamp; const decryptedEventIsBad = decryption.timestamp < timestamp;
const badEventId = decryptedEventIsBad ? eventId : decryption.eventId; const badEventId = decryptedEventIsBad ? eventId : decryption.eventId;
// discard result // discard result
this._results.delete(eventId); this.results.delete(eventId);
throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, { throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, {
messageIndex, messageIndex,

View file

@ -14,38 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {DecryptionChanges} from "./DecryptionChanges.js"; import {DecryptionChanges} from "./DecryptionChanges";
import {mergeMap} from "../../../../utils/mergeMap"; 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 * Class that contains all the state loaded from storage to decrypt the given events
*/ */
export class DecryptionPreparation { export class DecryptionPreparation {
constructor(roomId, sessionDecryptions, errors) { constructor(
this._roomId = roomId; private readonly roomId: string,
this._sessionDecryptions = sessionDecryptions; private readonly sessionDecryptions: SessionDecryption[],
this._initialErrors = errors; private errors: Map<string, Error>
} ) {}
async decrypt() { async decrypt(): Promise<DecryptionChanges> {
try { try {
const errors = this._initialErrors; const errors = this.errors;
const results = new Map(); const results = new Map();
const replayEntries = []; const replayEntries: ReplayDetectionEntry[] = [];
await Promise.all(this._sessionDecryptions.map(async sessionDecryption => { await Promise.all(this.sessionDecryptions.map(async sessionDecryption => {
const sessionResult = await sessionDecryption.decryptAll(); const sessionResult = await sessionDecryption.decryptAll();
mergeMap(sessionResult.errors, errors); mergeMap(sessionResult.errors, errors);
mergeMap(sessionResult.results, results); mergeMap(sessionResult.results, results);
replayEntries.push(...sessionResult.replayEntries); replayEntries.push(...sessionResult.replayEntries);
})); }));
return new DecryptionChanges(this._roomId, results, errors, replayEntries); return new DecryptionChanges(this.roomId, results, errors, replayEntries);
} finally { } finally {
this.dispose(); this.dispose();
} }
} }
dispose() { dispose(): void {
for (const sd of this._sessionDecryptions) { for (const sd of this.sessionDecryptions) {
sd.dispose(); 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); return this._entries.some(op => op.refCount !== 0);
} }
dispose() { dispose(): void {
for (let i = 0; i < this._entries.length; i += 1) { for (let i = 0; i < this._entries.length; i += 1) {
this._entries[i].dispose(); 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; op.refCount -= 1;
if (op.refCount <= 0 && this.resolveUnusedOperation) { if (op.refCount <= 0 && this.resolveUnusedOperation) {
this.resolveUnusedOperation(); this.resolveUnusedOperation();
@ -116,7 +116,7 @@ export class KeyLoader extends BaseLRUCache<KeyOperation> {
return this.operationBecomesUnusedPromise; return this.operationBecomesUnusedPromise;
} }
private findIndexForAllocation(key: RoomKey) { private findIndexForAllocation(key: RoomKey): number {
let idx = this.findIndexSameKey(key); // cache hit let idx = this.findIndexSameKey(key); // cache hit
if (idx === -1) { if (idx === -1) {
if (this.size < this.limit) { if (this.size < this.limit) {
@ -190,16 +190,16 @@ class KeyOperation {
} }
// assumes isForSameSession is true // assumes isForSameSession is true
isBetter(other: KeyOperation) { isBetter(other: KeyOperation): boolean {
return isBetterThan(this.session, other.session); return isBetterThan(this.session, other.session);
} }
isForKey(key: RoomKey) { isForKey(key: RoomKey): boolean {
return this.key.serializationKey === key.serializationKey && return this.key.serializationKey === key.serializationKey &&
this.key.serializationType === key.serializationType; this.key.serializationType === key.serializationType;
} }
dispose() { dispose(): void {
this.session.free(); this.session.free();
this.session = undefined as any; this.session = undefined as any;
} }

View file

@ -47,7 +47,7 @@ export abstract class RoomKey {
set isBetter(value: boolean | undefined) { this._isBetter = value; } 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(); return newSession.first_known_index() < existingSession.first_known_index();
} }
@ -90,7 +90,7 @@ export abstract class IncomingRoomKey extends RoomKey {
return true; 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> { private async _checkBetterThanKeyInStorage(loader: KeyLoader, callback: (((session: Olm.InboundGroupSession, pickleKey: string) => void) | undefined), txn: Transaction): Promise<boolean> {
if (this.isBetter !== undefined) { if (this.isBetter !== undefined) {
@ -144,15 +144,15 @@ class DeviceMessageRoomKey extends IncomingRoomKey {
this._decryptionResult = decryptionResult; this._decryptionResult = decryptionResult;
} }
get roomId() { return this._decryptionResult.event.content?.["room_id"]; } get roomId(): string { return this._decryptionResult.event.content?.["room_id"]; }
get senderKey() { return this._decryptionResult.senderCurve25519Key; } get senderKey(): string { return this._decryptionResult.senderCurve25519Key; }
get sessionId() { return this._decryptionResult.event.content?.["session_id"]; } get sessionId(): string { return this._decryptionResult.event.content?.["session_id"]; }
get claimedEd25519Key() { return this._decryptionResult.claimedEd25519Key; } get claimedEd25519Key(): string { return this._decryptionResult.claimedEd25519Key; }
get serializationKey(): string { return this._decryptionResult.event.content?.["session_key"]; } get serializationKey(): string { return this._decryptionResult.event.content?.["session_key"]; }
get serializationType(): string { return "create"; } get serializationType(): string { return "create"; }
protected get keySource(): KeySource { return KeySource.DeviceMessage; } protected get keySource(): KeySource { return KeySource.DeviceMessage; }
loadInto(session) { loadInto(session): void {
session.create(this.serializationKey); session.create(this.serializationKey);
} }
} }
@ -184,7 +184,7 @@ export class OutboundRoomKey extends IncomingRoomKey {
get serializationType(): string { return "create"; } get serializationType(): string { return "create"; }
protected get keySource(): KeySource { return KeySource.Outbound; } protected get keySource(): KeySource { return KeySource.Outbound; }
loadInto(session: Olm.InboundGroupSession) { loadInto(session: Olm.InboundGroupSession): void {
session.create(this.serializationKey); session.create(this.serializationKey);
} }
} }
@ -194,15 +194,15 @@ class BackupRoomKey extends IncomingRoomKey {
super(); super();
} }
get roomId() { return this._roomId; } get roomId(): void { return this._roomId; }
get senderKey() { return this._backupInfo["sender_key"]; } get senderKey(): void { return this._backupInfo["sender_key"]; }
get sessionId() { return this._sessionId; } get sessionId(): void { return this._sessionId; }
get claimedEd25519Key() { return this._backupInfo["sender_claimed_keys"]?.["ed25519"]; } get claimedEd25519Key(): void { return this._backupInfo["sender_claimed_keys"]?.["ed25519"]; }
get serializationKey(): string { return this._backupInfo["session_key"]; } get serializationKey(): string { return this._backupInfo["session_key"]; }
get serializationType(): string { return "import_session"; } get serializationType(): string { return "import_session"; }
protected get keySource(): KeySource { return KeySource.Backup; } protected get keySource(): KeySource { return KeySource.Backup; }
loadInto(session) { loadInto(session): void {
session.import_session(this.serializationKey); session.import_session(this.serializationKey);
} }
@ -220,19 +220,19 @@ export class StoredRoomKey extends RoomKey {
this.storageEntry = storageEntry; this.storageEntry = storageEntry;
} }
get roomId() { return this.storageEntry.roomId; } get roomId(): string { return this.storageEntry.roomId; }
get senderKey() { return this.storageEntry.senderKey; } get senderKey(): string { return this.storageEntry.senderKey; }
get sessionId() { return this.storageEntry.sessionId; } get sessionId(): string { return this.storageEntry.sessionId; }
get claimedEd25519Key() { return this.storageEntry.claimedKeys!["ed25519"]; } get claimedEd25519Key(): string { return this.storageEntry.claimedKeys!["ed25519"]; }
get eventIds() { return this.storageEntry.eventIds; } get eventIds(): string[] | undefined { return this.storageEntry.eventIds; }
get serializationKey(): string { return this.storageEntry.session || ""; } get serializationKey(): string { return this.storageEntry.session || ""; }
get serializationType(): string { return "unpickle"; } get serializationType(): string { return "unpickle"; }
loadInto(session, pickleKey) { loadInto(session, pickleKey): void {
session.unpickle(pickleKey, this.serializationKey); session.unpickle(pickleKey, this.serializationKey);
} }
get hasSession() { get hasSession(): boolean {
// sessions are stored before they are received // sessions are stored before they are received
// to keep track of events that need it to be decrypted. // 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. // 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 sender_key
session_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 sessionKey = backupInfo["session_key"];
const senderKey = backupInfo["sender_key"]; const senderKey = backupInfo["sender_key"];
// TODO: can we just trust this? // TODO: can we just trust this?

View file

@ -28,17 +28,26 @@ export enum KeySource {
Outbound Outbound
} }
export interface InboundGroupSessionEntry { type InboundGroupSessionEntryBase = {
roomId: string; roomId: string;
senderKey: string; senderKey: string;
sessionId: 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 }; type InboundGroupSessionStorageEntry = InboundGroupSessionEntry & { key: string };