1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Split up, rename, and move ISecretStorageKeyInfo (#3242)

* Move SecretStorageKeyInfo interfaces out to a new module

* Replace usages of ISecretStorageKeyInfo with SecretStorageKeyDescription
This commit is contained in:
Richard van der Hoff
2023-04-03 11:11:03 +01:00
committed by GitHub
parent 78aa6cb62b
commit 41d3ffdab9
10 changed files with 149 additions and 64 deletions

View File

@@ -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<ISecretStorageKeyInfo>();
const keyInfo = alice
.getAccountData("m.secret_storage.key.key_id")!
.getContent<SecretStorageKeyDescription>();
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2",

View File

@@ -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<EmittedEvents, ClientEventHa
return this.crypto.beginKeyVerification(method, userId, deviceId);
}
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -2863,7 +2863,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
algorithm: string,
opts: IAddSecretStorageKeyOpts,
keyName?: string,
): Promise<{ keyId: string; keyInfo: ISecretStorageKeyInfo }> {
): 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<EmittedEvents, ClientEventHa
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -3306,7 +3306,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* encrypted with, or null if it is not present or not encrypted with a
* trusted key
*/
public isKeyBackupKeyStored(): Promise<Record<string, ISecretStorageKeyInfo> | null> {
public isKeyBackupKeyStored(): Promise<Record<string, SecretStorageKeyDescription> | null> {
return Promise.resolve(this.isSecretStored("m.megolm_backup.v1"));
}

View File

@@ -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<string, ISecretStorageKeyInfo>): void {
function intersect(s: Record<string, SecretStorageKeyDescription>): void {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];

View File

@@ -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<string, ISecretStorageKeyInfo> },
{ keys }: { keys: Record<string, SecretStorageKeyDescription> },
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);

View File

@@ -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<B extends MatrixClient | undefined = MatrixClient> {
opts: IAddSecretStorageKeyOpts = {},
keyId?: string,
): Promise<SecretStorageKeyObject> {
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<ISecretStorageKeyInfo>(
await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
`m.secret_storage.key.${keyId}`,
)
);
@@ -181,7 +182,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
return null;
}
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null;
@@ -206,7 +207,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
*
* @returns whether or not the key matches
*/
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
public async checkKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
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<B extends MatrixClient | undefined = MatrixClient> {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) {
@@ -284,10 +285,10 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
}
// get possible keys to decrypt
const keys: Record<string, ISecretStorageKeyInfo> = {};
const keys: Record<string, SecretStorageKeyDescription> = {};
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
"m.secret_storage.key." + keyId,
);
const encInfo = secretInfo.encrypted[keyId];
@@ -322,17 +323,17 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public async isStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
// check if secret exists
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo?.encrypted) return null;
const ret: Record<string, ISecretStorageKeyInfo> = {};
const ret: Record<string, SecretStorageKeyDescription> = {};
// 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<ISecretStorageKeyInfo>(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) continue;
@@ -544,7 +545,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
}
private async getSecretStorageKey(
keys: Record<string, ISecretStorageKeyInfo>,
keys: Record<string, SecretStorageKeyDescription>,
name: string,
): Promise<[string, IDecryptors]> {
if (!this.cryptoCallbacks.getSecretStorageKey) {

View File

@@ -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<Uint8Array>;
}
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;
}

View File

@@ -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

View File

@@ -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<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
getSecretStorageKey?: (
keys: { keys: Record<string, ISecretStorageKeyInfo> },
keys: { keys: Record<string, SecretStorageKeyDescription> },
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<string | undefined>;
getDehydrationKey?: (keyInfo: ISecretStorageKeyInfo, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>;
getDehydrationKey?: (
keyInfo: SecretStorageKeyDescription,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
@@ -923,7 +926,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return keyId;
};
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo): Promise<void> => {
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: SecretStorageKeyDescription): Promise<void> => {
if (!keyInfo.mac) {
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
{ keys: { [keyId]: keyInfo } },
@@ -1130,7 +1133,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.secretStorage.get(name);
}
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}
@@ -1149,7 +1152,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.secretStorage.setDefaultKeyId(k);
}
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
return this.secretStorage.checkKey(key, info);
}

View File

@@ -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";

88
src/secret-storage.ts Normal file
View File

@@ -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;
}