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

LukaJCB / ts-mls / 17283659198

28 Aug 2025 02:05AM UTC coverage: 93.75% (+1.7%) from 92.046%
17283659198

push

github

web-flow
Migrate from jest to vitest and parallelize tests (#90)

1121 of 1250 branches covered (89.68%)

Branch coverage included in aggregate %.

388 of 393 new or added lines in 63 files covered. (98.73%)

57 existing lines in 13 files now uncovered.

6364 of 6734 relevant lines covered (94.51%)

51774.21 hits per line

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

97.79
/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
  validateLeafNodeUpdateOrCommit,
11
} from "./clientState.js"
12
import { applyUpdatePathSecret } from "./createCommit.js"
1✔
13
import { CiphersuiteImpl } from "./crypto/ciphersuite.js"
14
import { Kdf, deriveSecret } from "./crypto/kdf.js"
1✔
15
import { verifyConfirmationTag } from "./framedContent.js"
1✔
16
import { GroupContext } from "./groupContext.js"
17
import { acceptAll, IncomingMessageAction, IncomingMessageCallback } from "./incomingMessageAction.js"
1✔
18
import { initializeEpoch } from "./keySchedule.js"
1✔
19
import { MlsPrivateMessage, MlsPublicMessage } from "./message.js"
20
import { unprotectPrivateMessage } from "./messageProtection.js"
1✔
21
import { unprotectPublicMessage } from "./messageProtectionPublic.js"
1✔
22
import { CryptoVerificationError, InternalError, ValidationError } from "./mlsError.js"
1✔
23
import { pathToRoot } from "./pathSecrets.js"
1✔
24
import { PrivateKeyPath, mergePrivateKeyPaths, toPrivateKeyPath } from "./privateKeyPath.js"
1✔
25
import { PrivateMessage } from "./privateMessage.js"
26
import { emptyPskIndex, PskIndex } from "./pskIndex.js"
1✔
27
import { PublicMessage } from "./publicMessage.js"
28
import { findBlankLeafNodeIndex, RatchetTree, addLeafNode } from "./ratchetTree.js"
1✔
29
import { createSecretTree } from "./secretTree.js"
1✔
30
import { getSenderLeafNodeIndex, Sender } from "./sender.js"
1✔
31
import { treeHashRoot } from "./treeHash.js"
1✔
32
import {
1✔
33
  LeafIndex,
34
  leafToNodeIndex,
35
  leafWidth,
36
  NodeIndex,
37
  nodeToLeafIndex,
38
  root,
39
  toLeafIndex,
40
  toNodeIndex,
41
} from "./treemath.js"
42
import { UpdatePath, applyUpdatePath } from "./updatePath.js"
1✔
43
import { addToMap } from "./util/addToMap.js"
1✔
44
import { WireformatName } from "./wireformat.js"
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

217
    throwIfDefined(
2,007✔
218
      await validateLeafNodeUpdateOrCommit(
2,007✔
219
        content.content.commit.path.leafNode,
2,007✔
220
        committerLeafIndex,
2,007✔
221
        state.groupContext,
2,007✔
222
        result.tree,
2,007✔
223
        state.clientConfig.authService,
2,007✔
224
        cs.signature,
2,007✔
225
      ),
2,007✔
226
    )
2,007✔
227
  }
2,007✔
228

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

232
  const groupContextWithExtensions =
3,327✔
233
    result.additionalResult.kind === "memberCommit" && result.additionalResult.extensions.length > 0
3,327✔
UNCOV
234
      ? { ...state.groupContext, extensions: result.additionalResult.extensions }
✔
235
      : state.groupContext
3,327✔
236

237
  const [pkp, commitSecret, tree] = await applyTreeUpdate(
3,365✔
238
    content.content.commit.path,
3,365✔
239
    content.content.sender,
3,365✔
240
    result.tree,
3,365✔
241
    cs,
3,365✔
242
    state,
3,365✔
243
    groupContextWithExtensions,
3,365✔
244
    result.additionalResult.kind === "memberCommit"
3,365✔
245
      ? result.additionalResult.addedLeafNodes.map((l) => leafToNodeIndex(toLeafIndex(l[0])))
3,213✔
246
      : [findBlankLeafNodeIndex(result.tree) ?? toNodeIndex(result.tree.length + 1)],
114✔
247
    cs.kdf,
3,365✔
248
  )
3,365✔
249

250
  const newTreeHash = await treeHashRoot(tree, cs.hash)
3,327✔
251

252
  if (content.auth.contentType !== "commit") throw new ValidationError("Received content as commit, but not auth") //todo solve this with types?
3,327!
253
  const updatedGroupContext = await nextEpochContext(
3,327✔
254
    groupContextWithExtensions,
3,327✔
255
    wireformat,
3,327✔
256
    content.content,
3,327✔
257
    content.auth.signature,
3,327✔
258
    newTreeHash,
3,327✔
259
    state.confirmationTag,
3,327✔
260
    cs.hash,
3,327✔
261
  )
3,327✔
262

263
  const initSecret =
3,327✔
264
    result.additionalResult.kind === "externalCommit"
3,327✔
265
      ? result.additionalResult.externalInitSecret
76✔
266
      : state.keySchedule.initSecret
3,251✔
267

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

270
  const confirmationTagValid = await verifyConfirmationTag(
3,327✔
271
    epochSecrets.keySchedule.confirmationKey,
3,327✔
272
    content.auth.confirmationTag,
3,327✔
273
    updatedGroupContext.confirmedTranscriptHash,
3,327✔
274
    cs.hash,
3,327✔
275
  )
3,327✔
276

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

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

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

283
  const groupActiveState: GroupActiveState = result.selfRemoved
3,365✔
284
    ? { kind: "removedFromGroup" }
209✔
285
    : suspendedPendingReinit !== undefined
3,118✔
286
      ? { kind: "suspendedPendingReinit", reinit: suspendedPendingReinit }
38✔
287
      : { kind: "active" }
3,080✔
288

289
  return {
3,365✔
290
    newState: {
3,365✔
291
      ...state,
3,365✔
292
      secretTree,
3,365✔
293
      ratchetTree: tree,
3,365✔
294
      privatePath: pkp,
3,365✔
295
      groupContext: updatedGroupContext,
3,365✔
296
      keySchedule: epochSecrets.keySchedule,
3,365✔
297
      confirmationTag: content.auth.confirmationTag,
3,365✔
298
      historicalReceiverData: addHistoricalReceiverData(state),
3,365✔
299
      unappliedProposals: {},
3,365✔
300
      groupActiveState,
3,365✔
301
    },
3,365✔
302
    actionTaken: action,
3,365✔
303
  }
3,365✔
304
}
3,365✔
305

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

320
    const [pkp, commitSecret] = await updatePrivateKeyPath(
1,931✔
321
      updatedTree,
1,931✔
322
      state,
1,931✔
323
      toLeafIndex(sender.leafIndex),
1,931✔
324
      { ...groupContext, treeHash: await treeHashRoot(updatedTree, cs.hash), epoch: groupContext.epoch + 1n },
1,931✔
325
      path,
1,931✔
326
      excludeNodes,
1,931✔
327
      cs,
1,931✔
328
    )
1,931✔
329
    return [pkp, commitSecret, updatedTree] as const
1,931✔
330
  } else {
3,327✔
331
    const [treeWithLeafNode, leafNodeIndex] = addLeafNode(tree, path.leafNode)
76✔
332

333
    const senderLeafIndex = nodeToLeafIndex(leafNodeIndex)
76✔
334
    const updatedTree = await applyUpdatePath(treeWithLeafNode, senderLeafIndex, path, cs.hash, true)
76✔
335

336
    const [pkp, commitSecret] = await updatePrivateKeyPath(
76✔
337
      updatedTree,
76✔
338
      state,
76✔
339
      senderLeafIndex,
76✔
340
      { ...groupContext, treeHash: await treeHashRoot(updatedTree, cs.hash), epoch: groupContext.epoch + 1n },
76✔
341
      path,
76✔
342
      excludeNodes,
76✔
343
      cs,
76✔
344
    )
76✔
345
    return [pkp, commitSecret, updatedTree] as const
76✔
346
  }
76✔
347
}
3,327✔
348

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

373
  const rootIndex = root(leafWidth(tree.length))
2,007✔
374
  const rootSecret = pathSecrets[rootIndex]
2,007✔
375
  if (rootSecret === undefined) throw new InternalError("Could not find secret for root")
2,007!
376

377
  const commitSecret = await deriveSecret(rootSecret, "path", cs.kdf)
2,007✔
378
  return [newPkp, commitSecret] as const
2,007✔
379
}
2,007✔
380

381
export async function processMessage(
266✔
382
  message: MlsPrivateMessage | MlsPublicMessage,
266✔
383
  state: ClientState,
266✔
384
  pskIndex: PskIndex,
266✔
385
  action: IncomingMessageCallback,
266✔
386
  cs: CiphersuiteImpl,
266✔
387
): Promise<ProcessMessageResult> {
266✔
388
  if (message.wireformat === "mls_public_message") {
266✔
389
    const result = await processPublicMessage(state, message.publicMessage, pskIndex, cs, action)
114✔
390

391
    return { ...result, kind: "newState" }
114✔
392
  } else return processPrivateMessage(state, message.privateMessage, emptyPskIndex, cs, action)
152✔
393
}
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