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 // we don't handle anything other for now
} }
async _writeDecryptedEvents(payloads, txn) { /**
const megOlmRoomKeysPayloads = payloads.filter(p => { * [_writeDecryptedEvents description]
return p.event?.type === "m.room_key" && p.event.content?.algorithm === MEGOLM_ALGORITHM; * @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; let roomKeys;
if (megOlmRoomKeysPayloads.length) { if (megOlmRoomKeysResults.length) {
roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysPayloads, txn); roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysResults, txn);
} }
return {roomKeys}; return {roomKeys};
} }
@ -84,7 +90,7 @@ export class DeviceMessageHandler {
]); ]);
let changes; let changes;
try { try {
changes = await this._writeDecryptedEvents(decryptChanges.payloads, txn); changes = await this._writeDecryptedEvents(decryptChanges.results, txn);
decryptChanges.write(txn); decryptChanges.write(txn);
txn.session.remove(PENDING_ENCRYPTED_EVENTS); txn.session.remove(PENDING_ENCRYPTED_EVENTS);
} catch (err) { } 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); 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) { if (event.content?.algorithm !== MEGOLM_ALGORITHM) {
throw new Error("Unsupported algorithm: " + event.content?.algorithm); throw new Error("Unsupported algorithm: " + event.content?.algorithm);
} }
let sessionCache = isSync ? this._megolmSyncCache : this._megolmBackfillCache; 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); this._room.id, event, sessionCache, txn);
if (!payload) { if (!result) {
this._addMissingSessionEvent(event, isSync, retryData); this._addMissingSessionEvent(event, isSync, retryData);
} }
return payload; return result;
} }
_addMissingSessionEvent(event, isSync, data) { _addMissingSessionEvent(event, isSync, data) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import {DecryptionError} from "../common.js"; import {DecryptionError} from "../common.js";
import {DecryptionResult} from "../DecryptionResult.js";
const CACHE_MAX_SIZE = 10; const CACHE_MAX_SIZE = 10;
@ -28,6 +29,14 @@ export class Decryption {
return new SessionCache(); 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) { async decrypt(roomId, event, sessionCache, txn) {
const senderKey = event.content?.["sender_key"]; const senderKey = event.content?.["sender_key"];
const sessionId = event.content?.["session_id"]; const sessionId = event.content?.["session_id"];
@ -41,8 +50,13 @@ export class Decryption {
throw new DecryptionError("MEGOLM_INVALID_EVENT", event); throw new DecryptionError("MEGOLM_INVALID_EVENT", event);
} }
let session = sessionCache.get(roomId, senderKey, sessionId); let session;
if (!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); const sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId);
if (sessionEntry) { if (sessionEntry) {
session = new this._olm.InboundGroupSession(); session = new this._olm.InboundGroupSession();
@ -52,7 +66,8 @@ export class Decryption {
session.free(); session.free();
throw err; throw err;
} }
sessionCache.add(roomId, senderKey, session); claimedKeys = sessionEntry.claimedKeys;
sessionCache.add(roomId, senderKey, session, claimedKeys);
} }
} }
if (!session) { if (!session) {
@ -70,8 +85,7 @@ export class Decryption {
{encryptedRoomId: payload.room_id, eventRoomId: roomId}); {encryptedRoomId: payload.room_id, eventRoomId: roomId});
} }
await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn); await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn);
// TODO: verify event came from said senderKey return new DecryptionResult(payload, senderKey, claimedKeys);
return payload;
} }
async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) { async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) {
@ -142,6 +156,17 @@ class SessionCache {
this._sessions = []; 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) { get(roomId, senderKey, sessionId) {
const idx = this._sessions.findIndex(s => { const idx = this._sessions.findIndex(s => {
return s.roomId === roomId && return s.roomId === roomId &&
@ -155,13 +180,13 @@ class SessionCache {
this._sessions.splice(idx, 1); this._sessions.splice(idx, 1);
this._sessions.unshift(entry); this._sessions.unshift(entry);
} }
return entry.session; return entry;
} }
} }
add(roomId, senderKey, session) { add(roomId, senderKey, session, claimedKeys) {
// add new at top // add new at top
this._sessions.unshift({roomId, senderKey, session}); this._sessions.unshift({roomId, senderKey, session, claimedKeys});
if (this._sessions.length > CACHE_MAX_SIZE) { if (this._sessions.length > CACHE_MAX_SIZE) {
// free sessions we're about to remove // free sessions we're about to remove
for (let i = CACHE_MAX_SIZE; i < this._sessions.length; i += 1) { 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 {DecryptionError} from "../common.js";
import {groupBy} from "../../../utils/groupBy.js"; import {groupBy} from "../../../utils/groupBy.js";
import {Session} from "./Session.js"; import {Session} from "./Session.js";
import {DecryptionResult} from "../DecryptionResult.js";
const SESSION_LIMIT_PER_SENDER_KEY = 4; 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 // 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 // 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) { async decryptAll(events) {
const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]); const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]);
const timestamp = this._now(); const timestamp = this._now();
@ -61,15 +69,16 @@ export class Decryption {
try { try {
const readSessionsTxn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); const readSessionsTxn = await this._storage.readTxn([this._storage.storeNames.olmSessions]);
// decrypt events for different sender keys in parallel // 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); return this._decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn);
})); }));
const payloads = results.reduce((all, r) => all.concat(r.payloads), []); const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []);
const errors = results.reduce((all, r) => all.concat(r.errors), []); const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []);
const senderKeyDecryptions = results.map(r => r.senderKeyDecryption); const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption);
return new DecryptionChanges(senderKeyDecryptions, payloads, errors, this._account, locks); return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, locks);
} catch (err) { } catch (err) {
// make sure the locks are release if something throws // make sure the locks are release if something throws
// otherwise they will be released in DecryptionChanges after having written
for (const lock of locks) { for (const lock of locks) {
lock.release(); lock.release();
} }
@ -80,18 +89,18 @@ export class Decryption {
async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) { async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) {
const sessions = await this._getSessions(senderKey, readSessionsTxn); const sessions = await this._getSessions(senderKey, readSessionsTxn);
const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp); const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp);
const payloads = []; const results = [];
const errors = []; const errors = [];
// events for a single senderKey need to be decrypted one by one // events for a single senderKey need to be decrypted one by one
for (const event of events) { for (const event of events) {
try { try {
const payload = this._decryptForSenderKey(senderKeyDecryption, event, timestamp); const result = this._decryptForSenderKey(senderKeyDecryption, event, timestamp);
payloads.push(payload); results.push(result);
} catch (err) { } catch (err) {
errors.push(err); errors.push(err);
} }
} }
return {payloads, errors, senderKeyDecryption}; return {results, errors, senderKeyDecryption};
} }
_decryptForSenderKey(senderKeyDecryption, event, timestamp) { _decryptForSenderKey(senderKeyDecryption, event, timestamp) {
@ -118,7 +127,7 @@ export class Decryption {
throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err}); throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err});
} }
this._validatePayload(payload, event); this._validatePayload(payload, event);
return {event: payload, senderKey}; return new DecryptionResult(payload, senderKey, payload.keys);
} else { } else {
throw new DecryptionError("OLM_NO_MATCHING_SESSION", event, throw new DecryptionError("OLM_NO_MATCHING_SESSION", event,
{knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)}); {knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)});
@ -182,9 +191,9 @@ export class Decryption {
if (!payload.content) { if (!payload.content) {
throw new DecryptionError("missing content on payload", event, {payload}); throw new DecryptionError("missing content on payload", event, {payload});
} }
// TODO: how important is it to verify the message? if (typeof payload.keys?.ed25519 !== "string") {
// we should look at payload.keys.ed25519 for that... and compare it to the key we have fetched throw new DecryptionError("Missing or invalid claimed ed25519 key on payload", event, {payload});
// from /keys/query, which we might not have done yet at this point. }
} }
} }
@ -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 { class DecryptionChanges {
constructor(senderKeyDecryptions, payloads, errors, account, locks) { constructor(senderKeyDecryptions, results, errors, account, locks) {
this._senderKeyDecryptions = senderKeyDecryptions; this._senderKeyDecryptions = senderKeyDecryptions;
this._account = account; this._account = account;
this.payloads = payloads; this.results = results;
this.errors = errors; this.errors = errors;
this._locks = locks; this._locks = locks;
} }

View file

@ -26,7 +26,6 @@ import {fetchOrLoadMembers} from "./members/load.js";
import {MemberList} from "./members/MemberList.js"; import {MemberList} from "./members/MemberList.js";
import {Heroes} from "./members/Heroes.js"; import {Heroes} from "./members/Heroes.js";
import {EventEntry} from "./timeline/entries/EventEntry.js"; import {EventEntry} from "./timeline/entries/EventEntry.js";
import {EventKey} from "./timeline/EventKey.js";
export class Room extends EventEmitter { export class Room extends EventEmitter {
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) {
@ -101,12 +100,10 @@ export class Room extends EventEmitter {
async _decryptEntry(entry, txn, isSync) { async _decryptEntry(entry, txn, isSync) {
if (entry.eventType === "m.room.encrypted") { if (entry.eventType === "m.room.encrypted") {
try { try {
const {fragmentId, entryIndex} = entry; const decryptionResult = await this._roomEncryption.decrypt(
const key = new EventKey(fragmentId, entryIndex); entry.event, isSync, !!this._timeline, entry.asEventKey(), txn);
const decryptedEvent = await this._roomEncryption.decrypt( if (decryptionResult) {
entry.event, isSync, key, txn); entry.setDecryptionResult(decryptionResult);
if (decryptedEvent) {
entry.replaceWithDecrypted(decryptedEvent);
} }
} catch (err) { } catch (err) {
console.warn("event decryption error", err, entry.event); console.warn("event decryption error", err, entry.event);

View file

@ -22,7 +22,7 @@ export class EventEntry extends BaseEntry {
super(fragmentIdComparer); super(fragmentIdComparer);
this._eventEntry = eventEntry; this._eventEntry = eventEntry;
this._decryptionError = null; this._decryptionError = null;
this._decryptedEvent = null; this._decryptionResult = null;
} }
get event() { get event() {
@ -38,15 +38,16 @@ export class EventEntry extends BaseEntry {
} }
get content() { get content() {
return this._decryptedEvent?.content || this._eventEntry.event.content; return this._decryptionResult?.event?.content || this._eventEntry.event.content;
} }
get prevContent() { get prevContent() {
// doesn't look at _decryptionResult because state events are not encrypted
return getPrevContentFromStateEvent(this._eventEntry.event); return getPrevContentFromStateEvent(this._eventEntry.event);
} }
get eventType() { get eventType() {
return this._decryptedEvent?.type || this._eventEntry.event.type; return this._decryptionResult?.event?.type || this._eventEntry.event.type;
} }
get stateKey() { get stateKey() {
@ -73,8 +74,10 @@ export class EventEntry extends BaseEntry {
return this._eventEntry.event.event_id; return this._eventEntry.event.event_id;
} }
replaceWithDecrypted(event) { setDecryptionResult(result) {
this._decryptedEvent = event; this._decryptionResult = result;
}
} }
setDecryptionError(err) { setDecryptionError(err) {