diff --git a/spec/test-utils.js b/spec/test-utils.js index b5d53df83..d05e5004b 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -193,6 +193,12 @@ module.exports.MockStorageApi = function() { this.data = {}; }; module.exports.MockStorageApi.prototype = { + get length() { + return Object.keys(this.data).length; + }, + key: function(i) { + return Object.keys(this.data)[i]; + }, setItem: function(k, v) { this.data[k] = v; }, diff --git a/src/client.js b/src/client.js index de8cfb19e..06b4b4fa3 100644 --- a/src/client.js +++ b/src/client.js @@ -461,6 +461,21 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) { return this._crypto.isRoomEncrypted(roomId); }; +/** + * Get a list containing all of the room keys + * + * This should be encrypted before returning it to the user. + * + * @return {module:client.Promise} a promise which resolves to a list of + * session export objects + */ +MatrixClient.prototype.exportRoomKeys = function() { + if (!this._crypto) { + return q.reject(new Error("End-to-end encryption disabled")); + } + return this._crypto.exportRoomKeys(); +}; + /** * Decrypt a received event according to the algorithm specified in the event. * diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index e01b56d20..9dbc2683c 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -739,6 +739,41 @@ OlmDevice.prototype.decryptGroupMessage = function( ); }; +/** + * Export an inbound group session + * + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @return {object} exported session data + */ +OlmDevice.prototype.exportInboundGroupSession = 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); + + const messageIndex = session.first_known_index(); + + return { + "sender_key": senderKey, + "sender_claimed_keys": r.keysClaimed, + "room_id": r.room_id, + "session_id": sessionId, + "session_key": session.export_session(messageIndex), + }; + } finally { + session.free(); + } +}; // Utilities // ========= diff --git a/src/crypto/index.js b/src/crypto/index.js index 6e54d936a..871d1bf95 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -897,6 +897,28 @@ Crypto.prototype.isRoomEncrypted = function(roomId) { return Boolean(this._roomEncryptors[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 + */ +Crypto.prototype.exportRoomKeys = function() { + return q( + this._sessionStore.getAllEndToEndInboundGroupSessionKeys().map( + (s) => { + const sess = this._olmDevice.exportInboundGroupSession( + s.senderKey, s.sessionId + ); + + sess.algorithm = olmlib.MEGOLM_ALGORITHM; + return sess; + } + ) + ); +}; + /** * Encrypt an event according to the configuration of the room, if necessary. * diff --git a/src/store/session/webstorage.js b/src/store/session/webstorage.js index 155bb5652..2e1e52a23 100644 --- a/src/store/session/webstorage.js +++ b/src/store/session/webstorage.js @@ -37,7 +37,10 @@ function WebStorageSessionStore(webStore) { this.store = webStore; if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) || - !utils.isFunction(webStore.removeItem)) { + !utils.isFunction(webStore.removeItem) || + !utils.isFunction(webStore.key) || + typeof(webStore.length) !== 'number' + ) { throw new Error( "Supplied webStore does not meet the WebStorage API interface" ); @@ -120,6 +123,32 @@ WebStorageSessionStore.prototype = { return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); }, + /** + * Retrieve a list of all known inbound group sessions + * + * @return {{senderKey: string, sessionId: string}} + */ + getAllEndToEndInboundGroupSessionKeys: function() { + const prefix = E2E_PREFIX + 'inboundgroupsessions/'; + const result = []; + for (let i = 0; i < this.store.length; i++) { + const key = this.store.key(i); + if (!key.startsWith(prefix)) { + continue; + } + // 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). + + result.push({ + senderKey: key.substr(prefix.length, 43), + sessionId: key.substr(prefix.length + 44), + }); + } + return result; + }, + getEndToEndInboundGroupSession: function(senderKey, sessionId) { let key = keyEndToEndInboundGroupSession(senderKey, sessionId); return this.store.getItem(key);