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;
|
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) {
|
||||||
|
|
|
@ -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 didn’t say “yes” to, or something
|
// 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 {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,
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue