1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Use mapped types for account data content (#4590)

* Use mapped types around account data events

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Harden types for reading account data too

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Correct empty object type

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update src/secret-storage.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Michael Telatynski
2024-12-19 22:53:58 +00:00
committed by GitHub
parent bcf3d56bd5
commit 3fcc56601b
18 changed files with 235 additions and 120 deletions

View File

@ -50,6 +50,13 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
a: {};
b: {};
}
}
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";

View File

@ -45,6 +45,13 @@ import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
global_test: {};
tester: {};
}
}
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;

View File

@ -30,6 +30,7 @@ import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
import { SecretInfo } from "../../../src/secret-storage.ts";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@ -68,6 +69,12 @@ function sign<T extends IObject | ICurve25519AuthData>(
};
}
declare module "../../../src/@types/event" {
interface SecretStorageAccountDataEvents {
foo: SecretInfo;
}
}
describe("Secrets", function () {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");

View File

@ -94,6 +94,12 @@ function convertQueryDictToMap(queryDict?: QueryDict): Map<string, string> {
return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)]));
}
declare module "../../src/@types/event" {
interface AccountDataEvents {
"im.vector.test": {};
}
}
type HttpLookup = {
method: string;
path: string;

View File

@ -30,6 +30,7 @@ import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import {
AccountDataEvents,
Device,
DeviceVerification,
encodeBase64,
@ -1924,11 +1925,13 @@ class DummyAccountDataClient
super();
}
public async getAccountDataFromServer<T extends Record<string, any>>(eventType: string): Promise<T | null> {
public async getAccountDataFromServer<K extends keyof AccountDataEvents>(
eventType: K,
): Promise<AccountDataEvents[K] | null> {
const ret = this.storage.get(eventType);
if (eventType) {
return ret as T;
return ret;
} else {
return null;
}

View File

@ -19,6 +19,18 @@ import {
secretStorageContainsCrossSigningKeys,
} from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
import { SecretInfo } from "../../../src/secret-storage.ts";
declare module "../../../src/@types/event" {
interface SecretStorageAccountDataEvents {
secretA: SecretInfo;
secretB: SecretInfo;
secretC: SecretInfo;
secretD: SecretInfo;
secretE: SecretInfo;
Unknown: SecretInfo;
}
}
describe("secret-storage", () => {
describe("secretStorageContainsCrossSigningKeys", () => {

View File

@ -27,6 +27,14 @@ import {
trimTrailingEquals,
} from "../../src/secret-storage";
import { randomString } from "../../src/randomstring";
import { SecretInfo } from "../../src/secret-storage.ts";
import { AccountDataEvents } from "../../src";
declare module "../../src/@types/event" {
interface SecretStorageAccountDataEvents {
mysecret: SecretInfo;
}
}
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
@ -117,9 +125,11 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.key.my_key") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@ -135,11 +145,13 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.default_key") {
return { key: "default_key_id" } as unknown as T;
return { key: "default_key_id" } as any;
} else if (eventType === "m.secret_storage.key.default_key_id") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@ -236,9 +248,11 @@ describe("ServerSideSecretStorageImpl", function () {
// stub out getAccountData to return a key with an unknown algorithm
const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.key.keyid") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}

View File

@ -58,6 +58,10 @@ import {
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
import { SessionMembershipData } from "../matrixrtc/CallMembership.ts";
import { LocalNotificationSettings } from "./local_notifications.ts";
import { IPushRules } from "./PushRules.ts";
import { SecretInfo, SecretStorageKeyDescription } from "../secret-storage.ts";
import { POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts";
export enum EventType {
// Room state events
@ -368,3 +372,33 @@ export interface StateEvents {
// MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
}
/**
* Mapped type from event type to content type for all specified global account_data events.
*/
export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[EventType.PushRules]: IPushRules;
[EventType.Direct]: { [userId: string]: string[] };
[EventType.IgnoredUserList]: { [userId: string]: {} };
"m.secret_storage.default_key": { key: string };
"m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
// Invites-ignorer events
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
}
/**
* Mapped type from event type to content type for all specified global events encrypted by secret storage.
*
* See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface SecretStorageAccountDataEvents {
"m.megolm_backup.v1": SecretInfo;
"m.cross_signing.master": SecretInfo;
"m.cross_signing.self_signing": SecretInfo;
"m.cross_signing.user_signing": SecretInfo;
"org.matrix.msc3814": SecretInfo;
}

View File

@ -136,6 +136,7 @@ import {
UpdateDelayedEventAction,
} from "./@types/requests.ts";
import {
AccountDataEvents,
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
@ -232,6 +233,7 @@ import {
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import {
AddSecretStorageKeyOpts,
SecretStorageKey,
SecretStorageKeyDescription,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
@ -3070,7 +3072,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
*/
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public isSecretStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}
@ -4236,7 +4238,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an empty object
* @returns Rejects: with an error response.
*/
public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(
eventType: K,
content: AccountDataEvents[K] | Record<string, never>,
): Promise<{}> {
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
$type: eventType,
@ -4251,7 +4256,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param eventType - The event type
* @returns The contents of the given account data event
*/
public getAccountData(eventType: string): MatrixEvent | undefined {
public getAccountData<K extends keyof AccountDataEvents>(eventType: K): MatrixEvent | undefined {
return this.store.getAccountData(eventType);
}
@ -4263,7 +4268,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: The contents of the given account data event.
* @returns Rejects: with an error response.
*/
public async getAccountDataFromServer<T extends { [k: string]: any }>(eventType: string): Promise<T | null> {
public async getAccountDataFromServer<K extends keyof AccountDataEvents>(
eventType: K,
): Promise<AccountDataEvents[K] | null> {
if (this.isInitialSyncComplete()) {
const event = this.store.getAccountData(eventType);
if (!event) {
@ -4271,7 +4278,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// The network version below returns just the content, so this branch
// does the same to match.
return event.getContent<T>();
return event.getContent<AccountDataEvents[K]>();
}
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
@ -4287,7 +4294,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
public async deleteAccountData(eventType: string): Promise<void> {
public async deleteAccountData(eventType: keyof AccountDataEvents): Promise<void> {
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
// if deletion is not supported overwrite with empty content
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
@ -4310,7 +4317,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns The array of users that are ignored (empty if none)
*/
public getIgnoredUsers(): string[] {
const event = this.getAccountData("m.ignored_user_list");
const event = this.getAccountData(EventType.IgnoredUserList);
if (!event?.getContent()["ignored_users"]) return [];
return Object.keys(event.getContent()["ignored_users"]);
}
@ -4326,7 +4333,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
userIds.forEach((u) => {
content.ignored_users[u] = {};
});
return this.setAccountData("m.ignored_user_list", content);
return this.setAccountData(EventType.IgnoredUserList, content);
}
/**
@ -9264,7 +9271,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceId: string,
notificationSettings: LocalNotificationSettings,
): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
return this.setAccountData(key, notificationSettings);
}

View File

@ -184,7 +184,7 @@ export class CrossSigningInfo {
}
}
}
for (const type of ["self_signing", "user_signing"]) {
for (const type of ["self_signing", "user_signing"] as const) {
intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {});
}
return Object.keys(stored).length ? stored : null;

View File

@ -25,6 +25,7 @@ import { IKeyBackupInfo } from "./keybackup.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage.ts";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api/index.ts";
import { AccountDataEvents } from "../@types/event.ts";
interface ICrossSigningKeys {
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
@ -111,7 +112,10 @@ export class EncryptionSetupBuilder {
userSignatures[deviceId] = signature;
}
public async setAccountData(type: string, content: object): Promise<void> {
public async setAccountData<K extends keyof AccountDataEvents>(
type: K,
content: AccountDataEvents[K],
): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}
@ -160,7 +164,7 @@ export class EncryptionSetupOperation {
/**
*/
public constructor(
private readonly accountData: Map<string, object>,
private readonly accountData: Map<keyof AccountDataEvents, MatrixEvent>,
private readonly crossSigningKeys?: ICrossSigningKeys,
private readonly keyBackupInfo?: IKeyBackupInfo,
private readonly keySignatures?: KeySignatures,
@ -190,7 +194,7 @@ export class EncryptionSetupOperation {
// set account data
if (this.accountData) {
for (const [type, content] of this.accountData) {
await baseApis.setAccountData(type, content);
await baseApis.setAccountData(type, content.getContent());
}
}
// upload first cross-signing signatures with the new key
@ -236,7 +240,7 @@ class AccountDataClientAdapter
implements AccountDataClient
{
//
public readonly values = new Map<string, MatrixEvent>();
public readonly values = new Map<keyof AccountDataEvents, MatrixEvent>();
/**
* @param existingValues - existing account data
@ -248,33 +252,26 @@ class AccountDataClientAdapter
/**
* @returns the content of the account data
*/
public getAccountDataFromServer<T extends { [k: string]: any }>(type: string): Promise<T | null> {
public getAccountDataFromServer<K extends keyof AccountDataEvents>(type: K): Promise<AccountDataEvents[K] | null> {
return Promise.resolve(this.getAccountData(type));
}
/**
* @returns the content of the account data
*/
public getAccountData<T extends { [k: string]: any }>(type: string): T | null {
const modifiedValue = this.values.get(type);
if (modifiedValue) {
return modifiedValue as unknown as T;
}
const existingValue = this.existingValues.get(type);
if (existingValue) {
return existingValue.getContent<T>();
}
return null;
public getAccountData<K extends keyof AccountDataEvents>(type: K): AccountDataEvents[K] | null {
const event = this.values.get(type) ?? this.existingValues.get(type);
return event?.getContent<AccountDataEvents[K]>() ?? null;
}
public setAccountData(type: string, content: any): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(type: K, content: AccountDataEvents[K]): Promise<{}> {
const event = new MatrixEvent({ type, content });
const lastEvent = this.values.get(type);
this.values.set(type, content);
this.values.set(type, event);
// ensure accountData is emitted on the next tick,
// as SecretStorage listens for it while calling this method
// and it seems to rely on this.
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit(ClientEvent.AccountData, event, lastEvent);
return {};
});

View File

@ -25,12 +25,12 @@ import {
AccountDataClient,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
SecretStorageKey,
} from "../secret-storage.ts";
import { ISecretRequest, SecretSharing } from "./SecretSharing.ts";
/* re-exports for backwards compatibility */
export type {
AccountDataClient as IAccountDataClient,
SecretStorageKeyTuple,
SecretStorageKeyObject,
SECRET_STORAGE_ALGORITHM_V1_AES,
@ -101,21 +101,21 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> im
/**
* Store an encrypted secret on the server
*/
public store(name: string, secret: string, keys?: string[] | null): Promise<void> {
public store(name: SecretStorageKey, secret: string, keys?: string[] | null): Promise<void> {
return this.storageImpl.store(name, secret, keys);
}
/**
* Get a secret from storage.
*/
public get(name: string): Promise<string | undefined> {
public get(name: SecretStorageKey): Promise<string | undefined> {
return this.storageImpl.get(name);
}
/**
* Check if a secret is stored on the server.
*/
public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public async isStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.storageImpl.isStored(name);
}

View File

@ -77,6 +77,7 @@ import {
AddSecretStorageKeyOpts,
calculateKeyCheck,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorageKey,
SecretStorageKeyDescription,
SecretStorageKeyObject,
SecretStorageKeyTuple,
@ -1194,21 +1195,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#store}.
*/
public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
public storeSecret(name: SecretStorageKey, secret: string, keys?: string[]): Promise<void> {
return this.secretStorage.store(name, secret, keys);
}
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}.
*/
public getSecret(name: string): Promise<string | undefined> {
public getSecret(name: SecretStorageKey): Promise<string | undefined> {
return this.secretStorage.get(name);
}
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
*/
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public isSecretStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}

View File

@ -0,0 +1,58 @@
/*
Copyright 2022 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.
*/
import { UnstableValue } from "matrix-events-sdk";
/// The event type storing the user's individual policies.
///
/// Exported for testing purposes.
export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
/// The key within the user's individual policies storing the user's ignored invites.
///
/// Exported for testing purposes.
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
"m.ignore.invites",
"org.matrix.msc3847.ignore.invites",
);
/// The types of recommendations understood.
export enum PolicyRecommendation {
Ban = "m.ban",
}
/**
* The various scopes for policies.
*/
export enum PolicyScope {
/**
* The policy deals with an individual user, e.g. reject invites
* from this user.
*/
User = "m.policy.user",
/**
* The policy deals with a room, e.g. reject invites towards
* a specific room.
*/
Room = "m.policy.room",
/**
* The policy deals with a server, e.g. reject invites from
* this server.
*/
Server = "m.policy.server",
}

View File

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "matrix-events-sdk";
import { MatrixClient } from "../client.ts";
import { IContent, MatrixEvent } from "./event.ts";
import { EventTimeline } from "./event-timeline.ts";
@ -23,47 +21,14 @@ import { Preset } from "../@types/partials.ts";
import { globToRegexp } from "../utils.ts";
import { Room } from "./room.ts";
import { EventType, StateEvents } from "../@types/event.ts";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyRecommendation,
PolicyScope,
} from "./invites-ignorer-types.ts";
/// The event type storing the user's individual policies.
///
/// Exported for testing purposes.
export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
/// The key within the user's individual policies storing the user's ignored invites.
///
/// Exported for testing purposes.
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
"m.ignore.invites",
"org.matrix.msc3847.ignore.invites",
);
/// The types of recommendations understood.
export enum PolicyRecommendation {
Ban = "m.ban",
}
/**
* The various scopes for policies.
*/
export enum PolicyScope {
/**
* The policy deals with an individual user, e.g. reject invites
* from this user.
*/
User = "m.policy.user",
/**
* The policy deals with a room, e.g. reject invites towards
* a specific room.
*/
Room = "m.policy.room",
/**
* The policy deals with a server, e.g. reject invites from
* this server.
*/
Server = "m.policy.server",
}
export { IGNORE_INVITES_ACCOUNT_EVENT_KEY, POLICIES_ACCOUNT_EVENT_TYPE, PolicyRecommendation, PolicyScope };
const scopeToEventTypeMap: Record<PolicyScope, keyof StateEvents> = {
[PolicyScope.User]: EventType.PolicyRuleUser,

View File

@ -71,7 +71,7 @@ import {
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
import { Device, DeviceMap } from "../models/device.ts";
import { SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage.ts";
import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorageKey, ServerSideSecretStorage } from "../secret-storage.ts";
import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
@ -770,7 +770,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
*/
public async isSecretStorageReady(): Promise<boolean> {
// make sure that the cross-signing keys are stored
const secretsToCheck = [
const secretsToCheck: SecretStorageKey[] = [
"m.cross_signing.master",
"m.cross_signing.user_signing",
"m.cross_signing.self_signing",

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ServerSideSecretStorage } from "../secret-storage.ts";
import { SecretStorageKey, ServerSideSecretStorage } from "../secret-storage.ts";
/**
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the default secret storage key.
@ -44,7 +44,7 @@ export async function secretStorageContainsCrossSigningKeys(secretStorage: Serve
*/
export async function secretStorageCanAccessSecrets(
secretStorage: ServerSideSecretStorage,
secretNames: string[],
secretNames: SecretStorageKey[],
): Promise<boolean> {
const defaultKeyId = await secretStorage.getDefaultKeyId();
if (!defaultKeyId) return false;

View File

@ -28,6 +28,7 @@ import { logger } from "./logger.ts";
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
import { AccountDataEvents, SecretStorageAccountDataEvents } from "./@types/event.ts";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@ -138,7 +139,7 @@ export interface AccountDataClient extends TypedEventEmitter<ClientEvent.Account
* @param eventType - The type of account data
* @returns The contents of the given account data event, or `null` if the event is not found
*/
getAccountDataFromServer: <T extends Record<string, any>>(eventType: string) => Promise<T | null>;
getAccountDataFromServer: <K extends keyof AccountDataEvents>(eventType: K) => Promise<AccountDataEvents[K] | null>;
/**
* Set account data event for the current user, with retries
@ -147,7 +148,7 @@ export interface AccountDataClient extends TypedEventEmitter<ClientEvent.Account
* @param content - the content object to be set
* @returns an empty object
*/
setAccountData: (eventType: string, content: any) => Promise<{}>;
setAccountData: <K extends keyof AccountDataEvents>(eventType: K, content: AccountDataEvents[K]) => Promise<{}>;
}
/**
@ -200,7 +201,17 @@ export interface SecretStorageCallbacks {
) => Promise<[string, Uint8Array] | null>;
}
interface SecretInfo {
/**
* Account Data event types which can store secret-storage-encrypted information.
*/
export type SecretStorageKey = keyof SecretStorageAccountDataEvents;
/**
* Account Data event content type for storing secret-storage-encrypted information.
*
* See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface SecretInfo {
encrypted: {
[keyId: string]: AESEncryptedSecretStoragePayload;
};
@ -293,7 +304,7 @@ export interface ServerSideSecretStorage {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
isStored(name: string): Promise<Record<string, SecretStorageKeyDescriptionAesV1> | null>;
isStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescriptionAesV1> | null>;
/**
* Get the current default key ID for encrypting secrets.
@ -340,11 +351,9 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* @returns The default key ID or null if no default key ID is set
*/
public async getDefaultKeyId(): Promise<string | null> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
"m.secret_storage.default_key",
);
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.default_key");
if (!defaultKey) return null;
return defaultKey.key;
return defaultKey.key ?? null;
}
/**
@ -409,11 +418,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
if (!keyId) {
do {
keyId = randomString(32);
} while (
await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
`m.secret_storage.key.${keyId}`,
)
);
} while (await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`));
}
await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
@ -441,9 +446,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
return null;
}
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>(
"m.secret_storage.key." + keyId,
);
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
return keyInfo ? [keyId, keyInfo] : null;
}
@ -492,7 +495,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* @param secret - The secret contents.
* @param keys - The IDs of the keys to use to encrypt the secret, or null/undefined to use the default key.
*/
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
public async store(name: SecretStorageKey, secret: string, keys?: string[] | null): Promise<void> {
const encrypted: Record<string, AESEncryptedSecretStoragePayload> = {};
if (!keys) {
@ -509,9 +512,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>(
"m.secret_storage.key." + keyId,
);
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
@ -542,8 +543,8 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* @returns the decrypted contents of the secret, or "undefined" if `name` is not found in
* the user's account data.
*/
public async get(name: string): Promise<string | undefined> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretInfo>(name);
public async get(name: SecretStorageKey): Promise<string | undefined> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo) {
return;
}
@ -555,9 +556,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
const keys: Record<string, SecretStorageKeyDescriptionAesV1> = {};
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>(
"m.secret_storage.key." + keyId,
);
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
const encInfo = secretInfo.encrypted[keyId];
// only use keys we understand the encryption algorithm of
if (keyInfo?.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
@ -590,9 +589,9 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescriptionAesV1> | null> {
public async isStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescriptionAesV1> | null> {
// check if secret exists
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretInfo>(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo?.encrypted) return null;
const ret: Record<string, SecretStorageKeyDescriptionAesV1> = {};
@ -600,9 +599,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
// 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<SecretStorageKeyDescriptionAesV1>(
"m.secret_storage.key." + keyId,
);
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId];