support setting up dehydration from blank account, SSO support, and other fixes

pull/21833/head
Hubert Chathi 2020-09-03 16:28:42 -04:00
parent 1c2e05e925
commit 4398f1eb94
5 changed files with 103 additions and 14 deletions

View File

@ -30,6 +30,16 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
let secretStorageKeys = {}; let secretStorageKeys = {};
let secretStorageBeingAccessed = false; let secretStorageBeingAccessed = false;
let dehydrationInfo = {};
export function cacheDehydrationKey(key, keyInfo = {}) {
dehydrationInfo = {key, keyInfo};
}
export function getDehydrationKeyCache() {
return dehydrationInfo;
}
function isCachingAllowed() { function isCachingAllowed() {
return secretStorageBeingAccessed; return secretStorageBeingAccessed;
} }
@ -64,6 +74,22 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
return [name, secretStorageKeys[name]]; 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 }) => { const inputToKey = async ({ passphrase, recoveryKey }) => {
if (passphrase) { if (passphrase) {
return deriveKey( return deriveKey(

View File

@ -42,6 +42,7 @@ 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";
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url"; const ID_SERVER_URL_KEY = "mx_is_url";
@ -311,6 +312,25 @@ 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 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}`); console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({ await _doSetLoggedIn({
userId: userId, userId: userId,
@ -320,6 +340,9 @@ async function _restoreFromLocalStorage(opts) {
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
pickleKey: pickleKey, pickleKey: pickleKey,
rehydrationKey: rehydrationKey,
rehydrationKeyInfo: rehydrationKeyInfo,
olmAccount: olmAccount,
}, false); }, false);
return true; return true;
} else { } else {
@ -463,7 +486,13 @@ async function _doSetLoggedIn(credentials, clearStorage) {
if (localStorage) { if (localStorage) {
try { 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 // 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.
@ -528,6 +557,19 @@ 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

@ -20,7 +20,12 @@ limitations under the License.
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './index'; import * as sdk from './index';
import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; import {
AccessCancelledError,
cacheDehydrationKey,
confirmToDismiss,
getDehydrationKeyCache,
} from "./CrossSigningManager";
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
@ -164,9 +169,6 @@ export default class Login {
* @returns {MatrixClientCreds} * @returns {MatrixClientCreds}
*/ */
export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
let rehydrationKeyInfo;
let rehydrationKey;
const client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
idBaseUrl: isUrl, idBaseUrl: isUrl,
@ -190,14 +192,16 @@ 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, rehydrationKeyInfo: dehydrationKeyCache.keyInfo,
rehydrationKey, rehydrationKey: dehydrationKeyCache.key,
olmAccount: data._olm_account, olmAccount: data._olm_account,
}; };
} }
@ -243,5 +247,7 @@ async function getDehydrationKey(keyInfo) {
throw new AccessCancelledError(); throw new AccessCancelledError();
} }
const key = await inputToKey(input); const key = await inputToKey(input);
// need to copy the key because rehydration (unpickling) will clobber it
cacheDehydrationKey(new Uint8Array(key), keyInfo);
return key; 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 { crossSigningCallbacks } from './CrossSigningManager'; import { cacheDehydrationKey, crossSigningCallbacks } from './CrossSigningManager';
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 {
@ -270,33 +270,43 @@ class _MatrixClientPeg implements IMatrixClientPeg {
}; };
if (creds.olmAccount) { if (creds.olmAccount) {
console.log("got a dehydrated account");
opts.deviceToImport = { opts.deviceToImport = {
olmDevice: { olmDevice: {
pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"),
sessions: [], sessions: [],
pickleKey: "DEFAULT_KEY", pickleKey: creds.pickleKey || "DEFAULT_KEY",
}, },
userId: creds.userId, userId: creds.userId,
deviceId: creds.deviceId, deviceId: creds.deviceId,
}; };
creds.olmAccount.free();
} else { } else {
opts.userId = creds.userId; opts.userId = creds.userId;
opts.deviceId = creds.deviceId; 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 // 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);
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) { 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 // we're going to add eventlisteners for each matrix event tile, so the
// potential number of event listeners is quite high. // potential number of event listeners is quite high.
this.matrixClient.setMaxListeners(500); this.matrixClient.setMaxListeners(500);

View File

@ -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); 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) {