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

LukaJCB / ts-mls / 26133710435

20 May 2026 12:29AM UTC coverage: 96.578% (-0.1%) from 96.703%
26133710435

push

github

web-flow
Use updated tree for encryption for updatePath (#505)

445 of 449 branches covered (99.11%)

Branch coverage included in aggregate %.

7 of 8 new or added lines in 2 files covered. (87.5%)

1 existing line in 1 file now uncovered.

2462 of 2561 relevant lines covered (96.13%)

148478.95 hits per line

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

95.87
/src/clientState.ts
1
import { AuthenticatedContent, makeProposalRef } from "./authenticatedContent.js"
2
import { CiphersuiteImpl } from "./crypto/ciphersuite.js"
3
import { Hash } from "./crypto/hash.js"
4
import {
5
  ExtensionExternalSenders,
6
  ExtensionRequiredCapabilities,
7
  extensionsEqual,
8
  extensionsSupportedByCapabilities,
9
  GroupContextExtension,
10
  GroupInfoExtension,
11
} from "./extension.js"
12
import { createConfirmationTag, FramedContentCommit } from "./framedContent.js"
13
import { groupContextDecoder, GroupContext, groupContextEncoder } from "./groupContext.js"
14
import { ratchetTreeFromExtension, verifyGroupInfoConfirmationTag, verifyGroupInfoSignature } from "./groupInfo.js"
15
import { KeyPackage, makeKeyPackageRef, PrivateKeyPackage, verifyKeyPackage } from "./keyPackage.js"
16
import {
17
  keyScheduleDecoder,
18
  deriveKeySchedule,
19
  initializeKeySchedule,
20
  KeySchedule,
21
  keyScheduleEncoder,
22
} from "./keySchedule.js"
23
import { pskIdEncoder, PskId, pskTypes, resumptionPSKUsages } from "./presharedkey.js"
24

25
import {
26
  ratchetTreeDecoder,
27
  findBlankLeafNodeIndexOrExtend,
28
  findLeafIndex,
29
  ratchetTreeEncoder,
30
  updateLeafNodeMutable,
31
  addLeafNodeMutable,
32
  removeLeafNodeMutable,
33
} from "./ratchetTree.js"
34
import { RatchetTree } from "./ratchetTree.js"
35
import {
36
  appendSecretTreeValues,
37
  createSecretTree,
38
  SecretTree,
39
  secretTreeDecoder,
40
  secretTreeEncoder,
41
} from "./secretTree.js"
42
import { createConfirmedHash, createInterimHash } from "./transcriptHash.js"
43
import { treeHashRoot, TreeHashCache } from "./treeHash.js"
44
import {
45
  directPath,
46
  isLeaf,
47
  LeafIndex,
48
  leafToNodeIndex,
49
  leafWidth,
50
  nodeToLeafIndex,
51
  toLeafIndex,
52
  toNodeIndex,
53
} from "./treemath.js"
54
import { WireformatName, wireformats } from "./wireformat.js"
55
import { ProposalOrRef, proposalOrRefTypes } from "./proposalOrRefType.js"
56
import {
57
  isDefaultProposal,
58
  Proposal,
59
  ProposalAdd,
60
  ProposalExternalInit,
61
  ProposalGroupContextExtensions,
62
  ProposalPSK,
63
  ProposalReinit,
64
  ProposalRemove,
65
  ProposalUpdate,
66
  Reinit,
67
  Remove,
68
} from "./proposal.js"
69
import { defaultProposalTypes } from "./defaultProposalType.js"
70
import { defaultExtensionTypes } from "./defaultExtensionType.js"
71
import { pathToRoot } from "./pathSecrets.js"
72
import {
73
  PrivateKeyPath,
74
  privateKeyPathDecoder,
75
  mergePrivateKeyPaths,
76
  privateKeyPathEncoder,
77
  toPrivateKeyPath,
78
} from "./privateKeyPath.js"
79
import {
80
  UnappliedProposals,
81
  addUnappliedProposal,
82
  ProposalWithSender,
83
  unappliedProposalsEncoder,
84
  unappliedProposalsDecoder,
85
} from "./unappliedProposals.js"
86
import { accumulatePskSecret, PskIndex } from "./pskIndex.js"
87
import { getSenderLeafNodeIndex } from "./sender.js"
88
import { addToMap } from "./util/addToMap.js"
89
import { bytesToBase64, zeroOutUint8Array } from "./util/byteArray.js"
90
import { constantTimeEqual } from "./util/constantTimeCompare.js"
91
import {
92
  CryptoVerificationError,
93
  CodecError,
94
  InternalError,
95
  UsageError,
96
  ValidationError,
97
  MlsError,
98
} from "./mlsError.js"
99
import { Signature } from "./crypto/signature.js"
100
import {
101
  LeafNode,
102
  LeafNodeCommit,
103
  LeafNodeKeyPackage,
104
  LeafNodeUpdate,
105
  verifyLeafNodeSignature,
106
  verifyLeafNodeSignatureKeyPackage,
107
} from "./leafNode.js"
108
import { leafNodeSources } from "./leafNodeSource.js"
109
import { nodeTypes } from "./nodeType.js"
110
import { protocolVersions } from "./protocolVersion.js"
111
import { RequiredCapabilities } from "./requiredCapabilities.js"
112
import { Capabilities } from "./capabilities.js"
113
import { verifyParentHashes } from "./parentHash.js"
114
import { firstCommonAncestor } from "./updatePath.js"
115
import { decryptGroupInfo, decryptGroupSecrets, Welcome } from "./welcome.js"
116
import { AuthenticationService } from "./authenticationService.js"
117
import { LifetimeConfig } from "./lifetimeConfig.js"
118
import { KeyPackageEqualityConfig } from "./keyPackageEqualityConfig.js"
119
import { ClientConfig, defaultClientConfig } from "./clientConfig.js"
120
import { Encoder, contramapBufferEncoders, encode } from "./codec/tlsEncoder.js"
121

122
import {
123
  bigintMapEncoder,
124
  bigintMapDecoder,
125
  varLenDataDecoder,
126
  varLenDataEncoder,
127
  varLenTypeDecoder,
128
  varLenTypeEncoder,
129
} from "./codec/variableLength.js"
130
import { optionalDecoder, optionalEncoder } from "./codec/optional.js"
131
import { groupActiveStateDecoder, GroupActiveState, groupActiveStateEncoder } from "./groupActiveState.js"
132
import { epochReceiverDataDecoder, EpochReceiverData, epochReceiverDataEncoder } from "./epochReceiverData.js"
133
import { Decoder, mapDecoders } from "./codec/tlsDecoder.js"
134
import { deriveSecret } from "./crypto/kdf.js"
135
import { MlsContext } from "./mlsContext.js"
136

137
/** @public */
138
export type ClientState = GroupState & PublicGroupState
139

140
/** @public */
141
export interface PublicGroupState {
142
  ratchetTree: RatchetTree
143
  groupContext: GroupContext
144
}
145

146
/** @public */
147
export interface GroupState {
148
  keySchedule: KeySchedule
149
  secretTree: SecretTree
150
  privatePath: PrivateKeyPath
151
  signaturePrivateKey: Uint8Array
152
  unappliedProposals: UnappliedProposals
153
  confirmationTag: Uint8Array
154
  historicalReceiverData: Map<bigint, EpochReceiverData>
155
  groupActiveState: GroupActiveState
156
  treeHashCache: TreeHashCache
157
}
158

159
/** @public */
160
export const publicGroupStateEncoder: Encoder<PublicGroupState> = contramapBufferEncoders(
3✔
161
  [groupContextEncoder, ratchetTreeEncoder],
162
  (state) => [state.groupContext, state.ratchetTree] as const,
38✔
163
)
164

165
const treeHashCacheEncoder: Encoder<TreeHashCache> = varLenTypeEncoder(optionalEncoder(varLenDataEncoder))
3✔
166
const treeHashCacheDecoder: Decoder<TreeHashCache> = varLenTypeDecoder(optionalDecoder(varLenDataDecoder))
3✔
167

168
/** @public */
169
export const groupStateEncoder: Encoder<GroupState> = contramapBufferEncoders(
3✔
170
  [
171
    keyScheduleEncoder,
172
    secretTreeEncoder,
173
    privateKeyPathEncoder,
174
    varLenDataEncoder,
175
    unappliedProposalsEncoder,
176
    varLenDataEncoder,
177
    bigintMapEncoder(epochReceiverDataEncoder),
178
    groupActiveStateEncoder,
179
    treeHashCacheEncoder,
180
  ],
181
  (state) =>
182
    [
38✔
183
      state.keySchedule,
184
      state.secretTree,
185
      state.privatePath,
186
      state.signaturePrivateKey,
187
      state.unappliedProposals,
188
      state.confirmationTag,
189
      state.historicalReceiverData,
190
      state.groupActiveState,
191
      state.treeHashCache,
192
    ] as const,
193
)
194

195
/** @public */
196
export const clientStateEncoder: Encoder<ClientState> = contramapBufferEncoders(
3✔
197
  [publicGroupStateEncoder, groupStateEncoder],
198
  (state) => [state, state] as const,
38✔
199
)
200

201
/** @public */
202
export const publicGroupStateDecoder: Decoder<PublicGroupState> = mapDecoders(
3✔
203
  [groupContextDecoder, ratchetTreeDecoder],
204
  (groupContext, ratchetTree) => ({
38✔
205
    groupContext,
206
    ratchetTree,
207
  }),
208
)
209

210
/** @public */
211
export const groupStateDecoder: Decoder<GroupState> = mapDecoders(
3✔
212
  [
213
    keyScheduleDecoder,
214
    secretTreeDecoder,
215
    privateKeyPathDecoder,
216
    varLenDataDecoder,
217
    unappliedProposalsDecoder,
218
    varLenDataDecoder,
219
    bigintMapDecoder(epochReceiverDataDecoder),
220
    groupActiveStateDecoder,
221
    treeHashCacheDecoder,
222
  ],
223
  (
224
    keySchedule,
225
    secretTree,
226
    privatePath,
227
    signaturePrivateKey,
228
    unappliedProposals,
229
    confirmationTag,
230
    historicalReceiverData,
231
    groupActiveState,
232
    treeHashCache,
233
  ) => ({
38✔
234
    keySchedule,
235
    secretTree,
236
    privatePath,
237
    signaturePrivateKey,
238
    unappliedProposals,
239
    confirmationTag,
240
    historicalReceiverData,
241
    groupActiveState,
242
    treeHashCache,
243
  }),
244
)
245

246
/** @public */
247
export const clientStateDecoder: Decoder<ClientState> = mapDecoders(
3✔
248
  [publicGroupStateDecoder, groupStateDecoder],
249
  (publicState, state) => ({
38✔
250
    ...publicState,
251
    ...state,
252
  }),
253
)
254

255
/** @public */
256
export function getOwnLeafNode(state: ClientState): LeafNode {
257
  const idx = leafToNodeIndex(toLeafIndex(state.privatePath.leafIndex))
158✔
258
  const leaf = state.ratchetTree[idx]
158✔
259
  if (leaf?.nodeType !== nodeTypes.leaf) throw new InternalError("Expected leaf node")
158✔
260
  return leaf.leaf
158✔
261
}
262

263
/** @public */
264
export interface SignatureKeyPair {
265
  signKey: Uint8Array
266
  publicKey: Uint8Array
267
}
268

269
/** @public */
270
export function getOwnSignatureKeyPair(state: ClientState): SignatureKeyPair {
271
  return {
3✔
272
    signKey: state.signaturePrivateKey,
273
    publicKey: getOwnLeafNode(state).signaturePublicKey,
274
  }
275
}
276

277
/** @public */
278
export function getGroupMembers(state: ClientState): LeafNode[] {
279
  return extractFromGroupMembers(
1✔
280
    state,
281
    () => false,
3✔
282
    (l) => l,
3✔
283
  )
284
}
285

286
export function extractFromGroupMembers<T>(
287
  state: ClientState,
288
  exclude: (l: LeafNode) => boolean,
289
  map: (l: LeafNode) => T,
290
): T[] {
291
  const recipients = []
3✔
292
  for (const node of state.ratchetTree) {
3✔
293
    if (node?.nodeType === nodeTypes.leaf && !exclude(node.leaf)) {
21✔
294
      recipients.push(map(node.leaf))
8✔
295
    }
296
  }
297
  return recipients
3✔
298
}
299

300
export function checkCanSendApplicationMessages(state: ClientState): void {
301
  if (Object.keys(state.unappliedProposals).length !== 0)
2,856✔
302
    throw new UsageError("Cannot send application message with unapplied proposals")
19✔
303

304
  checkCanSendHandshakeMessages(state)
2,837✔
305
}
306

307
export function checkCanSendHandshakeMessages(state: ClientState): void {
308
  if (state.groupActiveState.kind === "suspendedPendingReinit")
5,229✔
309
    throw new UsageError("Cannot send messages while Group is suspended pending reinit")
5,210✔
310
  else if (state.groupActiveState.kind === "removedFromGroup")
311
    throw new UsageError("Cannot send messages after being removed from group")
76✔
312
}
313

314
interface Proposals {
315
  [defaultProposalTypes.add]: { senderLeafIndex: number | undefined; proposal: ProposalAdd }[]
316
  [defaultProposalTypes.update]: { senderLeafIndex: number | undefined; proposal: ProposalUpdate }[]
317
  [defaultProposalTypes.remove]: { senderLeafIndex: number | undefined; proposal: ProposalRemove }[]
318
  [defaultProposalTypes.psk]: { senderLeafIndex: number | undefined; proposal: ProposalPSK }[]
319
  [defaultProposalTypes.reinit]: { senderLeafIndex: number | undefined; proposal: ProposalReinit }[]
320
  [defaultProposalTypes.external_init]: { senderLeafIndex: number | undefined; proposal: ProposalExternalInit }[]
321
  [defaultProposalTypes.group_context_extensions]: {
322
    senderLeafIndex: number | undefined
323
    proposal: ProposalGroupContextExtensions
324
  }[]
325
}
326

327
const emptyProposals: Proposals = {
3✔
328
  [defaultProposalTypes.add]: [],
329
  [defaultProposalTypes.update]: [],
330
  [defaultProposalTypes.remove]: [],
331
  [defaultProposalTypes.psk]: [],
332
  [defaultProposalTypes.reinit]: [],
333
  [defaultProposalTypes.external_init]: [],
334
  [defaultProposalTypes.group_context_extensions]: [],
335
}
336

337
function flattenExtensions(
338
  groupContextExtensions: { proposal: ProposalGroupContextExtensions }[],
339
): GroupContextExtension[] {
340
  return groupContextExtensions[0]?.proposal.groupContextExtensions.extensions ?? []
11,501✔
341
}
342

343
async function validateProposals(
344
  p: Proposals,
345
  committerLeafIndex: number | undefined,
346
  groupContext: GroupContext,
347
  config: KeyPackageEqualityConfig,
348
  authService: AuthenticationService,
349
  tree: RatchetTree,
350
): Promise<MlsError | undefined> {
351
  const containsUpdateByCommitter = p[defaultProposalTypes.update].some(
5,778✔
352
    (o) => o.senderLeafIndex !== undefined && o.senderLeafIndex === committerLeafIndex,
129✔
353
  )
354

355
  if (containsUpdateByCommitter)
5,778✔
356
    return new ValidationError("Commit cannot contain an update proposal sent by committer")
1✔
357

358
  const containsRemoveOfCommitter = p[defaultProposalTypes.remove].some(
5,777✔
359
    (o) => o.proposal.remove.removed === committerLeafIndex,
2,428✔
360
  )
361

362
  if (containsRemoveOfCommitter)
5,777✔
363
    return new ValidationError("Commit cannot contain a remove proposal removing committer")
1✔
364

365
  const multipleUpdateRemoveForSameLeaf =
366
    p[defaultProposalTypes.update].some(
5,776✔
367
      ({ senderLeafIndex: a }, indexA) =>
368
        p[defaultProposalTypes.update].some(({ senderLeafIndex: b }, indexB) => a === b && indexA !== indexB) ||
128✔
369
        p[defaultProposalTypes.remove].some((r) => r.proposal.remove.removed === a),
7✔
370
    ) ||
371
    p[defaultProposalTypes.remove].some(
372
      (a, indexA) =>
373
        p[defaultProposalTypes.remove].some(
2,426✔
374
          (b, indexB) => b.proposal.remove.removed === a.proposal.remove.removed && indexA !== indexB,
40,379✔
375
        ) ||
376
        p[defaultProposalTypes.update].some(({ senderLeafIndex }) => a.proposal.remove.removed === senderLeafIndex),
7✔
377
    )
378

379
  if (multipleUpdateRemoveForSameLeaf)
5,778✔
380
    return new ValidationError(
1✔
381
      "Commit cannot contain multiple update and/or remove proposals that apply to the same leaf",
382
    )
383

384
  const multipleAddsContainSameKeypackage = p[defaultProposalTypes.add].some(({ proposal: a }, indexA) =>
5,775✔
385
    p[defaultProposalTypes.add].some(
3,498✔
386
      ({ proposal: b }, indexB) => config.compareKeyPackages(a.add.keyPackage, b.add.keyPackage) && indexA !== indexB,
33,391✔
387
    ),
388
  )
389

390
  if (multipleAddsContainSameKeypackage)
5,775✔
391
    return new ValidationError(
1✔
392
      "Commit cannot contain multiple Add proposals that contain KeyPackages that represent the same client",
393
    )
394

395
  // checks if there is an Add proposal with a KeyPackage that matches a client already in the group
396
  // unless there is a Remove proposal in the list removing the matching client from the group.
397
  const addsContainExistingKeypackage = p[defaultProposalTypes.add].some(({ proposal }) =>
5,774✔
398
    tree.some(
3,497✔
399
      (node, nodeIndex) =>
400
        node !== undefined &&
186,981✔
401
        node.nodeType === nodeTypes.leaf &&
402
        config.compareKeyPackageToLeafNode(proposal.add.keyPackage, node.leaf) &&
403
        p[defaultProposalTypes.remove].every(
404
          (r) => r.proposal.remove.removed !== nodeToLeafIndex(toNodeIndex(nodeIndex)),
×
405
        ),
406
    ),
407
  )
408

409
  if (addsContainExistingKeypackage)
5,774✔
410
    return new ValidationError("Commit cannot contain an Add proposal for someone already in the group")
1✔
411

412
  const everyLeafSupportsGroupExtensions = p[defaultProposalTypes.add].every(({ proposal }) =>
5,773✔
413
    extensionsSupportedByCapabilities(groupContext.extensions, proposal.add.keyPackage.leafNode.capabilities),
414
  )
415

416
  if (!everyLeafSupportsGroupExtensions)
5,773✔
417
    return new ValidationError("Added leaf node that doesn't support extension in GroupContext")
19✔
418

419
  const multiplePskWithSamePskId = p[defaultProposalTypes.psk].some((a, indexA) =>
5,754✔
420
    p[defaultProposalTypes.psk].some(
266✔
421
      (b, indexB) =>
371✔
422
        constantTimeEqual(
423
          encode(pskIdEncoder, a.proposal.psk.preSharedKeyId),
424
          encode(pskIdEncoder, b.proposal.psk.preSharedKeyId),
425
        ) && indexA !== indexB,
426
    ),
427
  )
428

429
  if (multiplePskWithSamePskId)
5,754✔
430
    return new ValidationError("Commit cannot contain PreSharedKey proposals that reference the same PreSharedKeyID")
1✔
431

432
  const multipleGroupContextExtensions = p[defaultProposalTypes.group_context_extensions].length > 1
5,753✔
433

434
  if (multipleGroupContextExtensions)
5,753✔
435
    return new ValidationError("Commit cannot contain multiple GroupContextExtensions proposals")
1✔
436

437
  const allExtensions = flattenExtensions(p[defaultProposalTypes.group_context_extensions])
5,752✔
438

439
  const requiredCapabilities = allExtensions.find(
5,752✔
440
    (e): e is ExtensionRequiredCapabilities => e.extensionType === defaultExtensionTypes.required_capabilities,
98✔
441
  )
442

443
  if (requiredCapabilities !== undefined) {
5,752✔
444
    const caps = requiredCapabilities.extensionData
2✔
445
    const everyLeafSupportsCapabilities = tree
2✔
446
      .filter((n) => n !== undefined && n.nodeType === nodeTypes.leaf)
14✔
447
      .every((l) => capabiltiesAreSupported(caps, l.leaf.capabilities))
4✔
448

449
    if (!everyLeafSupportsCapabilities) return new ValidationError("Not all members support required capabilities")
2✔
450

451
    const allAdditionsSupportCapabilities = p[defaultProposalTypes.add].every((a) =>
1✔
452
      capabiltiesAreSupported(caps, a.proposal.add.keyPackage.leafNode.capabilities),
1✔
453
    )
454

455
    if (!allAdditionsSupportCapabilities)
1✔
456
      return new ValidationError("Commit contains add proposals of member without required capabilities")
1✔
457
  }
458

459
  return await validateExternalSenders(allExtensions, authService)
5,750✔
460
}
461

462
async function validateExternalSenders(
463
  extensions: GroupContextExtension[],
464
  authService: AuthenticationService,
465
): Promise<MlsError | undefined> {
466
  const externalSenders = extensions.find(
6,873✔
467
    (e): e is ExtensionExternalSenders => e.extensionType === defaultExtensionTypes.external_senders,
172✔
468
  )
469
  if (externalSenders) {
6,873✔
470
    for (const externalSender of externalSenders.extensionData) {
134✔
471
      const validCredential = await authService.validateCredential(
153✔
472
        externalSender.credential,
473
        externalSender.signaturePublicKey,
474
      )
475
      if (!validCredential) return new ValidationError("Could not validate external credential")
153✔
476
    }
477
  }
478
}
479

480
function capabiltiesAreSupported(caps: RequiredCapabilities, cs: Capabilities): boolean {
481
  return (
81✔
482
    caps.credentialTypes.every((c) => cs.credentials.includes(c)) &&
483
    caps.extensionTypes.every((e) => cs.extensions.includes(e)) &&
484
    caps.proposalTypes.every((p) => cs.proposals.includes(p))
1✔
485
  )
486
}
487

488
export async function validateRatchetTree(
489
  tree: RatchetTree,
490
  groupContext: GroupContext,
491
  config: LifetimeConfig,
492
  authService: AuthenticationService,
493
  treeHash: Uint8Array,
494
  cs: CiphersuiteImpl,
495
  mutableTreeHashCache?: TreeHashCache,
496
): Promise<MlsError | undefined> {
497
  const cache = mutableTreeHashCache ?? []
1,554✔
498
  const hpkeKeys = new Set<string>()
1,554✔
499
  const signatureKeys = new Set<string>()
1,554✔
500
  const credentialTypes = new Set<number>()
1,554✔
501
  for (const [i, n] of tree.entries()) {
1,554✔
502
    const nodeIndex = toNodeIndex(i)
9,808✔
503
    if (n?.nodeType === nodeTypes.leaf) {
9,808✔
504
      if (!isLeaf(nodeIndex)) return new ValidationError("Received Ratchet Tree is not structurally sound")
5,167✔
505

506
      const hpkeKey = bytesToBase64(n.leaf.hpkePublicKey)
5,167✔
507
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
5,167✔
508
      else hpkeKeys.add(hpkeKey)
5,148✔
509

510
      const signatureKey = bytesToBase64(n.leaf.signaturePublicKey)
5,148✔
511
      if (signatureKeys.has(signatureKey)) return new ValidationError("signature keys not unique")
5,148✔
512
      else signatureKeys.add(signatureKey)
5,129✔
513

514
      {
515
        credentialTypes.add(n.leaf.credential.credentialType)
5,129✔
516
      }
517

518
      const err =
519
        n.leaf.leafNodeSource === leafNodeSources.key_package
5,129✔
520
          ? await validateLeafNodeKeyPackage(n.leaf, groupContext, false, config, authService, cs.signature)
521
          : await validateLeafNodeUpdateOrCommit(
522
              n.leaf,
523
              nodeToLeafIndex(nodeIndex),
524
              groupContext,
525
              authService,
526
              cs.signature,
527
            )
528

529
      if (err !== undefined) return err
474✔
530
    } else if (n?.nodeType === nodeTypes.parent) {
4,641✔
531
      if (isLeaf(nodeIndex)) return new ValidationError("Received Ratchet Tree is not structurally sound")
604✔
532

533
      const hpkeKey = bytesToBase64(n.parent.hpkePublicKey)
585✔
534
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
585✔
535
      else hpkeKeys.add(hpkeKey)
585✔
536

537
      for (const unmergedLeaf of n.parent.unmergedLeaves) {
585✔
538
        const leafIndex = toLeafIndex(unmergedLeaf)
114✔
539
        const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length))
114✔
540
        const nodeIndex = leafToNodeIndex(leafIndex)
114✔
541
        if (tree[nodeIndex]?.nodeType !== nodeTypes.leaf && !dp.includes(toNodeIndex(i)))
114✔
542
          return new ValidationError("Unmerged leaf did not represent a non-blank descendant leaf node")
×
543

544
        for (const parentIdx of dp) {
114✔
545
          if (parentIdx === toNodeIndex(i)) break
342✔
546
          const dpNode = tree[parentIdx]
228✔
547

548
          if (dpNode !== undefined) {
228✔
UNCOV
549
            if (dpNode.nodeType !== nodeTypes.parent) return new InternalError("Expected parent node")
×
550

NEW
551
            if (!dpNode.parent.unmergedLeaves.includes(unmergedLeaf))
×
552
              return new ValidationError("non-blank intermediate node must list leaf node in its unmerged_leaves")
×
553
          }
554
        }
555
      }
556
    }
557
  }
558

559
  for (const n of tree) {
1,440✔
560
    if (n?.nodeType === nodeTypes.leaf) {
9,618✔
561
      for (const credentialType of credentialTypes) {
5,034✔
562
        if (!n.leaf.capabilities.credentials.includes(credentialType))
5,034✔
563
          return new ValidationError("LeafNode has credential that is not supported by member of the group")
×
564
      }
565
    }
566
  }
567

568
  const parentHashesVerified = await verifyParentHashes(tree, cs.hash, cache)
1,440✔
569

570
  if (!parentHashesVerified) return new CryptoVerificationError("Unable to verify parent hash")
1,440✔
571

572
  if (!constantTimeEqual(treeHash, await treeHashRoot(tree, cs.hash, cache)))
1,421✔
573
    return new ValidationError("Unable to verify tree hash")
19✔
574
}
575

576
export async function validateLeafNodeUpdateOrCommit(
577
  leafNode: LeafNodeCommit | LeafNodeUpdate,
578
  leafIndex: number,
579
  groupContext: GroupContext,
580
  authService: AuthenticationService,
581
  s: Signature,
582
): Promise<MlsError | undefined> {
583
  const signatureValid = await verifyLeafNodeSignature(leafNode, groupContext.groupId, leafIndex, s)
3,274✔
584

585
  if (!signatureValid) return new CryptoVerificationError("Could not verify leaf node signature")
3,274✔
586

587
  const commonError = await validateLeafNodeCommon(leafNode, groupContext, authService)
3,255✔
588

589
  if (commonError !== undefined) return commonError
3,255✔
590
}
591

592
export function throwIfDefined(err: MlsError | undefined): void {
593
  if (err !== undefined) throw err
20,071✔
594
}
595

596
async function validateLeafNodeCommon(
597
  leafNode: LeafNode,
598
  groupContext: GroupContext,
599
  authService: AuthenticationService,
600
) {
601
  const credentialValid = await authService.validateCredential(leafNode.credential, leafNode.signaturePublicKey)
11,328✔
602

603
  if (!credentialValid) return new ValidationError("Could not validate credential")
11,328✔
604

605
  const requiredCapabilities = groupContext.extensions.find(
11,308✔
606
    (e): e is ExtensionRequiredCapabilities => e.extensionType === defaultExtensionTypes.required_capabilities,
399✔
607
  )
608

609
  if (requiredCapabilities !== undefined) {
11,308✔
610
    const caps = requiredCapabilities.extensionData
76✔
611

612
    const leafSupportsCapabilities = capabiltiesAreSupported(caps, leafNode.capabilities)
76✔
613

614
    if (!leafSupportsCapabilities) return new ValidationError("LeafNode does not support required capabilities")
76✔
615
  }
616

617
  const extensionsSupported = extensionsSupportedByCapabilities(leafNode.extensions, leafNode.capabilities)
11,289✔
618

619
  if (!extensionsSupported) return new ValidationError("LeafNode contains extension not listed in capabilities")
11,289✔
620
}
621

622
async function validateLeafNodeKeyPackage(
623
  leafNode: LeafNodeKeyPackage,
624
  groupContext: GroupContext,
625
  sentByClient: boolean,
626
  config: LifetimeConfig,
627
  authService: AuthenticationService,
628
  s: Signature,
629
): Promise<MlsError | undefined> {
630
  const signatureValid = await verifyLeafNodeSignatureKeyPackage(leafNode, s)
8,092✔
631
  if (!signatureValid) return new CryptoVerificationError("Could not verify leaf node signature")
8,092✔
632

633
  //verify lifetime
634
  if (sentByClient || config.validateLifetimeOnReceive) {
8,073✔
635
    if (leafNode.leafNodeSource === leafNodeSources.key_package) {
1,389✔
636
      const currentTime = BigInt(Math.floor(Date.now() / 1000))
1,389✔
637
      if (leafNode.lifetime.notBefore > currentTime || leafNode.lifetime.notAfter < currentTime)
1,389✔
638
        return new ValidationError("Current time not within Lifetime")
×
639
    }
640
  }
641

642
  const commonError = await validateLeafNodeCommon(leafNode, groupContext, authService)
8,073✔
643

644
  if (commonError !== undefined) return commonError
8,073✔
645
}
646

647
export async function validateLeafNodeCredentialAndKeyUniqueness(
648
  tree: RatchetTree,
649
  leafNode: LeafNode,
650
  existingLeafIndex?: number,
651
): Promise<ValidationError | undefined> {
652
  const hpkeKeys = new Set<string>()
6,238✔
653
  const signatureKeys = new Set<string>()
6,238✔
654
  for (const [nodeIndex, node] of tree.entries()) {
6,238✔
655
    if (node?.nodeType === nodeTypes.leaf) {
236,930✔
656
      const credentialType = leafNode.credential.credentialType
64,834✔
657
      if (!node.leaf.capabilities.credentials.includes(credentialType)) {
64,834✔
658
        return new ValidationError("LeafNode has credential that is not supported by member of the group")
1✔
659
      }
660

661
      const hpkeKey = bytesToBase64(node.leaf.hpkePublicKey)
64,833✔
662
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
64,833✔
663
      else hpkeKeys.add(hpkeKey)
64,833✔
664

665
      const signatureKey = bytesToBase64(node.leaf.signaturePublicKey)
64,833✔
666
      if (signatureKeys.has(signatureKey) && existingLeafIndex !== nodeToLeafIndex(toNodeIndex(nodeIndex)))
64,833✔
667
        return new ValidationError("signature keys not unique")
×
668
      else signatureKeys.add(signatureKey)
64,833✔
669
    } else if (node?.nodeType === nodeTypes.parent) {
172,096✔
670
      const hpkeKey = bytesToBase64(node.parent.hpkePublicKey)
17,899✔
671
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
17,899✔
672
      else hpkeKeys.add(hpkeKey)
17,899✔
673
    }
674
  }
675
}
676

677
async function validateKeyPackage(
678
  kp: KeyPackage,
679
  groupContext: GroupContext,
680
  tree: RatchetTree,
681
  sentByClient: boolean,
682
  config: LifetimeConfig,
683
  authService: AuthenticationService,
684
  s: Signature,
685
): Promise<MlsError | undefined> {
686
  if (kp.cipherSuite !== groupContext.cipherSuite) return new ValidationError("Invalid CipherSuite")
3,476✔
687

688
  if (kp.version !== groupContext.version) return new ValidationError("Invalid mls version")
3,457✔
689

690
  const leafNodeConsistentWithTree = await validateLeafNodeCredentialAndKeyUniqueness(tree, kp.leafNode)
3,438✔
691

692
  if (leafNodeConsistentWithTree !== undefined) return leafNodeConsistentWithTree
3,438✔
693

694
  const leafNodeError = await validateLeafNodeKeyPackage(
3,437✔
695
    kp.leafNode,
696
    groupContext,
697
    sentByClient,
698
    config,
699
    authService,
700
    s,
701
  )
702
  if (leafNodeError !== undefined) return leafNodeError
3,437✔
703

704
  const signatureValid = await verifyKeyPackage(kp, s)
3,416✔
705
  if (!signatureValid) return new CryptoVerificationError("Invalid keypackage signature")
3,416✔
706

707
  if (constantTimeEqual(kp.initKey, kp.leafNode.hpkePublicKey))
3,397✔
708
    return new ValidationError("Cannot have identicial init and encryption keys")
3,397✔
709
}
710

711
function validateReinit(
712
  allProposals: ProposalWithSender[],
713
  reinit: Reinit,
714
  gc: GroupContext,
715
): ValidationError | undefined {
716
  if (allProposals.length !== 1) return new ValidationError("Reinit proposal needs to be commited by itself")
77✔
717

718
  if (reinit.version < gc.version)
77✔
719
    return new ValidationError("A ReInit proposal cannot use a version less than the version for the current group")
×
720
}
721

722
function validateExternalInit(grouped: Proposals): ValidationError | undefined {
723
  if (grouped[defaultProposalTypes.external_init].length > 1)
76✔
724
    return new ValidationError("Cannot contain more than one external_init proposal")
×
725

726
  if (grouped[defaultProposalTypes.remove].length > 1)
76✔
727
    return new ValidationError("Cannot contain more than one remove proposal")
×
728

729
  if (
76✔
730
    grouped[defaultProposalTypes.add].length > 0 ||
731
    grouped[defaultProposalTypes.group_context_extensions].length > 0 ||
732
    grouped[defaultProposalTypes.reinit].length > 0 ||
733
    grouped[defaultProposalTypes.update].length > 0
734
  )
735
    return new ValidationError("Invalid proposals")
×
736
}
737

738
function validateRemove(remove: Remove, tree: RatchetTree): MlsError | undefined {
739
  if (tree[leafToNodeIndex(toLeafIndex(remove.removed))] === undefined)
2,425✔
740
    return new ValidationError("Tried to remove empty leaf node")
×
741
}
742

743
export interface ApplyProposalsResult {
744
  pskSecret: Uint8Array
745
  pskIds: PskId[]
746
  needsUpdatePath: boolean
747
  additionalResult: ApplyProposalsData
748
  selfRemoved: boolean
749
  allProposals: ProposalWithSender[]
750
  updatedLeaves: LeafIndex[]
751
  removedLeaves: LeafIndex[]
752
}
753

754
type ApplyProposalsData =
755
  | { kind: "memberCommit"; addedLeafNodes: [LeafIndex, KeyPackage][]; extensions: GroupContextExtension[] }
756
  | { kind: "externalCommit"; externalInitSecret: Uint8Array; newMemberLeafIndex: LeafIndex }
757
  | { kind: "reinit"; reinit: Reinit }
758

759
export async function applyProposals(
760
  state: ClientState,
761
  mutableTree: RatchetTree,
762
  proposals: ProposalOrRef[],
763
  committerLeafIndex: LeafIndex | undefined,
764
  pskSearch: PskIndex,
765
  sentByClient: boolean,
766
  clientConfig: ClientConfig,
767
  authService: AuthenticationService,
768
  cs: CiphersuiteImpl,
769
): Promise<ApplyProposalsResult> {
770
  const allProposals = proposals.reduce((acc, cur) => {
5,931✔
771
    if (cur.proposalOrRefType === proposalOrRefTypes.proposal)
6,756✔
772
      return [...acc, { proposal: cur.proposal, senderLeafIndex: committerLeafIndex }]
4,712✔
773

774
    const p = state.unappliedProposals[bytesToBase64(cur.reference)]
2,044✔
775
    if (p === undefined) throw new ValidationError("Could not find proposal with supplied reference")
2,044✔
776
    return [...acc, p]
2,044✔
777
  }, [] as ProposalWithSender[])
778

779
  const grouped = allProposals.reduce((acc, cur) => {
5,931✔
780
    //this skips any custom proposals
781
    if (isDefaultProposal(cur.proposal)) {
6,756✔
782
      const proposalType = cur.proposal.proposalType
6,642✔
783
      const proposals = acc[proposalType] ?? []
6,642✔
784
      return { ...acc, [cur.proposal.proposalType]: [...proposals, cur] }
6,642✔
785
    } else {
786
      return acc
114✔
787
    }
788
  }, emptyProposals)
789

790
  const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
5,931✔
791

792
  const isExternalInit = grouped[defaultProposalTypes.external_init].length > 0
5,931✔
793

794
  if (!isExternalInit) {
5,931✔
795
    if (grouped[defaultProposalTypes.reinit].length > 0) {
5,855✔
796
      const reinit = grouped[defaultProposalTypes.reinit].at(0)!.proposal.reinit
77✔
797

798
      throwIfDefined(validateReinit(allProposals, reinit, state.groupContext))
77✔
799

800
      return {
77✔
801
        pskSecret: zeroes,
802
        pskIds: [],
803
        needsUpdatePath: false,
804
        additionalResult: {
805
          kind: "reinit",
806
          reinit,
807
        },
808
        selfRemoved: false,
809
        allProposals,
810
        updatedLeaves: [],
811
        removedLeaves: [],
812
      }
813
    }
814

815
    throwIfDefined(
5,778✔
816
      await validateProposals(
817
        grouped,
818
        committerLeafIndex,
819
        state.groupContext,
820
        clientConfig.keyPackageEqualityConfig,
821
        authService,
822
        mutableTree,
823
      ),
824
    )
825

826
    const newExtensions = flattenExtensions(grouped[defaultProposalTypes.group_context_extensions])
5,778✔
827

828
    const addedLeafNodes = await applyTreeMutations(
5,778✔
829
      mutableTree,
830
      grouped,
831
      state.groupContext,
832
      sentByClient,
833
      authService,
834
      clientConfig.lifetimeConfig,
835
      cs.signature,
836
    )
837

838
    const [updatedPskSecret, pskIds] = await accumulatePskSecret(
5,670✔
839
      grouped[defaultProposalTypes.psk].map((p) => p.proposal.psk.preSharedKeyId),
265✔
840
      pskSearch,
841
      cs,
842
      zeroes,
843
    )
844

845
    const selfRemoved = mutableTree[leafToNodeIndex(toLeafIndex(state.privatePath.leafIndex))] === undefined
5,670✔
846

847
    const needsUpdatePath =
848
      allProposals.length === 0 ||
5,670✔
849
      allProposals.some(({ proposal }) => {
850
        const t = proposal.proposalType
4,994✔
851
        return t !== defaultProposalTypes.add && t !== defaultProposalTypes.psk && t !== defaultProposalTypes.reinit
4,994✔
852
      })
853

854
    const updatedLeaves: LeafIndex[] = [
5,855✔
855
      ...grouped[defaultProposalTypes.update].map(({ senderLeafIndex }) => toLeafIndex(senderLeafIndex!)),
128✔
856
      ...addedLeafNodes.map(([leafIndex]) => leafIndex),
3,397✔
857
    ]
858
    const removedLeaves: LeafIndex[] = grouped[defaultProposalTypes.remove].map(({ proposal }) =>
5,855✔
859
      toLeafIndex(proposal.remove.removed),
860
    )
861

862
    return {
5,855✔
863
      pskSecret: updatedPskSecret,
864
      additionalResult: {
865
        kind: "memberCommit" as const,
866
        addedLeafNodes,
867
        extensions: newExtensions,
868
      },
869
      pskIds,
870
      needsUpdatePath,
871
      selfRemoved,
872
      allProposals,
873
      updatedLeaves,
874
      removedLeaves,
875
    }
876
  } else {
877
    throwIfDefined(validateExternalInit(grouped))
76✔
878

879
    grouped[defaultProposalTypes.remove].forEach(({ proposal }) => {
76✔
880
      removeLeafNodeMutable(mutableTree, toLeafIndex(proposal.remove.removed))
38✔
881
    })
882

883
    const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
76✔
884

885
    const [updatedPskSecret, pskIds] = await accumulatePskSecret(
76✔
886
      grouped[defaultProposalTypes.psk].map((p) => p.proposal.psk.preSharedKeyId),
×
887
      pskSearch,
888
      cs,
889
      zeroes,
890
    )
891

892
    const initProposal = grouped[defaultProposalTypes.external_init].at(0)!
76✔
893

894
    const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
76✔
895

896
    const externalInitSecret = await importSecret(
76✔
897
      await cs.hpke.exportPrivateKey(externalKeyPair.privateKey),
898
      initProposal.proposal.externalInit.kemOutput,
899
      cs,
900
    )
901

902
    const removedLeaves: LeafIndex[] = grouped[defaultProposalTypes.remove].map(({ proposal }) =>
76✔
903
      toLeafIndex(proposal.remove.removed),
904
    )
905

906
    return {
76✔
907
      needsUpdatePath: true,
908
      pskSecret: updatedPskSecret,
909
      pskIds,
910
      additionalResult: {
911
        kind: "externalCommit",
912
        externalInitSecret,
913
        newMemberLeafIndex: nodeToLeafIndex(findBlankLeafNodeIndexOrExtend(mutableTree)),
914
      },
915
      selfRemoved: false,
916
      allProposals,
917
      updatedLeaves: [],
918
      removedLeaves,
919
    }
920
  }
921
}
922

923
/** @public */
924
export function makePskIndex(state: ClientState | undefined, externalPsks: Record<string, Uint8Array>): PskIndex {
925
  return {
3,940✔
926
    findPsk(preSharedKeyId) {
927
      if (preSharedKeyId.psktype === pskTypes.external) {
517✔
928
        return externalPsks[bytesToBase64(preSharedKeyId.pskId)]
261✔
929
      }
930

931
      if (state !== undefined && constantTimeEqual(preSharedKeyId.pskGroupId, state.groupContext.groupId)) {
256✔
932
        if (preSharedKeyId.pskEpoch === state.groupContext.epoch) return state.keySchedule.resumptionPsk
256✔
933
        else return state.historicalReceiverData.get(preSharedKeyId.pskEpoch)?.resumptionPsk
28✔
934
      }
935
    },
936
  }
937
}
938

939
export async function nextEpochContext(
940
  groupContext: GroupContext,
941
  wireformat: WireformatName,
942
  content: FramedContentCommit,
943
  signature: Uint8Array,
944
  updatedTreeHash: Uint8Array,
945
  confirmationTag: Uint8Array,
946
  h: Hash,
947
): Promise<GroupContext> {
948
  const interimTranscriptHash = await createInterimHash(groupContext.confirmedTranscriptHash, confirmationTag, h)
5,212✔
949
  const newConfirmedHash = await createConfirmedHash(
5,212✔
950
    interimTranscriptHash,
951
    { wireformat: wireformats[wireformat], content, signature },
952
    h,
953
  )
954

955
  return {
5,212✔
956
    ...groupContext,
957
    epoch: groupContext.epoch + 1n,
958
    treeHash: updatedTreeHash,
959
    confirmedTranscriptHash: newConfirmedHash,
960
  }
961
}
962

963
/** @public */
964
export async function joinGroup(params: {
965
  context: MlsContext
966
  welcome: Welcome
967
  keyPackage: KeyPackage
968
  privateKeys: PrivateKeyPackage
969
  ratchetTree?: RatchetTree
970
}): Promise<ClientState> {
971
  const res = await joinGroupInternal(params)
1,174✔
972

973
  return res.state
1,174✔
974
}
975

976
/** @public */
977
export interface JoinGroupResult {
978
  state: ClientState
979
  groupInfoExtensions: GroupInfoExtension[]
980
}
981

982
export async function joinGroupInternal(params: {
983
  context: MlsContext
984
  welcome: Welcome
985
  keyPackage: KeyPackage
986
  privateKeys: PrivateKeyPackage
987
  ratchetTree?: RatchetTree
988
  resumingFromState?: ClientState
989
}): Promise<JoinGroupResult> {
990
  const context = params.context
1,402✔
991
  const welcome = params.welcome
1,402✔
992
  const keyPackage = params.keyPackage
1,402✔
993
  const privateKeys = params.privateKeys
1,402✔
994

995
  const pskSearch = makePskIndex(params.resumingFromState, context.externalPsks ?? {})
1,402✔
996
  const authService = context.authService
1,402✔
997
  const cs = context.cipherSuite
1,402✔
998
  const clientConfig = context.clientConfig ?? defaultClientConfig
1,402✔
999

1000
  const ratchetTree = params.ratchetTree
1,402✔
1001
  const resumingFromState = params.resumingFromState
1,402✔
1002

1003
  const keyPackageRef = await makeKeyPackageRef(keyPackage, cs.hash)
1,402✔
1004
  const privKey = await cs.hpke.importPrivateKey(privateKeys.initPrivateKey)
1,402✔
1005
  const groupSecrets = await decryptGroupSecrets(privKey, keyPackageRef, welcome, cs.hpke)
1,402✔
1006

1007
  if (groupSecrets === undefined) throw new CodecError("Could not decode group secrets")
1,402✔
1008

1009
  const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
1,402✔
1010

1011
  const [pskSecret, pskIds] = await accumulatePskSecret(groupSecrets.psks, pskSearch, cs, zeroes)
1,402✔
1012

1013
  const gi = await decryptGroupInfo(welcome, groupSecrets.joinerSecret, pskSecret, cs)
1,402✔
1014
  if (gi === undefined) throw new CodecError("Could not decode group info")
1,402✔
1015

1016
  const resumptionPsk = pskIds.find((id) => id.psktype === pskTypes.resumption)
1,402✔
1017
  if (resumptionPsk !== undefined) {
1,402✔
1018
    if (resumingFromState === undefined) throw new ValidationError("No prior state passed for resumption")
114✔
1019

1020
    if (resumptionPsk.pskEpoch !== resumingFromState.groupContext.epoch) throw new ValidationError("Epoch mismatch")
114✔
1021

1022
    if (!constantTimeEqual(resumptionPsk.pskGroupId, resumingFromState.groupContext.groupId))
114✔
1023
      throw new ValidationError("old groupId mismatch")
×
1024

1025
    if (gi.groupContext.epoch !== 1n) throw new ValidationError("Resumption must be started at epoch 1")
114✔
1026

1027
    if (resumptionPsk.usage === resumptionPSKUsages.reinit) {
114✔
1028
      if (resumingFromState.groupActiveState.kind !== "suspendedPendingReinit")
76✔
1029
        throw new ValidationError("Found reinit psk but no old suspended clientState")
×
1030

1031
      if (!constantTimeEqual(resumingFromState.groupActiveState.reinit.groupId, gi.groupContext.groupId))
76✔
1032
        throw new ValidationError("new groupId mismatch")
19✔
1033

1034
      if (resumingFromState.groupActiveState.reinit.version !== gi.groupContext.version)
57✔
1035
        throw new ValidationError("Version mismatch")
19✔
1036

1037
      if (resumingFromState.groupActiveState.reinit.cipherSuite !== gi.groupContext.cipherSuite)
38✔
1038
        throw new ValidationError("Ciphersuite mismatch")
×
1039

1040
      if (!extensionsEqual(resumingFromState.groupActiveState.reinit.extensions, gi.groupContext.extensions))
38✔
1041
        throw new ValidationError("Extensions mismatch")
19✔
1042
    }
1043
  }
1044

1045
  const allExtensionsSupported = extensionsSupportedByCapabilities(
1,345✔
1046
    gi.groupContext.extensions,
1047
    keyPackage.leafNode.capabilities,
1048
  )
1049
  if (!allExtensionsSupported) throw new UsageError("client does not support every extension in the GroupContext")
1,345✔
1050

1051
  const tree = ratchetTreeFromExtension(gi) ?? ratchetTree
1,345✔
1052

1053
  if (tree === undefined) throw new UsageError("No RatchetTree passed and no ratchet_tree extension")
1,402✔
1054

1055
  const signerNode = tree[leafToNodeIndex(toLeafIndex(gi.signer))]
1,345✔
1056

1057
  if (signerNode === undefined) {
1,345✔
1058
    throw new ValidationError("Could not find signer leafNode")
×
1059
  }
1060
  if (signerNode.nodeType === nodeTypes.parent) throw new ValidationError("Expected non blank leaf node")
1,345✔
1061

1062
  const credentialVerified = await authService.validateCredential(
1,345✔
1063
    signerNode.leaf.credential,
1064
    signerNode.leaf.signaturePublicKey,
1065
  )
1066

1067
  if (!credentialVerified) throw new ValidationError("Could not validate credential")
1,345✔
1068

1069
  const groupInfoSignatureVerified = await verifyGroupInfoSignature(
1,345✔
1070
    gi,
1071
    signerNode.leaf.signaturePublicKey,
1072
    cs.signature,
1073
  )
1074

1075
  if (!groupInfoSignatureVerified) throw new CryptoVerificationError("Could not verify groupInfo signature")
1,345✔
1076

1077
  if (gi.groupContext.cipherSuite !== keyPackage.cipherSuite)
1,345✔
1078
    throw new ValidationError("cipher suite in the GroupInfo does not match the cipher_suite in the KeyPackage")
1,345✔
1079

1080
  const treeHashCache: TreeHashCache = []
1,345✔
1081

1082
  throwIfDefined(
1,345✔
1083
    await validateRatchetTree(
1084
      tree,
1085
      gi.groupContext,
1086
      clientConfig.lifetimeConfig,
1087
      authService,
1088
      gi.groupContext.treeHash,
1089
      cs,
1090
      treeHashCache,
1091
    ),
1092
  )
1093

1094
  const newLeaf = findLeafIndex(tree, keyPackage.leafNode)
1,345✔
1095

1096
  if (newLeaf === undefined) throw new ValidationError("Could not find own leaf when processing welcome")
1,345✔
1097

1098
  const privateKeyPath: PrivateKeyPath = {
1,345✔
1099
    leafIndex: newLeaf,
1100
    privateKeys: { [leafToNodeIndex(newLeaf)]: privateKeys.hpkePrivateKey },
1101
  }
1102

1103
  const ancestorNodeIndex = firstCommonAncestor(tree, newLeaf, toLeafIndex(gi.signer))
1,345✔
1104

1105
  const updatedPkp =
1106
    groupSecrets.pathSecret === undefined
1,345✔
1107
      ? privateKeyPath
1108
      : mergePrivateKeyPaths(
1109
          await toPrivateKeyPath(
1110
            await pathToRoot(tree, ancestorNodeIndex, groupSecrets.pathSecret, cs.kdf),
1111
            newLeaf,
1112
            cs,
1113
          ),
1114
          privateKeyPath,
1115
        )
1116

1117
  const [keySchedule, encryptionSecret] = await deriveKeySchedule(
1,402✔
1118
    groupSecrets.joinerSecret,
1119
    pskSecret,
1120
    gi.groupContext,
1121
    cs.kdf,
1122
  )
1123

1124
  const confirmationTagVerified = await verifyGroupInfoConfirmationTag(gi, groupSecrets.joinerSecret, pskSecret, cs)
1,345✔
1125

1126
  if (!confirmationTagVerified) throw new CryptoVerificationError("Could not verify confirmation tag")
1,345✔
1127

1128
  const secretTree = createSecretTree(leafWidth(tree.length), encryptionSecret)
1,345✔
1129

1130
  zeroOutUint8Array(groupSecrets.joinerSecret)
1,345✔
1131

1132
  return {
1,345✔
1133
    state: {
1134
      groupContext: gi.groupContext,
1135
      ratchetTree: tree,
1136
      privatePath: updatedPkp,
1137
      signaturePrivateKey: privateKeys.signaturePrivateKey,
1138
      confirmationTag: gi.confirmationTag,
1139
      unappliedProposals: {},
1140
      keySchedule,
1141
      secretTree,
1142
      historicalReceiverData: new Map(),
1143
      groupActiveState: { kind: "active" },
1144
      treeHashCache,
1145
    },
1146
    groupInfoExtensions: gi.extensions,
1147
  }
1148
}
1149

1150
/** @public */
1151
export async function joinGroupWithExtensions(params: {
1152
  context: MlsContext
1153
  welcome: Welcome
1154
  keyPackage: KeyPackage
1155
  privateKeys: PrivateKeyPackage
1156
  ratchetTree?: RatchetTree
1157
}): Promise<JoinGroupResult> {
1158
  return joinGroupInternal(params)
19✔
1159
}
1160

1161
/** @public */
1162
export interface CreateGroupParams {
1163
  context: MlsContext
1164
  groupId: Uint8Array
1165
  keyPackage: KeyPackage
1166
  privateKeyPackage: PrivateKeyPackage
1167
  extensions?: GroupContextExtension[]
1168
}
1169

1170
/** @public */
1171
export async function createGroup(params: CreateGroupParams): Promise<ClientState> {
1172
  const { context, groupId, keyPackage, privateKeyPackage } = params
1,123✔
1173
  const extensions = params.extensions ?? []
1,123✔
1174
  const authService = context.authService
1,123✔
1175
  const cs = context.cipherSuite
1,123✔
1176
  const ratchetTree: RatchetTree = [{ nodeType: nodeTypes.leaf, leaf: keyPackage.leafNode }]
1,123✔
1177

1178
  const privatePath: PrivateKeyPath = {
1,123✔
1179
    leafIndex: 0,
1180
    privateKeys: { [0]: privateKeyPackage.hpkePrivateKey },
1181
  }
1182

1183
  const confirmedTranscriptHash = new Uint8Array()
1,123✔
1184

1185
  const groupContext: GroupContext = {
1,123✔
1186
    version: protocolVersions.mls10,
1187
    cipherSuite: cs.id,
1188
    epoch: 0n,
1189
    treeHash: await treeHashRoot(ratchetTree, cs.hash),
1190
    groupId,
1191
    extensions,
1192
    confirmedTranscriptHash,
1193
  }
1194

1195
  throwIfDefined(await validateExternalSenders(extensions, authService))
1,123✔
1196

1197
  const epochSecret = cs.rng.randomBytes(cs.kdf.size)
1,123✔
1198

1199
  const keySchedule = await initializeKeySchedule(epochSecret, cs.kdf)
1,123✔
1200

1201
  const confirmationTag = await createConfirmationTag(keySchedule.confirmationKey, confirmedTranscriptHash, cs.hash)
1,123✔
1202

1203
  const encryptionSecret = await deriveSecret(epochSecret, "encryption", cs.kdf)
1,123✔
1204

1205
  const secretTree = createSecretTree(1, encryptionSecret)
1,123✔
1206

1207
  zeroOutUint8Array(epochSecret)
1,123✔
1208

1209
  return {
1,123✔
1210
    ratchetTree,
1211
    keySchedule,
1212
    secretTree,
1213
    privatePath,
1214
    signaturePrivateKey: privateKeyPackage.signaturePrivateKey,
1215
    unappliedProposals: {},
1216
    historicalReceiverData: new Map(),
1217
    groupContext,
1218
    confirmationTag,
1219
    groupActiveState: { kind: "active" },
1220
    treeHashCache: [],
1221
  }
1222
}
1223

1224
export async function exportSecret(
1225
  publicKey: Uint8Array,
1226
  cs: CiphersuiteImpl,
1227
): Promise<{ enc: Uint8Array; secret: Uint8Array }> {
1228
  return cs.hpke.exportSecret(
171✔
1229
    await cs.hpke.importPublicKey(publicKey),
1230
    new TextEncoder().encode("MLS 1.0 external init secret"),
1231
    cs.kdf.size,
1232
    new Uint8Array(),
1233
  )
1234
}
1235

1236
async function importSecret(privateKey: Uint8Array, kemOutput: Uint8Array, cs: CiphersuiteImpl): Promise<Uint8Array> {
1237
  return cs.hpke.importSecret(
76✔
1238
    await cs.hpke.importPrivateKey(privateKey),
1239
    new TextEncoder().encode("MLS 1.0 external init secret"),
1240
    kemOutput,
1241
    cs.kdf.size,
1242
    new Uint8Array(),
1243
  )
1244
}
1245

1246
async function applyTreeMutations(
1247
  mutableTree: RatchetTree,
1248
  grouped: Proposals,
1249
  gc: GroupContext,
1250
  sentByClient: boolean,
1251
  authService: AuthenticationService,
1252
  lifetimeConfig: LifetimeConfig,
1253
  s: Signature,
1254
): Promise<[LeafIndex, KeyPackage][]> {
1255
  for (const { senderLeafIndex, proposal } of grouped[defaultProposalTypes.update]) {
5,749✔
1256
    if (senderLeafIndex === undefined) throw new InternalError("No sender index found for update proposal")
128✔
1257

1258
    throwIfDefined(await validateLeafNodeUpdateOrCommit(proposal.update.leafNode, senderLeafIndex, gc, authService, s))
128✔
1259
    throwIfDefined(
128✔
1260
      await validateLeafNodeCredentialAndKeyUniqueness(mutableTree, proposal.update.leafNode, senderLeafIndex),
1261
    )
1262

1263
    updateLeafNodeMutable(mutableTree, proposal.update.leafNode, toLeafIndex(senderLeafIndex))
128✔
1264
  }
1265

1266
  grouped[defaultProposalTypes.remove].forEach(({ proposal }) => {
5,749✔
1267
    throwIfDefined(validateRemove(proposal.remove, mutableTree))
2,425✔
1268

1269
    removeLeafNodeMutable(mutableTree, toLeafIndex(proposal.remove.removed))
2,425✔
1270
  })
1271

1272
  const addedLNs = new Array<[LeafIndex, KeyPackage]>(grouped[defaultProposalTypes.add].length)
5,749✔
1273

1274
  for (const [index, { proposal }] of grouped[defaultProposalTypes.add].entries()) {
5,749✔
1275
    throwIfDefined(
3,476✔
1276
      await validateKeyPackage(proposal.add.keyPackage, gc, mutableTree, sentByClient, lifetimeConfig, authService, s),
1277
    )
1278

1279
    const leafNodeIndex = addLeafNodeMutable(mutableTree, proposal.add.keyPackage.leafNode)
3,476✔
1280
    addedLNs[index] = [nodeToLeafIndex(leafNodeIndex), proposal.add.keyPackage]
3,476✔
1281
  }
1282

1283
  return addedLNs
5,670✔
1284
}
1285

1286
export async function processProposal(
1287
  state: ClientState,
1288
  content: AuthenticatedContent,
1289
  proposal: Proposal,
1290
  h: Hash,
1291
): Promise<ClientState> {
1292
  const ref = await makeProposalRef(content, h)
2,006✔
1293
  return {
2,006✔
1294
    ...state,
1295
    unappliedProposals: addUnappliedProposal(
1296
      ref,
1297
      state.unappliedProposals,
1298
      proposal,
1299
      getSenderLeafNodeIndex(content.content.sender),
1300
    ),
1301
  }
1302
}
1303

1304
export function addHistoricalReceiverData(
1305
  state: ClientState,
1306
  clientConfig: ClientConfig,
1307
): [Map<bigint, EpochReceiverData>, Uint8Array[]] {
1308
  const withNew = addToMap(state.historicalReceiverData, state.groupContext.epoch, {
5,577✔
1309
    secretTree: state.secretTree,
1310
    ratchetTree: state.ratchetTree,
1311
    senderDataSecret: state.keySchedule.senderDataSecret,
1312
    groupContext: state.groupContext,
1313
    resumptionPsk: state.keySchedule.resumptionPsk,
1314
  })
1315

1316
  const epochs = [...withNew.keys()]
5,577✔
1317

1318
  const result: [Map<bigint, EpochReceiverData>, Uint8Array[]] =
1319
    epochs.length >= clientConfig.keyRetentionConfig.retainKeysForEpochs
5,577✔
1320
      ? removeOldHistoricalReceiverData(withNew, clientConfig.keyRetentionConfig.retainKeysForEpochs)
1321
      : [withNew, []]
1322

1323
  return result
5,577✔
1324
}
1325

1326
function removeOldHistoricalReceiverData(
1327
  historicalReceiverData: Map<bigint, EpochReceiverData>,
1328
  max: number,
1329
): [Map<bigint, EpochReceiverData>, Uint8Array[]] {
1330
  const sortedEpochs = [...historicalReceiverData.keys()].sort((a, b) => (a < b ? -1 : 1))
10,230✔
1331

1332
  const cutoff = sortedEpochs.length - max
2,667✔
1333

1334
  const toBeDeleted: Uint8Array[] = []
2,667✔
1335
  for (let n = 0; n < cutoff; n++) {
2,667✔
1336
    appendSecretTreeValues(historicalReceiverData.get(sortedEpochs[n]!)!.secretTree, toBeDeleted)
2,419✔
1337
  }
1338

1339
  const map = new Map(sortedEpochs.slice(-max).map((epoch) => [epoch, historicalReceiverData.get(epoch)!]))
10,478✔
1340

1341
  return [map, toBeDeleted]
2,667✔
1342
}
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