From 1c2e05e925529afe71910fa43cc6257c15f6cd03 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 31 Aug 2020 14:40:16 -0400 Subject: [PATCH 01/10] initial version of device rehydration support --- src/CrossSigningManager.js | 2 +- src/Login.js | 60 +++++++++++++++++++++++++++++++++++++- src/MatrixClientPeg.ts | 28 ++++++++++++++++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 676c41d7d7c..43d089010cd 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -40,7 +40,7 @@ export class AccessCancelledError extends Error { } } -async function confirmToDismiss() { +export async function confirmToDismiss() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), diff --git a/src/Login.js b/src/Login.js index 04805b4af93..4e46fc3665a 100644 --- a/src/Login.js +++ b/src/Login.js @@ -18,7 +18,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import Modal from './Modal'; +import * as sdk from './index'; +import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; import Matrix from "matrix-js-sdk"; +import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { @@ -159,12 +164,18 @@ export default class Login { * @returns {MatrixClientCreds} */ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { + let rehydrationKeyInfo; + let rehydrationKey; + const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, + cryptoCallbacks: { + getDehydrationKey + } }); - const data = await client.login(loginType, loginParams); + const data = await client.loginWithRehydration(null, loginType, loginParams); const wellknown = data.well_known; if (wellknown) { @@ -185,5 +196,52 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, + rehydrationKeyInfo, + rehydrationKey, + olmAccount: data._olm_account, }; } + +async function getDehydrationKey(keyInfo) { + const inputToKey = async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; + const AccessSecretStorageDialog = + sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, + /* props= */ + { + keyInfo, + checkPrivateKey: async (input) => { + // FIXME: + return true; + }, + }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(); + } + return true; + }, + }, + ); + const [input] = await finished; + if (!input) { + throw new AccessCancelledError(); + } + const key = await inputToKey(input); + return key; +} diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index be16f5fe102..61b7a040690 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -42,6 +42,9 @@ export interface IMatrixClientCreds { accessToken: string; guest: boolean; pickleKey?: string; + rehydrationKey?: Uint8Array; + rehydrationKeyInfo?: {[props: string]: any}; + olmAccount?: any; } // TODO: Move this to the js-sdk @@ -248,12 +251,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { private createClient(creds: IMatrixClientCreds): void { // TODO: Make these opts typesafe with the js-sdk - const opts = { + const opts: any = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, - userId: creds.userId, - deviceId: creds.deviceId, pickleKey: creds.pickleKey, timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), @@ -268,6 +269,23 @@ class _MatrixClientPeg implements IMatrixClientPeg { cryptoCallbacks: {}, }; + if (creds.olmAccount) { + opts.deviceToImport = { + olmDevice: { + pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), + sessions: [], + pickleKey: "DEFAULT_KEY", + }, + userId: creds.userId, + deviceId: creds.deviceId, + }; + } else { + opts.userId = creds.userId; + opts.deviceId = creds.deviceId; + } + + // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info + // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. @@ -275,6 +293,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { this.matrixClient = createMatrixClient(opts); + if (creds.rehydrationKey) { + this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {}); + } + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); From 4398f1eb949c8f4b5b3e61c51c756bec3c9c97d8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Sep 2020 16:28:42 -0400 Subject: [PATCH 02/10] support setting up dehydration from blank account, SSO support, and other fixes --- src/CrossSigningManager.js | 26 +++++++++++ src/Lifecycle.js | 44 ++++++++++++++++++- src/Login.js | 18 +++++--- src/MatrixClientPeg.ts | 24 +++++++--- .../CreateSecretStorageDialog.js | 5 +++ 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 43d089010cd..111fc26889d 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -30,6 +30,16 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; let secretStorageKeys = {}; let secretStorageBeingAccessed = false; +let dehydrationInfo = {}; + +export function cacheDehydrationKey(key, keyInfo = {}) { + dehydrationInfo = {key, keyInfo}; +} + +export function getDehydrationKeyCache() { + return dehydrationInfo; +} + function isCachingAllowed() { return secretStorageBeingAccessed; } @@ -64,6 +74,22 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { return [name, secretStorageKeys[name]]; } + // if we dehydrated a device, see if that key works for SSSS + if (dehydrationInfo.key) { + try { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) { + const key = dehydrationInfo.key; + // Save to cache to avoid future prompts in the current session + if (isCachingAllowed()) { + secretStorageKeys[name] = key; + } + dehydrationInfo = {}; + return [name, key]; + } + } catch {} + dehydrationInfo = {}; + } + const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( diff --git a/src/Lifecycle.js b/src/Lifecycle.js index d2de31eb80a..9a84d4e1f41 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -42,6 +42,7 @@ import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; +import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -311,6 +312,25 @@ async function _restoreFromLocalStorage(opts) { console.log("No pickle key available"); } + const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info"); + const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON); + const rehydrationKeyB64 = sessionStorage.getItem("mx_rehydration_key"); + const rehydrationKey = rehydrationKeyB64 && decodeBase64(rehydrationKeyB64); + const rehydrationOlmPickle = sessionStorage.getItem("mx_rehydration_account"); + let olmAccount; + if (rehydrationOlmPickle) { + olmAccount = new global.Olm.Account(); + try { + olmAccount.unpickle("DEFAULT_KEY", rehydrationOlmPickle); + } catch { + olmAccount.free(); + olmAccount = undefined; + } + } + sessionStorage.removeItem("mx_rehydration_key_info"); + sessionStorage.removeItem("mx_rehydration_key"); + sessionStorage.removeItem("mx_rehydration_account"); + console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ userId: userId, @@ -320,6 +340,9 @@ async function _restoreFromLocalStorage(opts) { identityServerUrl: isUrl, guest: isGuest, pickleKey: pickleKey, + rehydrationKey: rehydrationKey, + rehydrationKeyInfo: rehydrationKeyInfo, + olmAccount: olmAccount, }, false); return true; } else { @@ -463,7 +486,13 @@ async function _doSetLoggedIn(credentials, clearStorage) { if (localStorage) { try { - _persistCredentialsToLocalStorage(credentials); + // drop dehydration key and olm account before persisting. (Those + // get persisted for token login, but aren't needed at this point.) + const strippedCredentials = Object.assign({}, credentials); + delete strippedCredentials.rehydrationKeyInfo; + delete strippedCredentials.rehydrationKey; + delete strippedCredentials.olmAcconut; + _persistCredentialsToLocalStorage(strippedCredentials); // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. @@ -528,6 +557,19 @@ function _persistCredentialsToLocalStorage(credentials) { localStorage.setItem("mx_device_id", credentials.deviceId); } + // Temporarily save dehydration information if it's provided. This is + // needed for token logins, because the page reloads after the login, so we + // can't keep it in memory. + if (credentials.rehydrationKeyInfo) { + sessionStorage.setItem("mx_rehydration_key_info", JSON.stringify(credentials.rehydrationKeyInfo)); + } + if (credentials.rehydrationKey) { + sessionStorage.setItem("mx_rehydration_key", encodeBase64(credentials.rehydrationKey)); + } + if (credentials.olmAccount) { + sessionStorage.setItem("mx_rehydration_account", credentials.olmAccount.pickle("DEFAULT_KEY")); + } + console.log(`Session persisted for ${credentials.userId}`); } diff --git a/src/Login.js b/src/Login.js index 4e46fc3665a..0563952c5d9 100644 --- a/src/Login.js +++ b/src/Login.js @@ -20,7 +20,12 @@ limitations under the License. import Modal from './Modal'; import * as sdk from './index'; -import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; +import { + AccessCancelledError, + cacheDehydrationKey, + confirmToDismiss, + getDehydrationKeyCache, +} from "./CrossSigningManager"; import Matrix from "matrix-js-sdk"; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -164,9 +169,6 @@ export default class Login { * @returns {MatrixClientCreds} */ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { - let rehydrationKeyInfo; - let rehydrationKey; - const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, @@ -190,14 +192,16 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { } } + const dehydrationKeyCache = getDehydrationKeyCache(); + return { homeserverUrl: hsUrl, identityServerUrl: isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, - rehydrationKeyInfo, - rehydrationKey, + rehydrationKeyInfo: dehydrationKeyCache.keyInfo, + rehydrationKey: dehydrationKeyCache.key, olmAccount: data._olm_account, }; } @@ -243,5 +247,7 @@ async function getDehydrationKey(keyInfo) { throw new AccessCancelledError(); } const key = await inputToKey(input); + // need to copy the key because rehydration (unpickling) will clobber it + cacheDehydrationKey(new Uint8Array(key), keyInfo); return key; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 61b7a040690..18af378facd 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import { crossSigningCallbacks } from './CrossSigningManager'; +import { cacheDehydrationKey, crossSigningCallbacks } from './CrossSigningManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { @@ -270,33 +270,43 @@ class _MatrixClientPeg implements IMatrixClientPeg { }; if (creds.olmAccount) { + console.log("got a dehydrated account"); opts.deviceToImport = { olmDevice: { - pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), + pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"), sessions: [], - pickleKey: "DEFAULT_KEY", + pickleKey: creds.pickleKey || "DEFAULT_KEY", }, userId: creds.userId, deviceId: creds.deviceId, }; + creds.olmAccount.free(); } else { opts.userId = creds.userId; opts.deviceId = creds.deviceId; } - // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info - // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - this.matrixClient = createMatrixClient(opts); + // set dehydration key after cross-signing gets set up -- we wait until + // cross-signing is set up because we want to cross-sign the dehydrated + // key + const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey + opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { + const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); + this.matrixClient.setDehydrationKey(key, {passphrase: keyinfo.keys[name].passphrase}); + return [name, key]; + } if (creds.rehydrationKey) { - this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {}); + cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); } + this.matrixClient = createMatrixClient(opts); + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 53b3033330e..b1c9dc5a60a 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -304,6 +304,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } + const dehydrationKeyInfo = + this._recoveryKey.keyInfo && this._recoveryKey.keyInfo.passphrase + ? {passphrase: this._recoveryKey.keyInfo.passphrase} + : {}; + await cli.setDehydrationKey(this._recoveryKey.privateKey, dehydrationKeyInfo); this.props.onFinished(true); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { From b1b7215532596acac7fe17fd83f65ca317dd8e7d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 18 Sep 2020 18:08:17 -0400 Subject: [PATCH 03/10] fix lint and merge issues --- src/Login.js | 6 +++--- src/SecurityManager.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Login.js b/src/Login.js index 0563952c5d9..c04b086afa4 100644 --- a/src/Login.js +++ b/src/Login.js @@ -25,7 +25,7 @@ import { cacheDehydrationKey, confirmToDismiss, getDehydrationKeyCache, -} from "./CrossSigningManager"; +} from "./SecurityManager"; import Matrix from "matrix-js-sdk"; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -173,8 +173,8 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { baseUrl: hsUrl, idBaseUrl: isUrl, cryptoCallbacks: { - getDehydrationKey - } + getDehydrationKey, + }, }); const data = await client.loginWithRehydration(null, loginType, loginParams); diff --git a/src/SecurityManager.js b/src/SecurityManager.js index e8bd63d2ff5..967c0cc2660 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -91,7 +91,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { // if we dehydrated a device, see if that key works for SSSS if (dehydrationInfo.key) { try { - if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, keyInfo)) { const key = dehydrationInfo.key; // Save to cache to avoid future prompts in the current session if (isCachingAllowed()) { From 4e2397a79db8242f6ff04f9b5c5e61693d21533c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 18 Sep 2020 20:53:39 -0400 Subject: [PATCH 04/10] doc fixes and minor code improvement --- src/MatrixClientPeg.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index a5fa0fb3cfc..84bc610896b 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -271,11 +271,12 @@ class _MatrixClientPeg implements IMatrixClientPeg { if (creds.olmAccount) { console.log("got a dehydrated account"); + const pickleKey = creds.pickleKey || "DEFAULT_KEY"; opts.deviceToImport = { olmDevice: { - pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"), + pickledAccount: creds.olmAccount.pickle(pickleKey), sessions: [], - pickleKey: creds.pickleKey || "DEFAULT_KEY", + pickleKey: pickleKey, }, userId: creds.userId, deviceId: creds.deviceId, @@ -293,7 +294,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { // set dehydration key after cross-signing gets set up -- we wait until // cross-signing is set up because we want to cross-sign the dehydrated - // key + // device const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); @@ -302,6 +303,8 @@ class _MatrixClientPeg implements IMatrixClientPeg { } if (creds.rehydrationKey) { + // cache the key so that the SSSS prompt tries using it without + // prompting the user cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); } From 744f46417ae6bc0c6cb6d20786eae953c9c5db72 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 00:52:47 -0400 Subject: [PATCH 05/10] update to latest js-sdk changes --- src/Lifecycle.js | 72 ++++++-------- src/Login.js | 66 +------------ src/MatrixClientPeg.ts | 44 +-------- src/SecurityManager.js | 95 +++++++++++++++---- .../security/CreateSecretStorageDialog.js | 5 - 5 files changed, 109 insertions(+), 173 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 88a5e8c5b5f..dba9dd7d654 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -42,7 +42,6 @@ import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; -import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; const HOMESERVER_URL_KEY = "mx_hs_url"; @@ -187,6 +186,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { console.log("Logged in with token"); return _clearStorage().then(() => { _persistCredentialsToLocalStorage(creds); + // remember that we just logged in + sessionStorage.setItem("mx_fresh_login", true); return true; }); }).catch((err) => { @@ -313,24 +314,8 @@ async function _restoreFromLocalStorage(opts) { console.log("No pickle key available"); } - const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info"); - const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON); - const rehydrationKeyB64 = sessionStorage.getItem("mx_rehydration_key"); - const rehydrationKey = rehydrationKeyB64 && decodeBase64(rehydrationKeyB64); - const rehydrationOlmPickle = sessionStorage.getItem("mx_rehydration_account"); - let olmAccount; - if (rehydrationOlmPickle) { - olmAccount = new global.Olm.Account(); - try { - olmAccount.unpickle("DEFAULT_KEY", rehydrationOlmPickle); - } catch { - olmAccount.free(); - olmAccount = undefined; - } - } - sessionStorage.removeItem("mx_rehydration_key_info"); - sessionStorage.removeItem("mx_rehydration_key"); - sessionStorage.removeItem("mx_rehydration_account"); + const freshLogin = sessionStorage.getItem("mx_fresh_login"); + sessionStorage.removeItem("mx_fresh_login"); console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ @@ -341,9 +326,7 @@ async function _restoreFromLocalStorage(opts) { identityServerUrl: isUrl, guest: isGuest, pickleKey: pickleKey, - rehydrationKey: rehydrationKey, - rehydrationKeyInfo: rehydrationKeyInfo, - olmAccount: olmAccount, + freshLogin: freshLogin, }, false); return true; } else { @@ -387,6 +370,7 @@ async function _handleLoadSessionFailure(e) { * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ export async function setLoggedIn(credentials) { + credentials.freshLogin = true; stopMatrixClient(); const pickleKey = credentials.userId && credentials.deviceId ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) @@ -452,6 +436,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { " guest: " + credentials.guest + " hs: " + credentials.homeserverUrl + " softLogout: " + softLogout, + " freshLogin: " + credentials.freshLogin, ); // This is dispatched to indicate that the user is still in the process of logging in @@ -485,15 +470,27 @@ async function _doSetLoggedIn(credentials, clearStorage) { Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); + MatrixClientPeg.replaceUsingCreds(credentials); + const client = MatrixClientPeg.get(); + + if (credentials.freshLogin) { + // If we just logged in, try to rehydrate a device instead of using a + // new device. If it succeeds, we'll get a new device ID, so make sure + // we persist that ID to localStorage + const newDeviceId = await client.rehydrateDevice(); + if (newDeviceId) { + credentials.deviceId = newDeviceId; + } + + delete credentials.freshLogin; + } + if (localStorage) { try { - // drop dehydration key and olm account before persisting. (Those - // get persisted for token login, but aren't needed at this point.) - const strippedCredentials = Object.assign({}, credentials); - delete strippedCredentials.rehydrationKeyInfo; - delete strippedCredentials.rehydrationKey; - delete strippedCredentials.olmAcconut; - _persistCredentialsToLocalStorage(strippedCredentials); + _persistCredentialsToLocalStorage(credentials); + + // make sure we don't think that it's a fresh login any more + sessionStorage.removeItem("mx_fresh_login"); // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. @@ -511,12 +508,10 @@ async function _doSetLoggedIn(credentials, clearStorage) { console.warn("No local storage available: can't persist session!"); } - MatrixClientPeg.replaceUsingCreds(credentials); - dis.dispatch({ action: 'on_logged_in' }); await startMatrixClient(/*startSyncing=*/!softLogout); - return MatrixClientPeg.get(); + return client; } function _showStorageEvictedDialog() { @@ -558,19 +553,6 @@ function _persistCredentialsToLocalStorage(credentials) { localStorage.setItem("mx_device_id", credentials.deviceId); } - // Temporarily save dehydration information if it's provided. This is - // needed for token logins, because the page reloads after the login, so we - // can't keep it in memory. - if (credentials.rehydrationKeyInfo) { - sessionStorage.setItem("mx_rehydration_key_info", JSON.stringify(credentials.rehydrationKeyInfo)); - } - if (credentials.rehydrationKey) { - sessionStorage.setItem("mx_rehydration_key", encodeBase64(credentials.rehydrationKey)); - } - if (credentials.olmAccount) { - sessionStorage.setItem("mx_rehydration_account", credentials.olmAccount.pickle("DEFAULT_KEY")); - } - console.log(`Session persisted for ${credentials.userId}`); } diff --git a/src/Login.js b/src/Login.js index c04b086afa4..04805b4af93 100644 --- a/src/Login.js +++ b/src/Login.js @@ -18,17 +18,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Modal from './Modal'; -import * as sdk from './index'; -import { - AccessCancelledError, - cacheDehydrationKey, - confirmToDismiss, - getDehydrationKeyCache, -} from "./SecurityManager"; import Matrix from "matrix-js-sdk"; -import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; -import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { @@ -172,12 +162,9 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, - cryptoCallbacks: { - getDehydrationKey, - }, }); - const data = await client.loginWithRehydration(null, loginType, loginParams); + const data = await client.login(loginType, loginParams); const wellknown = data.well_known; if (wellknown) { @@ -192,62 +179,11 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { } } - const dehydrationKeyCache = getDehydrationKeyCache(); - return { homeserverUrl: hsUrl, identityServerUrl: isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, - rehydrationKeyInfo: dehydrationKeyCache.keyInfo, - rehydrationKey: dehydrationKeyCache.key, - olmAccount: data._olm_account, - }; -} - -async function getDehydrationKey(keyInfo) { - const inputToKey = async ({ passphrase, recoveryKey }) => { - if (passphrase) { - return deriveKey( - passphrase, - keyInfo.passphrase.salt, - keyInfo.passphrase.iterations, - ); - } else { - return decodeRecoveryKey(recoveryKey); - } }; - const AccessSecretStorageDialog = - sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); - const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", - AccessSecretStorageDialog, - /* props= */ - { - keyInfo, - checkPrivateKey: async (input) => { - // FIXME: - return true; - }, - }, - /* className= */ null, - /* isPriorityModal= */ false, - /* isStaticModal= */ false, - /* options= */ { - onBeforeClose: async (reason) => { - if (reason === "backgroundClick") { - return confirmToDismiss(); - } - return true; - }, - }, - ); - const [input] = await finished; - if (!input) { - throw new AccessCancelledError(); - } - const key = await inputToKey(input); - // need to copy the key because rehydration (unpickling) will clobber it - cacheDehydrationKey(new Uint8Array(key), keyInfo); - return key; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 84bc610896b..63af7c47665 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import { cacheDehydrationKey, crossSigningCallbacks } from './SecurityManager'; +import { crossSigningCallbacks } from './SecurityManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { @@ -42,9 +42,7 @@ export interface IMatrixClientCreds { accessToken: string; guest: boolean; pickleKey?: string; - rehydrationKey?: Uint8Array; - rehydrationKeyInfo?: {[props: string]: any}; - olmAccount?: any; + freshLogin?: boolean; } // TODO: Move this to the js-sdk @@ -251,10 +249,12 @@ class _MatrixClientPeg implements IMatrixClientPeg { private createClient(creds: IMatrixClientCreds): void { // TODO: Make these opts typesafe with the js-sdk - const opts: any = { + const opts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, + userId: creds.userId, + deviceId: creds.deviceId, pickleKey: creds.pickleKey, timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), @@ -269,45 +269,11 @@ class _MatrixClientPeg implements IMatrixClientPeg { cryptoCallbacks: {}, }; - if (creds.olmAccount) { - console.log("got a dehydrated account"); - const pickleKey = creds.pickleKey || "DEFAULT_KEY"; - opts.deviceToImport = { - olmDevice: { - pickledAccount: creds.olmAccount.pickle(pickleKey), - sessions: [], - pickleKey: pickleKey, - }, - userId: creds.userId, - deviceId: creds.deviceId, - }; - creds.olmAccount.free(); - } else { - opts.userId = creds.userId; - opts.deviceId = creds.deviceId; - } - // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - // set dehydration key after cross-signing gets set up -- we wait until - // cross-signing is set up because we want to cross-sign the dehydrated - // device - const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey - opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { - const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); - this.matrixClient.setDehydrationKey(key, {passphrase: keyinfo.keys[name].passphrase}); - return [name, key]; - } - - if (creds.rehydrationKey) { - // cache the key so that the SSSS prompt tries using it without - // prompting the user - cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); - } - this.matrixClient = createMatrixClient(opts); // we're going to add eventlisteners for each matrix event tile, so the diff --git a/src/SecurityManager.js b/src/SecurityManager.js index 967c0cc2660..61a4c7d0a03 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -31,6 +31,7 @@ import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreK // single secret storage operation, as it will clear the cached keys once the // operation ends. let secretStorageKeys = {}; +let secretStorageKeyInfo = {}; let secretStorageBeingAccessed = false; let dehydrationInfo = {}; @@ -64,7 +65,7 @@ export class AccessCancelledError extends Error { } } -export async function confirmToDismiss() { +async function confirmToDismiss() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), @@ -76,6 +77,20 @@ export async function confirmToDismiss() { return !sure; } +function makeInputToKey(keyInfo) { + return async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; +} + async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { @@ -91,12 +106,10 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { // if we dehydrated a device, see if that key works for SSSS if (dehydrationInfo.key) { try { - if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, keyInfo)) { - const key = dehydrationInfo.key; + const key = dehydrationInfo.key; + if (await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo)) { // Save to cache to avoid future prompts in the current session - if (isCachingAllowed()) { - secretStorageKeys[name] = key; - } + cacheSecretStorageKey(keyId, key, keyInfo); dehydrationInfo = {}; return [name, key]; } @@ -104,17 +117,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { dehydrationInfo = {}; } - const inputToKey = async ({ passphrase, recoveryKey }) => { - if (passphrase) { - return deriveKey( - passphrase, - keyInfo.passphrase.salt, - keyInfo.passphrase.iterations, - ); - } else { - return decodeRecoveryKey(recoveryKey); - } - }; + const inputToKey = makeInputToKey(keyInfo); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", AccessSecretStorageDialog, /* props= */ @@ -144,14 +147,54 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - cacheSecretStorageKey(keyId, key); + cacheSecretStorageKey(keyId, key, keyInfo); return [keyId, key]; } -function cacheSecretStorageKey(keyId, key) { +export async function getDehydrationKey(keyInfo, checkFunc) { + const inputToKey = makeInputToKey(keyInfo); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, + /* props= */ + { + keyInfo, + checkPrivateKey: async (input) => { + const key = await inputToKey(input); + try { + checkFunc(key); + return true; + } catch (e) { + return false; + } + }, + }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(); + } + return true; + }, + }, + ); + const [input] = await finished; + if (!input) { + throw new AccessCancelledError(); + } + const key = await inputToKey(input); + // need to copy the key because rehydration (unpickling) will clobber it + cacheDehydrationKey(key, keyInfo); + return key; +} + +function cacheSecretStorageKey(keyId, key, keyInfo) { if (isCachingAllowed()) { secretStorageKeys[keyId] = key; + secretStorageKeyInfo[keyId] = keyInfo; } } @@ -202,6 +245,7 @@ export const crossSigningCallbacks = { getSecretStorageKey, cacheSecretStorageKey, onSecretRequested, + getDehydrationKey, }; export async function promptForBackupPassphrase() { @@ -288,6 +332,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f await cli.bootstrapSecretStorage({ getKeyBackupPassphrase: promptForBackupPassphrase, }); + + const keyId = Object.keys(secretStorageKeys)[0]; + if (keyId) { + const dehydrationKeyInfo = + secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase + ? {passphrase: secretStorageKeyInfo[keyId].passphrase} + : {}; + console.log("Setting dehydration key"); + await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo); + } else { + console.log("Not setting dehydration key: no SSSS key found"); + } } // `return await` needed here to ensure `finally` block runs after the @@ -298,6 +354,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f secretStorageBeingAccessed = false; if (!isCachingAllowed()) { secretStorageKeys = {}; + secretStorageKeyInfo = {}; } } } diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index ba2521f0cd0..f3b52da141c 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -314,11 +314,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } - const dehydrationKeyInfo = - this._recoveryKey.keyInfo && this._recoveryKey.keyInfo.passphrase - ? {passphrase: this._recoveryKey.keyInfo.passphrase} - : {}; - await cli.setDehydrationKey(this._recoveryKey.privateKey, dehydrationKeyInfo); this.props.onFinished(true); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { From 503f32948c89aec83b168a2a86bb819b65593dd5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Oct 2020 20:23:12 -0400 Subject: [PATCH 06/10] try to unlock secret storage with dehydration key --- src/MatrixClientPeg.ts | 3 +- src/SecurityManager.js | 85 +++++++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 63af7c47665..69e586c58dd 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import { crossSigningCallbacks } from './SecurityManager'; +import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { @@ -193,6 +193,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { this.matrixClient.setCryptoTrustCrossSignedDevices( !SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'), ); + await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient); StorageManager.setCryptoInitialised(true); } } catch (e) { diff --git a/src/SecurityManager.js b/src/SecurityManager.js index 61a4c7d0a03..323c0c3a4f1 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -34,15 +34,9 @@ let secretStorageKeys = {}; let secretStorageKeyInfo = {}; let secretStorageBeingAccessed = false; -let dehydrationInfo = {}; +let nonInteractive = false; -export function cacheDehydrationKey(key, keyInfo = {}) { - dehydrationInfo = {key, keyInfo}; -} - -export function getDehydrationKeyCache() { - return dehydrationInfo; -} +let dehydrationCache = {}; function isCachingAllowed() { return secretStorageBeingAccessed; @@ -103,18 +97,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { return [keyId, secretStorageKeys[keyId]]; } - // if we dehydrated a device, see if that key works for SSSS - if (dehydrationInfo.key) { - try { - const key = dehydrationInfo.key; - if (await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo)) { - // Save to cache to avoid future prompts in the current session - cacheSecretStorageKey(keyId, key, keyInfo); - dehydrationInfo = {}; - return [name, key]; - } - } catch {} - dehydrationInfo = {}; + if (dehydrationCache.key) { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) { + cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo); + return [keyId, dehydrationCache.key]; + } + } + + if (nonInteractive) { + throw new Error("Could not unlock non-interactively"); } const inputToKey = makeInputToKey(keyInfo); @@ -186,8 +177,10 @@ export async function getDehydrationKey(keyInfo, checkFunc) { throw new AccessCancelledError(); } const key = await inputToKey(input); + // need to copy the key because rehydration (unpickling) will clobber it - cacheDehydrationKey(key, keyInfo); + dehydrationCache = {key: new Uint8Array(key), keyInfo}; + return key; } @@ -358,3 +351,53 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f } } } + +// FIXME: this function name is a bit of a mouthful +export async function tryToUnlockSecretStorageWithDehydrationKey(client) { + const key = dehydrationCache.key; + let restoringBackup = false; + if (key && await client.isSecretStorageReady()) { + console.log("Trying to set up cross-signing using dehydration key"); + secretStorageBeingAccessed = true; + nonInteractive = true; + try { + await client.checkOwnCrossSigningTrust(); + + // we also need to set a new dehydrated device to replace the + // device we rehydrated + const dehydrationKeyInfo = + dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase + ? {passphrase: dehydrationCache.keyInfo.passphrase} + : {}; + await client.setDehydrationKey(key, dehydrationKeyInfo); + + // and restore from backup + const backupInfo = await client.getKeyBackupVersion(); + if (backupInfo) { + restoringBackup = true; + // don't await, because this can take a long time + client.restoreKeyBackupWithSecretStorage(backupInfo) + .finally(() => { + secretStorageBeingAccessed = false; + nonInteractive = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + secretStorageKeyInfo = {}; + } + }); + } + } finally { + dehydrationCache = {}; + // the secret storage cache is needed for restoring from backup, so + // don't clear it yet if we're restoring from backup + if (!restoringBackup) { + secretStorageBeingAccessed = false; + nonInteractive = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + secretStorageKeyInfo = {}; + } + } + } + } +} From 0db81e4093b7fa36efa5fd25da1353f0a8070e6e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Oct 2020 21:41:03 -0400 Subject: [PATCH 07/10] add a feature flag for dehydration --- src/SecurityManager.js | 7 ++++--- src/settings/Settings.ts | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/SecurityManager.js b/src/SecurityManager.js index 323c0c3a4f1..3272c0f0151 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -24,6 +24,7 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; import { isSecureBackupRequired } from './utils/WellKnownUtils'; import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog'; import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; +import SettingsStore from "./settings/SettingsStore"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -327,13 +328,13 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f }); const keyId = Object.keys(secretStorageKeys)[0]; - if (keyId) { + if (keyId && SettingsStore.getValue("feature_dehydration")) { const dehydrationKeyInfo = secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase ? {passphrase: secretStorageKeyInfo[keyId].passphrase} : {}; console.log("Setting dehydration key"); - await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo); + await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); } else { console.log("Not setting dehydration key: no SSSS key found"); } @@ -369,7 +370,7 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(client) { dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase ? {passphrase: dehydrationCache.keyInfo.passphrase} : {}; - await client.setDehydrationKey(key, dehydrationKeyInfo); + await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device"); // and restore from backup const backupInfo = await client.getKeyBackupVersion(); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 737c8829192..a94ad242565 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -186,6 +186,12 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_dehydration": { + isFeature: true, + displayName: _td("Offline encrypted messaging using dehydrated devices."), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "advancedRoomListLogging": { // TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231 displayName: _td("Enable advanced debugging for the room list"), From bd9cebe35d020186687ed57dc53e84b75379ec99 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 1 Oct 2020 21:52:28 -0400 Subject: [PATCH 08/10] oh right, I need to run the i18n script too --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a3776635701..a2ad5f4b295 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -452,6 +452,7 @@ "Support adding custom themes": "Support adding custom themes", "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", + "Offline encrypted messaging using dehydrated devices.": "Offline encrypted messaging using dehydrated devices.", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", From c1765c857e5db05dd95a953ab2953eb64e3ccb92 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 2 Oct 2020 17:43:49 -0400 Subject: [PATCH 09/10] apply changes from review --- src/Lifecycle.js | 2 +- src/settings/Settings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index dba9dd7d654..dc04e475358 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -473,7 +473,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { MatrixClientPeg.replaceUsingCreds(credentials); const client = MatrixClientPeg.get(); - if (credentials.freshLogin) { + if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) { // If we just logged in, try to rehydrate a device instead of using a // new device. If it succeeds, we'll get a new device ID, so make sure // we persist that ID to localStorage diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index a94ad242565..8b96a2e819e 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -188,7 +188,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "feature_dehydration": { isFeature: true, - displayName: _td("Offline encrypted messaging using dehydrated devices."), + displayName: _td("Offline encrypted messaging using dehydrated devices"), supportedLevels: LEVELS_FEATURE, default: false, }, From 49cc62ca438f9bf2de3560d1aa3f20abada4ea48 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 2 Oct 2020 17:48:13 -0400 Subject: [PATCH 10/10] Oh right. Run i18n again. --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a2ad5f4b295..af925d10c40 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -452,7 +452,7 @@ "Support adding custom themes": "Support adding custom themes", "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", - "Offline encrypted messaging using dehydrated devices.": "Offline encrypted messaging using dehydrated devices.", + "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size",