diff --git a/package.json b/package.json index d66bc6530bf..5e24e990e47 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "jest-localstorage-mock": "^2.4.3", "jsdoc": "^3.6.6", "matrix-mock-request": "^1.2.3", - "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", + "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "rimraf": "^3.0.2", "terser": "^4.8.0", "tsify": "^4.0.2", diff --git a/spec/unit/crypto/CrossSigningInfo.spec.js b/spec/unit/crypto/CrossSigningInfo.spec.js index 485a21ef047..541f6e47993 100644 --- a/spec/unit/crypto/CrossSigningInfo.spec.js +++ b/spec/unit/crypto/CrossSigningInfo.spec.js @@ -84,9 +84,16 @@ describe("CrossSigningInfo.getCrossSigningKey", function() { const info = new CrossSigningInfo(userId, { getCrossSigningKey: () => testKey, }); - const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub); + const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub); expect(pubKey).toEqual(masterKeyPub); - expect(ab).toEqual({a: 106712, b: 106712}); + // check that the pkSigning object corresponds to the pubKey + const signature = pkSigning.sign("message"); + const util = new global.Olm.Utility(); + try { + util.ed25519_verify(pubKey, "message", signature); + } finally { + util.free(); + } }); it.each(types)("should request a key from the cache callback (if set)" + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2f541ee9969..86e57e5cf03 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Olm from "olm"; +// this is needed to tell TS about global.Olm +import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars export {}; @@ -22,10 +23,6 @@ declare global { namespace NodeJS { interface Global { localStorage: Storage; - Olm: Olm; } } - interface Global { - Olm: Olm; - } } diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 128bfcad80b..462d4543293 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -477,6 +477,36 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) { ); }; +/** + * Generate a new fallback keys + * + * @return {Promise} Resolved once the account is saved back having generated the key + */ +OlmDevice.prototype.generateFallbackKey = async function() { + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._getAccount(txn, (account) => { + account.generate_fallback_key(); + this._storeAccount(txn, account); + }); + }, + ); +}; + +OlmDevice.prototype.getFallbackKey = async function() { + let result; + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._getAccount(txn, (account) => { + result = JSON.parse(account.fallback_key()); + }); + }, + ); + return result; +}; + /** * Generate a new outbound session * diff --git a/src/crypto/index.js b/src/crypto/index.js index 4847950a17e..4f5f0b559bf 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1859,6 +1859,14 @@ Crypto.prototype.updateOneTimeKeyCount = function(currentCount) { } }; +Crypto.prototype.setNeedsNewFallback = function(needsNewFallback) { + this._needsNewFallback = !!needsNewFallback; +}; + +Crypto.prototype.getNeedsNewFallback = function() { + return this._needsNewFallback; +}; + // check if it's time to upload one-time keys, and do so if so. function _maybeUploadOneTimeKeys(crypto) { // frequency with which to check & upload one-time keys @@ -1906,27 +1914,31 @@ function _maybeUploadOneTimeKeys(crypto) { // out stale private keys that won't receive a message. const keyLimit = Math.floor(maxOneTimeKeys / 2); - function uploadLoop(keyCount) { - if (keyLimit <= keyCount) { - // If we don't need to generate any more keys then we are done. - return Promise.resolve(); - } + async function uploadLoop(keyCount) { + while (keyLimit > keyCount || crypto.getNeedsNewFallback()) { + // Ask olm to generate new one time keys, then upload them to synapse. + if (keyLimit > keyCount) { + logger.info("generating oneTimeKeys"); + const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); + await crypto._olmDevice.generateOneTimeKeys(keysThisLoop); + } - const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); + if (crypto.getNeedsNewFallback()) { + logger.info("generating fallback key"); + await crypto._olmDevice.generateFallbackKey(); + } - // Ask olm to generate new one time keys, then upload them to synapse. - return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(() => { - return _uploadOneTimeKeys(crypto); - }).then((res) => { + logger.info("calling _uploadOneTimeKeys"); + const res = await _uploadOneTimeKeys(crypto); if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) { // if the response contains a more up to date value use this // for the next loop - return uploadLoop(res.one_time_key_counts.signed_curve25519); + keyCount = res.one_time_key_counts.signed_curve25519; } else { - throw new Error("response for uploading keys does not contain " - + "one_time_key_counts.signed_curve25519"); + throw new Error("response for uploading keys does not contain " + + "one_time_key_counts.signed_curve25519"); } - }); + } } crypto._oneTimeKeyCheckInProgress = true; @@ -1958,11 +1970,22 @@ function _maybeUploadOneTimeKeys(crypto) { // returns a promise which resolves to the response async function _uploadOneTimeKeys(crypto) { + const promises = []; + + const fallbackJson = {}; + if (crypto.getNeedsNewFallback()) { + const fallbackKeys = await crypto._olmDevice.getFallbackKey(); + for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) { + const k = { key, fallback: true }; + fallbackJson["signed_curve25519:" + keyId] = k; + promises.push(crypto._signObject(k)); + } + crypto.setNeedsNewFallback(false); + } + const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys(); const oneTimeJson = {}; - const promises = []; - for (const keyId in oneTimeKeys.curve25519) { if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { const k = { @@ -1976,7 +1999,8 @@ async function _uploadOneTimeKeys(crypto) { await Promise.all(promises); const res = await crypto._baseApis.uploadKeysRequest({ - one_time_keys: oneTimeJson, + "one_time_keys": oneTimeJson, + "org.matrix.msc2732.fallback_keys": fallbackJson, }); await crypto._olmDevice.markKeysAsPublished(); diff --git a/src/sync.js b/src/sync.js index a0663979097..f1120001e3d 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1361,6 +1361,16 @@ SyncApi.prototype._processSyncResponse = async function( const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0; this.opts.crypto.updateOneTimeKeyCount(currentCount); } + if (this.opts.crypto && data["org.matrix.msc2732.device_unused_fallback_key_types"]) { + // The presence of device_unused_fallback_key_types indicates that the + // server supports fallback keys. If there's no unused + // signed_curve25519 fallback key we need a new one. + const unusedFallbackKeys = data["org.matrix.msc2732.device_unused_fallback_key_types"]; + this.opts.crypto.setNeedsNewFallback( + unusedFallbackKeys instanceof Array && + !unusedFallbackKeys.includes("signed_curve25519"), + ); + } }; /** diff --git a/yarn.lock b/yarn.lock index b72adcbf4e5..71718d35553 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5411,9 +5411,9 @@ object.values@^1.1.1: function-bind "^1.1.1" has "^1.0.3" -"olm@https://packages.matrix.org/npm/olm/olm-3.1.4.tgz": - version "3.1.4" - resolved "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz#0f03128b7d3b2f614d2216409a1dfccca765fdb3" +"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz": + version "3.2.1" + resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0"