mirror of https://github.com/vector-im/riot-web
commit
f5f0686586
|
@ -186,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) => {
|
||||||
|
@ -312,6 +314,9 @@ async function _restoreFromLocalStorage(opts) {
|
||||||
console.log("No pickle key available");
|
console.log("No pickle key available");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const freshLogin = sessionStorage.getItem("mx_fresh_login");
|
||||||
|
sessionStorage.removeItem("mx_fresh_login");
|
||||||
|
|
||||||
console.log(`Restoring session for ${userId}`);
|
console.log(`Restoring session for ${userId}`);
|
||||||
await _doSetLoggedIn({
|
await _doSetLoggedIn({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
@ -321,6 +326,7 @@ async function _restoreFromLocalStorage(opts) {
|
||||||
identityServerUrl: isUrl,
|
identityServerUrl: isUrl,
|
||||||
guest: isGuest,
|
guest: isGuest,
|
||||||
pickleKey: pickleKey,
|
pickleKey: pickleKey,
|
||||||
|
freshLogin: freshLogin,
|
||||||
}, false);
|
}, false);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -364,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)
|
||||||
|
@ -429,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
|
||||||
|
@ -462,10 +470,28 @@ 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 && 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
|
||||||
|
const newDeviceId = await client.rehydrateDevice();
|
||||||
|
if (newDeviceId) {
|
||||||
|
credentials.deviceId = newDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete credentials.freshLogin;
|
||||||
|
}
|
||||||
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
_persistCredentialsToLocalStorage(credentials);
|
_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
|
// 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.
|
||||||
if (credentials.password) {
|
if (credentials.password) {
|
||||||
|
@ -482,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() {
|
||||||
|
|
|
@ -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 './SecurityManager';
|
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } 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,6 +42,7 @@ export interface IMatrixClientCreds {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
guest: boolean;
|
guest: boolean;
|
||||||
pickleKey?: string;
|
pickleKey?: string;
|
||||||
|
freshLogin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to the js-sdk
|
// TODO: Move this to the js-sdk
|
||||||
|
@ -192,6 +193,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
this.matrixClient.setCryptoTrustCrossSignedDevices(
|
this.matrixClient.setCryptoTrustCrossSignedDevices(
|
||||||
!SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
|
!SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
|
||||||
);
|
);
|
||||||
|
await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient);
|
||||||
StorageManager.setCryptoInitialised(true);
|
StorageManager.setCryptoInitialised(true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||||
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
|
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
|
||||||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
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
|
// 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
|
// only meant to act as a cache to avoid prompting the user multiple times
|
||||||
|
@ -31,8 +32,13 @@ 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 nonInteractive = false;
|
||||||
|
|
||||||
|
let dehydrationCache = {};
|
||||||
|
|
||||||
function isCachingAllowed() {
|
function isCachingAllowed() {
|
||||||
return secretStorageBeingAccessed;
|
return secretStorageBeingAccessed;
|
||||||
}
|
}
|
||||||
|
@ -66,6 +72,20 @@ 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) {
|
||||||
|
@ -78,17 +98,18 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||||
return [keyId, secretStorageKeys[keyId]];
|
return [keyId, secretStorageKeys[keyId]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputToKey = async ({ passphrase, recoveryKey }) => {
|
if (dehydrationCache.key) {
|
||||||
if (passphrase) {
|
if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
|
||||||
return deriveKey(
|
cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo);
|
||||||
passphrase,
|
return [keyId, dehydrationCache.key];
|
||||||
keyInfo.passphrase.salt,
|
|
||||||
keyInfo.passphrase.iterations,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return decodeRecoveryKey(recoveryKey);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (nonInteractive) {
|
||||||
|
throw new Error("Could not unlock non-interactively");
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputToKey = makeInputToKey(keyInfo);
|
||||||
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
||||||
AccessSecretStorageDialog,
|
AccessSecretStorageDialog,
|
||||||
/* props= */
|
/* props= */
|
||||||
|
@ -118,14 +139,56 @@ 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
|
||||||
|
dehydrationCache = {key: new Uint8Array(key), keyInfo};
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheSecretStorageKey(keyId, key, keyInfo) {
|
||||||
if (isCachingAllowed()) {
|
if (isCachingAllowed()) {
|
||||||
secretStorageKeys[keyId] = key;
|
secretStorageKeys[keyId] = key;
|
||||||
|
secretStorageKeyInfo[keyId] = keyInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +239,7 @@ export const crossSigningCallbacks = {
|
||||||
getSecretStorageKey,
|
getSecretStorageKey,
|
||||||
cacheSecretStorageKey,
|
cacheSecretStorageKey,
|
||||||
onSecretRequested,
|
onSecretRequested,
|
||||||
|
getDehydrationKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function promptForBackupPassphrase() {
|
export async function promptForBackupPassphrase() {
|
||||||
|
@ -262,6 +326,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 && 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, "Backup device");
|
||||||
|
} 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
|
||||||
|
@ -272,6 +348,57 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
||||||
secretStorageBeingAccessed = false;
|
secretStorageBeingAccessed = false;
|
||||||
if (!isCachingAllowed()) {
|
if (!isCachingAllowed()) {
|
||||||
secretStorageKeys = {};
|
secretStorageKeys = {};
|
||||||
|
secretStorageKeyInfo = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "Backup device");
|
||||||
|
|
||||||
|
// 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 = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -452,6 +452,7 @@
|
||||||
"Support adding custom themes": "Support adding custom themes",
|
"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 DMs": "Show message previews for reactions in DMs",
|
||||||
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
"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",
|
"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",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
|
|
|
@ -186,6 +186,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_dehydration": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Offline encrypted messaging using dehydrated devices"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"advancedRoomListLogging": {
|
"advancedRoomListLogging": {
|
||||||
// TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
|
// TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
|
||||||
displayName: _td("Enable advanced debugging for the room list"),
|
displayName: _td("Enable advanced debugging for the room list"),
|
||||||
|
|
Loading…
Reference in New Issue