• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LukaJCB / ts-mls / 19520921507

20 Nov 2025 12:25AM UTC coverage: 96.901% (+0.2%) from 96.711%
19520921507

push

github

web-flow
Break down validation into smaller tests (#150)

1169 of 1288 branches covered (90.76%)

Branch coverage included in aggregate %.

6680 of 6812 relevant lines covered (98.06%)

47808.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.83
/src/processMessages.ts
1
import { AuthenticatedContentCommit } from "./authenticatedContent.js"
2
import {
1✔
3
  ClientState,
4
  GroupActiveState,
5
  addHistoricalReceiverData,
6
  applyProposals,
7
  nextEpochContext,
8
  processProposal,
9
  throwIfDefined,
10
  validateLeafNodeCredentialAndKeyUniqueness,
11
  validateLeafNodeUpdateOrCommit,
12
} from "./clientState.js"
13
import { applyUpdatePathSecret } from "./createCommit.js"
1✔
14
import { CiphersuiteImpl } from "./crypto/ciphersuite.js"
15
import { Kdf, deriveSecret } from "./crypto/kdf.js"
1✔
16
import { verifyConfirmationTag } from "./framedContent.js"
1✔
17
import { GroupContext } from "./groupContext.js"
18
import { acceptAll, IncomingMessageAction, IncomingMessageCallback } from "./incomingMessageAction.js"
1✔
19
import { initializeEpoch } from "./keySchedule.js"
1✔
20
import { MlsPrivateMessage, MlsPublicMessage } from "./message.js"
21
import { unprotectPrivateMessage } from "./messageProtection.js"
1✔
22
import { unprotectPublicMessage } from "./messageProtectionPublic.js"
1✔
23
import { CryptoVerificationError, InternalError, ValidationError } from "./mlsError.js"
1✔
24
import { pathToRoot } from "./pathSecrets.js"
1✔
25
import { PrivateKeyPath, mergePrivateKeyPaths, toPrivateKeyPath } from "./privateKeyPath.js"
1✔
26
import { PrivateMessage } from "./privateMessage.js"
27
import { emptyPskIndex, PskIndex } from "./pskIndex.js"
1✔
28
import { PublicMessage } from "./publicMessage.js"
29
import { findBlankLeafNodeIndex, RatchetTree, addLeafNode } from "./ratchetTree.js"
1✔
30
import { createSecretTree } from "./secretTree.js"
1✔
31
import { getSenderLeafNodeIndex, Sender } from "./sender.js"
1✔
32
import { treeHashRoot } from "./treeHash.js"
1✔
33
import {
1✔
34
  LeafIndex,
35
  leafToNodeIndex,
36
  leafWidth,
37
  NodeIndex,
38
  nodeToLeafIndex,
39
  root,
40
  toLeafIndex,
41
  toNodeIndex,
42
} from "./treemath.js"
43
import { UpdatePath, applyUpdatePath } from "./updatePath.js"
1✔
44
import { addToMap } from "./util/addToMap.js"
1✔
45
import { WireformatName } from "./wireformat.js"
46

47
export type ProcessMessageResult =
48
  | {
49
      kind: "newState"
50
      newState: ClientState
51
      actionTaken: IncomingMessageAction
52
    }
53
  | { kind: "applicationMessage"; message: Uint8Array; newState: ClientState }
54

55
/**
56
 * Process private message and apply proposal or commit and return the updated ClientState or return an application message
57
 */
58
export async function processPrivateMessage(
8,892✔
59
  state: ClientState,
8,892✔
60
  pm: PrivateMessage,
8,892✔
61
  pskSearch: PskIndex,
8,892✔
62
  cs: CiphersuiteImpl,
8,892✔
63
  callback: IncomingMessageCallback = acceptAll,
8,892✔
64
): Promise<ProcessMessageResult> {
8,892✔
65
  if (pm.epoch < state.groupContext.epoch) {
8,892✔
66
    const receiverData = state.historicalReceiverData.get(pm.epoch)
171✔
67

68
    if (receiverData !== undefined) {
171✔
69
      const result = await unprotectPrivateMessage(
152✔
70
        receiverData.senderDataSecret,
152✔
71
        pm,
152✔
72
        receiverData.secretTree,
152✔
73
        receiverData.ratchetTree,
152✔
74
        receiverData.groupContext,
152✔
75
        state.clientConfig.keyRetentionConfig,
152✔
76
        cs,
152✔
77
      )
152✔
78

79
      const newHistoricalReceiverData = addToMap(state.historicalReceiverData, pm.epoch, {
152✔
80
        ...receiverData,
152✔
81
        secretTree: result.tree,
152✔
82
      })
152✔
83

84
      const newState = { ...state, historicalReceiverData: newHistoricalReceiverData }
152✔
85

86
      if (result.content.content.contentType === "application") {
152✔
87
        return { kind: "applicationMessage", message: result.content.content.applicationData, newState }
133✔
88
      } else {
152✔
89
        throw new ValidationError("Cannot process commit or proposal from former epoch")
19✔
90
      }
19✔
91
    } else {
171✔
92
      throw new ValidationError("Cannot process message, epoch too old")
19✔
93
    }
19✔
94
  }
171✔
95

96
  const result = await unprotectPrivateMessage(
8,721✔
97
    state.keySchedule.senderDataSecret,
8,721✔
98
    pm,
8,721✔
99
    state.secretTree,
8,721✔
100
    state.ratchetTree,
8,721✔
101
    state.groupContext,
8,721✔
102
    state.clientConfig.keyRetentionConfig,
8,721✔
103
    cs,
8,721✔
104
  )
8,721✔
105

106
  const updatedState = { ...state, secretTree: result.tree }
8,702✔
107

108
  if (result.content.content.contentType === "application") {
8,892✔
109
    return { kind: "applicationMessage", message: result.content.content.applicationData, newState: updatedState }
5,776✔
110
  } else if (result.content.content.contentType === "commit") {
8,702✔
111
    const { newState, actionTaken } = await processCommit(
2,850✔
112
      updatedState,
2,850✔
113
      result.content as AuthenticatedContentCommit,
2,850✔
114
      "mls_private_message",
2,850✔
115
      pskSearch,
2,850✔
116
      callback,
2,850✔
117
      cs,
2,850✔
118
    ) //todo solve with types
2,850✔
119
    return {
2,850✔
120
      kind: "newState",
2,850✔
121
      newState,
2,850✔
122
      actionTaken,
2,850✔
123
    }
2,850✔
124
  } else {
2,850✔
125
    const action = callback({
76✔
126
      kind: "proposal",
76✔
127
      proposal: {
76✔
128
        proposal: result.content.content.proposal,
76✔
129
        senderLeafIndex: getSenderLeafNodeIndex(result.content.content.sender),
76✔
130
      },
76✔
131
    })
76✔
132
    if (action === "reject")
76✔
133
      return {
76✔
134
        kind: "newState",
19✔
135
        newState: updatedState,
19✔
136
        actionTaken: action,
19✔
137
      }
19✔
138
    else
139
      return {
57✔
140
        kind: "newState",
57✔
141
        newState: await processProposal(updatedState, result.content, result.content.content.proposal, cs.hash),
57✔
142
        actionTaken: action,
57✔
143
      }
57✔
144
  }
76✔
145
}
8,892✔
146

147
export interface NewStateWithActionTaken {
148
  newState: ClientState
149
  actionTaken: IncomingMessageAction
150
}
151

152
export async function processPublicMessage(
2,274✔
153
  state: ClientState,
2,274✔
154
  pm: PublicMessage,
2,274✔
155
  pskSearch: PskIndex,
2,274✔
156
  cs: CiphersuiteImpl,
2,274✔
157
  callback: IncomingMessageCallback = acceptAll,
2,274✔
158
): Promise<NewStateWithActionTaken> {
2,274✔
159
  if (pm.content.epoch < state.groupContext.epoch) throw new ValidationError("Cannot process message, epoch too old")
2,274!
160

161
  const content = await unprotectPublicMessage(
2,274✔
162
    state.keySchedule.membershipKey,
2,274✔
163
    state.groupContext,
2,274✔
164
    state.ratchetTree,
2,274✔
165
    pm,
2,274✔
166
    cs,
2,274✔
167
  )
2,274✔
168

169
  if (content.content.contentType === "proposal") {
2,274✔
170
    const action = callback({
1,759✔
171
      kind: "proposal",
1,759✔
172
      proposal: { proposal: content.content.proposal, senderLeafIndex: getSenderLeafNodeIndex(content.content.sender) },
1,759✔
173
    })
1,759✔
174
    if (action === "reject")
1,759✔
175
      return {
1,759✔
176
        newState: state,
19✔
177
        actionTaken: action,
19✔
178
      }
19✔
179
    else
180
      return {
1,740✔
181
        newState: await processProposal(state, content, content.content.proposal, cs.hash),
1,740✔
182
        actionTaken: action,
1,740✔
183
      }
1,740✔
184
  } else {
2,236✔
185
    return processCommit(state, content as AuthenticatedContentCommit, "mls_public_message", pskSearch, callback, cs) //todo solve with types
515✔
186
  }
515✔
187
}
2,274✔
188

189
async function processCommit(
3,365✔
190
  state: ClientState,
3,365✔
191
  content: AuthenticatedContentCommit,
3,365✔
192
  wireformat: WireformatName,
3,365✔
193
  pskSearch: PskIndex,
3,365✔
194
  callback: IncomingMessageCallback,
3,365✔
195
  cs: CiphersuiteImpl,
3,365✔
196
): Promise<NewStateWithActionTaken> {
3,365✔
197
  if (content.content.epoch !== state.groupContext.epoch) throw new ValidationError("Could not validate epoch")
3,365!
198

199
  const senderLeafIndex =
3,365✔
200
    content.content.sender.senderType === "member" ? toLeafIndex(content.content.sender.leafIndex) : undefined
3,365✔
201

202
  const result = await applyProposals(state, content.content.commit.proposals, senderLeafIndex, pskSearch, false, cs)
3,365✔
203

204
  const action = callback({ kind: "commit", proposals: result.allProposals })
3,365✔
205

206
  if (action === "reject") {
3,365✔
207
    return { newState: state, actionTaken: action }
38✔
208
  }
38✔
209

210
  if (content.content.commit.path !== undefined) {
3,327✔
211
    const committerLeafIndex =
2,007✔
212
      senderLeafIndex ??
2,007✔
213
      (result.additionalResult.kind === "externalCommit" ? result.additionalResult.newMemberLeafIndex : undefined)
76!
214

215
    if (committerLeafIndex === undefined)
2,007✔
216
      throw new ValidationError("Cannot verify commit leaf node because no commiter leaf index found")
2,007!
217

218
    throwIfDefined(
2,007✔
219
      await validateLeafNodeUpdateOrCommit(
2,007✔
220
        content.content.commit.path.leafNode,
2,007✔
221
        committerLeafIndex,
2,007✔
222
        state.groupContext,
2,007✔
223
        state.clientConfig.authService,
2,007✔
224
        cs.signature,
2,007✔
225
      ),
2,007✔
226
    )
2,007✔
227
    throwIfDefined(
2,007✔
228
      await validateLeafNodeCredentialAndKeyUniqueness(
2,007✔
229
        result.tree,
2,007✔
230
        content.content.commit.path.leafNode,
2,007✔
231
        committerLeafIndex,
2,007✔
232
      ),
2,007✔
233
    )
2,007✔
234
  }
2,007✔
235

236
  if (result.needsUpdatePath && content.content.commit.path === undefined)
3,327✔
237
    throw new ValidationError("Update path is required")
3,365✔
238

239
  const groupContextWithExtensions =
3,327✔
240
    result.additionalResult.kind === "memberCommit" && result.additionalResult.extensions.length > 0
3,327✔
241
      ? { ...state.groupContext, extensions: result.additionalResult.extensions }
✔
242
      : state.groupContext
3,327✔
243

244
  const [pkp, commitSecret, tree] = await applyTreeUpdate(
3,365✔
245
    content.content.commit.path,
3,365✔
246
    content.content.sender,
3,365✔
247
    result.tree,
3,365✔
248
    cs,
3,365✔
249
    state,
3,365✔
250
    groupContextWithExtensions,
3,365✔
251
    result.additionalResult.kind === "memberCommit"
3,365✔
252
      ? result.additionalResult.addedLeafNodes.map((l) => leafToNodeIndex(toLeafIndex(l[0])))
3,213✔
253
      : [findBlankLeafNodeIndex(result.tree) ?? toNodeIndex(result.tree.length + 1)],
114✔
254
    cs.kdf,
3,365✔
255
  )
3,365✔
256

257
  const newTreeHash = await treeHashRoot(tree, cs.hash)
3,327✔
258

259
  if (content.auth.contentType !== "commit") throw new ValidationError("Received content as commit, but not auth") //todo solve this with types?
3,327!
260
  const updatedGroupContext = await nextEpochContext(
3,327✔
261
    groupContextWithExtensions,
3,327✔
262
    wireformat,
3,327✔
263
    content.content,
3,327✔
264
    content.auth.signature,
3,327✔
265
    newTreeHash,
3,327✔
266
    state.confirmationTag,
3,327✔
267
    cs.hash,
3,327✔
268
  )
3,327✔
269

270
  const initSecret =
3,327✔
271
    result.additionalResult.kind === "externalCommit"
3,327✔
272
      ? result.additionalResult.externalInitSecret
76✔
273
      : state.keySchedule.initSecret
3,251✔
274

275
  const epochSecrets = await initializeEpoch(initSecret, commitSecret, updatedGroupContext, result.pskSecret, cs.kdf)
3,365✔
276

277
  const confirmationTagValid = await verifyConfirmationTag(
3,327✔
278
    epochSecrets.keySchedule.confirmationKey,
3,327✔
279
    content.auth.confirmationTag,
3,327✔
280
    updatedGroupContext.confirmedTranscriptHash,
3,327✔
281
    cs.hash,
3,327✔
282
  )
3,327✔
283

284
  if (!confirmationTagValid) throw new CryptoVerificationError("Could not verify confirmation tag")
3,327!
285

286
  const secretTree = await createSecretTree(leafWidth(tree.length), epochSecrets.keySchedule.encryptionSecret, cs.kdf)
3,327✔
287

288
  const suspendedPendingReinit = result.additionalResult.kind === "reinit" ? result.additionalResult.reinit : undefined
3,365✔
289

290
  const groupActiveState: GroupActiveState = result.selfRemoved
3,365✔
291
    ? { kind: "removedFromGroup" }
209✔
292
    : suspendedPendingReinit !== undefined
3,118✔
293
      ? { kind: "suspendedPendingReinit", reinit: suspendedPendingReinit }
38✔
294
      : { kind: "active" }
3,080✔
295

296
  return {
3,365✔
297
    newState: {
3,365✔
298
      ...state,
3,365✔
299
      secretTree,
3,365✔
300
      ratchetTree: tree,
3,365✔
301
      privatePath: pkp,
3,365✔
302
      groupContext: updatedGroupContext,
3,365✔
303
      keySchedule: epochSecrets.keySchedule,
3,365✔
304
      confirmationTag: content.auth.confirmationTag,
3,365✔
305
      historicalReceiverData: addHistoricalReceiverData(state),
3,365✔
306
      unappliedProposals: {},
3,365✔
307
      groupActiveState,
3,365✔
308
    },
3,365✔
309
    actionTaken: action,
3,365✔
310
  }
3,365✔
311
}
3,365✔
312

313
async function applyTreeUpdate(
3,327✔
314
  path: UpdatePath | undefined,
3,327✔
315
  sender: Sender,
3,327✔
316
  tree: RatchetTree,
3,327✔
317
  cs: CiphersuiteImpl,
3,327✔
318
  state: ClientState,
3,327✔
319
  groupContext: GroupContext,
3,327✔
320
  excludeNodes: NodeIndex[],
3,327✔
321
  kdf: Kdf,
3,327✔
322
): Promise<[PrivateKeyPath, Uint8Array, RatchetTree]> {
3,327✔
323
  if (path === undefined) return [state.privatePath, new Uint8Array(kdf.size), tree] as const
3,327✔
324
  if (sender.senderType === "member") {
2,007✔
325
    const updatedTree = await applyUpdatePath(tree, toLeafIndex(sender.leafIndex), path, cs.hash)
1,931✔
326

327
    const [pkp, commitSecret] = await updatePrivateKeyPath(
1,931✔
328
      updatedTree,
1,931✔
329
      state,
1,931✔
330
      toLeafIndex(sender.leafIndex),
1,931✔
331
      { ...groupContext, treeHash: await treeHashRoot(updatedTree, cs.hash), epoch: groupContext.epoch + 1n },
1,931✔
332
      path,
1,931✔
333
      excludeNodes,
1,931✔
334
      cs,
1,931✔
335
    )
1,931✔
336
    return [pkp, commitSecret, updatedTree] as const
1,931✔
337
  } else {
3,327✔
338
    const [treeWithLeafNode, leafNodeIndex] = addLeafNode(tree, path.leafNode)
76✔
339

340
    const senderLeafIndex = nodeToLeafIndex(leafNodeIndex)
76✔
341
    const updatedTree = await applyUpdatePath(treeWithLeafNode, senderLeafIndex, path, cs.hash, true)
76✔
342

343
    const [pkp, commitSecret] = await updatePrivateKeyPath(
76✔
344
      updatedTree,
76✔
345
      state,
76✔
346
      senderLeafIndex,
76✔
347
      { ...groupContext, treeHash: await treeHashRoot(updatedTree, cs.hash), epoch: groupContext.epoch + 1n },
76✔
348
      path,
76✔
349
      excludeNodes,
76✔
350
      cs,
76✔
351
    )
76✔
352
    return [pkp, commitSecret, updatedTree] as const
76✔
353
  }
76✔
354
}
3,327✔
355

356
async function updatePrivateKeyPath(
2,007✔
357
  tree: RatchetTree,
2,007✔
358
  state: ClientState,
2,007✔
359
  leafNodeIndex: LeafIndex,
2,007✔
360
  groupContext: GroupContext,
2,007✔
361
  path: UpdatePath,
2,007✔
362
  excludeNodes: NodeIndex[],
2,007✔
363
  cs: CiphersuiteImpl,
2,007✔
364
): Promise<[PrivateKeyPath, Uint8Array]> {
2,007✔
365
  const secret = await applyUpdatePathSecret(
2,007✔
366
    tree,
2,007✔
367
    state.privatePath,
2,007✔
368
    leafNodeIndex,
2,007✔
369
    groupContext,
2,007✔
370
    path,
2,007✔
371
    excludeNodes,
2,007✔
372
    cs,
2,007✔
373
  )
2,007✔
374
  const pathSecrets = await pathToRoot(tree, toNodeIndex(secret.nodeIndex), secret.pathSecret, cs.kdf)
2,007✔
375
  const newPkp = mergePrivateKeyPaths(
2,007✔
376
    state.privatePath,
2,007✔
377
    await toPrivateKeyPath(pathSecrets, state.privatePath.leafIndex, cs),
2,007✔
378
  )
2,007✔
379

380
  const rootIndex = root(leafWidth(tree.length))
2,007✔
381
  const rootSecret = pathSecrets[rootIndex]
2,007✔
382
  if (rootSecret === undefined) throw new InternalError("Could not find secret for root")
2,007!
383

384
  const commitSecret = await deriveSecret(rootSecret, "path", cs.kdf)
2,007✔
385
  return [newPkp, commitSecret] as const
2,007✔
386
}
2,007✔
387

388
export async function processMessage(
266✔
389
  message: MlsPrivateMessage | MlsPublicMessage,
266✔
390
  state: ClientState,
266✔
391
  pskIndex: PskIndex,
266✔
392
  action: IncomingMessageCallback,
266✔
393
  cs: CiphersuiteImpl,
266✔
394
): Promise<ProcessMessageResult> {
266✔
395
  if (message.wireformat === "mls_public_message") {
266✔
396
    const result = await processPublicMessage(state, message.publicMessage, pskIndex, cs, action)
114✔
397

398
    return { ...result, kind: "newState" }
114✔
399
  } else return processPrivateMessage(state, message.privateMessage, emptyPskIndex, cs, action)
152✔
400
}
266✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc