forked from mystiq/hydrogen-web
make decryption algorithms return DecryptionResult
which contains curve25519 key and claimed ed25519 key as well as payload
This commit is contained in:
parent
b8ba4c5771
commit
9137d5dcbb
7 changed files with 159 additions and 45 deletions
|
@ -41,13 +41,19 @@ export class DeviceMessageHandler {
|
|||
// we don't handle anything other for now
|
||||
}
|
||||
|
||||
async _writeDecryptedEvents(payloads, txn) {
|
||||
const megOlmRoomKeysPayloads = payloads.filter(p => {
|
||||
return p.event?.type === "m.room_key" && p.event.content?.algorithm === MEGOLM_ALGORITHM;
|
||||
/**
|
||||
* [_writeDecryptedEvents description]
|
||||
* @param {Array<DecryptionResult>} olmResults
|
||||
* @param {[type]} txn [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async _writeDecryptedEvents(olmResults, txn) {
|
||||
const megOlmRoomKeysResults = olmResults.filter(r => {
|
||||
return r.event?.type === "m.room_key" && r.event.content?.algorithm === MEGOLM_ALGORITHM;
|
||||
});
|
||||
let roomKeys;
|
||||
if (megOlmRoomKeysPayloads.length) {
|
||||
roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysPayloads, txn);
|
||||
if (megOlmRoomKeysResults.length) {
|
||||
roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysResults, txn);
|
||||
}
|
||||
return {roomKeys};
|
||||
}
|
||||
|
@ -84,7 +90,7 @@ export class DeviceMessageHandler {
|
|||
]);
|
||||
let changes;
|
||||
try {
|
||||
changes = await this._writeDecryptedEvents(decryptChanges.payloads, txn);
|
||||
changes = await this._writeDecryptedEvents(decryptChanges.results, txn);
|
||||
decryptChanges.write(txn);
|
||||
txn.session.remove(PENDING_ENCRYPTED_EVENTS);
|
||||
} catch (err) {
|
||||
|
|
70
src/matrix/e2ee/DecryptionResult.js
Normal file
70
src/matrix/e2ee/DecryptionResult.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @property {object} event the plaintext event (type and content property)
|
||||
* @property {string} senderCurve25519Key the curve25519 sender key of the olm event
|
||||
* @property {string} claimedEd25519Key The ed25519 fingerprint key retrieved from the decryption payload.
|
||||
* The sender of the olm event claims this is the ed25519 fingerprint key
|
||||
* that matches the curve25519 sender key.
|
||||
* The caller needs to check if this key does indeed match the senderKey
|
||||
* for a device with a valid signature returned from /keys/query,
|
||||
* see DeviceTracker
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export class DecryptionResult {
|
||||
constructor(event, senderCurve25519Key, claimedKeys) {
|
||||
this.event = event;
|
||||
this.senderCurve25519Key = senderCurve25519Key;
|
||||
this.claimedEd25519Key = claimedKeys.ed25519;
|
||||
this._device = null;
|
||||
this._roomTracked = true;
|
||||
}
|
||||
|
||||
setDevice(device) {
|
||||
this._device = device;
|
||||
}
|
||||
|
||||
setRoomNotTrackedYet() {
|
||||
this._roomTracked = false;
|
||||
}
|
||||
|
||||
get isVerified() {
|
||||
if (this._device) {
|
||||
const comesFromDevice = this._device.ed25519Key === this.claimedEd25519Key;
|
||||
return comesFromDevice;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get isUnverified() {
|
||||
if (this._device) {
|
||||
return !this.isVerified;
|
||||
} else if (this.isVerificationUnknown) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get isVerificationUnknown() {
|
||||
// verification is unknown if we haven't yet fetched the devices for the room
|
||||
return !this._device && !this._roomTracked;
|
||||
}
|
||||
}
|
|
@ -46,17 +46,17 @@ export class RoomEncryption {
|
|||
return await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn);
|
||||
}
|
||||
|
||||
async decrypt(event, isSync, retryData, txn) {
|
||||
async decrypt(event, isSync, isTimelineOpen, retryData, txn) {
|
||||
if (event.content?.algorithm !== MEGOLM_ALGORITHM) {
|
||||
throw new Error("Unsupported algorithm: " + event.content?.algorithm);
|
||||
}
|
||||
let sessionCache = isSync ? this._megolmSyncCache : this._megolmBackfillCache;
|
||||
const payload = await this._megolmDecryption.decrypt(
|
||||
const result = await this._megolmDecryption.decrypt(
|
||||
this._room.id, event, sessionCache, txn);
|
||||
if (!payload) {
|
||||
if (!result) {
|
||||
this._addMissingSessionEvent(event, isSync, retryData);
|
||||
}
|
||||
return payload;
|
||||
return result;
|
||||
}
|
||||
|
||||
_addMissingSessionEvent(event, isSync, data) {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {DecryptionError} from "../common.js";
|
||||
import {DecryptionResult} from "../DecryptionResult.js";
|
||||
|
||||
const CACHE_MAX_SIZE = 10;
|
||||
|
||||
|
@ -28,6 +29,14 @@ export class Decryption {
|
|||
return new SessionCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* [decrypt description]
|
||||
* @param {[type]} roomId [description]
|
||||
* @param {[type]} event [description]
|
||||
* @param {[type]} sessionCache [description]
|
||||
* @param {[type]} txn [description]
|
||||
* @return {DecryptionResult?} the decrypted event result, or undefined if the session id is not known.
|
||||
*/
|
||||
async decrypt(roomId, event, sessionCache, txn) {
|
||||
const senderKey = event.content?.["sender_key"];
|
||||
const sessionId = event.content?.["session_id"];
|
||||
|
@ -41,8 +50,13 @@ export class Decryption {
|
|||
throw new DecryptionError("MEGOLM_INVALID_EVENT", event);
|
||||
}
|
||||
|
||||
let session = sessionCache.get(roomId, senderKey, sessionId);
|
||||
if (!session) {
|
||||
let session;
|
||||
let claimedKeys;
|
||||
const cacheEntry = sessionCache.get(roomId, senderKey, sessionId);
|
||||
if (cacheEntry) {
|
||||
session = cacheEntry.session;
|
||||
claimedKeys = cacheEntry.claimedKeys;
|
||||
} else {
|
||||
const sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId);
|
||||
if (sessionEntry) {
|
||||
session = new this._olm.InboundGroupSession();
|
||||
|
@ -52,7 +66,8 @@ export class Decryption {
|
|||
session.free();
|
||||
throw err;
|
||||
}
|
||||
sessionCache.add(roomId, senderKey, session);
|
||||
claimedKeys = sessionEntry.claimedKeys;
|
||||
sessionCache.add(roomId, senderKey, session, claimedKeys);
|
||||
}
|
||||
}
|
||||
if (!session) {
|
||||
|
@ -70,8 +85,7 @@ export class Decryption {
|
|||
{encryptedRoomId: payload.room_id, eventRoomId: roomId});
|
||||
}
|
||||
await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn);
|
||||
// TODO: verify event came from said senderKey
|
||||
return payload;
|
||||
return new DecryptionResult(payload, senderKey, claimedKeys);
|
||||
}
|
||||
|
||||
async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) {
|
||||
|
@ -142,6 +156,17 @@ class SessionCache {
|
|||
this._sessions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {CacheEntry}
|
||||
* @property {InboundGroupSession} session the unpickled session
|
||||
* @property {Object} claimedKeys an object with the claimed ed25519 key
|
||||
*
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {string} senderKey
|
||||
* @param {string} sessionId
|
||||
* @return {CacheEntry?}
|
||||
*/
|
||||
get(roomId, senderKey, sessionId) {
|
||||
const idx = this._sessions.findIndex(s => {
|
||||
return s.roomId === roomId &&
|
||||
|
@ -155,13 +180,13 @@ class SessionCache {
|
|||
this._sessions.splice(idx, 1);
|
||||
this._sessions.unshift(entry);
|
||||
}
|
||||
return entry.session;
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
add(roomId, senderKey, session) {
|
||||
add(roomId, senderKey, session, claimedKeys) {
|
||||
// add new at top
|
||||
this._sessions.unshift({roomId, senderKey, session});
|
||||
this._sessions.unshift({roomId, senderKey, session, claimedKeys});
|
||||
if (this._sessions.length > CACHE_MAX_SIZE) {
|
||||
// free sessions we're about to remove
|
||||
for (let i = CACHE_MAX_SIZE; i < this._sessions.length; i += 1) {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import {DecryptionError} from "../common.js";
|
||||
import {groupBy} from "../../../utils/groupBy.js";
|
||||
import {Session} from "./Session.js";
|
||||
import {DecryptionResult} from "../DecryptionResult.js";
|
||||
|
||||
const SESSION_LIMIT_PER_SENDER_KEY = 4;
|
||||
|
||||
|
@ -50,6 +51,13 @@ export class Decryption {
|
|||
// and also can avoid side-effects before all can be stored this way
|
||||
//
|
||||
// doing it one by one would be possible, but we would lose the opportunity for parallelization
|
||||
//
|
||||
|
||||
/**
|
||||
* [decryptAll description]
|
||||
* @param {[type]} events
|
||||
* @return {Promise<DecryptionChanges>} [description]
|
||||
*/
|
||||
async decryptAll(events) {
|
||||
const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]);
|
||||
const timestamp = this._now();
|
||||
|
@ -61,15 +69,16 @@ export class Decryption {
|
|||
try {
|
||||
const readSessionsTxn = await this._storage.readTxn([this._storage.storeNames.olmSessions]);
|
||||
// decrypt events for different sender keys in parallel
|
||||
const results = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => {
|
||||
const senderKeyOperations = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => {
|
||||
return this._decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn);
|
||||
}));
|
||||
const payloads = results.reduce((all, r) => all.concat(r.payloads), []);
|
||||
const errors = results.reduce((all, r) => all.concat(r.errors), []);
|
||||
const senderKeyDecryptions = results.map(r => r.senderKeyDecryption);
|
||||
return new DecryptionChanges(senderKeyDecryptions, payloads, errors, this._account, locks);
|
||||
const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []);
|
||||
const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []);
|
||||
const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption);
|
||||
return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, locks);
|
||||
} catch (err) {
|
||||
// make sure the locks are release if something throws
|
||||
// otherwise they will be released in DecryptionChanges after having written
|
||||
for (const lock of locks) {
|
||||
lock.release();
|
||||
}
|
||||
|
@ -80,18 +89,18 @@ export class Decryption {
|
|||
async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) {
|
||||
const sessions = await this._getSessions(senderKey, readSessionsTxn);
|
||||
const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp);
|
||||
const payloads = [];
|
||||
const results = [];
|
||||
const errors = [];
|
||||
// events for a single senderKey need to be decrypted one by one
|
||||
for (const event of events) {
|
||||
try {
|
||||
const payload = this._decryptForSenderKey(senderKeyDecryption, event, timestamp);
|
||||
payloads.push(payload);
|
||||
const result = this._decryptForSenderKey(senderKeyDecryption, event, timestamp);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
return {payloads, errors, senderKeyDecryption};
|
||||
return {results, errors, senderKeyDecryption};
|
||||
}
|
||||
|
||||
_decryptForSenderKey(senderKeyDecryption, event, timestamp) {
|
||||
|
@ -118,7 +127,7 @@ export class Decryption {
|
|||
throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err});
|
||||
}
|
||||
this._validatePayload(payload, event);
|
||||
return {event: payload, senderKey};
|
||||
return new DecryptionResult(payload, senderKey, payload.keys);
|
||||
} else {
|
||||
throw new DecryptionError("OLM_NO_MATCHING_SESSION", event,
|
||||
{knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)});
|
||||
|
@ -182,9 +191,9 @@ export class Decryption {
|
|||
if (!payload.content) {
|
||||
throw new DecryptionError("missing content on payload", event, {payload});
|
||||
}
|
||||
// TODO: how important is it to verify the message?
|
||||
// we should look at payload.keys.ed25519 for that... and compare it to the key we have fetched
|
||||
// from /keys/query, which we might not have done yet at this point.
|
||||
if (typeof payload.keys?.ed25519 !== "string") {
|
||||
throw new DecryptionError("Missing or invalid claimed ed25519 key on payload", event, {payload});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,11 +261,15 @@ class SenderKeyDecryption {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {Array<DecryptionResult>} results
|
||||
* @property {Array<DecryptionError>} errors see DecryptionError.event to retrieve the event that failed to decrypt.
|
||||
*/
|
||||
class DecryptionChanges {
|
||||
constructor(senderKeyDecryptions, payloads, errors, account, locks) {
|
||||
constructor(senderKeyDecryptions, results, errors, account, locks) {
|
||||
this._senderKeyDecryptions = senderKeyDecryptions;
|
||||
this._account = account;
|
||||
this.payloads = payloads;
|
||||
this.results = results;
|
||||
this.errors = errors;
|
||||
this._locks = locks;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import {fetchOrLoadMembers} from "./members/load.js";
|
|||
import {MemberList} from "./members/MemberList.js";
|
||||
import {Heroes} from "./members/Heroes.js";
|
||||
import {EventEntry} from "./timeline/entries/EventEntry.js";
|
||||
import {EventKey} from "./timeline/EventKey.js";
|
||||
|
||||
export class Room extends EventEmitter {
|
||||
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) {
|
||||
|
@ -101,12 +100,10 @@ export class Room extends EventEmitter {
|
|||
async _decryptEntry(entry, txn, isSync) {
|
||||
if (entry.eventType === "m.room.encrypted") {
|
||||
try {
|
||||
const {fragmentId, entryIndex} = entry;
|
||||
const key = new EventKey(fragmentId, entryIndex);
|
||||
const decryptedEvent = await this._roomEncryption.decrypt(
|
||||
entry.event, isSync, key, txn);
|
||||
if (decryptedEvent) {
|
||||
entry.replaceWithDecrypted(decryptedEvent);
|
||||
const decryptionResult = await this._roomEncryption.decrypt(
|
||||
entry.event, isSync, !!this._timeline, entry.asEventKey(), txn);
|
||||
if (decryptionResult) {
|
||||
entry.setDecryptionResult(decryptionResult);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("event decryption error", err, entry.event);
|
||||
|
|
|
@ -22,7 +22,7 @@ export class EventEntry extends BaseEntry {
|
|||
super(fragmentIdComparer);
|
||||
this._eventEntry = eventEntry;
|
||||
this._decryptionError = null;
|
||||
this._decryptedEvent = null;
|
||||
this._decryptionResult = null;
|
||||
}
|
||||
|
||||
get event() {
|
||||
|
@ -38,15 +38,16 @@ export class EventEntry extends BaseEntry {
|
|||
}
|
||||
|
||||
get content() {
|
||||
return this._decryptedEvent?.content || this._eventEntry.event.content;
|
||||
return this._decryptionResult?.event?.content || this._eventEntry.event.content;
|
||||
}
|
||||
|
||||
get prevContent() {
|
||||
// doesn't look at _decryptionResult because state events are not encrypted
|
||||
return getPrevContentFromStateEvent(this._eventEntry.event);
|
||||
}
|
||||
|
||||
get eventType() {
|
||||
return this._decryptedEvent?.type || this._eventEntry.event.type;
|
||||
return this._decryptionResult?.event?.type || this._eventEntry.event.type;
|
||||
}
|
||||
|
||||
get stateKey() {
|
||||
|
@ -73,8 +74,10 @@ export class EventEntry extends BaseEntry {
|
|||
return this._eventEntry.event.event_id;
|
||||
}
|
||||
|
||||
replaceWithDecrypted(event) {
|
||||
this._decryptedEvent = event;
|
||||
setDecryptionResult(result) {
|
||||
this._decryptionResult = result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setDecryptionError(err) {
|
||||
|
|
Loading…
Reference in a new issue