mirror of https://github.com/vector-im/riot-web
update to latest js-sdk changes
parent
4e2397a79d
commit
744f46417a
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
66
src/Login.js
66
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue