From 3f448df1d3b20a30dfe874c0894d4d213839be32 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Dec 2019 16:28:58 +0000 Subject: [PATCH 01/26] Create key backup with secret storage When secret storage is enable, create a random key for encrypting key backups and store it in SSSS. --- src/client.js | 34 ++++++++++++++++------------------ src/crypto/index.js | 10 ++++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/client.js b/src/client.js index 6f0b4b374..ed5987770 100644 --- a/src/client.js +++ b/src/client.js @@ -53,6 +53,7 @@ import { isCryptoAvailable } from './crypto'; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { randomString } from './randomstring'; +import { encodeBase64 } from '../lib/crypto/olmlib'; const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = isCryptoAvailable(); @@ -1437,7 +1438,7 @@ MatrixClient.prototype.disableKeyBackup = function() { * when restoring the backup as an alternative to entering the recovery key. * Optional. * @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure - * Secret Storage (MSC1946) to store the key encrypting key backups. + * Secret Storage to store the key encrypting key backups. * Optional, defaults to false. * * @returns {Promise} Object that can be passed to createKeyBackupVersion and @@ -1451,28 +1452,25 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function( throw new Error("End-to-end encryption disabled"); } - if (secureSecretStorage) { - logger.log("Preparing key backup version with Secure Secret Storage"); - - // Ensure Secure Secret Storage is ready for use - if (!this.hasSecretStorageKey()) { - throw new Error("Secure Secret Storage has no keys, needs bootstrapping"); - } - - throw new Error("Not yet implemented"); - } - - const [keyInfo, encodedPrivateKey] = + const [keyInfo, encodedPrivateKey, privateKey] = await this.createRecoveryKeyFromPassphrase(password); + if (secureSecretStorage) { + await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey)); + logger.info("Key backup private key stored in secret storage"); + } + // Reshape objects into form expected for key backup + const authData = { + public_key: keyInfo.pubkey, + }; + if (keyInfo.passphrase) { + authData.private_key_salt = keyInfo.passphrase.salt; + authData.private_key_iterations = keyInfo.passphrase.iterations; + } return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: keyInfo.pubkey, - private_key_salt: keyInfo.passphrase.salt, - private_key_iterations: keyInfo.passphrase.iterations, - }, + auth_data: authData, recovery_key: encodedPrivateKey, }; }; diff --git a/src/crypto/index.js b/src/crypto/index.js index f246299c7..33fda2895 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -296,8 +296,9 @@ Crypto.prototype.init = async function() { * @param {string} password Passphrase string that can be entered by the user * when restoring the backup as an alternative to entering the recovery key. * Optional. - * @returns {Promise} Array with public key metadata and encoded private - * recovery key which should be disposed of after displaying to the user. + * @returns {Promise} Array with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. */ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { const decryption = new global.Olm.PkDecryption(); @@ -314,8 +315,9 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { } else { keyInfo.pubkey = decryption.generate_key(); } - const encodedPrivateKey = encodeRecoveryKey(decryption.get_private_key()); - return [keyInfo, encodedPrivateKey]; + const privateKey = decryption.get_private_key(); + const encodedPrivateKey = encodeRecoveryKey(privateKey); + return [keyInfo, encodedPrivateKey, privateKey]; } finally { decryption.free(); } From 880438c5c121dfe9827a08018c899e6438e089ea Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 12:13:31 +0000 Subject: [PATCH 02/26] Remove unused user verification event This was added with cross-signing work, but nothing actually emits it. Let's remove it until we find a need. --- src/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.js b/src/client.js index 6f0b4b374..d9e5635e1 100644 --- a/src/client.js +++ b/src/client.js @@ -672,7 +672,6 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.warning", "crypto.devicesUpdated", "deviceVerificationChanged", - "userVerificationChanged", "crossSigning.keysChanged", ]); From 443e01d38c8ea547f6d9a319c92225dd87b899d5 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 12:39:06 +0000 Subject: [PATCH 03/26] Always check backup validity, even during enabling This ensure we run the full backup validity check even when enabling (rather than assuming we've signed things correctly) to ensure any problem are reported right away. --- src/client.js | 14 +++++++++----- src/crypto/index.js | 3 +-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client.js b/src/client.js index ed5987770..aebf18ec6 100644 --- a/src/client.js +++ b/src/client.js @@ -1506,11 +1506,15 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { undefined, "POST", "/room_keys/version", undefined, data, {prefix: httpApi.PREFIX_UNSTABLE}, ); - this.enableKeyBackup({ - algorithm: info.algorithm, - auth_data: info.auth_data, - version: res.version, - }); + + // We could assume everything's okay and enable directly, but this ensures + // we run the same signature verification that will be used for future + // sessions. + await this.checkKeyBackup(); + if (!this.getKeyBackupEnabled()) { + throw new Error("Key backup not usable even though we just created it"); + } + return res; }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 33fda2895..e38af407b 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -952,8 +952,7 @@ Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) { */ Crypto.prototype.checkKeyBackup = async function() { this._checkedForBackup = false; - const returnInfo = await this._checkAndStartKeyBackup(); - return returnInfo; + return this._checkAndStartKeyBackup(); }; /** From 7457da80e93263b056b6dacea45d87cf7e1d8c1b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 13:18:32 +0000 Subject: [PATCH 04/26] Clean up backup trust checks There were several inaccurate comments and redundant code paths around backup trust checks. --- src/client.js | 8 ++++---- src/crypto/index.js | 13 ++++++------- src/crypto/olmlib.js | 3 +-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/client.js b/src/client.js index aebf18ec6..4ba045118 100644 --- a/src/client.js +++ b/src/client.js @@ -1492,13 +1492,13 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { auth_data: info.auth_data, }; - // Now sign the backup auth data. Do it as this device first because crypto._signObject - // is dumb and bluntly replaces the whole signatures block... - // this can probably go away very soon in favour of just signing with the SSK. + // Sign the backup auth data with the device key for backwards compat with + // older devices with cross-signing. This can probably go away very soon in + // favour of just signing with the cross-singing master key. await this._crypto._signObject(data.auth_data); if (this._crypto._crossSigningInfo.getId()) { - // now also sign the auth data with the master key + // now also sign the auth data with the cross-signing master key await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); } diff --git a/src/crypto/index.js b/src/crypto/index.js index e38af407b..918f3cfb4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -999,12 +999,13 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { logger.log("Ignoring unknown signature type: " + keyIdParts[0]); continue; } - // Could be an SSK but just say this is the device ID for backwards compat - const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID? + // Could be a cross-signing master key, but just say this is the device + // ID for backwards compat + const sigInfo = { deviceId: keyIdParts[1] }; // first check to see if it's from our cross-signing key const crossSigningId = this._crossSigningInfo.getId(); - if (crossSigningId === keyId) { + if (crossSigningId === sigInfo.deviceId) { sigInfo.cross_signing_key = crossSigningId; try { await olmlib.verifySignature( @@ -1027,7 +1028,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { // Now look for a sig from a device // At some point this can probably go away and we'll just support - // it being signed by the SSK + // it being signed by the cross-signing master key const device = this._deviceList.getStoredDevice( this._userId, sigInfo.deviceId, ); @@ -1036,9 +1037,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { try { await olmlib.verifySignature( this._olmDevice, - // verifySignature modifies the object so we need to copy - // if we verify more than one sig - Object.assign({}, backupInfo.auth_data), + backupInfo.auth_data, this._userId, device.deviceId, device.getFingerprint(), diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index ab1b13e96..e00431d93 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -303,8 +303,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn * * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op * - * @param {Object} obj object to check signature on. Note that this will be - * stripped of its 'signatures' and 'unsigned' properties. + * @param {Object} obj object to check signature on. * * @param {string} signingUserId ID of the user whose signature should be checked * From dbab75eae7451b1cc661acf9eeda84b001a7ef10 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 13:25:03 +0000 Subject: [PATCH 05/26] Report cross-signing sig as JS style boolean --- src/crypto/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 918f3cfb4..a02d79995 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1006,7 +1006,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { // first check to see if it's from our cross-signing key const crossSigningId = this._crossSigningInfo.getId(); if (crossSigningId === sigInfo.deviceId) { - sigInfo.cross_signing_key = crossSigningId; + sigInfo.crossSigningId = true; try { await olmlib.verifySignature( this._olmDevice, @@ -1062,7 +1062,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { return ( s.valid && ( (s.device && s.device.isVerified()) || - (s.cross_signing_key) + (s.crossSigningId) ) ); }); From 72f856eca47923a859eaf21103442c9da547940f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 14:06:14 +0000 Subject: [PATCH 06/26] Add util to check whether backup key is stored --- src/client.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client.js b/src/client.js index 4ba045118..834cc9615 100644 --- a/src/client.js +++ b/src/client.js @@ -1475,6 +1475,13 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function( }; }; +/** + * Check whether the key backup private key is stored in secret storage. + */ +MatrixClient.prototype.isKeyBackupKeyStored = async function() { + return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */); +}; + /** * Create a new key backup version and enable it, using the information return * from prepareKeyBackupVersion. From 376e56d5fd4fdbc7786fc864a852148820146d9c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 14:49:49 +0000 Subject: [PATCH 07/26] Guard free calls --- src/crypto/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index a02d79995..badf89013 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -319,7 +319,7 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { const encodedPrivateKey = encodeRecoveryKey(privateKey); return [keyInfo, encodedPrivateKey, privateKey]; } finally { - decryption.free(); + if (decryption) decryption.free(); } }; @@ -790,7 +790,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { throw new Error("Cross-signing master private key not available"); } } finally { - signing.free(); + if (signing) signing.free(); } logger.info("Got matching private key from callback for new public master key"); From d35f5152a9216a5101d36fe0b1f5410279428a8e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 15:11:48 +0000 Subject: [PATCH 08/26] Restore key backup from stored key --- src/client.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 834cc9615..e22169946 100644 --- a/src/client.js +++ b/src/client.js @@ -53,7 +53,7 @@ import { isCryptoAvailable } from './crypto'; import { decodeRecoveryKey } from './crypto/recoverykey'; import { keyFromAuthData } from './crypto/key_passphrase'; import { randomString } from './randomstring'; -import { encodeBase64 } from '../lib/crypto/olmlib'; +import { encodeBase64, decodeBase64 } from '../lib/crypto/olmlib'; const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = isCryptoAvailable(); @@ -1634,6 +1634,15 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function( ); }; +MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function( + backupInfo, targetRoomId, targetSessionId, +) { + const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1")); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, backupInfo, + ); +}; + MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( recoveryKey, targetRoomId, targetSessionId, backupInfo, ) { From f2c5b2bd499796ea1cd8052cc95744d7d1882865 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 15:41:56 +0000 Subject: [PATCH 09/26] Emit user trust via the client --- src/client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.js b/src/client.js index 7fdce7a43..230aa364d 100644 --- a/src/client.js +++ b/src/client.js @@ -673,6 +673,7 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.warning", "crypto.devicesUpdated", "deviceVerificationChanged", + "userTrustStatusChanged", "crossSigning.keysChanged", ]); From 1fc2ab7f7df5ab101e01f6e181e006a661ec5475 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 16:06:46 +0000 Subject: [PATCH 10/26] Fix backup tests --- spec/unit/crypto/backup.spec.js | 4 ++-- src/client.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index acba78310..f4d6a71cf 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -336,7 +336,7 @@ describe("MegolmBackup", function() { }); await client.resetCrossSigningKeys(); let numCalls = 0; - await new Promise(async (resolve, reject) => { + await new Promise((resolve, reject) => { client._http.authedRequest = function( callback, method, path, queryParams, data, opts, ) { @@ -361,7 +361,7 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({}); }; - await client.createKeyBackupVersion({ + client.createKeyBackupVersion({ algorithm: "m.megolm_backup.v1", auth_data: { public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", diff --git a/src/client.js b/src/client.js index 230aa364d..b295b9c03 100644 --- a/src/client.js +++ b/src/client.js @@ -1519,7 +1519,7 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { // sessions. await this.checkKeyBackup(); if (!this.getKeyBackupEnabled()) { - throw new Error("Key backup not usable even though we just created it"); + logger.error("Key backup not usable even though we just created it"); } return res; From 049b769f68a4800085b4123fb81147af7c30197d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 12 Dec 2019 17:27:01 +0000 Subject: [PATCH 11/26] Add docs --- src/client.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/client.js b/src/client.js index b295b9c03..3503530b7 100644 --- a/src/client.js +++ b/src/client.js @@ -1477,6 +1477,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function( /** * Check whether the key backup private key is stored in secret storage. + * @return {Promise} Whether the backup key is stored. */ MatrixClient.prototype.isKeyBackupKeyStored = async function() { return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */); @@ -1625,6 +1626,18 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; +/** + * Restore from an existing key backup via a passphrase. + * + * @param {string} password Passphrase + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ MatrixClient.prototype.restoreKeyBackupWithPassword = async function( password, targetRoomId, targetSessionId, backupInfo, ) { @@ -1634,6 +1647,18 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function( ); }; +/** + * Restore from an existing key backup via a private key stored in secret + * storage. + * + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function( backupInfo, targetRoomId, targetSessionId, ) { @@ -1643,6 +1668,18 @@ MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function( ); }; +/** + * Restore from an existing key backup via an encoded recovery key. + * + * @param {string} recoveryKey Encoded recovery key + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( recoveryKey, targetRoomId, targetSessionId, backupInfo, ) { From 83a8a0cf2125be9573f03d48b25d3ebe0cc93730 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 11:23:57 +0000 Subject: [PATCH 12/26] Add support for passthrough SSSS secrets So we can migrate key backup keys Adding a passthrough secret itself isn't exposed outside of the js-sdk: hopefully this should only ever be necessary for this bootstrap process which the js-sdk handles. --- src/crypto/SecretStorage.js | 31 +++++++++++++++++++++++++--- src/crypto/index.js | 40 +++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index db984b950..f557c6383 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -231,6 +231,26 @@ export default class SecretStorage extends EventEmitter { await this._baseApis.setAccountData(name, {encrypted}); } + /** + * Store a secret defined to be the same as the given key. + * No secret information will be stored, instead the secret will + * be stored with a marker to say that the contents of the secret is + * the value of the given key. + * This is useful for migration from systems that predate SSSS such as + * key backup. + * + * @param {string} name The name of the secret + * @param {string} keyId The ID of the key whose value will be the + * value of the secret + */ + storePassthrough(name, keyId) { + return this._baseApis.setAccountData(name, { + [keyId]: { + passthrough: true, + } + }); + } + /** * Get a secret from storage. * @@ -273,11 +293,14 @@ export default class SecretStorage extends EventEmitter { let keyId; let decryption; try { + const encInfo = secretContent.encrypted[keyId]; + // fetch private key from app - [keyId, decryption] = await this._getSecretStorageKey(keys); + [keyId, decryption] = await this._getSecretStorageKey(keys, encInfo.passthrough); + + if (encInfo.passthrough) return decryption; // decrypt secret - const encInfo = secretContent.encrypted[keyId]; switch (keys[keyId].algorithm) { case SECRET_STORAGE_ALGORITHM_V1: return decryption.decrypt( @@ -518,7 +541,7 @@ export default class SecretStorage extends EventEmitter { } } - async _getSecretStorageKey(keys) { + async _getSecretStorageKey(keys, raw) { if (!this._cryptoCallbacks.getSecretStorageKey) { throw new Error("No getSecretStorageKey callback supplied"); } @@ -537,6 +560,8 @@ export default class SecretStorage extends EventEmitter { throw new Error("App returned unknown key from getSecretStorageKey!"); } + if (raw) return [keyId, privateKey]; + switch (keys[keyId].algorithm) { case SECRET_STORAGE_ALGORITHM_V1: { diff --git a/src/crypto/index.js b/src/crypto/index.js index badf89013..817db2ac0 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -336,6 +336,8 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { * auth data as an object. * @param {function} [opts.createSecretStorageKey] Optional. Function * called to await a secret storage key creation flow. + * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * the passphrase and recovery key from this backup will be used. * Returns: * {Promise} A promise which resolves to key creation data for * SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields. @@ -343,6 +345,7 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) { Crypto.prototype.bootstrapSecretStorage = async function({ authUploadDeviceSigningKeys, createSecretStorageKey = async () => { }, + keyBackupInfo, } = {}) { logger.log("Bootstrapping Secure Secret Storage"); @@ -388,12 +391,37 @@ Crypto.prototype.bootstrapSecretStorage = async function({ // Check if Secure Secret Storage has a default key. If we don't have one, create // the default key (which will also be signed by the cross-signing master key). if (!this.hasSecretStorageKey()) { - logger.log("Secret storage default key not found, creating new key"); - const keyOptions = await createSecretStorageKey(); - const newKeyId = await this.addSecretStorageKey( - SECRET_STORAGE_ALGORITHM_V1, - keyOptions, - ); + let newKeyId; + if (keyBackupInfo) { + logger.log("Secret storage default key not found, using key backup key"); + const opts = { + pubkey: keyBackupInfo.auth_data.public_key, + }; + + if ( + keyBackupInfo.auth_data.private_key_salt && + keyBackupInfo.auth_data.private_key_iterations + ) { + opts.passphrase = { + algorithm: "m.pbkdf2", + iterations: keyBackupInfo.auth_data.private_key_iterations, + salt: keyBackupInfo.auth_data.private_key_salt, + }; + } + + newKeyId = await cli.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1, opts); + + // Add an entry for the backup key in SSSS as a 'passthrough' key + // (ie. the secret is the key itself). + this._secretStorage.storePassthrough('m.megolm_backup.v1', newKeyId); + } else { + logger.log("Secret storage default key not found, creating new key"); + const keyOptions = await createSecretStorageKey(); + newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1, + keyOptions, + ); + } await this.setDefaultSecretStorageKeyId(newKeyId); } From 8f7ed1dc15ea78afc14b89e6cec84517d26a9ab6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 11:50:25 +0000 Subject: [PATCH 13/26] Lint --- src/crypto/SecretStorage.js | 7 +++++-- src/crypto/index.js | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index f557c6383..f408d8a2d 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -242,12 +242,13 @@ export default class SecretStorage extends EventEmitter { * @param {string} name The name of the secret * @param {string} keyId The ID of the key whose value will be the * value of the secret + * @returns {Promise} resolved when account data is saved */ storePassthrough(name, keyId) { return this._baseApis.setAccountData(name, { [keyId]: { passthrough: true, - } + }, }); } @@ -296,7 +297,9 @@ export default class SecretStorage extends EventEmitter { const encInfo = secretContent.encrypted[keyId]; // fetch private key from app - [keyId, decryption] = await this._getSecretStorageKey(keys, encInfo.passthrough); + [keyId, decryption] = await this._getSecretStorageKey( + keys, encInfo.passthrough, + ); if (encInfo.passthrough) return decryption; diff --git a/src/crypto/index.js b/src/crypto/index.js index 817db2ac0..db891fafd 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -409,7 +409,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({ }; } - newKeyId = await cli.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1, opts); + newKeyId = await this.addSecretStorageKey( + SECRET_STORAGE_ALGORITHM_V1, opts, + ); // Add an entry for the backup key in SSSS as a 'passthrough' key // (ie. the secret is the key itself). From 36db57615db7d93bd90cffdf96329f601e29cbe3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 13:21:05 +0000 Subject: [PATCH 14/26] stop retrying to send event if we get M_TOO_LARGE --- src/scheduler.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scheduler.js b/src/scheduler.js index 85ce50fd3..6e3e9bf33 100644 --- a/src/scheduler.js +++ b/src/scheduler.js @@ -156,6 +156,11 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) { return -1; } + // if event that we are trying to send is too large in any way then retrying won't help + if (err.name === "M_TOO_LARGE") { + return -1; + } + if (err.name === "M_LIMIT_EXCEEDED") { const waitTime = err.data.retry_after_ms; if (waitTime) { From 41b763f33182a7fd62cfebecca49c533bd7e935c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 17:25:28 +0000 Subject: [PATCH 15/26] Just get the private key from the decryption object --- src/crypto/SecretStorage.js | 15 ++++++--------- src/crypto/index.js | 4 ++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index f408d8a2d..52dbdee2c 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -294,14 +294,12 @@ export default class SecretStorage extends EventEmitter { let keyId; let decryption; try { + // fetch private key from app + [keyId, decryption] = await this._getSecretStorageKey(keys); + const encInfo = secretContent.encrypted[keyId]; - // fetch private key from app - [keyId, decryption] = await this._getSecretStorageKey( - keys, encInfo.passthrough, - ); - - if (encInfo.passthrough) return decryption; + if (encInfo.passthrough) return decryption.get_private_key();; // decrypt secret switch (keys[keyId].algorithm) { @@ -544,7 +542,7 @@ export default class SecretStorage extends EventEmitter { } } - async _getSecretStorageKey(keys, raw) { + async _getSecretStorageKey(keys) { if (!this._cryptoCallbacks.getSecretStorageKey) { throw new Error("No getSecretStorageKey callback supplied"); } @@ -563,8 +561,6 @@ export default class SecretStorage extends EventEmitter { throw new Error("App returned unknown key from getSecretStorageKey!"); } - if (raw) return [keyId, privateKey]; - switch (keys[keyId].algorithm) { case SECRET_STORAGE_ALGORITHM_V1: { @@ -587,5 +583,6 @@ export default class SecretStorage extends EventEmitter { default: throw new Error("Unknown key type: " + keys[keyId].algorithm); } +p } } diff --git a/src/crypto/index.js b/src/crypto/index.js index db891fafd..fa547c395 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -386,6 +386,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({ { authUploadDeviceSigningKeys }, ); } + } else { + logger.log("Cross signing keys are present in secret storage"); } // Check if Secure Secret Storage has a default key. If we don't have one, create @@ -425,6 +427,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({ ); } await this.setDefaultSecretStorageKeyId(newKeyId); + } else { + logger.log("Have secret storage key"); } // If cross-signing keys were reset, store them in Secure Secret Storage. From 1816d7aa4cb031992c7ba99356a48ea03faac48b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 17:27:15 +0000 Subject: [PATCH 16/26] comment --- src/crypto/SecretStorage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 52dbdee2c..7fc978d4d 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -299,7 +299,9 @@ export default class SecretStorage extends EventEmitter { const encInfo = secretContent.encrypted[keyId]; - if (encInfo.passthrough) return decryption.get_private_key();; + // We don't actually need the decryption object if it's a passthrough + // since we just want to return the key itself. + if (encInfo.passthrough) return decryption.get_private_key(); // decrypt secret switch (keys[keyId].algorithm) { From 320ab050fe47dbcd405e7bbcda90e8aac13572f8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 17:28:07 +0000 Subject: [PATCH 17/26] Stray p --- src/crypto/SecretStorage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.js index 7fc978d4d..36aa59cf3 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.js @@ -585,6 +585,5 @@ export default class SecretStorage extends EventEmitter { default: throw new Error("Unknown key type: " + keys[keyId].algorithm); } -p } } From b39abba41ea6fdd961c3c19332108d20925ff9fb Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 13:32:31 +0000 Subject: [PATCH 18/26] Use checkDeviceTrust with key backup We did check if it was signed with a cross signing key which should be the norm going forward, but for completeness, use the proper cross-signing ernabled check for sigs from individual devices too. Also adds a deviceTrust member to the signature so the app can see the cross-signing trust status ofthe device without having to recalculate it. --- src/crypto/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index fa547c395..10b0a0e54 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1068,6 +1068,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ); if (device) { sigInfo.device = device; + sigInfo.deviceTrust = await this.checkDeviceTrust(this._userId, sigInfo.deviceId); try { await olmlib.verifySignature( this._olmDevice, @@ -1095,7 +1096,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ret.usable = ret.sigs.some((s) => { return ( s.valid && ( - (s.device && s.device.isVerified()) || + (s.device && s.deviceTrust.isVerified()) || (s.crossSigningId) ) ); From 0438c6c51c335b640b8b30ebbf0a7f89bcccb4ed Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 13:37:55 +0000 Subject: [PATCH 19/26] Oh great linter, your wisdom knows no bounds. --- src/crypto/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 10b0a0e54..c5340ad30 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1068,7 +1068,9 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ); if (device) { sigInfo.device = device; - sigInfo.deviceTrust = await this.checkDeviceTrust(this._userId, sigInfo.deviceId); + sigInfo.deviceTrust = await this.checkDeviceTrust( + this._userId, sigInfo.deviceId, + ); try { await olmlib.verifySignature( this._olmDevice, From 20bc8071fc75929f7d62614183db75d0927603ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 13:52:36 +0000 Subject: [PATCH 20/26] Fix creating a key backup with cross signing diabled It broke if no scret key callback was supplied but a cross-signing identity did exist (as hopefully explained in comment). Fixes https://github.com/vector-im/riot-web/issues/11763 --- src/client.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 3503530b7..00cf2824d 100644 --- a/src/client.js +++ b/src/client.js @@ -1505,8 +1505,11 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { // favour of just signing with the cross-singing master key. await this._crypto._signObject(data.auth_data); - if (this._crypto._crossSigningInfo.getId()) { + if (this._cryptoCallbacks.getSecretStorageKey && this._crypto._crossSigningInfo.getId()) { // now also sign the auth data with the cross-signing master key + // we check for the callback explicitly here because we still want to be able + // to create an un-cross-signed key backup if there is a cross-signing key but + // no callback supplied. await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); } From fbb355c5c9d93cad932a6da7564a0c744746038b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 14:02:38 +0000 Subject: [PATCH 21/26] Thank you once again, o great linter, for saving our lines from being too long --- src/client.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 00cf2824d..e89ea5035 100644 --- a/src/client.js +++ b/src/client.js @@ -1505,7 +1505,10 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { // favour of just signing with the cross-singing master key. await this._crypto._signObject(data.auth_data); - if (this._cryptoCallbacks.getSecretStorageKey && this._crypto._crossSigningInfo.getId()) { + if ( + this._cryptoCallbacks.getSecretStorageKey && + this._crypto._crossSigningInfo.getId() + ) { // now also sign the auth data with the cross-signing master key // we check for the callback explicitly here because we still want to be able // to create an un-cross-signed key backup if there is a cross-signing key but From e998be3a9b5ba010f1d165f0b413392a8c57cae8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 3 Jan 2020 14:21:02 +0000 Subject: [PATCH 22/26] Fix typos in comments --- src/crypto/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index c5340ad30..c4e37c16f 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -588,8 +588,8 @@ Crypto.prototype.resetCrossSigningKeys = async function(level, { /** * Run various follow-up actions after cross-signing keys have changed locally - * (either by resetting the keys for the account or bye getting them from secret - * storaoge), such as signing the current device, upgrading device + * (either by resetting the keys for the account or by getting them from secret + * storage), such as signing the current device, upgrading device * verifications, etc. */ Crypto.prototype._afterCrossSigningLocalKeyChange = async function() { From 5487cf20707cd51e94e53de8b01eb70ab22993bc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 14:36:04 +0000 Subject: [PATCH 23/26] Fix callback check We need to check for getCrossSisgningKey but that was added unconditionally elsewhere - only add it if we actually have a getSecretStorageKey callback to use. --- src/client.js | 2 +- src/crypto/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index e89ea5035..1ba3c4604 100644 --- a/src/client.js +++ b/src/client.js @@ -1506,7 +1506,7 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { await this._crypto._signObject(data.auth_data); if ( - this._cryptoCallbacks.getSecretStorageKey && + this._cryptoCallbacks.getCrossSigningKey && this._crypto._crossSigningInfo.getId() ) { // now also sign the auth data with the cross-signing master key diff --git a/src/crypto/index.js b/src/crypto/index.js index fa547c395..b2c4001c0 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -218,7 +218,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, ); // Assuming no app-supplied callback, default to getting from SSSS. - if (!cryptoCallbacks.getCrossSigningKey) { + if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { cryptoCallbacks.getCrossSigningKey = async (type) => { return CrossSigningInfo.getFromSecretStorage(type, this._secretStorage); }; From 99e58b0297ed7146829404e024acd0d037d180db Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jan 2020 19:40:04 +0000 Subject: [PATCH 24/26] Make displayName disambiguation more fuzzy especially against RTL/LTR content Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/models/room-member.js | 12 ++++++++++-- src/utils.js | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index d92267720..2070f1311 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -311,10 +311,18 @@ function calculateDisplayName(selfUserId, displayName, roomState) { // Next check if the name contains something that look like a mxid // If it does, it may be someone trying to impersonate someone else // Show full mxid in this case - // Also show mxid if there are other people with the same or similar - // displayname, after hidden character removal. let disambiguate = /@.+:.+/.test(displayName); + if (!disambiguate) { + // Also show mxid if the display name contains any LTR/RTL characters as these + // make it very difficult for us to find similar *looking* display names + // E.g "Mark" could be cloned by writing "kraM" but in RTL. + disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName); + } + + if (!disambiguate) { + // Also show mxid if there are other people with the same or similar + // displayname, after hidden character removal. const userIds = roomState.getUserIdsWithDisplayName(displayName); disambiguate = userIds.some((u) => u !== selfUserId); } diff --git a/src/utils.js b/src/utils.js index e830a5daf..cd982c22c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -675,7 +675,14 @@ module.exports.isNumber = function(value) { module.exports.removeHiddenChars = function(str) { return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, '')); }; -const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g; +// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters. +// Includes: +// various width spaces U+2000 - U+200D +// LTR and RTL marks U+200E and U+200F +// LTR/RTL and other directional formatting marks U+202A - U+202F +// Combining characters U+0300 - U+036F +// Zero width no-break space (BOM) U+FEFF +const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); From 01f6b3dfc6f8b64622691abc77d951cbe5252d12 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 6 Jan 2020 17:47:22 -0500 Subject: [PATCH 25/26] notify devices when we don't send them keys (#1135) and handle incoming notifications --- spec/integ/megolm-integ.spec.js | 6 + spec/unit/crypto/algorithms/megolm.spec.js | 153 ++++++++++ src/crypto/OlmDevice.js | 121 +++++++- src/crypto/algorithms/megolm.js | 264 +++++++++++++++--- src/crypto/index.js | 25 +- .../store/indexeddb-crypto-store-backend.js | 45 ++- src/crypto/store/indexeddb-crypto-store.js | 18 +- src/crypto/store/localStorage-crypto-store.js | 30 +- src/crypto/store/memory-crypto-store.js | 15 +- 9 files changed, 621 insertions(+), 56 deletions(-) diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index 858568e94..459725e2c 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -617,6 +617,9 @@ describe("megolm", function() { ).respond(200, { event_id: '$event_id', }); + aliceTestClient.httpBackend.when( + 'PUT', '/sendToDevice/org.matrix.room_key.withheld/', + ).respond(200, {}); return Promise.all([ aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'), @@ -714,6 +717,9 @@ describe("megolm", function() { event_id: '$event_id', }; }); + aliceTestClient.httpBackend.when( + 'PUT', '/sendToDevice/org.matrix.room_key.withheld/', + ).respond(200, {}); return Promise.all([ aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'), diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 88beac84f..57f6a3fe4 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -8,6 +8,9 @@ import testUtils from '../../../test-utils'; import OlmDevice from '../../../../lib/crypto/OlmDevice'; import Crypto from '../../../../lib/crypto'; import logger from '../../../../lib/logger'; +import TestClient from '../../../TestClient'; +import olmlib from '../../../../lib/crypto/olmlib'; +import Room from '../../../../lib/models/room'; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -342,4 +345,154 @@ describe("MegolmDecryption", function() { expect(ct2.session_id).toEqual(ct1.session_id); }); }); + + it("notifies devices that have been blocked", async function() { + const aliceClient = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const bobClient1 = (new TestClient( + "@bob:example.com", "bobdevice1", + )).client; + const bobClient2 = (new TestClient( + "@bob:example.com", "bobdevice2", + )).client; + await Promise.all([ + aliceClient.initCrypto(), + bobClient1.initCrypto(), + bobClient2.initCrypto(), + ]); + const aliceDevice = aliceClient._crypto._olmDevice; + const bobDevice1 = bobClient1._crypto._olmDevice; + const bobDevice2 = bobClient2._crypto._olmDevice; + + const encryptionCfg = { + "algorithm": "m.megolm.v1.aes-sha2", + }; + const roomId = "!someroom"; + const room = new Room(roomId, aliceClient, "@alice:example.com", {}); + room.getEncryptionTargetMembers = async function() { + return [{userId: "@bob:example.com"}]; + }; + room.setBlacklistUnverifiedDevices(true); + aliceClient.store.storeRoom(room); + await aliceClient.setRoomEncryption(roomId, encryptionCfg); + + const BOB_DEVICES = { + bobdevice1: { + user_id: "@bob:example.com", + device_id: "bobdevice1", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Dynabook": bobDevice1.deviceEd25519Key, + "curve25519:Dynabook": bobDevice1.deviceCurve25519Key, + }, + verified: 0, + }, + bobdevice2: { + user_id: "@bob:example.com", + device_id: "bobdevice2", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Dynabook": bobDevice2.deviceEd25519Key, + "curve25519:Dynabook": bobDevice2.deviceCurve25519Key, + }, + verified: -1, + }, + }; + + aliceClient._crypto._deviceList.storeDevicesForUser( + "@bob:example.com", BOB_DEVICES, + ); + aliceClient._crypto._deviceList.downloadKeys = async function(userIds) { + return this._getDevicesFromStore(userIds); + }; + + let run = false; + aliceClient.sendToDevice = async (msgtype, contentMap) => { + run = true; + expect(msgtype).toBe("org.matrix.room_key.withheld"); + delete contentMap["@bob:example.com"].bobdevice1.session_id; + delete contentMap["@bob:example.com"].bobdevice2.session_id; + expect(contentMap).toStrictEqual({ + '@bob:example.com': { + bobdevice1: { + algorithm: "m.megolm.v1.aes-sha2", + room_id: roomId, + code: 'm.unverified', + reason: + 'The sender has disabled encrypting to unverified devices.', + sender_key: aliceDevice.deviceCurve25519Key, + }, + bobdevice2: { + algorithm: "m.megolm.v1.aes-sha2", + room_id: roomId, + code: 'm.blacklisted', + reason: 'The sender has blocked you.', + sender_key: aliceDevice.deviceCurve25519Key, + }, + }, + }); + }; + + const event = new MatrixEvent({ + type: "m.room.message", + sender: "@alice:example.com", + room_id: roomId, + event_id: "$event", + content: { + msgtype: "m.text", + body: "secret", + }, + }); + await aliceClient._crypto.encryptEvent(event, room); + + expect(run).toBe(true); + + aliceClient.stopClient(); + bobClient1.stopClient(); + bobClient2.stopClient(); + }); + + it("throws an error describing why it doesn't have a key", async function() { + const aliceClient = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const bobClient = (new TestClient( + "@bob:example.com", "bobdevice", + )).client; + await Promise.all([ + aliceClient.initCrypto(), + bobClient.initCrypto(), + ]); + const bobDevice = bobClient._crypto._olmDevice; + + const roomId = "!someroom"; + + aliceClient._crypto._onToDeviceEvent(new MatrixEvent({ + type: "org.matrix.room_key.withheld", + sender: "@bob:example.com", + content: { + algorithm: "m.megolm.v1.aes-sha2", + room_id: roomId, + session_id: "session_id", + sender_key: bobDevice.deviceCurve25519Key, + code: "m.blacklisted", + reason: "You have been blocked", + }, + })); + + await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({ + type: "m.room.encrypted", + sender: "@bob:example.com", + event_id: "$event", + room_id: roomId, + content: { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "blablabla", + device_id: "bobdevice", + sender_key: bobDevice.deviceCurve25519Key, + session_id: "session_id", + }, + }))).rejects.toThrow("The sender has blocked you."); + }); }); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index b86b4f858..0c3161c34 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017, 2019 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +19,8 @@ limitations under the License. import logger from '../logger'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; +import algorithms from './algorithms'; + // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; @@ -818,9 +821,9 @@ OlmDevice.prototype._getInboundGroupSession = function( roomId, senderKey, sessionId, txn, func, ) { this._cryptoStore.getEndToEndInboundGroupSession( - senderKey, sessionId, txn, (sessionData) => { + senderKey, sessionId, txn, (sessionData, withheld) => { if (sessionData === null) { - func(null); + func(null, null, withheld); return; } @@ -834,7 +837,7 @@ OlmDevice.prototype._getInboundGroupSession = function( } this._unpickleInboundGroupSession(sessionData, (session) => { - func(session, sessionData); + func(session, sessionData, withheld); }); }, ); @@ -859,7 +862,10 @@ OlmDevice.prototype.addInboundGroupSession = async function( exportFormat, ) { await this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + 'readwrite', [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, + ], (txn) => { /* if we already have this session, consider updating it */ this._getInboundGroupSession( roomId, senderKey, sessionId, txn, @@ -914,6 +920,60 @@ OlmDevice.prototype.addInboundGroupSession = async function( ); }; +/** + * Record in the data store why an inbound group session was withheld. + * + * @param {string} roomId room that the session belongs to + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {string} code reason code + * @param {string} reason human-readable version of `code` + */ +OlmDevice.prototype.addInboundGroupSessionWithheld = async function( + roomId, senderKey, sessionId, code, reason, +) { + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], + (txn) => { + this._cryptoStore.storeEndToEndInboundGroupSessionWithheld( + senderKey, sessionId, + { + room_id: roomId, + code: code, + reason: reason, + }, + txn, + ); + }, + ); +}; + +export const WITHHELD_MESSAGES = { + "m.unverified": "The sender has disabled encrypting to unverified devices.", + "m.blacklisted": "The sender has blocked you.", + "m.unauthorised": "You are not authorised to read the message.", + "m.no_olm": "Unable to establish a secure channel.", +}; + +/** + * Calculate the message to use for the exception when a session key is withheld. + * + * @param {object} withheld An object that describes why the key was withheld. + * + * @return {string} the message + * + * @private + */ +function _calculateWithheldMessage(withheld) { + if (withheld.code && withheld.code in WITHHELD_MESSAGES) { + return WITHHELD_MESSAGES[withheld.code]; + } else if (withheld.reason) { + return withheld.reason; + } else { + return "decryption key withheld"; + } +} + /** * Decrypt a received message with an inbound group session * @@ -934,16 +994,48 @@ OlmDevice.prototype.decryptGroupMessage = async function( roomId, senderKey, sessionId, body, eventId, timestamp, ) { let result; + // when the localstorage crypto store is used as an indexeddb backend, + // exceptions thrown from within the inner function are not passed through + // to the top level, so we store exceptions in a variable and raise them at + // the end + let error; await this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + 'readwrite', [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, + ], (txn) => { this._getInboundGroupSession( - roomId, senderKey, sessionId, txn, (session, sessionData) => { + roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => { if (session === null) { + if (withheld) { + error = new algorithms.DecryptionError( + "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + _calculateWithheldMessage(withheld), + { + session: senderKey + '|' + sessionId, + }, + ); + } result = null; return; } - const res = session.decrypt(body); + let res; + try { + res = session.decrypt(body); + } catch (e) { + if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) { + error = new algorithms.DecryptionError( + "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + _calculateWithheldMessage(withheld), + { + session: senderKey + '|' + sessionId, + }, + ); + } else { + error = e; + } + } let plaintext = res.plaintext; if (plaintext === undefined) { @@ -965,7 +1057,7 @@ OlmDevice.prototype.decryptGroupMessage = async function( msgInfo.id !== eventId || msgInfo.timestamp !== timestamp ) { - throw new Error( + error = new Error( "Duplicate message index, possible replay attack: " + messageIndexKey, ); @@ -994,6 +1086,9 @@ OlmDevice.prototype.decryptGroupMessage = async function( }, ); + if (error) { + throw error; + } return result; }; @@ -1009,7 +1104,10 @@ OlmDevice.prototype.decryptGroupMessage = async function( OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) { let result; await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + 'readonly', [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, + ], (txn) => { this._cryptoStore.getEndToEndInboundGroupSession( senderKey, sessionId, txn, (sessionData) => { if (sessionData === null) { @@ -1060,7 +1158,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function( ) { let result; await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + 'readonly', [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, + ], (txn) => { this._getInboundGroupSession( roomId, senderKey, sessionId, txn, (session, sessionData) => { if (session === null) { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 8c60adf5c..909211d6a 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,6 +29,8 @@ const utils = require("../../utils"); const olmlib = require("../olmlib"); const base = require("./base"); +import {WITHHELD_MESSAGES} from '../OlmDevice'; + /** * @private * @constructor @@ -47,6 +50,7 @@ function OutboundSessionInfo(sessionId) { this.useCount = 0; this.creationTime = new Date().getTime(); this.sharedWithDevices = {}; + this.blockedDevicesNotified = {}; } @@ -84,6 +88,15 @@ OutboundSessionInfo.prototype.markSharedWithDevice = function( this.sharedWithDevices[userId][deviceId] = chainIndex; }; +OutboundSessionInfo.prototype.markNotifiedBlockedDevice = function( + userId, deviceId, +) { + if (!this.blockedDevicesNotified[userId]) { + this.blockedDevicesNotified[userId] = {}; + } + this.blockedDevicesNotified[userId][deviceId] = true; +}; + /** * Determine if this session has been shared with devices which it shouldn't * have been. @@ -166,11 +179,14 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm); * @private * * @param {Object} devicesInRoom The devices in this room, indexed by user ID + * @param {Object} blocked The devices that are blocked, indexed by user ID * * @return {module:client.Promise} Promise which resolves to the * OutboundSessionInfo when setup is complete. */ -MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { +MegolmEncryption.prototype._ensureOutboundSession = async function( + devicesInRoom, blocked, +) { const self = this; let session; @@ -237,9 +253,36 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { } } - return self._shareKeyWithDevices( + await self._shareKeyWithDevices( session, shareMap, ); + + // are there any new blocked devices that we need to notify? + const blockedMap = {}; + for (const userId in blocked) { + if (!blocked.hasOwnProperty(userId)) { + continue; + } + + const userBlockedDevices = blocked[userId]; + + for (const deviceId in userBlockedDevices) { + if (!userBlockedDevices.hasOwnProperty(deviceId)) { + continue; + } + + if ( + !session.blockedDevicesNotified[userId] || + session.blockedDevicesNotified[userId][deviceId] === undefined + ) { + blockedMap[userId] = blockedMap[userId] || []; + blockedMap[userId].push(userBlockedDevices[deviceId]); + } + } + } + + // notify blocked devices that they're blocked + await self._notifyBlockedDevices(session, blockedMap); } // helper which returns the session prepared by prepareSession @@ -363,6 +406,42 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( return mapSlices; }; +/** + * @private + * + * @param {object} devicesByUser map from userid to list of devices + * + * @return {array>} the blocked devices, split into chunks + */ +MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) { + const maxToDeviceMessagesPerRequest = 20; + + // use an array where the slices of a content map gets stored + let currentSlice = []; + const mapSlices = [currentSlice]; + + for (const userId of Object.keys(devicesByUser)) { + const userBlockedDevicesToShareWith = devicesByUser[userId]; + + for (const blockedInfo of userBlockedDevicesToShareWith) { + if (currentSlice.length > maxToDeviceMessagesPerRequest) { + // the current slice is filled up. Start inserting into the next slice + currentSlice = []; + mapSlices.push(currentSlice); + } + + currentSlice.push({ + userId: userId, + blockedInfo: blockedInfo, + }); + } + } + if (currentSlice.length === 0) { + mapSlices.pop(); + } + return mapSlices; +}; + /** * @private * @@ -427,6 +506,49 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function( }); }; +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {array} userDeviceMap list of blocked devices to notify + * + * @param {object} payload fields to include in the notification payload + * + * @return {module:client.Promise} Promise which resolves once the notifications + * for the given userDeviceMap is generated and has been sent. + */ +MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function( + session, userDeviceMap, payload, +) { + const contentMap = {}; + + for (const val of userDeviceMap) { + const userId = val.userId; + const blockedInfo = val.blockedInfo; + const deviceInfo = blockedInfo.deviceInfo; + const deviceId = deviceInfo.deviceId; + + const message = Object.assign({}, payload); + message.code = blockedInfo.code; + message.reason = blockedInfo.reason; + + if (!contentMap[userId]) { + contentMap[userId] = {}; + } + contentMap[userId][deviceId] = message; + } + + await this._baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap); + + // store that we successfully uploaded the keys of the current slice + for (const userId of Object.keys(contentMap)) { + for (const deviceId of Object.keys(contentMap[userId])) { + session.markNotifiedBlockedDevice(userId, deviceId); + } + } +}; + /** * Re-shares a megolm session key with devices if the key has already been * sent to them. @@ -561,6 +683,42 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device } }; +/** + * Notify blocked devices that they have been blocked. + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {object} devicesByUser + * map from userid to device ID to blocked data + */ +MegolmEncryption.prototype._notifyBlockedDevices = async function( + session, devicesByUser, +) { + const payload = { + room_id: this._roomId, + session_id: session.sessionId, + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + }; + + const userDeviceMaps = this._splitBlockedDevices(devicesByUser); + + for (let i = 0; i < userDeviceMaps.length; i++) { + try { + await this._sendBlockedNotificationsToDevices( + session, userDeviceMaps[i], payload, + ); + logger.log(`Completed blacklist notification for ${session.sessionId} ` + + `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`); + } catch (e) { + logger.log(`blacklist notification for ${session.sessionId} in ` + + `${this._roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`); + + throw e; + } + } +}; + /** * @inheritdoc * @@ -570,42 +728,41 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device * * @return {module:client.Promise} Promise which resolves to the new event body */ -MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { +MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) { const self = this; logger.log(`Starting to encrypt event for ${this._roomId}`); - return this._getDevicesInRoom(room).then(function(devicesInRoom) { - // check if any of these devices are not yet known to the user. - // if so, warn the user so they can verify or ignore. - self._checkForUnknownDevices(devicesInRoom); + const [devicesInRoom, blocked] = await this._getDevicesInRoom(room); - return self._ensureOutboundSession(devicesInRoom); - }).then(function(session) { - const payloadJson = { - room_id: self._roomId, - type: eventType, - content: content, - }; + // check if any of these devices are not yet known to the user. + // if so, warn the user so they can verify or ignore. + self._checkForUnknownDevices(devicesInRoom); - const ciphertext = self._olmDevice.encryptGroupMessage( - session.sessionId, JSON.stringify(payloadJson), - ); + const session = await self._ensureOutboundSession(devicesInRoom, blocked); + const payloadJson = { + room_id: self._roomId, + type: eventType, + content: content, + }; - const encryptedContent = { - algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: self._olmDevice.deviceCurve25519Key, - ciphertext: ciphertext, - session_id: session.sessionId, - // Include our device ID so that recipients can send us a - // m.new_device message if they don't have our session key. - // XXX: Do we still need this now that m.new_device messages - // no longer exist since #483? - device_id: self._deviceId, - }; + const ciphertext = self._olmDevice.encryptGroupMessage( + session.sessionId, JSON.stringify(payloadJson), + ); - session.useCount++; - return encryptedContent; - }); + const encryptedContent = { + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: self._olmDevice.deviceCurve25519Key, + ciphertext: ciphertext, + session_id: session.sessionId, + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + // XXX: Do we still need this now that m.new_device messages + // no longer exist since #483? + device_id: self._deviceId, + }; + + session.useCount++; + return encryptedContent; }; /** @@ -654,8 +811,11 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) { * * @param {module:models/room} room * - * @return {module:client.Promise} Promise which resolves to a map - * from userId to deviceId to deviceInfo + * @return {module:client.Promise} Promise which resolves to an array whose + * first element is a map from userId to deviceId to deviceInfo indicating + * the devices that messages should be encrypted to, and whose second + * element is a map from userId to deviceId to data indicating the devices + * that are in the room but that have been blocked */ MegolmEncryption.prototype._getDevicesInRoom = async function(room) { const members = await room.getEncryptionTargetMembers(); @@ -676,6 +836,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) { // using all the device_lists changes and left fields. // See https://github.com/vector-im/riot-web/issues/2305 for details. const devices = await this._crypto.downloadKeys(roomMembers, false); + const blocked = {}; // remove any blocked devices for (const userId in devices) { if (!devices.hasOwnProperty(userId)) { @@ -690,13 +851,27 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) { if (userDevices[deviceId].isBlocked() || (userDevices[deviceId].isUnverified() && isBlacklisting) - ) { + ) { + if (!blocked[userId]) { + blocked[userId] = {}; + } + const blockedInfo = userDevices[deviceId].isBlocked() + ? { + code: "m.blacklisted", + reason: WITHHELD_MESSAGES["m.blacklisted"], + } + : { + code: "m.unverified", + reason: WITHHELD_MESSAGES["m.unverified"], + }; + blockedInfo.deviceInfo = userDevices[deviceId]; + blocked[userId][deviceId] = blockedInfo; delete userDevices[deviceId]; } } } - return devices; + return [devices, blocked]; }; /** @@ -756,6 +931,11 @@ MegolmDecryption.prototype.decryptEvent = async function(event) { event.getId(), event.getTs(), ); } catch (e) { + if (e.name === "DecryptionError") { + // re-throw decryption errors as-is + throw e; + } + let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { @@ -963,6 +1143,20 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { }); }; +/** + * @inheritdoc + * + * @param {module:models/event.MatrixEvent} event key event + */ +MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) { + const content = event.getContent(); + + await this._olmDevice.addInboundGroupSessionWithheld( + content.room_id, content.sender_key, content.session_id, content.code, + content.reason, + ); +}; + /** * @inheritdoc */ diff --git a/src/crypto/index.js b/src/crypto/index.js index c91d6f9c2..6e3f8e534 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -2,7 +2,7 @@ Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018-2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019-2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -2408,6 +2408,8 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._secretStorage._onRequestReceived(event); } else if (event.getType() === "m.secret.send") { this._secretStorage._onSecretReceived(event); + } else if (event.getType() === "org.matrix.room_key.withheld") { + this._onRoomKeyWithheldEvent(event); } else if (event.getContent().transaction_id) { this._onKeyVerificationMessage(event); } else if (event.getContent().msgtype === "m.bad.encrypted") { @@ -2447,6 +2449,27 @@ Crypto.prototype._onRoomKeyEvent = function(event) { alg.onRoomKeyEvent(event); }; +/** + * Handle a key withheld event + * + * @private + * @param {module:models/event.MatrixEvent} event key withheld event + */ +Crypto.prototype._onRoomKeyWithheldEvent = function(event) { + const content = event.getContent(); + + if (!content.room_id || !content.session_id || !content.algorithm + || !content.sender_key) { + logger.error("key withheld event is missing fields"); + return; + } + + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); + if (alg.onRoomKeyWithheldEvent) { + alg.onRoomKeyWithheldEvent(event); + } +}; + /** * Handle a general key verification event. * diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index aeead378e..acac5c9f6 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +19,7 @@ limitations under the License. import logger from '../../logger'; import utils from '../../utils'; -export const VERSION = 7; +export const VERSION = 8; /** * Implementation of a CryptoStore which is backed by an existing @@ -79,7 +80,7 @@ export class Backend { `enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id, ); - txn.oncomplete = () => { resolve(request); }; + txn.oncomplete = () => {resolve(request);}; const store = txn.objectStore("outgoingRoomKeyRequests"); store.add(request); }); @@ -428,14 +429,36 @@ export class Backend { // Inbound group sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + let session = false; + let withheld = false; const objectStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.get([senderCurve25519Key, sessionId]); getReq.onsuccess = function() { try { if (getReq.result) { - func(getReq.result.session); + session = getReq.result.session; } else { - func(null); + session = null; + } + if (withheld !== false) { + func(session, withheld); + } + } catch (e) { + abortWithException(txn, e); + } + }; + + const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld"); + const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]); + withheldGetReq.onsuccess = function() { + try { + if (withheldGetReq.result) { + withheld = withheldGetReq.result.session; + } else { + withheld = null; + } + if (session !== false) { + func(session, withheld); } } catch (e) { abortWithException(txn, e); @@ -499,6 +522,15 @@ export class Backend { }); } + storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key, sessionId, sessionData, txn, + ) { + const objectStore = txn.objectStore("inbound_group_sessions_withheld"); + objectStore.put({ + senderCurve25519Key, sessionId, session: sessionData, + }); + } + getEndToEndDeviceData(txn, func) { const objectStore = txn.objectStore("device_data"); const getReq = objectStore.get("-"); @@ -662,6 +694,11 @@ export function upgradeDatabase(db, oldVersion) { keyPath: ["senderCurve25519Key", "sessionId"], }); } + if (oldVersion < 8) { + db.createObjectStore("inbound_group_sessions_withheld", { + keyPath: ["senderCurve25519Key", "sessionId"], + }); + } // Expand as needed. } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 68c10aec1..66c353681 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -104,7 +105,10 @@ export default class IndexedDBCryptoStore { // we can fall back to a different backend. return backend.doTxn( 'readonly', - [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], + [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, + ], (txn) => { backend.getEndToEndInboundGroupSession('', '', txn, () => {}); }).then(() => { @@ -471,6 +475,16 @@ export default class IndexedDBCryptoStore { }); } + storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key, sessionId, sessionData, txn, + ) { + this._backendPromise.then(backend => { + backend.storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key, sessionId, sessionData, txn, + ); + }); + } + // End-to-end device tracking /** @@ -607,6 +621,8 @@ export default class IndexedDBCryptoStore { IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; +IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD + = 'inbound_group_sessions_withheld'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 0f304fbb1..1f8220d15 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -1,5 +1,6 @@ /* Copyright 2017, 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,6 +33,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; +const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; @@ -43,6 +45,10 @@ function keyEndToEndInboundGroupSession(senderKey, sessionId) { return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; } +function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) { + return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId; +} + function keyEndToEndRoomsPrefix(roomId) { return KEY_ROOMS_PREFIX + roomId; } @@ -125,10 +131,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { // Inbound Group Sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { - func(getJsonItem( - this.store, - keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), - )); + func( + getJsonItem( + this.store, + keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), + ), + getJsonItem( + this.store, + keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), + ), + ); } getAllEndToEndInboundGroupSessions(txn, func) { @@ -170,6 +182,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } + storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key, sessionId, sessionData, txn, + ) { + setJsonItem( + this.store, + keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), + sessionData, + ); + } + getEndToEndDeviceData(txn, func) { func(getJsonItem( this.store, KEY_DEVICE_DATA, diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index eca79f037..952af6696 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -37,6 +38,7 @@ export default class MemoryCryptoStore { this._sessions = {}; // Map of {senderCurve25519Key+'/'+sessionId -> session data object} this._inboundGroupSessions = {}; + this._inboundGroupSessionsWithheld = {}; // Opaque device data object this._deviceData = null; // roomId -> Opaque roomInfo object @@ -276,7 +278,11 @@ export default class MemoryCryptoStore { // Inbound Group Sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { - func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null); + const k = senderCurve25519Key+'/'+sessionId; + func( + this._inboundGroupSessions[k] || null, + this._inboundGroupSessionsWithheld[k] || null, + ); } getAllEndToEndInboundGroupSessions(txn, func) { @@ -306,6 +312,13 @@ export default class MemoryCryptoStore { this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; } + storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key, sessionId, sessionData, txn, + ) { + const k = senderCurve25519Key+'/'+sessionId; + this._inboundGroupSessionsWithheld[k] = sessionData; + } + // Device Data getEndToEndDeviceData(txn, func) { From 55ecb40190c68a7c0980b70ba6eb705d8720cdfa Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 7 Jan 2020 10:55:47 -0500 Subject: [PATCH 26/26] don't keep processing if we have an error --- src/crypto/OlmDevice.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 0c3161c34..7a0434682 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1035,6 +1035,7 @@ OlmDevice.prototype.decryptGroupMessage = async function( } else { error = e; } + return; } let plaintext = res.plaintext; @@ -1061,6 +1062,7 @@ OlmDevice.prototype.decryptGroupMessage = async function( "Duplicate message index, possible replay attack: " + messageIndexKey, ); + return; } } this._inboundGroupSessionMessageIndexes[messageIndexKey] = {