make decryption algorithms return DecryptionResult

which contains curve25519 key and claimed ed25519 key as well as payload
This commit is contained in:
Bruno Windels 2020-09-08 10:48:11 +02:00
parent b8ba4c5771
commit 9137d5dcbb
7 changed files with 159 additions and 45 deletions

View file

@ -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) {

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

View file

@ -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) {

View file

@ -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) {

View file

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

View file

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

View file

@ -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) {