1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +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 { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership"; import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
a: {};
b: {};
}
}
describe("MatrixClient syncing", () => { describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost"; const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";

View File

@@ -45,6 +45,13 @@ import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils"; import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership"; import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
global_test: {};
tester: {};
}
}
describe("SlidingSyncSdk", () => { describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined; let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | 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 { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64"; import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api"; import { CrossSigningKeyInfo } from "../../../src/crypto-api";
import { SecretInfo } from "../../../src/secret-storage.ts";
async function makeTestClient( async function makeTestClient(
userInfo: { userId: string; deviceId: string }, 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 () { describe("Secrets", function () {
if (!globalThis.Olm) { if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present"); 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)])); return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)]));
} }
declare module "../../src/@types/event" {
interface AccountDataEvents {
"im.vector.test": {};
}
}
type HttpLookup = { type HttpLookup = {
method: string; method: string;
path: string; path: string;

View File

@@ -30,6 +30,7 @@ import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto"; import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto"; import { initRustCrypto } from "../../../src/rust-crypto";
import { import {
AccountDataEvents,
Device, Device,
DeviceVerification, DeviceVerification,
encodeBase64, encodeBase64,
@@ -1924,11 +1925,13 @@ class DummyAccountDataClient
super(); 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); const ret = this.storage.get(eventType);
if (eventType) { if (eventType) {
return ret as T; return ret;
} else { } else {
return null; return null;
} }

View File

@@ -19,6 +19,18 @@ import {
secretStorageContainsCrossSigningKeys, secretStorageContainsCrossSigningKeys,
} from "../../../src/rust-crypto/secret-storage"; } from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/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("secret-storage", () => {
describe("secretStorageContainsCrossSigningKeys", () => { describe("secretStorageContainsCrossSigningKeys", () => {

View File

@@ -27,6 +27,14 @@ import {
trimTrailingEquals, trimTrailingEquals,
} from "../../src/secret-storage"; } from "../../src/secret-storage";
import { randomString } from "../../src/randomstring"; 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("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () { describe(".addKey", function () {
@@ -117,9 +125,11 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1; 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") { if (eventType === "m.secret_storage.key.my_key") {
return storedKey as unknown as T; return storedKey as any;
} else { } else {
throw new Error(`unexpected eventType ${eventType}`); throw new Error(`unexpected eventType ${eventType}`);
} }
@@ -135,11 +145,13 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1; 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") { 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") { } else if (eventType === "m.secret_storage.key.default_key_id") {
return storedKey as unknown as T; return storedKey as any;
} else { } else {
throw new Error(`unexpected eventType ${eventType}`); throw new Error(`unexpected eventType ${eventType}`);
} }
@@ -236,9 +248,11 @@ describe("ServerSideSecretStorageImpl", function () {
// stub out getAccountData to return a key with an unknown algorithm // stub out getAccountData to return a key with an unknown algorithm
const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon; 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") { if (eventType === "m.secret_storage.key.keyid") {
return storedKey as unknown as T; return storedKey as any;
} else { } else {
throw new Error(`unexpected eventType ${eventType}`); throw new Error(`unexpected eventType ${eventType}`);
} }

View File

@@ -58,6 +58,10 @@ import {
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts"; import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts"; import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
import { SessionMembershipData } from "../matrixrtc/CallMembership.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 { export enum EventType {
// Room state events // Room state events
@@ -368,3 +372,33 @@ export interface StateEvents {
// MSC3672 // MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent; [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, UpdateDelayedEventAction,
} from "./@types/requests.ts"; } from "./@types/requests.ts";
import { import {
AccountDataEvents,
EventType, EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX, LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP, MSC3912_RELATION_BASED_REDACTIONS_PROP,
@@ -232,6 +233,7 @@ import {
import { DeviceInfoMap } from "./crypto/DeviceList.ts"; import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import { import {
AddSecretStorageKeyOpts, AddSecretStorageKeyOpts,
SecretStorageKey,
SecretStorageKeyDescription, SecretStorageKeyDescription,
ServerSideSecretStorage, ServerSideSecretStorage,
ServerSideSecretStorageImpl, ServerSideSecretStorageImpl,
@@ -3070,7 +3072,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}. * @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); return this.secretStorage.isStored(name);
} }
@@ -4236,7 +4238,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an empty object * @returns Promise which resolves: an empty object
* @returns Rejects: with an error response. * @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", { const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!, $userId: this.credentials.userId!,
$type: eventType, $type: eventType,
@@ -4251,7 +4256,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param eventType - The event type * @param eventType - The event type
* @returns The contents of the given account data event * @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); 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 Promise which resolves: The contents of the given account data event.
* @returns Rejects: with an error response. * @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()) { if (this.isInitialSyncComplete()) {
const event = this.store.getAccountData(eventType); const event = this.store.getAccountData(eventType);
if (!event) { if (!event) {
@@ -4271,7 +4278,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
// The network version below returns just the content, so this branch // The network version below returns just the content, so this branch
// does the same to match. // does the same to match.
return event.getContent<T>(); return event.getContent<AccountDataEvents[K]>();
} }
const path = utils.encodeUri("/user/$userId/account_data/$type", { const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!, $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); const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
// if deletion is not supported overwrite with empty content // if deletion is not supported overwrite with empty content
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) { 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) * @returns The array of users that are ignored (empty if none)
*/ */
public getIgnoredUsers(): string[] { public getIgnoredUsers(): string[] {
const event = this.getAccountData("m.ignored_user_list"); const event = this.getAccountData(EventType.IgnoredUserList);
if (!event?.getContent()["ignored_users"]) return []; if (!event?.getContent()["ignored_users"]) return [];
return Object.keys(event.getContent()["ignored_users"]); return Object.keys(event.getContent()["ignored_users"]);
} }
@@ -4326,7 +4333,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
userIds.forEach((u) => { userIds.forEach((u) => {
content.ignored_users[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, deviceId: string,
notificationSettings: LocalNotificationSettings, notificationSettings: LocalNotificationSettings,
): Promise<{}> { ): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`; const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
return this.setAccountData(key, notificationSettings); 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}`)) || {}); intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {});
} }
return Object.keys(stored).length ? stored : null; 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 { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage.ts"; import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage.ts";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api/index.ts"; import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api/index.ts";
import { AccountDataEvents } from "../@types/event.ts";
interface ICrossSigningKeys { interface ICrossSigningKeys {
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
@@ -111,7 +112,10 @@ export class EncryptionSetupBuilder {
userSignatures[deviceId] = signature; 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); await this.accountDataClientAdapter.setAccountData(type, content);
} }
@@ -160,7 +164,7 @@ export class EncryptionSetupOperation {
/** /**
*/ */
public constructor( public constructor(
private readonly accountData: Map<string, object>, private readonly accountData: Map<keyof AccountDataEvents, MatrixEvent>,
private readonly crossSigningKeys?: ICrossSigningKeys, private readonly crossSigningKeys?: ICrossSigningKeys,
private readonly keyBackupInfo?: IKeyBackupInfo, private readonly keyBackupInfo?: IKeyBackupInfo,
private readonly keySignatures?: KeySignatures, private readonly keySignatures?: KeySignatures,
@@ -190,7 +194,7 @@ export class EncryptionSetupOperation {
// set account data // set account data
if (this.accountData) { if (this.accountData) {
for (const [type, content] of 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 // upload first cross-signing signatures with the new key
@@ -236,7 +240,7 @@ class AccountDataClientAdapter
implements AccountDataClient implements AccountDataClient
{ {
// //
public readonly values = new Map<string, MatrixEvent>(); public readonly values = new Map<keyof AccountDataEvents, MatrixEvent>();
/** /**
* @param existingValues - existing account data * @param existingValues - existing account data
@@ -248,33 +252,26 @@ class AccountDataClientAdapter
/** /**
* @returns the content of the account data * @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)); return Promise.resolve(this.getAccountData(type));
} }
/** /**
* @returns the content of the account data * @returns the content of the account data
*/ */
public getAccountData<T extends { [k: string]: any }>(type: string): T | null { public getAccountData<K extends keyof AccountDataEvents>(type: K): AccountDataEvents[K] | null {
const modifiedValue = this.values.get(type); const event = this.values.get(type) ?? this.existingValues.get(type);
if (modifiedValue) { return event?.getContent<AccountDataEvents[K]>() ?? null;
return modifiedValue as unknown as T;
}
const existingValue = this.existingValues.get(type);
if (existingValue) {
return existingValue.getContent<T>();
}
return 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); const lastEvent = this.values.get(type);
this.values.set(type, content); this.values.set(type, event);
// ensure accountData is emitted on the next tick, // ensure accountData is emitted on the next tick,
// as SecretStorage listens for it while calling this method // as SecretStorage listens for it while calling this method
// and it seems to rely on this. // and it seems to rely on this.
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit(ClientEvent.AccountData, event, lastEvent); this.emit(ClientEvent.AccountData, event, lastEvent);
return {}; return {};
}); });

View File

@@ -25,12 +25,12 @@ import {
AccountDataClient, AccountDataClient,
ServerSideSecretStorage, ServerSideSecretStorage,
ServerSideSecretStorageImpl, ServerSideSecretStorageImpl,
SecretStorageKey,
} from "../secret-storage.ts"; } from "../secret-storage.ts";
import { ISecretRequest, SecretSharing } from "./SecretSharing.ts"; import { ISecretRequest, SecretSharing } from "./SecretSharing.ts";
/* re-exports for backwards compatibility */ /* re-exports for backwards compatibility */
export type { export type {
AccountDataClient as IAccountDataClient,
SecretStorageKeyTuple, SecretStorageKeyTuple,
SecretStorageKeyObject, SecretStorageKeyObject,
SECRET_STORAGE_ALGORITHM_V1_AES, 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 * 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); return this.storageImpl.store(name, secret, keys);
} }
/** /**
* Get a secret from storage. * Get a secret from storage.
*/ */
public get(name: string): Promise<string | undefined> { public get(name: SecretStorageKey): Promise<string | undefined> {
return this.storageImpl.get(name); return this.storageImpl.get(name);
} }
/** /**
* Check if a secret is stored on the server. * 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); return this.storageImpl.isStored(name);
} }

View File

@@ -77,6 +77,7 @@ import {
AddSecretStorageKeyOpts, AddSecretStorageKeyOpts,
calculateKeyCheck, calculateKeyCheck,
SECRET_STORAGE_ALGORITHM_V1_AES, SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorageKey,
SecretStorageKeyDescription, SecretStorageKeyDescription,
SecretStorageKeyObject, SecretStorageKeyObject,
SecretStorageKeyTuple, SecretStorageKeyTuple,
@@ -1194,21 +1195,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/** /**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#store}. * @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); return this.secretStorage.store(name, secret, keys);
} }
/** /**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}. * @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); return this.secretStorage.get(name);
} }
/** /**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}. * @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); 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. limitations under the License.
*/ */
import { UnstableValue } from "matrix-events-sdk";
import { MatrixClient } from "../client.ts"; import { MatrixClient } from "../client.ts";
import { IContent, MatrixEvent } from "./event.ts"; import { IContent, MatrixEvent } from "./event.ts";
import { EventTimeline } from "./event-timeline.ts"; import { EventTimeline } from "./event-timeline.ts";
@@ -23,47 +21,14 @@ import { Preset } from "../@types/partials.ts";
import { globToRegexp } from "../utils.ts"; import { globToRegexp } from "../utils.ts";
import { Room } from "./room.ts"; import { Room } from "./room.ts";
import { EventType, StateEvents } from "../@types/event.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. export { IGNORE_INVITES_ACCOUNT_EVENT_KEY, POLICIES_ACCOUNT_EVENT_TYPE, PolicyRecommendation, PolicyScope };
///
/// 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",
}
const scopeToEventTypeMap: Record<PolicyScope, keyof StateEvents> = { const scopeToEventTypeMap: Record<PolicyScope, keyof StateEvents> = {
[PolicyScope.User]: EventType.PolicyRuleUser, [PolicyScope.User]: EventType.PolicyRuleUser,

View File

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

View File

@@ -28,6 +28,7 @@ import { logger } from "./logger.ts";
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts"; import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts"; import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.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"; 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 * @param eventType - The type of account data
* @returns The contents of the given account data event, or `null` if the event is not found * @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 * 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 * @param content - the content object to be set
* @returns an empty object * @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>; ) => 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: { encrypted: {
[keyId: string]: AESEncryptedSecretStoragePayload; [keyId: string]: AESEncryptedSecretStoragePayload;
}; };
@@ -293,7 +304,7 @@ export interface ServerSideSecretStorage {
* with, or null if it is not present or not encrypted with a trusted * with, or null if it is not present or not encrypted with a trusted
* key * 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. * 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 * @returns The default key ID or null if no default key ID is set
*/ */
public async getDefaultKeyId(): Promise<string | null> { public async getDefaultKeyId(): Promise<string | null> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>( const defaultKey = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.default_key");
"m.secret_storage.default_key",
);
if (!defaultKey) return null; if (!defaultKey) return null;
return defaultKey.key; return defaultKey.key ?? null;
} }
/** /**
@@ -409,11 +418,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
if (!keyId) { if (!keyId) {
do { do {
keyId = randomString(32); keyId = randomString(32);
} while ( } while (await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`));
await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
`m.secret_storage.key.${keyId}`,
)
);
} }
await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo); await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
@@ -441,9 +446,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
return null; return null;
} }
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null; return keyInfo ? [keyId, keyInfo] : null;
} }
@@ -492,7 +495,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* @param secret - The secret contents. * @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. * @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> = {}; const encrypted: Record<string, AESEncryptedSecretStoragePayload> = {};
if (!keys) { if (!keys) {
@@ -509,9 +512,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
for (const keyId of keys) { for (const keyId of keys) {
// get key information from key storage // get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
"m.secret_storage.key." + keyId,
);
if (!keyInfo) { if (!keyInfo) {
throw new Error("Unknown key: " + keyId); 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 * @returns the decrypted contents of the secret, or "undefined" if `name` is not found in
* the user's account data. * the user's account data.
*/ */
public async get(name: string): Promise<string | undefined> { public async get(name: SecretStorageKey): Promise<string | undefined> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretInfo>(name); const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo) { if (!secretInfo) {
return; return;
} }
@@ -555,9 +556,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
const keys: Record<string, SecretStorageKeyDescriptionAesV1> = {}; const keys: Record<string, SecretStorageKeyDescriptionAesV1> = {};
for (const keyId of Object.keys(secretInfo.encrypted)) { for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage // get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
"m.secret_storage.key." + keyId,
);
const encInfo = secretInfo.encrypted[keyId]; const encInfo = secretInfo.encrypted[keyId];
// only use keys we understand the encryption algorithm of // only use keys we understand the encryption algorithm of
if (keyInfo?.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { 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 * with, or null if it is not present or not encrypted with a trusted
* key * 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 // check if secret exists
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretInfo>(name); const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo?.encrypted) return null; if (!secretInfo?.encrypted) return null;
const ret: Record<string, SecretStorageKeyDescriptionAesV1> = {}; const ret: Record<string, SecretStorageKeyDescriptionAesV1> = {};
@@ -600,9 +599,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
// filter secret encryption keys with supported algorithm // filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) { for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage // get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescriptionAesV1>( const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`);
"m.secret_storage.key." + keyId,
);
if (!keyInfo) continue; if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId]; const encInfo = secretInfo.encrypted[keyId];