This repository has been archived on 2022-08-19. You can view files and clone it, but cannot push or open issues or pull requests.
hydrogen-web/src/matrix/e2ee/megolm/Decryption.js

216 lines
7.7 KiB
JavaScript
Raw Normal View History

2020-09-02 17:54:38 +05:30
/*
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.
*/
2020-09-04 15:36:26 +05:30
import {DecryptionError} from "../common.js";
import {DecryptionResult} from "../DecryptionResult.js";
2020-09-04 15:36:26 +05:30
const CACHE_MAX_SIZE = 10;
2020-09-02 17:54:38 +05:30
export class Decryption {
2020-09-02 18:22:19 +05:30
constructor({pickleKey, olm}) {
2020-09-02 17:54:38 +05:30
this._pickleKey = pickleKey;
2020-09-02 18:22:19 +05:30
this._olm = olm;
2020-09-02 17:54:38 +05:30
}
2020-09-04 15:36:26 +05:30
createSessionCache() {
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) {
2020-09-04 15:36:26 +05:30
const senderKey = event.content?.["sender_key"];
const sessionId = event.content?.["session_id"];
const ciphertext = event.content?.ciphertext;
if (
typeof senderKey !== "string" ||
typeof sessionId !== "string" ||
typeof ciphertext !== "string"
) {
throw new DecryptionError("MEGOLM_INVALID_EVENT", event);
}
let session;
let claimedKeys;
const cacheEntry = sessionCache.get(roomId, senderKey, sessionId);
if (cacheEntry) {
session = cacheEntry.session;
claimedKeys = cacheEntry.claimedKeys;
} else {
2020-09-04 15:36:26 +05:30
const sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId);
if (sessionEntry) {
session = new this._olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, sessionEntry.session);
} catch (err) {
session.free();
throw err;
}
claimedKeys = sessionEntry.claimedKeys;
sessionCache.add(roomId, senderKey, session, claimedKeys);
2020-09-04 15:36:26 +05:30
}
}
if (!session) {
return;
}
const {plaintext, message_index: messageIndex} = session.decrypt(ciphertext);
let payload;
try {
payload = JSON.parse(plaintext);
} catch (err) {
throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err});
2020-09-04 15:36:26 +05:30
}
if (payload.room_id !== roomId) {
throw new DecryptionError("MEGOLM_WRONG_ROOM", event,
{encryptedRoomId: payload.room_id, eventRoomId: roomId});
}
await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn);
return new DecryptionResult(payload, senderKey, claimedKeys);
2020-09-04 15:36:26 +05:30
}
async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) {
2020-09-04 15:36:26 +05:30
const eventId = event.event_id;
const timestamp = event.origin_server_ts;
const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex);
if (decryption && decryption.eventId !== eventId) {
// the one with the newest timestamp should be the attack
const decryptedEventIsBad = decryption.timestamp < timestamp;
const badEventId = decryptedEventIsBad ? eventId : decryption.eventId;
throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, {badEventId, otherEventId: decryption.eventId});
2020-09-04 15:36:26 +05:30
}
if (!decryption) {
txn.groupSessionDecryptions.set({
roomId,
sessionId,
messageIndex,
eventId,
timestamp
});
}
}
/**
* @type {MegolmInboundSessionDescription}
* @property {string} senderKey the sender key of the session
* @property {string} sessionId the session identifier
*
* Adds room keys as inbound group sessions
* @param {Array<OlmDecryptionResult>} decryptionResults an array of m.room_key decryption results.
* @param {[type]} txn a storage transaction with read/write on inboundGroupSessions
* @return {Promise<Array<MegolmInboundSessionDescription>>} an array with the newly added sessions
*/
async addRoomKeys(decryptionResults, txn) {
2020-09-02 17:54:38 +05:30
const newSessions = [];
for (const {senderCurve25519Key: senderKey, event, claimedEd25519Key} of decryptionResults) {
2020-09-02 17:54:38 +05:30
const roomId = event.content?.["room_id"];
const sessionId = event.content?.["session_id"];
const sessionKey = event.content?.["session_key"];
if (
typeof roomId !== "string" ||
typeof sessionId !== "string" ||
typeof senderKey !== "string" ||
typeof sessionKey !== "string"
) {
return;
}
2020-09-04 20:16:13 +05:30
// TODO: compare first_known_index to see which session to keep
2020-09-02 17:54:38 +05:30
const hasSession = await txn.inboundGroupSessions.has(roomId, senderKey, sessionId);
if (!hasSession) {
const session = new this._olm.InboundGroupSession();
try {
session.create(sessionKey);
const sessionEntry = {
roomId,
senderKey,
sessionId,
session: session.pickle(this._pickleKey),
claimedKeys: {ed25519: claimedEd25519Key},
2020-09-02 17:54:38 +05:30
};
2020-09-02 18:23:50 +05:30
txn.inboundGroupSessions.set(sessionEntry);
2020-09-02 17:54:38 +05:30
newSessions.push(sessionEntry);
} finally {
session.free();
}
}
}
2020-09-04 15:36:26 +05:30
// this will be passed to the Room in notifyRoomKeys
2020-09-02 17:54:38 +05:30
return newSessions;
}
2020-09-04 15:36:26 +05:30
}
class SessionCache {
constructor() {
this._sessions = [];
}
2020-09-02 17:54:38 +05:30
/**
* @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?}
*/
2020-09-04 15:36:26 +05:30
get(roomId, senderKey, sessionId) {
const idx = this._sessions.findIndex(s => {
return s.roomId === roomId &&
s.senderKey === senderKey &&
sessionId === s.session.session_id();
});
if (idx !== -1) {
const entry = this._sessions[idx];
// move to top
if (idx > 0) {
this._sessions.splice(idx, 1);
this._sessions.unshift(entry);
}
return entry;
2020-09-04 15:36:26 +05:30
}
}
add(roomId, senderKey, session, claimedKeys) {
2020-09-04 15:36:26 +05:30
// add new at top
this._sessions.unshift({roomId, senderKey, session, claimedKeys});
2020-09-04 15:36:26 +05:30
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) {
this._sessions[i].session.free();
}
this._sessions = this._sessions.slice(0, CACHE_MAX_SIZE);
2020-09-02 17:54:38 +05:30
}
}
2020-09-04 15:36:26 +05:30
dispose() {
for (const entry of this._sessions) {
entry.session.free();
}
}
2020-09-02 17:54:38 +05:30
}