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";
|
2020-09-08 14:18:11 +05:30
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2020-09-08 14:18:11 +05:30
|
|
|
/**
|
|
|
|
* [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.
|
|
|
|
*/
|
2020-09-04 18:58:22 +05:30
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-09-08 14:18:11 +05:30
|
|
|
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;
|
|
|
|
}
|
2020-09-08 14:18:11 +05:30
|
|
|
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) {
|
2020-09-04 18:58:22 +05:30
|
|
|
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});
|
|
|
|
}
|
2020-09-04 18:58:22 +05:30
|
|
|
await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn);
|
2020-09-08 14:18:11 +05:30
|
|
|
return new DecryptionResult(payload, senderKey, claimedKeys);
|
2020-09-04 15:36:26 +05:30
|
|
|
}
|
|
|
|
|
2020-09-04 18:58:22 +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;
|
2020-09-04 18:58:22 +05:30
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-02 17:54:38 +05:30
|
|
|
async addRoomKeys(payloads, txn) {
|
|
|
|
const newSessions = [];
|
|
|
|
for (const {senderKey, event} of payloads) {
|
|
|
|
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: event.keys,
|
|
|
|
};
|
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
|
|
|
|
2020-09-08 14:18:11 +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);
|
|
|
|
}
|
2020-09-08 14:18:11 +05:30
|
|
|
return entry;
|
2020-09-04 15:36:26 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-08 14:18:11 +05:30
|
|
|
add(roomId, senderKey, session, claimedKeys) {
|
2020-09-04 15:36:26 +05:30
|
|
|
// add new at top
|
2020-09-08 14:18:11 +05:30
|
|
|
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
|
|
|
}
|