From d8bcc4e3f1bd743810c5b07d00baaa9628ff5cda Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Oct 2018 13:34:22 -0600 Subject: [PATCH 01/54] Initial support for specifying which servers to try in joinRoom This has a bug when using browser-request where the query string for `server_name: [a, b]` comes out as `?server_name=a,b` instead of `?server_name=a&server_name=b`. This is due to browser-request not supporting the same qs options as request, so the qsStringifyOptions do nothing. --- src/client.js | 11 ++++++++++- src/http-api.js | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index be4e5857b..1956b9225 100644 --- a/src/client.js +++ b/src/client.js @@ -921,6 +921,8 @@ MatrixClient.prototype.isUserIgnored = function(userId) { * Default: true. * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, * the signing URL is passed in this parameter. + * @param {string[]} opts.viaServers The server names to try and join through in + * addition to those that are automatically chosen. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: Room object. * @return {module:http-api.MatrixError} Rejects: with an error response. @@ -949,6 +951,13 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { ); } + const queryString = {}; + if (opts.viaServers) { + queryString["server_name"] = opts.viaServers; + } + + const reqOpts = {qsStringifyOptions: {arrayFormat: 'repeat'}}; + const defer = Promise.defer(); const self = this; @@ -959,7 +968,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { } const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); - return self._http.authedRequest(undefined, "POST", path, undefined, data); + return self._http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); }).then(function(res) { const roomId = res.room_id; const syncApi = new SyncApi(self, self._clientOpts); diff --git a/src/http-api.js b/src/http-api.js index b753d6bf8..ca71d5668 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -668,6 +668,9 @@ module.exports.MatrixHttpApi.prototype = { * @param {function=} opts.bodyParser function to parse the body of the * response before passing it to the promise and callback. * + * @param (object=} opts.qsStringifyOptions options for stringifying the + * query string. + * * @return {module:client.Promise} a promise which resolves to either the * response object (if this.opts.onlyData is truthy), or the parsed * body. Rejects @@ -752,6 +755,8 @@ module.exports.MatrixHttpApi.prototype = { method: method, withCredentials: false, qs: queryParams, + //qsStringifyOptions: opts.qsStringifyOptions, + useQuerystring: true, body: data, json: false, timeout: localTimeoutMs, From 73dd07aadf66cdffb7ab415dc0676d3836804872 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 11:14:59 +0100 Subject: [PATCH 02/54] Prepare changelog for v0.12.1-rc.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82902fc22..bb0a47560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +Changes in [0.12.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1-rc.1) (2018-10-24) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0...v0.12.1-rc.1) + + * Add repository type to package.json to make it valid + [\#762](https://github.com/matrix-org/matrix-js-sdk/pull/762) + * Add getMediaConfig() + [\#761](https://github.com/matrix-org/matrix-js-sdk/pull/761) + * add new examples, to be expanded into a post + [\#739](https://github.com/matrix-org/matrix-js-sdk/pull/739) + Changes in [0.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0) (2018-10-16) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0-rc.1...v0.12.0) From f5832423f420b2b688c0674c09c5c6b069ff5db7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 11:14:59 +0100 Subject: [PATCH 03/54] v0.12.1-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88f87f90b..211c3c30b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.0", + "version": "0.12.1-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 6033b7b886e0d4190df520c57089fc8ceb5aff4e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Oct 2018 16:36:00 -0600 Subject: [PATCH 04/54] Update request and browserify; Use request in the browser --- browser-index.js | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/browser-index.js b/browser-index.js index 66a6036f2..32b62640c 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,5 +1,5 @@ var matrixcs = require("./lib/matrix"); -matrixcs.request(require("browser-request")); +matrixcs.request(require("request")); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/package.json b/package.json index 88f87f90b..ce88c7422 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", - "request": "^2.53.0" + "request": "^2.88.0" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -65,7 +65,7 @@ "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.18.0", - "browserify": "^14.0.0", + "browserify": "^16.2.3", "browserify-shim": "^3.8.13", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", @@ -81,7 +81,7 @@ "source-map-support": "^0.4.11", "sourceify": "^0.1.0", "uglify-js": "^2.8.26", - "watchify": "^3.2.1" + "watchify": "^3.11.0" }, "browserify": { "transform": [ From 0c540ac8de7f36bd347ecbe3ad5993747cd24351 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Oct 2018 16:36:12 -0600 Subject: [PATCH 05/54] Re-add the querystring options --- src/http-api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http-api.js b/src/http-api.js index ca71d5668..65521df69 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -755,7 +755,7 @@ module.exports.MatrixHttpApi.prototype = { method: method, withCredentials: false, qs: queryParams, - //qsStringifyOptions: opts.qsStringifyOptions, + qsStringifyOptions: opts.qsStringifyOptions, useQuerystring: true, body: data, json: false, From 2cccb8b450eaebeebe3c0265e1e06da7679ed8c9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Oct 2018 16:57:36 -0600 Subject: [PATCH 06/54] Install memfs because webpack is made of fail --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ce88c7422..0c3fa3643 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", + "memfs": "^2.10.1", "request": "^2.88.0" }, "devDependencies": { From 568ff5a3f576cf5b07b00c04fb9b47a8d82c07f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Oct 2018 14:36:14 -0600 Subject: [PATCH 07/54] Appease the linter --- src/client.js | 3 ++- src/http-api.js | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index 1956b9225..77d62c6f0 100644 --- a/src/client.js +++ b/src/client.js @@ -968,7 +968,8 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { } const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); - return self._http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); + return self._http.authedRequest( + undefined, "POST", path, queryString, data, reqOpts); }).then(function(res) { const roomId = res.room_id; const syncApi = new SyncApi(self, self._clientOpts); diff --git a/src/http-api.js b/src/http-api.js index 65521df69..c29250d2a 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -668,9 +668,6 @@ module.exports.MatrixHttpApi.prototype = { * @param {function=} opts.bodyParser function to parse the body of the * response before passing it to the promise and callback. * - * @param (object=} opts.qsStringifyOptions options for stringifying the - * query string. - * * @return {module:client.Promise} a promise which resolves to either the * response object (if this.opts.onlyData is truthy), or the parsed * body. Rejects From a76f0c7cb417946cba7a5e2a86a5d062c6e4f0a2 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 26 Oct 2018 22:06:33 -0500 Subject: [PATCH 08/54] Update babel-eslint to 8.1.1 Signed-off-by: Aaron Raimist --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f2ff9630..b718feeef 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "devDependencies": { "babel-cli": "^6.18.0", - "babel-eslint": "^7.1.1", + "babel-eslint": "^8.1.1", "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.18.0", From 4cbf9c7f47cc3cf4783e234512280707f9fa9b1e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 29 Oct 2018 13:59:39 +0000 Subject: [PATCH 09/54] Prepare changelog for v0.12.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0a47560..4295d74fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1) + + * No changes since rc.1 + Changes in [0.12.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1-rc.1) (2018-10-24) ============================================================================================================ [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0...v0.12.1-rc.1) From 68c6393eb2b658120ad832f0866ba70a997f650c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 29 Oct 2018 13:59:39 +0000 Subject: [PATCH 10/54] v0.12.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 211c3c30b..8c9c9544f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.1-rc.1", + "version": "0.12.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From ad279dc566bbba4b4f7f00c7371fd09bbc31753f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 30 Oct 2018 16:05:44 +0100 Subject: [PATCH 11/54] correctly check for crypto being present --- src/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index 76122874b..1ce75ba82 100644 --- a/src/sync.js +++ b/src/sync.js @@ -516,7 +516,7 @@ SyncApi.prototype.sync = function() { console.warn("InvalidStoreError: store is not usable: stopping sync."); return; } - if (this.opts.lazyLoadMembers && this._crypto) { + if (this.opts.lazyLoadMembers && this.opts.crypto) { this.opts.crypto.enableLazyLoading(); } await this.client._storeClientOptions(); From f2881126cd35fd97e09299222eb0b5cbcc4ee938 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 13:59:29 -0600 Subject: [PATCH 12/54] Manually construct query strings for browser-request instances Because `request` just doesn't work for us in the browser, but `browser-request` is fine despite us having to do our own query strings. Fixes https://github.com/vector-im/riot-web/issues/7620 --- browser-index.js | 4 +++- package.json | 1 + src/http-api.js | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/browser-index.js b/browser-index.js index 32b62640c..c08249231 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,5 +1,7 @@ var matrixcs = require("./lib/matrix"); -matrixcs.request(require("request")); +var request = require("browser-request"); +request.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide +matrixcs.request(request); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/package.json b/package.json index 5dfc84c94..1f421a003 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "content-type": "^1.0.2", "loglevel": "1.6.1", "memfs": "^2.10.1", + "query-string": "^6.2.0", "request": "^2.88.0" }, "devDependencies": { diff --git a/src/http-api.js b/src/http-api.js index c29250d2a..ab4b749ba 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -19,6 +19,7 @@ limitations under the License. * @module http-api */ import Promise from 'bluebird'; +const queryString = require('query-string'); const parseContentType = require('content-type').parse; const utils = require("./utils"); @@ -746,6 +747,9 @@ module.exports.MatrixHttpApi.prototype = { const reqPromise = defer.promise; try { + if (this.opts.request.enableConstructionOfQueryString) { + queryParams = queryString.stringify(queryParams, opts.qsStringifyOptions); + } req = this.opts.request( { uri: uri, From 68b65dd357b7071d4dad299ed48d9c277fc9904b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:12:41 -0600 Subject: [PATCH 13/54] Because uglify-js breaks everything --- browser-index.js | 2 +- src/http-api.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-index.js b/browser-index.js index c08249231..259e02015 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,6 +1,6 @@ var matrixcs = require("./lib/matrix"); var request = require("browser-request"); -request.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide +global.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide matrixcs.request(request); // just *accessing* indexedDB throws an exception in firefox with diff --git a/src/http-api.js b/src/http-api.js index ab4b749ba..1c210073d 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -747,7 +747,7 @@ module.exports.MatrixHttpApi.prototype = { const reqPromise = defer.promise; try { - if (this.opts.request.enableConstructionOfQueryString) { + if (global && global.enableConstructionOfQueryString) { queryParams = queryString.stringify(queryParams, opts.qsStringifyOptions); } req = this.opts.request( From 054aac17aa8870157a6ea2d740fca9aef6ef2af7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:25:25 -0600 Subject: [PATCH 14/54] Just use the interface provided to us rather than hack in a flag --- browser-index.js | 16 +++++++++++++--- src/http-api.js | 4 ---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/browser-index.js b/browser-index.js index 259e02015..9ca2bf5f1 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,7 +1,17 @@ var matrixcs = require("./lib/matrix"); -var request = require("browser-request"); -global.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide -matrixcs.request(request); +const request = require('browser-request'); +const queryString = require('query-string'); + +matrixcs.request((opts, fn) => { + // We manually fix the query string for browser-request because + // it doesn't correctly handle cases like ?via=one&via=two. Instead + // we mimic `request`'s query string interface to make it all work + // as expected. + // browser-request will happily take the constructed string as the + // query string without trying to modify it further. + opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions); + return request(opts, fn); +}); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/src/http-api.js b/src/http-api.js index 1c210073d..c29250d2a 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -19,7 +19,6 @@ limitations under the License. * @module http-api */ import Promise from 'bluebird'; -const queryString = require('query-string'); const parseContentType = require('content-type').parse; const utils = require("./utils"); @@ -747,9 +746,6 @@ module.exports.MatrixHttpApi.prototype = { const reqPromise = defer.promise; try { - if (global && global.enableConstructionOfQueryString) { - queryParams = queryString.stringify(queryParams, opts.qsStringifyOptions); - } req = this.opts.request( { uri: uri, From b57e858ad131c13d7789862ee310b5e6bda0cc9f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:27:51 -0600 Subject: [PATCH 15/54] We can't use arrow functions, apparently. --- browser-index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-index.js b/browser-index.js index 9ca2bf5f1..e9576626f 100644 --- a/browser-index.js +++ b/browser-index.js @@ -2,7 +2,7 @@ var matrixcs = require("./lib/matrix"); const request = require('browser-request'); const queryString = require('query-string'); -matrixcs.request((opts, fn) => { +matrixcs.request(function(opts, fn) { // We manually fix the query string for browser-request because // it doesn't correctly handle cases like ?via=one&via=two. Instead // we mimic `request`'s query string interface to make it all work From 2fb29ae8fd14515b6c3d176e93a5697f09c52d78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:59:57 -0600 Subject: [PATCH 16/54] Use the right query string lib --- browser-index.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-index.js b/browser-index.js index e9576626f..cc7e50fe8 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,6 +1,6 @@ var matrixcs = require("./lib/matrix"); const request = require('browser-request'); -const queryString = require('query-string'); +const queryString = require('qs'); matrixcs.request(function(opts, fn) { // We manually fix the query string for browser-request because diff --git a/package.json b/package.json index 1f421a003..26608e1e2 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "content-type": "^1.0.2", "loglevel": "1.6.1", "memfs": "^2.10.1", - "query-string": "^6.2.0", + "qs": "^6.5.2", "request": "^2.88.0" }, "devDependencies": { From e89879d8a61acc0496c28d676e28138856268e9d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 31 Oct 2018 13:07:31 -0600 Subject: [PATCH 17/54] Remove the request-only stuff we don't need anymore This was introduced in https://github.com/matrix-org/matrix-react-sdk/pull/2250 but can be pulled out due to https://github.com/matrix-org/matrix-js-sdk/pull/770. See https://github.com/vector-im/riot-web/issues/7634 for more information about the future. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 26608e1e2..4e1495ce0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "browser-request": "^0.3.3", "content-type": "^1.0.2", "loglevel": "1.6.1", - "memfs": "^2.10.1", "qs": "^6.5.2", "request": "^2.88.0" }, From 28540ad50a8fd93db22aea585ca3441071e6c7f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Nov 2018 12:59:38 +0000 Subject: [PATCH 18/54] Use the last olm session that got a message Implements https://github.com/matrix-org/matrix-doc/pull/1596 For https://github.com/vector-im/riot-web/issues/3822 Requires https://github.com/matrix-org/olm-backup/pull/77 (+release) --- src/crypto/OlmDevice.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 5ce7438a7..851cca185 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -558,13 +558,20 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK * @return {Promise} session id, or null if no established session */ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) { - const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey); - if (sessionIds.length === 0) { + const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey); + if (sessionInfos.length === 0) { return null; } - // Use the session with the lowest ID. - sessionIds.sort(); - return sessionIds[0]; + // Use the session that has most recently received a message + sessionInfos.sort((a, b) => { + if (a.lastReceivedMessageTs !== b.lastReceivedMessageTs) { + return a.lastReceivedMessageTs - b.lastReceivedMessageTs; + } else { + if (a.sessionId === b.sessionId) return 0; + return a.sessionId < b.sessionId ? -1 : 1; + } + }); + return sessionInfos[sessionInfos.length - 1].sessionId; }; /** @@ -589,6 +596,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) for (const sessionId of sessionIds) { this._unpickleSession(sessions[sessionId], (session) => { info.push({ + lastReceivedMessageTs: session.last_received_message_ts(), hasReceivedMessage: session.has_received_message(), sessionId: sessionId, }); @@ -649,6 +657,7 @@ OlmDevice.prototype.decryptMessage = async function( (txn) => { this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { payloadString = session.decrypt(messageType, ciphertext); + session.set_last_received_message_ts(Date.now()); this._saveSession(theirDeviceIdentityKey, session, txn); }); }, From 37f0a9ad7b2602918d8d914faadd6bdfcea1f164 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Nov 2018 13:54:41 +0000 Subject: [PATCH 19/54] Try tests on node 10 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 67382e760..823517fc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: - - node # Latest stable version of nodejs. + - "10.11.0" script: - ./travis.sh From 2a6a67c6cc849644a1c03e590ba5dec0151afca1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Nov 2018 17:08:43 +0000 Subject: [PATCH 20/54] Inbound session creation counts as a received message --- src/crypto/OlmDevice.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 851cca185..46f879ec2 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -509,6 +509,8 @@ OlmDevice.prototype.createInboundSession = async function( this._storeAccount(txn, account); const payloadString = session.decrypt(messageType, ciphertext); + // this counts as an received message + session.set_last_received_message_ts(Date.now()); this._saveSession(theirDeviceIdentityKey, session, txn); From c94382b46c586fbbb5e7c292ce42a649116a5117 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Nov 2018 12:05:13 +0000 Subject: [PATCH 21/54] A unit test for olm Megolm has plenty but none for just the olm layer by itself. I was playing with getting session re-establishment to work and a unit test came out. --- spec/unit/crypto/algorithms/olm.spec.js | 90 +++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 spec/unit/crypto/algorithms/olm.spec.js diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js new file mode 100644 index 000000000..e222e9694 --- /dev/null +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -0,0 +1,90 @@ +/* +Copyright 2018 New Vector Ltd + +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. +*/ + +try { + global.Olm = require('olm'); +} catch (e) { + console.warn("unable to run megolm tests: libolm not available"); +} + +import expect from 'expect'; +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'; + +import OlmDevice from '../../../../lib/crypto/OlmDevice'; + +function makeOlmDevice() { + const mockStorage = new MockStorageApi(); + const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); + const olmDevice = new OlmDevice(sessionStore, cryptoStore); + // expose for tests + olmDevice.__cryptoStore = cryptoStore; + return olmDevice; +} + +async function setupSession(initiator, opponent) { + await opponent.generateOneTimeKeys(1); + const keys = await opponent.getOneTimeKeys(); + const firstKey = Object.values(keys['curve25519'])[0]; + + const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key, firstKey); + return sid; +} + +describe("OlmDecryption", function() { + if (!global.Olm) { + console.warn('Not running megolm unit tests: libolm not present'); + return; + } + + let aliceOlmDevice; + let bobOlmDevice; + + beforeEach(async function() { + testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + + await global.Olm.init(); + + aliceOlmDevice = makeOlmDevice(); + bobOlmDevice = makeOlmDevice(); + await aliceOlmDevice.init(); + await bobOlmDevice.init(); + }); + + describe('olm', function() { + it("session reverted to a previous state can no longer decrypt messages", async function() { + const sid = await setupSession(aliceOlmDevice, bobOlmDevice); + //console.log("alice id key: " + aliceOlmDevice.deviceCurve25519Key); + //console.log("bob id key: " + bobOlmDevice.deviceCurve25519Key); + + const ciphertext = await aliceOlmDevice.encryptMessage( + bobOlmDevice.deviceCurve25519Key, + sid, + "The olm or proteus is an aquatic salamander in the family Proteidae", + ); + + const result = await bobOlmDevice.createInboundSession( + aliceOlmDevice.deviceCurve25519Key, + ciphertext.type, + ciphertext.body, + ); + expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae"); + }); + }); +}); From ec8b3ae5157aa2f5cf809733ba45513888aedde0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Nov 2018 12:33:46 +0000 Subject: [PATCH 22/54] Lint (and also name the test right) --- spec/unit/crypto/algorithms/olm.spec.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js index e222e9694..2dd6334cd 100644 --- a/spec/unit/crypto/algorithms/olm.spec.js +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -43,7 +43,9 @@ async function setupSession(initiator, opponent) { const keys = await opponent.getOneTimeKeys(); const firstKey = Object.values(keys['curve25519'])[0]; - const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key, firstKey); + const sid = await initiator.createOutboundSession( + opponent.deviceCurve25519Key, firstKey, + ); return sid; } @@ -68,7 +70,7 @@ describe("OlmDecryption", function() { }); describe('olm', function() { - it("session reverted to a previous state can no longer decrypt messages", async function() { + it("can decrypt messages", async function() { const sid = await setupSession(aliceOlmDevice, bobOlmDevice); //console.log("alice id key: " + aliceOlmDevice.deviceCurve25519Key); //console.log("bob id key: " + bobOlmDevice.deviceCurve25519Key); @@ -84,7 +86,9 @@ describe("OlmDecryption", function() { ciphertext.type, ciphertext.body, ); - expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae"); + expect(result.payload).toEqual( + "The olm or proteus is an aquatic salamander in the family Proteidae", + ); }); }); }); From 394e37f9ead40021779d4b351f63dd57fe0f0045 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 19:43:18 +0000 Subject: [PATCH 23/54] Set access_token and user_id after login in with username and password. --- src/base-apis.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index 6dc9169dd..d00ab64dc 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -277,7 +277,21 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, callback); + }, (error, response) => { + if (response && response.access_token) { + this._http.opts.accessToken = response.access_token; + } + + if (response && response.user_id) { + this.credentials = { + userId: response.user_id, + }; + } + + if(callback) { + callback(error, response); + } + }); }; /** From 3aabd63975fe760debbd8bb8727a7a7a98826222 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 21:49:17 +0000 Subject: [PATCH 24/54] Add function to get currently joined rooms. --- src/base-apis.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index 6dc9169dd..a0a842140 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -927,6 +927,14 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = ); }; +/** + * @return {module:client.Promise} Resolves: A list of the user's current rooms + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedRooms = function() { + const path = utils.encodeUri("/joined_rooms"); + return this._http.authedRequest(undefined, "GET", path); +}; // Room Directory operations // ========================= From a0d51803ed46bb3bec03c85ac6fbcede603c5de8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 00:08:04 +0000 Subject: [PATCH 25/54] Add function to get currently joined room members. --- src/base-apis.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index a0a842140..38de1d693 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -936,6 +936,20 @@ MatrixBaseApis.prototype.getJoinedRooms = function() { return this._http.authedRequest(undefined, "GET", path); }; +/** + * Retrieve membership info. for a room. + * @param {string} roomId ID of the room to get membership for + * @return {module:client.Promise} Resolves: A list of currently joined users + * and their profile data. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) { + const path = utils.encodeUri("/rooms/$roomId/joined_members", { + $roomId: roomId, + }); + return this._http.authedRequest(undefined, "GET", path); +}; + // Room Directory operations // ========================= From ff4125c11e809fb4b650f4df0fec5e042d14c07e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 5 Nov 2018 09:39:46 +0000 Subject: [PATCH 26/54] Remove unused stuff & comments --- spec/unit/crypto/algorithms/olm.spec.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js index 2dd6334cd..46fcff38b 100644 --- a/spec/unit/crypto/algorithms/olm.spec.js +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -33,8 +33,6 @@ function makeOlmDevice() { const sessionStore = new WebStorageSessionStore(mockStorage); const cryptoStore = new MemoryCryptoStore(mockStorage); const olmDevice = new OlmDevice(sessionStore, cryptoStore); - // expose for tests - olmDevice.__cryptoStore = cryptoStore; return olmDevice; } @@ -72,8 +70,6 @@ describe("OlmDecryption", function() { describe('olm', function() { it("can decrypt messages", async function() { const sid = await setupSession(aliceOlmDevice, bobOlmDevice); - //console.log("alice id key: " + aliceOlmDevice.deviceCurve25519Key); - //console.log("bob id key: " + bobOlmDevice.deviceCurve25519Key); const ciphertext = await aliceOlmDevice.encryptMessage( bobOlmDevice.deviceCurve25519Key, From 094598196a6e8166e2dd05ef6aaa0b01b7d0d9c1 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:02:30 +0000 Subject: [PATCH 27/54] Linting. --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index d00ab64dc..8836c59d8 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -288,7 +288,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) }; } - if(callback) { + if (callback) { callback(error, response); } }); From b716e71784492b2c90555daae5f0648e5edf1922 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:40:11 +0000 Subject: [PATCH 28/54] Refactor code to base 'login' method. --- src/base-apis.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 8836c59d8..ad0f84b45 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -262,7 +262,19 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) { utils.extend(login_data, data); return this._http.authedRequest( - callback, "POST", "/login", undefined, login_data, + (error, response) => { + if (loginType === "m.login.password" && response && + response.access_token && response.user_id) { + this._http.opts.accessToken = response.access_token; + this.credentials = { + userId: response.user_id, + }; + } + + if (callback) { + callback(error, response); + } + }, "POST", "/login", undefined, login_data, ); }; @@ -277,21 +289,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, (error, response) => { - if (response && response.access_token) { - this._http.opts.accessToken = response.access_token; - } - - if (response && response.user_id) { - this.credentials = { - userId: response.user_id, - }; - } - - if (callback) { - callback(error, response); - } - }); + }, callback); }; /** From f4abd7d027a921a7c34a3357b1da38f5fe907cde Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:06:39 +0000 Subject: [PATCH 29/54] Update CHANGELOG. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4295d74fe..8c5b7ec7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0) + +BREAKING CHANGE +---------------- + * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login. + Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1) From 23dfeb13dfd56bd0814da927cbe8fea63ec0fd7d Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:17:22 +0000 Subject: [PATCH 30/54] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5b7ec7d..666558997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0. BREAKING CHANGE ---------------- - * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login. + * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login with username and password. Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29) ================================================================================================== From d74ed508f99f0cf873f53c2f2f69cc1ba803adb6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 8 Nov 2018 19:09:28 +0000 Subject: [PATCH 31/54] Restart broken Olm sessions * Start a new Olm sessions with a device when we get an undecryptable message on it. * Send a dummy message on that sessions such that the other end knows about it. * Re-send any outstanding keyshare requests for that device. Also includes a unit test for megolm that isn't very related but came out as a result anyway. Includes https://github.com/matrix-org/matrix-js-sdk/pull/776 Fixes https://github.com/vector-im/riot-web/issues/3822 --- spec/unit/crypto.spec.js | 100 +++++++++++++++++- spec/unit/crypto/algorithms/megolm.spec.js | 93 +++++++++++++++- src/crypto/OlmDevice.js | 26 ++++- src/crypto/OutgoingRoomKeyRequestManager.js | 15 +++ src/crypto/algorithms/megolm.js | 87 ++++++++++++++- src/crypto/index.js | 94 +++++++++++++++- src/crypto/olmlib.js | 11 +- .../store/indexeddb-crypto-store-backend.js | 36 +++++++ src/crypto/store/indexeddb-crypto-store.js | 18 ++++ src/crypto/store/memory-crypto-store.js | 13 +++ 10 files changed, 476 insertions(+), 17 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index ee06ef369..47d7d2d67 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,9 +1,13 @@ - -"use strict"; import 'source-map-support/register'; import Crypto from '../../lib/crypto'; import expect from 'expect'; +import WebStorageSessionStore from '../../lib/store/session/webstorage'; +import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js'; +import MockStorageApi from '../MockStorageApi'; + +const EventEmitter = require("events").EventEmitter; + const sdk = require("../.."); const Olm = global.Olm; @@ -20,4 +24,96 @@ describe("Crypto", function() { it("Crypto exposes the correct olm library version", function() { expect(Crypto.getOlmVersion()[0]).toEqual(3); }); + + + describe('Session management', function() { + const otkResponse = { + one_time_keys: { + '@alice:home.server': { + aliceDevice: { + 'signed_curve25519:FLIBBLE': { + key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI', + signatures: { + '@alice:home.server': { + 'ed25519:aliceDevice': 'totally a valid signature', + }, + }, + }, + }, + }, + }, + }; + let crypto; + let mockBaseApis; + let mockRoomList; + + let fakeEmitter; + + beforeEach(async function() { + const mockStorage = new MockStorageApi(); + const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); + + cryptoStore.storeEndToEndDeviceData({ + devices: { + '@bob:home.server': { + 'BOBDEVICE': { + keys: { + 'curve25519:BOBDEVICE': 'this is a key', + }, + }, + }, + }, + trackingStatus: {}, + }); + + mockBaseApis = { + sendToDevice: expect.createSpy(), + }; + mockRoomList = {}; + + fakeEmitter = new EventEmitter(); + + crypto = new Crypto( + mockBaseApis, + sessionStore, + "@alice:home.server", + "FLIBBLE", + sessionStore, + cryptoStore, + mockRoomList, + ); + crypto.registerEventHandlers(fakeEmitter); + await crypto.init(); + }); + + afterEach(async function() { + await crypto.stop(); + }); + + it("restarts wedged Olm sessions", async function() { + const prom = new Promise((resolve) => { + mockBaseApis.claimOneTimeKeys = function() { + resolve(); + return otkResponse; + }; + }); + + fakeEmitter.emit('toDeviceEvent', { + getType: expect.createSpy().andReturn('m.room.message'), + getContent: expect.createSpy().andReturn({ + msgtype: 'm.bad.encrypted', + }), + getWireContent: expect.createSpy().andReturn({ + algorithm: 'm.olm.v1.curve25519-aes-sha2', + sender_key: 'this is a key', + }), + getSender: expect.createSpy().andReturn('@bob:home.server'), + }); + + console.log("waiting"); + await prom; + console.log("done"); + }); + }); }); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 6c777859e..641adb19c 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -18,6 +18,7 @@ import Crypto from '../../../../lib/crypto'; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; +const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const ROOM_ID = '!ROOM:ID'; @@ -34,9 +35,11 @@ describe("MegolmDecryption", function() { let mockCrypto; let mockBaseApis; - beforeEach(function() { + beforeEach(async function() { testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + await Olm.init(); + mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockBaseApis = {}; @@ -66,7 +69,6 @@ describe("MegolmDecryption", function() { describe('receives some keys:', function() { let groupSession; beforeEach(async function() { - await Olm.init(); groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -263,5 +265,92 @@ describe("MegolmDecryption", function() { // test is successful if no exception is thrown }); }); + + it("re-uses sessions for sequential messages", async function() { + const mockStorage = new MockStorageApi(); + const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); + + const olmDevice = new OlmDevice(sessionStore, cryptoStore); + olmDevice.verifySignature = expect.createSpy(); + await olmDevice.init(); + + mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({ + one_time_keys: { + '@alice:home.server': { + aliceDevice: { + 'signed_curve25519:flooble': { + key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI', + signatures: { + '@alice:home.server': { + 'ed25519:aliceDevice': 'totally valid', + }, + }, + }, + }, + }, + }, + })); + mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve()); + + mockCrypto.downloadKeys.andReturn(Promise.resolve({ + '@alice:home.server': { + aliceDevice: { + deviceId: 'aliceDevice', + isBlocked: expect.createSpy().andReturn(false), + isUnverified: expect.createSpy().andReturn(false), + getIdentityKey: expect.createSpy().andReturn( + 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE', + ), + getFingerprint: expect.createSpy().andReturn(''), + }, + }, + })); + + const megolmEncryption = new MegolmEncryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: mockBaseApis, + roomId: ROOM_ID, + config: { + rotation_period_ms: 9999999999999, + }, + }); + const mockRoom = { + getEncryptionTargetMembers: expect.createSpy().andReturn( + [{userId: "@alice:home.server"}], + ), + getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false), + }; + const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { + body: "Some text", + }); + expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled(); + + // this should have claimed a key for alice as it's starting a new session + expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled( + [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', + ); + expect(mockCrypto.downloadKeys).toHaveBeenCalledWith( + ['@alice:home.server'], false, + ); + expect(mockBaseApis.sendToDevice).toHaveBeenCalled(); + expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled( + [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', + ); + + mockBaseApis.claimOneTimeKeys.reset(); + + const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { + body: "Some more text", + }); + + // this should *not* have claimed a key as it should be using the same session + expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled(); + + // likewise they should show the same session ID + expect(ct2.session_id).toEqual(ct1.session_id); + }); }); }); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 46f879ec2..f9c907c78 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -461,6 +461,12 @@ OlmDevice.prototype.createOutboundSession = async function( session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); this._storeAccount(txn, account); + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session (storing the creation time separately would + // make the pickle longer and would not be useful otherwise). + session.set_last_received_message_ts(Date.now()); + this._saveSession(theirIdentityKey, session, txn); } finally { session.free(); @@ -725,7 +731,7 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) { */ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) { const pickled = this._outboundGroupSessionStore[sessionId]; - if (pickled === null) { + if (pickled === undefined) { throw new Error("Unknown outbound group session " + sessionId); } @@ -1059,6 +1065,8 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se * @param {string} roomId room in which the message was received * @param {string} senderKey base64-encoded curve25519 key of the sender * @param {string} sessionId session identifier + * @param {integer} chainIndex The chain index at which to export the session. + * If omitted, export at the first index we know about. * * @returns {Promise<{chain_index: number, key: string, * forwarding_curve25519_key_chain: Array, @@ -1066,9 +1074,12 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se * }>} * details of the session key. The key is a base64-encoded megolm key in * export format. + * + * @throws Error If the given chain index could not be obtained from the known + * index (ie. the given chain index is before the first we have). */ OlmDevice.prototype.getInboundGroupSessionKey = async function( - roomId, senderKey, sessionId, + roomId, senderKey, sessionId, chainIndex, ) { let result; await this._cryptoStore.doTxn( @@ -1079,14 +1090,19 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function( result = null; return; } - const messageIndex = session.first_known_index(); + + if (chainIndex === undefined) { + chainIndex = session.first_known_index(); + } + + const exportedSession = session.export_session(chainIndex); const claimedKeys = sessionData.keysClaimed || {}; const senderEd25519Key = claimedKeys.ed25519 || null; result = { - "chain_index": messageIndex, - "key": session.export_session(messageIndex), + "chain_index": chainIndex, + "key": exportedSession, "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [], "sender_claimed_ed25519_key": senderEd25519Key, diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 4c9b7cbf5..bfde6019b 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -244,6 +244,21 @@ export default class OutgoingRoomKeyRequestManager { }); } + /** + * Look for room key requests by target device and state + * + * @param {string} userId Target user ID + * @param {string} deviceId Target device ID + * + * @return {Promise} resolves to a list of all the + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + */ + getOutgoingSentRoomKeyRequest(userId, deviceId) { + return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget( + userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT], + ); + } + // start the background timer to send queued requests, if the timer isn't // already running _startTimer() { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index d8d8fd8f2..c9792b38e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -144,6 +144,11 @@ function MegolmEncryption(params) { // room). this._setupPromise = Promise.resolve(); + // Map of outbound sessions by sessions ID. Used if we need a particular + // session (the session we're currently using to send is always obtained + // using _setupPromise). + this._outboundSessions = {}; + // default rotation periods this._sessionRotationPeriodMsgs = 100; this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000; @@ -195,6 +200,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { if (!session) { logger.log(`Starting new megolm session for room ${self._roomId}`); session = await self._prepareNewSession(); + self._outboundSessions[session.sessionId] = session; } // now check if we need to share with any devices @@ -409,8 +415,87 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function( }; /** - * @private + * Re-shares a megolm session key with devices if the key has already been + * sent to them. * + * @param {string} senderKey The key of the originating device for the session + * @param {string} sessionId ID of the outbound session to share + * @param {string} userId ID of the user who owns the target device + * @param {module:crypto/deviceinfo} device The target device + */ +MegolmEncryption.prototype.reshareKeyWithDevice = async function( + senderKey, sessionId, userId, device, +) { + const obSessionInfo = this._outboundSessions[sessionId]; + if (!obSessionInfo) { + logger.debug("Session ID " + sessionId + " not found: not re-sharing keys"); + return; + } + + // The chain index of the key we previously sent this device + const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; + + // get the key from the inbound session: the outbound one will already + // have been ratcheted to the next chain index. + const key = await this._olmDevice.getInboundGroupSessionKey( + this._roomId, senderKey, sessionId, sentChainIndex, + ); + + if (!key) { + logger.warn( + "No outbound session key found for " + sessionId + ": not re-sharing keys", + ); + return; + } + + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, { + [userId]: { + [device.deviceId]: device, + }, + }, + ); + + const payload = { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, + }, + }; + + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + userId, + device, + payload, + ), + + await this._baseApis.sendToDevice("m.room.encrypted", { + [userId]: { + [device.deviceId]: encryptedContent, + }, + }); + logger.debug( + `Re-shared key for session ${sessionId} with {userId}:{device.deviceId}`, + ); +}; + +/** * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * * @param {object} devicesByUser diff --git a/src/crypto/index.js b/src/crypto/index.js index 082f6d2c9..54c6bb16a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1133,6 +1133,8 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onRoomKeyEvent(event); } else if (event.getType() == "m.room_key_request") { this._onRoomKeyRequestEvent(event); + } else if (event.getContent().msgtype === "m.bad.encrypted") { + this._onToDeviceBadEncrypted(event); } else if (event.isBeingDecrypted()) { // once the event has been decrypted, try again event.once('Event.decrypted', (ev) => { @@ -1162,6 +1164,74 @@ Crypto.prototype._onRoomKeyEvent = function(event) { alg.onRoomKeyEvent(event); }; +/** + * Handle a toDevice event that couldn't be decrypted + * + * @private + * @param {module:models/event.MatrixEvent} event undecryptable event + */ +Crypto.prototype._onToDeviceBadEncrypted = async function(event) { + const content = event.getWireContent(); + const sender = event.getSender(); + const algorithm = content.algorithm; + const deviceKey = content.sender_key; + + if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { + return; + } + + // establish a new olm session with this device since we're failing to decrypt messages + // on a current session. + // Note that an undecryptable message from another device could easily be spoofed - + // is there anything we can do to mitigate this? + const device = this._deviceList.getDeviceByIdentityKey(sender, algorithm, deviceKey); + const devicesByUser = {}; + devicesByUser[sender] = [device]; + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, devicesByUser, true, + ); + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // it the session it has most recently received a message on). + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + sender, + device, + {type: "m.dummy"}, + ); + + await this._baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent, + }, + }); + + + // Most of the time this probably won't be necessary since we'll have queued up a key request when + // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending + // it. This won't always be the case though so we need to re-send any that have already been sent + // to avoid races. + const requestsToResend = + await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest( + sender, device.deviceId, + ); + for (const keyReq of requestsToResend) { + this.cancelRoomKeyRequest(keyReq.requestBody, true); + } +}; + /** * Handle a change in the membership state of a member of a room * @@ -1287,9 +1357,27 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); if (userId !== this._userId) { - // TODO: determine if we sent this device the keys already: in - // which case we can do so again. - logger.log("Ignoring room key request from other user for now"); + if (!this._roomEncryptors[roomId]) { + logger.debug(`room key request for unencrypted room ${roomId}`); + return; + } + const encryptor = this._roomEncryptors[roomId]; + const device = this._deviceList.getStoredDevice(userId, deviceId); + if (!device) { + logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); + return; + } + + try { + await encryptor.reshareKeyWithDevice( + body.sender_key, body.session_id, userId, device, + ); + } catch (e) { + logger.warn( + "Failed to re-share keys for session " + body.session_id + + " with device " + userId + ":" + device.deviceId, e, + ); + } return; } diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 49ec4c20e..75b8abb95 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -116,14 +116,17 @@ module.exports.encryptMessageForDevice = async function( * @param {module:base-apis~MatrixBaseApis} baseApis * * @param {object} devicesByUser - * map from userid to list of devices + * map from userid to list of devices to ensure sessions for + * + * @param {bolean} force If true, establish a new session even if one already exists. + * Optional. * * @return {module:client.Promise} resolves once the sessions are complete, to * an Object mapping from userId to deviceId to * {@link module:crypto~OlmSessionResult} */ module.exports.ensureOlmSessionsForDevices = async function( - olmDevice, baseApis, devicesByUser, + olmDevice, baseApis, devicesByUser, force, ) { const devicesWithoutSession = [ // [userId, deviceId], ... @@ -141,7 +144,7 @@ module.exports.ensureOlmSessionsForDevices = async function( const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); const sessionId = await olmDevice.getSessionIdForDevice(key); - if (sessionId === null) { + if (sessionId === null || force) { devicesWithoutSession.push([userId, deviceId]); } result[userId][deviceId] = { @@ -177,7 +180,7 @@ module.exports.ensureOlmSessionsForDevices = async function( for (let j = 0; j < devices.length; j++) { const deviceInfo = devices[j]; const deviceId = deviceInfo.deviceId; - if (result[userId][deviceId].sessionId) { + if (result[userId][deviceId].sessionId && !force) { // we already have a result for this device continue; } diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 96bbec68e..09841d2b7 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -206,6 +206,42 @@ export class Backend { return promiseifyTxn(txn).then(() => result); } + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + let stateIndex = 0; + const results = []; + + function onsuccess(ev) { + const cursor = ev.target.result; + if (cursor) { + const keyReq = cursor.value; + if (keyReq.recipients.includes({userId, deviceId})) { + results.push(keyReq); + } + cursor.continue(); + } else { + // try the next state in the list + stateIndex++; + if (stateIndex >= wantedStates.length) { + // no matches + return; + } + + const wantedState = wantedStates[stateIndex]; + const cursorReq = ev.target.source.openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + } + } + + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const store = txn.objectStore("outgoingRoomKeyRequests"); + + const wantedState = wantedStates[stateIndex]; + const cursorReq = store.index("state").openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + + return promiseifyTxn(txn).then(() => results); + } + /** * Look for an existing room key request by id and state, and update it if * found diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index c9210da23..468d90a51 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -207,6 +207,24 @@ export default class IndexedDBCryptoStore { }); } + /** + * Look for room key requests by target device and state + * + * @param {string} userId Target user ID + * @param {string} deviceId Target device ID + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to a list of all the + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + */ + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + return this._connect().then((backend) => { + return backend.getOutgoingRoomKeyRequestsByTarget( + userId, deviceId, wantedStates, + ); + }); + } + /** * Look for an existing room key request by id and state, and update it if * found diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 4c2baf9e1..81bee95ba 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -145,6 +145,19 @@ export default class MemoryCryptoStore { return Promise.resolve(null); } + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + const results = []; + + for (const req of this._outgoingRoomKeyRequests) { + for (const state of wantedStates) { + if (req.state === state && req.recipients.includes({userId, deviceId})) { + results.push(req); + } + } + } + return Promise.resolve(results); + } + /** * Look for an existing room key request by id and state, and update it if * found From c9a79bf32e86dae7fb060851b76276c9468d3e49 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Nov 2018 16:46:03 -0700 Subject: [PATCH 32/54] Prevent messages from being sent if other messages have failed to send Fixes https://github.com/vector-im/riot-web/issues/5408 --- src/client.js | 7 +++++++ src/models/room.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/client.js b/src/client.js index d8f0d67ca..64bbf5082 100644 --- a/src/client.js +++ b/src/client.js @@ -1186,6 +1186,13 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, room.addPendingEvent(localEvent, txnId); } + // addPendingEvent can change the state to NOT_SENT if it believes + // that there's other events that have failed. We won't bother to + // try sending the event if the state has changed as such. + if (localEvent.status === EventStatus.NOT_SENT) { + return Promise.reject(new Error("Event blocked by other events not yet sent")); + } + return _sendEvent(this, room, localEvent, callback); }; diff --git a/src/models/room.js b/src/models/room.js index 4b685c878..6f05b48aa 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -999,6 +999,10 @@ Room.prototype.addPendingEvent = function(event, txnId) { this._txnToEvent[txnId] = event; if (this._opts.pendingEventOrdering == "detached") { + if (this._pendingEventList.some(e => e.status === EventStatus.NOT_SENT)) { + console.warn("Setting new event's status as " + EventStatus.NOT_SENT + " due to other similar messages"); + event.status = EventStatus.NOT_SENT; + } this._pendingEventList.push(event); } else { for (let i = 0; i < this._timelineSets.length; i++) { From 16db970558499df946568413921485d06d2d9966 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Nov 2018 16:51:21 -0700 Subject: [PATCH 33/54] Appease the linter --- src/models/room.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 6f05b48aa..c58b4a632 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -999,8 +999,8 @@ Room.prototype.addPendingEvent = function(event, txnId) { this._txnToEvent[txnId] = event; if (this._opts.pendingEventOrdering == "detached") { - if (this._pendingEventList.some(e => e.status === EventStatus.NOT_SENT)) { - console.warn("Setting new event's status as " + EventStatus.NOT_SENT + " due to other similar messages"); + if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { + console.warn("Setting event as NOT_SENT due to messages in the same state"); event.status = EventStatus.NOT_SENT; } this._pendingEventList.push(event); From fed67192bc802d885b3a5e520a28f354417d1e36 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Nov 2018 17:17:23 -0700 Subject: [PATCH 34/54] Fix test: Actually resend the event after unknown devices are found This test didn't actually test that resending would work, despite its name. --- spec/integ/megolm-integ.spec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index 9a37e0b38..b302856cc 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -817,8 +817,14 @@ describe("megolm", function() { }; }); + // Grab the event that we'll need to resend + const room = aliceTestClient.client.getRoom(ROOM_ID); + const pendingEvents = room.getPendingEvents(); + expect(pendingEvents.length).toEqual(1); + const unsentEvent = pendingEvents[0]; + return Promise.all([ - aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'), + aliceTestClient.client.resendEvent(unsentEvent, room), // the crypto stuff can take a while, so give the requests a whole second. aliceTestClient.httpBackend.flushAllExpected({ From 231fde219c3bc96427a7b4b4e343dc2a89892191 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Nov 2018 18:10:11 +0000 Subject: [PATCH 35/54] Store last received message ts on olm session --- src/crypto/OlmDevice.js | 75 +++++++++++-------- .../store/indexeddb-crypto-store-backend.js | 19 ++++- src/crypto/store/indexeddb-crypto-store.js | 16 ++-- src/crypto/store/localStorage-crypto-store.js | 20 ++++- src/crypto/store/memory-crypto-store.js | 4 +- 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index f9c907c78..1b317e7ab 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -295,8 +295,8 @@ OlmDevice.prototype._storeAccount = function(txn, account) { */ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { this._cryptoStore.getEndToEndSession( - deviceKey, sessionId, txn, (pickledSession) => { - this._unpickleSession(pickledSession, func); + deviceKey, sessionId, txn, (sessionInfo) => { + this._unpickleSession(sessionInfo, func); }, ); }; @@ -306,15 +306,17 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { * function with it. The session object is destroyed once the function * returns. * - * @param {string} pickledSession + * @param {object} sessionInfo * @param {function} func * @private */ -OlmDevice.prototype._unpickleSession = function(pickledSession, func) { +OlmDevice.prototype._unpickleSession = function(sessionInfo, func) { const session = new global.Olm.Session(); try { - session.unpickle(this._pickleKey, pickledSession); - func(session); + session.unpickle(this._pickleKey, sessionInfo.session); + const unpickledSessInfo = Object.assign({}, sessionInfo, {session}); + + func(unpickledSessInfo); } finally { session.free(); } @@ -324,14 +326,17 @@ OlmDevice.prototype._unpickleSession = function(pickledSession, func) { * store our OlmSession in the session store * * @param {string} deviceKey - * @param {OlmSession} session + * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} * @param {*} txn Opaque transaction object from cryptoStore.doTxn() * @private */ -OlmDevice.prototype._saveSession = function(deviceKey, session, txn) { - const pickledSession = session.pickle(this._pickleKey); +OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) { + const sessionId = sessionInfo.session.session_id(); + const pickledSessionInfo = Object.assign(sessionInfo, { + session: sessionInfo.session.pickle(this._pickleKey), + }); this._cryptoStore.storeEndToEndSession( - deviceKey, session.session_id(), pickledSession, txn, + deviceKey, sessionId, pickledSessionInfo, txn, ); }; @@ -461,13 +466,15 @@ OlmDevice.prototype.createOutboundSession = async function( session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); this._storeAccount(txn, account); - // Pretend we've received a message at this point, otherwise - // if we try to send a message to the device, it won't use - // this session (storing the creation time separately would - // make the pickle longer and would not be useful otherwise). - session.set_last_received_message_ts(Date.now()); - - this._saveSession(theirIdentityKey, session, txn); + const sessionInfo = { + session, + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session (storing the creation time separately would + // make the pickle longer and would not be useful otherwise). + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirIdentityKey, sessionInfo, txn); } finally { session.free(); } @@ -515,10 +522,14 @@ OlmDevice.prototype.createInboundSession = async function( this._storeAccount(txn, account); const payloadString = session.decrypt(messageType, ciphertext); - // this counts as an received message - session.set_last_received_message_ts(Date.now()); - this._saveSession(theirDeviceIdentityKey, session, txn); + const sessionInfo = { + session, + // this counts as an received message: set last received message time + // to now + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); result = { payload: payloadString, @@ -602,10 +613,10 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => { const sessionIds = Object.keys(sessions).sort(); for (const sessionId of sessionIds) { - this._unpickleSession(sessions[sessionId], (session) => { + this._unpickleSession(sessions[sessionId], (sessInfo) => { info.push({ - lastReceivedMessageTs: session.last_received_message_ts(), - hasReceivedMessage: session.has_received_message(), + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + hasReceivedMessage: sessInfo.session.has_received_message(), sessionId: sessionId, }); }); @@ -636,9 +647,9 @@ OlmDevice.prototype.encryptMessage = async function( await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - res = session.encrypt(payloadString); - this._saveSession(theirDeviceIdentityKey, session, txn); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + res = sessionInfo.session.encrypt(payloadString); + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, ); @@ -663,10 +674,10 @@ OlmDevice.prototype.decryptMessage = async function( await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - payloadString = session.decrypt(messageType, ciphertext); - session.set_last_received_message_ts(Date.now()); - this._saveSession(theirDeviceIdentityKey, session, txn); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + payloadString = sessionInfo.session.decrypt(messageType, ciphertext); + sessionInfo.lastReceivedMessageTs = Date.now(); + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, ); @@ -696,8 +707,8 @@ OlmDevice.prototype.matchesSession = async function( await this._cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - matches = session.matches_inbound(ciphertext); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + matches = sessionInfo.session.matches_inbound(ciphertext); }); }, ); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 09841d2b7..e3fb16d19 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -350,7 +350,10 @@ export class Backend { getReq.onsuccess = function() { const cursor = getReq.result; if (cursor) { - results[cursor.value.sessionId] = cursor.value.session; + results[cursor.value.sessionId] = { + session: cursor.value.session, + lastReceivedMessagets: cursor.value.lastReceivedMessageTs, + }; cursor.continue(); } else { try { @@ -368,7 +371,10 @@ export class Backend { getReq.onsuccess = function() { try { if (getReq.result) { - func(getReq.result.session); + func({ + session: getReq.result.session, + lastReceivedMessagets: getReq.result.lastReceivedMessageTs, + }); } else { func(null); } @@ -378,9 +384,14 @@ export class Backend { }; } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const objectStore = txn.objectStore("sessions"); - objectStore.put({deviceKey, sessionId, session}); + objectStore.put({ + deviceKey, + sessionId, + session: sessionInfo.session, + lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs, + }); } // Inbound group sessions diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 468d90a51..3a7d6d1d6 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -302,7 +302,10 @@ export default class IndexedDBCryptoStore { * @param {string} sessionId The ID of the session to retrieve * @param {*} txn An active transaction. See doTxn(). * @param {function(object)} func Called with A map from sessionId - * to Base64 end-to-end session. + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. */ getEndToEndSession(deviceKey, sessionId, txn, func) { this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func); @@ -314,7 +317,10 @@ export default class IndexedDBCryptoStore { * @param {string} deviceKey The public key of the other device. * @param {*} txn An active transaction. See doTxn(). * @param {function(object)} func Called with A map from sessionId - * to Base64 end-to-end session. + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. */ getEndToEndSessions(deviceKey, txn, func) { this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func); @@ -324,12 +330,12 @@ export default class IndexedDBCryptoStore { * Store a session between the logged-in user and another device * @param {string} deviceKey The public key of the other device. * @param {string} sessionId The ID for this end-to-end session. - * @param {string} session Base64 encoded end-to-end session. + * @param {string} sessionInfo Session information object * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { this._backendPromise.value().storeEndToEndSession( - deviceKey, sessionId, session, txn, + deviceKey, sessionId, sessionInfo, txn, ); } diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index ed0b7ede4..a47861b53 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -67,7 +67,21 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { } _getEndToEndSessions(deviceKey, txn, func) { - return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const fixedSessions = {}; + + // fix up any old sessions to be objects rather than just the base64 pickle + for (const [sid, val] of Object.entries(sessions || {})) { + if (typeof val === 'string') { + fixedSessions[sid] = { + session: val, + }; + } else { + fixedSessions[sid] = val; + } + } + + return fixedSessions; } getEndToEndSession(deviceKey, sessionId, txn, func) { @@ -79,9 +93,9 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(this._getEndToEndSessions(deviceKey) || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const sessions = this._getEndToEndSessions(deviceKey) || {}; - sessions[sessionId] = session; + sessions[sessionId] = sessionInfo; setJsonItem( this.store, keyEndToEndSessions(deviceKey), sessions, ); diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 81bee95ba..56b541391 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -247,13 +247,13 @@ export default class MemoryCryptoStore { func(this._sessions[deviceKey] || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { let deviceSessions = this._sessions[deviceKey]; if (deviceSessions === undefined) { deviceSessions = {}; this._sessions[deviceKey] = deviceSessions; } - deviceSessions[sessionId] = session; + deviceSessions[sessionId] = sessionInfo; } // Inbound Group Sessions From fcadf6ec4a052ba9f1a05fd26f23cadedb056b0c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Nov 2018 18:10:11 +0000 Subject: [PATCH 36/54] Store last received message ts on olm session --- src/crypto/OlmDevice.js | 69 ++++++++++++------- .../store/indexeddb-crypto-store-backend.js | 19 +++-- src/crypto/store/indexeddb-crypto-store.js | 16 +++-- src/crypto/store/localStorage-crypto-store.js | 20 +++++- src/crypto/store/memory-crypto-store.js | 4 +- 5 files changed, 88 insertions(+), 40 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 46f879ec2..2c195fef3 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -295,8 +295,8 @@ OlmDevice.prototype._storeAccount = function(txn, account) { */ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { this._cryptoStore.getEndToEndSession( - deviceKey, sessionId, txn, (pickledSession) => { - this._unpickleSession(pickledSession, func); + deviceKey, sessionId, txn, (sessionInfo) => { + this._unpickleSession(sessionInfo, func); }, ); }; @@ -306,15 +306,17 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { * function with it. The session object is destroyed once the function * returns. * - * @param {string} pickledSession + * @param {object} sessionInfo * @param {function} func * @private */ -OlmDevice.prototype._unpickleSession = function(pickledSession, func) { +OlmDevice.prototype._unpickleSession = function(sessionInfo, func) { const session = new global.Olm.Session(); try { - session.unpickle(this._pickleKey, pickledSession); - func(session); + session.unpickle(this._pickleKey, sessionInfo.session); + const unpickledSessInfo = Object.assign({}, sessionInfo, {session}); + + func(unpickledSessInfo); } finally { session.free(); } @@ -324,14 +326,17 @@ OlmDevice.prototype._unpickleSession = function(pickledSession, func) { * store our OlmSession in the session store * * @param {string} deviceKey - * @param {OlmSession} session + * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} * @param {*} txn Opaque transaction object from cryptoStore.doTxn() * @private */ -OlmDevice.prototype._saveSession = function(deviceKey, session, txn) { - const pickledSession = session.pickle(this._pickleKey); +OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) { + const sessionId = sessionInfo.session.session_id(); + const pickledSessionInfo = Object.assign(sessionInfo, { + session: sessionInfo.session.pickle(this._pickleKey), + }); this._cryptoStore.storeEndToEndSession( - deviceKey, session.session_id(), pickledSession, txn, + deviceKey, sessionId, pickledSessionInfo, txn, ); }; @@ -461,7 +466,15 @@ OlmDevice.prototype.createOutboundSession = async function( session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); this._storeAccount(txn, account); - this._saveSession(theirIdentityKey, session, txn); + const sessionInfo = { + session, + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session (storing the creation time separately would + // make the pickle longer and would not be useful otherwise). + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirIdentityKey, sessionInfo, txn); } finally { session.free(); } @@ -509,10 +522,14 @@ OlmDevice.prototype.createInboundSession = async function( this._storeAccount(txn, account); const payloadString = session.decrypt(messageType, ciphertext); - // this counts as an received message - session.set_last_received_message_ts(Date.now()); - this._saveSession(theirDeviceIdentityKey, session, txn); + const sessionInfo = { + session, + // this counts as an received message: set last received message time + // to now + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); result = { payload: payloadString, @@ -596,10 +613,10 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => { const sessionIds = Object.keys(sessions).sort(); for (const sessionId of sessionIds) { - this._unpickleSession(sessions[sessionId], (session) => { + this._unpickleSession(sessions[sessionId], (sessInfo) => { info.push({ - lastReceivedMessageTs: session.last_received_message_ts(), - hasReceivedMessage: session.has_received_message(), + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + hasReceivedMessage: sessInfo.session.has_received_message(), sessionId: sessionId, }); }); @@ -630,9 +647,9 @@ OlmDevice.prototype.encryptMessage = async function( await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - res = session.encrypt(payloadString); - this._saveSession(theirDeviceIdentityKey, session, txn); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + res = sessionInfo.session.encrypt(payloadString); + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, ); @@ -657,10 +674,10 @@ OlmDevice.prototype.decryptMessage = async function( await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - payloadString = session.decrypt(messageType, ciphertext); - session.set_last_received_message_ts(Date.now()); - this._saveSession(theirDeviceIdentityKey, session, txn); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + payloadString = sessionInfo.session.decrypt(messageType, ciphertext); + sessionInfo.lastReceivedMessageTs = Date.now(); + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, ); @@ -690,8 +707,8 @@ OlmDevice.prototype.matchesSession = async function( await this._cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - matches = session.matches_inbound(ciphertext); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + matches = sessionInfo.session.matches_inbound(ciphertext); }); }, ); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 96bbec68e..ba15e6673 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -314,7 +314,10 @@ export class Backend { getReq.onsuccess = function() { const cursor = getReq.result; if (cursor) { - results[cursor.value.sessionId] = cursor.value.session; + results[cursor.value.sessionId] = { + session: cursor.value.session, + lastReceivedMessagets: cursor.value.lastReceivedMessageTs, + }; cursor.continue(); } else { try { @@ -332,7 +335,10 @@ export class Backend { getReq.onsuccess = function() { try { if (getReq.result) { - func(getReq.result.session); + func({ + session: getReq.result.session, + lastReceivedMessagets: getReq.result.lastReceivedMessageTs, + }); } else { func(null); } @@ -342,9 +348,14 @@ export class Backend { }; } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const objectStore = txn.objectStore("sessions"); - objectStore.put({deviceKey, sessionId, session}); + objectStore.put({ + deviceKey, + sessionId, + session: sessionInfo.session, + lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs, + }); } // Inbound group sessions diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index c9210da23..a4d7b48c0 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -284,7 +284,10 @@ export default class IndexedDBCryptoStore { * @param {string} sessionId The ID of the session to retrieve * @param {*} txn An active transaction. See doTxn(). * @param {function(object)} func Called with A map from sessionId - * to Base64 end-to-end session. + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. */ getEndToEndSession(deviceKey, sessionId, txn, func) { this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func); @@ -296,7 +299,10 @@ export default class IndexedDBCryptoStore { * @param {string} deviceKey The public key of the other device. * @param {*} txn An active transaction. See doTxn(). * @param {function(object)} func Called with A map from sessionId - * to Base64 end-to-end session. + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. */ getEndToEndSessions(deviceKey, txn, func) { this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func); @@ -306,12 +312,12 @@ export default class IndexedDBCryptoStore { * Store a session between the logged-in user and another device * @param {string} deviceKey The public key of the other device. * @param {string} sessionId The ID for this end-to-end session. - * @param {string} session Base64 encoded end-to-end session. + * @param {string} sessionInfo Session information object * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { this._backendPromise.value().storeEndToEndSession( - deviceKey, sessionId, session, txn, + deviceKey, sessionId, sessionInfo, txn, ); } diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index ed0b7ede4..a47861b53 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -67,7 +67,21 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { } _getEndToEndSessions(deviceKey, txn, func) { - return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const fixedSessions = {}; + + // fix up any old sessions to be objects rather than just the base64 pickle + for (const [sid, val] of Object.entries(sessions || {})) { + if (typeof val === 'string') { + fixedSessions[sid] = { + session: val, + }; + } else { + fixedSessions[sid] = val; + } + } + + return fixedSessions; } getEndToEndSession(deviceKey, sessionId, txn, func) { @@ -79,9 +93,9 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(this._getEndToEndSessions(deviceKey) || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const sessions = this._getEndToEndSessions(deviceKey) || {}; - sessions[sessionId] = session; + sessions[sessionId] = sessionInfo; setJsonItem( this.store, keyEndToEndSessions(deviceKey), sessions, ); diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 4c2baf9e1..8c8543c18 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -234,13 +234,13 @@ export default class MemoryCryptoStore { func(this._sessions[deviceKey] || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { let deviceSessions = this._sessions[deviceKey]; if (deviceSessions === undefined) { deviceSessions = {}; this._sessions[deviceKey] = deviceSessions; } - deviceSessions[sessionId] = session; + deviceSessions[sessionId] = sessionInfo; } // Inbound Group Sessions From e17a39d446e0acd389e49110d94044ced45da88d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Nov 2018 12:10:26 +0000 Subject: [PATCH 37/54] PR feedback --- src/crypto/OlmDevice.js | 24 ++++++++++--------- .../store/indexeddb-crypto-store-backend.js | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 2c195fef3..c87ef9525 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -470,8 +470,7 @@ OlmDevice.prototype.createOutboundSession = async function( session, // Pretend we've received a message at this point, otherwise // if we try to send a message to the device, it won't use - // this session (storing the creation time separately would - // make the pickle longer and would not be useful otherwise). + // this session lastReceivedMessageTs: Date.now(), }; this._saveSession(theirIdentityKey, sessionInfo, txn); @@ -525,7 +524,7 @@ OlmDevice.prototype.createInboundSession = async function( const sessionInfo = { session, - // this counts as an received message: set last received message time + // this counts as a received message: set last received message time // to now lastReceivedMessageTs: Date.now(), }; @@ -582,15 +581,18 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe return null; } // Use the session that has most recently received a message - sessionInfos.sort((a, b) => { - if (a.lastReceivedMessageTs !== b.lastReceivedMessageTs) { - return a.lastReceivedMessageTs - b.lastReceivedMessageTs; - } else { - if (a.sessionId === b.sessionId) return 0; - return a.sessionId < b.sessionId ? -1 : 1; + let idxOfMin = 0; + for (let i = 1; i < sessionInfos.length; i++) { + if ( + sessionInfos[i].lastReceivedMessageTs < sessionInfos[idxOfMin].lastReceiveMessageTs || ( + sessionInfos[i].lastReceivedMessageTs === sessionInfos[idxOfMin].lastReceiveMessageTs && + sessionInfos[i].sessionId < sessinInfos[idxOfMin].sessionId + ) + ) { + idxOfMin = i; } - }); - return sessionInfos[sessionInfos.length - 1].sessionId; + } + return sessionInfos[idxOfMin].sessionId; }; /** diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index ba15e6673..0f31584cd 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -316,7 +316,7 @@ export class Backend { if (cursor) { results[cursor.value.sessionId] = { session: cursor.value.session, - lastReceivedMessagets: cursor.value.lastReceivedMessageTs, + lastReceivedMessageTs: cursor.value.lastReceivedMessageTs, }; cursor.continue(); } else { @@ -337,7 +337,7 @@ export class Backend { if (getReq.result) { func({ session: getReq.result.session, - lastReceivedMessagets: getReq.result.lastReceivedMessageTs, + lastReceivedMessageTs: getReq.result.lastReceivedMessageTs, }); } else { func(null); From 6c413bba4854986a8bbec98ff74918f93dbfdf00 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Nov 2018 12:15:33 +0000 Subject: [PATCH 38/54] Typo + exit if session was never shared --- src/crypto/algorithms/megolm.js | 8 ++++++++ src/crypto/index.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index c9792b38e..b4a78334a 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -433,7 +433,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( } // The chain index of the key we previously sent this device + if (obSessionInfo.sharedWithDevices[userId] === undefined) { + logger.debug("Session ID " + sessionId + " never shared with user " + userId); + return; + } const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; + if (sentChainIndex === undefined) { + logger.debug("Session ID " + sessionId + " never shared with device " + userId + ":" + device.deviceId); + return; + } // get the key from the inbound session: the outbound one will already // have been ratcheted to the next chain index. diff --git a/src/crypto/index.js b/src/crypto/index.js index 54c6bb16a..7c0ac003a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1196,7 +1196,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { // We send this first such that, as long as the toDevice messages arrive in the // same order we sent them, the other end will get this first, set up the new session, // then get the keyshare request and send the key over this new session (because it - // it the session it has most recently received a message on). + // is the session it has most recently received a message on). const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this._olmDevice.deviceCurve25519Key, From a30845f9ce55c4fac4fb46a6f6c4d2979d0dd824 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 08:03:23 +0000 Subject: [PATCH 39/54] lint --- src/crypto/OlmDevice.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index c87ef9525..0ac53b3ed 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -583,10 +583,12 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe // Use the session that has most recently received a message let idxOfMin = 0; for (let i = 1; i < sessionInfos.length; i++) { + const thisSessInfo = sessionInfos[i]; + const minSessInfo = sessionInfos[idxOfMin]; if ( - sessionInfos[i].lastReceivedMessageTs < sessionInfos[idxOfMin].lastReceiveMessageTs || ( - sessionInfos[i].lastReceivedMessageTs === sessionInfos[idxOfMin].lastReceiveMessageTs && - sessionInfos[i].sessionId < sessinInfos[idxOfMin].sessionId + thisSessInfo.lastReceivedMessageTs < minSessInfo.lastReceiveMessageTs || ( + thisSessInfo.lastReceivedMessageTs === minSessInfo.lastReceiveMessageTs && + thisSessInfo.sessionId < minSessInfo.sessionId ) ) { idxOfMin = i; From dbb6d8ac71524303a9a6f09bb24bbca0323b523b Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 19:43:18 +0000 Subject: [PATCH 40/54] Set access_token and user_id after login in with username and password. --- src/base-apis.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index 6dc9169dd..d00ab64dc 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -277,7 +277,21 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, callback); + }, (error, response) => { + if (response && response.access_token) { + this._http.opts.accessToken = response.access_token; + } + + if (response && response.user_id) { + this.credentials = { + userId: response.user_id, + }; + } + + if(callback) { + callback(error, response); + } + }); }; /** From b0d0782a729357aad63079a044c8c1ea48fa82fc Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:02:30 +0000 Subject: [PATCH 41/54] Linting. --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index d00ab64dc..8836c59d8 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -288,7 +288,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) }; } - if(callback) { + if (callback) { callback(error, response); } }); From 11be68ad4919d416891236308c8d8c072effc35c Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:40:11 +0000 Subject: [PATCH 42/54] Refactor code to base 'login' method. --- src/base-apis.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 8836c59d8..ad0f84b45 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -262,7 +262,19 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) { utils.extend(login_data, data); return this._http.authedRequest( - callback, "POST", "/login", undefined, login_data, + (error, response) => { + if (loginType === "m.login.password" && response && + response.access_token && response.user_id) { + this._http.opts.accessToken = response.access_token; + this.credentials = { + userId: response.user_id, + }; + } + + if (callback) { + callback(error, response); + } + }, "POST", "/login", undefined, login_data, ); }; @@ -277,21 +289,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, (error, response) => { - if (response && response.access_token) { - this._http.opts.accessToken = response.access_token; - } - - if (response && response.user_id) { - this.credentials = { - userId: response.user_id, - }; - } - - if (callback) { - callback(error, response); - } - }); + }, callback); }; /** From d40d7e18f5b4f1d54f67e8a9b0721420037a8db5 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:06:39 +0000 Subject: [PATCH 43/54] Update CHANGELOG. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4295d74fe..8c5b7ec7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0) + +BREAKING CHANGE +---------------- + * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login. + Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1) From 977d5331c0ca8779692fd14a1ceaef9cbe463370 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:17:22 +0000 Subject: [PATCH 44/54] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5b7ec7d..666558997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0. BREAKING CHANGE ---------------- - * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login. + * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login with username and password. Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29) ================================================================================================== From fbe174fb64e2c04b9dbd6654f9f95258b454db29 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 14 Nov 2018 10:53:42 +0100 Subject: [PATCH 45/54] v0.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c9c9544f..0c872e9fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.1", + "version": "0.13.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From ebf20d5b2cb03e23d13bfc8243cb32a287270372 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:20:55 +0000 Subject: [PATCH 46/54] Don't force more than one new session per device per hour --- src/crypto/algorithms/megolm.js | 2 +- src/crypto/index.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index b4a78334a..ae018a42c 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -499,7 +499,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( }, }); logger.debug( - `Re-shared key for session ${sessionId} with {userId}:{device.deviceId}`, + `Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`, ); }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 7c0ac003a..26292f3bb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -41,6 +41,8 @@ export function isCryptoAvailable() { return Boolean(global.Olm); } +const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; + /** * Cryptography bits * @@ -120,6 +122,15 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, // has happened for a given room. This is delayed // to avoid loading room members as long as possible. this._roomDeviceTrackingState = {}; + + // The timestamp of the last time we forced establishment + // of a new session for each device, in milliseconds. + // { + // userId: { + // deviceId: 1234567890000, + // }, + // } + this._lastNewSessionForced = {}; } utils.inherits(Crypto, EventEmitter); @@ -1180,6 +1191,19 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { return; } + // check when we last forced a new session with this device: if we've already done so + // recently, don't do it again. + this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {}; + const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0; + if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { + logger.debug( + "New session already forced with device " + sender + ":" + deviceKey + + " at " + lastNewSessionForced + ": not forcing another", + ); + return; + } + this._lastNewSessionForced[sender][deviceKey] = Date.now(); + // establish a new olm session with this device since we're failing to decrypt messages // on a current session. // Note that an undecryptable message from another device could easily be spoofed - From 5bc68c0c6dad66d868b33f78643189f166d85db3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:29:03 +0000 Subject: [PATCH 47/54] Handle last received message ts being undefined --- src/crypto/OlmDevice.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 0ac53b3ed..c63f0e3a5 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -584,10 +584,15 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe let idxOfMin = 0; for (let i = 1; i < sessionInfos.length; i++) { const thisSessInfo = sessionInfos[i]; + const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? + 0 : thisSessInfo.lastReceivedMessageTs; + const minSessInfo = sessionInfos[idxOfMin]; + const minLastReceived = minSessInfo.lastReceivedMessageTs === undefined ? + 0 : thisSessInfo.lastReceivedMessageTs; if ( - thisSessInfo.lastReceivedMessageTs < minSessInfo.lastReceiveMessageTs || ( - thisSessInfo.lastReceivedMessageTs === minSessInfo.lastReceiveMessageTs && + thisLastReceived < minLastReceived || ( + thisLastReceived === minLastReceived && thisSessInfo.sessionId < minSessInfo.sessionId ) ) { From 408407b33d082cb5c600d71b8e33e3e94e3d7260 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:34:36 +0000 Subject: [PATCH 48/54] Fix typo --- src/crypto/OlmDevice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index c63f0e3a5..61d61789d 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -589,7 +589,7 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe const minSessInfo = sessionInfos[idxOfMin]; const minLastReceived = minSessInfo.lastReceivedMessageTs === undefined ? - 0 : thisSessInfo.lastReceivedMessageTs; + 0 : minSessInfo.lastReceivedMessageTs; if ( thisLastReceived < minLastReceived || ( thisLastReceived === minLastReceived && From 3c85bd55d31384ce0173fe47f0feea32fe43e6f7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:57:48 +0000 Subject: [PATCH 49/54] Time goes forwards --- src/crypto/OlmDevice.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 61d61789d..e5b05fa4b 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -581,25 +581,25 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe return null; } // Use the session that has most recently received a message - let idxOfMin = 0; + let idxOfBest = 0; for (let i = 1; i < sessionInfos.length; i++) { const thisSessInfo = sessionInfos[i]; const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? 0 : thisSessInfo.lastReceivedMessageTs; - const minSessInfo = sessionInfos[idxOfMin]; - const minLastReceived = minSessInfo.lastReceivedMessageTs === undefined ? - 0 : minSessInfo.lastReceivedMessageTs; + const bestSessInfo = sessionInfos[idxOfBest]; + const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ? + 0 : bestSessInfo.lastReceivedMessageTs; if ( - thisLastReceived < minLastReceived || ( - thisLastReceived === minLastReceived && - thisSessInfo.sessionId < minSessInfo.sessionId + thisLastReceived > bestLastReceived || ( + thisLastReceived === bestLastReceived && + thisSessInfo.sessionId < bestSessInfo.sessionId ) ) { - idxOfMin = i; + idxOfBest = i; } } - return sessionInfos[idxOfMin].sessionId; + return sessionInfos[idxOfBest].sessionId; }; /** From 23efd0850d50a3108b2586a6b3ec937a17a1b7b8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 21:49:17 +0000 Subject: [PATCH 50/54] Add function to get currently joined rooms. --- src/base-apis.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index ad0f84b45..b0b132fed 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -939,6 +939,14 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = ); }; +/** + * @return {module:client.Promise} Resolves: A list of the user's current rooms + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedRooms = function() { + const path = utils.encodeUri("/joined_rooms"); + return this._http.authedRequest(undefined, "GET", path); +}; // Room Directory operations // ========================= From 988be628043f931b932ef25ca9f4fd38ee9a54d3 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 00:08:04 +0000 Subject: [PATCH 51/54] Add function to get currently joined room members. --- src/base-apis.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index b0b132fed..1515ca735 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -948,6 +948,20 @@ MatrixBaseApis.prototype.getJoinedRooms = function() { return this._http.authedRequest(undefined, "GET", path); }; +/** + * Retrieve membership info. for a room. + * @param {string} roomId ID of the room to get membership for + * @return {module:client.Promise} Resolves: A list of currently joined users + * and their profile data. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) { + const path = utils.encodeUri("/rooms/$roomId/joined_members", { + $roomId: roomId, + }); + return this._http.authedRequest(undefined, "GET", path); +}; + // Room Directory operations // ========================= From 980d55a2f3caf3e5906c76811459d0e8d3f524a3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 14 Nov 2018 18:18:11 +0100 Subject: [PATCH 52/54] Prepare changelog for v0.13.1 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666558997..46c90e029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.13.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.1) (2018-11-14) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.0...v0.13.1) + + * Add function to get currently joined rooms. + [\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779) + Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0) From 6ce7b30b72926aa4c165934282bf0e2cc1fd18c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 14 Nov 2018 18:18:12 +0100 Subject: [PATCH 53/54] v0.13.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c872e9fd..9aadecd3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.13.0", + "version": "0.13.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From c9917e4079c5f846b794465ba54254fee586b4a4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Nov 2018 10:03:16 +0000 Subject: [PATCH 54/54] lint --- src/crypto/algorithms/megolm.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index ae018a42c..0bbf1ca52 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -439,7 +439,10 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( } const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; if (sentChainIndex === undefined) { - logger.debug("Session ID " + sessionId + " never shared with device " + userId + ":" + device.deviceId); + logger.debug( + "Session ID " + sessionId + " never shared with device " + + userId + ":" + device.deviceId, + ); return; }