diff --git a/spec/unit/crypto/outgoing-room-key-requests.spec.js b/spec/unit/crypto/outgoing-room-key-requests.spec.js index a1fa62a70..24b9325b4 100644 --- a/spec/unit/crypto/outgoing-room-key-requests.spec.js +++ b/spec/unit/crypto/outgoing-room-key-requests.spec.js @@ -21,25 +21,23 @@ import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store import 'fake-indexeddb/auto'; import 'jest-localstorage-mock'; -import { - ROOM_KEY_REQUEST_STATES, -} from '../../../src/crypto/OutgoingRoomKeyRequestManager'; +import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager'; const requests = [ { requestId: "A", requestBody: { session_id: "A", room_id: "A" }, - state: ROOM_KEY_REQUEST_STATES.SENT, + state: RoomKeyRequestState.Sent, }, { requestId: "B", requestBody: { session_id: "B", room_id: "B" }, - state: ROOM_KEY_REQUEST_STATES.SENT, + state: RoomKeyRequestState.Sent, }, { requestId: "C", requestBody: { session_id: "C", room_id: "C" }, - state: ROOM_KEY_REQUEST_STATES.UNSENT, + state: RoomKeyRequestState.Unsent, }, ]; @@ -68,9 +66,9 @@ describe.each([ it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state", async () => { const r = await - store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT); + store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); expect(r).toHaveLength(2); - requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => { + requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => { expect(r).toContainEqual(e); }); }); @@ -78,10 +76,10 @@ describe.each([ test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state", async () => { const r = - await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]); + await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]); expect(r).not.toBeNull(); expect(r).not.toBeUndefined(); - expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT); + expect(r.state).toEqual(RoomKeyRequestState.Sent); expect(requests).toContainEqual(r); }); }); diff --git a/src/client.ts b/src/client.ts index 5e06cc4e1..32874e2e1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -59,7 +59,7 @@ import { IKeyBackupPrepareOpts, IKeyBackupRestoreOpts, IKeyBackupRestoreResult, - IKeyBackupVersion, + IKeyBackupInfo, } from "./crypto/keybackup"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import type Request from "request"; @@ -116,6 +116,7 @@ import { ReadStream } from "fs"; import { WebStorageSessionStore } from "./store/session/webstorage"; import { BackupManager, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace"; +import { ISignatures } from "./@types/signed"; export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend; export type SessionStore = WebStorageSessionStore; @@ -375,6 +376,39 @@ interface ICapabilities { "m.room_versions"?: IRoomVersionsCapability; } +/* eslint-disable camelcase */ +export interface ICrossSigningKey { + keys: { [algorithm: string]: string }; + signatures?: ISignatures; + usage: string[]; + user_id: string; +} + +enum CrossSigningKeyType { + MasterKey = "master_key", + SelfSigningKey = "self_signing_key", + UserSigningKey = "user_signing_key", +} + +export type CrossSigningKeys = Record; + +export interface ISignedKey { + keys: Record; + signatures: ISignatures; + user_id: string; + algorithms: string[]; + device_id: string; +} +/* eslint-enable camelcase */ + +export type KeySignatures = Record>; +interface IUploadKeySignaturesResponse { + failures: Record>; +} + /** * Represents a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used @@ -2086,7 +2120,7 @@ export class MatrixClient extends EventEmitter { * Get information about the current key backup. * @returns {Promise} Information object from API or null */ - public getKeyBackupVersion(): Promise { + public getKeyBackupVersion(): Promise { return this.http.authedRequest( undefined, "GET", "/room_keys/version", undefined, undefined, { prefix: PREFIX_UNSTABLE }, @@ -2120,7 +2154,7 @@ export class MatrixClient extends EventEmitter { * ] * } */ - public isKeyBackupTrusted(info: IKeyBackupVersion): Promise { + public isKeyBackupTrusted(info: IKeyBackupInfo): Promise { return this.crypto.backupManager.isKeyBackupTrusted(info); } @@ -2143,7 +2177,7 @@ export class MatrixClient extends EventEmitter { * @param {object} info Backup information object as returned by getKeyBackupVersion * @returns {Promise} Resolves when complete. */ - public enableKeyBackup(info: IKeyBackupVersion): Promise { + public enableKeyBackup(info: IKeyBackupInfo): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2219,7 +2253,7 @@ export class MatrixClient extends EventEmitter { * @returns {Promise} Object with 'version' param indicating the version created */ // TODO: Fix types - public async createKeyBackupVersion(info: IKeyBackupVersion): Promise { + public async createKeyBackupVersion(info: IKeyBackupInfo): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2375,7 +2409,7 @@ export class MatrixClient extends EventEmitter { * @param {object} backupInfo Backup metadata from `checkKeyBackup` * @return {Promise} key backup key */ - public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupVersion): Promise { + public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise { return keyFromAuthData(backupInfo.auth_data, password); } @@ -2388,7 +2422,7 @@ export class MatrixClient extends EventEmitter { * @param {string} recoveryKey The recovery key * @return {Uint8Array} key backup key */ - public keyBackupKeyFromRecoveryKey(recoveryKey: string): ArrayLike { + public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array { return decodeRecoveryKey(recoveryKey); } @@ -2410,7 +2444,7 @@ export class MatrixClient extends EventEmitter { password: string, targetRoomId: string, targetSessionId: string, - backupInfo: IKeyBackupVersion, + backupInfo: IKeyBackupInfo, opts: IKeyBackupRestoreOpts, ): Promise { const privKey = await keyFromAuthData(backupInfo.auth_data, password); @@ -2434,7 +2468,7 @@ export class MatrixClient extends EventEmitter { */ // TODO: Types public async restoreKeyBackupWithSecretStorage( - backupInfo: IKeyBackupVersion, + backupInfo: IKeyBackupInfo, targetRoomId?: string, targetSessionId?: string, opts?: IKeyBackupRestoreOpts, @@ -2474,7 +2508,7 @@ export class MatrixClient extends EventEmitter { recoveryKey: string, targetRoomId: string, targetSessionId: string, - backupInfo: IKeyBackupVersion, + backupInfo: IKeyBackupInfo, opts: IKeyBackupRestoreOpts, ): Promise { const privKey = decodeRecoveryKey(recoveryKey); @@ -2485,7 +2519,7 @@ export class MatrixClient extends EventEmitter { public async restoreKeyBackupWithCache( targetRoomId: string, targetSessionId: string, - backupInfo: IKeyBackupVersion, + backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise { const privKey = await this.crypto.getSessionBackupPrivateKey(); @@ -2499,7 +2533,7 @@ export class MatrixClient extends EventEmitter { privKey: ArrayLike, targetRoomId: string, targetSessionId: string, - backupInfo: IKeyBackupVersion, + backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise { const cacheCompleteCallback = opts?.cacheCompleteCallback; @@ -7090,7 +7124,7 @@ export class MatrixClient extends EventEmitter { return this.http.authedRequest(callback, "POST", "/keys/upload", undefined, content); } - public uploadKeySignatures(content: any): Promise { // TODO: Types + public uploadKeySignatures(content: KeySignatures): Promise { return this.http.authedRequest( undefined, "POST", '/keys/signatures/upload', undefined, content, { @@ -7189,7 +7223,7 @@ export class MatrixClient extends EventEmitter { return this.http.authedRequest(undefined, "GET", path, qps, undefined); } - public uploadDeviceSigningKeys(auth: any, keys: any): Promise { // TODO: Lots of types + public uploadDeviceSigningKeys(auth: any, keys: CrossSigningKeys): Promise<{}> { // TODO: types const data = Object.assign({}, keys); if (auth) Object.assign(data, { auth }); return this.http.authedRequest( @@ -7601,7 +7635,11 @@ export class MatrixClient extends EventEmitter { * supplied. * @return {Promise} Resolves to the result object */ - public sendToDevice(eventType: string, contentMap: any, txnId?: string): Promise { // TODO: Types + public sendToDevice( + eventType: string, + contentMap: { [userId: string]: { [deviceId: string]: Record; } }, + txnId?: string, + ): Promise<{}> { const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", { $eventType: eventType, $txnId: txnId ? txnId : this.makeTxnId(), diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 7ce177922..720a3f5bd 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -28,26 +28,27 @@ import { decryptAES, encryptAES } from './aes'; import { PkSigning } from "@matrix-org/olm"; import { DeviceInfo } from "./deviceinfo"; import { SecretStorage } from "./SecretStorage"; -import { CryptoStore, MatrixClient } from "../client"; +import { CryptoStore, ICrossSigningKey, ISignedKey, MatrixClient } from "../client"; import { OlmDevice } from "./OlmDevice"; import { ICryptoCallbacks } from "../matrix"; +import { ISignatures } from "../@types/signed"; const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; -function publicKeyFromKeyInfo(keyInfo: any): any { // TODO types +function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string { // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } // We assume only a single key, and we want the bare form without type // prefix, so we select the values. return Object.values(keyInfo.keys)[0]; } -interface ICacheCallbacks { +export interface ICacheCallbacks { getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise; storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise; } export class CrossSigningInfo extends EventEmitter { - public keys: Record = {}; // TODO types + public keys: Record = {}; public firstUse = true; // This tracks whether we've ever verified this user with any identity. // When you verify a user, any devices online at the time that receive @@ -368,8 +369,8 @@ export class CrossSigningInfo extends EventEmitter { this.keys = {}; } - public setKeys(keys: Record): void { - const signingKeys: Record = {}; + public setKeys(keys: Record): void { + const signingKeys: Record = {}; if (keys.master) { if (keys.master.user_id !== this.userId) { const error = "Mismatched user ID " + keys.master.user_id + @@ -448,7 +449,7 @@ export class CrossSigningInfo extends EventEmitter { } } - public async signObject(data: T, type: string): Promise { + public async signObject(data: T, type: string): Promise { if (!this.keys[type]) { throw new Error( "Attempted to sign with " + type + " key but no such key present", @@ -457,13 +458,13 @@ export class CrossSigningInfo extends EventEmitter { const [pubkey, signing] = await this.getCrossSigningKey(type); try { pkSign(data, signing, this.userId, pubkey); - return data; + return data as T & { signatures: ISignatures }; } finally { signing.free(); } } - public async signUser(key: CrossSigningInfo): Promise { + public async signUser(key: CrossSigningInfo): Promise { if (!this.keys.user_signing) { logger.info("No user signing key: not signing user"); return; @@ -471,7 +472,7 @@ export class CrossSigningInfo extends EventEmitter { return this.signObject(key.keys.master, "user_signing"); } - public async signDevice(userId: string, device: DeviceInfo): Promise { + public async signDevice(userId: string, device: DeviceInfo): Promise { if (userId !== this.userId) { throw new Error( `Trying to sign ${userId}'s device; can only sign our own device`, @@ -481,7 +482,7 @@ export class CrossSigningInfo extends EventEmitter { logger.info("No self signing key: not signing device"); return; } - return this.signObject( + return this.signObject>( { algorithms: device.algorithms, keys: device.keys, diff --git a/src/crypto/EncryptionSetup.js b/src/crypto/EncryptionSetup.ts similarity index 61% rename from src/crypto/EncryptionSetup.js rename to src/crypto/EncryptionSetup.ts index 1a7fcf36a..2afcc15de 100644 --- a/src/crypto/EncryptionSetup.js +++ b/src/crypto/EncryptionSetup.ts @@ -1,11 +1,24 @@ import { logger } from "../logger"; import { MatrixEvent } from "../models/event"; import { EventEmitter } from "events"; -import { createCryptoStoreCacheCallbacks } from "./CrossSigning"; +import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning"; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; +import { PREFIX_UNSTABLE } from "../http-api"; +import { Crypto } from "./index"; import { - PREFIX_UNSTABLE, -} from "../http-api"; + CrossSigningKeys, + ICrossSigningKey, + ICryptoCallbacks, + ISecretStorageKeyInfo, + ISignedKey, + KeySignatures, +} from "../matrix"; +import { IKeyBackupInfo } from "./keybackup"; + +interface ICrossSigningKeys { + authUpload(authData: any): Promise<{}>; + keys: Record; +} /** * Builds an EncryptionSetupOperation by calling any of the add.. methods. @@ -17,18 +30,23 @@ import { * more than once. */ export class EncryptionSetupBuilder { + public readonly accountDataClientAdapter: AccountDataClientAdapter; + public readonly crossSigningCallbacks: CrossSigningCallbacks; + public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks; + + private crossSigningKeys: ICrossSigningKeys = null; + private keySignatures: KeySignatures = null; + private keyBackupInfo: IKeyBackupInfo = null; + private sessionBackupPrivateKey: Uint8Array; + /** * @param {Object.} accountData pre-existing account data, will only be read, not written. * @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet */ - constructor(accountData, delegateCryptoCallbacks) { + constructor(accountData: Record, delegateCryptoCallbacks: ICryptoCallbacks) { this.accountDataClientAdapter = new AccountDataClientAdapter(accountData); this.crossSigningCallbacks = new CrossSigningCallbacks(); this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks); - - this._crossSigningKeys = null; - this._keySignatures = null; - this._keyBackupInfo = null; } /** @@ -42,8 +60,8 @@ export class EncryptionSetupBuilder { * an empty authDict, to obtain the flows. * @param {Object} keys the new keys */ - addCrossSigningKeys(authUpload, keys) { - this._crossSigningKeys = { authUpload, keys }; + public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void { + this.crossSigningKeys = { authUpload, keys }; } /** @@ -54,8 +72,8 @@ export class EncryptionSetupBuilder { * * @param {Object} keyBackupInfo as received from/sent to the server */ - addSessionBackup(keyBackupInfo) { - this._keyBackupInfo = keyBackupInfo; + public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void { + this.keyBackupInfo = keyBackupInfo; } /** @@ -65,8 +83,8 @@ export class EncryptionSetupBuilder { * * @param {Uint8Array} privateKey */ - addSessionBackupPrivateKeyToCache(privateKey) { - this._sessionBackupPrivateKey = privateKey; + public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void { + this.sessionBackupPrivateKey = privateKey; } /** @@ -75,14 +93,14 @@ export class EncryptionSetupBuilder { * * @param {String} userId * @param {String} deviceId - * @param {String} signature + * @param {Object} signature */ - addKeySignature(userId, deviceId, signature) { - if (!this._keySignatures) { - this._keySignatures = {}; + public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void { + if (!this.keySignatures) { + this.keySignatures = {}; } - const userSignatures = this._keySignatures[userId] || {}; - this._keySignatures[userId] = userSignatures; + const userSignatures = this.keySignatures[userId] || {}; + this.keySignatures[userId] = userSignatures; userSignatures[deviceId] = signature; } @@ -91,7 +109,7 @@ export class EncryptionSetupBuilder { * @param {Object} content * @return {Promise} */ - setAccountData(type, content) { + public setAccountData(type: string, content: object): Promise { return this.accountDataClientAdapter.setAccountData(type, content); } @@ -99,13 +117,13 @@ export class EncryptionSetupBuilder { * builds the operation containing all the parts that have been added to the builder * @return {EncryptionSetupOperation} */ - buildOperation() { - const accountData = this.accountDataClientAdapter._values; + public buildOperation(): EncryptionSetupOperation { + const accountData = this.accountDataClientAdapter.values; return new EncryptionSetupOperation( accountData, - this._crossSigningKeys, - this._keyBackupInfo, - this._keySignatures, + this.crossSigningKeys, + this.keyBackupInfo, + this.keySignatures, ); } @@ -118,9 +136,9 @@ export class EncryptionSetupBuilder { * @param {Crypto} crypto * @return {Promise} */ - async persist(crypto) { + public async persist(crypto: Crypto): Promise { // store private keys in cache - if (this._crossSigningKeys) { + if (this.crossSigningKeys) { const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice); for (const type of ["master", "self_signing", "user_signing"]) { logger.log(`Cache ${type} cross-signing private key locally`); @@ -132,13 +150,13 @@ export class EncryptionSetupBuilder { 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { crypto.cryptoStore.storeCrossSigningKeys( - txn, this._crossSigningKeys.keys); + txn, this.crossSigningKeys.keys); }, ); } // store session backup key in cache - if (this._sessionBackupPrivateKey) { - await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey); + if (this.sessionBackupPrivateKey) { + await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey); } } } @@ -156,58 +174,58 @@ export class EncryptionSetupOperation { * @param {Object} keyBackupInfo * @param {Object} keySignatures */ - constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) { - this._accountData = accountData; - this._crossSigningKeys = crossSigningKeys; - this._keyBackupInfo = keyBackupInfo; - this._keySignatures = keySignatures; - } + constructor( + private readonly accountData: Map, + private readonly crossSigningKeys: ICrossSigningKeys, + private readonly keyBackupInfo: IKeyBackupInfo, + private readonly keySignatures: KeySignatures, + ) {} /** * Runs the (remaining part of, in the future) operation by sending requests to the server. - * @param {Crypto} crypto + * @param {Crypto} crypto */ - async apply(crypto) { + public async apply(crypto: Crypto): Promise { const baseApis = crypto.baseApis; // upload cross-signing keys - if (this._crossSigningKeys) { - const keys = {}; - for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) { + if (this.crossSigningKeys) { + const keys: Partial = {}; + for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) { keys[name + "_key"] = key; } // We must only call `uploadDeviceSigningKeys` from inside this auth // helper to ensure we properly handle auth errors. - await this._crossSigningKeys.authUpload(authDict => { - return baseApis.uploadDeviceSigningKeys(authDict, keys); + await this.crossSigningKeys.authUpload(authDict => { + return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys); }); // pass the new keys to the main instance of our own CrossSigningInfo. - crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys); + crypto.crossSigningInfo.setKeys(this.crossSigningKeys.keys); } // set account data - if (this._accountData) { - for (const [type, content] of this._accountData) { + if (this.accountData) { + for (const [type, content] of this.accountData) { await baseApis.setAccountData(type, content); } } // upload first cross-signing signatures with the new key // (e.g. signing our own device) - if (this._keySignatures) { - await baseApis.uploadKeySignatures(this._keySignatures); + if (this.keySignatures) { + await baseApis.uploadKeySignatures(this.keySignatures); } // need to create/update key backup info - if (this._keyBackupInfo) { - if (this._keyBackupInfo.version) { + if (this.keyBackupInfo) { + if (this.keyBackupInfo.version) { // session backup signature // The backup is trusted because the user provided the private key. // Sign the backup with the cross signing key so the key backup can // be trusted via cross-signing. await baseApis.http.authedRequest( - undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version, + undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version, undefined, { - algorithm: this._keyBackupInfo.algorithm, - auth_data: this._keyBackupInfo.auth_data, + algorithm: this.keyBackupInfo.algorithm, + auth_data: this.keyBackupInfo.auth_data, }, { prefix: PREFIX_UNSTABLE }, ); @@ -215,7 +233,7 @@ export class EncryptionSetupOperation { // add new key backup await baseApis.http.authedRequest( undefined, "POST", "/room_keys/version", - undefined, this._keyBackupInfo, + undefined, this.keyBackupInfo, { prefix: PREFIX_UNSTABLE }, ); } @@ -228,20 +246,20 @@ export class EncryptionSetupOperation { * implementing the methods related to account data in MatrixClient */ class AccountDataClientAdapter extends EventEmitter { + public readonly values = new Map(); + /** - * @param {Object.} accountData existing account data + * @param {Object.} existingValues existing account data */ - constructor(accountData) { + constructor(private readonly existingValues: Record) { super(); - this._existingValues = accountData; - this._values = new Map(); } /** * @param {String} type * @return {Promise} the content of the account data */ - getAccountDataFromServer(type) { + public getAccountDataFromServer(type: string): Promise { return Promise.resolve(this.getAccountData(type)); } @@ -249,12 +267,12 @@ class AccountDataClientAdapter extends EventEmitter { * @param {String} type * @return {Object} the content of the account data */ - getAccountData(type) { - const modifiedValue = this._values.get(type); + public getAccountData(type: string): object { + const modifiedValue = this.values.get(type); if (modifiedValue) { return modifiedValue; } - const existingValue = this._existingValues[type]; + const existingValue = this.existingValues[type]; if (existingValue) { return existingValue.getContent(); } @@ -266,9 +284,9 @@ class AccountDataClientAdapter extends EventEmitter { * @param {Object} content * @return {Promise} */ - setAccountData(type, content) { - const lastEvent = this._values.get(type); - this._values.set(type, content); + public setAccountData(type: string, content: object): Promise { + const lastEvent = this.values.get(type); + this.values.set(type, content); // ensure accountData is emitted on the next tick, // as SecretStorage listens for it while calling this method // and it seems to rely on this. @@ -284,27 +302,25 @@ class AccountDataClientAdapter extends EventEmitter { * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks. * See CrossSigningInfo constructor */ -class CrossSigningCallbacks { - constructor() { - this.privateKeys = new Map(); - } +class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks { + public readonly privateKeys = new Map(); // cache callbacks - getCrossSigningKeyCache(type, expectedPublicKey) { + public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise { return this.getCrossSigningKey(type, expectedPublicKey); } - storeCrossSigningKeyCache(type, key) { + public storeCrossSigningKeyCache(type: string, key: Uint8Array): Promise { this.privateKeys.set(type, key); return Promise.resolve(); } // non-cache callbacks - getCrossSigningKey(type, _expectedPubkey) { + public getCrossSigningKey(type: string, expectedPubkey: string): Promise { return Promise.resolve(this.privateKeys.get(type)); } - saveCrossSigningKeys(privateKeys) { + public saveCrossSigningKeys(privateKeys: Record) { for (const [type, privateKey] of Object.entries(privateKeys)) { this.privateKeys.set(type, privateKey); } @@ -316,39 +332,36 @@ class CrossSigningCallbacks { * the SecretStorage crypto callbacks */ class SSSSCryptoCallbacks { - constructor(delegateCryptoCallbacks) { - this._privateKeys = new Map(); - this._delegateCryptoCallbacks = delegateCryptoCallbacks; - } + private readonly privateKeys = new Map(); - async getSecretStorageKey({ keys }, name) { + constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {} + + public async getSecretStorageKey( + { keys }: { keys: Record }, + name: string, + ): Promise<[string, Uint8Array]> { for (const keyId of Object.keys(keys)) { - const privateKey = this._privateKeys.get(keyId); + const privateKey = this.privateKeys.get(keyId); if (privateKey) { return [keyId, privateKey]; } } // if we don't have the key cached yet, ask // for it to the general crypto callbacks and cache it - if (this._delegateCryptoCallbacks) { - const result = await this._delegateCryptoCallbacks. + if (this.delegateCryptoCallbacks) { + const result = await this.delegateCryptoCallbacks. getSecretStorageKey({ keys }, name); if (result) { const [keyId, privateKey] = result; - this._privateKeys.set(keyId, privateKey); + this.privateKeys.set(keyId, privateKey); } return result; } } - addPrivateKey(keyId, keyInfo, privKey) { - this._privateKeys.set(keyId, privKey); + public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void { + this.privateKeys.set(keyId, privKey); // Also pass along to application to cache if it wishes - if ( - this._delegateCryptoCallbacks && - this._delegateCryptoCallbacks.cacheSecretStorageKey - ) { - this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey); - } + this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey); } } diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.ts similarity index 54% rename from src/crypto/OutgoingRoomKeyRequestManager.js rename to src/crypto/OutgoingRoomKeyRequestManager.ts index 7f64b5313..07426148a 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd +Copyright 2017 - 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. @@ -15,6 +15,10 @@ limitations under the License. */ import { logger } from '../logger'; +import {CryptoStore, MatrixClient} from "../client"; +import {IRoomKeyRequestBody, IRoomKeyRequestRecipient} from "./index"; +import { OutgoingRoomKeyRequest } from './store/base'; +import {EventType} from "../@types/event"; /** * Internal module. Management of outgoing room key requests. @@ -57,61 +61,58 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500; * * @enum {number} */ -export const ROOM_KEY_REQUEST_STATES = { +export enum RoomKeyRequestState { /** request not yet sent */ - UNSENT: 0, - + Unsent, /** request sent, awaiting reply */ - SENT: 1, - + Sent, /** reply received, cancellation not yet sent */ - CANCELLATION_PENDING: 2, - + CancellationPending, /** * Cancellation not yet sent and will transition to UNSENT instead of * being deleted once the cancellation has been sent. */ - CANCELLATION_PENDING_AND_WILL_RESEND: 3, -}; + CancellationPendingAndWillResend, +} export class OutgoingRoomKeyRequestManager { - constructor(baseApis, deviceId, cryptoStore) { - this._baseApis = baseApis; - this._deviceId = deviceId; - this._cryptoStore = cryptoStore; + // handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null + // if the callback has been set, or if it is still running. + private sendOutgoingRoomKeyRequestsTimer: NodeJS.Timeout = null; - // handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null - // if the callback has been set, or if it is still running. - this._sendOutgoingRoomKeyRequestsTimer = null; + // sanity check to ensure that we don't end up with two concurrent runs + // of sendOutgoingRoomKeyRequests + private sendOutgoingRoomKeyRequestsRunning = false; - // sanity check to ensure that we don't end up with two concurrent runs - // of _sendOutgoingRoomKeyRequests - this._sendOutgoingRoomKeyRequestsRunning = false; + private clientRunning = false; - this._clientRunning = false; - } + constructor( + private readonly baseApis: MatrixClient, + private readonly deviceId: string, + private readonly cryptoStore: CryptoStore, + ) {} /** * Called when the client is started. Sets background processes running. */ - start() { - this._clientRunning = true; + public start(): void { + this.clientRunning = true; } /** * Called when the client is stopped. Stops any running background processes. */ - stop() { + public stop(): void { logger.log('stopping OutgoingRoomKeyRequestManager'); // stop the timer on the next run - this._clientRunning = false; + this.clientRunning = false; } /** * Send any requests that have been queued */ - sendQueuedRequests() { - this._startTimer(); + public sendQueuedRequests(): void { + this.startTimer(); } /** @@ -131,95 +132,99 @@ export class OutgoingRoomKeyRequestManager { * pending list (or we have established that a similar request already * exists) */ - async queueRoomKeyRequest(requestBody, recipients, resend=false) { - const req = await this._cryptoStore.getOutgoingRoomKeyRequest( + public async queueRoomKeyRequest( + requestBody: IRoomKeyRequestBody, + recipients: IRoomKeyRequestRecipient[], + resend = false, + ): Promise { + const req = await this.cryptoStore.getOutgoingRoomKeyRequest( requestBody, ); if (!req) { - await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({ + await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({ requestBody: requestBody, recipients: recipients, - requestId: this._baseApis.makeTxnId(), - state: ROOM_KEY_REQUEST_STATES.UNSENT, + requestId: this.baseApis.makeTxnId(), + state: RoomKeyRequestState.Unsent, }); } else { switch (req.state) { - case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: - case ROOM_KEY_REQUEST_STATES.UNSENT: - // nothing to do here, since we're going to send a request anyways - return; + case RoomKeyRequestState.CancellationPendingAndWillResend: + case RoomKeyRequestState.Unsent: + // nothing to do here, since we're going to send a request anyways + return; - case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: { - // existing request is about to be cancelled. If we want to - // resend, then change the state so that it resends after - // cancelling. Otherwise, just cancel the cancellation. - const state = resend ? - ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND : - ROOM_KEY_REQUEST_STATES.SENT; - await this._cryptoStore.updateOutgoingRoomKeyRequest( - req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, { - state, - cancellationTxnId: this._baseApis.makeTxnId(), - }, - ); - break; - } - case ROOM_KEY_REQUEST_STATES.SENT: { - // a request has already been sent. If we don't want to - // resend, then do nothing. If we do want to, then cancel the - // existing request and send a new one. - if (resend) { - const state = - ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND; - const updatedReq = - await this._cryptoStore.updateOutgoingRoomKeyRequest( - req.requestId, ROOM_KEY_REQUEST_STATES.SENT, { - state, - cancellationTxnId: this._baseApis.makeTxnId(), - // need to use a new transaction ID so that - // the request gets sent - requestTxnId: this._baseApis.makeTxnId(), - }, - ); - if (!updatedReq) { - // updateOutgoingRoomKeyRequest couldn't find the request - // in state ROOM_KEY_REQUEST_STATES.SENT, so we must have - // raced with another tab to mark the request cancelled. - // Try again, to make sure the request is resent. - return await this.queueRoomKeyRequest( - requestBody, recipients, resend, - ); - } - - // We don't want to wait for the timer, so we send it - // immediately. (We might actually end up racing with the timer, - // but that's ok: even if we make the request twice, we'll do it - // with the same transaction_id, so only one message will get - // sent). - // - // (We also don't want to wait for the response from the server - // here, as it will slow down processing of received keys if we - // do.) - try { - await this._sendOutgoingRoomKeyRequestCancellation( - updatedReq, - true, - ); - } catch (e) { - logger.error( - "Error sending room key request cancellation;" - + " will retry later.", e, - ); - } - // The request has transitioned from - // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We - // still need to resend the request which is now UNSENT, so - // start the timer if it isn't already started. + case RoomKeyRequestState.CancellationPending: { + // existing request is about to be cancelled. If we want to + // resend, then change the state so that it resends after + // cancelling. Otherwise, just cancel the cancellation. + const state = resend ? + RoomKeyRequestState.CancellationPendingAndWillResend : + RoomKeyRequestState.Sent; + await this.cryptoStore.updateOutgoingRoomKeyRequest( + req.requestId, RoomKeyRequestState.CancellationPending, { + state, + cancellationTxnId: this.baseApis.makeTxnId(), + }, + ); + break; } - break; - } - default: - throw new Error('unhandled state: ' + req.state); + case RoomKeyRequestState.Sent: { + // a request has already been sent. If we don't want to + // resend, then do nothing. If we do want to, then cancel the + // existing request and send a new one. + if (resend) { + const state = + RoomKeyRequestState.CancellationPendingAndWillResend; + const updatedReq = + await this.cryptoStore.updateOutgoingRoomKeyRequest( + req.requestId, RoomKeyRequestState.Sent, { + state, + cancellationTxnId: this.baseApis.makeTxnId(), + // need to use a new transaction ID so that + // the request gets sent + requestTxnId: this.baseApis.makeTxnId(), + }, + ); + if (!updatedReq) { + // updateOutgoingRoomKeyRequest couldn't find the request + // in state ROOM_KEY_REQUEST_STATES.SENT, so we must have + // raced with another tab to mark the request cancelled. + // Try again, to make sure the request is resent. + return await this.queueRoomKeyRequest( + requestBody, recipients, resend, + ); + } + + // We don't want to wait for the timer, so we send it + // immediately. (We might actually end up racing with the timer, + // but that's ok: even if we make the request twice, we'll do it + // with the same transaction_id, so only one message will get + // sent). + // + // (We also don't want to wait for the response from the server + // here, as it will slow down processing of received keys if we + // do.) + try { + await this.sendOutgoingRoomKeyRequestCancellation( + updatedReq, + true, + ); + } catch (e) { + logger.error( + "Error sending room key request cancellation;" + + " will retry later.", e, + ); + } + // The request has transitioned from + // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We + // still need to resend the request which is now UNSENT, so + // start the timer if it isn't already started. + } + break; + } + default: + throw new Error('unhandled state: ' + req.state); } } } @@ -232,8 +237,8 @@ export class OutgoingRoomKeyRequestManager { * @returns {Promise} resolves when the request has been updated in our * pending list. */ - cancelRoomKeyRequest(requestBody) { - return this._cryptoStore.getOutgoingRoomKeyRequest( + public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { + return this.cryptoStore.getOutgoingRoomKeyRequest( requestBody, ).then((req) => { if (!req) { @@ -241,12 +246,12 @@ export class OutgoingRoomKeyRequestManager { return; } switch (req.state) { - case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: - case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + case RoomKeyRequestState.CancellationPending: + case RoomKeyRequestState.CancellationPendingAndWillResend: // nothing to do here return; - case ROOM_KEY_REQUEST_STATES.UNSENT: + case RoomKeyRequestState.Unsent: // just delete it // FIXME: ghahah we may have attempted to send it, and @@ -258,16 +263,16 @@ export class OutgoingRoomKeyRequestManager { 'deleting unnecessary room key request for ' + stringifyRequestBody(requestBody), ); - return this._cryptoStore.deleteOutgoingRoomKeyRequest( - req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT, + return this.cryptoStore.deleteOutgoingRoomKeyRequest( + req.requestId, RoomKeyRequestState.Unsent, ); - case ROOM_KEY_REQUEST_STATES.SENT: { + case RoomKeyRequestState.Sent: { // send a cancellation. - return this._cryptoStore.updateOutgoingRoomKeyRequest( - req.requestId, ROOM_KEY_REQUEST_STATES.SENT, { - state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, - cancellationTxnId: this._baseApis.makeTxnId(), + return this.cryptoStore.updateOutgoingRoomKeyRequest( + req.requestId, RoomKeyRequestState.Sent, { + state: RoomKeyRequestState.CancellationPending, + cancellationTxnId: this.baseApis.makeTxnId(), }, ).then((updatedReq) => { if (!updatedReq) { @@ -294,14 +299,14 @@ export class OutgoingRoomKeyRequestManager { // (We also don't want to wait for the response from the server // here, as it will slow down processing of received keys if we // do.) - this._sendOutgoingRoomKeyRequestCancellation( + this.sendOutgoingRoomKeyRequestCancellation( updatedReq, ).catch((e) => { logger.error( "Error sending room key request cancellation;" + " will retry later.", e, ); - this._startTimer(); + this.startTimer(); }); }); } @@ -320,10 +325,8 @@ export class OutgoingRoomKeyRequestManager { * @return {Promise} resolves to a list of all the * {@link module:crypto/store/base~OutgoingRoomKeyRequest} */ - getOutgoingSentRoomKeyRequest(userId, deviceId) { - return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget( - userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT], - ); + public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): OutgoingRoomKeyRequest[] { + return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); } /** @@ -333,29 +336,27 @@ export class OutgoingRoomKeyRequestManager { * For example, after initialization or self-verification. * @return {Promise} An array of `queueRoomKeyRequest` outputs. */ - async cancelAndResendAllOutgoingRequests() { - const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState( - ROOM_KEY_REQUEST_STATES.SENT, - ); + public async cancelAndResendAllOutgoingRequests(): Promise { + const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); return Promise.all(outgoings.map(({ requestBody, recipients }) => this.queueRoomKeyRequest(requestBody, recipients, true))); } // start the background timer to send queued requests, if the timer isn't // already running - _startTimer() { - if (this._sendOutgoingRoomKeyRequestsTimer) { + private startTimer(): void { + if (this.sendOutgoingRoomKeyRequestsTimer) { return; } const startSendingOutgoingRoomKeyRequests = () => { - if (this._sendOutgoingRoomKeyRequestsRunning) { + if (this.sendOutgoingRoomKeyRequestsRunning) { throw new Error("RoomKeyRequestSend already in progress!"); } - this._sendOutgoingRoomKeyRequestsRunning = true; + this.sendOutgoingRoomKeyRequestsRunning = true; - this._sendOutgoingRoomKeyRequests().finally(() => { - this._sendOutgoingRoomKeyRequestsRunning = false; + this.sendOutgoingRoomKeyRequests().finally(() => { + this.sendOutgoingRoomKeyRequestsRunning = false; }).catch((e) => { // this should only happen if there is an indexeddb error, // in which case we're a bit stuffed anyway. @@ -365,7 +366,7 @@ export class OutgoingRoomKeyRequestManager { }); }; - this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout( + this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout( startSendingOutgoingRoomKeyRequests, SEND_KEY_REQUESTS_DELAY_MS, ); @@ -374,47 +375,47 @@ export class OutgoingRoomKeyRequestManager { // look for and send any queued requests. Runs itself recursively until // there are no more requests, or there is an error (in which case, the // timer will be restarted before the promise resolves). - _sendOutgoingRoomKeyRequests() { - if (!this._clientRunning) { - this._sendOutgoingRoomKeyRequestsTimer = null; + private sendOutgoingRoomKeyRequests(): Promise { + if (!this.clientRunning) { + this.sendOutgoingRoomKeyRequestsTimer = null; return Promise.resolve(); } - return this._cryptoStore.getOutgoingRoomKeyRequestByState([ - ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, - ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, - ROOM_KEY_REQUEST_STATES.UNSENT, - ]).then((req) => { + return this.cryptoStore.getOutgoingRoomKeyRequestByState([ + RoomKeyRequestState.CancellationPending, + RoomKeyRequestState.CancellationPendingAndWillResend, + RoomKeyRequestState.Unsent, + ]).then((req: OutgoingRoomKeyRequest) => { if (!req) { - this._sendOutgoingRoomKeyRequestsTimer = null; + this.sendOutgoingRoomKeyRequestsTimer = null; return; } let prom; switch (req.state) { - case ROOM_KEY_REQUEST_STATES.UNSENT: - prom = this._sendOutgoingRoomKeyRequest(req); + case RoomKeyRequestState.Unsent: + prom = this.sendOutgoingRoomKeyRequest(req); break; - case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: - prom = this._sendOutgoingRoomKeyRequestCancellation(req); + case RoomKeyRequestState.CancellationPending: + prom = this.sendOutgoingRoomKeyRequestCancellation(req); break; - case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: - prom = this._sendOutgoingRoomKeyRequestCancellation(req, true); + case RoomKeyRequestState.CancellationPendingAndWillResend: + prom = this.sendOutgoingRoomKeyRequestCancellation(req, true); break; } return prom.then(() => { // go around the loop again - return this._sendOutgoingRoomKeyRequests(); + return this.sendOutgoingRoomKeyRequests(); }).catch((e) => { logger.error("Error sending room key request; will retry later.", e); - this._sendOutgoingRoomKeyRequestsTimer = null; + this.sendOutgoingRoomKeyRequestsTimer = null; }); }); } // given a RoomKeyRequest, send it and update the request record - _sendOutgoingRoomKeyRequest(req) { + private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise { logger.log( `Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + @@ -423,24 +424,24 @@ export class OutgoingRoomKeyRequestManager { const requestMessage = { action: "request", - requesting_device_id: this._deviceId, + requesting_device_id: this.deviceId, request_id: req.requestId, body: req.requestBody, }; - return this._sendMessageToDevices( + return this.sendMessageToDevices( requestMessage, req.recipients, req.requestTxnId || req.requestId, ).then(() => { - return this._cryptoStore.updateOutgoingRoomKeyRequest( - req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT, - { state: ROOM_KEY_REQUEST_STATES.SENT }, + return this.cryptoStore.updateOutgoingRoomKeyRequest( + req.requestId, RoomKeyRequestState.Unsent, + { state: RoomKeyRequestState.Sent }, ); }); } // Given a RoomKeyRequest, cancel it and delete the request record unless // andResend is set, in which case transition to UNSENT. - _sendOutgoingRoomKeyRequestCancellation(req, andResend) { + private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise { logger.log( `Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + @@ -450,30 +451,30 @@ export class OutgoingRoomKeyRequestManager { const requestMessage = { action: "request_cancellation", - requesting_device_id: this._deviceId, + requesting_device_id: this.deviceId, request_id: req.requestId, }; - return this._sendMessageToDevices( + return this.sendMessageToDevices( requestMessage, req.recipients, req.cancellationTxnId, ).then(() => { if (andResend) { // We want to resend, so transition to UNSENT - return this._cryptoStore.updateOutgoingRoomKeyRequest( + return this.cryptoStore.updateOutgoingRoomKeyRequest( req.requestId, - ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, - { state: ROOM_KEY_REQUEST_STATES.UNSENT }, + RoomKeyRequestState.CancellationPendingAndWillResend, + { state: RoomKeyRequestState.Unsent }, ); } - return this._cryptoStore.deleteOutgoingRoomKeyRequest( - req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, + return this.cryptoStore.deleteOutgoingRoomKeyRequest( + req.requestId, RoomKeyRequestState.CancellationPending, ); }); } // send a RoomKeyRequest to a list of recipients - _sendMessageToDevices(message, recipients, txnId) { - const contentMap = {}; + private sendMessageToDevices(message, recipients, txnId: string): Promise<{}> { + const contentMap: Record>> = {}; for (const recip of recipients) { if (!contentMap[recip.userId]) { contentMap[recip.userId] = {}; @@ -481,9 +482,7 @@ export class OutgoingRoomKeyRequestManager { contentMap[recip.userId][recip.deviceId] = message; } - return this._baseApis.sendToDevice( - 'm.room_key_request', contentMap, txnId, - ); + return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId); } } diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 3cc465419..1c370ecc4 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -25,7 +25,7 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? // salt for HKDF, with 8 bytes of zeros const zeroSalt = new Uint8Array(8); -interface IEncryptedPayload { +export interface IEncryptedPayload { iv: string; ciphertext: string; mac: string; diff --git a/src/crypto/api.ts b/src/crypto/api.ts index 39469a83a..8daafb6d7 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { DeviceInfo } from "./deviceinfo"; -import { IKeyBackupVersion } from "./keybackup"; +import { IKeyBackupInfo } from "./keybackup"; import { ISecretStorageKeyInfo } from "../matrix"; // TODO: Merge this with crypto.js once converted @@ -85,7 +85,7 @@ export interface ICreateSecretStorageOpts { * The current key backup object. If passed, * the passphrase and recovery key from this backup will be used. */ - keyBackupInfo?: IKeyBackupVersion; + keyBackupInfo?: IKeyBackupInfo; /** * If true, a new key backup version will be diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 3d5485f27..3a8422a74 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -29,11 +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"; +import { IKeyBackupInfo } from "./keybackup"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; -type AuthData = IKeyBackupVersion["auth_data"]; +type AuthData = IKeyBackupInfo["auth_data"]; type SigInfo = { deviceId: string, @@ -49,7 +49,7 @@ export type TrustInfo = { }; export interface IKeyBackupCheck { - backupInfo: IKeyBackupVersion; + backupInfo: IKeyBackupInfo; trustInfo: TrustInfo; } @@ -90,7 +90,7 @@ interface BackupAlgorithm { */ export class BackupManager { private algorithm: BackupAlgorithm | undefined; - public backupInfo: IKeyBackupVersion | undefined; // The info dict from /room_keys/version + public backupInfo: IKeyBackupInfo | 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) { @@ -102,7 +102,7 @@ export class BackupManager { return this.backupInfo && this.backupInfo.version; } - public static async makeAlgorithm(info: IKeyBackupVersion, getKey: GetKey): Promise { + public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise { const Algorithm = algorithmsByName[info.algorithm]; if (!Algorithm) { throw new Error("Unknown backup algorithm"); @@ -110,7 +110,7 @@ export class BackupManager { return await Algorithm.init(info.auth_data, getKey); } - public async enableKeyBackup(info: IKeyBackupVersion): Promise { + public async enableKeyBackup(info: IKeyBackupInfo): Promise { this.backupInfo = info; if (this.algorithm) { this.algorithm.free(); @@ -166,7 +166,7 @@ export class BackupManager { }; } - public async createKeyBackupVersion(info: IKeyBackupVersion): Promise { + public async createKeyBackupVersion(info: IKeyBackupInfo): Promise { this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); } @@ -183,7 +183,7 @@ export class BackupManager { this.checkedForBackup = true; return null; } - let backupInfo: IKeyBackupVersion; + let backupInfo: IKeyBackupInfo; try { backupInfo = await this.baseApis.getKeyBackupVersion(); } catch (e) { @@ -260,7 +260,7 @@ export class BackupManager { * ] * } */ - public async isKeyBackupTrusted(backupInfo: IKeyBackupVersion): Promise { + public async isKeyBackupTrusted(backupInfo: IKeyBackupInfo): Promise { const ret = { usable: false, trusted_locally: false, diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts index d723eac4b..3272e047e 100644 --- a/src/crypto/deviceinfo.ts +++ b/src/crypto/deviceinfo.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ISignatures } from "../@types/signed"; + /** * @module crypto/deviceinfo */ diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 5752f9df6..caacc4dae 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -52,10 +52,11 @@ import { IStore } from "../store"; import { Room } from "../models/room"; import { RoomMember } from "../models/room-member"; import { MatrixEvent } from "../models/event"; -import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore } from "../client"; +import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client"; import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; import type { RoomList } from "./RoomList"; import { IRecoveryKey, IEncryptedEventInfo } from "./api"; +import { IKeyBackupInfo } from "./keybackup"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -91,7 +92,7 @@ interface IInitOpts { export interface IBootstrapCrossSigningOpts { setupNewCrossSigning?: boolean; - authUploadDeviceSigningKeys?(makeRequest: (authData: any) => void): Promise; + authUploadDeviceSigningKeys?(makeRequest: (authData: any) => void): Promise<{}>; } interface IBootstrapSecretStorageOpts { @@ -111,7 +112,7 @@ interface IRoomKey { algorithm: string; } -interface IRoomKeyRequestBody extends IRoomKey { +export interface IRoomKeyRequestBody extends IRoomKey { session_id: string; sender_key: string } @@ -157,7 +158,7 @@ interface ISyncDeviceLists { left: string[]; } -interface IRoomKeyRequestRecipient { +export interface IRoomKeyRequestRecipient { userId: string; deviceId: string; } @@ -276,7 +277,7 @@ export class Crypto extends EventEmitter { * or a class that implements a verification method. */ constructor( - private readonly baseApis: MatrixClient, + public readonly baseApis: MatrixClient, public readonly sessionStore: SessionStore, private readonly userId: string, private readonly deviceId: string, @@ -622,7 +623,7 @@ export class Crypto extends EventEmitter { // Cross-sign own device const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); - const deviceSignature = await crossSigningInfo.signDevice(this.userId, device); + const deviceSignature = await crossSigningInfo.signDevice(this.userId, device) as ISignedKey; builder.addKeySignature(this.userId, this.deviceId, deviceSignature); // Sign message key backup with cross-signing master key @@ -932,7 +933,7 @@ export class Crypto extends EventEmitter { await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); // create keyBackupInfo object to add to builder - const data = { + const data: IKeyBackupInfo = { algorithm: info.algorithm, auth_data: info.auth_data, }; @@ -2843,8 +2844,8 @@ export class Crypto extends EventEmitter { * Re-send any outgoing key requests, eg after verification * @returns {Promise} */ - public cancelAndResendAllOutgoingKeyRequests(): Promise { - return this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); + public async cancelAndResendAllOutgoingKeyRequests(): Promise { + await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); } /** @@ -3291,9 +3292,7 @@ export class Crypto extends EventEmitter { // it. This won't always be the case though so we need to re-send any that have already been sent // to avoid races. const requestsToResend = - await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest( - sender, device.deviceId, - ); + await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(sender, device.deviceId); for (const keyReq of requestsToResend) { this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true); } diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index 20ca4f146..123f18f76 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -32,7 +32,7 @@ export interface IKeyBackupRoomSessions { } /* eslint-disable camelcase */ -export interface IKeyBackupVersion { +export interface IKeyBackupInfo { algorithm: string; auth_data: { public_key: string; @@ -41,9 +41,9 @@ export interface IKeyBackupVersion { private_key_iterations: number; private_key_bits?: number; }; - count: number; - etag: string; - version: string; // number contained within + count?: number; + etag?: string; + version?: string; // number contained within } /* eslint-enable camelcase */ diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts index 646f330ec..f7ed067ee 100644 --- a/src/crypto/olmlib.ts +++ b/src/crypto/olmlib.ts @@ -566,7 +566,7 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { * @param {Uint8Array} uint8Array The data to encode. * @return {string} The unpadded base64. */ -export function encodeUnpaddedBase64(uint8Array: Uint8Array): string { +export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { return encodeBase64(uint8Array).replace(/=+$/g, ''); } diff --git a/src/crypto/store/base.js b/src/crypto/store/base.ts similarity index 72% rename from src/crypto/store/base.js rename to src/crypto/store/base.ts index d9d1f7a94..d76fb9ead 100644 --- a/src/crypto/store/base.js +++ b/src/crypto/store/base.ts @@ -10,6 +10,9 @@ * @interface CryptoStore */ +import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index"; +import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager"; + /** * Represents an outgoing room key request * @@ -32,3 +35,11 @@ * @property {Number} state current state of this request (states are defined * in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES}) */ +export interface OutgoingRoomKeyRequest { + requestId: string; + requestTxnId?: string; + cancellationTxnId?: string; + recipients: IRoomKeyRequestRecipient[]; + requestBody: IRoomKeyRequestBody; + state: RoomKeyRequestState; +}