forked from mystiq/hydrogen-web
Compare commits
2 commits
master
...
bwindels/t
Author | SHA1 | Date | |
---|---|---|---|
|
f29f52347d | ||
|
c114eab26d |
6 changed files with 77 additions and 69 deletions
|
@ -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) {
|
||||
|
|
|
@ -15,35 +15,32 @@ 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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -51,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<void> {
|
||||
const {messageIndex, sessionId, eventId, timestamp} = replayEntry;
|
||||
const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex);
|
||||
|
||||
|
@ -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,
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue