update to latest js-sdk changes

This commit is contained in:
Hubert Chathi 2020-09-30 00:52:47 -04:00
parent 4e2397a79d
commit 744f46417a
5 changed files with 109 additions and 173 deletions

View file

@ -42,7 +42,6 @@ import {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener"; import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi"; import {Jitsi} from "./widgets/Jitsi";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; 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"; import ThreepidInviteStore from "./stores/ThreepidInviteStore";
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
@ -187,6 +186,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
console.log("Logged in with token"); console.log("Logged in with token");
return _clearStorage().then(() => { return _clearStorage().then(() => {
_persistCredentialsToLocalStorage(creds); _persistCredentialsToLocalStorage(creds);
// remember that we just logged in
sessionStorage.setItem("mx_fresh_login", true);
return true; return true;
}); });
}).catch((err) => { }).catch((err) => {
@ -313,24 +314,8 @@ async function _restoreFromLocalStorage(opts) {
console.log("No pickle key available"); console.log("No pickle key available");
} }
const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info"); const freshLogin = sessionStorage.getItem("mx_fresh_login");
const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON); sessionStorage.removeItem("mx_fresh_login");
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}`); console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({ await _doSetLoggedIn({
@ -341,9 +326,7 @@ async function _restoreFromLocalStorage(opts) {
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
pickleKey: pickleKey, pickleKey: pickleKey,
rehydrationKey: rehydrationKey, freshLogin: freshLogin,
rehydrationKeyInfo: rehydrationKeyInfo,
olmAccount: olmAccount,
}, false); }, false);
return true; return true;
} else { } else {
@ -387,6 +370,7 @@ async function _handleLoadSessionFailure(e) {
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/ */
export async function setLoggedIn(credentials) { export async function setLoggedIn(credentials) {
credentials.freshLogin = true;
stopMatrixClient(); stopMatrixClient();
const pickleKey = credentials.userId && credentials.deviceId const pickleKey = credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
@ -452,6 +436,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
" guest: " + credentials.guest + " guest: " + credentials.guest +
" hs: " + credentials.homeserverUrl + " hs: " + credentials.homeserverUrl +
" softLogout: " + softLogout, " softLogout: " + softLogout,
" freshLogin: " + credentials.freshLogin,
); );
// This is dispatched to indicate that the user is still in the process of logging in // 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); 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) { if (localStorage) {
try { try {
// drop dehydration key and olm account before persisting. (Those _persistCredentialsToLocalStorage(credentials);
// get persisted for token login, but aren't needed at this point.)
const strippedCredentials = Object.assign({}, credentials); // make sure we don't think that it's a fresh login any more
delete strippedCredentials.rehydrationKeyInfo; sessionStorage.removeItem("mx_fresh_login");
delete strippedCredentials.rehydrationKey;
delete strippedCredentials.olmAcconut;
_persistCredentialsToLocalStorage(strippedCredentials);
// The user registered as a PWLU (PassWord-Less User), the generated password // 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. // 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!"); console.warn("No local storage available: can't persist session!");
} }
MatrixClientPeg.replaceUsingCreds(credentials);
dis.dispatch({ action: 'on_logged_in' }); dis.dispatch({ action: 'on_logged_in' });
await startMatrixClient(/*startSyncing=*/!softLogout); await startMatrixClient(/*startSyncing=*/!softLogout);
return MatrixClientPeg.get(); return client;
} }
function _showStorageEvictedDialog() { function _showStorageEvictedDialog() {
@ -558,19 +553,6 @@ function _persistCredentialsToLocalStorage(credentials) {
localStorage.setItem("mx_device_id", credentials.deviceId); 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}`); console.log(`Session persisted for ${credentials.userId}`);
} }

View file

@ -18,17 +18,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 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 { export default class Login {
constructor(hsUrl, isUrl, fallbackHsUrl, opts) { constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
@ -172,12 +162,9 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
const client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
idBaseUrl: isUrl, idBaseUrl: isUrl,
cryptoCallbacks: {
getDehydrationKey,
},
}); });
const data = await client.loginWithRehydration(null, loginType, loginParams); const data = await client.login(loginType, loginParams);
const wellknown = data.well_known; const wellknown = data.well_known;
if (wellknown) { if (wellknown) {
@ -192,62 +179,11 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
} }
} }
const dehydrationKeyCache = getDehydrationKeyCache();
return { return {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token, 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;
}

View file

@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
import * as StorageManager from './utils/StorageManager'; import * as StorageManager from './utils/StorageManager';
import IdentityAuthClient from './IdentityAuthClient'; 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"; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
@ -42,9 +42,7 @@ export interface IMatrixClientCreds {
accessToken: string; accessToken: string;
guest: boolean; guest: boolean;
pickleKey?: string; pickleKey?: string;
rehydrationKey?: Uint8Array; freshLogin?: boolean;
rehydrationKeyInfo?: {[props: string]: any};
olmAccount?: any;
} }
// TODO: Move this to the js-sdk // TODO: Move this to the js-sdk
@ -251,10 +249,12 @@ class _MatrixClientPeg implements IMatrixClientPeg {
private createClient(creds: IMatrixClientCreds): void { private createClient(creds: IMatrixClientCreds): void {
// TODO: Make these opts typesafe with the js-sdk // TODO: Make these opts typesafe with the js-sdk
const opts: any = { const opts = {
baseUrl: creds.homeserverUrl, baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl, idBaseUrl: creds.identityServerUrl,
accessToken: creds.accessToken, accessToken: creds.accessToken,
userId: creds.userId,
deviceId: creds.deviceId,
pickleKey: creds.pickleKey, pickleKey: creds.pickleKey,
timelineSupport: true, timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'),
@ -269,45 +269,11 @@ class _MatrixClientPeg implements IMatrixClientPeg {
cryptoCallbacks: {}, 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 // These are always installed regardless of the labs flag so that
// cross-signing features can toggle on without reloading and also be // cross-signing features can toggle on without reloading and also be
// accessed immediately after login. // accessed immediately after login.
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); 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); this.matrixClient = createMatrixClient(opts);
// we're going to add eventlisteners for each matrix event tile, so the // we're going to add eventlisteners for each matrix event tile, so the

View file

@ -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 // single secret storage operation, as it will clear the cached keys once the
// operation ends. // operation ends.
let secretStorageKeys = {}; let secretStorageKeys = {};
let secretStorageKeyInfo = {};
let secretStorageBeingAccessed = false; let secretStorageBeingAccessed = false;
let dehydrationInfo = {}; 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 QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const [sure] = await Modal.createDialog(QuestionDialog, { const [sure] = await Modal.createDialog(QuestionDialog, {
title: _t("Cancel entering passphrase?"), title: _t("Cancel entering passphrase?"),
@ -76,6 +77,20 @@ export async function confirmToDismiss() {
return !sure; 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) { async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const keyInfoEntries = Object.entries(keyInfos); const keyInfoEntries = Object.entries(keyInfos);
if (keyInfoEntries.length > 1) { 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 we dehydrated a device, see if that key works for SSSS
if (dehydrationInfo.key) { if (dehydrationInfo.key) {
try { 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 // Save to cache to avoid future prompts in the current session
if (isCachingAllowed()) { cacheSecretStorageKey(keyId, key, keyInfo);
secretStorageKeys[name] = key;
}
dehydrationInfo = {}; dehydrationInfo = {};
return [name, key]; return [name, key];
} }
@ -104,17 +117,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
dehydrationInfo = {}; dehydrationInfo = {};
} }
const inputToKey = async ({ passphrase, recoveryKey }) => { const inputToKey = makeInputToKey(keyInfo);
if (passphrase) {
return deriveKey(
passphrase,
keyInfo.passphrase.salt,
keyInfo.passphrase.iterations,
);
} else {
return decodeRecoveryKey(recoveryKey);
}
};
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog, AccessSecretStorageDialog,
/* props= */ /* props= */
@ -144,14 +147,54 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const key = await inputToKey(input); const key = await inputToKey(input);
// Save to cache to avoid future prompts in the current session // Save to cache to avoid future prompts in the current session
cacheSecretStorageKey(keyId, key); cacheSecretStorageKey(keyId, key, keyInfo);
return [keyId, key]; 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()) { if (isCachingAllowed()) {
secretStorageKeys[keyId] = key; secretStorageKeys[keyId] = key;
secretStorageKeyInfo[keyId] = keyInfo;
} }
} }
@ -202,6 +245,7 @@ export const crossSigningCallbacks = {
getSecretStorageKey, getSecretStorageKey,
cacheSecretStorageKey, cacheSecretStorageKey,
onSecretRequested, onSecretRequested,
getDehydrationKey,
}; };
export async function promptForBackupPassphrase() { export async function promptForBackupPassphrase() {
@ -288,6 +332,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
getKeyBackupPassphrase: promptForBackupPassphrase, 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 // `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; secretStorageBeingAccessed = false;
if (!isCachingAllowed()) { if (!isCachingAllowed()) {
secretStorageKeys = {}; secretStorageKeys = {};
secretStorageKeyInfo = {};
} }
} }
} }

View file

@ -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); this.props.onFinished(true);
} catch (e) { } catch (e) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {