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

Add support for dehydrated devices #5239

Merged
merged 12 commits into from
Oct 5, 2020
3 changes: 2 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 Down Expand Up @@ -193,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
90 changes: 67 additions & 23 deletions src/SecurityManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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
Expand All @@ -34,15 +35,9 @@ let secretStorageKeys = {};
let secretStorageKeyInfo = {};
let secretStorageBeingAccessed = false;

let dehydrationInfo = {};
let nonInteractive = false;

export function cacheDehydrationKey(key, keyInfo = {}) {
dehydrationInfo = {key, keyInfo};
}

export function getDehydrationKeyCache() {
return dehydrationInfo;
}
let dehydrationCache = {};

function isCachingAllowed() {
return secretStorageBeingAccessed;
Expand Down Expand Up @@ -103,18 +98,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
return [keyId, secretStorageKeys[keyId]];
}

// if we dehydrated a device, see if that key works for SSSS
if (dehydrationInfo.key) {
try {
const key = dehydrationInfo.key;
if (await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo)) {
// Save to cache to avoid future prompts in the current session
cacheSecretStorageKey(keyId, key, keyInfo);
dehydrationInfo = {};
return [name, key];
}
} catch {}
dehydrationInfo = {};
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);
Expand Down Expand Up @@ -186,8 +178,10 @@ export async function getDehydrationKey(keyInfo, checkFunc) {
throw new AccessCancelledError();
}
const key = await inputToKey(input);

// need to copy the key because rehydration (unpickling) will clobber it
cacheDehydrationKey(key, keyInfo);
dehydrationCache = {key: new Uint8Array(key), keyInfo};

return key;
}

Expand Down Expand Up @@ -334,13 +328,13 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
});

const keyId = Object.keys(secretStorageKeys)[0];
if (keyId) {
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);
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
jryans marked this conversation as resolved.
Show resolved Hide resolved
} else {
console.log("Not setting dehydration key: no SSSS key found");
}
Expand All @@ -358,3 +352,53 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
}
}
}

// FIXME: this function name is a bit of a mouthful
export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
jryans marked this conversation as resolved.
Show resolved Hide resolved
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."),
jryans marked this conversation as resolved.
Show resolved Hide resolved
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