Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #5239 from uhoreg/dehydration
Browse files Browse the repository at this point in the history
Add support for dehydrated devices
  • Loading branch information
uhoreg authored Oct 5, 2020
2 parents fdb8cb7 + 49cc62c commit f5f0686
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 16 deletions.
30 changes: 27 additions & 3 deletions src/Lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,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) => {
Expand Down Expand Up @@ -312,6 +314,9 @@ async function _restoreFromLocalStorage(opts) {
console.log("No pickle key available");
}

const freshLogin = sessionStorage.getItem("mx_fresh_login");
sessionStorage.removeItem("mx_fresh_login");

console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({
userId: userId,
Expand All @@ -321,6 +326,7 @@ async function _restoreFromLocalStorage(opts) {
identityServerUrl: isUrl,
guest: isGuest,
pickleKey: pickleKey,
freshLogin: freshLogin,
}, false);
return true;
} else {
Expand Down Expand Up @@ -364,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)
Expand Down Expand Up @@ -429,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
Expand Down Expand Up @@ -462,10 +470,28 @@ async function _doSetLoggedIn(credentials, clearStorage) {

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) {
try {
_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.
if (credentials.password) {
Expand All @@ -482,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() {
Expand Down
4 changes: 3 additions & 1 deletion src/MatrixClientPeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { crossSigningCallbacks } from './SecurityManager';
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";

export interface IMatrixClientCreds {
Expand All @@ -42,6 +42,7 @@ export interface IMatrixClientCreds {
accessToken: string;
guest: boolean;
pickleKey?: string;
freshLogin?: boolean;
}

// TODO: Move this to the js-sdk
Expand Down Expand Up @@ -192,6 +193,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
this.matrixClient.setCryptoTrustCrossSignedDevices(
!SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
);
await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient);
StorageManager.setCryptoInitialised(true);
}
} catch (e) {
Expand Down
151 changes: 139 additions & 12 deletions src/SecurityManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
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
// only meant to act as a cache to avoid prompting the user multiple times
// during the same single operation. Use `accessSecretStorage` below to scope a
// single secret storage operation, as it will clear the cached keys once the
// operation ends.
let secretStorageKeys = {};
let secretStorageKeyInfo = {};
let secretStorageBeingAccessed = false;

let nonInteractive = false;

let dehydrationCache = {};

function isCachingAllowed() {
return secretStorageBeingAccessed;
}
Expand Down Expand Up @@ -66,6 +72,20 @@ 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) {
Expand All @@ -78,17 +98,18 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
return [keyId, secretStorageKeys[keyId]];
}

const inputToKey = async ({ passphrase, recoveryKey }) => {
if (passphrase) {
return deriveKey(
passphrase,
keyInfo.passphrase.salt,
keyInfo.passphrase.iterations,
);
} else {
return decodeRecoveryKey(recoveryKey);
if (dehydrationCache.key) {
if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo);
return [keyId, dehydrationCache.key];
}
};
}

if (nonInteractive) {
throw new Error("Could not unlock non-interactively");
}

const inputToKey = makeInputToKey(keyInfo);
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
/* props= */
Expand Down Expand Up @@ -118,14 +139,56 @@ 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
dehydrationCache = {key: new Uint8Array(key), keyInfo};

return key;
}

function cacheSecretStorageKey(keyId, key, keyInfo) {
if (isCachingAllowed()) {
secretStorageKeys[keyId] = key;
secretStorageKeyInfo[keyId] = keyInfo;
}
}

Expand Down Expand Up @@ -176,6 +239,7 @@ export const crossSigningCallbacks = {
getSecretStorageKey,
cacheSecretStorageKey,
onSecretRequested,
getDehydrationKey,
};

export async function promptForBackupPassphrase() {
Expand Down Expand Up @@ -262,6 +326,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
await cli.bootstrapSecretStorage({
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
Expand All @@ -272,6 +348,57 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
secretStorageBeingAccessed = false;
if (!isCachingAllowed()) {
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 = {};
}
}
}
}
}
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@
"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 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",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"Font size": "Font size",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_dehydration": {
isFeature: true,
displayName: _td("Offline encrypted messaging using dehydrated devices"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"advancedRoomListLogging": {
// TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
displayName: _td("Enable advanced debugging for the room list"),
Expand Down

0 comments on commit f5f0686

Please sign in to comment.