From 41d3ffdab91ff0a98a22f3c9c5f98f1292599a65 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 3 Apr 2023 11:11:03 +0100 Subject: [PATCH] Split up, rename, and move `ISecretStorageKeyInfo` (#3242) * Move SecretStorageKeyInfo interfaces out to a new module * Replace usages of ISecretStorageKeyInfo with SecretStorageKeyDescription --- spec/unit/crypto/secrets.spec.ts | 6 ++- src/client.ts | 10 ++-- src/crypto/CrossSigning.ts | 4 +- src/crypto/EncryptionSetup.ts | 6 +-- src/crypto/SecretStorage.ts | 51 +++++++++--------- src/crypto/api.ts | 26 +++------- src/crypto/dehydration.ts | 4 +- src/crypto/index.ts | 17 +++--- src/matrix.ts | 1 + src/secret-storage.ts | 88 ++++++++++++++++++++++++++++++++ 10 files changed, 149 insertions(+), 64 deletions(-) create mode 100644 src/secret-storage.ts diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 2a31f856c..5bd73e9a5 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -25,10 +25,10 @@ import { encryptAES } from "../../../src/crypto/aes"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { logger } from "../../../src/logger"; import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client"; -import { ISecretStorageKeyInfo } from "../../../src/crypto/api"; import { DeviceInfo } from "../../../src/crypto/deviceinfo"; import { ISignatures } from "../../../src/@types/signed"; import { ICurve25519AuthData } from "../../../src/crypto/keybackup"; +import { SecretStorageKeyDescription } from "../../../src/secret-storage"; async function makeTestClient( userInfo: { userId: string; deviceId: string }, @@ -541,7 +541,9 @@ describe("Secrets", function () { await alice.bootstrapSecretStorage({}); expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" }); - const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent(); + const keyInfo = alice + .getAccountData("m.secret_storage.key.key_id")! + .getContent(); expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2"); expect(keyInfo.passphrase).toEqual({ algorithm: "m.pbkdf2", diff --git a/src/client.ts b/src/client.ts index fbb133c66..0e47ff6e4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -106,7 +106,6 @@ import { IEncryptedEventInfo, IImportRoomKeysOpts, IRecoveryKey, - ISecretStorageKeyInfo, } from "./crypto/api"; import { EventTimelineSet } from "./models/event-timeline-set"; import { VerificationRequest } from "./crypto/verification/request/VerificationRequest"; @@ -208,6 +207,7 @@ import { CryptoBackend } from "./common-crypto/CryptoBackend"; import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants"; import { CryptoApi } from "./crypto-api"; import { DeviceInfoMap } from "./crypto/DeviceList"; +import { SecretStorageKeyDescription } from "./secret-storage"; export type Store = IStore; @@ -2463,7 +2463,7 @@ export class MatrixClient extends TypedEventEmitter { + public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2863,7 +2863,7 @@ export class MatrixClient extends TypedEventEmitter { + ): Promise<{ keyId: string; keyInfo: SecretStorageKeyDescription }> { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2929,7 +2929,7 @@ export class MatrixClient extends TypedEventEmitter | null> { + public isSecretStored(name: string): Promise | null> { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -3306,7 +3306,7 @@ export class MatrixClient extends TypedEventEmitter | null> { + public isKeyBackupKeyStored(): Promise | null> { return Promise.resolve(this.isSecretStored("m.megolm_backup.v1")); } diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index c94546219..31ed2d4dd 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -31,7 +31,7 @@ import { OlmDevice } from "./OlmDevice"; import { ICryptoCallbacks } from "."; import { ISignatures } from "../@types/signed"; import { CryptoStore, SecretStorePrivateKeys } from "./store/base"; -import { ISecretStorageKeyInfo } from "./api"; +import { SecretStorageKeyDescription } from "../secret-storage"; const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; @@ -169,7 +169,7 @@ export class CrossSigningInfo { // check what SSSS keys have encrypted the master key (if any) const stored = (await secretStorage.isStored("m.cross_signing.master")) || {}; // then check which of those SSSS keys have also encrypted the SSK and USK - function intersect(s: Record): void { + function intersect(s: Record): void { for (const k of Object.keys(stored)) { if (!s[k]) { delete stored[k]; diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index f0cf4bf40..4efe677ad 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -28,10 +28,10 @@ import { ISignedKey, KeySignatures, } from "../client"; -import { ISecretStorageKeyInfo } from "./api"; import { IKeyBackupInfo } from "./keybackup"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { IAccountDataClient } from "./SecretStorage"; +import { SecretStorageKeyDescription } from "../secret-storage"; interface ICrossSigningKeys { authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; @@ -326,7 +326,7 @@ class SSSSCryptoCallbacks { public constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {} public async getSecretStorageKey( - { keys }: { keys: Record }, + { keys }: { keys: Record }, name: string, ): Promise<[string, Uint8Array] | null> { for (const keyId of Object.keys(keys)) { @@ -348,7 +348,7 @@ class SSSSCryptoCallbacks { return null; } - public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void { + public addPrivateKey(keyId: string, keyInfo: SecretStorageKeyDescription, privKey: Uint8Array): void { this.privateKeys.set(keyId, privKey); // Also pass along to application to cache if it wishes this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey); diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index f5e3fb59c..5c9049fba 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -23,16 +23,17 @@ import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./ import { ICryptoCallbacks, IEncryptedContent } from "."; import { IContent, MatrixEvent } from "../models/event"; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client"; -import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api"; +import { IAddSecretStorageKeyOpts } from "./api"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { defer, IDeferred } from "../utils"; import { ToDeviceMessageId } from "../@types/event"; +import { SecretStorageKeyDescription, SecretStorageKeyDescriptionAesV1 } from "../secret-storage"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; // Some of the key functions use a tuple and some use an object... -export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo]; -export type SecretStorageKeyObject = { keyId: string; keyInfo: ISecretStorageKeyInfo }; +export type SecretStorageKeyTuple = [keyId: string, keyInfo: SecretStorageKeyDescription]; +export type SecretStorageKeyObject = { keyId: string; keyInfo: SecretStorageKeyDescription }; export interface ISecretRequest { requestId: string; @@ -127,30 +128,30 @@ export class SecretStorage { opts: IAddSecretStorageKeyOpts = {}, keyId?: string, ): Promise { - const keyInfo = { algorithm } as ISecretStorageKeyInfo; + if (algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) { + throw new Error(`Unknown key algorithm ${algorithm}`); + } + + const keyInfo = { algorithm } as SecretStorageKeyDescriptionAesV1; if (opts.name) { keyInfo.name = opts.name; } - if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { - if (opts.passphrase) { - keyInfo.passphrase = opts.passphrase; - } - if (opts.key) { - const { iv, mac } = await calculateKeyCheck(opts.key); - keyInfo.iv = iv; - keyInfo.mac = mac; - } - } else { - throw new Error(`Unknown key algorithm ${algorithm}`); + if (opts.passphrase) { + keyInfo.passphrase = opts.passphrase; + } + if (opts.key) { + const { iv, mac } = await calculateKeyCheck(opts.key); + keyInfo.iv = iv; + keyInfo.mac = mac; } if (!keyId) { do { keyId = randomString(32); } while ( - await this.accountDataAdapter.getAccountDataFromServer( + await this.accountDataAdapter.getAccountDataFromServer( `m.secret_storage.key.${keyId}`, ) ); @@ -181,7 +182,7 @@ export class SecretStorage { return null; } - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); return keyInfo ? [keyId, keyInfo] : null; @@ -206,7 +207,7 @@ export class SecretStorage { * * @returns whether or not the key matches */ - public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise { + public async checkKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise { if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (info.mac) { const { mac } = await calculateKeyCheck(key, info.iv); @@ -245,7 +246,7 @@ export class SecretStorage { for (const keyId of keys) { // get key information from key storage - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); if (!keyInfo) { @@ -284,10 +285,10 @@ export class SecretStorage { } // get possible keys to decrypt - const keys: Record = {}; + const keys: Record = {}; for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); const encInfo = secretInfo.encrypted[keyId]; @@ -322,17 +323,17 @@ export class SecretStorage { * with, or null if it is not present or not encrypted with a trusted * key */ - public async isStored(name: string): Promise | null> { + public async isStored(name: string): Promise | null> { // check if secret exists const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo?.encrypted) return null; - const ret: Record = {}; + const ret: Record = {}; // 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.accountDataAdapter.getAccountDataFromServer( + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( "m.secret_storage.key." + keyId, ); if (!keyInfo) continue; @@ -544,7 +545,7 @@ export class SecretStorage { } private async getSecretStorageKey( - keys: Record, + keys: Record, name: string, ): Promise<[string, IDecryptors]> { if (!this.cryptoCallbacks.getSecretStorageKey) { diff --git a/src/crypto/api.ts b/src/crypto/api.ts index 468cc9933..9e9ba52c3 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -16,6 +16,13 @@ limitations under the License. import { DeviceInfo } from "./deviceinfo"; import { IKeyBackupInfo } from "./keybackup"; +import { PassphraseInfo } from "../secret-storage"; + +/* re-exports for backwards compatibility. */ +export { + PassphraseInfo as IPassphraseInfo, + SecretStorageKeyDescription as ISecretStorageKeyInfo, +} from "../secret-storage"; // TODO: Merge this with crypto.js once converted @@ -98,26 +105,9 @@ export interface ICreateSecretStorageOpts { getKeyBackupPassphrase?: () => Promise; } -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 interface IPassphraseInfo { - algorithm: "m.pbkdf2"; - iterations: number; - salt: string; - bits?: number; -} - export interface IAddSecretStorageKeyOpts { pubkey?: string; - passphrase?: IPassphraseInfo; + passphrase?: PassphraseInfo; name?: string; key?: Uint8Array; } diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 26640c808..373b236b2 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -21,13 +21,13 @@ import { decodeBase64, encodeBase64 } from "./olmlib"; import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store"; import { decryptAES, encryptAES } from "./aes"; import { logger } from "../logger"; -import { ISecretStorageKeyInfo } from "./api"; import { Crypto } from "./index"; import { Method } from "../http-api"; +import { SecretStorageKeyDescription } from "../secret-storage"; export interface IDehydratedDevice { device_id: string; // eslint-disable-line camelcase - device_data: ISecretStorageKeyInfo & { + device_data: SecretStorageKeyDescription & { // eslint-disable-line camelcase algorithm: string; account: string; // pickle diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 21c4692cd..68df6cacf 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -48,7 +48,6 @@ import { IEncryptedEventInfo, IImportRoomKeysOpts, IRecoveryKey, - ISecretStorageKeyInfo, } from "./api"; import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager"; import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; @@ -91,6 +90,7 @@ import { IMessage } from "./algorithms/olm"; import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { RoomState, RoomStateEvent } from "../models/room-state"; import { MapWithDefault, recursiveMapToObject } from "../utils"; +import { SecretStorageKeyDescription } from "../secret-storage"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -142,10 +142,10 @@ export interface ICryptoCallbacks { saveCrossSigningKeys?: (keys: Record) => void; shouldUpgradeDeviceVerifications?: (users: Record) => Promise; getSecretStorageKey?: ( - keys: { keys: Record }, + keys: { keys: Record }, name: string, ) => Promise<[string, Uint8Array] | null>; - cacheSecretStorageKey?: (keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array) => void; + cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void; onSecretRequested?: ( userId: string, deviceId: string, @@ -153,7 +153,10 @@ export interface ICryptoCallbacks { secretName: string, deviceTrust: DeviceTrustLevel, ) => Promise; - getDehydrationKey?: (keyInfo: ISecretStorageKeyInfo, checkFunc: (key: Uint8Array) => void) => Promise; + getDehydrationKey?: ( + keyInfo: SecretStorageKeyDescription, + checkFunc: (key: Uint8Array) => void, + ) => Promise; getBackupKey?: () => Promise; } @@ -923,7 +926,7 @@ export class Crypto extends TypedEventEmitter => { + const ensureCanCheckPassphrase = async (keyId: string, keyInfo: SecretStorageKeyDescription): Promise => { if (!keyInfo.mac) { const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.( { keys: { [keyId]: keyInfo } }, @@ -1130,7 +1133,7 @@ export class Crypto extends TypedEventEmitter | null> { + public isSecretStored(name: string): Promise | null> { return this.secretStorage.isStored(name); } @@ -1149,7 +1152,7 @@ export class Crypto extends TypedEventEmitter { + public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise { return this.secretStorage.checkKey(key, info); } diff --git a/src/matrix.ts b/src/matrix.ts index a6fa12e6a..591c5e359 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -55,6 +55,7 @@ export * from "./@types/requests"; export * from "./@types/search"; export * from "./models/room-summary"; export * as ContentHelpers from "./content-helpers"; +export * as SecretStorage from "./secret-storage"; export type { ICryptoCallbacks } from "./crypto"; // used to be located here export { createNewMatrixCall } from "./webrtc/call"; export type { MatrixCall } from "./webrtc/call"; diff --git a/src/secret-storage.ts b/src/secret-storage.ts new file mode 100644 index 000000000..f0c19c44b --- /dev/null +++ b/src/secret-storage.ts @@ -0,0 +1,88 @@ +/* +Copyright 2021-2023 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Implementation of server-side secret storage + * + * @see https://spec.matrix.org/v1.6/client-server-api/#storage + */ + +/** + * Common base interface for Secret Storage Keys. + * + * The common properties for all encryption keys used in server-side secret storage. + * + * @see https://spec.matrix.org/v1.6/client-server-api/#key-storage + */ +export interface SecretStorageKeyDescriptionCommon { + /** A human-readable name for this key. */ + // XXX: according to the spec, this is optional + name: string; + + /** The encryption algorithm used with this key. */ + algorithm: string; + + /** Information for deriving this key from a passphrase. */ + // XXX: according to the spec, this is optional + passphrase: PassphraseInfo; +} + +/** + * Properties for a SSSS key using the `m.secret_storage.v1.aes-hmac-sha2` algorithm. + * + * Corresponds to `AesHmacSha2KeyDescription` in the specification. + * + * @see https://spec.matrix.org/v1.6/client-server-api/#msecret_storagev1aes-hmac-sha2 + */ +export interface SecretStorageKeyDescriptionAesV1 extends SecretStorageKeyDescriptionCommon { + // XXX: strictly speaking, we should be able to enforce the algorithm here. But + // this interface ends up being incorrectly used where other algorithms are in use (notably + // in device-dehydration support), and unpicking that is too much like hard work + // at the moment. + // algorithm: "m.secret_storage.v1.aes-hmac-sha2"; + + /** The 16-byte AES initialization vector, encoded as base64. */ + iv: string; + + /** The MAC of the result of encrypting 32 bytes of 0, encoded as base64. */ + mac: string; +} + +/** + * Union type for secret storage keys. + * + * For now, this is only {@link SecretStorageKeyDescriptionAesV1}, but other interfaces may be added in future. + */ +export type SecretStorageKeyDescription = SecretStorageKeyDescriptionAesV1; + +/** + * Information on how to generate the key from a passphrase. + * + * @see https://spec.matrix.org/v1.6/client-server-api/#deriving-keys-from-passphrases + */ +export interface PassphraseInfo { + /** The algorithm to be used to derive the key. */ + algorithm: "m.pbkdf2"; + + /** The number of PBKDF2 iterations to use. */ + iterations: number; + + /** The salt to be used for PBKDF2. */ + salt: string; + + /** The number of bits to generate. Defaults to 256. */ + bits?: number; +}