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

LukaJCB / ts-mls / 20770960568

07 Jan 2026 04:49AM UTC coverage: 95.703% (-0.5%) from 96.185%
20770960568

Pull #194

github

web-flow
Merge 34df8e333 into 529822824
Pull Request #194: Return consumed values to be deleted later

1260 of 1407 branches covered (89.55%)

Branch coverage included in aggregate %.

83 of 130 new or added lines in 7 files covered. (63.85%)

1 existing line in 1 file now uncovered.

7270 of 7506 relevant lines covered (96.86%)

41898.69 hits per line

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

93.36
/src/clientState.ts
1
import { AuthenticatedContent, makeProposalRef } from "./authenticatedContent.js"
1✔
2
import { CiphersuiteImpl } from "./crypto/ciphersuite.js"
3
import { Hash } from "./crypto/hash.js"
4
import { Extension, extensionsEqual, extensionsSupportedByCapabilities } from "./extension.js"
1✔
5
import { createConfirmationTag, FramedContentCommit } from "./framedContent.js"
1✔
6
import { decodeGroupContext, GroupContext, groupContextEncoder } from "./groupContext.js"
1✔
7
import { ratchetTreeFromExtension, verifyGroupInfoConfirmationTag, verifyGroupInfoSignature } from "./groupInfo.js"
1✔
8
import { KeyPackage, makeKeyPackageRef, PrivateKeyPackage, verifyKeyPackage } from "./keyPackage.js"
1✔
9
import {
1✔
10
  decodeKeySchedule,
11
  deriveKeySchedule,
12
  initializeKeySchedule,
13
  KeySchedule,
14
  keyScheduleEncoder,
15
} from "./keySchedule.js"
16
import { pskIdEncoder, PreSharedKeyID } from "./presharedkey.js"
1✔
17

18
import {
1✔
19
  addLeafNode,
20
  decodeRatchetTree,
21
  findBlankLeafNodeIndexOrExtend,
22
  findLeafIndex,
23
  ratchetTreeEncoder,
24
  removeLeafNode,
25
  updateLeafNode,
26
} from "./ratchetTree.js"
27
import { RatchetTree } from "./ratchetTree.js"
28
import { createSecretTree, decodeSecretTree, SecretTree, secretTreeEncoder } from "./secretTree.js"
1✔
29
import { createConfirmedHash, createInterimHash } from "./transcriptHash.js"
1✔
30
import { treeHashRoot } from "./treeHash.js"
1✔
31
import {
1✔
32
  directPath,
33
  isLeaf,
34
  LeafIndex,
35
  leafToNodeIndex,
36
  leafWidth,
37
  nodeToLeafIndex,
38
  toLeafIndex,
39
  toNodeIndex,
40
} from "./treemath.js"
41
import { firstCommonAncestor } from "./updatePath.js"
1✔
42
import { bytesToBase64 } from "./util/byteArray.js"
1✔
43
import { constantTimeEqual } from "./util/constantTimeCompare.js"
1✔
44
import { decryptGroupInfo, decryptGroupSecrets, Welcome } from "./welcome.js"
1✔
45
import { WireformatName } from "./wireformat.js"
46
import { ProposalOrRef } from "./proposalOrRefType.js"
47
import {
48
  Proposal,
49
  ProposalAdd,
50
  ProposalExternalInit,
51
  ProposalGroupContextExtensions,
52
  ProposalPSK,
53
  ProposalReinit,
54
  ProposalRemove,
55
  ProposalUpdate,
56
  Reinit,
57
  Remove,
58
} from "./proposal.js"
59
import { pathToRoot } from "./pathSecrets.js"
1✔
60
import {
1✔
61
  PrivateKeyPath,
62
  decodePrivateKeyPath,
63
  mergePrivateKeyPaths,
64
  privateKeyPathEncoder,
65
  toPrivateKeyPath,
66
} from "./privateKeyPath.js"
67
import {
1✔
68
  UnappliedProposals,
69
  addUnappliedProposal,
70
  ProposalWithSender,
71
  unappliedProposalsEncoder,
72
  decodeUnappliedProposals,
73
} from "./unappliedProposals.js"
74
import { accumulatePskSecret, PskIndex } from "./pskIndex.js"
1✔
75
import { getSenderLeafNodeIndex } from "./sender.js"
1✔
76
import { addToMap } from "./util/addToMap.js"
1✔
77
import {
1✔
78
  CryptoVerificationError,
79
  CodecError,
80
  InternalError,
81
  UsageError,
82
  ValidationError,
83
  MlsError,
84
} from "./mlsError.js"
85
import { Signature } from "./crypto/signature.js"
86
import {
1✔
87
  LeafNode,
88
  LeafNodeCommit,
89
  LeafNodeKeyPackage,
90
  LeafNodeUpdate,
91
  verifyLeafNodeSignature,
92
  verifyLeafNodeSignatureKeyPackage,
93
} from "./leafNode.js"
94
import { protocolVersions } from "./protocolVersion.js"
1✔
95
import { decodeRequiredCapabilities, RequiredCapabilities } from "./requiredCapabilities.js"
1✔
96
import { Capabilities } from "./capabilities.js"
97
import { verifyParentHashes } from "./parentHash.js"
1✔
98
import { AuthenticationService } from "./authenticationService.js"
99
import { LifetimeConfig } from "./lifetimeConfig.js"
100
import { KeyPackageEqualityConfig } from "./keyPackageEqualityConfig.js"
101
import { ClientConfig, defaultClientConfig } from "./clientConfig.js"
1✔
102
import { decodeExternalSender } from "./externalSender.js"
1✔
103
import { arraysEqual } from "./util/array.js"
1✔
104
import { BufferEncoder, contramapBufferEncoders, encode, Encoder } from "./codec/tlsEncoder.js"
1✔
105
import { CredentialTypeName } from "./credentialType.js"
106
import { bigintMapEncoder, decodeBigintMap, decodeVarLenData, varLenDataEncoder } from "./codec/variableLength.js"
1✔
107
import { decodeGroupActiveState, GroupActiveState, groupActiveStateEncoder } from "./groupActiveState.js"
1✔
108
import { decodeEpochReceiverData, EpochReceiverData, epochReceiverDataEncoder } from "./epochReceiverData.js"
1✔
109
import { Decoder, mapDecoders } from "./codec/tlsDecoder.js"
1✔
110
import { deriveSecret } from "./crypto/kdf.js"
1✔
111

112
export type ClientState = GroupState & { clientConfig: ClientConfig }
113

114
export interface GroupState {
115
  groupContext: GroupContext
116
  keySchedule: KeySchedule
117
  secretTree: SecretTree
118
  ratchetTree: RatchetTree
119
  privatePath: PrivateKeyPath
120
  signaturePrivateKey: Uint8Array
121
  unappliedProposals: UnappliedProposals
122
  confirmationTag: Uint8Array
123
  historicalReceiverData: Map<bigint, EpochReceiverData>
124
  groupActiveState: GroupActiveState
125
}
126

127
export const groupStateEncoder: BufferEncoder<GroupState> = contramapBufferEncoders(
1✔
128
  [
1✔
129
    groupContextEncoder,
1✔
130
    keyScheduleEncoder,
1✔
131
    secretTreeEncoder,
1✔
132
    ratchetTreeEncoder,
1✔
133
    privateKeyPathEncoder,
1✔
134
    varLenDataEncoder,
1✔
135
    unappliedProposalsEncoder,
1✔
136
    varLenDataEncoder,
1✔
137
    bigintMapEncoder(epochReceiverDataEncoder),
1✔
138
    groupActiveStateEncoder,
1✔
139
  ],
1✔
140
  (state) =>
1✔
141
    [
1✔
142
      state.groupContext,
1✔
143
      state.keySchedule,
1✔
144
      state.secretTree,
1✔
145
      state.ratchetTree,
1✔
146
      state.privatePath,
1✔
147
      state.signaturePrivateKey,
1✔
148
      state.unappliedProposals,
1✔
149
      state.confirmationTag,
1✔
150
      state.historicalReceiverData,
1✔
151
      state.groupActiveState,
1✔
152
    ] as const,
1✔
153
)
1✔
154

155
export const encodeGroupState: Encoder<GroupState> = encode(groupStateEncoder)
1✔
156

157
export const decodeGroupState: Decoder<GroupState> = mapDecoders(
1✔
158
  [
1✔
159
    decodeGroupContext,
1✔
160
    decodeKeySchedule,
1✔
161
    decodeSecretTree,
1✔
162
    decodeRatchetTree,
1✔
163
    decodePrivateKeyPath,
1✔
164
    decodeVarLenData,
1✔
165
    decodeUnappliedProposals,
1✔
166
    decodeVarLenData,
1✔
167
    decodeBigintMap(decodeEpochReceiverData),
1✔
168
    decodeGroupActiveState,
1✔
169
  ],
1✔
170
  (
1✔
171
    groupContext,
1✔
172
    keySchedule,
1✔
173
    secretTree,
1✔
174
    ratchetTree,
1✔
175
    privatePath,
1✔
176
    signaturePrivateKey,
1✔
177
    unappliedProposals,
1✔
178
    confirmationTag,
1✔
179
    historicalReceiverData,
1✔
180
    groupActiveState,
1✔
181
  ) => ({
1✔
182
    groupContext,
1✔
183
    keySchedule,
1✔
184
    secretTree,
1✔
185
    ratchetTree,
1✔
186
    privatePath,
1✔
187
    signaturePrivateKey,
1✔
188
    unappliedProposals,
1✔
189
    confirmationTag,
1✔
190
    historicalReceiverData,
1✔
191
    groupActiveState,
1✔
192
  }),
1✔
193
)
1✔
194

195
export const groupStateEncoderWithoutTree: BufferEncoder<GroupState> = contramapBufferEncoders(
1✔
196
  [
1✔
197
    groupContextEncoder,
1✔
198
    keyScheduleEncoder,
1✔
199
    secretTreeEncoder,
1✔
200
    privateKeyPathEncoder,
1✔
201
    varLenDataEncoder,
1✔
202
    unappliedProposalsEncoder,
1✔
203
    varLenDataEncoder,
1✔
204
    bigintMapEncoder(epochReceiverDataEncoder),
1✔
205
    groupActiveStateEncoder,
1✔
206
  ],
1✔
207
  (state) =>
1✔
NEW
208
    [
×
NEW
209
      state.groupContext,
×
NEW
210
      state.keySchedule,
×
NEW
211
      state.secretTree,
×
NEW
212
      state.privatePath,
×
NEW
213
      state.signaturePrivateKey,
×
NEW
214
      state.unappliedProposals,
×
NEW
215
      state.confirmationTag,
×
NEW
216
      state.historicalReceiverData,
×
NEW
217
      state.groupActiveState,
×
NEW
218
    ] as const,
×
219
)
1✔
220

221
export const encodeGroupStateWithoutTree: Encoder<GroupState> = encode(groupStateEncoderWithoutTree)
1✔
222

223
export function decodeGroupStateWithoutTree(ratchetTree: RatchetTree): Decoder<GroupState> {
1✔
NEW
224
  return mapDecoders(
×
NEW
225
    [
×
NEW
226
      decodeGroupContext,
×
NEW
227
      decodeKeySchedule,
×
NEW
228
      decodeSecretTree,
×
NEW
229
      decodePrivateKeyPath,
×
NEW
230
      decodeVarLenData,
×
NEW
231
      decodeUnappliedProposals,
×
NEW
232
      decodeVarLenData,
×
NEW
233
      decodeBigintMap(decodeEpochReceiverData),
×
NEW
234
      decodeGroupActiveState,
×
NEW
235
    ],
×
NEW
236
    (
×
NEW
237
      groupContext,
×
NEW
238
      keySchedule,
×
NEW
239
      secretTree,
×
NEW
240
      privatePath,
×
NEW
241
      signaturePrivateKey,
×
NEW
242
      unappliedProposals,
×
NEW
243
      confirmationTag,
×
NEW
244
      historicalReceiverData,
×
NEW
245
      groupActiveState,
×
NEW
246
    ) => ({
×
NEW
247
      groupContext,
×
NEW
248
      keySchedule,
×
NEW
249
      secretTree,
×
NEW
250
      ratchetTree,
×
NEW
251
      privatePath,
×
NEW
252
      signaturePrivateKey,
×
NEW
253
      unappliedProposals,
×
NEW
254
      confirmationTag,
×
NEW
255
      historicalReceiverData,
×
NEW
256
      groupActiveState,
×
NEW
257
    }),
×
NEW
258
  )
×
NEW
259
}
×
260

261
export function getOwnLeafNode(state: ClientState): LeafNode {
1✔
262
  const idx = leafToNodeIndex(toLeafIndex(state.privatePath.leafIndex))
3✔
263
  const leaf = state.ratchetTree[idx]
3✔
264
  if (leaf?.nodeType !== "leaf") throw new InternalError("Expected leaf node")
3!
265
  return leaf.leaf
3✔
266
}
3✔
267

268
export function getGroupMembers(state: ClientState): LeafNode[] {
1✔
269
  return extractFromGroupMembers(
1✔
270
    state,
1✔
271
    () => false,
1✔
272
    (l) => l,
1✔
273
  )
1✔
274
}
1✔
275

276
export function extractFromGroupMembers<T>(
1✔
277
  state: ClientState,
3✔
278
  exclude: (l: LeafNode) => boolean,
3✔
279
  map: (l: LeafNode) => T,
3✔
280
): T[] {
3✔
281
  const recipients = []
3✔
282
  for (const node of state.ratchetTree) {
3✔
283
    if (node?.nodeType === "leaf" && !exclude(node.leaf)) {
21✔
284
      recipients.push(map(node.leaf))
8✔
285
    }
8✔
286
  }
21✔
287
  return recipients
3✔
288
}
3✔
289

290
export function checkCanSendApplicationMessages(state: ClientState): void {
1✔
291
  if (Object.keys(state.unappliedProposals).length !== 0)
2,470✔
292
    throw new UsageError("Cannot send application message with unapplied proposals")
2,470✔
293

294
  checkCanSendHandshakeMessages(state)
2,451✔
295
}
2,451✔
296

297
export function checkCanSendHandshakeMessages(state: ClientState): void {
1✔
298
  if (state.groupActiveState.kind === "suspendedPendingReinit")
4,429✔
299
    throw new UsageError("Cannot send messages while Group is suspended pending reinit")
4,429✔
300
  else if (state.groupActiveState.kind === "removedFromGroup")
4,410✔
301
    throw new UsageError("Cannot send messages after being removed from group")
4,410✔
302
}
4,429✔
303

304
export interface Proposals {
305
  add: { senderLeafIndex: number | undefined; proposal: ProposalAdd }[]
306
  update: { senderLeafIndex: number | undefined; proposal: ProposalUpdate }[]
307
  remove: { senderLeafIndex: number | undefined; proposal: ProposalRemove }[]
308
  psk: { senderLeafIndex: number | undefined; proposal: ProposalPSK }[]
309
  reinit: { senderLeafIndex: number | undefined; proposal: ProposalReinit }[]
310
  external_init: { senderLeafIndex: number | undefined; proposal: ProposalExternalInit }[]
311
  group_context_extensions: { senderLeafIndex: number | undefined; proposal: ProposalGroupContextExtensions }[]
312
}
313

314
const emptyProposals: Proposals = {
1✔
315
  add: [],
1✔
316
  update: [],
1✔
317
  remove: [],
1✔
318
  psk: [],
1✔
319
  reinit: [],
1✔
320
  external_init: [],
1✔
321
  group_context_extensions: [],
1✔
322
}
1✔
323

324
function flattenExtensions(groupContextExtensions: { proposal: ProposalGroupContextExtensions }[]): Extension[] {
8,449✔
325
  return groupContextExtensions.reduce((acc, { proposal }) => {
8,449✔
326
    return [...acc, ...proposal.groupContextExtensions.extensions]
61✔
327
  }, [] as Extension[])
8,449✔
328
}
8,449✔
329

330
async function validateProposals(
5,155✔
331
  p: Proposals,
5,155✔
332
  committerLeafIndex: number | undefined,
5,155✔
333
  groupContext: GroupContext,
5,155✔
334
  config: KeyPackageEqualityConfig,
5,155✔
335
  authService: AuthenticationService,
5,155✔
336
  tree: RatchetTree,
5,155✔
337
): Promise<MlsError | undefined> {
5,155✔
338
  const containsUpdateByCommitter = p.update.some(
5,155✔
339
    (o) => o.senderLeafIndex !== undefined && o.senderLeafIndex === committerLeafIndex,
5,155✔
340
  )
5,155✔
341

342
  if (containsUpdateByCommitter)
5,155✔
343
    return new ValidationError("Commit cannot contain an update proposal sent by committer")
5,155✔
344

345
  const containsRemoveOfCommitter = p.remove.some((o) => o.proposal.remove.removed === committerLeafIndex)
5,154✔
346

347
  if (containsRemoveOfCommitter)
5,154✔
348
    return new ValidationError("Commit cannot contain a remove proposal removing committer")
5,155✔
349

350
  const multipleUpdateRemoveForSameLeaf =
5,153✔
351
    p.update.some(
5,153✔
352
      ({ senderLeafIndex: a }, indexA) =>
5,153✔
353
        p.update.some(({ senderLeafIndex: b }, indexB) => a === b && indexA !== indexB) ||
14✔
354
        p.remove.some((r) => r.proposal.remove.removed === a),
14✔
355
    ) ||
5,153✔
356
    p.remove.some(
5,153✔
357
      (a, indexA) =>
5,153✔
358
        p.remove.some((b, indexB) => b.proposal.remove.removed === a.proposal.remove.removed && indexA !== indexB) ||
2,426✔
359
        p.update.some(({ senderLeafIndex }) => a.proposal.remove.removed === senderLeafIndex),
2,425✔
360
    )
5,153✔
361

362
  if (multipleUpdateRemoveForSameLeaf)
5,155✔
363
    return new ValidationError(
5,155✔
364
      "Commit cannot contain multiple update and/or remove proposals that apply to the same leaf",
1✔
365
    )
1✔
366

367
  const multipleAddsContainSameKeypackage = p.add.some(({ proposal: a }, indexA) =>
5,152✔
368
    p.add.some(
3,159✔
369
      ({ proposal: b }, indexB) => config.compareKeyPackages(a.add.keyPackage, b.add.keyPackage) && indexA !== indexB,
3,159✔
370
    ),
3,159✔
371
  )
5,152✔
372

373
  if (multipleAddsContainSameKeypackage)
5,152✔
374
    return new ValidationError(
5,155✔
375
      "Commit cannot contain multiple Add proposals that contain KeyPackages that represent the same client",
1✔
376
    )
1✔
377

378
  // checks if there is an Add proposal with a KeyPackage that matches a client already in the group
379
  // unless there is a Remove proposal in the list removing the matching client from the group.
380
  const addsContainExistingKeypackage = p.add.some(({ proposal }) =>
5,151✔
381
    tree.some(
3,158✔
382
      (node, nodeIndex) =>
3,158✔
383
        node !== undefined &&
186,496✔
384
        node.nodeType === "leaf" &&
41,524✔
385
        config.compareKeyPackageToLeafNode(proposal.add.keyPackage, node.leaf) &&
32,450✔
386
        p.remove.every((r) => r.proposal.remove.removed !== nodeToLeafIndex(toNodeIndex(nodeIndex))),
1✔
387
    ),
3,158✔
388
  )
5,151✔
389

390
  if (addsContainExistingKeypackage)
5,151✔
391
    return new ValidationError("Commit cannot contain an Add proposal for someone already in the group")
5,155✔
392

393
  const everyLeafSupportsGroupExtensions = p.add.every(({ proposal }) =>
5,150✔
394
    extensionsSupportedByCapabilities(groupContext.extensions, proposal.add.keyPackage.leafNode.capabilities),
3,157✔
395
  )
5,150✔
396

397
  if (!everyLeafSupportsGroupExtensions)
5,150✔
398
    return new ValidationError("Added leaf node that doesn't support extension in GroupContext")
5,155✔
399

400
  const multiplePskWithSamePskId = p.psk.some((a, indexA) =>
5,131✔
401
    p.psk.some(
209✔
402
      (b, indexB) =>
209✔
403
        constantTimeEqual(
314✔
404
          encode(pskIdEncoder)(a.proposal.psk.preSharedKeyId),
314✔
405
          encode(pskIdEncoder)(b.proposal.psk.preSharedKeyId),
314✔
406
        ) && indexA !== indexB,
314✔
407
    ),
209✔
408
  )
5,131✔
409

410
  if (multiplePskWithSamePskId)
5,131✔
411
    return new ValidationError("Commit cannot contain PreSharedKey proposals that reference the same PreSharedKeyID")
5,155✔
412

413
  const multipleGroupContextExtensions = p.group_context_extensions.length > 1
5,130✔
414

415
  if (multipleGroupContextExtensions)
5,130✔
416
    return new ValidationError("Commit cannot contain multiple GroupContextExtensions proposals")
5,155✔
417

418
  const allExtensions = flattenExtensions(p.group_context_extensions)
5,129✔
419

420
  const requiredCapabilities = allExtensions.find((e) => e.extensionType === "required_capabilities")
5,129✔
421

422
  if (requiredCapabilities !== undefined) {
5,155✔
423
    const caps = decodeRequiredCapabilities(requiredCapabilities.extensionData, 0)
3✔
424
    if (caps === undefined) return new CodecError("Could not decode required_capabilities")
3✔
425

426
    const everyLeafSupportsCapabilities = tree
2✔
427
      .filter((n) => n !== undefined && n.nodeType === "leaf")
2✔
428
      .every((l) => capabiltiesAreSupported(caps[0], l.leaf.capabilities))
2✔
429

430
    if (!everyLeafSupportsCapabilities) return new ValidationError("Not all members support required capabilities")
3✔
431

432
    const allAdditionsSupportCapabilities = p.add.every((a) =>
1✔
433
      capabiltiesAreSupported(caps[0], a.proposal.add.keyPackage.leafNode.capabilities),
1✔
434
    )
1✔
435

436
    if (!allAdditionsSupportCapabilities)
1✔
437
      return new ValidationError("Commit contains add proposals of member without required capabilities")
1✔
438
  }
3✔
439

440
  return await validateExternalSenders(allExtensions, authService)
5,126✔
441
}
5,126✔
442

443
async function validateExternalSenders(
6,023✔
444
  extensions: Extension[],
6,023✔
445
  authService: AuthenticationService,
6,023✔
446
): Promise<MlsError | undefined> {
6,023✔
447
  const externalSenders = extensions.filter((e) => e.extensionType === "external_senders")
6,023✔
448
  for (const externalSender of externalSenders) {
6,023✔
449
    const decoded = decodeExternalSender(externalSender.extensionData, 0)
21✔
450
    if (decoded === undefined) return new CodecError("Could not decode external_senders")
21✔
451

452
    const validCredential = await authService.validateCredential(decoded[0].credential, decoded[0].signaturePublicKey)
20✔
453
    if (!validCredential) return new ValidationError("Could not validate external credential")
20✔
454
  }
21✔
455
}
6,021✔
456

457
function capabiltiesAreSupported(caps: RequiredCapabilities, cs: Capabilities): boolean {
81✔
458
  return (
81✔
459
    caps.credentialTypes.every((c) => cs.credentials.includes(c)) &&
81✔
460
    caps.extensionTypes.every((e) => cs.extensions.includes(e)) &&
61✔
461
    caps.proposalTypes.every((p) => cs.proposals.includes(p))
61✔
462
  )
463
}
81✔
464

465
export async function validateRatchetTree(
1,271✔
466
  tree: RatchetTree,
1,271✔
467
  groupContext: GroupContext,
1,271✔
468
  config: LifetimeConfig,
1,271✔
469
  authService: AuthenticationService,
1,271✔
470
  treeHash: Uint8Array,
1,271✔
471
  cs: CiphersuiteImpl,
1,271✔
472
): Promise<MlsError | undefined> {
1,271✔
473
  const hpkeKeys = new Set<string>()
1,271✔
474
  const signatureKeys = new Set<string>()
1,271✔
475
  const credentialTypes = new Set<CredentialTypeName>()
1,271✔
476
  for (const [i, n] of tree.entries()) {
1,271✔
477
    const nodeIndex = toNodeIndex(i)
8,701✔
478
    if (n?.nodeType === "leaf") {
8,701✔
479
      if (!isLeaf(nodeIndex)) return new ValidationError("Received Ratchet Tree is not structurally sound")
4,508!
480

481
      const hpkeKey = bytesToBase64(n.leaf.hpkePublicKey)
4,508✔
482
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
4,508✔
483
      else hpkeKeys.add(hpkeKey)
4,489✔
484

485
      const signatureKey = bytesToBase64(n.leaf.signaturePublicKey)
4,489✔
486
      if (signatureKeys.has(signatureKey)) return new ValidationError("signature keys not unique")
4,508✔
487
      else signatureKeys.add(signatureKey)
4,470✔
488

489
      credentialTypes.add(n.leaf.credential.credentialType)
4,470✔
490

491
      const err =
4,470✔
492
        n.leaf.leafNodeSource === "key_package"
4,470✔
493
          ? await validateLeafNodeKeyPackage(n.leaf, groupContext, false, config, authService, cs.signature)
4,034✔
494
          : await validateLeafNodeUpdateOrCommit(
436✔
495
              n.leaf,
436✔
496
              nodeToLeafIndex(nodeIndex),
436✔
497
              groupContext,
436✔
498
              authService,
436✔
499
              cs.signature,
436✔
500
            )
436✔
501

502
      if (err !== undefined) return err
2,510✔
503
    } else if (n?.nodeType === "parent") {
8,701✔
504
      if (isLeaf(nodeIndex)) return new ValidationError("Received Ratchet Tree is not structurally sound")
528✔
505

506
      const hpkeKey = bytesToBase64(n.parent.hpkePublicKey)
509✔
507
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
528✔
508
      else hpkeKeys.add(hpkeKey)
509✔
509

510
      for (const unmergedLeaf of n.parent.unmergedLeaves) {
528✔
511
        const leafIndex = toLeafIndex(unmergedLeaf)
114✔
512
        const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length))
114✔
513
        const nodeIndex = leafToNodeIndex(leafIndex)
114✔
514
        if (tree[nodeIndex]?.nodeType !== "leaf" && !dp.includes(toNodeIndex(i)))
114!
515
          return new ValidationError("Unmerged leaf did not represent a non-blank descendant leaf node")
114!
516

517
        for (const parentIdx of dp) {
114✔
518
          const dpNode = tree[parentIdx]
342✔
519

520
          if (dpNode !== undefined) {
342✔
521
            if (dpNode.nodeType !== "parent") return new InternalError("Expected parent node")
114!
522

523
            if (!arraysEqual(dpNode.parent.unmergedLeaves, n.parent.unmergedLeaves))
114✔
524
              return new ValidationError("non-blank intermediate node must list leaf node in its unmerged_leaves")
114!
525
          }
114✔
526
        }
342✔
527
      }
114✔
528
    }
509✔
529
  }
8,701✔
530

531
  for (const n of tree) {
1,271✔
532
    if (n?.nodeType === "leaf") {
8,511✔
533
      for (const credentialType of credentialTypes) {
4,375✔
534
        if (!n.leaf.capabilities.credentials.includes(credentialType))
4,375✔
535
          return new ValidationError("LeafNode has credential that is not supported by member of the group")
4,375!
536
      }
4,375✔
537
    }
4,375✔
538
  }
8,511✔
539

540
  const parentHashesVerified = await verifyParentHashes(tree, cs.hash)
1,157✔
541

542
  if (!parentHashesVerified) return new CryptoVerificationError("Unable to verify parent hash")
1,271✔
543

544
  if (!constantTimeEqual(treeHash, await treeHashRoot(tree, cs.hash)))
1,138✔
545
    return new ValidationError("Unable to verify tree hash")
1,138✔
546
}
1,271✔
547

548
export async function validateLeafNodeUpdateOrCommit(
2,457✔
549
  leafNode: LeafNodeCommit | LeafNodeUpdate,
2,457✔
550
  leafIndex: number,
2,457✔
551
  groupContext: GroupContext,
2,457✔
552
  authService: AuthenticationService,
2,457✔
553
  s: Signature,
2,457✔
554
): Promise<MlsError | undefined> {
2,457✔
555
  const signatureValid = await verifyLeafNodeSignature(leafNode, groupContext.groupId, leafIndex, s)
2,457✔
556

557
  if (!signatureValid) return new CryptoVerificationError("Could not verify leaf node signature")
2,457✔
558

559
  const commonError = await validateLeafNodeCommon(leafNode, groupContext, authService)
2,438✔
560

561
  if (commonError !== undefined) return commonError
2,457✔
562
}
2,457✔
563

564
export function throwIfDefined(err: MlsError | undefined): void {
1✔
565
  if (err !== undefined) throw err
17,042✔
566
}
17,042✔
567

568
async function validateLeafNodeCommon(
9,551✔
569
  leafNode: LeafNode,
9,551✔
570
  groupContext: GroupContext,
9,551✔
571
  authService: AuthenticationService,
9,551✔
572
) {
9,551✔
573
  const credentialValid = await authService.validateCredential(leafNode.credential, leafNode.signaturePublicKey)
9,551✔
574

575
  if (!credentialValid) return new ValidationError("Could not validate credential")
9,551✔
576

577
  const requiredCapabilities = groupContext.extensions.find((e) => e.extensionType === "required_capabilities")
9,531✔
578

579
  if (requiredCapabilities !== undefined) {
9,551✔
580
    const caps = decodeRequiredCapabilities(requiredCapabilities.extensionData, 0)
76✔
581
    if (caps === undefined) return new CodecError("Could not decode required_capabilities")
76!
582

583
    const leafSupportsCapabilities = capabiltiesAreSupported(caps[0], leafNode.capabilities)
76✔
584

585
    if (!leafSupportsCapabilities) return new ValidationError("LeafNode does not support required capabilities")
76✔
586
  }
76✔
587

588
  const extensionsSupported = extensionsSupportedByCapabilities(leafNode.extensions, leafNode.capabilities)
9,512✔
589

590
  if (!extensionsSupported) return new ValidationError("LeafNode contains extension not listed in capabilities")
9,551✔
591
}
9,551✔
592

593
async function validateLeafNodeKeyPackage(
7,132✔
594
  leafNode: LeafNodeKeyPackage,
7,132✔
595
  groupContext: GroupContext,
7,132✔
596
  sentByClient: boolean,
7,132✔
597
  config: LifetimeConfig,
7,132✔
598
  authService: AuthenticationService,
7,132✔
599
  s: Signature,
7,132✔
600
): Promise<MlsError | undefined> {
7,132✔
601
  const signatureValid = await verifyLeafNodeSignatureKeyPackage(leafNode, s)
7,132✔
602
  if (!signatureValid) return new CryptoVerificationError("Could not verify leaf node signature")
7,132✔
603

604
  //verify lifetime
605
  if (sentByClient || config.validateLifetimeOnReceive) {
7,132✔
606
    if (leafNode.leafNodeSource === "key_package") {
1,088✔
607
      const currentTime = BigInt(Math.floor(Date.now() / 1000))
1,088✔
608
      if (leafNode.lifetime.notBefore > currentTime || leafNode.lifetime.notAfter < currentTime)
1,088✔
609
        return new ValidationError("Current time not within Lifetime")
1,088!
610
    }
1,088✔
611
  }
1,088✔
612

613
  const commonError = await validateLeafNodeCommon(leafNode, groupContext, authService)
7,113✔
614

615
  if (commonError !== undefined) return commonError
7,132✔
616
}
7,132✔
617

618
export async function validateLeafNodeCredentialAndKeyUniqueness(
5,120✔
619
  tree: RatchetTree,
5,120✔
620
  leafNode: LeafNode,
5,120✔
621
  existingLeafIndex?: number,
5,120✔
622
): Promise<ValidationError | undefined> {
5,120✔
623
  const hpkeKeys = new Set<string>()
5,120✔
624
  const signatureKeys = new Set<string>()
5,120✔
625
  for (const [nodeIndex, node] of tree.entries()) {
5,120✔
626
    if (node?.nodeType === "leaf") {
222,552✔
627
      if (!node.leaf.capabilities.credentials.includes(leafNode.credential.credentialType)) {
46,525✔
628
        return new ValidationError("LeafNode has credential that is not supported by member of the group")
1✔
629
      }
1✔
630

631
      const hpkeKey = bytesToBase64(node.leaf.hpkePublicKey)
46,524✔
632
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
46,525✔
633
      else hpkeKeys.add(hpkeKey)
46,524✔
634

635
      const signatureKey = bytesToBase64(node.leaf.signaturePublicKey)
46,524✔
636
      if (signatureKeys.has(signatureKey) && existingLeafIndex !== nodeToLeafIndex(toNodeIndex(nodeIndex)))
46,525!
637
        return new ValidationError("signature keys not unique")
46,525✔
638
      else signatureKeys.add(signatureKey)
46,524✔
639
    } else if (node?.nodeType === "parent") {
222,552✔
640
      const hpkeKey = bytesToBase64(node.parent.hpkePublicKey)
16,828✔
641
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
16,828!
642
      else hpkeKeys.add(hpkeKey)
16,828✔
643
    }
16,828✔
644
  }
222,552✔
645
}
5,119✔
646

647
async function validateKeyPackage(
3,137✔
648
  kp: KeyPackage,
3,137✔
649
  groupContext: GroupContext,
3,137✔
650
  tree: RatchetTree,
3,137✔
651
  sentByClient: boolean,
3,137✔
652
  config: LifetimeConfig,
3,137✔
653
  authService: AuthenticationService,
3,137✔
654
  s: Signature,
3,137✔
655
): Promise<MlsError | undefined> {
3,137✔
656
  if (kp.cipherSuite !== groupContext.cipherSuite) return new ValidationError("Invalid CipherSuite")
3,137✔
657

658
  if (kp.version !== groupContext.version) return new ValidationError("Invalid mls version")
3,137✔
659

660
  const leafNodeConsistentWithTree = await validateLeafNodeCredentialAndKeyUniqueness(tree, kp.leafNode)
3,099✔
661

662
  if (leafNodeConsistentWithTree !== undefined) return leafNodeConsistentWithTree
3,137✔
663

664
  const leafNodeError = await validateLeafNodeKeyPackage(
3,098✔
665
    kp.leafNode,
3,098✔
666
    groupContext,
3,098✔
667
    sentByClient,
3,098✔
668
    config,
3,098✔
669
    authService,
3,098✔
670
    s,
3,098✔
671
  )
3,098✔
672
  if (leafNodeError !== undefined) return leafNodeError
3,137✔
673

674
  const signatureValid = await verifyKeyPackage(kp, s)
3,077✔
675
  if (!signatureValid) return new CryptoVerificationError("Invalid keypackage signature")
3,137✔
676

677
  if (constantTimeEqual(kp.initKey, kp.leafNode.hpkePublicKey))
3,058✔
678
    return new ValidationError("Cannot have identicial init and encryption keys")
3,137!
679
}
3,137✔
680

681
function validateReinit(
77✔
682
  allProposals: ProposalWithSender[],
77✔
683
  reinit: Reinit,
77✔
684
  gc: GroupContext,
77✔
685
): ValidationError | undefined {
77✔
686
  if (allProposals.length !== 1) return new ValidationError("Reinit proposal needs to be commited by itself")
77!
687

688
  if (protocolVersions[reinit.version] < protocolVersions[gc.version])
77✔
689
    return new ValidationError("A ReInit proposal cannot use a version less than the version for the current group")
77!
690
}
77✔
691

692
function validateExternalInit(grouped: Proposals): ValidationError | undefined {
76✔
693
  if (grouped.external_init.length > 1)
76✔
694
    return new ValidationError("Cannot contain more than one external_init proposal")
76!
695

696
  if (grouped.remove.length > 1) return new ValidationError("Cannot contain more than one remove proposal")
76!
697

698
  if (
76✔
699
    grouped.add.length > 0 ||
76✔
700
    grouped.group_context_extensions.length > 0 ||
76✔
701
    grouped.reinit.length > 0 ||
76✔
702
    grouped.update.length > 0
76✔
703
  )
704
    return new ValidationError("Invalid proposals")
76!
705
}
76✔
706

707
function validateRemove(remove: Remove, tree: RatchetTree): MlsError | undefined {
2,425✔
708
  if (tree[leafToNodeIndex(toLeafIndex(remove.removed))] === undefined)
2,425✔
709
    return new ValidationError("Tried to remove empty leaf node")
2,425!
710
}
2,425✔
711

712
export interface ApplyProposalsResult {
713
  tree: RatchetTree
714
  pskSecret: Uint8Array
715
  pskIds: PreSharedKeyID[]
716
  needsUpdatePath: boolean
717
  additionalResult: ApplyProposalsData
718
  selfRemoved: boolean
719
  allProposals: ProposalWithSender[]
720
}
721

722
export type ApplyProposalsData =
723
  | { kind: "memberCommit"; addedLeafNodes: [LeafIndex, KeyPackage][]; extensions: Extension[] }
724
  | { kind: "externalCommit"; externalInitSecret: Uint8Array; newMemberLeafIndex: LeafIndex }
725
  | { kind: "reinit"; reinit: Reinit }
726

727
export async function applyProposals(
5,308✔
728
  state: ClientState,
5,308✔
729
  proposals: ProposalOrRef[],
5,308✔
730
  committerLeafIndex: LeafIndex | undefined,
5,308✔
731
  pskSearch: PskIndex,
5,308✔
732
  sentByClient: boolean,
5,308✔
733
  cs: CiphersuiteImpl,
5,308✔
734
): Promise<ApplyProposalsResult> {
5,308✔
735
  const allProposals = proposals.reduce((acc, cur) => {
5,308✔
736
    if (cur.proposalOrRefType === "proposal")
6,077✔
737
      return [...acc, { proposal: cur.proposal, senderLeafIndex: committerLeafIndex }]
6,077✔
738

739
    const p = state.unappliedProposals[bytesToBase64(cur.reference)]
1,854✔
740
    if (p === undefined) throw new ValidationError("Could not find proposal with supplied reference")
3,580✔
741
    return [...acc, p]
1,854✔
742
  }, [] as ProposalWithSender[])
5,308✔
743

744
  const grouped = allProposals.reduce((acc, cur) => {
5,308✔
745
    //this skips any custom proposals
746
    if (typeof cur.proposal.proposalType === "number") return acc
6,077✔
747
    const proposal = acc[cur.proposal.proposalType] ?? []
6,077!
748
    return { ...acc, [cur.proposal.proposalType]: [...proposal, cur] }
6,077✔
749
  }, emptyProposals)
5,308✔
750

751
  const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
5,308✔
752

753
  const isExternalInit = grouped.external_init.length > 0
5,308✔
754

755
  if (!isExternalInit) {
5,308✔
756
    if (grouped.reinit.length > 0) {
5,232✔
757
      const reinit = grouped.reinit.at(0)!.proposal.reinit
77✔
758

759
      throwIfDefined(validateReinit(allProposals, reinit, state.groupContext))
77✔
760

761
      return {
77✔
762
        tree: state.ratchetTree,
77✔
763
        pskSecret: zeroes,
77✔
764
        pskIds: [],
77✔
765
        needsUpdatePath: false,
77✔
766
        additionalResult: {
77✔
767
          kind: "reinit",
77✔
768
          reinit,
77✔
769
        },
77✔
770
        selfRemoved: false,
77✔
771
        allProposals,
77✔
772
      }
77✔
773
    }
77✔
774

775
    throwIfDefined(
5,155✔
776
      await validateProposals(
5,155✔
777
        grouped,
5,155✔
778
        committerLeafIndex,
5,155✔
779
        state.groupContext,
5,155✔
780
        state.clientConfig.keyPackageEqualityConfig,
5,155✔
781
        state.clientConfig.authService,
5,155✔
782
        state.ratchetTree,
5,155✔
783
      ),
5,155✔
784
    )
5,155✔
785

786
    const newExtensions = flattenExtensions(grouped.group_context_extensions)
5,155✔
787

788
    const [mutatedTree, addedLeafNodes] = await applyTreeMutations(
5,155✔
789
      state.ratchetTree,
5,155✔
790
      grouped,
5,155✔
791
      state.groupContext,
5,155✔
792
      sentByClient,
5,155✔
793
      state.clientConfig.authService,
5,155✔
794
      state.clientConfig.lifetimeConfig,
5,155✔
795
      cs.signature,
5,155✔
796
    )
5,155✔
797

798
    const [updatedPskSecret, pskIds] = await accumulatePskSecret(
5,045✔
799
      grouped.psk.map((p) => p.proposal.psk.preSharedKeyId),
5,045✔
800
      pskSearch,
5,045✔
801
      cs,
5,045✔
802
      zeroes,
5,045✔
803
    )
5,045✔
804

805
    const selfRemoved = mutatedTree[leafToNodeIndex(toLeafIndex(state.privatePath.leafIndex))] === undefined
5,045✔
806

807
    const needsUpdatePath =
5,045✔
808
      allProposals.length === 0 || Object.values(grouped.update).length > 1 || Object.values(grouped.remove).length > 1
5,232✔
809

810
    return {
5,232✔
811
      tree: mutatedTree,
5,232✔
812
      pskSecret: updatedPskSecret,
5,232✔
813
      additionalResult: {
5,232✔
814
        kind: "memberCommit" as const,
5,232✔
815
        addedLeafNodes,
5,232✔
816
        extensions: newExtensions,
5,232✔
817
      },
5,232✔
818
      pskIds,
5,232✔
819
      needsUpdatePath,
5,232✔
820
      selfRemoved,
5,232✔
821
      allProposals,
5,232✔
822
    }
5,232✔
823
  } else {
5,308✔
824
    throwIfDefined(validateExternalInit(grouped))
76✔
825

826
    const treeAfterRemove = grouped.remove.reduce((acc, { proposal }) => {
76✔
827
      return removeLeafNode(acc, toLeafIndex(proposal.remove.removed))
38✔
828
    }, state.ratchetTree)
76✔
829

830
    const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
76✔
831

832
    const [updatedPskSecret, pskIds] = await accumulatePskSecret(
76✔
833
      grouped.psk.map((p) => p.proposal.psk.preSharedKeyId),
76✔
834
      pskSearch,
76✔
835
      cs,
76✔
836
      zeroes,
76✔
837
    )
76✔
838

839
    const initProposal = grouped.external_init.at(0)!
76✔
840

841
    const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
76✔
842

843
    const externalInitSecret = await importSecret(
76✔
844
      await cs.hpke.exportPrivateKey(externalKeyPair.privateKey),
76✔
845
      initProposal.proposal.externalInit.kemOutput,
76✔
846
      cs,
76✔
847
    )
76✔
848

849
    return {
76✔
850
      needsUpdatePath: true,
76✔
851
      tree: treeAfterRemove,
76✔
852
      pskSecret: updatedPskSecret,
76✔
853
      pskIds,
76✔
854
      additionalResult: {
76✔
855
        kind: "externalCommit",
76✔
856
        externalInitSecret,
76✔
857
        newMemberLeafIndex: nodeToLeafIndex(findBlankLeafNodeIndexOrExtend(treeAfterRemove)),
76✔
858
      },
76✔
859
      selfRemoved: false,
76✔
860
      allProposals,
76✔
861
    }
76✔
862
  }
76✔
863
}
5,308✔
864

865
export function makePskIndex(state: ClientState | undefined, externalPsks: Record<string, Uint8Array>): PskIndex {
1✔
866
  return {
8,469✔
867
    findPsk(preSharedKeyId) {
8,469✔
868
      if (preSharedKeyId.psktype === "external") {
441✔
869
        return externalPsks[bytesToBase64(preSharedKeyId.pskId)]
261✔
870
      }
261✔
871

872
      if (state !== undefined && constantTimeEqual(preSharedKeyId.pskGroupId, state.groupContext.groupId)) {
441✔
873
        if (preSharedKeyId.pskEpoch === state.groupContext.epoch) return state.keySchedule.resumptionPsk
180✔
874
        else return state.historicalReceiverData.get(preSharedKeyId.pskEpoch)?.resumptionPsk
28✔
875
      }
180✔
876
    },
441✔
877
  }
8,469✔
878
}
8,469✔
879

880
export async function nextEpochContext(
4,314✔
881
  groupContext: GroupContext,
4,314✔
882
  wireformat: WireformatName,
4,314✔
883
  content: FramedContentCommit,
4,314✔
884
  signature: Uint8Array,
4,314✔
885
  updatedTreeHash: Uint8Array,
4,314✔
886
  confirmationTag: Uint8Array,
4,314✔
887
  h: Hash,
4,314✔
888
): Promise<GroupContext> {
4,314✔
889
  const interimTranscriptHash = await createInterimHash(groupContext.confirmedTranscriptHash, confirmationTag, h)
4,314✔
890
  const newConfirmedHash = await createConfirmedHash(interimTranscriptHash, { wireformat, content, signature }, h)
4,314✔
891

892
  return {
4,314✔
893
    ...groupContext,
4,314✔
894
    epoch: groupContext.epoch + 1n,
4,314✔
895
    treeHash: updatedTreeHash,
4,314✔
896
    confirmedTranscriptHash: newConfirmedHash,
4,314✔
897
  }
4,314✔
898
}
4,314✔
899

900
export async function joinGroup(
1,100✔
901
  welcome: Welcome,
1,100✔
902
  keyPackage: KeyPackage,
1,100✔
903
  privateKeys: PrivateKeyPackage,
1,100✔
904
  pskSearch: PskIndex,
1,100✔
905
  cs: CiphersuiteImpl,
1,100✔
906
  ratchetTree?: RatchetTree,
1,100✔
907
  resumingFromState?: ClientState,
1,100✔
908
  clientConfig: ClientConfig = defaultClientConfig,
1,100✔
909
): Promise<ClientState> {
1,100✔
910
  const res = await joinGroupWithExtensions(
1,100✔
911
    welcome,
1,100✔
912
    keyPackage,
1,100✔
913
    privateKeys,
1,100✔
914
    pskSearch,
1,100✔
915
    cs,
1,100✔
916
    ratchetTree,
1,100✔
917
    resumingFromState,
1,100✔
918
    clientConfig,
1,100✔
919
  )
1,100✔
920

921
  return res[0]
1,043✔
922
}
1,043✔
923

924
export async function joinGroupWithExtensions(
1,138✔
925
  welcome: Welcome,
1,138✔
926
  keyPackage: KeyPackage,
1,138✔
927
  privateKeys: PrivateKeyPackage,
1,138✔
928
  pskSearch: PskIndex,
1,138✔
929
  cs: CiphersuiteImpl,
1,138✔
930
  ratchetTree?: RatchetTree,
1,138✔
931
  resumingFromState?: ClientState,
1,138✔
932
  clientConfig: ClientConfig = defaultClientConfig,
1,138✔
933
): Promise<[ClientState, Extension[]]> {
1,138✔
934
  const keyPackageRef = await makeKeyPackageRef(keyPackage, cs.hash)
1,138✔
935
  const privKey = await cs.hpke.importPrivateKey(privateKeys.initPrivateKey)
1,138✔
936
  const groupSecrets = await decryptGroupSecrets(privKey, keyPackageRef, welcome, cs.hpke)
1,138✔
937

938
  if (groupSecrets === undefined) throw new CodecError("Could not decode group secrets")
1,138!
939

940
  const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
1,138✔
941

942
  const [pskSecret, pskIds] = await accumulatePskSecret(groupSecrets.psks, pskSearch, cs, zeroes)
1,138✔
943

944
  const gi = await decryptGroupInfo(welcome, groupSecrets.joinerSecret, pskSecret, cs)
1,138✔
945
  if (gi === undefined) throw new CodecError("Could not decode group info")
1,138!
946

947
  const resumptionPsk = pskIds.find((id) => id.psktype === "resumption")
1,138✔
948
  if (resumptionPsk !== undefined) {
1,138✔
949
    if (resumingFromState === undefined) throw new ValidationError("No prior state passed for resumption")
95!
950

951
    if (resumptionPsk.pskEpoch !== resumingFromState.groupContext.epoch) throw new ValidationError("Epoch mismatch")
95!
952

953
    if (!constantTimeEqual(resumptionPsk.pskGroupId, resumingFromState.groupContext.groupId))
95✔
954
      throw new ValidationError("old groupId mismatch")
95!
955

956
    if (gi.groupContext.epoch !== 1n) throw new ValidationError("Resumption must be started at epoch 1")
95!
957

958
    if (resumptionPsk.usage === "reinit") {
95✔
959
      if (resumingFromState.groupActiveState.kind !== "suspendedPendingReinit")
76✔
960
        throw new ValidationError("Found reinit psk but no old suspended clientState")
76!
961

962
      if (!constantTimeEqual(resumingFromState.groupActiveState.reinit.groupId, gi.groupContext.groupId))
76✔
963
        throw new ValidationError("new groupId mismatch")
76✔
964

965
      if (resumingFromState.groupActiveState.reinit.version !== gi.groupContext.version)
57✔
966
        throw new ValidationError("Version mismatch")
76✔
967

968
      if (resumingFromState.groupActiveState.reinit.cipherSuite !== gi.groupContext.cipherSuite)
38✔
969
        throw new ValidationError("Ciphersuite mismatch")
76✔
970

971
      if (!extensionsEqual(resumingFromState.groupActiveState.reinit.extensions, gi.groupContext.extensions))
38✔
972
        throw new ValidationError("Extensions mismatch")
38✔
973
    }
76✔
974
  }
95✔
975

976
  const allExtensionsSupported = extensionsSupportedByCapabilities(
1,081✔
977
    gi.groupContext.extensions,
1,081✔
978
    keyPackage.leafNode.capabilities,
1,081✔
979
  )
1,081✔
980
  if (!allExtensionsSupported) throw new UsageError("client does not support every extension in the GroupContext")
1,138✔
981

982
  const tree = ratchetTreeFromExtension(gi) ?? ratchetTree
1,081✔
983

984
  if (tree === undefined) throw new UsageError("No RatchetTree passed and no ratchet_tree extension")
1,138✔
985

986
  const signerNode = tree[leafToNodeIndex(toLeafIndex(gi.signer))]
1,081✔
987

988
  if (signerNode === undefined) {
1,138!
989
    throw new ValidationError("Could not find signer leafNode")
×
990
  }
✔
991
  if (signerNode.nodeType === "parent") throw new ValidationError("Expected non blank leaf node")
1,138✔
992

993
  const credentialVerified = await clientConfig.authService.validateCredential(
1,081✔
994
    signerNode.leaf.credential,
1,081✔
995
    signerNode.leaf.signaturePublicKey,
1,081✔
996
  )
1,081✔
997

998
  if (!credentialVerified) throw new ValidationError("Could not validate credential")
1,138✔
999

1000
  const groupInfoSignatureVerified = await verifyGroupInfoSignature(
1,081✔
1001
    gi,
1,081✔
1002
    signerNode.leaf.signaturePublicKey,
1,081✔
1003
    cs.signature,
1,081✔
1004
  )
1,081✔
1005

1006
  if (!groupInfoSignatureVerified) throw new CryptoVerificationError("Could not verify groupInfo signature")
1,138✔
1007

1008
  if (gi.groupContext.cipherSuite !== keyPackage.cipherSuite)
1,081✔
1009
    throw new ValidationError("cipher suite in the GroupInfo does not match the cipher_suite in the KeyPackage")
1,138✔
1010

1011
  throwIfDefined(
1,081✔
1012
    await validateRatchetTree(
1,081✔
1013
      tree,
1,081✔
1014
      gi.groupContext,
1,081✔
1015
      clientConfig.lifetimeConfig,
1,081✔
1016
      clientConfig.authService,
1,081✔
1017
      gi.groupContext.treeHash,
1,081✔
1018
      cs,
1,081✔
1019
    ),
1,081✔
1020
  )
1,081✔
1021

1022
  const newLeaf = findLeafIndex(tree, keyPackage.leafNode)
1,081✔
1023

1024
  if (newLeaf === undefined) throw new ValidationError("Could not find own leaf when processing welcome")
1,138✔
1025

1026
  const privateKeyPath: PrivateKeyPath = {
1,081✔
1027
    leafIndex: newLeaf,
1,081✔
1028
    privateKeys: { [leafToNodeIndex(newLeaf)]: privateKeys.hpkePrivateKey },
1,081✔
1029
  }
1,081✔
1030

1031
  const ancestorNodeIndex = firstCommonAncestor(tree, newLeaf, toLeafIndex(gi.signer))
1,081✔
1032

1033
  const updatedPkp =
1,081✔
1034
    groupSecrets.pathSecret === undefined
1,081✔
1035
      ? privateKeyPath
1,025✔
1036
      : mergePrivateKeyPaths(
56✔
1037
          await toPrivateKeyPath(
56✔
1038
            await pathToRoot(tree, ancestorNodeIndex, groupSecrets.pathSecret, cs.kdf),
56✔
1039
            newLeaf,
56✔
1040
            cs,
56✔
1041
          ),
56✔
1042
          privateKeyPath,
56✔
1043
        )
56✔
1044

1045
  const [keySchedule, encryptionSecret] = await deriveKeySchedule(
1,138✔
1046
    groupSecrets.joinerSecret,
1,138✔
1047
    pskSecret,
1,138✔
1048
    gi.groupContext,
1,138✔
1049
    cs.kdf,
1,138✔
1050
  )
1,138✔
1051

1052
  const confirmationTagVerified = await verifyGroupInfoConfirmationTag(gi, groupSecrets.joinerSecret, pskSecret, cs)
1,081✔
1053

1054
  if (!confirmationTagVerified) throw new CryptoVerificationError("Could not verify confirmation tag")
1,138✔
1055

1056
  const secretTree = await createSecretTree(leafWidth(tree.length), encryptionSecret, cs.kdf)
1,081✔
1057

1058
  return [
1,081✔
1059
    {
1,081✔
1060
      groupContext: gi.groupContext,
1,081✔
1061
      ratchetTree: tree,
1,081✔
1062
      privatePath: updatedPkp,
1,081✔
1063
      signaturePrivateKey: privateKeys.signaturePrivateKey,
1,081✔
1064
      confirmationTag: gi.confirmationTag,
1,081✔
1065
      unappliedProposals: {},
1,081✔
1066
      keySchedule,
1,081✔
1067
      secretTree,
1,081✔
1068
      historicalReceiverData: new Map(),
1,081✔
1069
      groupActiveState: { kind: "active" },
1,081✔
1070
      clientConfig,
1,081✔
1071
    },
1,081✔
1072
    gi.extensions,
1,081✔
1073
  ]
1,081✔
1074
}
1,081✔
1075

1076
export async function createGroup(
897✔
1077
  groupId: Uint8Array,
897✔
1078
  keyPackage: KeyPackage,
897✔
1079
  privateKeyPackage: PrivateKeyPackage,
897✔
1080
  extensions: Extension[],
897✔
1081
  cs: CiphersuiteImpl,
897✔
1082
  clientConfig: ClientConfig = defaultClientConfig,
897✔
1083
): Promise<ClientState> {
897✔
1084
  const ratchetTree: RatchetTree = [{ nodeType: "leaf", leaf: keyPackage.leafNode }]
897✔
1085

1086
  const privatePath: PrivateKeyPath = {
897✔
1087
    leafIndex: 0,
897✔
1088
    privateKeys: { [0]: privateKeyPackage.hpkePrivateKey },
897✔
1089
  }
897✔
1090

1091
  const confirmedTranscriptHash = new Uint8Array()
897✔
1092

1093
  const groupContext: GroupContext = {
897✔
1094
    version: "mls10",
897✔
1095
    cipherSuite: cs.name,
897✔
1096
    epoch: 0n,
897✔
1097
    treeHash: await treeHashRoot(ratchetTree, cs.hash),
897✔
1098
    groupId,
897✔
1099
    extensions,
897✔
1100
    confirmedTranscriptHash,
897✔
1101
  }
897✔
1102

1103
  throwIfDefined(await validateExternalSenders(extensions, clientConfig.authService))
897✔
1104

1105
  const epochSecret = cs.rng.randomBytes(cs.kdf.size)
897✔
1106

1107
  const keySchedule = await initializeKeySchedule(epochSecret, cs.kdf)
897✔
1108

1109
  const confirmationTag = await createConfirmationTag(keySchedule.confirmationKey, confirmedTranscriptHash, cs.hash)
897✔
1110

1111
  const encryptionSecret = await deriveSecret(epochSecret, "encryption", cs.kdf)
897✔
1112

1113
  const secretTree = await createSecretTree(1, encryptionSecret, cs.kdf)
897✔
1114

1115
  return {
897✔
1116
    ratchetTree,
897✔
1117
    keySchedule,
897✔
1118
    secretTree,
897✔
1119
    privatePath,
897✔
1120
    signaturePrivateKey: privateKeyPackage.signaturePrivateKey,
897✔
1121
    unappliedProposals: {},
897✔
1122
    historicalReceiverData: new Map(),
897✔
1123
    groupContext,
897✔
1124
    confirmationTag,
897✔
1125
    groupActiveState: { kind: "active" },
897✔
1126
    clientConfig,
897✔
1127
  }
897✔
1128
}
897✔
1129

1130
export async function exportSecret(
152✔
1131
  publicKey: Uint8Array,
152✔
1132
  cs: CiphersuiteImpl,
152✔
1133
): Promise<{ enc: Uint8Array; secret: Uint8Array }> {
152✔
1134
  return cs.hpke.exportSecret(
152✔
1135
    await cs.hpke.importPublicKey(publicKey),
152✔
1136
    new TextEncoder().encode("MLS 1.0 external init secret"),
152✔
1137
    cs.kdf.size,
152✔
1138
    new Uint8Array(),
152✔
1139
  )
152✔
1140
}
152✔
1141

1142
async function importSecret(privateKey: Uint8Array, kemOutput: Uint8Array, cs: CiphersuiteImpl): Promise<Uint8Array> {
76✔
1143
  return cs.hpke.importSecret(
76✔
1144
    await cs.hpke.importPrivateKey(privateKey),
76✔
1145
    new TextEncoder().encode("MLS 1.0 external init secret"),
76✔
1146
    kemOutput,
76✔
1147
    cs.kdf.size,
76✔
1148
    new Uint8Array(),
76✔
1149
  )
76✔
1150
}
76✔
1151

1152
async function applyTreeMutations(
4,877✔
1153
  ratchetTree: RatchetTree,
4,877✔
1154
  grouped: Proposals,
4,877✔
1155
  gc: GroupContext,
4,877✔
1156
  sentByClient: boolean,
4,877✔
1157
  authService: AuthenticationService,
4,877✔
1158
  lifetimeConfig: LifetimeConfig,
4,877✔
1159
  s: Signature,
4,877✔
1160
): Promise<[RatchetTree, [LeafIndex, KeyPackage][]]> {
4,877✔
1161
  const treeAfterUpdate = await grouped.update.reduce(async (acc, { senderLeafIndex, proposal }) => {
4,877✔
1162
    if (senderLeafIndex === undefined) throw new InternalError("No sender index found for update proposal")
14!
1163

1164
    throwIfDefined(await validateLeafNodeUpdateOrCommit(proposal.update.leafNode, senderLeafIndex, gc, authService, s))
14✔
1165
    throwIfDefined(
14✔
1166
      await validateLeafNodeCredentialAndKeyUniqueness(ratchetTree, proposal.update.leafNode, senderLeafIndex),
14✔
1167
    )
14✔
1168

1169
    return updateLeafNode(await acc, proposal.update.leafNode, toLeafIndex(senderLeafIndex))
14✔
1170
  }, Promise.resolve(ratchetTree))
4,877✔
1171

1172
  const treeAfterRemove = grouped.remove.reduce((acc, { proposal }) => {
4,877✔
1173
    throwIfDefined(validateRemove(proposal.remove, ratchetTree))
2,425✔
1174

1175
    return removeLeafNode(acc, toLeafIndex(proposal.remove.removed))
2,425✔
1176
  }, treeAfterUpdate)
4,877✔
1177

1178
  const [treeAfterAdd, addedLeafNodes] = await grouped.add.reduce(
4,877✔
1179
    async (acc, { proposal }) => {
4,877✔
1180
      throwIfDefined(
3,137✔
1181
        await validateKeyPackage(
3,137✔
1182
          proposal.add.keyPackage,
3,137✔
1183
          gc,
3,137✔
1184
          ratchetTree,
3,137✔
1185
          sentByClient,
3,137✔
1186
          lifetimeConfig,
3,137✔
1187
          authService,
3,137✔
1188
          s,
3,137✔
1189
        ),
3,137✔
1190
      )
3,137✔
1191

1192
      const [tree, ws] = await acc
3,137✔
1193
      const [updatedTree, leafNodeIndex] = addLeafNode(tree, proposal.add.keyPackage.leafNode)
3,058✔
1194
      return [
3,058✔
1195
        updatedTree,
3,058✔
1196
        [...ws, [nodeToLeafIndex(leafNodeIndex), proposal.add.keyPackage] as [LeafIndex, KeyPackage]],
3,058✔
1197
      ]
3,058✔
1198
    },
3,137✔
1199
    Promise.resolve([treeAfterRemove, []] as [RatchetTree, [LeafIndex, KeyPackage][]]),
4,877✔
1200
  )
4,877✔
1201

1202
  return [treeAfterAdd, addedLeafNodes]
4,798✔
1203
}
4,798✔
1204

1205
export async function processProposal(
1,835✔
1206
  state: ClientState,
1,835✔
1207
  content: AuthenticatedContent,
1,835✔
1208
  proposal: Proposal,
1,835✔
1209
  h: Hash,
1,835✔
1210
): Promise<ClientState> {
1,835✔
1211
  const ref = await makeProposalRef(content, h)
1,835✔
1212
  return {
1,835✔
1213
    ...state,
1,835✔
1214
    unappliedProposals: addUnappliedProposal(
1,835✔
1215
      ref,
1,835✔
1216
      state.unappliedProposals,
1,835✔
1217
      proposal,
1,835✔
1218
      getSenderLeafNodeIndex(content.content.sender),
1,835✔
1219
    ),
1,835✔
1220
  }
1,835✔
1221
}
1,835✔
1222

1223
export function addHistoricalReceiverData(state: ClientState): Map<bigint, EpochReceiverData> {
1✔
1224
  const withNew = addToMap(state.historicalReceiverData, state.groupContext.epoch, {
5,161✔
1225
    secretTree: state.secretTree,
5,161✔
1226
    ratchetTree: state.ratchetTree,
5,161✔
1227
    senderDataSecret: state.keySchedule.senderDataSecret,
5,161✔
1228
    groupContext: state.groupContext,
5,161✔
1229
    resumptionPsk: state.keySchedule.resumptionPsk,
5,161✔
1230
  })
5,161✔
1231

1232
  const epochs = [...withNew.keys()]
5,161✔
1233

1234
  const result =
5,161✔
1235
    epochs.length >= state.clientConfig.keyRetentionConfig.retainKeysForEpochs
5,161✔
1236
      ? removeOldHistoricalReceiverData(withNew, state.clientConfig.keyRetentionConfig.retainKeysForEpochs)
2,762✔
1237
      : withNew
2,399✔
1238

1239
  return result
5,161✔
1240
}
5,161✔
1241

1242
function removeOldHistoricalReceiverData(
2,762✔
1243
  historicalReceiverData: Map<bigint, EpochReceiverData>,
2,762✔
1244
  max: number,
2,762✔
1245
): Map<bigint, EpochReceiverData> {
2,762✔
1246
  const sortedEpochs = [...historicalReceiverData.keys()].sort((a, b) => (a < b ? -1 : 1))
2,762✔
1247

1248
  return new Map(sortedEpochs.slice(-max).map((epoch) => [epoch, historicalReceiverData.get(epoch)!]))
2,762✔
1249
}
2,762✔
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