diff --git a/src/@types/event.ts b/src/@types/event.ts index 9f43b4840cd..96206f90435 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -456,6 +456,13 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents { */ export type WritableAccountDataEvents = Exclude; +/** + * Subset of AccountDataEvents, only including events encryptable via MSC4483. + * + * Currently, this is none. + */ +export type EncryptableAccountDataEvents = {}; + /** * Mapped type from event type to content type for all specified global events encrypted by secret storage. * @@ -466,5 +473,6 @@ export interface SecretStorageAccountDataEvents { "m.cross_signing.master": SecretInfo; "m.cross_signing.self_signing": SecretInfo; "m.cross_signing.user_signing": SecretInfo; + "dev.zirco.msc4483.account_data.key": SecretInfo; "org.matrix.msc3814": SecretInfo; } diff --git a/src/client.ts b/src/client.ts index 884b49a6a2f..8212d188162 100644 --- a/src/client.ts +++ b/src/client.ts @@ -54,7 +54,7 @@ import { deepCompare, noUnsafeEventProps, type QueryDict, replaceParam, safeSet, import { Direction, EventTimeline } from "./models/event-timeline.ts"; import { type IActionsObject, PushProcessor } from "./pushprocessor.ts"; import { AutoDiscovery, type AutoDiscoveryAction } from "./autodiscovery.ts"; -import { encodeUnpaddedBase64Url } from "./base64.ts"; +import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64.ts"; import { TypedReEmitter } from "./ReEmitter.ts"; import { logger, type Logger } from "./logger.ts"; import { SERVICE_TYPES } from "./service-types.ts"; @@ -147,6 +147,7 @@ import { UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, type WritableAccountDataEvents, + type EncryptableAccountDataEvents, } from "./@types/event.ts"; import { GuestAccess, @@ -247,6 +248,10 @@ import { type EmptyObject } from "./@types/common.ts"; import { UnsupportedDelayedEventsEndpointError, UnsupportedStickyEventsEndpointError } from "./errors.ts"; import { type Transport } from "./matrixrtc/index.ts"; import { RetentionPolicyService } from "./retentionPolicy.ts"; +import { randomBytes } from "node:crypto"; +import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts"; +import { AESEncryptedSecretStoragePayload } from "./types.ts"; +import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts"; export type Store = IStore; @@ -2366,6 +2371,57 @@ export class MatrixClient extends TypedEventEmitter> { + const ADK_ID = "dev.zirco.msc4483.account_data.key"; + + let key = await this.secretStorage.get(ADK_ID); + + if (!key) { + throw new Error("Account data key not found in secret storage"); + } + + return decodeBase64(key); + } + + public async getOrCreateAccountDataKey(): Promise> { + // get the ADK secret from secret storage, or create it if it doesn't exist + const ADK_ID = "dev.zirco.msc4483.account_data.key"; + + let key = await this.secretStorage.get(ADK_ID); + + if (!key) { + // generate a new 256 byte secret, base64 encode it, and store it in 4S + key = encodeBase64(randomBytes(32)); + + await this.secretStorage.store(ADK_ID, key); + } + + return decodeBase64(key); + } + + public async getEncryptedAccountData( + eventType: K, + ): Promise { + const key = await this.getAccountDataKey(); + + const event = await this.getAccountDataFromServer(eventType) as never as AESEncryptedSecretStoragePayload; + if (!event) { + return null; + } + + const decrypted = await decryptAESSecretStorageItem(event, key, eventType); + return decrypted as EncryptableAccountDataEvents[K]; + } + + public async setEncryptedAccountData( + eventType: K, + content: EncryptableAccountDataEvents[K], + ): Promise { + const key = await this.getOrCreateAccountDataKey(); + const encrypted = await encryptAESSecretStorageItem(content, key, eventType); + await this.setAccountData(eventType, encrypted as never); + } + /** * Gets the users that are ignored by this client * @returns The array of users that are ignored (empty if none)