From 2698a12a6fc61e8e9a1d992b7054f116d355651f Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
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<T extends any[]>(
         analyticsAction: string,
         analyticsInfo: string,
-        ...rest: Parameters<ModalManager["appendDialogAsync"]>
+        ...rest: Parameters<ModalManager["createDialogAsync"]>
     ) {
         Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
         return this.createDialogAsync<T>(...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<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,
+): ({ passphrase, recoveryKey }: { 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

From 6498f3fcef99891939ea57e48a1d1b6c4d7f6004 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
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<boolean> {
 
 function makeInputToKey(
     keyInfo: ISecretStorageKeyInfo,
-): ({ passphrase, recoveryKey }: { passphrase: string, recoveryKey: string }) => Promise<Uint8Array> {
+): (keyParams: { passphrase: string, recoveryKey: string }) => Promise<Uint8Array> {
     return async ({ passphrase, recoveryKey }) => {
         if (passphrase) {
             return deriveKey(