From c34d4e777f8d0e568c15292d0dd24b24979bc664 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 7 Jul 2021 19:37:22 +0100 Subject: [PATCH] Convert SecretStorage to TypeScript --- src/client.ts | 12 +- src/crypto/EncryptionSetup.ts | 4 +- .../{SecretStorage.js => SecretStorage.ts} | 294 ++++++++++-------- src/crypto/api.ts | 14 +- src/crypto/dehydration.ts | 2 +- src/crypto/index.ts | 32 +- src/matrix.ts | 12 +- 7 files changed, 206 insertions(+), 164 deletions(-) rename src/crypto/{SecretStorage.js => SecretStorage.ts} (63%) diff --git a/src/client.ts b/src/client.ts index 637cf9cba..9106e5277 100644 --- a/src/client.ts +++ b/src/client.ts @@ -64,7 +64,8 @@ import { import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import type Request from "request"; import { MatrixScheduler } from "./scheduler"; -import { ICryptoCallbacks, ISecretStorageKeyInfo, NotificationCountType } from "./matrix"; +import { ICryptoCallbacks, NotificationCountType } from "./matrix"; +import { ISecretStorageKeyInfo } from "./crypto/SecretStorage"; import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; @@ -83,7 +84,6 @@ import { IEncryptedEventInfo, IImportRoomKeysOpts, IRecoveryKey, - ISecretStorageKey, } from "./crypto/api"; import { CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./crypto/CrossSigning"; import { Room } from "./models/room"; @@ -1841,7 +1841,9 @@ export class MatrixClient extends EventEmitter { * keyId: {string} the ID of the key * keyInfo: {object} details about the key (iv, mac, passphrase) */ - public addSecretStorageKey(algorithm: string, opts: IAddSecretStorageKeyOpts, keyName?: string): ISecretStorageKey { + public addSecretStorageKey( + algorithm: string, opts: IAddSecretStorageKeyOpts, keyName?: string, + ): Promise<{keyId: string, keyInfo: ISecretStorageKeyInfo}> { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1857,7 +1859,7 @@ export class MatrixClient extends EventEmitter { * for. Defaults to the default key ID if not provided. * @return {boolean} Whether we have the key. */ - public hasSecretStorageKey(keyId?: string): boolean { + public hasSecretStorageKey(keyId?: string): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -1910,7 +1912,7 @@ export class MatrixClient extends EventEmitter { * with, or null if it is not present or not encrypted with a trusted * key */ - public isSecretStored(name: string, checkKey: boolean): Record { + public isSecretStored(name: string, checkKey: boolean): Promise> { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 140c4cadb..ccf8aeb47 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -9,10 +9,10 @@ import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, - ISecretStorageKeyInfo, ISignedKey, KeySignatures, } from "../matrix"; +import { ISecretStorageKeyInfo } from "./SecretStorage"; import { IKeyBackupInfo } from "./keybackup"; interface ICrossSigningKeys { @@ -337,7 +337,7 @@ class SSSSCryptoCallbacks { constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {} public async getSecretStorageKey( - { keys }: { keys: Record }, + { keys }: { keys: Record }, name: string, ): Promise<[string, Uint8Array]> { for (const keyId of Object.keys(keys)) { diff --git a/src/crypto/SecretStorage.js b/src/crypto/SecretStorage.ts similarity index 63% rename from src/crypto/SecretStorage.js rename to src/crypto/SecretStorage.ts index f57c61cb1..3b2fca435 100644 --- a/src/crypto/SecretStorage.js +++ b/src/crypto/SecretStorage.ts @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019 - 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,61 +14,107 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from 'events'; import { logger } from '../logger'; import * as olmlib from './olmlib'; import { randomString } from '../randomstring'; -import { encryptAES, decryptAES } from './aes'; +import { encryptAES, decryptAES, IEncryptedPayload } from './aes'; import { encodeBase64 } from "./olmlib"; +import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix'; +import { IAddSecretStorageKeyOpts, IPassphraseInfo } from './api'; +import { EventEmitter } from 'stream'; -export const SECRET_STORAGE_ALGORITHM_V1_AES - = "m.secret_storage.v1.aes-hmac-sha2"; +export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; +export interface ISecretStorageKeyInfo { + name: string; + algorithm: string; + // technically the below are specific to AES keys. If we ever introduce another type, + // we can split into separate interfaces. + iv: string; + mac: string; + passphrase: IPassphraseInfo; +} + +export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo]; + +export interface ISecretRequest { + requestId: string; + promise: Promise; + cancel: (reason: string) => void; +} + +export interface IAccountDataClient extends EventEmitter { + getAccountDataFromServer: (string) => Promise; + getAccountData: (string) => object; + setAccountData: (string, object) => Promise; +} + +interface ISecretRequestInternal { + name: string; + devices: Array; + resolve: (string) => void; + reject: (Error) => void; +} + +interface IDecryptors { + encrypt: (string) => Promise; + decrypt: (IEncryptedPayload) => Promise; +} + /** * Implements Secure Secret Storage and Sharing (MSC1946) * @module crypto/SecretStorage */ -export class SecretStorage extends EventEmitter { - constructor(baseApis, cryptoCallbacks) { - super(); - this._baseApis = baseApis; - this._cryptoCallbacks = cryptoCallbacks; - this._requests = {}; - this._incomingRequests = {}; +export class SecretStorage { + private accountDataAdapter: IAccountDataClient; + private baseApis: MatrixClient; + private cryptoCallbacks: ICryptoCallbacks; + private requests = new Map(); + + // In it's pure javascript days, this was relying on some proper Javascript-style + // type-abuse where sometimes we'd pass in a fake client object with just the account + // data methods implemented, which is all this class needs unless you use the secret + // sharing code, so it was fine. As a low-touch TypeScript migration, this now has + // an extra, optional param for a real matrix client, so you can not pass it as long + // as you don't request any secrets. + // A better solution would probably be to split this class up into secret storage and + // secret sharing which are really two separate things, even though they share an MSC. + constructor(accountDataClient: IAccountDataClient, cryptoCallbacks: ICryptoCallbacks, matrixClient?: MatrixClient) { + this.accountDataAdapter = accountDataClient; + this.baseApis = matrixClient; + this.cryptoCallbacks = cryptoCallbacks; } - async getDefaultKeyId() { - const defaultKey = await this._baseApis.getAccountDataFromServer( + public async getDefaultKeyId(): Promise { + const defaultKey = await this.accountDataAdapter.getAccountDataFromServer( 'm.secret_storage.default_key', ); if (!defaultKey) return null; return defaultKey.key; } - setDefaultKeyId(keyId) { - return new Promise(async (resolve, reject) => { + public setDefaultKeyId(keyId: string) { + return new Promise((resolve, reject) => { const listener = (ev) => { if ( ev.getType() === 'm.secret_storage.default_key' && ev.getContent().key === keyId ) { - this._baseApis.removeListener('accountData', listener); + this.accountDataAdapter.removeListener('accountData', listener); resolve(); } }; - this._baseApis.on('accountData', listener); + this.accountDataAdapter.on('accountData', listener); - try { - await this._baseApis.setAccountData( - 'm.secret_storage.default_key', - { key: keyId }, - ); - } catch (e) { - this._baseApis.removeListener('accountData', listener); + this.accountDataAdapter.setAccountData( + 'm.secret_storage.default_key', + { key: keyId }, + ).catch(e => { + this.accountDataAdapter.removeListener('accountData', listener); reject(e); - } + }); }); } @@ -85,10 +131,12 @@ export class SecretStorage extends EventEmitter { * keyId: {string} the ID of the key * keyInfo: {object} details about the key (iv, mac, passphrase) */ - async addKey(algorithm, opts, keyId) { - const keyInfo = { algorithm }; + public async addKey( + algorithm: string, opts: IAddSecretStorageKeyOpts, keyId?: string, + ): Promise<{keyId: string, keyInfo: ISecretStorageKeyInfo}> { + const keyInfo = { algorithm } as ISecretStorageKeyInfo; - if (!opts) opts = {}; + if (!opts) opts = {} as IAddSecretStorageKeyOpts; if (opts.name) { keyInfo.name = opts.name; @@ -104,20 +152,20 @@ export class SecretStorage extends EventEmitter { keyInfo.mac = mac; } } else { - throw new Error(`Unknown key algorithm ${opts.algorithm}`); + throw new Error(`Unknown key algorithm ${algorithm}`); } if (!keyId) { do { keyId = randomString(32); } while ( - await this._baseApis.getAccountDataFromServer( + await this.accountDataAdapter.getAccountDataFromServer( `m.secret_storage.key.${keyId}`, ) ); } - await this._baseApis.setAccountData( + await this.accountDataAdapter.setAccountData( `m.secret_storage.key.${keyId}`, keyInfo, ); @@ -134,8 +182,9 @@ export class SecretStorage extends EventEmitter { * for. Defaults to the default key ID if not provided. * @returns {Array?} If the key was found, the return value is an array of * the form [keyId, keyInfo]. Otherwise, null is returned. + * XXX: why is this an array when addKey returns an object? */ - async getKey(keyId) { + public async getKey(keyId: string): Promise { if (!keyId) { keyId = await this.getDefaultKeyId(); } @@ -143,7 +192,7 @@ export class SecretStorage extends EventEmitter { return null; } - const keyInfo = await this._baseApis.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); return keyInfo ? [keyId, keyInfo] : null; @@ -156,8 +205,8 @@ export class SecretStorage extends EventEmitter { * for. Defaults to the default key ID if not provided. * @return {boolean} Whether we have the key. */ - async hasKey(keyId) { - return !!(await this.getKey(keyId)); + public async hasKey(keyId?: string): Promise { + return Boolean(await this.getKey(keyId)); } /** @@ -168,7 +217,7 @@ export class SecretStorage extends EventEmitter { * * @return {boolean} whether or not the key matches */ - async checkKey(key, info) { + public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise { if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (info.mac) { const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv); @@ -182,7 +231,7 @@ export class SecretStorage extends EventEmitter { } } - static async _calculateKeyCheck(key, iv) { + public static async _calculateKeyCheck(key: Uint8Array, iv?: string): Promise { return await encryptAES(ZERO_STR, key, "", iv); } @@ -194,7 +243,7 @@ export class SecretStorage extends EventEmitter { * @param {Array} keys The IDs of the keys to use to encrypt the secret * or null/undefined to use the default key. */ - async store(name, secret, keys) { + public async store(name: string, secret: string, keys?: Array): Promise { const encrypted = {}; if (!keys) { @@ -211,7 +260,7 @@ export class SecretStorage extends EventEmitter { for (const keyId of keys) { // get key information from key storage - const keyInfo = await this._baseApis.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); if (!keyInfo) { @@ -221,7 +270,7 @@ export class SecretStorage extends EventEmitter { // encrypt secret, based on the algorithm if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const keys = { [keyId]: keyInfo }; - const [, encryption] = await this._getSecretStorageKey(keys, name); + const [, encryption] = await this.getSecretStorageKey(keys, name); encrypted[keyId] = await encryption.encrypt(secret); } else { logger.warn("unknown algorithm for secret storage key " + keyId @@ -231,34 +280,7 @@ export class SecretStorage extends EventEmitter { } // save encrypted secret - await this._baseApis.setAccountData(name, { encrypted }); - } - - /** - * Temporary method to fix up existing accounts where secrets - * are incorrectly stored without the 'encrypted' level - * - * @param {string} name The name of the secret - * @param {object} secretInfo The account data object - * @returns {object} The fixed object or null if no fix was performed - */ - async _fixupStoredSecret(name, secretInfo) { - // We assume the secret was only stored passthrough for 1 - // key - this was all the broken code supported. - const keys = Object.keys(secretInfo); - if ( - keys.length === 1 && keys[0] !== 'encrypted' && - secretInfo[keys[0]].passthrough - ) { - const hasKey = await this.hasKey(keys[0]); - if (hasKey) { - logger.log("Fixing up passthrough secret: " + name); - await this.storePassthrough(name, keys[0]); - const newData = await this._baseApis.getAccountDataFromServer(name); - return newData; - } - } - return null; + await this.accountDataAdapter.setAccountData(name, { encrypted }); } /** @@ -268,24 +290,20 @@ export class SecretStorage extends EventEmitter { * * @return {string} the contents of the secret */ - async get(name) { - let secretInfo = await this._baseApis.getAccountDataFromServer(name); + async get(name: string): Promise { + const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo) { return; } if (!secretInfo.encrypted) { - // try to fix it up - secretInfo = await this._fixupStoredSecret(name, secretInfo); - if (!secretInfo || !secretInfo.encrypted) { - throw new Error("Content is not encrypted!"); - } + throw new Error("Content is not encrypted!"); } // get possible keys to decrypt const keys = {}; for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage - const keyInfo = await this._baseApis.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); const encInfo = secretInfo.encrypted[keyId]; @@ -306,7 +324,7 @@ export class SecretStorage extends EventEmitter { let decryption; try { // fetch private key from app - [keyId, decryption] = await this._getSecretStorageKey(keys, name); + [keyId, decryption] = await this.getSecretStorageKey(keys, name); const encInfo = secretInfo.encrypted[keyId]; @@ -331,16 +349,12 @@ export class SecretStorage extends EventEmitter { * with, or null if it is not present or not encrypted with a trusted * key */ - async isStored(name, checkKey) { + async isStored(name: string, checkKey: boolean): Promise> { // check if secret exists - let secretInfo = await this._baseApis.getAccountDataFromServer(name); + const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo) return null; if (!secretInfo.encrypted) { - // try to fix it up - secretInfo = await this._fixupStoredSecret(name, secretInfo); - if (!secretInfo || !secretInfo.encrypted) { - return null; - } + return null; } if (checkKey === undefined) checkKey = true; @@ -350,7 +364,7 @@ export class SecretStorage extends EventEmitter { // filter secret encryption keys with supported algorithm for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage - const keyInfo = await this._baseApis.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); if (!keyInfo) continue; @@ -371,45 +385,48 @@ export class SecretStorage extends EventEmitter { * * @param {string} name the name of the secret to request * @param {string[]} devices the devices to request the secret from - * - * @return {string} the contents of the secret */ - request(name, devices) { - const requestId = this._baseApis.makeTxnId(); + request(name: string, devices: Array): ISecretRequest { + const requestId = this.baseApis.makeTxnId(); - const requestControl = this._requests[requestId] = { + let resolve: (string) => void; + let reject: (Error) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + this.requests.set(requestId, { name, devices, - }; - const promise = new Promise((resolve, reject) => { - requestControl.resolve = resolve; - requestControl.reject = reject; + resolve, + reject, }); + const cancel = (reason) => { // send cancellation event const cancelData = { action: "request_cancellation", - requesting_device_id: this._baseApis.deviceId, + requesting_device_id: this.baseApis.deviceId, request_id: requestId, }; const toDevice = {}; for (const device of devices) { toDevice[device] = cancelData; } - this._baseApis.sendToDevice("m.secret.request", { - [this._baseApis.getUserId()]: toDevice, + this.baseApis.sendToDevice("m.secret.request", { + [this.baseApis.getUserId()]: toDevice, }); // and reject the promise so that anyone waiting on it will be // notified - requestControl.reject(new Error(reason || "Cancelled")); + reject(new Error(reason || "Cancelled")); }; // send request to devices const requestData = { name, action: "request", - requesting_device_id: this._baseApis.deviceId, + requesting_device_id: this.baseApis.deviceId, request_id: requestId, }; const toDevice = {}; @@ -417,21 +434,21 @@ export class SecretStorage extends EventEmitter { toDevice[device] = requestData; } logger.info(`Request secret ${name} from ${devices}, id ${requestId}`); - this._baseApis.sendToDevice("m.secret.request", { - [this._baseApis.getUserId()]: toDevice, + this.baseApis.sendToDevice("m.secret.request", { + [this.baseApis.getUserId()]: toDevice, }); return { - request_id: requestId, + requestId, promise, cancel, }; } - async _onRequestReceived(event) { + public async onRequestReceived(event: MatrixEvent): Promise { const sender = event.getSender(); const content = event.getContent(); - if (sender !== this._baseApis.getUserId() + if (sender !== this.baseApis.getUserId() || !(content.name && content.action && content.requesting_device_id && content.request_id)) { // ignore requests from anyone else, for now @@ -440,34 +457,45 @@ export class SecretStorage extends EventEmitter { const deviceId = content.requesting_device_id; // check if it's a cancel if (content.action === "request_cancellation") { + /* + Looks like we intended to emit events when we got cancelations, but + we never put anything in the _incomingRequests object, and the request + itself doesn't use events anyway so if we were to wire up cancellations, + they probably ought to use the same callback interface. I'm leaving them + disabled for now while converting this file to typescript. if (this._incomingRequests[deviceId] && this._incomingRequests[deviceId][content.request_id]) { - logger.info("received request cancellation for secret (" + sender - + ", " + deviceId + ", " + content.request_id + ")"); - this._baseApis.emit("crypto.secrets.requestCancelled", { + logger.info( + "received request cancellation for secret (" + sender + + ", " + deviceId + ", " + content.request_id + ")", + ); + this.baseApis.emit("crypto.secrets.requestCancelled", { user_id: sender, device_id: deviceId, request_id: content.request_id, }); } + */ } else if (content.action === "request") { - if (deviceId === this._baseApis.deviceId) { + if (deviceId === this.baseApis.deviceId) { // no point in trying to send ourself the secret return; } // check if we have the secret - logger.info("received request for secret (" + sender - + ", " + deviceId + ", " + content.request_id + ")"); - if (!this._cryptoCallbacks.onSecretRequested) { + logger.info( + "received request for secret (" + sender + + ", " + deviceId + ", " + content.request_id + ")", + ); + if (!this.cryptoCallbacks.onSecretRequested) { return; } - const secret = await this._cryptoCallbacks.onSecretRequested( + const secret = await this.cryptoCallbacks.onSecretRequested( sender, deviceId, content.request_id, content.name, - this._baseApis.checkDeviceTrust(sender, deviceId), + this.baseApis.checkDeviceTrust(sender, deviceId), ); if (secret) { logger.info(`Preparing ${content.name} secret for ${deviceId}`); @@ -480,25 +508,25 @@ export class SecretStorage extends EventEmitter { }; const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key, + sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key, ciphertext: {}, }; await olmlib.ensureOlmSessionsForDevices( - this._baseApis.crypto.olmDevice, - this._baseApis, + this.baseApis.crypto.olmDevice, + this.baseApis, { [sender]: [ - this._baseApis.getStoredDevice(sender, deviceId), + this.baseApis.getStoredDevice(sender, deviceId), ], }, ); await olmlib.encryptMessageForDevice( encryptedContent.ciphertext, - this._baseApis.getUserId(), - this._baseApis.deviceId, - this._baseApis.crypto.olmDevice, + this.baseApis.getUserId(), + this.baseApis.deviceId, + this.baseApis.crypto.olmDevice, sender, - this._baseApis.getStoredDevice(sender, deviceId), + this.baseApis.getStoredDevice(sender, deviceId), payload, ); const contentMap = { @@ -508,26 +536,26 @@ export class SecretStorage extends EventEmitter { }; logger.info(`Sending ${content.name} secret for ${deviceId}`); - this._baseApis.sendToDevice("m.room.encrypted", contentMap); + this.baseApis.sendToDevice("m.room.encrypted", contentMap); } else { logger.info(`Request denied for ${content.name} secret for ${deviceId}`); } } } - _onSecretReceived(event) { - if (event.getSender() !== this._baseApis.getUserId()) { + public onSecretReceived(event: MatrixEvent): void { + if (event.getSender() !== this.baseApis.getUserId()) { // we shouldn't be receiving secrets from anyone else, so ignore // because someone could be trying to send us bogus data return; } const content = event.getContent(); logger.log("got secret share for request", content.request_id); - const requestControl = this._requests[content.request_id]; + const requestControl = this.requests.get(content.request_id); if (requestControl) { // make sure that the device that sent it is one of the devices that // we requested from - const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey( + const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey( olmlib.OLM_ALGORITHM, event.getSenderKey(), ); @@ -550,12 +578,14 @@ export class SecretStorage extends EventEmitter { } } - async _getSecretStorageKey(keys, name) { - if (!this._cryptoCallbacks.getSecretStorageKey) { + private async getSecretStorageKey( + keys: Record, name: string, + ): Promise<[string, IDecryptors]> { + if (!this.cryptoCallbacks.getSecretStorageKey) { throw new Error("No getSecretStorageKey callback supplied"); } - const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name); + const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name); if (!returned) { throw new Error("getSecretStorageKey callback returned falsey"); @@ -571,10 +601,10 @@ export class SecretStorage extends EventEmitter { if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const decryption = { - encrypt: async function(secret) { + encrypt: async function(secret: string): Promise { return await encryptAES(secret, privateKey, name); }, - decrypt: async function(encInfo) { + decrypt: async function(encInfo: IEncryptedPayload): Promise { return await decryptAES(encInfo, privateKey, name); }, }; diff --git a/src/crypto/api.ts b/src/crypto/api.ts index 8daafb6d7..a15f8bd5c 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -16,7 +16,7 @@ limitations under the License. import { DeviceInfo } from "./deviceinfo"; import { IKeyBackupInfo } from "./keybackup"; -import { ISecretStorageKeyInfo } from "../matrix"; +import { ISecretStorageKeyInfo } from "./SecretStorage"; // TODO: Merge this with crypto.js once converted @@ -112,9 +112,17 @@ export interface ISecretStorageKey { keyInfo: ISecretStorageKeyInfo; } +export interface IPassphraseInfo { + algorithm: "m.pbkdf2"; + iterations: number; + salt: string; + bits: number; +} + export interface IAddSecretStorageKeyOpts { - // depends on algorithm - // TODO: Types + name: string; + passphrase: IPassphraseInfo; + key: Uint8Array; } export interface IImportOpts { diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 95fb77752..ef441e7d8 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -19,7 +19,7 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store'; import { decryptAES, encryptAES } from './aes'; import anotherjson from "another-json"; import { logger } from '../logger'; -import { ISecretStorageKeyInfo } from "../matrix"; +import { ISecretStorageKeyInfo } from "./SecretStorage"; // FIXME: these types should eventually go in a different file type Signatures = Record>; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 1af4cfcc3..0b717c5b7 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -33,7 +33,14 @@ import { DeviceInfo, IDevice } from "./deviceinfo"; import * as algorithms from "./algorithms"; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { EncryptionSetupBuilder } from "./EncryptionSetup"; -import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage } from './SecretStorage'; +import { + SECRET_STORAGE_ALGORITHM_V1_AES, + SecretStorage, + ISecretStorageKeyInfo, + SecretStorageKeyTuple, + ISecretRequest, +} from './SecretStorage'; +import { IAddSecretStorageKeyOpts } from "./api"; import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode'; @@ -359,7 +366,8 @@ export class Crypto extends EventEmitter { const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice); this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); - this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks); + // Yes, we pass the client twice here: see SecretStorage + this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis); this.dehydrationManager = new DehydrationManager(this); // Assuming no app-supplied callback, default to getting from SSSS. @@ -970,15 +978,17 @@ export class Crypto extends EventEmitter { logger.log("Secure Secret Storage ready"); } - public addSecretStorageKey(algorithm: string, opts: any, keyID: string): any { // TODO types + public addSecretStorageKey( + algorithm: string, opts: IAddSecretStorageKeyOpts, keyID: string, + ): Promise<{keyId: string, keyInfo: ISecretStorageKeyInfo}> { return this.secretStorage.addKey(algorithm, opts, keyID); } - public hasSecretStorageKey(keyID: string): boolean { + public hasSecretStorageKey(keyID: string): Promise { return this.secretStorage.hasKey(keyID); } - public getSecretStorageKey(keyID?: string): any { // TODO types + public getSecretStorageKey(keyID?: string): Promise { return this.secretStorage.getKey(keyID); } @@ -990,11 +1000,13 @@ export class Crypto extends EventEmitter { return this.secretStorage.get(name); } - public isSecretStored(name: string, checkKey?: boolean): any { // TODO types + public isSecretStored( + name: string, checkKey?: boolean, + ): Promise> { return this.secretStorage.isStored(name, checkKey); } - public requestSecret(name: string, devices: string[]): Promise { // TODO types + public requestSecret(name: string, devices: string[]): ISecretRequest { // TODO types if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId)); } @@ -1009,7 +1021,7 @@ export class Crypto extends EventEmitter { return this.secretStorage.setDefaultKeyId(k); } - public checkSecretStorageKey(key: string, info: any): Promise { // TODO types + public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise { return this.secretStorage.checkKey(key, info); } @@ -2996,9 +3008,9 @@ export class Crypto extends EventEmitter { } else if (event.getType() == "m.room_key_request") { this.onRoomKeyRequestEvent(event); } else if (event.getType() === "m.secret.request") { - this.secretStorage._onRequestReceived(event); + this.secretStorage.onRequestReceived(event); } else if (event.getType() === "m.secret.send") { - this.secretStorage._onSecretReceived(event); + this.secretStorage.onSecretReceived(event); } else if (event.getType() === "org.matrix.room_key.withheld") { this.onRoomKeyWithheldEvent(event); } else if (event.getContent().transaction_id) { diff --git a/src/matrix.ts b/src/matrix.ts index 2cce6867a..fbda72fe5 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -20,6 +20,7 @@ import { MatrixScheduler } from "./scheduler"; import { MatrixClient } from "./client"; import { ICreateClientOpts } from "./client"; import { DeviceTrustLevel } from "./crypto/CrossSigning"; +import { ISecretStorageKeyInfo } from "./crypto/SecretStorage"; export * from "./client"; export * from "./http-api"; @@ -122,17 +123,6 @@ export interface ICryptoCallbacks { getBackupKey?: () => Promise; } -// TODO: Move this to `SecretStorage` once converted -export interface ISecretStorageKeyInfo { - passphrase?: { - algorithm: "m.pbkdf2"; - iterations: number; - salt: string; - }; - iv?: string; - mac?: string; -} - /** * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied.