From 744f46417ae6bc0c6cb6d20786eae953c9c5db72 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 00:52:47 -0400 Subject: [PATCH] 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 88a5e8c5b5..dba9dd7d65 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 c04b086afa..04805b4af9 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 84bc610896..63af7c4766 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 967c0cc266..61a4c7d0a0 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 ba2521f0cd..f3b52da141 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) {