From 2698a12a6fc61e8e9a1d992b7054f116d355651f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 9 Oct 2020 16:59:56 +0100 Subject: [PATCH 1/2] Convert `src/SecurityManager.js` to TypeScript This includes a few small API tweaks as well, only in cases where TS revealed we were doing something confusing or wrong. Part of https://github.com/vector-im/element-web/issues/15350 --- src/@types/global.d.ts | 1 + src/MatrixClientPeg.ts | 4 +- src/Modal.tsx | 2 +- ...{SecurityManager.js => SecurityManager.ts} | 83 ++++++++++++------- 4 files changed, 55 insertions(+), 35 deletions(-) rename src/{SecurityManager.js => SecurityManager.ts} (86%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 91b91de90d..93be0fafc0 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import * as ModernizrStatic from "modernizr"; import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 4651a0afe3..5bb10dfa89 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -17,6 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix'; import {MatrixClient} from 'matrix-js-sdk/src/client'; import {MemoryStore} from 'matrix-js-sdk/src/store/memory'; import * as utils from 'matrix-js-sdk/src/utils'; @@ -249,8 +250,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { } private createClient(creds: IMatrixClientCreds): void { - // TODO: Make these opts typesafe with the js-sdk - const opts = { + const opts: ICreateClientOpts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, diff --git a/src/Modal.tsx b/src/Modal.tsx index 0a36813961..b0f6ef988e 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -132,7 +132,7 @@ export class ModalManager { public createTrackedDialogAsync( analyticsAction: string, analyticsInfo: string, - ...rest: Parameters + ...rest: Parameters ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.createDialogAsync(...rest); diff --git a/src/SecurityManager.js b/src/SecurityManager.ts similarity index 86% rename from src/SecurityManager.js rename to src/SecurityManager.ts index 3272c0f015..2d97ce690b 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.ts @@ -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 = {}; +let secretStorageKeyInfo: Record = {}; 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 { 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, +): ({ passphrase, recoveryKey }: { passphrase: string, recoveryKey: string }) => Promise { 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 }, + 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 { 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 { 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 { 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 { 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 From 6498f3fcef99891939ea57e48a1d1b6c4d7f6004 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 12 Oct 2020 11:11:17 +0100 Subject: [PATCH 2/2] Simplify types --- src/SecurityManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 2d97ce690b..4d277692df 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -79,7 +79,7 @@ async function confirmToDismiss(): Promise { function makeInputToKey( keyInfo: ISecretStorageKeyInfo, -): ({ passphrase, recoveryKey }: { passphrase: string, recoveryKey: string }) => Promise { +): (keyParams: { passphrase: string, recoveryKey: string }) => Promise { return async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey(