Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MLS Views #2162

Open
wants to merge 6 commits into
base: jakub/elogger
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/sdk/src/mls/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
import { EncryptedDataVersion } from '@river-build/proto'

export const MLS_ALGORITHM = 'mls_0.0.1'
export const MLS_ENCRYPTED_DATA_VERSION = EncryptedDataVersion.ENCRYPTED_DATA_VERSION_1
62 changes: 62 additions & 0 deletions packages/sdk/src/mls/epochEncryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
CipherSuite as MlsCipherSuite,
HpkeCiphertext,
HpkePublicKey,
HpkeSecretKey,
Secret as MlsSecret,
} from '@river-build/mls-rs-wasm'
import { ELogger, elogger } from '@river-build/dlog'

const defaultLogger = elogger('csb:mls:ee')

export type DerivedKeys = {
secretKey: Uint8Array
publicKey: Uint8Array
}

export type EpochEncryptionOpts = {
cipherSuite: MlsCipherSuite
log: ELogger
}

export const defaultEpochEncryptionOpts = {
cipherSuite: new MlsCipherSuite(),
log: defaultLogger,
}

export class EpochEncryption {
private cipherSuite: MlsCipherSuite

private log: ELogger

constructor(opts?: EpochEncryptionOpts) {
const epochEncryptionOpts: EpochEncryptionOpts = {
...defaultEpochEncryptionOpts,
...opts,
}
this.log = epochEncryptionOpts.log
this.cipherSuite = epochEncryptionOpts.cipherSuite
}

public async seal(derivedKeys: DerivedKeys, plaintext: Uint8Array): Promise<Uint8Array> {
const publicKey_ = HpkePublicKey.fromBytes(derivedKeys.publicKey)
const ciphertext_ = await this.cipherSuite.seal(publicKey_, plaintext)
return ciphertext_.toBytes()
}

public async open(derivedKeys: DerivedKeys, ciphertext: Uint8Array): Promise<Uint8Array> {
const publicKey_ = HpkePublicKey.fromBytes(derivedKeys.publicKey)
const secretKey_ = HpkeSecretKey.fromBytes(derivedKeys.secretKey)
const ciphertext_ = HpkeCiphertext.fromBytes(ciphertext)
return await this.cipherSuite.open(ciphertext_, secretKey_, publicKey_)
}

public async deriveKeys(secret: Uint8Array): Promise<DerivedKeys> {
const mlsSecret = MlsSecret.fromBytes(secret)
const deriveOutput = await this.cipherSuite.kemDerive(mlsSecret)
return {
publicKey: deriveOutput.publicKey.toBytes(),
secretKey: deriveOutput.secretKey.toBytes(),
}
}
}
139 changes: 139 additions & 0 deletions packages/sdk/src/mls/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
EncryptedData,
MemberPayload_Mls,
MemberPayload_Mls_ExternalJoin,
MemberPayload_Mls_InitializeGroup,
} from '@river-build/proto'
import { LocalEpochSecret } from './view/local'
import { Message, PlainMessage } from '@bufbuild/protobuf'
import { RemoteGroupInfo } from './view/remote'
import {
Client as MlsClient,
ExternalClient as MlsExternalClient,
Group as MlsGroup,
ExportedTree as MlsExportedTree,
MlsMessage,
} from '@river-build/mls-rs-wasm'
import { MLS_ALGORITHM, MLS_ENCRYPTED_DATA_VERSION } from './constants'
import { EpochEncryption } from './epochEncryption'
import { ExternalJoin, InitializeGroup } from './types'

const crypto = new EpochEncryption()

export async function encryptEpochSecretMessage(
epochSecret: LocalEpochSecret,
event: Message,
): Promise<EncryptedData> {
const plaintext = event.toBinary()
const ciphertext = await crypto.seal(epochSecret.derivedKeys, plaintext)

return new EncryptedData({
algorithm: MLS_ALGORITHM,
mls: {
epoch: epochSecret.epoch,
ciphertext,
},
version: MLS_ENCRYPTED_DATA_VERSION,
})
}

export function epochSecretsMessage(
epochSecrets: { epoch: bigint; secret: Uint8Array }[],
): PlainMessage<MemberPayload_Mls> {
return {
content: {
case: 'epochSecrets',
value: {
secrets: epochSecrets,
},
},
}
}

export async function prepareExternalJoinMessage(
mlsClient: MlsClient,
externalInfo: RemoteGroupInfo,
) {
const groupInfoMessage = MlsMessage.fromBytes(externalInfo.latestGroupInfo)
const exportedTree = MlsExportedTree.fromBytes(externalInfo.exportedTree)
const { group, commit } = await mlsClient.commitExternal(groupInfoMessage, exportedTree)
const updatedGroupInfoMessage = await group.groupInfoMessageAllowingExtCommit(false)
const updatedGroupInfoMessageBytes = updatedGroupInfoMessage.toBytes()
const commitBytes = commit.toBytes()
const event = makeExternalJoin(
mlsClient.signaturePublicKey(),
commitBytes,
updatedGroupInfoMessageBytes,
)
const message = { content: event }
return {
group,
message,
}
}

export async function prepareInitializeGroup(mlsClient: MlsClient) {
const group = await mlsClient.createGroup()
const { groupInfoMessage, externalGroupSnapshot } = await createGroupInfoAndExternalSnapshot(
group,
)
const event = makeInitializeGroup(
mlsClient.signaturePublicKey(),
externalGroupSnapshot,
groupInfoMessage,
)
const message = { content: event }
return {
group,
message,
}
}

export function makeInitializeGroup(
signaturePublicKey: Uint8Array,
externalGroupSnapshot: Uint8Array,
groupInfoMessage: Uint8Array,
): InitializeGroup {
const value = new MemberPayload_Mls_InitializeGroup({
signaturePublicKey: signaturePublicKey,
externalGroupSnapshot: externalGroupSnapshot,
groupInfoMessage: groupInfoMessage,
})
return {
case: 'initializeGroup',
value,
}
}

export function makeExternalJoin(
signaturePublicKey: Uint8Array,
commit: Uint8Array,
groupInfoMessage: Uint8Array,
): ExternalJoin {
const value = new MemberPayload_Mls_ExternalJoin({
signaturePublicKey: signaturePublicKey,
commit: commit,
groupInfoMessage: groupInfoMessage,
})
return {
case: 'externalJoin',
value,
}
}

// helper function to create a group + external snapshot
export async function createGroupInfoAndExternalSnapshot(group: MlsGroup): Promise<{
groupInfoMessage: Uint8Array
externalGroupSnapshot: Uint8Array
}> {
const groupInfoMessage = await group.groupInfoMessageAllowingExtCommit(false)
const tree = group.exportTree()
const externalClient = new MlsExternalClient()
const externalGroup = externalClient.observeGroup(groupInfoMessage.toBytes(), tree.toBytes())

const externalGroupSnapshot = (await externalGroup).snapshot()
return {
groupInfoMessage: groupInfoMessage.toBytes(),
externalGroupSnapshot: externalGroupSnapshot.toBytes(),
}
}
63 changes: 63 additions & 0 deletions packages/sdk/src/mls/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { PlainMessage } from '@bufbuild/protobuf'
import {
MemberPayload_Mls,
MemberPayload_Mls_EpochSecrets,
MemberPayload_Mls_ExternalJoin,
MemberPayload_Mls_InitializeGroup,
MemberPayload_Mls_WelcomeMessage,
MemberPayload_Snapshot_Mls,
} from '@river-build/proto'
import { EncryptedContent } from '../encryptedContentTypes'

type ConfirmedMetadata = {
confirmedEventNum: bigint
miniblockNum: bigint
eventId: string
}

// export type MlsSnapshot = PlainMessage<MemberPayload_Snapshot_Mls> & ConfirmedMetadata
export type MlsSnapshot = PlainMessage<MemberPayload_Snapshot_Mls>
export type MlsConfirmedSnapshot = MlsSnapshot & ConfirmedMetadata

export type MlsEvent = PlainMessage<MemberPayload_Mls>['content']

export type MlsConfirmedEvent = MlsEvent & ConfirmedMetadata

export type InitializeGroup = {
case: 'initializeGroup'
value: PlainMessage<MemberPayload_Mls_InitializeGroup>
}

export type ExternalJoin = {
case: 'externalJoin'
value: PlainMessage<MemberPayload_Mls_ExternalJoin>
}

export type ConfirmedInitializeGroup = InitializeGroup & ConfirmedMetadata

export type MlsEventWithCommit =
| {
case: 'externalJoin'
value: PlainMessage<MemberPayload_Mls_ExternalJoin>
}
| {
case: 'welcomeMessage'
value: PlainMessage<MemberPayload_Mls_WelcomeMessage>
}

export type ConfirmedMlsEventWithCommit = MlsEventWithCommit & ConfirmedMetadata

export type EpochSecrets = {
case: 'epochSecrets'
value: PlainMessage<MemberPayload_Mls_EpochSecrets>
}

export type ConfirmedEpochSecrets = EpochSecrets & ConfirmedMetadata

export type MlsEncryptedContentItem = {
streamId: string
eventId: string
kind: EncryptedContent['kind']
epoch: bigint
ciphertext: Uint8Array
}
Loading
Loading