diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index b81e86689..cf8e58f2e 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -10,6 +10,7 @@ import Promise from 'bluebird'; import sdk from '../../../..'; import algorithms from '../../../../lib/crypto/algorithms'; import WebStorageSessionStore from '../../../../lib/store/session/webstorage'; +import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js'; import MockStorageApi from '../../../MockStorageApi'; import testUtils from '../../../test-utils'; @@ -45,8 +46,9 @@ describe("MegolmDecryption", function() { const mockStorage = new MockStorageApi(); const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); - const olmDevice = new OlmDevice(sessionStore); + const olmDevice = new OlmDevice(sessionStore, cryptoStore); megolmDecryption = new MegolmDecryption({ userId: '@user:id', diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index b6900a9ab..cda14779c 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -216,6 +216,41 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { this._sessionStore.removeAllEndToEndSessions(); } + + // inbound group sessions + const ibGroupSessions = this._sessionStore.getAllEndToEndInboundGroupSessionKeys(); + if (Object.keys(ibGroupSessions).length > 0) { + let numIbSessions = 0; + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + // We always migrate inbound group sessions, even if we already have some + // in the new store. They should be be safe to migrate. + for (const s of ibGroupSessions) { + try { + this._cryptoStore.addEndToEndInboundGroupSession( + s.senderKey, s.sessionId, + JSON.parse( + this._sessionStore.getEndToEndInboundGroupSession( + s.senderKey, s.sessionId, + ), + ), txn, + ); + } catch (e) { + console.warn( + "Failed to migrate session " + s.senderKey + "/" + + s.sessionId + ": " + e.stack || e, + ); + } + ++numIbSessions; + } + console.log( + "Migrated " + numIbSessions + + " inbound group sessions from session store", + ); + }, + ); + this._sessionStore.removeAllEndToEndInboundGroupSessions(); + } }; /** @@ -773,68 +808,62 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) { */ /** - * store an InboundGroupSession in the session store + * Unpickle a session from a sessionData object and invoke the given function. + * The session is valid only until func returns. * - * @param {string} senderCurve25519Key - * @param {string} sessionId - * @param {InboundGroupSessionData} sessionData - * @private + * @param {Object} sessionData Object describing the session. + * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session + * @return {*} result of func */ -OlmDevice.prototype._saveInboundGroupSession = function( - senderCurve25519Key, sessionId, sessionData, -) { - this._sessionStore.storeEndToEndInboundGroupSession( - senderCurve25519Key, sessionId, JSON.stringify(sessionData), - ); -}; - -/** - * extract an InboundGroupSession from the session store and call the given function - * - * @param {string} roomId - * @param {string} senderKey - * @param {string} sessionId - * @param {function(Olm.InboundGroupSession, InboundGroupSessionData): T} func - * function to call. - * - * @return {null} the sessionId is unknown - * - * @return {T} result of func - * - * @private - * @template {T} - */ -OlmDevice.prototype._getInboundGroupSession = function( - roomId, senderKey, sessionId, func, -) { - let r = this._sessionStore.getEndToEndInboundGroupSession( - senderKey, sessionId, - ); - - if (r === null) { - return null; - } - - r = JSON.parse(r); - - // check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId !== r.room_id) { - throw new Error( - "Mismatched room_id for inbound group session (expected " + r.room_id + - ", was " + roomId + ")", - ); - } - +OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) { const session = new Olm.InboundGroupSession(); try { - session.unpickle(this._pickleKey, r.session); - return func(session, r); + session.unpickle(this._pickleKey, sessionData.session); + return func(session); } finally { session.free(); } }; +/** + * extract an InboundGroupSession from the crypto store and call the given function + * + * @param {string} roomId The room ID to extract the session for, or null to fetch + * sessions for any room. + * @param {string} senderKey + * @param {string} sessionId + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func + * function to call. + * + * @private + */ +OlmDevice.prototype._getInboundGroupSession = function( + roomId, senderKey, sessionId, txn, func, +) { + this._cryptoStore.getEndToEndInboundGroupSession( + senderKey, sessionId, txn, (sessionData) => { + if (sessionData === null) { + func(null); + return; + } + + // if we were given a room ID, check that the it matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId !== null && roomId !== sessionData.room_id) { + throw new Error( + "Mismatched room_id for inbound group session (expected " + + sessionData.room_id + ", was " + roomId + ")", + ); + } + + this._unpickleInboundGroupSession(sessionData, (session) => { + func(session, sessionData); + }); + }, + ); +}; + /** * Add an inbound group session to the session store * @@ -853,100 +882,52 @@ OlmDevice.prototype.addInboundGroupSession = async function( sessionId, sessionKey, keysClaimed, exportFormat, ) { - const self = this; + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + /* if we already have this session, consider updating it */ + this._getInboundGroupSession( + roomId, senderKey, sessionId, txn, + (existingSession, existingSessionData) => { + if (existingSession) { + console.log( + "Update for megolm session " + senderKey + "/" + sessionId, + ); + // for now we just ignore updates. TODO: implement something here + return; + } - /* if we already have this session, consider updating it */ - function updateSession(session, sessionData) { - console.log("Update for megolm session " + senderKey + "/" + sessionId); - // for now we just ignore updates. TODO: implement something here + // new session. + const session = new Olm.InboundGroupSession(); + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + if (sessionId != session.session_id()) { + throw new Error( + "Mismatched group session ID from senderKey: " + + senderKey, + ); + } - return true; - } + const sessionData = { + room_id: roomId, + session: session.pickle(this._pickleKey), + keysClaimed: keysClaimed, + forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, + }; - const r = this._getInboundGroupSession( - roomId, senderKey, sessionId, updateSession, - ); - - if (r !== null) { - return; - } - - // new session. - const session = new Olm.InboundGroupSession(); - try { - if (exportFormat) { - session.import_session(sessionKey); - } else { - session.create(sessionKey); - } - if (sessionId != session.session_id()) { - throw new Error( - "Mismatched group session ID from senderKey: " + senderKey, + this._cryptoStore.addEndToEndInboundGroupSession( + senderKey, sessionId, sessionData, txn, + ); + } finally { + session.free(); + } + }, ); - } - - const sessionData = { - room_id: roomId, - session: session.pickle(this._pickleKey), - keysClaimed: keysClaimed, - forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, - }; - - self._saveInboundGroupSession( - senderKey, sessionId, sessionData, - ); - } finally { - session.free(); - } -}; - - -/** - * Add a previously-exported inbound group session to the session store - * - * @param {module:crypto/OlmDevice.MegolmSessionData} data session data - */ -OlmDevice.prototype.importInboundGroupSession = async function(data) { - /* if we already have this session, consider updating it */ - function updateSession(session, sessionData) { - console.log("Update for megolm session " + data.sender_key + "|" + - data.session_id); - // for now we just ignore updates. TODO: implement something here - - return true; - } - - const r = this._getInboundGroupSession( - data.room_id, data.sender_key, data.session_id, updateSession, + }, ); - - if (r !== null) { - return; - } - - // new session. - const session = new Olm.InboundGroupSession(); - try { - session.import_session(data.session_key); - if (data.session_id != session.session_id()) { - throw new Error( - "Mismatched group session ID from senderKey: " + data.sender_key, - ); - } - - const sessionData = { - room_id: data.room_id, - session: session.pickle(this._pickleKey), - keysClaimed: data.sender_claimed_keys, - forwardingCurve25519KeyChain: data.forwarding_curve25519_key_chain, - }; - - this._saveInboundGroupSession( - data.sender_key, data.session_id, sessionData, - ); - } finally { - session.free(); - } }; /** @@ -968,51 +949,68 @@ OlmDevice.prototype.importInboundGroupSession = async function(data) { OlmDevice.prototype.decryptGroupMessage = async function( roomId, senderKey, sessionId, body, eventId, timestamp, ) { - const self = this; + let result; - function decrypt(session, sessionData) { - const res = session.decrypt(body); + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + this._getInboundGroupSession( + roomId, senderKey, sessionId, txn, (session, sessionData) => { + if (session === null) { + result = null; + return; + } + const res = session.decrypt(body); - let plaintext = res.plaintext; - if (plaintext === undefined) { - // Compatibility for older olm versions. - plaintext = res; - } else { - // Check if we have seen this message index before to detect replay attacks. - // If the event ID and timestamp are specified, and the match the event ID - // and timestamp from the last time we used this message index, then we - // don't consider it a replay attack. - const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index; - if (messageIndexKey in self._inboundGroupSessionMessageIndexes) { - const msgInfo = self._inboundGroupSessionMessageIndexes[messageIndexKey]; - if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) { - throw new Error( - "Duplicate message index, possible replay attack: " + - messageIndexKey, + let plaintext = res.plaintext; + if (plaintext === undefined) { + // Compatibility for older olm versions. + plaintext = res; + } else { + // Check if we have seen this message index before to detect replay attacks. + // If the event ID and timestamp are specified, and the match the event ID + // and timestamp from the last time we used this message index, then we + // don't consider it a replay attack. + const messageIndexKey = ( + senderKey + "|" + sessionId + "|" + res.message_index + ); + if (messageIndexKey in this._inboundGroupSessionMessageIndexes) { + const msgInfo = ( + this._inboundGroupSessionMessageIndexes[messageIndexKey] + ); + if ( + msgInfo.id !== eventId || + msgInfo.timestamp !== timestamp + ) { + throw new Error( + "Duplicate message index, possible replay attack: " + + messageIndexKey, + ); + } + } + this._inboundGroupSessionMessageIndexes[messageIndexKey] = { + id: eventId, + timestamp: timestamp, + }; + } + + sessionData.session = session.pickle(this._pickleKey); + this._cryptoStore.storeEndToEndInboundGroupSession( + senderKey, sessionId, sessionData, txn, ); - } - } - self._inboundGroupSessionMessageIndexes[messageIndexKey] = { - id: eventId, - timestamp: timestamp, - }; - } - - sessionData.session = session.pickle(self._pickleKey); - self._saveInboundGroupSession( - senderKey, sessionId, sessionData, - ); - return { - result: plaintext, - keysClaimed: sessionData.keysClaimed || {}, - senderKey: senderKey, - forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [], - }; - } - - return this._getInboundGroupSession( - roomId, senderKey, sessionId, decrypt, + result = { + result: plaintext, + keysClaimed: sessionData.keysClaimed || {}, + senderKey: senderKey, + forwardingCurve25519KeyChain: ( + sessionData.forwardingCurve25519KeyChain || [] + ), + }; + }, + ); + }, ); + + return result; }; /** @@ -1025,25 +1023,33 @@ OlmDevice.prototype.decryptGroupMessage = async function( * @returns {Promise} true if we have the keys to this session */ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) { - const s = this._sessionStore.getEndToEndInboundGroupSession( - senderKey, sessionId, + let result; + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + this._cryptoStore.getEndToEndInboundGroupSession( + senderKey, sessionId, txn, (sessionData) => { + if (sessionData === null) { + result = false; + return; + } + + if (roomId !== sessionData.room_id) { + console.warn( + `requested keys for inbound group session ${senderKey}|` + + `${sessionId}, with incorrect room_id ` + + `(expected ${sessionData.room_id}, ` + + `was ${roomId})`, + ); + result = false; + } else { + result = true; + } + }, + ); + }, ); - if (s === null) { - return false; - } - - const r = JSON.parse(s); - if (roomId !== r.room_id) { - console.warn( - `requested keys for inbound group session ${senderKey}|` + - `${sessionId}, with incorrect room_id (expected ${r.room_id}, ` + - `was ${roomId})`, - ); - return false; - } - - return true; + return result; }; /** @@ -1063,24 +1069,33 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se OlmDevice.prototype.getInboundGroupSessionKey = async function( roomId, senderKey, sessionId, ) { - function getKey(session, sessionData) { - const messageIndex = session.first_known_index(); + let result; + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + this._getInboundGroupSession( + roomId, senderKey, sessionId, txn, (session, sessionData) => { + if (session === null) { + result = null; + return; + } + const messageIndex = session.first_known_index(); - const claimedKeys = sessionData.keysClaimed || {}; - const senderEd25519Key = claimedKeys.ed25519 || null; + const claimedKeys = sessionData.keysClaimed || {}; + const senderEd25519Key = claimedKeys.ed25519 || null; - return { - "chain_index": messageIndex, - "key": session.export_session(messageIndex), - "forwarding_curve25519_key_chain": - sessionData.forwardingCurve25519KeyChain || [], - "sender_claimed_ed25519_key": senderEd25519Key, - }; - } - - return this._getInboundGroupSession( - roomId, senderKey, sessionId, getKey, + result = { + "chain_index": messageIndex, + "key": session.export_session(messageIndex), + "forwarding_curve25519_key_chain": + sessionData.forwardingCurve25519KeyChain || [], + "sender_claimed_ed25519_key": senderEd25519Key, + }; + }, + ); + }, ); + + return result; }; /** @@ -1088,37 +1103,24 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function( * * @param {string} senderKey base64-encoded curve25519 key of the sender * @param {string} sessionId session identifier - * @return {Promise} exported session data + * @param {string} sessionData The session object from the store + * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data */ -OlmDevice.prototype.exportInboundGroupSession = async function(senderKey, sessionId) { - const s = this._sessionStore.getEndToEndInboundGroupSession( - senderKey, sessionId, - ); - - if (s === null) { - throw new Error("Unknown inbound group session [" + senderKey + "," + - sessionId + "]"); - } - const r = JSON.parse(s); - - const session = new Olm.InboundGroupSession(); - try { - session.unpickle(this._pickleKey, r.session); - +OlmDevice.prototype.exportInboundGroupSession = function( + senderKey, sessionId, sessionData, +) { + return this._unpickleInboundGroupSession(sessionData, (session) => { const messageIndex = session.first_known_index(); return { "sender_key": senderKey, - "sender_claimed_keys": r.keysClaimed, - "room_id": r.room_id, + "sender_claimed_keys": sessionData.keysClaimed, + "room_id": sessionData.room_id, "session_id": sessionId, "session_key": session.export_session(messageIndex), - "forwarding_curve25519_key_chain": - session.forwardingCurve25519KeyChain || [], + "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], }; - } finally { - session.free(); - } + }); }; // Utilities diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 372caaf17..7534b3ed1 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -927,10 +927,18 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function( * @param {module:crypto/OlmDevice.MegolmSessionData} session */ MegolmDecryption.prototype.importRoomKey = function(session) { - this._olmDevice.importInboundGroupSession(session); - - // have another go at decrypting events sent with this session. - this._retryDecryption(session.sender_key, session.session_id); + return this._olmDevice.addInboundGroupSession( + session.room_id, + session.sender_key, + session.forwarding_curve25519_key_chain, + session.session_id, + session.session_key, + session.sender_claimed_keys, + true, + ).then(() => { + // have another go at decrypting events sent with this session. + this._retryDecryption(session.sender_key, session.session_id); + }); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index 41b76b020..6b1d8f477 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -700,21 +700,25 @@ Crypto.prototype.isRoomEncrypted = function(roomId) { /** * Get a list containing all of the room keys * - * @return {module:client.Promise} a promise which resolves to a list of - * session export objects + * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects */ -Crypto.prototype.exportRoomKeys = function() { - return Promise.map( - this._sessionStore.getAllEndToEndInboundGroupSessionKeys(), - (s) => { - return this._olmDevice.exportInboundGroupSession( - s.senderKey, s.sessionId, - ).then((sess) => { +Crypto.prototype.exportRoomKeys = async function() { + const exportedSessions = []; + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => { + if (s === null) return; + + const sess = this._olmDevice.exportInboundGroupSession( + s.senderKey, s.sessionId, s.sessionData, + ); sess.algorithm = olmlib.MEGOLM_ALGORITHM; - return sess; + exportedSessions.push(sess); }); }, ); + + return exportedSessions; }; /** diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index f3928632c..1a5442c13 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -1,7 +1,7 @@ import Promise from 'bluebird'; import utils from '../../utils'; -export const VERSION = 3; +export const VERSION = 4; /** * Implementation of a CryptoStore which is backed by an existing @@ -258,6 +258,8 @@ export class Backend { return promiseifyTxn(txn); } + // Olm Account + getAccount(txn, func) { const objectStore = txn.objectStore("account"); const getReq = objectStore.get("-"); @@ -275,6 +277,8 @@ export class Backend { objectStore.put(newData, "-"); } + // Olm Sessions + countEndToEndSessions(txn, func) { const objectStore = txn.objectStore("sessions"); const countReq = objectStore.count(); @@ -324,6 +328,69 @@ export class Backend { objectStore.put({deviceKey, sessionId, session}); } + // Inbound group sessions + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + const objectStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.get([senderCurve25519Key, sessionId]); + getReq.onsuccess = function() { + try { + if (getReq.result) { + func(getReq.result.session); + } else { + func(null); + } + } catch (e) { + abortWithException(txn, e); + } + }; + } + + getAllEndToEndInboundGroupSessions(txn, func) { + const objectStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.openCursor(); + getReq.onsuccess = function() { + const cursor = getReq.result; + if (cursor) { + try { + func({ + senderKey: cursor.value.senderCurve25519Key, + sessionId: cursor.value.sessionId, + sessionData: cursor.value.session, + }); + } catch (e) { + abortWithException(txn, e); + } + cursor.continue(); + } else { + try { + func(null); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const objectStore = txn.objectStore("inbound_group_sessions"); + const addReq = objectStore.add({ + senderCurve25519Key, sessionId, session: sessionData, + }); + addReq.onerror = () => { + abortWithException(txn, new Error( + "Failed to add inbound group session - session may already exist", + )); + }; + } + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const objectStore = txn.objectStore("inbound_group_sessions"); + objectStore.put({ + senderCurve25519Key, sessionId, session: sessionData, + }); + } + doTxn(mode, stores, func) { const txn = this._db.transaction(stores, mode); const promise = promiseifyTxn(txn); @@ -351,6 +418,11 @@ export function upgradeDatabase(db, oldVersion) { }); sessionsStore.createIndex("deviceKey", "deviceKey"); } + if (oldVersion < 4) { + db.createObjectStore("inbound_group_sessions", { + keyPath: ["senderCurve25519Key", "sessionId"], + }); + } // Expand as needed. } @@ -376,13 +448,28 @@ function abortWithException(txn, e) { // We could alternatively make the thing we pass back to the app // an object containing the transaction and exception. txn._mx_abortexception = e; - txn.abort(); + try { + txn.abort(); + } catch (e) { + // sometimes we won't be able to abort the transaction + // (ie. if it's aborted or completed) + } } function promiseifyTxn(txn) { return new Promise((resolve, reject) => { - txn.oncomplete = resolve; - txn.onerror = reject; + txn.oncomplete = () => { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } + resolve(); + }; + txn.onerror = () => { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } + reject(); + }; txn.onabort = () => reject(txn._mx_abortexception); }); } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 4a1e26200..6758e5da9 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -227,6 +227,8 @@ export default class IndexedDBCryptoStore { }); } + // Olm Account + /* * Get the account pickle from the store. * This requires an active transaction. See doTxn(). @@ -249,6 +251,8 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().storeAccount(txn, newData); } + // Olm sessions + /** * Returns the number of end-to-end sessions in the store * @param {*} txn An active transaction. See doTxn(). @@ -296,6 +300,64 @@ export default class IndexedDBCryptoStore { ); } + // Inbound group saessions + + /** + * Retrieve the end-to-end inbound group session for a given + * server key and session ID + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to Base64 end-to-end session. + */ + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + this._backendPromise.value().getEndToEndInboundGroupSession( + senderCurve25519Key, sessionId, txn, func, + ); + } + + /** + * Fetches all inbound group sessions in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called once for each group session + * in the store with an object having keys {senderKey, sessionId, + * sessionData}, then once with null to indicate the end of the list. + */ + getAllEndToEndInboundGroupSessions(txn, func) { + this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func); + } + + /** + * Adds an end-to-end inbound group session to the store. + * If there already exists an inbound group session with the same + * senderCurve25519Key and sessionID, the session will not be added. + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {object} sessionData The session data structure + * @param {*} txn An active transaction. See doTxn(). + */ + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._backendPromise.value().addEndToEndInboundGroupSession( + senderCurve25519Key, sessionId, sessionData, txn, + ); + } + + /** + * Writes an end-to-end inbound group session to the store. + * If there already exists an inbound group session with the same + * senderCurve25519Key and sessionID, it will be overwritten. + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {object} sessionData The session data structure + * @param {*} txn An active transaction. See doTxn(). + */ + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._backendPromise.value().storeEndToEndInboundGroupSession( + senderCurve25519Key, sessionId, sessionData, txn, + ); + } + /** * Perform a transaction on the crypto store. Any store methods * that require a transaction (txn) object to be passed in may @@ -326,3 +388,4 @@ export default class IndexedDBCryptoStore { IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; +IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 4f9855a18..df1bb087a 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -29,11 +29,16 @@ import MemoryCryptoStore from './memory-crypto-store.js'; const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; function keyEndToEndSessions(deviceKey) { return E2E_PREFIX + "sessions/" + deviceKey; } +function keyEndToEndInboundGroupSession(senderKey, sessionId) { + return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; +} + /** * @implements {module:crypto/store/base~CryptoStore} */ @@ -43,6 +48,8 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { this.store = global.localStorage; } + // Olm Sessions + countEndToEndSessions(txn, func) { let count = 0; for (let i = 0; i < this.store.length; ++i) { @@ -72,6 +79,54 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } + // Inbound Group Sessions + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + func(getJsonItem( + this.store, + keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), + )); + } + + getAllEndToEndInboundGroupSessions(txn, func) { + for (let i = 0; i < this.store.length; ++i) { + const key = this.store.key(i); + if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + + func({ + senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43), + sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44), + sessionData: getJsonItem(this.store, key), + }); + } + } + func(null); + } + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const existing = getJsonItem( + this.store, + keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), + ); + if (!existing) { + this.storeEndToEndInboundGroupSession( + senderCurve25519Key, sessionId, sessionData, txn, + ); + } + } + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + setJsonItem( + this.store, + keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), + sessionData, + ); + } + /** * Delete all data from this store. * @@ -82,6 +137,8 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { return Promise.resolve(); } + // Olm account + getAccount(txn, func) { const account = this.store.getItem(KEY_END_TO_END_ACCOUNT); func(account); diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 6b3a0c9e4..cefb80ee8 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -34,6 +34,8 @@ export default class MemoryCryptoStore { // Map of {devicekey -> {sessionId -> session pickle}} this._sessions = {}; + // Map of {senderCurve25519Key+'/'+sessionId -> session data object} + this._inboundGroupSessions = {}; } /** @@ -201,6 +203,8 @@ export default class MemoryCryptoStore { return Promise.resolve(null); } + // Olm Account + getAccount(txn, func) { func(this._account); } @@ -209,6 +213,8 @@ export default class MemoryCryptoStore { this._account = newData; } + // Olm Sessions + countEndToEndSessions(txn, func) { return Object.keys(this._sessions).length; } @@ -231,6 +237,40 @@ export default class MemoryCryptoStore { deviceSessions[sessionId] = session; } + // Inbound Group Sessions + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null); + } + + getAllEndToEndInboundGroupSessions(txn, func) { + for (const key of Object.keys(this._inboundGroupSessions)) { + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + + func({ + senderKey: key.substr(0, 43), + sessionId: key.substr(44), + sessionData: this._inboundGroupSessions[key], + }); + } + func(null); + } + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const k = senderCurve25519Key+'/'+sessionId; + if (this._inboundGroupSessions[k] === undefined) { + this._inboundGroupSessions[k] = sessionData; + } + } + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; + } + + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } diff --git a/src/store/session/webstorage.js b/src/store/session/webstorage.js index 675de187a..0fa813a27 100644 --- a/src/store/session/webstorage.js +++ b/src/store/session/webstorage.js @@ -178,9 +178,8 @@ WebStorageSessionStore.prototype = { return this.store.getItem(key); }, - storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) { - const key = keyEndToEndInboundGroupSession(senderKey, sessionId); - return this.store.setItem(key, pickledSession); + removeAllEndToEndInboundGroupSessions: function() { + removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/'); }, /**