diff --git a/src/client.ts b/src/client.ts index eff9a9c3c..5e06cc4e1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -114,7 +114,7 @@ import url from "url"; import { randomString } from "./randomstring"; import { ReadStream } from "fs"; import { WebStorageSessionStore } from "./store/session/webstorage"; -import { BackupManager, IKeyBackupCheck, TrustInfo } from "./crypto/backup"; +import { BackupManager, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace"; export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; @@ -2180,7 +2180,7 @@ export class MatrixClient extends EventEmitter { public async prepareKeyBackupVersion( password: string, opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }, - ): Promise { + ): Promise> { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2198,7 +2198,7 @@ export class MatrixClient extends EventEmitter { algorithm, auth_data, recovery_key, - } as any; // TODO: Types + }; } /** @@ -2313,7 +2313,7 @@ export class MatrixClient extends EventEmitter { * Back up session keys to the homeserver. * @param {string} roomId ID of the room that the keys are for Optional. * @param {string} sessionId ID of the session that the keys are for Optional. - * @param {integer} version backup version Optional. + * @param {number} version backup version Optional. * @param {object} data Object keys to send * @return {Promise} a promise that will resolve when the keys * are uploaded @@ -2388,7 +2388,7 @@ export class MatrixClient extends EventEmitter { * @param {string} recoveryKey The recovery key * @return {Uint8Array} key backup key */ - public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array { + public keyBackupKeyFromRecoveryKey(recoveryKey: string): ArrayLike { return decodeRecoveryKey(recoveryKey); } @@ -2478,9 +2478,7 @@ export class MatrixClient extends EventEmitter { opts: IKeyBackupRestoreOpts, ): Promise { const privKey = decodeRecoveryKey(recoveryKey); - return this.restoreKeyBackup( - privKey, targetRoomId, targetSessionId, backupInfo, opts, - ); + return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } // TODO: Types @@ -2498,7 +2496,7 @@ export class MatrixClient extends EventEmitter { } private async restoreKeyBackup( - privKey: Uint8Array, + privKey: ArrayLike, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupVersion, diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 2d983c2e5..7ce177922 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -296,7 +296,7 @@ export class CrossSigningInfo extends EventEmitter { } const privateKeys: Record = {}; - const keys: Record = {}; + const keys: Record = {}; // TODO types let masterSigning; let masterPub; @@ -719,12 +719,12 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O ); } const pickleKey = Buffer.from(olmDevice._pickleKey); - key = await encryptAES(encodeBase64(key), pickleKey, type); + const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type); return store.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - store.storeSecretStorePrivateKey(txn, type, key); + store.storeSecretStorePrivateKey(txn, type, encryptedKey); }, ); }, diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 1188b9585..3cc465419 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type { BinaryLike } from "crypto"; + import { getCrypto } from '../utils'; import { decodeBase64, encodeBase64 } from './olmlib'; @@ -23,6 +25,12 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? // salt for HKDF, with 8 bytes of zeros const zeroSalt = new Uint8Array(8); +interface IEncryptedPayload { + iv: string; + ciphertext: string; + mac: string; +} + /** * encrypt a string in Node.js * @@ -31,7 +39,7 @@ const zeroSalt = new Uint8Array(8); * @param {string} name the name of the secret * @param {string} ivStr the initialization vector to use */ -async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string) { +async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string): Promise { const crypto = getCrypto(); if (!crypto) { throw new Error("No usable crypto implementation"); @@ -77,7 +85,7 @@ async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: * @param {Uint8Array} key the encryption key to use * @param {string} name the name of the secret */ -async function decryptNode(data: IData, key: Uint8Array, name: string) { +async function decryptNode(data: IEncryptedPayload, key: Uint8Array, name: string): Promise { const crypto = getCrypto(); if (!crypto) { throw new Error("No usable crypto implementation"); @@ -100,10 +108,9 @@ async function decryptNode(data: IData, key: Uint8Array, name: string) { + decipher.final("utf8"); } -function deriveKeysNode(key, name) { +function deriveKeysNode(key: BinaryLike, name: string): [Buffer, Buffer] { const crypto = getCrypto(); - const prk = crypto.createHmac("sha256", zeroSalt) - .update(key).digest(); + const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest(); const b = Buffer.alloc(1, 1); const aesKey = crypto.createHmac("sha256", prk) @@ -123,7 +130,7 @@ function deriveKeysNode(key, name) { * @param {string} name the name of the secret * @param {string} ivStr the initialization vector to use */ -async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string) { +async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string): Promise { let iv; if (ivStr) { iv = decodeBase64(ivStr); @@ -163,12 +170,6 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr }; } -interface IData { - ciphertext: string; - iv: string; - mac: string; -} - /** * decrypt a string in the browser * @@ -179,7 +180,7 @@ interface IData { * @param {Uint8Array} key the encryption key to use * @param {string} name the name of the secret */ -async function decryptBrowser(data: IData, key: Uint8Array, name: string) { +async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: string): Promise { const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); const ciphertext = decodeBase64(data.ciphertext); @@ -206,7 +207,7 @@ async function decryptBrowser(data: IData, key: Uint8Array, name: string) { return new TextDecoder().decode(new Uint8Array(plaintext)); } -async function deriveKeysBrowser(key, name) { +async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> { const hkdfkey = await subtleCrypto.importKey( 'raw', key, @@ -252,11 +253,11 @@ async function deriveKeysBrowser(key, name) { return await Promise.all([aesProm, hmacProm]); } -export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string) { +export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise { return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr); } -export function decryptAES(data: IData, key: Uint8Array, name: string) { +export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise { return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name); } diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index f2b62cc01..3d5485f27 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -29,16 +29,11 @@ import { keyFromPassphrase } from './key_passphrase'; import { sleep } from "../utils"; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { encodeRecoveryKey } from './recoverykey'; +import { IKeyBackupVersion } from "./keybackup"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; -type AuthData = Record; - -type BackupInfo = { - algorithm: string, - auth_data: AuthData, // eslint-disable-line camelcase - [properties: string]: any, -}; +type AuthData = IKeyBackupVersion["auth_data"]; type SigInfo = { deviceId: string, @@ -54,13 +49,22 @@ export type TrustInfo = { }; export interface IKeyBackupCheck { - backupInfo: BackupInfo; + backupInfo: IKeyBackupVersion; trustInfo: TrustInfo; } +/* eslint-disable camelcase */ +export interface IPreparedKeyBackupVersion { + algorithm: string; + auth_data: AuthData; + recovery_key: string; + privateKey: Uint8Array; +} +/* eslint-enable camelcase */ + /** A function used to get the secret key for a backup. */ -type GetKey = () => Promise; +type GetKey = () => Promise>; interface BackupAlgorithmClass { algorithmName: string; @@ -77,7 +81,7 @@ interface BackupAlgorithm { encryptSession(data: Record): Promise; decryptSessions(ciphertexts: Record): Promise[]>; authData: AuthData; - keyMatches(key: Uint8Array): Promise; + keyMatches(key: ArrayLike): Promise; free(): void; } @@ -86,7 +90,7 @@ interface BackupAlgorithm { */ export class BackupManager { private algorithm: BackupAlgorithm | undefined; - public backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version + public backupInfo: IKeyBackupVersion | undefined; // The info dict from /room_keys/version public checkedForBackup: boolean; // Have we checked the server for a backup we can use? private sendingBackups: boolean; // Are we currently sending backups? constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) { @@ -98,7 +102,7 @@ export class BackupManager { return this.backupInfo && this.backupInfo.version; } - public static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise { + public static async makeAlgorithm(info: IKeyBackupVersion, getKey: GetKey): Promise { const Algorithm = algorithmsByName[info.algorithm]; if (!Algorithm) { throw new Error("Unknown backup algorithm"); @@ -106,7 +110,7 @@ export class BackupManager { return await Algorithm.init(info.auth_data, getKey); } - public async enableKeyBackup(info: BackupInfo): Promise { + public async enableKeyBackup(info: IKeyBackupVersion): Promise { this.backupInfo = info; if (this.algorithm) { this.algorithm.free(); @@ -145,7 +149,8 @@ export class BackupManager { public async prepareKeyBackupVersion( key?: string | Uint8Array | null, algorithm?: string | undefined, - ): Promise { + // eslint-disable-next-line camelcase + ): Promise { const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm; if (!Algorithm) { throw new Error("Unknown backup algorithm"); @@ -161,7 +166,7 @@ export class BackupManager { }; } - public async createKeyBackupVersion(info: BackupInfo): Promise { + public async createKeyBackupVersion(info: IKeyBackupVersion): Promise { this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); } @@ -171,14 +176,14 @@ export class BackupManager { * one of the user's verified devices, start backing up * to it. */ - public async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> { + public async checkAndStart(): Promise { logger.log("Checking key backup status..."); if (this.baseApis.isGuest()) { logger.log("Skipping key backup check since user is guest"); this.checkedForBackup = true; return null; } - let backupInfo: BackupInfo; + let backupInfo: IKeyBackupVersion; try { backupInfo = await this.baseApis.getKeyBackupVersion(); } catch (e) { @@ -255,7 +260,7 @@ export class BackupManager { * ] * } */ - public async isKeyBackupTrusted(backupInfo: BackupInfo): Promise { + public async isKeyBackupTrusted(backupInfo: IKeyBackupVersion): Promise { const ret = { usable: false, trusted_locally: false, @@ -569,7 +574,7 @@ export class Curve25519 implements BackupAlgorithm { ): Promise<[Uint8Array, AuthData]> { const decryption = new global.Olm.PkDecryption(); try { - const authData: AuthData = {}; + const authData: Partial = {}; if (!key) { authData.public_key = decryption.generate_key(); } else if (key instanceof Uint8Array) { @@ -585,7 +590,7 @@ export class Curve25519 implements BackupAlgorithm { return [ decryption.get_private_key(), - authData, + authData as AuthData, ]; } finally { decryption.free(); diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 930f5fbb6..5752f9df6 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -138,11 +138,6 @@ interface IDeviceVerificationUpgrade { * could be established */ -interface IOlmSessionResult { - device: DeviceInfo; - sessionId?: string; -} - interface IUserOlmSession { deviceIdKey: string; sessions: { @@ -1079,17 +1074,17 @@ export class Crypto extends EventEmitter { * @param {Uint8Array} key the private key * @returns {Promise} so you can catch failures */ - public async storeSessionBackupPrivateKey(key: Uint8Array): Promise { + public async storeSessionBackupPrivateKey(key: ArrayLike): Promise { if (!(key instanceof Uint8Array)) { throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); } const pickleKey = Buffer.from(this.olmDevice._pickleKey); - key = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); + const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); return this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", key); + this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey); }, ); } @@ -2573,7 +2568,7 @@ export class Crypto extends EventEmitter { * an Object mapping from userId to deviceId to * {@link module:crypto~OlmSessionResult} */ - ensureOlmSessionsForUsers(users: string[]): Promise { + ensureOlmSessionsForUsers(users: string[]): Promise>> { const devicesByUser = {}; for (let i = 0; i < users.length; ++i) { @@ -2598,9 +2593,7 @@ export class Crypto extends EventEmitter { } } - return olmlib.ensureOlmSessionsForDevices( - this.olmDevice, this.baseApis, devicesByUser, - ); + return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser); } /** @@ -3259,9 +3252,7 @@ export class Crypto extends EventEmitter { } const devicesByUser = {}; devicesByUser[sender] = [device]; - await olmlib.ensureOlmSessionsForDevices( - this.olmDevice, this.baseApis, devicesByUser, true, - ); + await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true); this.lastNewSessionForced[sender][deviceKey] = Date.now(); diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index 8376245cd..20ca4f146 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -44,7 +44,6 @@ export interface IKeyBackupVersion { count: number; etag: string; version: string; // number contained within - recovery_key: string; } /* eslint-enable camelcase */ diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts index a61058624..646f330ec 100644 --- a/src/crypto/olmlib.ts +++ b/src/crypto/olmlib.ts @@ -52,6 +52,11 @@ export const MEGOLM_ALGORITHM = Algorithm.Megolm; */ export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup; +export interface IOlmSessionResult { + device: DeviceInfo; + sessionId?: string; +} + /** * Encrypt an event payload for an Olm device * @@ -209,11 +214,11 @@ export async function ensureOlmSessionsForDevices( olmDevice: OlmDevice, baseApis: MatrixClient, devicesByUser: Record, - force: boolean, - otkTimeout: number, - failedServers: string[], - log: Logger, -) { + force = false, + otkTimeout?: number, + failedServers?: string[], + log: Logger = logger, +): Promise>> { if (typeof force === "number") { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - backwards compatibility @@ -224,9 +229,6 @@ export async function ensureOlmSessionsForDevices( otkTimeout = force; force = false; } - if (!log) { - log = logger; - } const devicesWithoutSession = [ // [userId, deviceId], ... @@ -439,9 +441,9 @@ async function _verifyKeyAndStartSession( return sid; } -interface IObject { - unsigned: object; - signatures: object; +export interface IObject { + unsigned?: object; + signatures?: object; } /** @@ -555,7 +557,7 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) { * @param {Uint8Array} uint8Array The data to encode. * @return {string} The base64. */ -export function encodeBase64(uint8Array: ArrayBuffer): string { +export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { return Buffer.from(uint8Array).toString("base64"); } diff --git a/src/crypto/recoverykey.ts b/src/crypto/recoverykey.ts index 94a5ae8e2..5c54e6085 100644 --- a/src/crypto/recoverykey.ts +++ b/src/crypto/recoverykey.ts @@ -35,7 +35,7 @@ export function encodeRecoveryKey(key: ArrayLike): string { return base58key.match(/.{1,4}/g).join(" "); } -export function decodeRecoveryKey(recoveryKey: string): ArrayLike { +export function decodeRecoveryKey(recoveryKey: string): Uint8Array { const result = bs58.decode(recoveryKey.replace(/ /g, '')); let parity = 0;