|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
|
|
|
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
|
|
|
|
import Modal from './Modal';
|
|
|
|
|
import * as sdk from './index';
|
|
|
|
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
|
|
|
@ -31,15 +33,18 @@ import SettingsStore from "./settings/SettingsStore";
|
|
|
|
|
// 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 secretStorageKeys: Record<string, Uint8Array> = {};
|
|
|
|
|
let secretStorageKeyInfo: Record<string, ISecretStorageKeyInfo> = {};
|
|
|
|
|
let secretStorageBeingAccessed = false;
|
|
|
|
|
|
|
|
|
|
let nonInteractive = false;
|
|
|
|
|
|
|
|
|
|
let dehydrationCache = {};
|
|
|
|
|
let dehydrationCache: {
|
|
|
|
|
key?: Uint8Array,
|
|
|
|
|
keyInfo?: ISecretStorageKeyInfo,
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
function isCachingAllowed() {
|
|
|
|
|
function isCachingAllowed(): boolean {
|
|
|
|
|
return secretStorageBeingAccessed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -50,7 +55,7 @@ function isCachingAllowed() {
|
|
|
|
|
*
|
|
|
|
|
* @returns {bool}
|
|
|
|
|
*/
|
|
|
|
|
export function isSecretStorageBeingAccessed() {
|
|
|
|
|
export function isSecretStorageBeingAccessed(): boolean {
|
|
|
|
|
return secretStorageBeingAccessed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -60,7 +65,7 @@ export class AccessCancelledError extends Error {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmToDismiss() {
|
|
|
|
|
async function confirmToDismiss(): Promise<boolean> {
|
|
|
|
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
|
|
|
const [sure] = await Modal.createDialog(QuestionDialog, {
|
|
|
|
|
title: _t("Cancel entering passphrase?"),
|
|
|
|
@ -72,7 +77,9 @@ async function confirmToDismiss() {
|
|
|
|
|
return !sure;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeInputToKey(keyInfo) {
|
|
|
|
|
function makeInputToKey(
|
|
|
|
|
keyInfo: ISecretStorageKeyInfo,
|
|
|
|
|
): (keyParams: { passphrase: string, recoveryKey: string }) => Promise<Uint8Array> {
|
|
|
|
|
return async ({ passphrase, recoveryKey }) => {
|
|
|
|
|
if (passphrase) {
|
|
|
|
|
return deriveKey(
|
|
|
|
@ -86,7 +93,10 @@ function makeInputToKey(keyInfo) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
|
|
|
|
async function getSecretStorageKey(
|
|
|
|
|
{ keys: keyInfos }: { keys: Record<string, ISecretStorageKeyInfo> },
|
|
|
|
|
ssssItemName,
|
|
|
|
|
): Promise<[string, Uint8Array]> {
|
|
|
|
|
const keyInfoEntries = Object.entries(keyInfos);
|
|
|
|
|
if (keyInfoEntries.length > 1) {
|
|
|
|
|
throw new Error("Multiple storage key requests not implemented");
|
|
|
|
@ -100,7 +110,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
|
|
|
|
|
|
|
|
|
if (dehydrationCache.key) {
|
|
|
|
|
if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
|
|
|
|
|
cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo);
|
|
|
|
|
cacheSecretStorageKey(keyId, keyInfo, dehydrationCache.key);
|
|
|
|
|
return [keyId, dehydrationCache.key];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -139,12 +149,15 @@ 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, keyInfo);
|
|
|
|
|
cacheSecretStorageKey(keyId, keyInfo, key);
|
|
|
|
|
|
|
|
|
|
return [keyId, key];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getDehydrationKey(keyInfo, checkFunc) {
|
|
|
|
|
export async function getDehydrationKey(
|
|
|
|
|
keyInfo: ISecretStorageKeyInfo,
|
|
|
|
|
checkFunc: (Uint8Array) => void,
|
|
|
|
|
): Promise<Uint8Array> {
|
|
|
|
|
const inputToKey = makeInputToKey(keyInfo);
|
|
|
|
|
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
|
|
|
|
AccessSecretStorageDialog,
|
|
|
|
@ -185,20 +198,24 @@ export async function getDehydrationKey(keyInfo, checkFunc) {
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cacheSecretStorageKey(keyId, key, keyInfo) {
|
|
|
|
|
function cacheSecretStorageKey(
|
|
|
|
|
keyId: string,
|
|
|
|
|
keyInfo: ISecretStorageKeyInfo,
|
|
|
|
|
key: Uint8Array,
|
|
|
|
|
): void {
|
|
|
|
|
if (isCachingAllowed()) {
|
|
|
|
|
secretStorageKeys[keyId] = key;
|
|
|
|
|
secretStorageKeyInfo[keyId] = keyInfo;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onSecretRequested = async function({
|
|
|
|
|
user_id: userId,
|
|
|
|
|
device_id: deviceId,
|
|
|
|
|
request_id: requestId,
|
|
|
|
|
name,
|
|
|
|
|
device_trust: deviceTrust,
|
|
|
|
|
}) {
|
|
|
|
|
async function onSecretRequested(
|
|
|
|
|
userId: string,
|
|
|
|
|
deviceId: string,
|
|
|
|
|
requestId: string,
|
|
|
|
|
name: string,
|
|
|
|
|
deviceTrust: IDeviceTrustLevel,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
|
if (userId !== client.getUserId()) {
|
|
|
|
@ -233,16 +250,16 @@ const onSecretRequested = async function({
|
|
|
|
|
return key && encodeBase64(key);
|
|
|
|
|
}
|
|
|
|
|
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const crossSigningCallbacks = {
|
|
|
|
|
export const crossSigningCallbacks: ICryptoCallbacks = {
|
|
|
|
|
getSecretStorageKey,
|
|
|
|
|
cacheSecretStorageKey,
|
|
|
|
|
onSecretRequested,
|
|
|
|
|
getDehydrationKey,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export async function promptForBackupPassphrase() {
|
|
|
|
|
export async function promptForBackupPassphrase(): Promise<Uint8Array> {
|
|
|
|
|
let key;
|
|
|
|
|
|
|
|
|
|
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
|
|
|
@ -292,7 +309,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|
|
|
|
/* priority = */ false,
|
|
|
|
|
/* static = */ true,
|
|
|
|
|
/* options = */ {
|
|
|
|
|
onBeforeClose(reason) {
|
|
|
|
|
onBeforeClose: async (reason) => {
|
|
|
|
|
// If Secure Backup is required, you cannot leave the modal.
|
|
|
|
|
if (reason === "backgroundClick") {
|
|
|
|
|
return !isSecureBackupRequired();
|
|
|
|
@ -329,10 +346,10 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|
|
|
|
|
|
|
|
|
const keyId = Object.keys(secretStorageKeys)[0];
|
|
|
|
|
if (keyId && SettingsStore.getValue("feature_dehydration")) {
|
|
|
|
|
const dehydrationKeyInfo =
|
|
|
|
|
secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase
|
|
|
|
|
? {passphrase: secretStorageKeyInfo[keyId].passphrase}
|
|
|
|
|
: {};
|
|
|
|
|
let dehydrationKeyInfo = {};
|
|
|
|
|
if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) {
|
|
|
|
|
dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase };
|
|
|
|
|
}
|
|
|
|
|
console.log("Setting dehydration key");
|
|
|
|
|
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
|
|
|
|
|
} else {
|
|
|
|
@ -354,7 +371,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME: this function name is a bit of a mouthful
|
|
|
|
|
export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
|
|
|
|
|
export async function tryToUnlockSecretStorageWithDehydrationKey(
|
|
|
|
|
client: MatrixClient,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const key = dehydrationCache.key;
|
|
|
|
|
let restoringBackup = false;
|
|
|
|
|
if (key && await client.isSecretStorageReady()) {
|
|
|
|
@ -366,10 +385,10 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
|
|
|
|
|
|
|
|
|
|
// 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}
|
|
|
|
|
: {};
|
|
|
|
|
let dehydrationKeyInfo = {};
|
|
|
|
|
if (dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase) {
|
|
|
|
|
dehydrationKeyInfo = { passphrase: dehydrationCache.keyInfo.passphrase };
|
|
|
|
|
}
|
|
|
|
|
await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device");
|
|
|
|
|
|
|
|
|
|
// and restore from backup
|