diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 4fa56af549..8aa5f2f2cb 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -30,7 +30,6 @@ "@river-build/dlog": "workspace:^", "@river-build/encryption": "workspace:^", "@river-build/generated": "workspace:^", - "@river-build/mls-rs-wasm": "^0.0.16", "@river-build/proto": "workspace:^", "@river-build/web3": "workspace:^", "browser-or-node": "^3.0.0", diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index f67231cdf6..804969ed8e 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -28,7 +28,6 @@ import { UserBio, Tags, BlockchainTransaction, - MemberPayload_Mls, MiniblockHeader, GetStreamResponse, CreateStreamResponse, @@ -139,7 +138,6 @@ import { make_UserPayload_BlockchainTransaction, ContractReceipt, make_MemberPayload_EncryptionAlgorithm, - make_MemberPayload_Mls, } from './types' import debug from 'debug' @@ -161,8 +159,6 @@ import { SignerContext } from './signerContext' import { decryptAESGCM, deriveKeyAndIV, encryptAESGCM, uint8ArrayToBase64 } from './crypto_utils' import { makeTags, makeTipTags } from './tags' import { TipEventObject } from '@river-build/generated/dev/typings/ITipping' -import { extractMlsExternalGroup, ExtractMlsExternalGroupResult } from './mls/utils/mlsutils' -import { MlsAdapter, MLS_ALGORITHM } from './mls' export type ClientEvents = StreamEvents & DecryptionEvents @@ -211,7 +207,6 @@ export class Client private entitlementsDelegate: EntitlementsDelegate private decryptionExtensions?: BaseDecryptionExtensions private syncedStreamsExtensions?: SyncedStreamsExtension - private mlsAdapter?: MlsAdapter private persistenceStore: IPersistenceStore private validatedEvents: Record = {} private defaultGroupEncryptionAlgorithm: GroupEncryptionAlgorithmId @@ -274,7 +269,6 @@ export class Client highPriorityStreamIds, { startSyncStreams: async () => { - this.mlsAdapter?.start() this.streams.startSyncStreams() this.decryptionExtensions?.start() }, @@ -389,7 +383,6 @@ export class Client encryptionDeviceInit?: EncryptionDeviceInitOpts }): Promise<{ initCryptoTime: number - //initMlsTime: number initUserStreamTime: number initUserInboxStreamTime: number initUserMetadataStreamTime: number @@ -405,11 +398,9 @@ export class Client this.logCall('initializeUser', this.userId) assert(this.userStreamId === undefined, 'already initialized') const initCrypto = await getTime(() => this.initCrypto(opts?.encryptionDeviceInit)) - //const initMls = await getTime(() => this.initMls()) check(isDefined(this.decryptionExtensions), 'decryptionExtensions must be defined') check(isDefined(this.syncedStreamsExtensions), 'syncedStreamsExtensions must be defined') - //check(isDefined(this.mlsAdapter), 'mlsAdapter must be defined') const [ initUserStream, @@ -435,7 +426,6 @@ export class Client return { initCryptoTime: initCrypto.time, - //initMlsTime: initMls.time, initUserStreamTime: initUserStream.time, initUserInboxStreamTime: initUserInboxStream.time, initUserMetadataStreamTime: initUserMetadataStream.time, @@ -978,13 +968,13 @@ export class Client check(isDefined(stream), 'stream not found') check( stream.view.membershipContent.encryptionAlgorithm != encryptionAlgorithm, - `mlsEnabled is already set to ${encryptionAlgorithm}`, + `encryptionAlgorithm is already set to ${encryptionAlgorithm}`, ) return this.makeEventAndAddToStream( streamId, make_MemberPayload_EncryptionAlgorithm(encryptionAlgorithm), { - method: 'setMlsEnabled', + method: 'setStreamEncryptionAlgorithm', }, ) } @@ -1620,9 +1610,6 @@ export class Client let message: EncryptedData const encryptionAlgorithm = stream.view.membershipContent.encryptionAlgorithm switch (encryptionAlgorithm) { - case MLS_ALGORITHM: - message = await this.encryptGroupEventEpochSecret(payload, streamId) - break case GroupEncryptionAlgorithmId.HybridGroupEncryption: message = await this.encryptGroupEvent( payload, @@ -2470,18 +2457,6 @@ export class Client ) } - /// Initialise MLS but do not start it - private async initMls(): Promise { - this.logCall('initMls') - if (this.mlsAdapter) { - this.logCall('Attempt to re-init mls adapter, ignoring') - return - } - - this.mlsAdapter = new MlsAdapter(this) - await this.mlsAdapter.initialize() - } - /** * Resets crypto backend and creates a new encryption account, uploading device keys to UserDeviceKey stream. */ @@ -2711,36 +2686,6 @@ export class Client public async debugDropStream(syncId: string, streamId: string): Promise { await this.rpcClient.info({ debug: ['drop_stream', syncId, streamId] }) } - - public async _debugSendMls( - streamId: string | Uint8Array, - payload: PlainMessage, - ) { - return this.makeEventAndAddToStream(streamId, make_MemberPayload_Mls(payload), { - method: 'mls', - }) - } - - public async getMlsExternalGroupInfo( - streamId: string, - ): Promise { - let streamView = this.stream(streamId)?.view - if (!streamView || !streamView.isInitialized) { - streamView = await this.getStream(streamId) - } - check(isDefined(streamView), `stream not found: ${streamId}`) - return extractMlsExternalGroup(streamView) - } - - private async encryptGroupEventEpochSecret( - payload: Message, - streamId: string, - ): Promise { - if (this.mlsAdapter === undefined) { - throw new Error('mls adapter not initialized') - } - return this.mlsAdapter.encryptGroupEventEpochSecret(streamId, payload) - } } function ensureNoHexPrefix(value: string): string { diff --git a/packages/sdk/src/mls/adapter.ts b/packages/sdk/src/mls/adapter.ts deleted file mode 100644 index 17b2cf8d0a..0000000000 --- a/packages/sdk/src/mls/adapter.ts +++ /dev/null @@ -1,64 +0,0 @@ -/// Adapter to hook MLS Coordinator to the client - -import { Client } from '../client' -import { Message } from '@bufbuild/protobuf' -import { EncryptedData } from '@river-build/proto' -import { DLogger, dlog } from '@river-build/dlog' -import { isMobileSafari } from '../utils' -import { IQueueService } from './queue' - -const defaultLogger = dlog('csb:mls:adapter') - -export class MlsAdapter { - private client: Client - private queueService?: IQueueService - private log!: { - error: DLogger - debug: DLogger - } - - public constructor(client: Client, opts?: { log: DLogger }) { - this.client = client - const logger = opts?.log ?? defaultLogger - this.log = { - debug: logger.extend('debug'), - error: logger.extend('error'), - } - } - - // API exposed to the client - public initialize(): Promise { - this.log.debug('initialize') - return Promise.resolve() - } - - public start(): void { - this.log.debug('start') - this.queueService?.start() - if (isMobileSafari() && this.queueService) { - document.addEventListener( - 'visibilitychange', - this.queueService.onMobileSafariPageVisibilityChanged, - ) - } - } - - public async stop(): Promise { - this.log.debug('stop') - await this.queueService?.stop() - if (isMobileSafari() && this.queueService) { - document.removeEventListener( - 'visibilitychange', - this.queueService.onMobileSafariPageVisibilityChanged, - ) - } - } - - public async encryptGroupEventEpochSecret( - _streamId: string, - _event: Message, - ): Promise { - this.log.debug('encryptGroupEventEpochSecret') - throw new Error('Not implemented') - } -} diff --git a/packages/sdk/src/mls/constants.ts b/packages/sdk/src/mls/constants.ts deleted file mode 100644 index e433b22d55..0000000000 --- a/packages/sdk/src/mls/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const MLS_ALGORITHM = 'mls_0.0.1' diff --git a/packages/sdk/src/mls/coordinator/awaiter.ts b/packages/sdk/src/mls/coordinator/awaiter.ts deleted file mode 100644 index 651c2fd31d..0000000000 --- a/packages/sdk/src/mls/coordinator/awaiter.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface IAwaiter { - promise: Promise - resolve: () => void -} - -export class NoopAwaiter implements IAwaiter { - public promise = Promise.resolve() - public resolve = () => {} -} - -export class IndefiniteAwaiter implements IAwaiter { - public promise: Promise - public resolve!: () => void - - public constructor() { - this.promise = new Promise((resolve) => { - this.resolve = resolve - }) - } -} - -export class TimeoutAwaiter implements IAwaiter { - // top level promise - public promise: Promise - // resolve handler to the inner promise - public resolve!: () => void - public constructor(timeoutMS: number, msg: string = 'Awaiter timed out') { - let timeout: NodeJS.Timeout - const timeoutPromise = new Promise((_resolve, reject) => { - timeout = setTimeout(() => { - reject(new Error(msg)) - }, timeoutMS) - }) - const internalPromise: Promise = new Promise( - (resolve: (value: void) => void, _reject) => { - this.resolve = () => { - resolve() - } - }, - ).finally(() => { - clearTimeout(timeout) - }) - this.promise = Promise.race([internalPromise, timeoutPromise]) - } -} diff --git a/packages/sdk/src/mls/coordinator/coordinator.ts b/packages/sdk/src/mls/coordinator/coordinator.ts deleted file mode 100644 index d537ec01a6..0000000000 --- a/packages/sdk/src/mls/coordinator/coordinator.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { Message, PlainMessage } from '@bufbuild/protobuf' -import { - EncryptedData, - MemberPayload_Mls_EpochSecrets, - MemberPayload_Mls_ExternalJoin, - MemberPayload_Mls_InitializeGroup, - StreamEvent, -} from '@river-build/proto' -import { GroupService, InMemoryGroupStore, Crypto } from '../group' -import { EpochSecret, EpochSecretService, InMemoryEpochSecretStore } from '../epoch' -import { ExternalCrypto, ExternalGroupService } from '../externalGroup' -import { check, dlog, DLogger } from '@river-build/dlog' -import { isDefined } from '../../check' -import { EncryptedContentItem } from '@river-build/encryption' -import { - EncryptedContent, - isEncryptedContentKind, - toDecryptedContent, -} from '../../encryptedContentTypes' -import { Client } from '../../client' -import { IPersistenceStore } from '../../persistenceStore' -import { IAwaiter, IndefiniteAwaiter } from './awaiter' -import { - IQueueService, - QueueService, - EpochSecretServiceCoordinatorAdapter, - GroupServiceCoordinatorAdapter, -} from '../queue' -import { addressFromUserId } from '../../id' -import { make_MemberPayload_Mls } from '../../types' - -type InitializeGroupMessage = PlainMessage -type ExternalJoinMessage = PlainMessage -type EpochSecretsMessage = PlainMessage - -type MlsEncryptedContentItem = { - streamId: string - eventId: string - kind: string - encryptedData: EncryptedData -} - -export interface ICoordinator { - // Commands - joinOrCreateGroup(streamId: string): Promise - groupActive(streamId: string): void - newOpenEpochSecret(streamId: string, epoch: bigint): Promise - newSealedEpochSecret(streamId: string, epoch: bigint): Promise - announceEpochSecret(streamId: string, epoch: bigint): Promise - // Events - handleInitializeGroup(streamId: string, message: InitializeGroupMessage): Promise - handleExternalJoin(streamId: string, message: ExternalJoinMessage): Promise - handleEpochSecrets(streamId: string, message: EpochSecretsMessage): Promise - handleEncryptedContent( - streamId: string, - eventId: string, - message: EncryptedContent, - ): Promise -} - -const defaultLogger = dlog('csb:mls:coordinator') - -export class Coordinator implements ICoordinator { - private userId: string - private readonly userAddress: Uint8Array - private readonly deviceKey: Uint8Array - private epochSecretService: EpochSecretService - private groupService: GroupService - private externalGroupService: ExternalGroupService - private decryptionFailures: Map> = new Map() - private client!: Client - private persistenceStore!: IPersistenceStore - private awaitingGroupActive: Map = new Map() - private readonly queueService: IQueueService - - private log: { - error: DLogger - debug: DLogger - } - - constructor( - userId: string, - deviceKey: Uint8Array, - client: Client, - persistenceStore: IPersistenceStore, - opts?: { log: DLogger }, - ) { - this.client = client - this.persistenceStore = persistenceStore - const logger = opts?.log ?? defaultLogger - this.log = { - debug: logger.extend('debug'), - error: logger.extend('error'), - } - - this.userId = userId - this.userAddress = addressFromUserId(userId) - this.deviceKey = deviceKey - - // Composing all the dependencies - this.queueService = new QueueService(this) - this.externalGroupService = new ExternalGroupService(new ExternalCrypto(), opts) - const groupStore = new InMemoryGroupStore() - const crypto = new Crypto(this.userAddress, this.deviceKey, opts) - const groupServiceCoordinator = new GroupServiceCoordinatorAdapter(this.queueService) - - this.groupService = new GroupService(groupStore, crypto, groupServiceCoordinator, opts) - const epochSecretStore = new InMemoryEpochSecretStore() - const epochSecretServiceCoordinator = new EpochSecretServiceCoordinatorAdapter( - this.queueService, - ) - this.epochSecretService = new EpochSecretService( - crypto.ciphersuite(), - epochSecretStore, - epochSecretServiceCoordinator, - opts, - ) - } - - // API needed by the client - // TODO: How long will be the timeout here? - public async encryptGroupEventEpochSecret( - streamId: string, - event: Message, - ): Promise { - const hasGroup = this.groupService.getGroup(streamId) !== undefined - if (!hasGroup) { - // No group so we request joining - // NOTE: We are enqueueing command instead of doing the async call - this.queueService?.enqueueCommand({ tag: 'joinOrCreateGroup', streamId }) - } - // Wait for the group to become active - await this.awaitGroupActive(streamId) - const activeGroup = this.groupService.getGroup(streamId) - if (activeGroup === undefined) { - throw new Error('Fatal: no group after awaitGroupActive') - } - - if (activeGroup.status !== 'GROUP_ACTIVE') { - throw new Error('Fatal: group is not active') - } - - const epoch = this.groupService.currentEpoch(activeGroup) - - let epochSecret = this.epochSecretService.getEpochSecret(streamId, epoch) - - if (epochSecret === undefined) { - // NOTE: queue has not processed new epoch event yet, force it manually - await this.newEpoch(streamId, epoch) - epochSecret = this.epochSecretService.getEpochSecret(streamId, epoch) - if (epochSecret === undefined) { - throw new Error('Fatal: epoch secret not found') - } - } - - const plainbytes = event.toBinary() - - return this.epochSecretService.encryptMessage(epochSecret, plainbytes) - } - - // TODO: Maybe this could be refactored into a separate class - private async decryptGroupEvent( - epochSecret: EpochSecret, - streamId: string, - eventId: string, - kind: string, // kind of data - encryptedData: EncryptedData, - ) { - // this.logCall('decryptGroupEvent', streamId, eventId, kind, - // encryptedData) - const stream = this.client.stream(streamId) - check(isDefined(stream), 'stream not found') - check(isEncryptedContentKind(kind), `invalid kind ${kind}`) - - // check cache - let cleartext = await this.persistenceStore.getCleartext(eventId) - if (cleartext === undefined) { - cleartext = await this.epochSecretService.decryptMessage(epochSecret, encryptedData) - } - const decryptedContent = toDecryptedContent(kind, encryptedData.version, cleartext) - - stream.updateDecryptedContent(eventId, decryptedContent) - } - - // # MLS Coordinator # - - public async handleInitializeGroup(streamId: string, message: InitializeGroupMessage) { - const group = this.groupService.getGroup(streamId) - if (group !== undefined) { - await this.groupService.handleInitializeGroup(group, message) - } - } - - public async handleExternalJoin(streamId: string, message: ExternalJoinMessage) { - const group = this.groupService.getGroup(streamId) - if (group !== undefined) { - await this.groupService.handleExternalJoin(group, message) - } - } - - public async handleEpochSecrets(streamId: string, message: EpochSecretsMessage) { - return this.epochSecretService.handleEpochSecrets(streamId, message) - } - - public async handleEncryptedContent( - streamId: string, - eventId: string, - message: EncryptedContent, - ) { - const encryptedData = message.content - // TODO: Check if message was encrypted with MLS - // const ciphertext = encryptedData.mls!.ciphertext - const epoch = encryptedData.mls!.epoch - const kind = message.kind - - const epochSecret = this.epochSecretService.getEpochSecret(streamId, epoch) - if (epochSecret === undefined) { - this.log.debug('Epoch secret not found', { streamId, epoch }) - this.enqueueDecryptionFailure(streamId, epoch, { - streamId, - eventId, - kind, - encryptedData, - }) - return - } - - // Decrypt immediately - return this.decryptGroupEvent(epochSecret, streamId, eventId, kind, encryptedData) - } - - private enqueueDecryptionFailure(streamId: string, epoch: bigint, item: EncryptedContentItem) { - let perStream = this.decryptionFailures.get(streamId) - if (perStream === undefined) { - perStream = new Map() - this.decryptionFailures.set(item.streamId, perStream) - } - let perEpoch = perStream.get(epoch) - if (perEpoch === undefined) { - perEpoch = [] - perStream.set(epoch, perEpoch) - } - perEpoch.push(item) - } - - private async initializeGroupMessage(streamId: string): Promise { - // TODO: Check preconditions - // TODO: Catch the error - return this.groupService.initializeGroupMessage(streamId) - } - - private async externalJoinMessage( - streamId: string, - externalInfo: { - externalGroupSnapshot: Uint8Array - groupInfoMessage: Uint8Array - commits: { commit: Uint8Array; groupInfoMessage: Uint8Array }[] - }, - ): Promise { - const externalGroup = await this.externalGroupService.loadSnapshot( - streamId, - externalInfo.externalGroupSnapshot, - ) - for (const commit of externalInfo.commits) { - await this.externalGroupService.processCommit(externalGroup, commit.commit) - } - const exportedTree = this.externalGroupService.exportTree(externalGroup) - const latestGroupInfo = externalInfo.groupInfoMessage - - return this.groupService.externalJoinMessage(streamId, latestGroupInfo, exportedTree) - } - - private async epochSecretsMessage(epochSecret: EpochSecret): Promise { - // TODO: Check preconditions - return this.epochSecretService.epochSecretMessage(epochSecret) - } - - public async joinOrCreateGroup(streamId: string): Promise { - const hasGroup = this.groupService.getGroup(streamId) !== undefined - if (hasGroup) { - return - } - const externalInfo = await this.client.getMlsExternalGroupInfo(streamId) - - let joinOrCreateGroupMessage: PlainMessage['payload'] - - if (externalInfo === undefined) { - const initializeGroupMessage = await this.initializeGroupMessage(streamId) - joinOrCreateGroupMessage = make_MemberPayload_Mls({ - content: { - case: 'initializeGroup', - value: initializeGroupMessage, - }, - }) - } else { - const externalJoinMessage = await this.externalJoinMessage(streamId, externalInfo) - joinOrCreateGroupMessage = make_MemberPayload_Mls({ - content: { - case: 'externalJoin', - value: externalJoinMessage, - }, - }) - } - - // Send message to the node - try { - await this.client.makeEventAndAddToStream(streamId, joinOrCreateGroupMessage) - } catch (e) { - this.log.error('Failed to join or create group', { streamId, error: e }) - if (this.groupService.getGroup(streamId) !== undefined) { - await this.groupService.clearGroup(streamId) - } - } - } - - // NOTE: Critical section, no awaits permitted - public awaitGroupActive(streamId: string): Promise { - // this.log(`awaitGroupActive ${streamId}`) - if (this.groupService.getGroup(streamId)?.status === 'GROUP_ACTIVE') { - return Promise.resolve() - } - - let awaiter = this.awaitingGroupActive.get(streamId) - if (awaiter === undefined) { - const internalAwaiter = new IndefiniteAwaiter() - // NOTE: we clear after the promise has resolved - const promise = internalAwaiter.promise.finally(() => { - this.awaitingGroupActive.delete(streamId) - }) - awaiter = { - promise, - resolve: internalAwaiter.resolve, - } - this.awaitingGroupActive.set(streamId, awaiter) - } - - return awaiter.promise - } - - public groupActive(streamId: string): void { - const awaiter = this.awaitingGroupActive.get(streamId) - if (awaiter !== undefined) { - awaiter.resolve() - } - } - - public async newEpoch(streamId: string, epoch: bigint): Promise { - const epochAlreadyProcessed = - this.epochSecretService.getEpochSecret(streamId, epoch) !== undefined - if (epochAlreadyProcessed) { - return - } - - const group = this.groupService.getGroup(streamId) - if (group === undefined) { - throw new Error('Fatal: newEpoch called for missing group') - } - - if (group.status !== 'GROUP_ACTIVE') { - throw new Error('Fatal: newEpoch called for non-active group') - } - - if (this.groupService.currentEpoch(group) !== epoch) { - throw new Error('Fatal: newEpoch called for wrong epoch') - } - - const epochSecret = await this.groupService.exportEpochSecret(group) - await this.epochSecretService.addOpenEpochSecret(streamId, epoch, epochSecret) - this.queueService?.enqueueCommand({ tag: 'announceEpochSecret', streamId, epoch }) - } - - public async newOpenEpochSecret(streamId: string, _epoch: bigint): Promise { - const epochSecret = this.epochSecretService.getEpochSecret(streamId, _epoch) - if (epochSecret === undefined) { - throw new Error('Fatal: newEpochSecret called for missing epoch secret') - } - - if (epochSecret.derivedKeys === undefined) { - throw new Error('Fatal: missing derived keys for open secret') - } - - // TODO: Decrypt all messages for that particular epoch secret - const perStream = this.decryptionFailures.get(streamId) - if (perStream !== undefined) { - const perEpoch = perStream.get(_epoch) - if (perEpoch !== undefined) { - perStream.delete(_epoch) - // TODO: Can this be Promise.all? - for (const decryptionFailure of perEpoch) { - await this.decryptGroupEvent( - epochSecret, - decryptionFailure.streamId, - decryptionFailure.eventId, - decryptionFailure.kind, - decryptionFailure.encryptedData, - ) - } - } - } - - const previousEpochSecret = this.epochSecretService.getEpochSecret(streamId, _epoch - 1n) - if ( - previousEpochSecret !== undefined && - this.epochSecretService.canBeOpened(previousEpochSecret) - ) { - await this.epochSecretService.openSealedEpochSecret( - previousEpochSecret, - epochSecret.derivedKeys, - ) - } - } - - public async newSealedEpochSecret(streamId: string, epoch: bigint): Promise { - const epochSecret = this.epochSecretService.getEpochSecret(streamId, epoch) - if (epochSecret === undefined) { - throw new Error('Fatal: newSealedEpochSecret called for missing epoch secret') - } - - if (epochSecret.sealedEpochSecret === undefined) { - throw new Error('Fatal: missing sealed secret for sealed secret') - } - - // TODO: Maybe this can be Promise.all? - await this.tryOpeningSealedEpochSecret(epochSecret) - await this.tryAnnouncingSealedEpochSecret(epochSecret) - } - - private async tryOpeningSealedEpochSecret(sealedEpochSecret: EpochSecret): Promise { - if (sealedEpochSecret.sealedEpochSecret === undefined) { - throw new Error('Fatal: tryOpeningSealedEpochSecret called for missing sealed secret') - } - - // Already open - if (sealedEpochSecret.openEpochSecret !== undefined) { - return - } - - // Missing derived keys needed to open - const nextEpochSecret = this.epochSecretService.getEpochSecret( - sealedEpochSecret.streamId, - sealedEpochSecret.epoch + 1n, - ) - if (nextEpochSecret?.derivedKeys === undefined) { - return - } - - return this.epochSecretService.openSealedEpochSecret( - sealedEpochSecret, - nextEpochSecret.derivedKeys, - ) - } - - public async announceEpochSecret(_streamId: string, _epoch: bigint) { - let epochSecret = this.epochSecretService.getEpochSecret(_streamId, _epoch) - if (epochSecret === undefined) { - throw new Error('Fatal: announceEpochSecret called for missing epoch secret') - } - - if (epochSecret.sealedEpochSecret === undefined) { - const nextEpochKey = this.epochSecretService.getEpochSecret( - epochSecret.streamId, - epochSecret.epoch + 1n, - ) - if (nextEpochKey?.derivedKeys !== undefined) { - await this.epochSecretService.sealEpochSecret(epochSecret, nextEpochKey.derivedKeys) - epochSecret = this.epochSecretService.getEpochSecret( - epochSecret.streamId, - epochSecret.epoch, - ) - if (epochSecret === undefined) { - throw new Error('Fatal: epoch secret not found after sealing') - } - } - } - - return this.tryAnnouncingSealedEpochSecret(epochSecret) - } - - private async tryAnnouncingSealedEpochSecret(epochSecret: EpochSecret): Promise { - const streamId = epochSecret.streamId - const epoch = epochSecret.epoch - - if (epochSecret.sealedEpochSecret === undefined) { - throw new Error('Fatal: announceSealedEpoch called for missing sealed secret') - } - - if (epochSecret.announced) { - return - } - - const epochSecretsMessage = await this.epochSecretsMessage(epochSecret) - - try { - await this.client.makeEventAndAddToStream( - streamId, - make_MemberPayload_Mls({ - content: { - case: 'epochSecrets', - value: epochSecretsMessage, - }, - }), - ) - } catch (e) { - this.log.error('Failed to announce epoch secret', { streamId, epoch, error: e }) - this.queueService.enqueueCommand({ tag: 'announceEpochSecret', streamId, epoch }) - } - } -} diff --git a/packages/sdk/src/mls/coordinator/index.ts b/packages/sdk/src/mls/coordinator/index.ts deleted file mode 100644 index df7c80d9f0..0000000000 --- a/packages/sdk/src/mls/coordinator/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './awaiter' -export * from './coordinator' diff --git a/packages/sdk/src/mls/epoch/epochSecret.ts b/packages/sdk/src/mls/epoch/epochSecret.ts deleted file mode 100644 index 45a37689ba..0000000000 --- a/packages/sdk/src/mls/epoch/epochSecret.ts +++ /dev/null @@ -1,38 +0,0 @@ -export type DerivedKeys = { - secretKey: Uint8Array - publicKey: Uint8Array -} - -export class EpochSecret { - private constructor( - public readonly streamId: string, - public readonly epoch: bigint, - public readonly openEpochSecret?: Uint8Array, - public readonly sealedEpochSecret?: Uint8Array, - public readonly derivedKeys?: DerivedKeys, - public readonly announced?: boolean, - ) {} - - public static fromSealedEpochSecret( - streamId: string, - epoch: bigint, - sealedEpochSecret: Uint8Array, - ): EpochSecret { - return new EpochSecret(streamId, epoch, undefined, sealedEpochSecret, undefined, true) - } - - public static fromOpenEpochSecret( - streamId: string, - epoch: bigint, - openEpochSecret: Uint8Array, - derivedKeys: DerivedKeys, - ): EpochSecret { - return new EpochSecret(streamId, epoch, openEpochSecret, undefined, derivedKeys, false) - } -} - -export type EpochSecretId = string & { __brand: 'EpochKeyId' } - -export function epochSecretId(streamId: string, epoch: bigint): EpochSecretId { - return `${streamId}/${epoch}` as EpochSecretId -} diff --git a/packages/sdk/src/mls/epoch/epochSecretService.ts b/packages/sdk/src/mls/epoch/epochSecretService.ts deleted file mode 100644 index 15086a168d..0000000000 --- a/packages/sdk/src/mls/epoch/epochSecretService.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { - CipherSuite as MlsCipherSuite, - HpkeCiphertext, - HpkePublicKey, - HpkeSecretKey, - Secret as MlsSecret, -} from '@river-build/mls-rs-wasm' -import { bin_toHexString, dlog, DLogger, shortenHexString } from '@river-build/dlog' -import { DerivedKeys, EpochSecret, EpochSecretId, epochSecretId } from './epochSecret' -import { - EncryptedData, - EncryptedDataVersion, - MemberPayload_Mls_EpochSecrets, -} from '@river-build/proto' -import { IEpochSecretStore } from './epochSecretStore' -import { PlainMessage } from '@bufbuild/protobuf' -import { MLS_ALGORITHM } from '../constants' - -type EpochSecretsMessage = PlainMessage - -export interface IEpochSecretServiceCoordinator { - newOpenEpochSecret(streamId: string, epoch: bigint): void - newSealedEpochSecret(streamId: string, epoch: bigint): void -} - -const defaultLogger = dlog('csb:mls:epochSecretService') - -export class EpochSecretService { - private epochSecretStore: IEpochSecretStore - private cipherSuite: MlsCipherSuite - private cache: Map = new Map() - private coordinator?: IEpochSecretServiceCoordinator - private log: { - error: DLogger - debug: DLogger - } - - public constructor( - cipherSuite: MlsCipherSuite, - epochSecretStore: IEpochSecretStore, - coordinator?: IEpochSecretServiceCoordinator, - opts?: { log: DLogger }, - ) { - this.cipherSuite = cipherSuite - this.epochSecretStore = epochSecretStore - this.coordinator = coordinator - const logger = opts?.log ?? defaultLogger - this.log = { - debug: logger.extend('debug'), - error: logger.extend('error'), - } - } - - /// Gets epochKey from the cache - public getEpochSecret(streamId: string, epoch: bigint): EpochSecret | undefined { - const epochId: EpochSecretId = epochSecretId(streamId, epoch) - return this.cache.get(epochId) - } - - /// Loads epoch secret from storage and rehydrates the cache - public async loadEpochSecret( - streamId: string, - epoch: bigint, - ): Promise { - const epochKey = await this.epochSecretStore.getEpochSecret(streamId, epoch) - const epochId = epochSecretId(streamId, epoch) - if (epochKey) { - this.cache.set(epochId, epochKey) - } else { - this.cache.delete(epochId) - } - return epochKey - } - - private async saveEpochSecret(epochSecret: EpochSecret): Promise { - this.log.debug('saveEpochSecret', { - streamId: epochSecret.streamId, - epoch: epochSecret.epoch, - }) - this.cache.set(epochSecretId(epochSecret.streamId, epochSecret.epoch), epochSecret) - await this.epochSecretStore.setEpochSecret(epochSecret) - } - - /// Seal epoch secret - public async sealEpochSecret( - epochKey: EpochSecret, - { publicKey }: { publicKey: Uint8Array }, - ): Promise { - this.log.debug('sealEpochSecret', { - streamId: epochKey.streamId, - epoch: epochKey.epoch, - publicKey: shortenHexString(bin_toHexString(publicKey)), - }) - - if (epochKey.openEpochSecret === undefined) { - throw new Error(`Epoch secret not open: ${epochKey.streamId} ${epochKey.epoch}`) - } - if (epochKey.sealedEpochSecret !== undefined) { - throw new Error(`Epoch secret already sealed: ${epochKey.streamId} ${epochKey.epoch}`) - } - - const publicKey_ = HpkePublicKey.fromBytes(publicKey) - const sealedEpochSecret = ( - await this.cipherSuite.seal(publicKey_, epochKey.openEpochSecret) - ).toBytes() - const updatedEpochKey = { ...epochKey, sealedEpochSecret } - // TODO: Should this method store epochKey? - await this.saveEpochSecret(updatedEpochKey) - } - - // TODO: Refactor this one not to perform load - public async addAnnouncedSealedEpochSecret( - streamId: string, - epoch: bigint, - sealedEpochSecret: Uint8Array, - ): Promise { - this.log.debug('addSealedEpochSecret', { - streamId, - epoch, - sealedEpochSecretBytes: shortenHexString(bin_toHexString(sealedEpochSecret)), - }) - let epochSecret = await this.epochSecretStore.getEpochSecret(streamId, epoch) - if (!epochSecret) { - epochSecret = EpochSecret.fromSealedEpochSecret(streamId, epoch, sealedEpochSecret) - } else { - epochSecret = { ...epochSecret, sealedEpochSecret, announced: true } - } - // TODO: Should this method store epochKey? - await this.saveEpochSecret(epochSecret) - this.coordinator?.newSealedEpochSecret(streamId, epoch) - } - - // TODO: Should this method persist the epoch secret? - public async addOpenEpochSecret( - streamId: string, - epoch: bigint, - openEpochSecret: Uint8Array, - ): Promise { - this.log.debug('addOpenEpochSecret', { - streamId, - epoch, - openEpochSecret: shortenHexString(bin_toHexString(openEpochSecret)), - }) - const openEpochSecret_ = MlsSecret.fromBytes(openEpochSecret) - const derivedKeys_ = await this.cipherSuite.kemDerive(openEpochSecret_) - const derivedKeys = { - publicKey: derivedKeys_.publicKey.toBytes(), - secretKey: derivedKeys_.secretKey.toBytes(), - } - - let epochSecret = await this.epochSecretStore.getEpochSecret(streamId, epoch) - if (!epochSecret) { - epochSecret = EpochSecret.fromOpenEpochSecret( - streamId, - epoch, - openEpochSecret, - derivedKeys, - ) - } else { - epochSecret = { ...epochSecret, openEpochSecret, derivedKeys } - } - // TODO: Should this method store epochKey - await this.saveEpochSecret(epochSecret) - this.coordinator?.newOpenEpochSecret(streamId, epoch) - } - - public async openSealedEpochSecret( - epochSecret: EpochSecret, - nextEpochKeys: DerivedKeys, - ): Promise { - this.log.debug('openSealedEpochSecret', { - streamId: epochSecret.streamId, - epoch: epochSecret.epoch, - }) - - if (epochSecret.openEpochSecret !== undefined) { - throw new Error( - `Epoch secret already open: ${epochSecret.streamId} ${epochSecret.epoch}`, - ) - } - - if (epochSecret.sealedEpochSecret === undefined) { - throw new Error(`Epoch secret not sealed: ${epochSecret.streamId} ${epochSecret.epoch}`) - } - - const sealedEpochSecret_ = HpkeCiphertext.fromBytes(epochSecret.sealedEpochSecret) - const secretKey_ = HpkeSecretKey.fromBytes(nextEpochKeys.secretKey) - const publicKey_ = HpkePublicKey.fromBytes(nextEpochKeys.publicKey) - const unsealedBytes = await this.cipherSuite.open( - sealedEpochSecret_, - secretKey_, - publicKey_, - ) - await this.addOpenEpochSecret(epochSecret.streamId, epochSecret.epoch, unsealedBytes) - } - - public async encryptMessage( - epochSecret: EpochSecret, - message: Uint8Array, - ): Promise { - this.log.debug('encryptMessage', { - streamId: epochSecret.streamId, - epoch: epochSecret.epoch, - }) - - if (epochSecret.derivedKeys === undefined) { - throw new Error(`Epoch secret not open: ${epochSecret.streamId} ${epochSecret.epoch}`) - } - - const publicKey_ = HpkePublicKey.fromBytes(epochSecret.derivedKeys.publicKey) - const ciphertext_ = await this.cipherSuite.seal(publicKey_, message) - const ciphertext = ciphertext_.toBytes() - - return new EncryptedData({ - algorithm: MLS_ALGORITHM, - mls: { - epoch: epochSecret.epoch, - ciphertext, - }, - version: EncryptedDataVersion.ENCRYPTED_DATA_VERSION_1, - }) - } - - public async decryptMessage( - epochSecret: EpochSecret, - message: EncryptedData, - ): Promise { - this.log.debug('decryptMessage', { - streamId: epochSecret.streamId, - epoch: epochSecret.epoch, - }) - - if (epochSecret.derivedKeys === undefined) { - throw new Error(`Epoch secret not open: ${epochSecret.streamId} ${epochSecret.epoch}`) - } - - if (message.algorithm !== MLS_ALGORITHM) { - throw new Error(`Invalid algorithm: ${message.algorithm}`) - } - - if (message.mls === undefined) { - throw new Error('Missing mls payload') - } - - if (message.mls.epoch !== epochSecret.epoch) { - throw new Error(`Epoch mismatch: ${message.mls.epoch} != ${epochSecret.epoch}`) - } - - const ciphertext = message.mls.ciphertext - const publicKey_ = HpkePublicKey.fromBytes(epochSecret.derivedKeys.publicKey) - const secretKey_ = HpkeSecretKey.fromBytes(epochSecret.derivedKeys.secretKey) - const ciphertext_ = HpkeCiphertext.fromBytes(ciphertext) - return await this.cipherSuite.open(ciphertext_, secretKey_, publicKey_) - } - - public async handleEpochSecrets(_streamId: string, _message: EpochSecretsMessage) { - for (const epochSecret of _message.secrets) { - await this.addAnnouncedSealedEpochSecret( - _streamId, - epochSecret.epoch, - epochSecret.secret, - ) - this.coordinator?.newSealedEpochSecret(_streamId, epochSecret.epoch) - } - } - - public epochSecretMessage(_epochSecret: EpochSecret): EpochSecretsMessage { - if (_epochSecret.sealedEpochSecret === undefined) { - throw new Error('Fatal: epoch secret not sealed') - } - - return { - secrets: [ - { - epoch: _epochSecret.epoch, - secret: _epochSecret.sealedEpochSecret, - }, - ], - } - } - - public isOpen(epochSecret: EpochSecret): boolean { - return epochSecret.openEpochSecret !== undefined - } - - public canBeOpened(epochSecret: EpochSecret): boolean { - return ( - epochSecret.openEpochSecret === undefined && epochSecret.sealedEpochSecret !== undefined - ) - } -} diff --git a/packages/sdk/src/mls/epoch/epochSecretStore.ts b/packages/sdk/src/mls/epoch/epochSecretStore.ts deleted file mode 100644 index 753f508d5d..0000000000 --- a/packages/sdk/src/mls/epoch/epochSecretStore.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { EpochSecret, epochSecretId, EpochSecretId } from './epochSecret' - -export interface IEpochSecretStore { - getEpochSecret(streamId: string, epoch: bigint): Promise - - setEpochSecret(epochSecret: EpochSecret): Promise -} - -export class InMemoryEpochSecretStore implements IEpochSecretStore { - private epochKeySates: Map = new Map() - - public async getEpochSecret(streamId: string, epoch: bigint): Promise { - const epochId: EpochSecretId = epochSecretId(streamId, epoch) - return this.epochKeySates.get(epochId) - } - - public async setEpochSecret(epochSecret: EpochSecret): Promise { - const epochId = epochSecretId(epochSecret.streamId, epochSecret.epoch) - this.epochKeySates.set(epochId, epochSecret) - } -} diff --git a/packages/sdk/src/mls/epoch/index.ts b/packages/sdk/src/mls/epoch/index.ts deleted file mode 100644 index f18c242ce1..0000000000 --- a/packages/sdk/src/mls/epoch/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Re-export everything for now -export * from './epochSecret' -export * from './epochSecretService' -export * from './epochSecretStore' diff --git a/packages/sdk/src/mls/externalGroup/externalCrypto.ts b/packages/sdk/src/mls/externalGroup/externalCrypto.ts deleted file mode 100644 index 461197dfac..0000000000 --- a/packages/sdk/src/mls/externalGroup/externalCrypto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ExternalGroup } from './externalGroup' -import { - MlsMessage, - ExternalSnapshot as MlsExternalSnapshot, - ExternalClient as MlsExternalClient, -} from '@river-build/mls-rs-wasm' - -export class ExternalCrypto { - public async processCommit(group: ExternalGroup, commit: Uint8Array) { - await group.externalGroup.processIncomingMessage(MlsMessage.fromBytes(commit)) - } - - public async loadExternalGroupFromSnapshot( - streamId: string, - snapshot: Uint8Array, - ): Promise { - const externalClient = new MlsExternalClient() - const externalSnapshot = MlsExternalSnapshot.fromBytes(snapshot) - const externalGroup = await externalClient.loadGroup(externalSnapshot) - return new ExternalGroup(streamId, externalGroup) - } - - public exportTree(group: ExternalGroup): Uint8Array { - return group.externalGroup.exportTree() - } - - public async externalGroupSnapshot( - groupInfoMessage: Uint8Array, - exportedTree: Uint8Array, - ): Promise { - const externalClient = new MlsExternalClient() - const externalGroup = await externalClient.observeGroup(groupInfoMessage, exportedTree) - return externalGroup.snapshot().toBytes() - } -} diff --git a/packages/sdk/src/mls/externalGroup/externalGroup.ts b/packages/sdk/src/mls/externalGroup/externalGroup.ts deleted file mode 100644 index e23d859bbf..0000000000 --- a/packages/sdk/src/mls/externalGroup/externalGroup.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExternalGroup as MlsExternalGroup } from '@river-build/mls-rs-wasm' - -export class ExternalGroup { - public readonly streamId: string - public readonly externalGroup: MlsExternalGroup - - constructor(streamId: string, externalGroup: MlsExternalGroup) { - this.streamId = streamId - this.externalGroup = externalGroup - } -} diff --git a/packages/sdk/src/mls/externalGroup/externalGroupService.ts b/packages/sdk/src/mls/externalGroup/externalGroupService.ts deleted file mode 100644 index efc2b53a72..0000000000 --- a/packages/sdk/src/mls/externalGroup/externalGroupService.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ExternalGroup } from './externalGroup' -import { ExternalCrypto } from './externalCrypto' -import { dlog, DLogger } from '@river-build/dlog' - -const defaultLogger = dlog('csb:mls:externalGroupService') - -export class ExternalGroupService { - private log: { - debug: DLogger - error: DLogger - } - - private crypto: ExternalCrypto - - constructor(crypto: ExternalCrypto, opts?: { log: DLogger }) { - this.crypto = crypto - const logger = opts?.log ?? defaultLogger - this.log = { - debug: logger.extend('debug'), - error: logger.extend('error'), - } - } - - public exportTree(group: ExternalGroup): Uint8Array { - return this.crypto.exportTree(group) - } - - public async loadSnapshot(streamId: string, snapshot: Uint8Array): Promise { - return this.crypto.loadExternalGroupFromSnapshot(streamId, snapshot) - } - - public async processCommit(externalGroup: ExternalGroup, commit: Uint8Array): Promise { - return this.crypto.processCommit(externalGroup, commit) - } -} diff --git a/packages/sdk/src/mls/externalGroup/index.ts b/packages/sdk/src/mls/externalGroup/index.ts deleted file mode 100644 index 012d7baf25..0000000000 --- a/packages/sdk/src/mls/externalGroup/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Re-export everything for now -export * from './externalCrypto' -export * from './externalGroup' -export * from './externalGroupService' diff --git a/packages/sdk/src/mls/group/crypto.ts b/packages/sdk/src/mls/group/crypto.ts deleted file mode 100644 index 0e12eae681..0000000000 --- a/packages/sdk/src/mls/group/crypto.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - Client as MlsClient, - ExportedTree as MlsExportedTree, - Group as MlsGroup, - CipherSuite as MlsCipherSuite, - MlsMessage, -} from '@river-build/mls-rs-wasm' -import { dlog, DLogger } from '@river-build/dlog' -import { Group } from './group' - -const log = dlog('csb:mls:crypto') - -export class Crypto { - private client!: MlsClient - public readonly userAddress: Uint8Array - public readonly deviceKey: Uint8Array - protected readonly log: { - info: DLogger - debug: DLogger - error: DLogger - } - - constructor(userAddress: Uint8Array, deviceKey: Uint8Array, opts?: { log: DLogger }) { - this.userAddress = userAddress - this.deviceKey = deviceKey - const log_ = opts?.log ?? log - this.log = { - info: log_.extend('info'), - debug: log_.extend('debug'), - error: log_.extend('error'), - } - } - - public async initialize() { - const name = new Uint8Array(this.userAddress.length + this.deviceKey.length) - name.set(this.userAddress, 0) - name.set(this.deviceKey, this.userAddress.length) - this.client = await MlsClient.create(name) - } - - public async createGroup(streamId: string): Promise { - if (!this.client) { - this.log.error('createGroup: Client not initialized') - throw new Error('Client not initialized') - } - - // TODO: Create group with a particular group id - // TODO: Does the Rust API permit returning groupInfo in a single call? - const mlsGroup = await this.client.createGroup() - const groupInfoWithExternalKey = ( - await mlsGroup.groupInfoMessageAllowingExtCommit(true) - ).toBytes() - - return Group.createGroup(streamId, mlsGroup, groupInfoWithExternalKey) - } - - public async externalJoin( - streamId: string, - groupInfo: Uint8Array, - exportedTree: Uint8Array, - ): Promise { - if (!this.client) { - this.log.error('externalJoin: Client not initialized') - throw new Error('Client not initialized') - } - - // TODO: Does the Rust API permit returning groupInfo in a single call? - const { group: mlsGroup, commit } = await this.client.commitExternal( - MlsMessage.fromBytes(groupInfo), - MlsExportedTree.fromBytes(exportedTree), - ) - const groupInfoWithExternalKey = ( - await mlsGroup.groupInfoMessageAllowingExtCommit(true) - ).toBytes() - const commitBytes = commit.toBytes() - - return Group.externalJoin(streamId, mlsGroup, commitBytes, groupInfoWithExternalKey) - } - - /// Process current group commit and return epoch - public async processCommit(group: Group, commit: Uint8Array): Promise { - await group.group.processIncomingMessage(MlsMessage.fromBytes(commit)) - return group.group.currentEpoch - } - - // TODO: Make this return undefined in case of an error? - public async loadGroup(groupId: Uint8Array): Promise { - if (!this.client) { - this.log.error('loadGroup: Client not initialized') - throw new Error('Client not initialized') - } - - return this.client.loadGroup(groupId) - } - - public async writeGroupToStorage(group: MlsGroup): Promise { - await group.writeToStorage() - } - - public currentEpoch(group: Group): bigint { - return group.group.currentEpoch - } - - public exportTree(group: Group): Uint8Array { - return group.group.exportTree().toBytes() - } - - public signaturePublicKey(): Uint8Array { - if (!this.client) { - this.log.error('signaturePublicKey: Client not initialized') - throw new Error('Client not initialized') - } - return this.client.signaturePublicKey() - } - - public async exportEpochSecret(group: Group): Promise { - const secret = await group.group.currentEpochSecret() - return secret.toBytes() - } - - public ciphersuite(): MlsCipherSuite { - return new MlsCipherSuite() - } -} diff --git a/packages/sdk/src/mls/group/group.ts b/packages/sdk/src/mls/group/group.ts deleted file mode 100644 index fc5a436659..0000000000 --- a/packages/sdk/src/mls/group/group.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Group as MlsGroup } from '@river-build/mls-rs-wasm' - -export type GroupStatus = 'GROUP_PENDING_CREATE' | 'GROUP_PENDING_JOIN' | 'GROUP_ACTIVE' - -export class Group { - public readonly streamId: string - public readonly status: GroupStatus - public readonly group: MlsGroup - public readonly groupInfoWithExternalKey?: Uint8Array - public readonly commit?: Uint8Array - - private constructor( - streamId: string, - group: MlsGroup, - status: GroupStatus, - groupInfoWithExternalKey?: Uint8Array, - commit?: Uint8Array, - ) { - this.streamId = streamId - this.group = group - this.groupInfoWithExternalKey = groupInfoWithExternalKey - this.commit = commit - this.status = status - } - - /// Factory method for creating the group from scratch - public static createGroup( - streamId: string, - group: MlsGroup, - groupInfoWithExternalKey: Uint8Array, - ): Group { - return new Group(streamId, group, 'GROUP_PENDING_CREATE', groupInfoWithExternalKey) - } - - /// Factory method for creating the group via external join - public static externalJoin( - streamId: string, - group: MlsGroup, - commit: Uint8Array, - groupInfoWithExternalKey: Uint8Array, - ): Group { - return new Group(streamId, group, 'GROUP_PENDING_JOIN', groupInfoWithExternalKey, commit) - } - - public static activeGroup(streamId: string, group: MlsGroup): Group { - return new Group(streamId, group, 'GROUP_ACTIVE') - } -} diff --git a/packages/sdk/src/mls/group/groupService.ts b/packages/sdk/src/mls/group/groupService.ts deleted file mode 100644 index 7de7ce2d9b..0000000000 --- a/packages/sdk/src/mls/group/groupService.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { IGroupStore } from './groupStore' -import { Group } from './group' -import { - MemberPayload_Mls_ExternalJoin, - MemberPayload_Mls_InitializeGroup, -} from '@river-build/proto' -import { PlainMessage } from '@bufbuild/protobuf' -import { Crypto } from './crypto' -import { DLogger, dlog, bin_equal } from '@river-build/dlog' - -type InitializeGroupMessage = PlainMessage -type ExternalJoinMessage = PlainMessage - -// Placeholder for a coordinator -export interface IGroupServiceCoordinator { - joinOrCreateGroup(streamId: string): void - groupActive(streamId: string): void - newEpoch(streamId: string, epoch: bigint): void -} - -const defaultLogger = dlog('csb:mls:groupService') - -/// Service handling group operations both for Group and External Group -export class GroupService { - private groupCache: Map = new Map() - private groupStore: IGroupStore - private log: { - debug: DLogger - error: DLogger - } - - private crypto: Crypto - private coordinator: IGroupServiceCoordinator | undefined - - constructor( - groupStore: IGroupStore, - crypto: Crypto, - coordinator?: IGroupServiceCoordinator, - opts?: { log: DLogger }, - ) { - this.groupStore = groupStore - this.crypto = crypto - this.coordinator = coordinator - - const logger = opts?.log ?? defaultLogger - - this.log = { - debug: logger.extend('debug'), - error: logger.extend('error'), - } - } - - public getGroup(streamId: string): Group | undefined { - this.log.debug('getGroup', { streamId }) - return this.groupCache.get(streamId) - } - - public async loadGroup(streamId: string): Promise { - this.log.debug('loadGroup', { streamId }) - const dto = await this.groupStore.getGroup(streamId) - - if (dto === undefined) { - return - } - - const { groupId, ...fields } = dto - - // TODO: Add error handling - const mlsGroup = await this.crypto.loadGroup(groupId) - - const group = { - ...fields, - group: mlsGroup, - } - - this.groupCache.set(streamId, group) - } - - // TODO: Add recovery in case any of those failing - public async saveGroup(group: Group): Promise { - this.log.debug('saveGroup', { streamId: group.streamId }) - - this.groupCache.set(group.streamId, group) - - const { group: mlsGroup, ...fields } = group - const groupId = mlsGroup.groupId - const dto = { ...fields, groupId } - - await this.groupStore.setGroup(dto) - await this.crypto.writeGroupToStorage(group.group) - } - - // TODO: Should this be private or public? - public async clearGroup(streamId: string): Promise { - this.log.debug('clearGroup', { streamId }) - - this.groupCache.delete(streamId) - await this.groupStore.clearGroup(streamId) - // TODO: Clear group in GroupStateStore - } - - // TODO: Should this throw an Error? - public async handleInitializeGroup(group: Group, _message: InitializeGroupMessage) { - this.log.debug('handleInitializeGroup', { streamId: group.streamId }) - - const isGroupActive = group.status === 'GROUP_ACTIVE' - if (isGroupActive) { - this.log.error('handleInitializeGroup: Group is already active', { - streamId: group.streamId, - }) - // Report programmer error - throw new Error('Programmer error: Group is already active') - } - - const wasInitializeGroupOurOwn = - group.status === 'GROUP_PENDING_CREATE' && - group.groupInfoWithExternalKey !== undefined && - bin_equal(_message.groupInfoMessage, group.groupInfoWithExternalKey) && - bin_equal(_message.signaturePublicKey, this.getSignaturePublicKey()) - - if (!wasInitializeGroupOurOwn) { - await this.clearGroup(group.streamId) - this.coordinator?.joinOrCreateGroup(group.streamId) - return - } - - const activeGroup = Group.activeGroup(group.streamId, group.group) - await this.saveGroup(activeGroup) - - this.coordinator?.groupActive(group.streamId) - const epoch = this.crypto.currentEpoch(group) - this.coordinator?.newEpoch(group.streamId, epoch) - } - - public async handleExternalJoin(group: Group, message: ExternalJoinMessage) { - this.log.debug('handleExternalJoin', { streamId: group.streamId }) - - const isGroupActive = group.status === 'GROUP_ACTIVE' - if (isGroupActive) { - await this.crypto.processCommit(group, message.commit) - await this.saveGroup(group) - const epoch = this.crypto.currentEpoch(group) - this.coordinator?.newEpoch(group.streamId, epoch) - return - } - - const wasExternalJoinOurOwn = - group.status === 'GROUP_PENDING_JOIN' && - group.groupInfoWithExternalKey !== undefined && - bin_equal(message.groupInfoMessage, group.groupInfoWithExternalKey) && - group.commit !== undefined && - bin_equal(message.commit, group.commit) && - bin_equal(message.signaturePublicKey, this.getSignaturePublicKey()) - - if (!wasExternalJoinOurOwn) { - await this.clearGroup(group.streamId) - this.coordinator?.joinOrCreateGroup(group.streamId) - return - } - - const activeGroup = Group.activeGroup(group.streamId, group.group) - await this.saveGroup(activeGroup) - - this.coordinator?.groupActive(group.streamId) - const epoch = this.crypto.currentEpoch(group) - this.coordinator?.newEpoch(group.streamId, epoch) - } - - public async initializeGroupMessage(streamId: string): Promise { - this.log.debug('initializeGroupMessage', { streamId }) - - if (this.groupCache.has(streamId)) { - this.log.error(`initializeGroupMessage: Group already exists for ${streamId}`) - throw new Error(`Group already exists for ${streamId}`) - } - - const group = await this.crypto.createGroup(streamId) - await this.saveGroup(group) - - const externalGroupSnapshot = this.exportGroupSnapshot(group) - - const signaturePublicKey = this.getSignaturePublicKey() - - return { - groupInfoMessage: group.groupInfoWithExternalKey!, - signaturePublicKey, - externalGroupSnapshot, - } - } - - public async externalJoinMessage( - streamId: string, - latestGroupInfo: Uint8Array, - exportedTree: Uint8Array, - ): Promise { - this.log.debug('externalJoinMessage', { streamId }) - - if (this.groupCache.has(streamId)) { - this.log.error(`externalJoinMessage: Group already exists for ${streamId}`) - throw new Error(`Group already exists for ${streamId}`) - } - - const group = await this.crypto.externalJoin(streamId, latestGroupInfo, exportedTree) - await this.saveGroup(group) - - const signaturePublicKey = this.getSignaturePublicKey() - - return { - commit: group.commit!, - groupInfoMessage: group.groupInfoWithExternalKey!, - signaturePublicKey, - } - } - - public exportGroupSnapshot(_group: Group): Uint8Array { - throw new Error('Not implemented') - } - - public currentEpoch(group: Group): bigint { - return this.crypto.currentEpoch(group) - } - - private getSignaturePublicKey(): Uint8Array { - return this.crypto.signaturePublicKey() - } - - public async exportEpochSecret(group: Group): Promise { - return this.crypto.exportEpochSecret(group) - } -} diff --git a/packages/sdk/src/mls/group/groupStore.ts b/packages/sdk/src/mls/group/groupStore.ts deleted file mode 100644 index f0ceef39ec..0000000000 --- a/packages/sdk/src/mls/group/groupStore.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Group } from './group' - -// Group DTO replaces group with groupId -export type GroupDTO = Omit & { groupId: Uint8Array } - -export interface IGroupStore { - hasGroup(streamId: string): Promise - getGroup(streamId: string): Promise - setGroup(dto: GroupDTO): Promise - clearGroup(streamId: string): Promise -} - -export class InMemoryGroupStore implements IGroupStore { - private groups: Map = new Map() - - public async hasGroup(streamId: string): Promise { - return this.groups.has(streamId) - } - - public async getGroup(streamId: string): Promise { - return this.groups.get(streamId) - } - - public async setGroup(dto: GroupDTO): Promise { - this.groups.set(dto.streamId, dto) - } - - public async clearGroup(streamId: string): Promise { - this.groups.delete(streamId) - } -} diff --git a/packages/sdk/src/mls/group/index.ts b/packages/sdk/src/mls/group/index.ts deleted file mode 100644 index 8e03f36eda..0000000000 --- a/packages/sdk/src/mls/group/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Re-export everything for now -export * from './crypto' -export * from './group' -export * from './groupService' -export * from './groupStore' diff --git a/packages/sdk/src/mls/groupStateStorage.ts b/packages/sdk/src/mls/groupStateStorage.ts deleted file mode 100644 index a474bb721c..0000000000 --- a/packages/sdk/src/mls/groupStateStorage.ts +++ /dev/null @@ -1,206 +0,0 @@ -/// An implementation of group state storage from mls-rs-wasm - -import { EpochRecord, IGroupStateStorage } from '@river-build/mls-rs-wasm' -import Dexie from 'dexie' -import { bin_toString } from '@river-build/dlog' - -/// GroupStateId is a branded string to avoid accidental use of an ordinary -/// string as a group state id. Brand exists only during compile-time and -// does not occur any run-time cost. -type GroupStateId = string & { __brand: 'GroupStateId' } - -/// Convert uint8Array to Base64 string -function uint8ArrayToBase64(arr: Uint8Array): string { - // Convert Uint8Array to a raw binary string - const binaryString = Array.from(arr, (byte) => String.fromCharCode(byte)).join('') - // Encode the binary string as Base64 - return btoa(binaryString) -} - -/// Convert uint8Array to GroupStateId -function groupStateId(groupId: Uint8Array): GroupStateId { - const base64GroupId = uint8ArrayToBase64(groupId) - return base64GroupId as GroupStateId -} - -/// Basic in-memory group state storage -export class InMemoryGroupStateStorage implements IGroupStateStorage { - groupStates: Map = new Map() - epochStorage: Map> = new Map() - maxEpochRetention: bigint = 3n - - state(groupId: Uint8Array): Promise { - return Promise.resolve(this.groupStates.get(groupStateId(groupId))) - } - - epoch(groupId: Uint8Array, epochId: bigint): Promise { - const epoch = this.epochStorage.get(groupStateId(groupId)) - if (epoch) { - return Promise.resolve(epoch.get(epochId)) - } - return Promise.resolve(undefined) - } - - private getOrCreateEpoch(groupId: GroupStateId): Map { - let epoch = this.epochStorage.get(groupId) - if (!epoch) { - epoch = new Map() - this.epochStorage.set(groupId, epoch) - } - return epoch - } - - write( - state_id: Uint8Array, - state_data: Uint8Array, - epochInserts: EpochRecord[], - epochUpdates: EpochRecord[], - ): Promise { - const groupId = groupStateId(state_id) - this.groupStates.set(groupId, state_data) - const epoch = this.getOrCreateEpoch(groupId) - - let maxEpochId = -1n - - // Inserting new epochs - epochInserts.forEach((e) => { - maxEpochId = e.id - epoch.set(e.id, e.data) - }) - - // Updating epochs - epochUpdates.forEach((e) => { - epoch.set(e.id, e.data) - }) - - // Removing epochs below maxEpochRetention - if (maxEpochId > this.maxEpochRetention) { - const deleteUnder = maxEpochId - this.maxEpochRetention - for (const epochId of epoch.keys()) { - if (epochId <= deleteUnder) { - epoch.delete(epochId) - } - } - } - - return Promise.resolve(undefined) - } - - maxEpochId(groupId: Uint8Array): Promise { - const epoch = this.epochStorage.get(groupStateId(groupId)) - if (!epoch) { - return Promise.resolve(undefined) - } - - const epochIds = Array.from(epoch.keys()) - if (epochIds.length === 0) { - return Promise.resolve(undefined) - } - - const maxEpochId = epochIds.reduce((a, b) => (a > b ? a : b)) - return Promise.resolve(maxEpochId) - } -} - -// Dexie-based GroupStateStorage -export class DexieGroupStateStorage extends Dexie implements IGroupStateStorage { - private groupStates!: Dexie.Table<{ groupId: string; data: Uint8Array; maxEpochId: string }> - private epochs!: Dexie.Table<{ groupId: string; epochId: string; data: Uint8Array }> - private maxEpochRetention: bigint = 3n - - constructor(deviceKey: Uint8Array) { - const databaseName = `mlsStore-${bin_toString(deviceKey)}` - super(databaseName) - this.version(1).stores({ - groupStates: 'groupId', - epochs: '[groupId+epochId]', - }) - } - - async state(groupId: Uint8Array): Promise { - const groupId_ = groupStateId(groupId) - const groupState = await this.groupStates.get(groupId_) - return groupState?.data - } - - async epoch(groupId: Uint8Array, epochId: bigint): Promise { - const groupId_ = groupStateId(groupId) - const epochId_ = epochId.toString() - const epoch = await this.epochs.get({ groupId: groupId_, epochId: epochId_ }) - return epoch?.data - } - - async write( - stateId: Uint8Array, - stateData: Uint8Array, - epochInserts: EpochRecord[], - epochUpdates: EpochRecord[], - ): Promise { - await this.transaction('rw', this.groupStates, this.epochs, async () => { - const groupId_ = groupStateId(stateId) - let maxEpochId = -1n - // process inserts - const epochInserts_ = epochInserts.map((e) => { - const epochId_ = e.id.toString() - maxEpochId = e.id - - return { - groupId: groupId_, - epochId: epochId_, - data: e.data, - } - }) - await this.epochs.bulkAdd(epochInserts_) - - const epochUpdates_ = epochUpdates.map((e) => { - const epochId_ = e.id.toString() - - return { - groupId: groupId_, - epochId: epochId_, - data: e.data, - } - }) - await this.epochs.bulkPut(epochUpdates_) - - // Remove epochs below maxEpochRetention - // Unfortunately has to go over all epochs - if (maxEpochId > this.maxEpochRetention) { - const deleteUnder = maxEpochId - this.maxEpochRetention - await this.epochs - .where('groupId') - .equals(groupId_) - .filter((e) => BigInt(e.epochId) <= deleteUnder) - .delete() - } - - // update group state - await this.groupStates.put({ - groupId: groupId_, - data: stateData, - maxEpochId: maxEpochId.toString(), - }) - }) - - return undefined - } - - async maxEpochId(groupId: Uint8Array): Promise { - const groupId_ = groupStateId(groupId) - const groupState = await this.groupStates.get(groupId_) - const maxEpochId_ = groupState?.maxEpochId - - if (maxEpochId_ === undefined) { - return undefined - } - - const maxEpochId = BigInt(maxEpochId_) - - // guard against incorrect values - if (maxEpochId < 0n) { - return undefined - } - - return maxEpochId - } -} diff --git a/packages/sdk/src/mls/index.ts b/packages/sdk/src/mls/index.ts deleted file mode 100644 index f2b2ab243c..0000000000 --- a/packages/sdk/src/mls/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Expose all for now -export * from './constants' -export * from './coordinator' -export * from './epoch' -export * from './externalGroup' -export * from './group' -export * from './queue' -export * from './adapter' diff --git a/packages/sdk/src/mls/queue/index.ts b/packages/sdk/src/mls/queue/index.ts deleted file mode 100644 index eb682cb75e..0000000000 --- a/packages/sdk/src/mls/queue/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './queueService' diff --git a/packages/sdk/src/mls/queue/queueService.ts b/packages/sdk/src/mls/queue/queueService.ts deleted file mode 100644 index 2cc91efb17..0000000000 --- a/packages/sdk/src/mls/queue/queueService.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { PlainMessage } from '@bufbuild/protobuf' -import { - MemberPayload_Mls_EpochSecrets, - MemberPayload_Mls_ExternalJoin, - MemberPayload_Mls_InitializeGroup, -} from '@river-build/proto' -import { IGroupServiceCoordinator } from '../group' -import { IEpochSecretServiceCoordinator } from '../epoch' -import { DLogger } from '@river-build/dlog' -import { logNever } from '../../check' -import { EncryptedContent } from '../../encryptedContentTypes' -import { ICoordinator } from '../coordinator' - -type InitializeGroupMessage = PlainMessage -type ExternalJoinMessage = PlainMessage -type EpochSecretsMessage = PlainMessage - -// Commands, which are internal commands of the Queue - -type JoinOrCreateGroupCommand = { - tag: 'joinOrCreateGroup' - streamId: string -} - -type GroupActiveCommand = { - tag: 'groupActive' - streamId: string -} - -type NewEpochCommand = { - tag: 'newEpoch' - streamId: string - epoch: bigint -} - -type NewOpenEpochSecretCommand = { - tag: 'newOpenEpochSecret' - streamId: string - epoch: bigint -} - -type NewSealedEpochSecretCommand = { - tag: 'newSealedEpochSecret' - streamId: string - epoch: bigint -} - -type AnnounceEpochSecretCommand = { - tag: 'announceEpochSecret' - streamId: string - epoch: bigint -} - -type QueueCommand = - | JoinOrCreateGroupCommand - | GroupActiveCommand - | NewEpochCommand - | NewOpenEpochSecretCommand - | NewSealedEpochSecretCommand - | AnnounceEpochSecretCommand - -// Events, which we are processing from outside -type InitializeGroupEvent = { - tag: 'initializeGroup' - streamId: string - message: InitializeGroupMessage -} - -type ExternalJoinEvent = { - tag: 'externalJoin' - streamId: string - message: ExternalJoinMessage -} - -type EpochSecretsEvent = { - tag: 'epochSecrets' - streamId: string - message: EpochSecretsMessage -} - -// TODO: Should encrypted content get its own queue? -type EncryptedContentEvent = { - tag: 'encryptedContent' - streamId: string - eventId: string - message: EncryptedContent -} - -type QueueEvent = - | InitializeGroupEvent - | ExternalJoinEvent - | EpochSecretsEvent - | EncryptedContentEvent - -export interface IQueueService { - // These are only needed by the Coordinator - enqueueCommand(command: QueueCommand): void - enqueueEvent(event: QueueEvent): void - // These are only needed by the adapter - start(): void - stop(): Promise - onMobileSafariPageVisibilityChanged(this: void): void -} - -// This feels more like a coordinator -export class QueueService implements IQueueService { - private coordinator: ICoordinator - - private log!: { - error: DLogger - debug: DLogger - } - - constructor(coordinator: ICoordinator) { - this.coordinator = coordinator - // nop - } - - // # Queue-related operations # - - // Queue-related fields - private commandQueue: QueueCommand[] = [] - private eventQueue: QueueEvent[] = [] - private delayMs = 15 - private started: boolean = false - private stopping: boolean = false - private timeoutId?: NodeJS.Timeout - private inProgressTick?: Promise - private isMobileSafariBackgrounded = false - - public enqueueCommand(command: QueueCommand) { - this.commandQueue.push(command) - // TODO: Is this needed when we tick after start - this.checkStartTicking() - } - - private dequeueCommand(): QueueCommand | undefined { - return this.commandQueue.shift() - } - - public enqueueEvent(event: QueueEvent) { - this.eventQueue.push(event) - // TODO: Is this needed when we tick after start - this.checkStartTicking() - } - - private dequeueEvent(): QueueEvent | undefined { - return this.eventQueue.shift() - } - - getDelayMs(): number { - return this.delayMs - } - - public start() { - // nop - this.started = true - this.checkStartTicking() - } - - public async stop(): Promise { - this.started = false - await this.stopTicking() - // nop - } - - private shouldPauseTicking(): boolean { - return this.isMobileSafariBackgrounded - } - - private checkStartTicking() { - if (this.stopping) { - // this.log.debug('ticking is being stopped') - return - } - - if (!this.started || this.timeoutId) { - // this.log.debug('previous tick is still running') - return - } - - if (this.shouldPauseTicking()) { - return - } - - // TODO: should this have any timeout? - this.timeoutId = setTimeout(() => { - this.inProgressTick = this.tick() - .catch((e) => this.log.error('MLS ProcessTick Error', e)) - .finally(() => { - this.timeoutId = undefined - setTimeout(() => this.checkStartTicking()) - }) - }, this.getDelayMs()) - } - - private async stopTicking() { - if (this.stopping) { - return - } - this.stopping = true - - if (this.timeoutId) { - clearTimeout(this.timeoutId) - this.timeoutId = undefined - } - if (this.inProgressTick) { - try { - await this.inProgressTick - } catch (e) { - this.log.error('ProcessTick Error while stopping', e) - } finally { - this.inProgressTick = undefined - } - } - this.stopping = false - } - - public async tick() { - // noop - const command = this.dequeueCommand() - if (command !== undefined) { - return this.processCommand(command) - } - - const event = this.dequeueEvent() - if (event !== undefined) { - return this.processEvent(event) - } - } - - public async processCommand(command: QueueCommand): Promise { - switch (command.tag) { - case 'joinOrCreateGroup': - return this.coordinator.joinOrCreateGroup(command.streamId) - case 'groupActive': - return this.coordinator.groupActive(command.streamId) - case 'newEpoch': - return this.coordinator.newOpenEpochSecret(command.streamId, command.epoch) - case 'newOpenEpochSecret': - return this.coordinator.newOpenEpochSecret(command.streamId, command.epoch) - case 'newSealedEpochSecret': - return this.coordinator.newSealedEpochSecret(command.streamId, command.epoch) - case 'announceEpochSecret': - return this.coordinator.announceEpochSecret(command.streamId, command.epoch) - default: - logNever(command) - } - } - - public async processEvent(event: QueueEvent): Promise { - switch (event.tag) { - case 'initializeGroup': - return this.coordinator.handleInitializeGroup(event.streamId, event.message) - case 'externalJoin': - return this.coordinator.handleExternalJoin(event.streamId, event.message) - case 'epochSecrets': - return this.coordinator.handleEpochSecrets(event.streamId, event.message) - case 'encryptedContent': - return this.coordinator.handleEncryptedContent( - event.streamId, - event.eventId, - event.message, - ) - default: - logNever(event) - } - } - - public readonly onMobileSafariPageVisibilityChanged = () => { - this.log.debug('onMobileSafariBackgrounded', this.isMobileSafariBackgrounded) - this.isMobileSafariBackgrounded = document.visibilityState === 'hidden' - if (!this.isMobileSafariBackgrounded) { - this.checkStartTicking() - } - } -} - -export class GroupServiceCoordinatorAdapter implements IGroupServiceCoordinator { - public readonly queueService: IQueueService - - constructor(queueService: IQueueService) { - this.queueService = queueService - } - - public joinOrCreateGroup(streamId: string): void { - this.queueService.enqueueCommand({ tag: 'joinOrCreateGroup', streamId }) - } - public groupActive(streamId: string): void { - this.queueService.enqueueCommand({ tag: 'groupActive', streamId }) - } - public newEpoch(streamId: string, epoch: bigint): void { - this.queueService.enqueueCommand({ tag: 'newEpoch', streamId, epoch }) - } -} - -export class EpochSecretServiceCoordinatorAdapter implements IEpochSecretServiceCoordinator { - public readonly queueService: IQueueService - - constructor(queueService: IQueueService) { - this.queueService = queueService - } - - public newOpenEpochSecret(streamId: string, epoch: bigint): void { - this.queueService.enqueueCommand({ tag: 'newOpenEpochSecret', streamId, epoch }) - } - public newSealedEpochSecret(streamId: string, epoch: bigint): void { - this.queueService.enqueueCommand({ tag: 'newSealedEpochSecret', streamId, epoch }) - } -} diff --git a/packages/sdk/src/mls/utils/mlsutils.ts b/packages/sdk/src/mls/utils/mlsutils.ts deleted file mode 100644 index 8488c5ecd2..0000000000 --- a/packages/sdk/src/mls/utils/mlsutils.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { check } from '@river-build/dlog' -import { IStreamStateView } from '../../streamStateView' - -export type ExtractMlsExternalGroupResult = { - externalGroupSnapshot: Uint8Array - groupInfoMessage: Uint8Array - commits: { commit: Uint8Array; groupInfoMessage: Uint8Array }[] -} - -export function extractMlsExternalGroup( - streamView: IStreamStateView, -): ExtractMlsExternalGroupResult | undefined { - // check if there is group info at all - if (streamView.membershipContent.mls.groupInfoMessage === undefined) { - return undefined - } - - const indexOfLastSnapshot = streamView.timeline.findLastIndex((event) => { - const payload = event.remoteEvent?.event.payload - if (payload?.case !== 'miniblockHeader') { - return false - } - return payload.value.snapshot !== undefined - }) - - const payload = streamView.timeline[indexOfLastSnapshot].remoteEvent?.event.payload - check(payload?.case === 'miniblockHeader', 'no snapshot found') - const snapshot = payload.value.snapshot - check(snapshot !== undefined, 'no snapshot found') - const externalGroupSnapshot = snapshot.members?.mls?.externalGroupSnapshot - check(externalGroupSnapshot !== undefined, 'no externalGroupSnapshot found') - const groupInfoMessage = snapshot.members?.mls?.groupInfoMessage - check(groupInfoMessage !== undefined, 'no groupInfoMessage found') - const commits: { commit: Uint8Array; groupInfoMessage: Uint8Array }[] = [] - for (let i = indexOfLastSnapshot; i < streamView.timeline.length; i++) { - const event = streamView.timeline[i] - const payload = event.remoteEvent?.event.payload - if (payload?.case !== 'memberPayload') { - continue - } - if (payload?.value?.content.case !== 'mls') { - continue - } - - const mlsPayload = payload.value.content.value - switch (mlsPayload.content.case) { - case 'externalJoin': - case 'welcomeMessage': - commits.push({ - commit: mlsPayload.content.value.commit, - groupInfoMessage: mlsPayload.content.value.groupInfoMessage, - }) - break - - case undefined: - break - default: - break - } - } - return { externalGroupSnapshot, groupInfoMessage, commits: commits } -} diff --git a/packages/sdk/src/streamEvents.ts b/packages/sdk/src/streamEvents.ts index a869a0aed6..5aaf859182 100644 --- a/packages/sdk/src/streamEvents.ts +++ b/packages/sdk/src/streamEvents.ts @@ -53,21 +53,6 @@ export type StreamEncryptionEvents = { }[], ) => void userDeviceKeyMessage: (streamId: string, userId: string, userDevice: UserDevice) => void - // MLS-specific encryption events - mlsNewEncryptedContent: (streamId: string, eventId: string, content: EncryptedContent) => void - mlsInitializeGroup: ( - streamId: string, - groupInfoMessage: Uint8Array, - externalGroupSnapshot: Uint8Array, - signaturePublicKey: Uint8Array, - ) => void - mlsExternalJoin: ( - streamId: string, - signaturePublicKey: Uint8Array, - commit: Uint8Array, - groupInfoMessage: Uint8Array, - ) => void - mlsEpochSecrets: (streamId: string, secrets: { epoch: bigint; secret: Uint8Array }[]) => void } export type SyncedStreamEvents = { diff --git a/packages/sdk/src/streamStateView_AbstractContent.ts b/packages/sdk/src/streamStateView_AbstractContent.ts index 7febdac493..cc1a0329ed 100644 --- a/packages/sdk/src/streamStateView_AbstractContent.ts +++ b/packages/sdk/src/streamStateView_AbstractContent.ts @@ -5,7 +5,6 @@ import { DecryptedContent, EncryptedContent, toDecryptedContent } from './encryp import { StreamStateView_ChannelMetadata } from './streamStateView_ChannelMetadata' import { StreamEncryptionEvents, StreamStateEvents } from './streamEvents' import { streamIdToBytes } from './id' -import { MLS_ALGORITHM } from './mls' export abstract class StreamStateView_AbstractContent { abstract readonly streamId: string @@ -32,24 +31,10 @@ export abstract class StreamStateView_AbstractContent { if (cleartext) { event.decryptedContent = toDecryptedContent(kind, content.version, cleartext) } else { - switch (content.algorithm) { - case MLS_ALGORITHM: - encryptionEmitter?.emit( - 'mlsNewEncryptedContent', - this.streamId, - event.hashStr, - { - kind, - content, - }, - ) - break - default: - encryptionEmitter?.emit('newEncryptedContent', this.streamId, event.hashStr, { - kind, - content, - }) - } + encryptionEmitter?.emit('newEncryptedContent', this.streamId, event.hashStr, { + kind, + content, + }) } } diff --git a/packages/sdk/src/streamStateView_Members.ts b/packages/sdk/src/streamStateView_Members.ts index cacc5b7270..31cc29b3ea 100644 --- a/packages/sdk/src/streamStateView_Members.ts +++ b/packages/sdk/src/streamStateView_Members.ts @@ -24,7 +24,6 @@ import { KeySolicitationContent } from '@river-build/encryption' import { makeParsedEvent } from './sign' import { StreamStateView_AbstractContent } from './streamStateView_AbstractContent' import { utils } from 'ethers' -import { StreamStateView_Mls } from './streamStateView_Mls' const log = dlog('csb:streamStateView_Members') @@ -51,7 +50,6 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { readonly membership: StreamStateView_Members_Membership readonly solicitHelper: StreamStateView_Members_Solicitations readonly memberMetadata: StreamStateView_MemberMetadata - readonly mls: StreamStateView_Mls readonly pins: Pin[] = [] tips: { [key: string]: bigint } = {} encryptionAlgorithm?: string = undefined @@ -62,7 +60,6 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { this.membership = new StreamStateView_Members_Membership(streamId) this.solicitHelper = new StreamStateView_Members_Solicitations(streamId) this.memberMetadata = new StreamStateView_MemberMetadata(streamId) - this.mls = new StreamStateView_Mls(streamId) } // initialization @@ -159,9 +156,6 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { } }) - if (snapshot.members.mls) { - this.mls.applySnapshot(snapshot.members.mls) - } this.tips = { ...snapshot.members.tips } this.encryptionAlgorithm = snapshot.members.encryptionAlgorithm?.algorithm } @@ -349,9 +343,6 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { } break } - case 'mls': - this.mls.appendEvent(event, cleartext, encryptionEmitter, stateEmitter) - break case 'encryptionAlgorithm': this.encryptionAlgorithm = payload.content.value.algorithm stateEmitter?.emit( @@ -362,6 +353,8 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { break case undefined: break + case 'mls': // TODO: remove after proto update + break default: logNever(payload.content) } @@ -370,7 +363,7 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { onConfirmedEvent( event: ConfirmedTimelineEvent, stateEmitter: TypedEmitter | undefined, - encryptionEmitter: TypedEmitter | undefined, + _: TypedEmitter | undefined, ): void { check(event.remoteEvent.event.payload.case === 'memberPayload') const payload: MemberPayload = event.remoteEvent.event.payload.value @@ -412,8 +405,7 @@ export class StreamStateView_Members extends StreamStateView_AbstractContent { break case 'memberBlockchainTransaction': break - case 'mls': - this.mls.onConfirmedEvent(event, stateEmitter, encryptionEmitter) + case 'mls': // TODO: remove after proto update break case 'encryptionAlgorithm': break diff --git a/packages/sdk/src/streamStateView_Mls.ts b/packages/sdk/src/streamStateView_Mls.ts deleted file mode 100644 index 9e2749eaab..0000000000 --- a/packages/sdk/src/streamStateView_Mls.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { StreamStateView_AbstractContent } from './streamStateView_AbstractContent' -import TypedEmitter from 'typed-emitter' -import { ConfirmedTimelineEvent, RemoteTimelineEvent } from './types' -import { StreamEncryptionEvents, StreamStateEvents } from './streamEvents' -import { - MemberPayload_KeyPackage, - MemberPayload_Snapshot_Mls, - MemberPayload_Snapshot_Mls_Member, -} from '@river-build/proto' -import { check } from '@river-build/dlog' -import { PlainMessage } from '@bufbuild/protobuf' -import { logNever } from './check' -import { bytesToHex } from 'ethereum-cryptography/utils' -import { userIdFromAddress } from './id' - -export class StreamStateView_Mls extends StreamStateView_AbstractContent { - readonly streamId: string - externalGroupSnapshot?: Uint8Array - groupInfoMessage?: Uint8Array - members: { [key: string]: PlainMessage } = {} - epochSecrets: { [key: string]: Uint8Array } = {} - pendingKeyPackages: { [key: string]: MemberPayload_KeyPackage } = {} - welcomeMessagesMiniblockNum: { [key: string]: bigint } = {} - - constructor(streamId: string) { - super() - this.streamId = streamId - } - - applySnapshot(content: MemberPayload_Snapshot_Mls): void { - this.externalGroupSnapshot = content.externalGroupSnapshot - this.groupInfoMessage = content.groupInfoMessage - this.members = content.members - this.epochSecrets = content.epochSecrets - this.pendingKeyPackages = content.pendingKeyPackages - this.welcomeMessagesMiniblockNum = content.welcomeMessagesMiniblockNum - } - - appendEvent( - event: RemoteTimelineEvent, - _cleartext: Uint8Array | string | undefined, - _encryptionEmitter: TypedEmitter | undefined, - _stateEmitter: TypedEmitter | undefined, - ): void { - check(event.remoteEvent.event.payload.value?.content.case == 'mls') - const mlsEvent = event.remoteEvent.event.payload.value.content.value - switch (mlsEvent.content.case) { - case 'initializeGroup': - this.externalGroupSnapshot = mlsEvent.content.value.externalGroupSnapshot - this.groupInfoMessage = mlsEvent.content.value.groupInfoMessage - this.members[event.creatorUserId] = { - signaturePublicKeys: [mlsEvent.content.value.signaturePublicKey], - } - break - case 'externalJoin': - this.addSignaturePublicKey( - event.creatorUserId, - mlsEvent.content.value.signaturePublicKey, - ) - break - case 'epochSecrets': - for (const secret of mlsEvent.content.value.secrets) { - if (!this.epochSecrets[secret.epoch.toString()]) { - this.epochSecrets[secret.epoch.toString()] = secret.secret - } - } - break - case 'keyPackage': - this.pendingKeyPackages[bytesToHex(mlsEvent.content.value.signaturePublicKey)] = - mlsEvent.content.value - - break - case 'welcomeMessage': - for (const signatureKey of mlsEvent.content.value.signaturePublicKeys) { - const keyPackage = this.pendingKeyPackages[bytesToHex(signatureKey)] - if (keyPackage) { - this.addSignaturePublicKey( - userIdFromAddress(keyPackage.userAddress), - keyPackage.signaturePublicKey, - ) - } - delete this.pendingKeyPackages[bytesToHex(signatureKey)] - } - break - case undefined: - break - default: - logNever(mlsEvent.content) - } - } - - prependEvent( - _event: RemoteTimelineEvent, - _cleartext: Uint8Array | string | undefined, - _encryptionEmitter: TypedEmitter | undefined, - _stateEmitter: TypedEmitter | undefined, - ): void { - // - } - - onConfirmedEvent( - event: ConfirmedTimelineEvent, - stateEmitter: TypedEmitter | undefined, - encryptionEmitter: TypedEmitter | undefined, - ): void { - super.onConfirmedEvent(event, stateEmitter, encryptionEmitter) - if (event.remoteEvent.event.payload.value?.content.case !== 'mls') { - return - } - - const payload = event.remoteEvent.event.payload.value.content.value - switch (payload.content.case) { - case 'welcomeMessage': - for (const key of payload.content.value.signaturePublicKeys) { - const signatureKey = bytesToHex(key) - this.welcomeMessagesMiniblockNum[signatureKey] = event.miniblockNum - } - break - case undefined: - break - default: - break - } - } - - addSignaturePublicKey(userId: string, signaturePublicKey: Uint8Array): void { - if (!this.members[userId]) { - this.members[userId] = { - signaturePublicKeys: [], - } - } - this.members[userId].signaturePublicKeys.push(signaturePublicKey) - } -} diff --git a/packages/sdk/src/sync-agent/timeline/models/timeline-types.ts b/packages/sdk/src/sync-agent/timeline/models/timeline-types.ts index 25382cb061..5077759d6f 100644 --- a/packages/sdk/src/sync-agent/timeline/models/timeline-types.ts +++ b/packages/sdk/src/sync-agent/timeline/models/timeline-types.ts @@ -74,7 +74,6 @@ export type TimelineEvent_OneOf = | KeySolicitationEvent | MiniblockHeaderEvent | MemberBlockchainTransactionEvent - | MlsEvent | PinEvent | ReactionEvent | RedactedEvent @@ -105,7 +104,6 @@ export enum RiverTimelineEvent { KeySolicitation = 'm.key_solicitation', MemberBlockchainTransaction = 'm.member_blockchain_transaction', MiniblockHeader = 'm.miniblockheader', - Mls = 'm.mls', Pin = 'm.pin', Reaction = 'm.reaction', RedactedEvent = 'm.redacted_event', @@ -221,10 +219,6 @@ export interface UnpinEvent { unpinnedEventId: string } -export interface MlsEvent { - kind: RiverTimelineEvent.Mls -} - export interface StreamEncryptionAlgorithmEvent { kind: RiverTimelineEvent.StreamEncryptionAlgorithm algorithm?: string diff --git a/packages/sdk/src/sync-agent/timeline/models/timelineEvent.ts b/packages/sdk/src/sync-agent/timeline/models/timelineEvent.ts index 7344ed5405..1dad8d60b9 100644 --- a/packages/sdk/src/sync-agent/timeline/models/timelineEvent.ts +++ b/packages/sdk/src/sync-agent/timeline/models/timelineEvent.ts @@ -361,11 +361,9 @@ function toTownsContent_MemberPayload( unpinnedEventId: bin_toHexString(value.content.value.eventId), } satisfies UnpinEvent, } - case 'mls': + case 'mls': // TODO: remove after proto update return { - content: { - kind: RiverTimelineEvent.Mls, - }, + error: 'not supported', } case 'encryptionAlgorithm': return { @@ -992,8 +990,6 @@ export function getFallbackContent( return `pinnedEventId: ${content.pinnedEventId} by: ${content.userId}` case RiverTimelineEvent.Unpin: return `unpinnedEventId: ${content.unpinnedEventId} by: ${content.userId}` - case RiverTimelineEvent.Mls: - return `mlsEvent` case RiverTimelineEvent.UserBlockchainTransaction: return getFallbackContent_BlockchainTransaction(content.transaction) case RiverTimelineEvent.MemberBlockchainTransaction: diff --git a/packages/sdk/src/tests/multi_ne/memberMetadata.test.ts b/packages/sdk/src/tests/multi_ne/memberMetadata.test.ts index 159c130a23..48954af353 100644 --- a/packages/sdk/src/tests/multi_ne/memberMetadata.test.ts +++ b/packages/sdk/src/tests/multi_ne/memberMetadata.test.ts @@ -676,20 +676,20 @@ describe('memberMetadataTests', () => { undefined, ) - // set mls enabled to mls_0.0.1 + const newAlgorithm = 'mega_v1' const truePromise = makeDonePromise() bobsClient.once('streamEncryptionAlgorithmUpdated', (updatedStreamId, value) => { expect(updatedStreamId).toBe(channelId) - expect(value).toBe('mls_0.0.1') + expect(value).toBe(newAlgorithm) truePromise.done() }) await expect( - bobsClient.setStreamEncryptionAlgorithm(channelId, 'mls_0.0.1'), + bobsClient.setStreamEncryptionAlgorithm(channelId, newAlgorithm), ).resolves.not.toThrow() await truePromise.expectToSucceed() expect(bobsClient.stream(channelId)?.view.membershipContent.encryptionAlgorithm).toBe( - 'mls_0.0.1', + newAlgorithm, ) // toggle back to to undefined diff --git a/packages/sdk/src/tests/multi_ne/mls/mls.test.ts b/packages/sdk/src/tests/multi_ne/mls/mls.test.ts deleted file mode 100644 index 59829615c1..0000000000 --- a/packages/sdk/src/tests/multi_ne/mls/mls.test.ts +++ /dev/null @@ -1,788 +0,0 @@ -/** - * @group main - */ - -import { makeTestClient, waitFor } from '../../testUtils' -import { Client } from '../../../client' -import { PlainMessage } from '@bufbuild/protobuf' -import { MemberPayload_Mls, MemberPayload_Mls_WelcomeMessage } from '@river-build/proto' -import { - ExternalClient, - Group as MlsGroup, - Client as MlsClient, - ExternalSnapshot, - MlsMessage, - ExportedTree, - ClientOptions as MlsClientOptions, -} from '@river-build/mls-rs-wasm' -import { randomBytes } from 'crypto' -import { bin_equal, check } from '@river-build/dlog' -import { addressFromUserId } from '../../../id' -import { bytesToHex } from 'ethereum-cryptography/utils' -import { isDefined } from '../../../check' -import { ParsedMiniblock } from '../../../types' -import { fail } from 'assert' - -describe('mlsTests', () => { - let clients: Client[] = [] - const makeInitAndStartClient = async () => { - const client = await makeTestClient() - await client.initializeUser() - client.startSync() - clients.push(client) - return client - } - - let bobClient: Client - let bobMlsGroup: MlsGroup - let aliceClient: Client - let bobMlsClient: MlsClient - let aliceMlsClient: MlsClient - let aliceMlsClient2: MlsClient - - // state data to retain between tests - let streamId: string - let latestGroupInfoMessage: Uint8Array - let latestExternalGroupSnapshot: Uint8Array - let latestAliceMlsKeyPackage: Uint8Array - let welcomeMessageCommit: Uint8Array - const commits: Uint8Array[] = [] - - beforeAll(async () => { - bobClient = await makeInitAndStartClient() - aliceClient = await makeInitAndStartClient() - const clientOptions: MlsClientOptions = { - withAllowExternalCommit: true, - withRatchetTreeExtension: false, - } - bobMlsClient = await MlsClient.create(new Uint8Array(randomBytes(32)), clientOptions) - aliceMlsClient = await MlsClient.create(new Uint8Array(randomBytes(32)), clientOptions) - aliceMlsClient2 = await MlsClient.create(new Uint8Array(randomBytes(32)), clientOptions) - bobMlsGroup = await bobMlsClient.createGroup() - const { streamId: dmStreamId } = await bobClient.createDMChannel(aliceClient.userId) - await bobClient.waitForStream(dmStreamId) - await aliceClient.waitForStream(dmStreamId) - streamId = dmStreamId - }) - - afterAll(async () => { - for (const client of clients) { - await client.stop() - } - clients = [] - }) - - afterEach(async () => { - for (const commit of commits) { - try { - const mlsMessage = MlsMessage.fromBytes(commit) - await bobMlsGroup.processIncomingMessage(mlsMessage) - } catch { - // noop - } - } - }) - - function makeMlsPayloadInitializeGroup( - signaturePublicKey: Uint8Array, - externalGroupSnapshot: Uint8Array, - groupInfoMessage: Uint8Array, - ): PlainMessage { - return { - content: { - case: 'initializeGroup', - value: { - signaturePublicKey: signaturePublicKey, - externalGroupSnapshot: externalGroupSnapshot, - groupInfoMessage: groupInfoMessage, - }, - }, - } - } - - function makeMlsPayloadExternalJoin( - signaturePublicKey: Uint8Array, - commit: Uint8Array, - groupInfoMessage: Uint8Array, - ): PlainMessage { - return { - content: { - case: 'externalJoin', - value: { - signaturePublicKey: signaturePublicKey, - commit: commit, - groupInfoMessage: groupInfoMessage, - }, - }, - } - } - - function makeMlsPayloadEpochSecrets( - secrets: { epoch: bigint; secret: Uint8Array }[], - ): PlainMessage { - return { - content: { - case: 'epochSecrets', - value: { - secrets: secrets, - }, - }, - } - } - - function makeMlsPayloadKeyPackage( - userAddress: Uint8Array, - signaturePublicKey: Uint8Array, - keyPackage: Uint8Array, - ): PlainMessage { - return { - content: { - case: 'keyPackage', - value: { - userAddress, - signaturePublicKey, - keyPackage, - }, - }, - } - } - - function makeMlsPayloadWelcomeMessage( - commit: Uint8Array, - signaturePublicKeys: Uint8Array[], - groupInfoMessage: Uint8Array, - welcomeMessages: Uint8Array[], - ): PlainMessage { - return { - content: { - case: 'welcomeMessage', - value: { - commit, - signaturePublicKeys, - groupInfoMessage, - welcomeMessages, - }, - }, - } - } - - // helper function to create a group + external snapshot - async function createGroupInfoAndExternalSnapshot(group: MlsGroup): Promise<{ - groupInfoMessage: Uint8Array - externalGroupSnapshot: Uint8Array - }> { - const groupInfoMessage = await group.groupInfoMessageAllowingExtCommit(false) - const tree = group.exportTree() - const externalClient = new ExternalClient() - const externalGroup = externalClient.observeGroup( - groupInfoMessage.toBytes(), - tree.toBytes(), - ) - - const externalGroupSnapshot = (await externalGroup).snapshot() - return { - groupInfoMessage: groupInfoMessage.toBytes(), - externalGroupSnapshot: externalGroupSnapshot.toBytes(), - } - } - - async function commitExternal( - client: MlsClient, - groupInfoMessage: Uint8Array, - externalGroupSnapshot: Uint8Array, - ): Promise<{ commit: Uint8Array; groupInfoMessage: Uint8Array; group: MlsGroup }> { - const externalClient = new ExternalClient() - const externalSnapshot = ExternalSnapshot.fromBytes(externalGroupSnapshot) - const externalGroup = await externalClient.loadGroup(externalSnapshot) - const tree = externalGroup.exportTree() - const exportedTree = ExportedTree.fromBytes(tree) - const mlsGroupInfoMessage = MlsMessage.fromBytes(groupInfoMessage) - const commitOutput = await client.commitExternal(mlsGroupInfoMessage, exportedTree) - const updatedGroupInfoMessage = await commitOutput.group.groupInfoMessageAllowingExtCommit( - false, - ) - return { - commit: commitOutput.commit.toBytes(), - groupInfoMessage: updatedGroupInfoMessage.toBytes(), - group: commitOutput.group, - } - } - - test('invalid signature public key is not accepted', async () => { - const group = await bobMlsClient.createGroup() - const { groupInfoMessage, externalGroupSnapshot } = - await createGroupInfoAndExternalSnapshot(group) - - const mlsPayload = makeMlsPayloadInitializeGroup( - bobMlsClient.signaturePublicKey().slice(1), // slice 1 byte to make it invalid - externalGroupSnapshot, - groupInfoMessage, - ) - await expect(bobClient._debugSendMls(streamId, mlsPayload)).rejects.toThrow( - 'INVALID_PUBLIC_SIGNATURE_KEY', - ) - }) - - test('invalid external MLS group is not accepted', async () => { - const mlsPayload = makeMlsPayloadInitializeGroup( - bobMlsClient.signaturePublicKey(), - new Uint8Array([]), - new Uint8Array([]), - ) - await expect(bobClient._debugSendMls(streamId, mlsPayload)).rejects.to.toThrow( - 'INVALID_EXTERNAL_GROUP', - ) - }) - - test('mismatching group ids throws an error', async () => { - const group1 = await bobMlsClient.createGroup() - const group2 = await bobMlsClient.createGroup() - const { externalGroupSnapshot: externalGroupSnapshot1 } = - await createGroupInfoAndExternalSnapshot(group1) - const { groupInfoMessage: groupInfoMessage2 } = await createGroupInfoAndExternalSnapshot( - group2, - ) - - const mlsPayload = makeMlsPayloadInitializeGroup( - bobMlsClient.signaturePublicKey(), - externalGroupSnapshot1, - groupInfoMessage2, - ) - await expect(bobClient._debugSendMls(streamId, mlsPayload)).rejects.toThrow( - 'INVALID_GROUP_INFO_GROUP_ID_MISMATCH', - ) - }) - - test('epoch not at 0 throws error', async () => { - const groupAtEpoch0 = await bobMlsClient.createGroup() - const groupInfoMessageAtEpoch0 = await groupAtEpoch0.groupInfoMessageAllowingExtCommit(true) - const output = await aliceMlsClient.commitExternal(groupInfoMessageAtEpoch0) - const groupAtEpoch1 = output.group - const { groupInfoMessage, externalGroupSnapshot } = - await createGroupInfoAndExternalSnapshot(groupAtEpoch1) - - const mlsPayload = makeMlsPayloadInitializeGroup( - aliceMlsClient.signaturePublicKey(), - externalGroupSnapshot, - groupInfoMessage, - ) - await expect(aliceClient._debugSendMls(streamId, mlsPayload)).rejects.toThrow( - 'INVALID_GROUP_INFO_EPOCH', - ) - }) - - test('invalid group info for initialize group is rejected', async () => { - const { groupInfoMessage, externalGroupSnapshot } = - await createGroupInfoAndExternalSnapshot(bobMlsGroup) - // tamper with the message a little bit - const invalidGroupInfoMessage = groupInfoMessage - invalidGroupInfoMessage[invalidGroupInfoMessage.length - 2] += 1 // make it invalid - const payload = makeMlsPayloadInitializeGroup( - bobMlsClient.signaturePublicKey(), - externalGroupSnapshot, - groupInfoMessage, - ) - await expect(bobClient._debugSendMls(streamId, payload)).rejects.toThrow( - 'INVALID_GROUP_INFO', - ) - }) - - test('clients can create MLS Groups in channels', async () => { - const { groupInfoMessage, externalGroupSnapshot } = - await createGroupInfoAndExternalSnapshot(bobMlsGroup) - const mlsPayload = makeMlsPayloadInitializeGroup( - bobMlsClient.signaturePublicKey(), - externalGroupSnapshot, - groupInfoMessage, - ) - await expect(bobClient._debugSendMls(streamId, mlsPayload)).resolves.not.toThrow() - - // save for later - latestExternalGroupSnapshot = externalGroupSnapshot - latestGroupInfoMessage = groupInfoMessage - }) - - test('initializing MLS groups twice throws an error', async () => { - const group = await bobMlsClient.createGroup() - const { groupInfoMessage, externalGroupSnapshot } = - await createGroupInfoAndExternalSnapshot(group) - const mlsPayload = makeMlsPayloadInitializeGroup( - bobMlsClient.signaturePublicKey(), - externalGroupSnapshot, - groupInfoMessage, - ) - await expect(bobClient._debugSendMls(streamId, mlsPayload)).rejects.toThrow( - 'group already initialized', - ) - }) - - test('MLS group is snapshotted', async () => { - // force a snapshot - await bobClient.debugForceMakeMiniblock(streamId, { forceSnapshot: true }) - // fetch the stream again and check that the MLS group is snapshotted - const streamAfterSnapshot = await bobClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls - expect(mls.externalGroupSnapshot).toBeDefined() - expect(mls.groupInfoMessage).toBeDefined() - expect(mls.externalGroupSnapshot!.length).toBeGreaterThan(0) - expect(mls.groupInfoMessage!.length).toBeGreaterThan(0) - expect(bin_equal(mls.externalGroupSnapshot, latestExternalGroupSnapshot)).toBe(true) - expect(bin_equal(mls.groupInfoMessage, latestGroupInfoMessage)).toBe(true) - }) - - test('External commits with invalid signature public keys are not accepted', async () => { - const { commit: aliceCommit, groupInfoMessage: aliceGroupInfoMessage } = - await commitExternal( - aliceMlsClient, - latestGroupInfoMessage, - latestExternalGroupSnapshot, - ) - - const aliceMlsPayload = makeMlsPayloadExternalJoin( - new Uint8Array([1, 2, 3]), - aliceCommit, - aliceGroupInfoMessage, - ) - await expect(aliceClient._debugSendMls(streamId, aliceMlsPayload)).rejects.toThrow( - 'INVALID_PUBLIC_SIGNATURE_KEY', - ) - }) - - test('Invalid group info for external commits is rejected', async () => { - const { commit, groupInfoMessage } = await commitExternal( - aliceMlsClient, - latestGroupInfoMessage, - latestExternalGroupSnapshot, - ) - // tamper with the message a little bit - const invalidGroupInfoMessage = groupInfoMessage - invalidGroupInfoMessage[invalidGroupInfoMessage.length - 2] += 1 // make it invalid - - const aliceMlsPayload = makeMlsPayloadExternalJoin( - aliceMlsClient.signaturePublicKey(), - commit, - invalidGroupInfoMessage, - ) - await expect(aliceClient._debugSendMls(streamId, aliceMlsPayload)).rejects.toThrow( - 'INVALID_GROUP_INFO', - ) - }) - - test('Valid external commits are accepted', async () => { - const { commit: aliceCommit, groupInfoMessage: aliceGroupInfoMessage } = - await commitExternal( - aliceMlsClient, - latestGroupInfoMessage, - latestExternalGroupSnapshot, - ) - - const aliceMlsPayload = makeMlsPayloadExternalJoin( - aliceMlsClient.signaturePublicKey(), - aliceCommit, - aliceGroupInfoMessage, - ) - await expect(aliceClient._debugSendMls(streamId, aliceMlsPayload)).resolves.not.toThrow() - latestGroupInfoMessage = aliceGroupInfoMessage - commits.push(aliceCommit) - }) - - test('MLS group is snapshotted after external commit', async () => { - // force another snapshot - await expect( - bobClient.debugForceMakeMiniblock(streamId, { forceSnapshot: true }), - ).resolves.not.toThrow() - - // this time, the snapshot should contain the group info message from Alice - // the only way it can end up in the snapshot is if the external join was successfully snapshotted - // by the node - const streamAfterSnapshot = await aliceClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls - expect(mls.externalGroupSnapshot).toBeDefined() - expect(mls.groupInfoMessage).toBeDefined() - expect(mls.externalGroupSnapshot!.length).toBeGreaterThan(0) - expect(mls.groupInfoMessage!.length).toBeGreaterThan(0) - expect(bin_equal(mls.groupInfoMessage, latestGroupInfoMessage)).toBe(true) - }) - - test('commits are snapshotted after external commit', async () => { - const streamAfterSnapshot = await aliceClient.getStream(streamId) - const lastSnapshotMiniblockNum = streamAfterSnapshot.miniblockInfo!.min - const header = await bobClient.getMiniblockHeader(streamId, lastSnapshotMiniblockNum) - const commitsSinceLastSnapshot = header.snapshot?.members?.mls?.commitsSinceLastSnapshot - expect(commitsSinceLastSnapshot).toBeDefined() - expect(commitsSinceLastSnapshot!.length).toBe(commits.length) - for (let i = 0; i < commits.length; i++) { - expect(bin_equal(commitsSinceLastSnapshot![i], commits[i])).toBe(true) - } - }) - - test('Signature public keys are mapped per user in the snapshot', async () => { - // force snapshot - await expect( - bobClient.debugForceMakeMiniblock(streamId, { forceSnapshot: true }), - ).resolves.not.toThrow() - - const bobSignaturePublicKey = bobMlsClient.signaturePublicKey() - const aliceSignaturePublicKey = aliceMlsClient.signaturePublicKey() - // verify that the signature public keys are mapped per user - // and that the signature public keys are correct - const streamAfterSnapshot = await bobClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls.members - expect(mls[bobClient.userId].signaturePublicKeys.length).toBe(1) - expect(mls[aliceClient.userId].signaturePublicKeys.length).toBe(1) - expect(bin_equal(mls[bobClient.userId].signaturePublicKeys[0], bobSignaturePublicKey)).toBe( - true, - ) - expect( - bin_equal(mls[aliceClient.userId].signaturePublicKeys[0], aliceSignaturePublicKey), - ).toBe(true) - }) - - test('epoch secrets are accepted', async () => { - const bobMlsSecretsPayload = makeMlsPayloadEpochSecrets([ - { epoch: 1n, secret: new Uint8Array([1, 2, 3, 4]) }, - { epoch: 2n, secret: new Uint8Array([3, 4, 5, 6]) }, // bogus for now - ]) - - await expect(bobClient._debugSendMls(streamId, bobMlsSecretsPayload)).resolves.not.toThrow() - - // verify that the epoch secrets have been picked up in the stream state view - await waitFor(() => { - const mls = bobClient.streams.get(streamId)?.view.membershipContent.mls - expect(mls).toBeDefined() - expect(bin_equal(mls!.epochSecrets[1n.toString()], new Uint8Array([1, 2, 3, 4]))).toBe( - true, - ) - expect(bin_equal(mls!.epochSecrets[2n.toString()], new Uint8Array([3, 4, 5, 6]))).toBe( - true, - ) - }) - }) - - test('epoch secrets can only be sent once', async () => { - // sending the same epoch twice returns an error - const bobMlsSecretsPayload = makeMlsPayloadEpochSecrets([ - { epoch: 1n, secret: new Uint8Array([1, 2, 3, 4]) }, - { epoch: 2n, secret: new Uint8Array([3, 4, 5, 6]) }, // bogus for now - ]) - - await expect(bobClient._debugSendMls(streamId, bobMlsSecretsPayload)).rejects.toThrow( - 'epoch already exists', - ) - }) - - test('epoch secrets are snapshotted', async () => { - // force snapshot - await expect( - bobClient.debugForceMakeMiniblock(streamId, { forceSnapshot: true }), - ).resolves.not.toThrow() - - // verify that the epoch secrets are picked up in the snapshot - const streamAfterSnapshot = await bobClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls - expect(bin_equal(mls.epochSecrets[1n.toString()], new Uint8Array([1, 2, 3, 4]))).toBe(true) - expect(bin_equal(mls.epochSecrets[2n.toString()], new Uint8Array([3, 4, 5, 6]))).toBe(true) - }) - - test('clients can publish key packages', async () => { - const keyPackage = await aliceMlsClient2.generateKeyPackageMessage() - const alicePayload = makeMlsPayloadKeyPackage( - addressFromUserId(aliceClient.userId), - aliceMlsClient2.signaturePublicKey(), - keyPackage.toBytes(), - ) - - await expect(aliceClient._debugSendMls(streamId, alicePayload)).resolves.not.toThrow() - latestAliceMlsKeyPackage = keyPackage.toBytes() - }) - - test('key packages are broadcasted to all members', async () => { - const aliceMlsClient2SignaturePublicKey = aliceMlsClient2.signaturePublicKey() - await waitFor(() => { - const stream = bobClient.streams.get(streamId) - check(Object.values(stream!.view.membershipContent.mls.pendingKeyPackages).length > 0) - const kp = - stream!.view.membershipContent.mls.pendingKeyPackages[ - bytesToHex(aliceMlsClient2SignaturePublicKey) - ].keyPackage - check(bin_equal(kp, latestAliceMlsKeyPackage)) - }) - }) - - test("clients can publish key packages twice (but it isn't encouraged)", async () => { - const alicePayload = makeMlsPayloadKeyPackage( - addressFromUserId(aliceClient.userId), - aliceMlsClient2.signaturePublicKey(), - latestAliceMlsKeyPackage, - ) - await expect(aliceClient._debugSendMls(streamId, alicePayload)).resolves.not.toThrow() - }) - - test('key packages are snapshotted', async () => { - // force snapshot - await expect( - bobClient.debugForceMakeMiniblock(streamId, { forceSnapshot: true }), - ).resolves.not.toThrow() - - // verify that the key package is picked up in the snapshot - const streamAfterSnapshot = await bobClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls - expect(Object.values(mls.pendingKeyPackages).length).toBe(1) - const key = bytesToHex(aliceMlsClient2.signaturePublicKey()) - expect(bin_equal(mls.pendingKeyPackages[key].keyPackage, latestAliceMlsKeyPackage)).toBe( - true, - ) - }) - - // TODO: Add more tests once we have support for clearing commits in mls-rs-wasm - test('invalid group infos are not accepted', async () => { - const payload = makeMlsPayloadWelcomeMessage( - new Uint8Array(), - [new Uint8Array([1, 2, 3])], - latestGroupInfoMessage, // bogus, no longer valid - [new Uint8Array([4, 5, 6])], - ) - await expect(bobClient._debugSendMls(streamId, payload)).rejects.to.toThrow( - 'INVALID_GROUP_INFO_EPOCH', - ) - }) - - test('signature key count need to match the number of added proposals', async () => { - const mls = bobClient.streams.get(streamId)!.view.membershipContent.mls - const keyPackage = Object.values(mls.pendingKeyPackages)[0] - const kp = MlsMessage.fromBytes(keyPackage.keyPackage) - const commitOutput = await bobMlsGroup.addMember(kp) - - // at this point, the commit is still pending - bobMlsGroup.clearPendingCommit() - - const groupInfoMessage = commitOutput.externalCommitGroupInfo - const commit = commitOutput.commitMessage.toBytes() - const welcomeMessages = commitOutput.welcomeMessages.map((wm) => wm.toBytes()) - - const payload = makeMlsPayloadWelcomeMessage( - commit, - [keyPackage.signaturePublicKey, new Uint8Array([1, 2, 3])], // add additional bogus signature key - groupInfoMessage!.toBytes(), - welcomeMessages, - ) - await expect(aliceClient._debugSendMls(streamId, payload)).rejects.to.toThrow( - 'INVALID_PUBLIC_SIGNATURE_KEY', - ) - }) - - test('signature keys need to match the keys inside the added proposals', async () => { - const mls = bobClient.streams.get(streamId)!.view.membershipContent.mls - const keyPackage = Object.values(mls.pendingKeyPackages)[0] - const kp = MlsMessage.fromBytes(keyPackage.keyPackage) - const commitOutput = await bobMlsGroup.addMember(kp) - - // at this point, the commit is still pending - bobMlsGroup.clearPendingCommit() - - const groupInfoMessage = commitOutput.externalCommitGroupInfo - const commit = commitOutput.commitMessage.toBytes() - const welcomeMessages = commitOutput.welcomeMessages.map((wm) => wm.toBytes()) - - const payload = makeMlsPayloadWelcomeMessage( - commit, - [new Uint8Array([1, 2, 3])], // add bogus signature key - groupInfoMessage!.toBytes(), - welcomeMessages, - ) - await expect(aliceClient._debugSendMls(streamId, payload)).rejects.to.toThrow( - 'INVALID_PUBLIC_SIGNATURE_KEY', - ) - }) - - test('invalid welcome messages return an error', async () => { - const mls = bobClient.streams.get(streamId)!.view.membershipContent.mls - const keyPackage = Object.values(mls.pendingKeyPackages)[0] - const kp = MlsMessage.fromBytes(keyPackage.keyPackage) - const commitOutput = await bobMlsGroup.addMember(kp) - // at this point, the commit is still pending - bobMlsGroup.clearPendingCommit() - - const groupInfoMessage = commitOutput.externalCommitGroupInfo - const commit = commitOutput.commitMessage.toBytes() - const welcomeMessages = commitOutput.welcomeMessages.map((wm) => wm.toBytes()) - - const payload = makeMlsPayloadWelcomeMessage( - commit, - [keyPackage.signaturePublicKey], - groupInfoMessage!.toBytes(), - welcomeMessages.map((wm) => wm.reverse()), // modify the content - ) - await expect(aliceClient._debugSendMls(streamId, payload)).rejects.toThrow( - 'INVALID_WELCOME_MESSAGE', - ) - }) - - test('invalid group info for welcome messages is rejected', async () => { - const mls = bobClient.streams.get(streamId)!.view.membershipContent.mls - const keyPackage = Object.values(mls.pendingKeyPackages)[0] - const kp = MlsMessage.fromBytes(keyPackage.keyPackage) - const commitOutput = await bobMlsGroup.addMember(kp) - bobMlsGroup.clearPendingCommit() - const groupInfoMessage = commitOutput.externalCommitGroupInfo!.toBytes() - - // tamper with the message a little bit - const invalidGroupInfoMessage = groupInfoMessage - invalidGroupInfoMessage[invalidGroupInfoMessage.length - 2] += 1 // make it invalid - - const commit = commitOutput.commitMessage.toBytes() - const welcomeMessages = commitOutput.welcomeMessages.map((wm) => wm.toBytes()) - - const payload = makeMlsPayloadWelcomeMessage( - commit, - [keyPackage.signaturePublicKey], - invalidGroupInfoMessage, - welcomeMessages, - ) - await expect(aliceClient._debugSendMls(streamId, payload)).rejects.toThrow( - 'INVALID_GROUP_INFO', - ) - }) - - test('clients can add other members from key packages', async () => { - const mls = bobClient.streams.get(streamId)!.view.membershipContent.mls - const keyPackage = Object.values(mls.pendingKeyPackages)[0] - const kp = MlsMessage.fromBytes(keyPackage.keyPackage) - const commitOutput = await bobMlsGroup.addMember(kp) - - // at this point, the commit is still pending - bobMlsGroup.clearPendingCommit() - - const groupInfoMessage = commitOutput.externalCommitGroupInfo - const commit = commitOutput.commitMessage.toBytes() - const welcomeMessages = commitOutput.welcomeMessages.map((wm) => wm.toBytes()) - - const payload = makeMlsPayloadWelcomeMessage( - commit, - [keyPackage.signaturePublicKey], - groupInfoMessage!.toBytes(), - welcomeMessages, - ) - await expect(aliceClient._debugSendMls(streamId, payload)).resolves.not.toThrow() - await expect( - bobMlsGroup.processIncomingMessage(commitOutput.commitMessage), - ).resolves.not.toThrow() - latestGroupInfoMessage = groupInfoMessage!.toBytes() - commits.push(commit) - welcomeMessageCommit = commit - }) - - test('key packages are cleared after being applied', async () => { - const aliceMlsClient2SignaturePublicKey = aliceMlsClient2.signaturePublicKey() - await waitFor(() => { - const stream = bobClient.streams.get(streamId) - check(Object.values(stream!.view.membershipContent.mls.pendingKeyPackages).length === 0) - const key = bytesToHex(aliceMlsClient2SignaturePublicKey) - const kp = stream!.view.membershipContent.mls.pendingKeyPackages[key] - check(!isDefined(kp)) - }) - }) - - test('devices added from key packages are added to the members', async () => { - await waitFor(() => { - const mls = bobClient.streams.get(streamId)!.view.membershipContent.mls - expect(mls.members[aliceClient.userId].signaturePublicKeys.length).toBe(2) - }) - }) - - // skipped after no longer storing miniblock headers generically, data should be saved in mls view - test.skip('correct external group info is returned', async () => { - const externalGroupInfo = (await bobClient.getMlsExternalGroupInfo(streamId))! - const externalClient = new ExternalClient() - const externalGroupSnapshot = ExternalSnapshot.fromBytes( - externalGroupInfo.externalGroupSnapshot, - ) - expect(externalGroupInfo.commits.length).toBe(1) - - let latestValidGroupInfoMessage = externalGroupInfo.groupInfoMessage - const externalGroup = await externalClient.loadGroup(externalGroupSnapshot) - for (const commit of externalGroupInfo.commits) { - try { - const mlsMessage = MlsMessage.fromBytes(commit.commit) - await externalGroup.processIncomingMessage(mlsMessage) - latestValidGroupInfoMessage = commit.groupInfoMessage - } catch { - // catch, in case this is an invalid commit - } - } - - expect(bin_equal(latestValidGroupInfoMessage, latestGroupInfoMessage)).toBe(true) - - const aliceThrowawayClient = await MlsClient.create(new Uint8Array(randomBytes(32))) - const { - commit: aliceCommit, - groupInfoMessage: aliceGroupInfoMessage, - group: aliceGroup, - } = await commitExternal( - aliceThrowawayClient, - latestValidGroupInfoMessage, - externalGroup.snapshot().toBytes(), - ) - const aliceMlsPayload = makeMlsPayloadExternalJoin( - aliceThrowawayClient.signaturePublicKey(), - aliceCommit, - aliceGroupInfoMessage, - ) - - await expect( - bobMlsGroup.processIncomingMessage(MlsMessage.fromBytes(aliceCommit)), - ).resolves.not.toThrow() - - expect(bobMlsGroup.currentEpoch).toBe(aliceGroup.currentEpoch) - await expect(aliceClient._debugSendMls(streamId, aliceMlsPayload)).resolves.not.toThrow() - commits.push(aliceCommit) - }) - - // skipped after no longer storing miniblock headers generically, data should be saved in mls view - test.skip('devices added from key packages are snapshotted', async () => { - // force snapshot - await expect( - bobClient.debugForceMakeMiniblock(streamId, { forceSnapshot: true }), - ).resolves.not.toThrow() - - // verify that the key package is picked up in the snapshot - const streamAfterSnapshot = await bobClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls - expect(mls.members[aliceClient.userId].signaturePublicKeys.length).toBe(3) - }) - - // skipped after no longer storing miniblock headers generically, data should be saved in mls view - test.skip('the snapshot contains a pointer to the miniblock containing the welcome message', async () => { - function getWelcomeMessage(miniblock: ParsedMiniblock): MemberPayload_Mls_WelcomeMessage { - for (const payload of miniblock.events.map((e) => e.event.payload)) { - if (payload.value?.content.case !== 'mls') { - continue - } - if (payload.value.content.value.content.case !== 'welcomeMessage') { - continue - } - return payload.value.content.value.content.value - } - fail('no welcome message found') - } - - const streamAfterSnapshot = await aliceClient.getStream(streamId) - const mls = streamAfterSnapshot.membershipContent.mls - const signature = aliceMlsClient2.signaturePublicKey() - const miniblockNum = mls.welcomeMessagesMiniblockNum[bytesToHex(signature)] - expect(miniblockNum).toBeGreaterThan(0n) - - const { miniblocks } = await aliceClient.getMiniblocks( - streamId, - miniblockNum, - miniblockNum + 1n, - ) - - expect(miniblocks.length).toBe(1) - const welcomeMessage = getWelcomeMessage(miniblocks[0]) - expect(bin_equal(welcomeMessage.commit, welcomeMessageCommit)).toBe(true) - expect( - welcomeMessage.signaturePublicKeys.find((val) => bin_equal(val, signature)), - ).toBeDefined() - }) -}) diff --git a/packages/sdk/src/tests/unit/mls/epoch.test.ts b/packages/sdk/src/tests/unit/mls/epoch.test.ts deleted file mode 100644 index 3642a6be6d..0000000000 --- a/packages/sdk/src/tests/unit/mls/epoch.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @group main - */ - -import { beforeEach, describe, expect } from 'vitest' -import { CipherSuite } from '@river-build/mls-rs-wasm' -import { - EpochSecret, - EpochSecretService, - IEpochSecretStore, - InMemoryEpochSecretStore, -} from '../../../mls/epoch' -import { EncryptedData } from '@river-build/proto' - -const encoder = new TextEncoder() - -describe('mlsEpochTests', () => { - let epochStore: IEpochSecretStore - let epochService: EpochSecretService - const cipherSuite = new CipherSuite() - const secret = encoder.encode('secret') - const epoch = 1n - const streamId = 'stream' - - beforeEach(() => { - epochStore = new InMemoryEpochSecretStore() - epochService = new EpochSecretService(cipherSuite, epochStore) - }) - - it('shouldCreateEpochSecretService', () => { - expect(cipherSuite).toBeDefined() - expect(epochStore).toBeDefined() - expect(epochService).toBeDefined() - }) - - it('shouldStartEmpty', async () => { - const epochSecret = epochService.getEpochSecret(streamId, epoch) - expect(epochSecret).toBeUndefined() - }) - - it('shouldAddOpenEpochSecret', async () => { - await epochService.addOpenEpochSecret(streamId, epoch, secret) - const epochSecret = epochService.getEpochSecret(streamId, epoch) - expect(epochSecret).toBeDefined() - }) - - it('shouldAddClosedEpochSecret', async () => { - await epochService.addAnnouncedSealedEpochSecret(streamId, epoch, secret) - const epochSecret = epochService.getEpochSecret(streamId, epoch) - expect(epochSecret).toBeDefined() - }) - - describe('openEpochSecret', () => { - let epochSecret: EpochSecret - - beforeEach(async () => { - await epochService.addOpenEpochSecret(streamId, epoch, secret) - epochSecret = epochService.getEpochSecret(streamId, epoch)! - expect(epochSecret).toBeDefined() - }) - - it('shouldStartOpen', () => { - expect(epochSecret.sealedEpochSecret).toBeUndefined() - }) - - it('shouldStartDerived', () => { - expect(epochSecret.derivedKeys).toBeDefined() - }) - - it('shouldStartNotAnnounced', () => { - expect(epochSecret.announced).toBeFalsy() - }) - - it('shouldSealEpochSecret', async () => { - const epoch2 = 2n - const secret2 = encoder.encode('secret2') - - await epochService.addOpenEpochSecret(streamId, epoch2, secret2) - - const epochSecret2 = epochService.getEpochSecret(streamId, epoch2)! - expect(epochSecret2).toBeDefined() - expect(epochSecret2.derivedKeys).toBeDefined() - await epochService.sealEpochSecret(epochSecret, epochSecret2.derivedKeys!) - - epochSecret = epochService.getEpochSecret(streamId, epoch)! - expect(epochSecret.sealedEpochSecret).toBeDefined() - }) - }) - - describe('sealedEpochSecret', () => { - let epochStore2: IEpochSecretStore - let epochService2: EpochSecretService - - let epochSecret: EpochSecret - let epochSecret2: EpochSecret - - const epoch2 = 2n - const secret2 = encoder.encode('secret2') - - // Create another service that seals it's epoch secret and gives us - beforeEach(async () => { - epochStore2 = new InMemoryEpochSecretStore() - epochService2 = new EpochSecretService(cipherSuite, epochStore2) - - await epochService2.addOpenEpochSecret(streamId, epoch, secret) - await epochService2.addOpenEpochSecret(streamId, epoch2, secret2) - - epochSecret = epochService2.getEpochSecret(streamId, epoch)! - epochSecret2 = epochService2.getEpochSecret(streamId, epoch2)! - - await epochService2.sealEpochSecret(epochSecret, epochSecret2.derivedKeys!) - - epochSecret = epochService2.getEpochSecret(streamId, epoch)! - - await epochService.addAnnouncedSealedEpochSecret( - streamId, - epoch, - epochSecret.sealedEpochSecret!, - ) - epochSecret = epochService.getEpochSecret(streamId, epoch)! - }) - - it('shouldStartSealed', () => { - expect(epochSecret.sealedEpochSecret).toBeDefined() - }) - - it('shouldStartAnnounced', () => { - expect(epochSecret.announced).toBeTruthy() - }) - - it('canBeOpenedWithDerivedKeys', async () => { - await epochService.openSealedEpochSecret(epochSecret, epochSecret2.derivedKeys!) - }) - - it('cannotBeOpenedWithWrongDerivedKeys', async () => { - const wrongSecret = encoder.encode('wrongSecret') - await epochService.addOpenEpochSecret(streamId, epoch2, wrongSecret) - epochSecret2 = epochService.getEpochSecret(streamId, epoch2)! - await expect( - epochService.openSealedEpochSecret(epochSecret, epochSecret2.derivedKeys!), - ).rejects.toThrow() - }) - }) - - describe('messageEncryption', () => { - let epochSecret: EpochSecret - let message: EncryptedData - - beforeEach(async () => { - await epochService.addOpenEpochSecret(streamId, epoch, secret) - epochSecret = epochService.getEpochSecret(streamId, epoch)! - expect(epochSecret).toBeDefined() - const plaintext = encoder.encode('message') - message = await epochService.encryptMessage(epochSecret, plaintext) - }) - - // encrypting message - it('shouldEncryptMessage', async () => { - expect(message).toBeDefined() - }) - - it('shouldDecryptMessage', async () => { - const plaintext_ = await epochService.decryptMessage(epochSecret, message) - const plaintext = new TextDecoder().decode(plaintext_) - - expect(plaintext).toEqual('message') - }) - - it('shouldFailToDecryptMessageWithWrongEpoch', async () => { - const secret2 = encoder.encode('secret2') - await epochService.addOpenEpochSecret(streamId, epoch + 1n, secret2) - const epochSecret2 = epochService.getEpochSecret(streamId, epoch + 1n)! - await expect(epochService.decryptMessage(epochSecret2, message)).rejects.toThrow() - }) - - it('shouldFailToDecryptMessageWithWrongSecret', async () => { - const secret2 = encoder.encode('secret2') - // update the epoch secret - await epochService.addOpenEpochSecret(streamId, epoch, secret2) - epochSecret = epochService.getEpochSecret(streamId, epoch)! - - await expect(epochService.decryptMessage(epochSecret, message)).rejects.toThrow() - }) - }) - - describe('epochSecretStorage', () => { - beforeEach(async () => { - await epochService.addOpenEpochSecret(streamId, epoch, secret) - epochService = new EpochSecretService(cipherSuite, epochStore) - }) - - it('shouldLoadEpochSecretFromStorage', async () => { - let epochSecret = epochService.getEpochSecret(streamId, epoch)! - expect(epochSecret).toBeUndefined() - - epochSecret = (await epochService.loadEpochSecret(streamId, epoch))! - expect(epochSecret).toBeDefined() - - expect(epochSecret.openEpochSecret).toStrictEqual(secret) - }) - }) -}) diff --git a/packages/sdk/src/tests/unit/mls/groupStateStorage.test.ts b/packages/sdk/src/tests/unit/mls/groupStateStorage.test.ts deleted file mode 100644 index 999dc3f443..0000000000 --- a/packages/sdk/src/tests/unit/mls/groupStateStorage.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @group main - */ - -import { beforeEach, describe, it } from 'vitest' -import { DexieGroupStateStorage, InMemoryGroupStateStorage } from '../../../mls/groupStateStorage' -import { IGroupStateStorage } from '@river-build/mls-rs-wasm' -import { randomBytes } from 'crypto' - -const encoder = new TextEncoder() - -const storages: Array<{ - type: string - createStorage: () => IGroupStateStorage -}> = [ - { - type: 'InMemoryGroupStateStorage', - createStorage: () => new InMemoryGroupStateStorage(), - }, - { - type: 'DexieGroupStateStorage', - createStorage: () => new DexieGroupStateStorage(randomBytes(16)), - }, -] - -describe.each(storages)('$type', ({ createStorage }) => { - let storage: IGroupStateStorage - const groupId = encoder.encode('group-id') - const data = encoder.encode('data') - const epochId = 1n - const epochData = encoder.encode('epoch-data') - - beforeEach(() => { - storage = createStorage() - }) - - it('shouldCreateStorage', () => { - expect(storage).toBeDefined() - }) - - it('shouldStartEmpty', async () => { - await expect(storage.state(groupId)).resolves.toBeUndefined() - await expect(storage.epoch(groupId, epochId)).resolves.toBeUndefined() - await expect(storage.maxEpochId(groupId)).resolves.toBeUndefined() - }) - - it('shouldBePossibleToAddGroup', async () => { - await storage.write(groupId, data, [], []) - - await expect(storage.state(groupId)).resolves.toStrictEqual(data) - }) - - it('shouldBePossibleToAddEpoch', async () => { - await storage.write(groupId, data, [{ id: epochId, data: epochData }], []) - - await expect(storage.epoch(groupId, epochId)).resolves.toStrictEqual(epochData) - await expect(storage.epoch(groupId, 2n)).resolves.toBeUndefined() - }) - - it('shouldBePossibleToUpdateEpoch', async () => { - await storage.write(groupId, data, [{ id: epochId, data: epochData }], []) - const epochData2 = encoder.encode('epoch-data-2') - await storage.write(groupId, data, [], [{ id: epochId, data: epochData2 }]) - - await expect(storage.epoch(groupId, epochId)).resolves.toStrictEqual(epochData2) - }) - - it('shouldBePossibleToGetMaxEpoch', async () => { - await expect(storage.maxEpochId(groupId)).resolves.toBeUndefined() - await storage.write(groupId, data, [{ id: epochId, data: epochData }], []) - await expect(storage.maxEpochId(groupId)).resolves.toBe(epochId) - await storage.write(groupId, data, [{ id: epochId + 1n, data: epochData }], []) - await expect(storage.maxEpochId(groupId)).resolves.toBe(epochId + 1n) - }) - - it('shouldTrimOldEpochs', async () => { - await storage.write(groupId, data, [{ id: 1n, data: epochData }], []) - await storage.write(groupId, data, [{ id: 2n, data: epochData }], []) - await storage.write(groupId, data, [{ id: 3n, data: epochData }], []) - await storage.write(groupId, data, [{ id: 4n, data: epochData }], []) - - await expect(storage.epoch(groupId, 1n)).resolves.toBeUndefined() - await expect(storage.epoch(groupId, 2n)).resolves.toStrictEqual(epochData) - }) -}) diff --git a/packages/sdk/src/tests/unit/mls/mls.test.ts b/packages/sdk/src/tests/unit/mls/mls.test.ts deleted file mode 100644 index 9b879ece8a..0000000000 --- a/packages/sdk/src/tests/unit/mls/mls.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @group main - */ - -import { Client as MlsClient } from '@river-build/mls-rs-wasm' -import { randomBytes } from 'crypto' - -describe('mls', () => { - test('initialize mls group', async () => { - const deviceKey = new Uint8Array(randomBytes(32)) - const client = await MlsClient.create(deviceKey) - const group = await client.createGroup() - expect(group).toBeDefined() - }) -}) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index adf0c3a4b7..d536396177 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -36,7 +36,6 @@ import { MemberPayload, MemberPayload_Nft, BlockchainTransaction, - MemberPayload_Mls, } from '@river-build/proto' import { keccak256 } from 'ethereum-cryptography/keccak' import { bin_toHexString } from '@river-build/dlog' @@ -368,20 +367,6 @@ export const make_MemberPayload_Unpin = ( } } -export const make_MemberPayload_Mls = ( - value: PlainMessage, -): PlainMessage['payload'] => { - return { - case: 'memberPayload', - value: { - content: { - case: 'mls', - value, - }, - }, - } -} - export const make_ChannelMessage_Post_Content_Text = ( body: string, mentions?: PlainMessage[], diff --git a/packages/stream-metadata/esbuild.config.mjs b/packages/stream-metadata/esbuild.config.mjs index bcb5d1a7d2..01118c66a1 100644 --- a/packages/stream-metadata/esbuild.config.mjs +++ b/packages/stream-metadata/esbuild.config.mjs @@ -5,8 +5,6 @@ build({ bundle: true, entryPoints: { node_esbuild: './src/node.ts', - // NOTE: For some reason esbuild is not picking it up - mls_rs_wasm_bg: '@river-build/mls-rs-wasm-node/mls_rs_wasm_bg.wasm', }, // Rename the entry point to control the output file name format: 'cjs', logLevel: 'info', @@ -30,9 +28,6 @@ build({ outExtension: { '.js': '.cjs' }, // Ensure the output file has .cjs extension platform: 'node', plugins: [esbuildPluginPino({ transports: ['pino-pretty'] })], - alias: { - '@river-build/mls-rs-wasm': '@river-build/mls-rs-wasm-node', - }, assetNames: '[name]', loader: { '.ts': 'ts', diff --git a/packages/stream-metadata/package.json b/packages/stream-metadata/package.json index 1d21bf0e6e..d5e621e961 100644 --- a/packages/stream-metadata/package.json +++ b/packages/stream-metadata/package.json @@ -49,7 +49,6 @@ "@river-build/dlog": "workspace:^", "@river-build/encryption": "workspace:^", "@river-build/eslint-config": "workspace:^", - "@river-build/mls-rs-wasm-node": "^0.0.16", "@river-build/prettier-config": "workspace:^", "@types/node": "^20.5.0", "@types/uuid": "^10.0.0", diff --git a/packages/stress/esbuild.config.mjs b/packages/stress/esbuild.config.mjs index e117167d0a..32029262d5 100644 --- a/packages/stress/esbuild.config.mjs +++ b/packages/stress/esbuild.config.mjs @@ -5,8 +5,6 @@ build({ entryPoints: { start: './src/start.ts', demo: './src/demo.ts', - // NOTE: For some reason esbuild is not picking it up - mls_rs_wasm_bg: '@river-build/mls-rs-wasm-node/mls_rs_wasm_bg.wasm', }, bundle: true, sourcemap: 'inline', @@ -16,9 +14,6 @@ build({ outdir: 'dist', outExtension: { '.js': '.cjs' }, plugins: [esbuildPluginPino({ transports: ['pino-pretty'] })], - alias: { - '@river-build/mls-rs-wasm': '@river-build/mls-rs-wasm-node', - }, ignoreAnnotations: true, assetNames: '[name]', loader: { diff --git a/packages/stress/package.json b/packages/stress/package.json index 53c1ac267e..7891d40cd8 100644 --- a/packages/stress/package.json +++ b/packages/stress/package.json @@ -29,7 +29,6 @@ "pino-pretty": "^13.0.0" }, "devDependencies": { - "@river-build/mls-rs-wasm-node": "^0.0.16", "@types/debug": "^4.1.8", "@types/lodash": "^4.14.186", "@types/node": "^20.5.0", diff --git a/packages/xchain-monitor/esbuild.config.mjs b/packages/xchain-monitor/esbuild.config.mjs index f59f2bc0ec..9ca7b1bfc8 100644 --- a/packages/xchain-monitor/esbuild.config.mjs +++ b/packages/xchain-monitor/esbuild.config.mjs @@ -5,8 +5,6 @@ build({ bundle: true, entryPoints: { node_esbuild: './src/index.ts', - // NOTE: For some reason esbuild is not picking it up - mls_rs_wasm_bg: '@river-build/mls-rs-wasm-node/mls_rs_wasm_bg.wasm', }, // Rename the entry point to control the output file name format: 'cjs', logLevel: 'info', @@ -26,9 +24,6 @@ build({ outExtension: { '.js': '.cjs' }, // Ensure the output file has .cjs extension platform: 'node', plugins: [esbuildPluginPino({ transports: ['pino-pretty'] })], - alias: { - '@river-build/mls-rs-wasm': '@river-build/mls-rs-wasm-node', - }, assetNames: '[name]', loader: { '.ts': 'ts', diff --git a/yarn.lock b/yarn.lock index 3084f9b84a..9bbc1a7c81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6694,7 +6694,6 @@ __metadata: "@river-build/dlog": "workspace:^" "@river-build/encryption": "workspace:^" "@river-build/generated": "workspace:^" - "@river-build/mls-rs-wasm": ^0.0.16 "@river-build/proto": "workspace:^" "@river-build/web3": "workspace:^" "@testing-library/react": ^14.2.1 @@ -6741,7 +6740,6 @@ __metadata: "@river-build/encryption": "workspace:^" "@river-build/eslint-config": "workspace:^" "@river-build/generated": "workspace:^" - "@river-build/mls-rs-wasm-node": ^0.0.16 "@river-build/prettier-config": "workspace:^" "@river-build/proto": "workspace:^" "@river-build/sdk": "workspace:^" @@ -6781,7 +6779,6 @@ __metadata: "@bufbuild/protobuf": ^1.9.0 "@river-build/dlog": "workspace:^" "@river-build/encryption": "workspace:^" - "@river-build/mls-rs-wasm-node": ^0.0.16 "@river-build/proto": "workspace:^" "@river-build/sdk": "workspace:^" "@river-build/web3": "workspace:^"