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

LukaJCB / ts-mls / 20546506344

28 Dec 2025 12:42AM UTC coverage: 96.121% (-0.1%) from 96.237%
20546506344

push

github

LukaJCB
Deprecate json codec for group state

1239 of 1388 branches covered (89.27%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

16 existing lines in 5 files now uncovered.

7162 of 7352 relevant lines covered (97.42%)

44152.98 hits per line

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

97.25
/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

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

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

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

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

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

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

198
  checkCanSendHandshakeMessages(state)
2,451✔
199
}
2,451✔
200

201
export function checkCanSendHandshakeMessages(state: ClientState): void {
1✔
202
  if (state.groupActiveState.kind === "suspendedPendingReinit")
4,422✔
203
    throw new UsageError("Cannot send messages while Group is suspended pending reinit")
4,422✔
204
  else if (state.groupActiveState.kind === "removedFromGroup")
4,403✔
205
    throw new UsageError("Cannot send messages after being removed from group")
4,403✔
206
}
4,422✔
207

208
export interface Proposals {
209
  add: { senderLeafIndex: number | undefined; proposal: ProposalAdd }[]
210
  update: { senderLeafIndex: number | undefined; proposal: ProposalUpdate }[]
211
  remove: { senderLeafIndex: number | undefined; proposal: ProposalRemove }[]
212
  psk: { senderLeafIndex: number | undefined; proposal: ProposalPSK }[]
213
  reinit: { senderLeafIndex: number | undefined; proposal: ProposalReinit }[]
214
  external_init: { senderLeafIndex: number | undefined; proposal: ProposalExternalInit }[]
215
  group_context_extensions: { senderLeafIndex: number | undefined; proposal: ProposalGroupContextExtensions }[]
216
}
217

218
const emptyProposals: Proposals = {
1✔
219
  add: [],
1✔
220
  update: [],
1✔
221
  remove: [],
1✔
222
  psk: [],
1✔
223
  reinit: [],
1✔
224
  external_init: [],
1✔
225
  group_context_extensions: [],
1✔
226
}
1✔
227

228
function flattenExtensions(groupContextExtensions: { proposal: ProposalGroupContextExtensions }[]): Extension[] {
3,053✔
229
  return groupContextExtensions.reduce((acc, { proposal }) => {
3,053✔
230
    return [...acc, ...proposal.groupContextExtensions.extensions]
61✔
231
  }, [] as Extension[])
3,053✔
232
}
3,053✔
233

234
async function validateProposals(
5,146✔
235
  p: Proposals,
5,146✔
236
  committerLeafIndex: number | undefined,
5,146✔
237
  groupContext: GroupContext,
5,146✔
238
  config: KeyPackageEqualityConfig,
5,146✔
239
  authService: AuthenticationService,
5,146✔
240
  tree: RatchetTree,
5,146✔
241
): Promise<MlsError | undefined> {
5,146✔
242
  const containsUpdateByCommitter = p.update.some(
5,146✔
243
    (o) => o.senderLeafIndex !== undefined && o.senderLeafIndex === committerLeafIndex,
5,146✔
244
  )
5,146✔
245

246
  if (containsUpdateByCommitter)
5,146✔
247
    return new ValidationError("Commit cannot contain an update proposal sent by committer")
5,146✔
248

249
  const containsRemoveOfCommitter = p.remove.some((o) => o.proposal.remove.removed === committerLeafIndex)
5,145✔
250

251
  if (containsRemoveOfCommitter)
5,145✔
252
    return new ValidationError("Commit cannot contain a remove proposal removing committer")
5,146✔
253

254
  const multipleUpdateRemoveForSameLeaf =
5,144✔
255
    p.update.some(
5,144✔
256
      ({ senderLeafIndex: a }, indexA) =>
5,144✔
257
        p.update.some(({ senderLeafIndex: b }, indexB) => a === b && indexA !== indexB) ||
14✔
258
        p.remove.some((r) => r.proposal.remove.removed === a),
14✔
259
    ) ||
5,144✔
260
    p.remove.some(
5,144✔
261
      (a, indexA) =>
5,144✔
262
        p.remove.some((b, indexB) => b.proposal.remove.removed === a.proposal.remove.removed && indexA !== indexB) ||
2,426✔
263
        p.update.some(({ senderLeafIndex }) => a.proposal.remove.removed === senderLeafIndex),
2,425✔
264
    )
5,144✔
265

266
  if (multipleUpdateRemoveForSameLeaf)
5,146✔
267
    return new ValidationError(
5,146✔
268
      "Commit cannot contain multiple update and/or remove proposals that apply to the same leaf",
1✔
269
    )
1✔
270

271
  const multipleAddsContainSameKeypackage = p.add.some(({ proposal: a }, indexA) =>
5,143✔
272
    p.add.some(
3,150✔
273
      ({ proposal: b }, indexB) => config.compareKeyPackages(a.add.keyPackage, b.add.keyPackage) && indexA !== indexB,
3,150✔
274
    ),
3,150✔
275
  )
5,143✔
276

277
  if (multipleAddsContainSameKeypackage)
5,143✔
278
    return new ValidationError(
5,146✔
279
      "Commit cannot contain multiple Add proposals that contain KeyPackages that represent the same client",
1✔
280
    )
1✔
281

282
  // checks if there is an Add proposal with a KeyPackage that matches a client already in the group
283
  // unless there is a Remove proposal in the list removing the matching client from the group.
284
  const addsContainExistingKeypackage = p.add.some(({ proposal }) =>
5,142✔
285
    tree.some(
3,149✔
286
      (node, nodeIndex) =>
3,149✔
287
        node !== undefined &&
186,475✔
288
        node.nodeType === "leaf" &&
41,509✔
289
        config.compareKeyPackageToLeafNode(proposal.add.keyPackage, node.leaf) &&
32,435✔
290
        p.remove.every((r) => r.proposal.remove.removed !== nodeToLeafIndex(toNodeIndex(nodeIndex))),
1✔
291
    ),
3,149✔
292
  )
5,142✔
293

294
  if (addsContainExistingKeypackage)
5,142✔
295
    return new ValidationError("Commit cannot contain an Add proposal for someone already in the group")
5,146✔
296

297
  const everyLeafSupportsGroupExtensions = p.add.every(({ proposal }) =>
5,141✔
298
    extensionsSupportedByCapabilities(groupContext.extensions, proposal.add.keyPackage.leafNode.capabilities),
3,148✔
299
  )
5,141✔
300

301
  if (!everyLeafSupportsGroupExtensions)
5,141✔
302
    return new ValidationError("Added leaf node that doesn't support extension in GroupContext")
5,146✔
303

304
  const multiplePskWithSamePskId = p.psk.some((a, indexA) =>
5,122✔
305
    p.psk.some(
209✔
306
      (b, indexB) =>
209✔
307
        constantTimeEqual(
314✔
308
          encode(pskIdEncoder)(a.proposal.psk.preSharedKeyId),
314✔
309
          encode(pskIdEncoder)(b.proposal.psk.preSharedKeyId),
314✔
310
        ) && indexA !== indexB,
314✔
311
    ),
209✔
312
  )
5,122✔
313

314
  if (multiplePskWithSamePskId)
5,122✔
315
    return new ValidationError("Commit cannot contain PreSharedKey proposals that reference the same PreSharedKeyID")
5,146✔
316

317
  const multipleGroupContextExtensions = p.group_context_extensions.length > 1
5,121✔
318

319
  if (multipleGroupContextExtensions)
5,121✔
320
    return new ValidationError("Commit cannot contain multiple GroupContextExtensions proposals")
5,146✔
321

322
  const allExtensions = flattenExtensions(p.group_context_extensions)
5,120✔
323

324
  const requiredCapabilities = allExtensions.find((e) => e.extensionType === "required_capabilities")
5,120✔
325

326
  if (requiredCapabilities !== undefined) {
5,146✔
327
    const caps = decodeRequiredCapabilities(requiredCapabilities.extensionData, 0)
3✔
328
    if (caps === undefined) return new CodecError("Could not decode required_capabilities")
3✔
329

330
    const everyLeafSupportsCapabilities = tree
2✔
331
      .filter((n) => n !== undefined && n.nodeType === "leaf")
2✔
332
      .every((l) => capabiltiesAreSupported(caps[0], l.leaf.capabilities))
2✔
333

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

336
    const allAdditionsSupportCapabilities = p.add.every((a) =>
1✔
337
      capabiltiesAreSupported(caps[0], a.proposal.add.keyPackage.leafNode.capabilities),
1✔
338
    )
1✔
339

340
    if (!allAdditionsSupportCapabilities)
1✔
341
      return new ValidationError("Commit contains add proposals of member without required capabilities")
1✔
342
  }
3✔
343

344
  return await validateExternalSenders(allExtensions, authService)
5,117✔
345
}
5,117✔
346

347
async function validateExternalSenders(
6,009✔
348
  extensions: Extension[],
6,009✔
349
  authService: AuthenticationService,
6,009✔
350
): Promise<MlsError | undefined> {
6,009✔
351
  const externalSenders = extensions.filter((e) => e.extensionType === "external_senders")
6,009✔
352
  for (const externalSender of externalSenders) {
6,009✔
353
    const decoded = decodeExternalSender(externalSender.extensionData, 0)
21✔
354
    if (decoded === undefined) return new CodecError("Could not decode external_senders")
21✔
355

356
    const validCredential = await authService.validateCredential(decoded[0].credential, decoded[0].signaturePublicKey)
20✔
357
    if (!validCredential) return new ValidationError("Could not validate external credential")
20✔
358
  }
21✔
359
}
6,007✔
360

361
function capabiltiesAreSupported(caps: RequiredCapabilities, cs: Capabilities): boolean {
81✔
362
  return (
81✔
363
    caps.credentialTypes.every((c) => cs.credentials.includes(c)) &&
81✔
364
    caps.extensionTypes.every((e) => cs.extensions.includes(e)) &&
61✔
365
    caps.proposalTypes.every((p) => cs.proposals.includes(p))
61✔
366
  )
367
}
81✔
368

369
export async function validateRatchetTree(
1,265✔
370
  tree: RatchetTree,
1,265✔
371
  groupContext: GroupContext,
1,265✔
372
  config: LifetimeConfig,
1,265✔
373
  authService: AuthenticationService,
1,265✔
374
  treeHash: Uint8Array,
1,265✔
375
  cs: CiphersuiteImpl,
1,265✔
376
): Promise<MlsError | undefined> {
1,265✔
377
  const hpkeKeys = new Set<string>()
1,265✔
378
  const signatureKeys = new Set<string>()
1,265✔
379
  const credentialTypes = new Set<CredentialTypeName>()
1,265✔
380
  for (const [i, n] of tree.entries()) {
1,265✔
381
    const nodeIndex = toNodeIndex(i)
8,671✔
382
    if (n?.nodeType === "leaf") {
8,671✔
383
      if (!isLeaf(nodeIndex)) return new ValidationError("Received Ratchet Tree is not structurally sound")
4,493!
384

385
      const hpkeKey = bytesToBase64(n.leaf.hpkePublicKey)
4,493✔
386
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
4,493✔
387
      else hpkeKeys.add(hpkeKey)
4,474✔
388

389
      const signatureKey = bytesToBase64(n.leaf.signaturePublicKey)
4,474✔
390
      if (signatureKeys.has(signatureKey)) return new ValidationError("signature keys not unique")
4,493✔
391
      else signatureKeys.add(signatureKey)
4,455✔
392

393
      credentialTypes.add(n.leaf.credential.credentialType)
4,455✔
394

395
      const err =
4,455✔
396
        n.leaf.leafNodeSource === "key_package"
4,455✔
397
          ? await validateLeafNodeKeyPackage(n.leaf, groupContext, false, config, authService, cs.signature)
4,019✔
398
          : await validateLeafNodeUpdateOrCommit(
436✔
399
              n.leaf,
436✔
400
              nodeToLeafIndex(nodeIndex),
436✔
401
              groupContext,
436✔
402
              authService,
436✔
403
              cs.signature,
436✔
404
            )
436✔
405

406
      if (err !== undefined) return err
2,510✔
407
    } else if (n?.nodeType === "parent") {
8,671✔
408
      if (isLeaf(nodeIndex)) return new ValidationError("Received Ratchet Tree is not structurally sound")
528✔
409

410
      const hpkeKey = bytesToBase64(n.parent.hpkePublicKey)
509✔
411
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
528✔
412
      else hpkeKeys.add(hpkeKey)
509✔
413

414
      for (const unmergedLeaf of n.parent.unmergedLeaves) {
528✔
415
        const leafIndex = toLeafIndex(unmergedLeaf)
114✔
416
        const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length))
114✔
417
        const nodeIndex = leafToNodeIndex(leafIndex)
114✔
418
        if (tree[nodeIndex]?.nodeType !== "leaf" && !dp.includes(toNodeIndex(i)))
114!
419
          return new ValidationError("Unmerged leaf did not represent a non-blank descendant leaf node")
114!
420

421
        for (const parentIdx of dp) {
114✔
422
          const dpNode = tree[parentIdx]
342✔
423

424
          if (dpNode !== undefined) {
342✔
425
            if (dpNode.nodeType !== "parent") return new InternalError("Expected parent node")
114!
426

427
            if (!arraysEqual(dpNode.parent.unmergedLeaves, n.parent.unmergedLeaves))
114✔
428
              return new ValidationError("non-blank intermediate node must list leaf node in its unmerged_leaves")
114!
429
          }
114✔
430
        }
342✔
431
      }
114✔
432
    }
509✔
433
  }
8,671✔
434

435
  for (const n of tree) {
1,265✔
436
    if (n?.nodeType === "leaf") {
8,481✔
437
      for (const credentialType of credentialTypes) {
4,360✔
438
        if (!n.leaf.capabilities.credentials.includes(credentialType))
4,360✔
439
          return new ValidationError("LeafNode has credential that is not supported by member of the group")
4,360!
440
      }
4,360✔
441
    }
4,360✔
442
  }
8,481✔
443

444
  const parentHashesVerified = await verifyParentHashes(tree, cs.hash)
1,151✔
445

446
  if (!parentHashesVerified) return new CryptoVerificationError("Unable to verify parent hash")
1,265✔
447

448
  if (!constantTimeEqual(treeHash, await treeHashRoot(tree, cs.hash)))
1,132✔
449
    return new ValidationError("Unable to verify tree hash")
1,132✔
450
}
1,265✔
451

452
export async function validateLeafNodeUpdateOrCommit(
2,457✔
453
  leafNode: LeafNodeCommit | LeafNodeUpdate,
2,457✔
454
  leafIndex: number,
2,457✔
455
  groupContext: GroupContext,
2,457✔
456
  authService: AuthenticationService,
2,457✔
457
  s: Signature,
2,457✔
458
): Promise<MlsError | undefined> {
2,457✔
459
  const signatureValid = await verifyLeafNodeSignature(leafNode, groupContext.groupId, leafIndex, s)
2,457✔
460

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

463
  const commonError = await validateLeafNodeCommon(leafNode, groupContext, authService)
2,438✔
464

465
  if (commonError !== undefined) return commonError
2,457✔
466
}
2,457✔
467

468
export function throwIfDefined(err: MlsError | undefined): void {
1✔
469
  if (err !== undefined) throw err
17,012✔
470
}
17,012✔
471

472
async function validateLeafNodeCommon(
9,527✔
473
  leafNode: LeafNode,
9,527✔
474
  groupContext: GroupContext,
9,527✔
475
  authService: AuthenticationService,
9,527✔
476
) {
9,527✔
477
  const credentialValid = await authService.validateCredential(leafNode.credential, leafNode.signaturePublicKey)
9,527✔
478

479
  if (!credentialValid) return new ValidationError("Could not validate credential")
9,527✔
480

481
  const requiredCapabilities = groupContext.extensions.find((e) => e.extensionType === "required_capabilities")
9,507✔
482

483
  if (requiredCapabilities !== undefined) {
9,527✔
484
    const caps = decodeRequiredCapabilities(requiredCapabilities.extensionData, 0)
76✔
485
    if (caps === undefined) return new CodecError("Could not decode required_capabilities")
76!
486

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

489
    if (!leafSupportsCapabilities) return new ValidationError("LeafNode does not support required capabilities")
76✔
490
  }
76✔
491

492
  const extensionsSupported = extensionsSupportedByCapabilities(leafNode.extensions, leafNode.capabilities)
9,488✔
493

494
  if (!extensionsSupported) return new ValidationError("LeafNode contains extension not listed in capabilities")
9,527✔
495
}
9,527✔
496

497
async function validateLeafNodeKeyPackage(
7,108✔
498
  leafNode: LeafNodeKeyPackage,
7,108✔
499
  groupContext: GroupContext,
7,108✔
500
  sentByClient: boolean,
7,108✔
501
  config: LifetimeConfig,
7,108✔
502
  authService: AuthenticationService,
7,108✔
503
  s: Signature,
7,108✔
504
): Promise<MlsError | undefined> {
7,108✔
505
  const signatureValid = await verifyLeafNodeSignatureKeyPackage(leafNode, s)
7,108✔
506
  if (!signatureValid) return new CryptoVerificationError("Could not verify leaf node signature")
7,108✔
507

508
  //verify lifetime
509
  if (sentByClient || config.validateLifetimeOnReceive) {
7,108✔
510
    if (leafNode.leafNodeSource === "key_package") {
1,082✔
511
      const currentTime = BigInt(Math.floor(Date.now() / 1000))
1,082✔
512
      if (leafNode.lifetime.notBefore > currentTime || leafNode.lifetime.notAfter < currentTime)
1,082✔
513
        return new ValidationError("Current time not within Lifetime")
1,082!
514
    }
1,082✔
515
  }
1,082✔
516

517
  const commonError = await validateLeafNodeCommon(leafNode, groupContext, authService)
7,089✔
518

519
  if (commonError !== undefined) return commonError
7,108✔
520
}
7,108✔
521

522
export async function validateLeafNodeCredentialAndKeyUniqueness(
5,111✔
523
  tree: RatchetTree,
5,111✔
524
  leafNode: LeafNode,
5,111✔
525
  existingLeafIndex?: number,
5,111✔
526
): Promise<ValidationError | undefined> {
5,111✔
527
  const hpkeKeys = new Set<string>()
5,111✔
528
  const signatureKeys = new Set<string>()
5,111✔
529
  for (const [nodeIndex, node] of tree.entries()) {
5,111✔
530
    if (node?.nodeType === "leaf") {
222,531✔
531
      if (!node.leaf.capabilities.credentials.includes(leafNode.credential.credentialType)) {
46,510✔
532
        return new ValidationError("LeafNode has credential that is not supported by member of the group")
1✔
533
      }
1✔
534

535
      const hpkeKey = bytesToBase64(node.leaf.hpkePublicKey)
46,509✔
536
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
46,510✔
537
      else hpkeKeys.add(hpkeKey)
46,509✔
538

539
      const signatureKey = bytesToBase64(node.leaf.signaturePublicKey)
46,509✔
540
      if (signatureKeys.has(signatureKey) && existingLeafIndex !== nodeToLeafIndex(toNodeIndex(nodeIndex)))
46,510!
541
        return new ValidationError("signature keys not unique")
46,510✔
542
      else signatureKeys.add(signatureKey)
46,509✔
543
    } else if (node?.nodeType === "parent") {
222,531✔
544
      const hpkeKey = bytesToBase64(node.parent.hpkePublicKey)
16,816✔
545
      if (hpkeKeys.has(hpkeKey)) return new ValidationError("hpke keys not unique")
16,816!
546
      else hpkeKeys.add(hpkeKey)
16,816✔
547
    }
16,816✔
548
  }
222,531✔
549
}
5,110✔
550

551
async function validateKeyPackage(
3,128✔
552
  kp: KeyPackage,
3,128✔
553
  groupContext: GroupContext,
3,128✔
554
  tree: RatchetTree,
3,128✔
555
  sentByClient: boolean,
3,128✔
556
  config: LifetimeConfig,
3,128✔
557
  authService: AuthenticationService,
3,128✔
558
  s: Signature,
3,128✔
559
): Promise<MlsError | undefined> {
3,128✔
560
  if (kp.cipherSuite !== groupContext.cipherSuite) return new ValidationError("Invalid CipherSuite")
3,128✔
561

562
  if (kp.version !== groupContext.version) return new ValidationError("Invalid mls version")
3,128✔
563

564
  const leafNodeConsistentWithTree = await validateLeafNodeCredentialAndKeyUniqueness(tree, kp.leafNode)
3,090✔
565

566
  if (leafNodeConsistentWithTree !== undefined) return leafNodeConsistentWithTree
3,128✔
567

568
  const leafNodeError = await validateLeafNodeKeyPackage(
3,089✔
569
    kp.leafNode,
3,089✔
570
    groupContext,
3,089✔
571
    sentByClient,
3,089✔
572
    config,
3,089✔
573
    authService,
3,089✔
574
    s,
3,089✔
575
  )
3,089✔
576
  if (leafNodeError !== undefined) return leafNodeError
3,128✔
577

578
  const signatureValid = await verifyKeyPackage(kp, s)
3,068✔
579
  if (!signatureValid) return new CryptoVerificationError("Invalid keypackage signature")
3,128✔
580

581
  if (constantTimeEqual(kp.initKey, kp.leafNode.hpkePublicKey))
3,049✔
582
    return new ValidationError("Cannot have identicial init and encryption keys")
3,128!
583
}
3,128✔
584

585
function validateReinit(
76✔
586
  allProposals: ProposalWithSender[],
76✔
587
  reinit: Reinit,
76✔
588
  gc: GroupContext,
76✔
589
): ValidationError | undefined {
76✔
590
  if (allProposals.length !== 1) return new ValidationError("Reinit proposal needs to be commited by itself")
76!
591

592
  if (protocolVersions[reinit.version] < protocolVersions[gc.version])
76✔
593
    return new ValidationError("A ReInit proposal cannot use a version less than the version for the current group")
76!
594
}
76✔
595

596
function validateExternalInit(grouped: Proposals): ValidationError | undefined {
76✔
597
  if (grouped.external_init.length > 1)
76✔
598
    return new ValidationError("Cannot contain more than one external_init proposal")
76!
599

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

602
  if (
76✔
603
    grouped.add.length > 0 ||
76✔
604
    grouped.group_context_extensions.length > 0 ||
76✔
605
    grouped.reinit.length > 0 ||
76✔
606
    grouped.update.length > 0
76✔
607
  )
608
    return new ValidationError("Invalid proposals")
76!
609
}
76✔
610

611
function validateRemove(remove: Remove, tree: RatchetTree): MlsError | undefined {
2,425✔
612
  if (tree[leafToNodeIndex(toLeafIndex(remove.removed))] === undefined)
2,425✔
613
    return new ValidationError("Tried to remove empty leaf node")
2,425!
614
}
2,425✔
615

616
export interface ApplyProposalsResult {
617
  tree: RatchetTree
618
  pskSecret: Uint8Array
619
  pskIds: PreSharedKeyID[]
620
  needsUpdatePath: boolean
621
  additionalResult: ApplyProposalsData
622
  selfRemoved: boolean
623
  allProposals: ProposalWithSender[]
624
}
625

626
export type ApplyProposalsData =
627
  | { kind: "memberCommit"; addedLeafNodes: [LeafIndex, KeyPackage][]; extensions: Extension[] }
628
  | { kind: "externalCommit"; externalInitSecret: Uint8Array; newMemberLeafIndex: LeafIndex }
629
  | { kind: "reinit"; reinit: Reinit }
630

631
export async function applyProposals(
5,298✔
632
  state: ClientState,
5,298✔
633
  proposals: ProposalOrRef[],
5,298✔
634
  committerLeafIndex: LeafIndex | undefined,
5,298✔
635
  pskSearch: PskIndex,
5,298✔
636
  sentByClient: boolean,
5,298✔
637
  cs: CiphersuiteImpl,
5,298✔
638
): Promise<ApplyProposalsResult> {
5,298✔
639
  const allProposals = proposals.reduce((acc, cur) => {
5,298✔
640
    if (cur.proposalOrRefType === "proposal")
6,067✔
641
      return [...acc, { proposal: cur.proposal, senderLeafIndex: committerLeafIndex }]
6,067✔
642

643
    const p = state.unappliedProposals[bytesToBase64(cur.reference)]
1,854✔
644
    if (p === undefined) throw new ValidationError("Could not find proposal with supplied reference")
3,580✔
645
    return [...acc, p]
1,854✔
646
  }, [] as ProposalWithSender[])
5,298✔
647

648
  const grouped = allProposals.reduce((acc, cur) => {
5,298✔
649
    //this skips any custom proposals
650
    if (typeof cur.proposal.proposalType === "number") return acc
6,067✔
651
    const proposal = acc[cur.proposal.proposalType] ?? []
6,067!
652
    return { ...acc, [cur.proposal.proposalType]: [...proposal, cur] }
6,067✔
653
  }, emptyProposals)
5,298✔
654

655
  const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
5,298✔
656

657
  const isExternalInit = grouped.external_init.length > 0
5,298✔
658

659
  if (!isExternalInit) {
5,298✔
660
    if (grouped.reinit.length > 0) {
5,222✔
661
      const reinit = grouped.reinit.at(0)!.proposal.reinit
76✔
662

663
      throwIfDefined(validateReinit(allProposals, reinit, state.groupContext))
76✔
664

665
      return {
76✔
666
        tree: state.ratchetTree,
76✔
667
        pskSecret: zeroes,
76✔
668
        pskIds: [],
76✔
669
        needsUpdatePath: false,
76✔
670
        additionalResult: {
76✔
671
          kind: "reinit",
76✔
672
          reinit,
76✔
673
        },
76✔
674
        selfRemoved: false,
76✔
675
        allProposals,
76✔
676
      }
76✔
677
    }
76✔
678

679
    throwIfDefined(
5,146✔
680
      await validateProposals(
5,146✔
681
        grouped,
5,146✔
682
        committerLeafIndex,
5,146✔
683
        state.groupContext,
5,146✔
684
        state.clientConfig.keyPackageEqualityConfig,
5,146✔
685
        state.clientConfig.authService,
5,146✔
686
        state.ratchetTree,
5,146✔
687
      ),
5,146✔
688
    )
5,146✔
689

690
    const newExtensions = flattenExtensions(grouped.group_context_extensions)
5,146✔
691

692
    const [mutatedTree, addedLeafNodes] = await applyTreeMutations(
5,146✔
693
      state.ratchetTree,
5,146✔
694
      grouped,
5,146✔
695
      state.groupContext,
5,146✔
696
      sentByClient,
5,146✔
697
      state.clientConfig.authService,
5,146✔
698
      state.clientConfig.lifetimeConfig,
5,146✔
699
      cs.signature,
5,146✔
700
    )
5,146✔
701

702
    const [updatedPskSecret, pskIds] = await accumulatePskSecret(
5,036✔
703
      grouped.psk.map((p) => p.proposal.psk.preSharedKeyId),
5,036✔
704
      pskSearch,
5,036✔
705
      cs,
5,036✔
706
      zeroes,
5,036✔
707
    )
5,036✔
708

709
    const selfRemoved = mutatedTree[leafToNodeIndex(toLeafIndex(state.privatePath.leafIndex))] === undefined
5,036✔
710

711
    const needsUpdatePath =
5,036✔
712
      allProposals.length === 0 || Object.values(grouped.update).length > 1 || Object.values(grouped.remove).length > 1
5,222✔
713

714
    return {
5,222✔
715
      tree: mutatedTree,
5,222✔
716
      pskSecret: updatedPskSecret,
5,222✔
717
      additionalResult: {
5,222✔
718
        kind: "memberCommit" as const,
5,222✔
719
        addedLeafNodes,
5,222✔
720
        extensions: newExtensions,
5,222✔
721
      },
5,222✔
722
      pskIds,
5,222✔
723
      needsUpdatePath,
5,222✔
724
      selfRemoved,
5,222✔
725
      allProposals,
5,222✔
726
    }
5,222✔
727
  } else {
5,298✔
728
    throwIfDefined(validateExternalInit(grouped))
76✔
729

730
    const treeAfterRemove = grouped.remove.reduce((acc, { proposal }) => {
76✔
731
      return removeLeafNode(acc, toLeafIndex(proposal.remove.removed))
38✔
732
    }, state.ratchetTree)
76✔
733

734
    const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
76✔
735

736
    const [updatedPskSecret, pskIds] = await accumulatePskSecret(
76✔
737
      grouped.psk.map((p) => p.proposal.psk.preSharedKeyId),
76✔
738
      pskSearch,
76✔
739
      cs,
76✔
740
      zeroes,
76✔
741
    )
76✔
742

743
    const initProposal = grouped.external_init.at(0)!
76✔
744

745
    const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
76✔
746

747
    const externalInitSecret = await importSecret(
76✔
748
      await cs.hpke.exportPrivateKey(externalKeyPair.privateKey),
76✔
749
      initProposal.proposal.externalInit.kemOutput,
76✔
750
      cs,
76✔
751
    )
76✔
752

753
    return {
76✔
754
      needsUpdatePath: true,
76✔
755
      tree: treeAfterRemove,
76✔
756
      pskSecret: updatedPskSecret,
76✔
757
      pskIds,
76✔
758
      additionalResult: {
76✔
759
        kind: "externalCommit",
76✔
760
        externalInitSecret,
76✔
761
        newMemberLeafIndex: nodeToLeafIndex(findBlankLeafNodeIndexOrExtend(treeAfterRemove)),
76✔
762
      },
76✔
763
      selfRemoved: false,
76✔
764
      allProposals,
76✔
765
    }
76✔
766
  }
76✔
767
}
5,298✔
768

769
export function makePskIndex(state: ClientState | undefined, externalPsks: Record<string, Uint8Array>): PskIndex {
1✔
770
  return {
1,477✔
771
    findPsk(preSharedKeyId) {
1,477✔
772
      if (preSharedKeyId.psktype === "external") {
441✔
773
        return externalPsks[bytesToBase64(preSharedKeyId.pskId)]
261✔
774
      }
261✔
775

776
      if (state !== undefined && constantTimeEqual(preSharedKeyId.pskGroupId, state.groupContext.groupId)) {
441✔
777
        if (preSharedKeyId.pskEpoch === state.groupContext.epoch) return state.keySchedule.resumptionPsk
180✔
778
        else return state.historicalReceiverData.get(preSharedKeyId.pskEpoch)?.resumptionPsk
28✔
779
      }
180✔
780
    },
441✔
781
  }
1,477✔
782
}
1,477✔
783

784
export async function nextEpochContext(
4,713✔
785
  groupContext: GroupContext,
4,713✔
786
  wireformat: WireformatName,
4,713✔
787
  content: FramedContentCommit,
4,713✔
788
  signature: Uint8Array,
4,713✔
789
  updatedTreeHash: Uint8Array,
4,713✔
790
  confirmationTag: Uint8Array,
4,713✔
791
  h: Hash,
4,713✔
792
): Promise<GroupContext> {
4,713✔
793
  const interimTranscriptHash = await createInterimHash(groupContext.confirmedTranscriptHash, confirmationTag, h)
4,713✔
794
  const newConfirmedHash = await createConfirmedHash(interimTranscriptHash, { wireformat, content, signature }, h)
4,713✔
795

796
  return {
4,713✔
797
    ...groupContext,
4,713✔
798
    epoch: groupContext.epoch + 1n,
4,713✔
799
    treeHash: updatedTreeHash,
4,713✔
800
    confirmedTranscriptHash: newConfirmedHash,
4,713✔
801
  }
4,713✔
802
}
4,713✔
803

804
export async function joinGroup(
1,094✔
805
  welcome: Welcome,
1,094✔
806
  keyPackage: KeyPackage,
1,094✔
807
  privateKeys: PrivateKeyPackage,
1,094✔
808
  pskSearch: PskIndex,
1,094✔
809
  cs: CiphersuiteImpl,
1,094✔
810
  ratchetTree?: RatchetTree,
1,094✔
811
  resumingFromState?: ClientState,
1,094✔
812
  clientConfig: ClientConfig = defaultClientConfig,
1,094✔
813
): Promise<ClientState> {
1,094✔
814
  const res = await joinGroupWithExtensions(
1,094✔
815
    welcome,
1,094✔
816
    keyPackage,
1,094✔
817
    privateKeys,
1,094✔
818
    pskSearch,
1,094✔
819
    cs,
1,094✔
820
    ratchetTree,
1,094✔
821
    resumingFromState,
1,094✔
822
    clientConfig,
1,094✔
823
  )
1,094✔
824

825
  return res[0]
1,037✔
826
}
1,037✔
827

828
export async function joinGroupWithExtensions(
1,132✔
829
  welcome: Welcome,
1,132✔
830
  keyPackage: KeyPackage,
1,132✔
831
  privateKeys: PrivateKeyPackage,
1,132✔
832
  pskSearch: PskIndex,
1,132✔
833
  cs: CiphersuiteImpl,
1,132✔
834
  ratchetTree?: RatchetTree,
1,132✔
835
  resumingFromState?: ClientState,
1,132✔
836
  clientConfig: ClientConfig = defaultClientConfig,
1,132✔
837
): Promise<[ClientState, Extension[]]> {
1,132✔
838
  const keyPackageRef = await makeKeyPackageRef(keyPackage, cs.hash)
1,132✔
839
  const privKey = await cs.hpke.importPrivateKey(privateKeys.initPrivateKey)
1,132✔
840
  const groupSecrets = await decryptGroupSecrets(privKey, keyPackageRef, welcome, cs.hpke)
1,132✔
841

842
  if (groupSecrets === undefined) throw new CodecError("Could not decode group secrets")
1,132!
843

844
  const zeroes: Uint8Array = new Uint8Array(cs.kdf.size)
1,132✔
845

846
  const [pskSecret, pskIds] = await accumulatePskSecret(groupSecrets.psks, pskSearch, cs, zeroes)
1,132✔
847

848
  const gi = await decryptGroupInfo(welcome, groupSecrets.joinerSecret, pskSecret, cs)
1,132✔
849
  if (gi === undefined) throw new CodecError("Could not decode group info")
1,132!
850

851
  const resumptionPsk = pskIds.find((id) => id.psktype === "resumption")
1,132✔
852
  if (resumptionPsk !== undefined) {
1,132✔
853
    if (resumingFromState === undefined) throw new ValidationError("No prior state passed for resumption")
95!
854

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

857
    if (!constantTimeEqual(resumptionPsk.pskGroupId, resumingFromState.groupContext.groupId))
95✔
858
      throw new ValidationError("old groupId mismatch")
95!
859

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

862
    if (resumptionPsk.usage === "reinit") {
95✔
863
      if (resumingFromState.groupActiveState.kind !== "suspendedPendingReinit")
76✔
864
        throw new ValidationError("Found reinit psk but no old suspended clientState")
76!
865

866
      if (!constantTimeEqual(resumingFromState.groupActiveState.reinit.groupId, gi.groupContext.groupId))
76✔
867
        throw new ValidationError("new groupId mismatch")
76✔
868

869
      if (resumingFromState.groupActiveState.reinit.version !== gi.groupContext.version)
57✔
870
        throw new ValidationError("Version mismatch")
76✔
871

872
      if (resumingFromState.groupActiveState.reinit.cipherSuite !== gi.groupContext.cipherSuite)
38✔
873
        throw new ValidationError("Ciphersuite mismatch")
76✔
874

875
      if (!extensionsEqual(resumingFromState.groupActiveState.reinit.extensions, gi.groupContext.extensions))
38✔
876
        throw new ValidationError("Extensions mismatch")
38✔
877
    }
76✔
878
  }
95✔
879

880
  const allExtensionsSupported = extensionsSupportedByCapabilities(
1,075✔
881
    gi.groupContext.extensions,
1,075✔
882
    keyPackage.leafNode.capabilities,
1,075✔
883
  )
1,075✔
884
  if (!allExtensionsSupported) throw new UsageError("client does not support every extension in the GroupContext")
1,132✔
885

886
  const tree = ratchetTreeFromExtension(gi) ?? ratchetTree
1,075✔
887

888
  if (tree === undefined) throw new UsageError("No RatchetTree passed and no ratchet_tree extension")
1,132✔
889

890
  const signerNode = tree[leafToNodeIndex(toLeafIndex(gi.signer))]
1,075✔
891

892
  if (signerNode === undefined) {
1,132!
UNCOV
893
    throw new ValidationError("Could not find signer leafNode")
×
UNCOV
894
  }
✔
895
  if (signerNode.nodeType === "parent") throw new ValidationError("Expected non blank leaf node")
1,132✔
896

897
  const credentialVerified = await clientConfig.authService.validateCredential(
1,075✔
898
    signerNode.leaf.credential,
1,075✔
899
    signerNode.leaf.signaturePublicKey,
1,075✔
900
  )
1,075✔
901

902
  if (!credentialVerified) throw new ValidationError("Could not validate credential")
1,132✔
903

904
  const groupInfoSignatureVerified = await verifyGroupInfoSignature(
1,075✔
905
    gi,
1,075✔
906
    signerNode.leaf.signaturePublicKey,
1,075✔
907
    cs.signature,
1,075✔
908
  )
1,075✔
909

910
  if (!groupInfoSignatureVerified) throw new CryptoVerificationError("Could not verify groupInfo signature")
1,132✔
911

912
  if (gi.groupContext.cipherSuite !== keyPackage.cipherSuite)
1,075✔
913
    throw new ValidationError("cipher suite in the GroupInfo does not match the cipher_suite in the KeyPackage")
1,132✔
914

915
  throwIfDefined(
1,075✔
916
    await validateRatchetTree(
1,075✔
917
      tree,
1,075✔
918
      gi.groupContext,
1,075✔
919
      clientConfig.lifetimeConfig,
1,075✔
920
      clientConfig.authService,
1,075✔
921
      gi.groupContext.treeHash,
1,075✔
922
      cs,
1,075✔
923
    ),
1,075✔
924
  )
1,075✔
925

926
  const newLeaf = findLeafIndex(tree, keyPackage.leafNode)
1,075✔
927

928
  if (newLeaf === undefined) throw new ValidationError("Could not find own leaf when processing welcome")
1,132✔
929

930
  const privateKeyPath: PrivateKeyPath = {
1,075✔
931
    leafIndex: newLeaf,
1,075✔
932
    privateKeys: { [leafToNodeIndex(newLeaf)]: privateKeys.hpkePrivateKey },
1,075✔
933
  }
1,075✔
934

935
  const ancestorNodeIndex = firstCommonAncestor(tree, newLeaf, toLeafIndex(gi.signer))
1,075✔
936

937
  const updatedPkp =
1,075✔
938
    groupSecrets.pathSecret === undefined
1,075✔
939
      ? privateKeyPath
1,019✔
940
      : mergePrivateKeyPaths(
56✔
941
          await toPrivateKeyPath(
56✔
942
            await pathToRoot(tree, ancestorNodeIndex, groupSecrets.pathSecret, cs.kdf),
56✔
943
            newLeaf,
56✔
944
            cs,
56✔
945
          ),
56✔
946
          privateKeyPath,
56✔
947
        )
56✔
948

949
  const keySchedule = await deriveKeySchedule(groupSecrets.joinerSecret, pskSecret, gi.groupContext, cs.kdf)
1,132✔
950

951
  const confirmationTagVerified = await verifyGroupInfoConfirmationTag(gi, groupSecrets.joinerSecret, pskSecret, cs)
1,075✔
952

953
  if (!confirmationTagVerified) throw new CryptoVerificationError("Could not verify confirmation tag")
1,132✔
954

955
  const secretTree = await createSecretTree(leafWidth(tree.length), keySchedule.encryptionSecret, cs.kdf)
1,075✔
956

957
  return [
1,075✔
958
    {
1,075✔
959
      groupContext: gi.groupContext,
1,075✔
960
      ratchetTree: tree,
1,075✔
961
      privatePath: updatedPkp,
1,075✔
962
      signaturePrivateKey: privateKeys.signaturePrivateKey,
1,075✔
963
      confirmationTag: gi.confirmationTag,
1,075✔
964
      unappliedProposals: {},
1,075✔
965
      keySchedule,
1,075✔
966
      secretTree,
1,075✔
967
      historicalReceiverData: new Map(),
1,075✔
968
      groupActiveState: { kind: "active" },
1,075✔
969
      clientConfig,
1,075✔
970
    },
1,075✔
971
    gi.extensions,
1,075✔
972
  ]
1,075✔
973
}
1,075✔
974

975
export async function createGroup(
854✔
976
  groupId: Uint8Array,
854✔
977
  keyPackage: KeyPackage,
854✔
978
  privateKeyPackage: PrivateKeyPackage,
854✔
979
  extensions: Extension[],
854✔
980
  cs: CiphersuiteImpl,
854✔
981
  clientConfig: ClientConfig = defaultClientConfig,
854✔
982
): Promise<ClientState> {
854✔
983
  const ratchetTree: RatchetTree = [{ nodeType: "leaf", leaf: keyPackage.leafNode }]
854✔
984

985
  const privatePath: PrivateKeyPath = {
854✔
986
    leafIndex: 0,
854✔
987
    privateKeys: { [0]: privateKeyPackage.hpkePrivateKey },
854✔
988
  }
854✔
989

990
  const confirmedTranscriptHash = new Uint8Array()
854✔
991

992
  const groupContext: GroupContext = {
854✔
993
    version: "mls10",
854✔
994
    cipherSuite: cs.name,
854✔
995
    epoch: 0n,
854✔
996
    treeHash: await treeHashRoot(ratchetTree, cs.hash),
854✔
997
    groupId,
854✔
998
    extensions,
854✔
999
    confirmedTranscriptHash,
854✔
1000
  }
854✔
1001

1002
  throwIfDefined(await validateExternalSenders(extensions, clientConfig.authService))
854✔
1003

1004
  const epochSecret = cs.rng.randomBytes(cs.kdf.size)
854✔
1005

1006
  const keySchedule = await initializeKeySchedule(epochSecret, cs.kdf)
854✔
1007

1008
  const confirmationTag = await createConfirmationTag(keySchedule.confirmationKey, confirmedTranscriptHash, cs.hash)
854✔
1009

1010
  const secretTree = await createSecretTree(1, keySchedule.encryptionSecret, cs.kdf)
854✔
1011

1012
  return {
854✔
1013
    ratchetTree,
854✔
1014
    keySchedule,
854✔
1015
    secretTree,
854✔
1016
    privatePath,
854✔
1017
    signaturePrivateKey: privateKeyPackage.signaturePrivateKey,
854✔
1018
    unappliedProposals: {},
854✔
1019
    historicalReceiverData: new Map(),
854✔
1020
    groupContext,
854✔
1021
    confirmationTag,
854✔
1022
    groupActiveState: { kind: "active" },
854✔
1023
    clientConfig,
854✔
1024
  }
854✔
1025
}
854✔
1026

1027
export async function exportSecret(
152✔
1028
  publicKey: Uint8Array,
152✔
1029
  cs: CiphersuiteImpl,
152✔
1030
): Promise<{ enc: Uint8Array; secret: Uint8Array }> {
152✔
1031
  return cs.hpke.exportSecret(
152✔
1032
    await cs.hpke.importPublicKey(publicKey),
152✔
1033
    new TextEncoder().encode("MLS 1.0 external init secret"),
152✔
1034
    cs.kdf.size,
152✔
1035
    new Uint8Array(),
152✔
1036
  )
152✔
1037
}
152✔
1038

1039
async function importSecret(privateKey: Uint8Array, kemOutput: Uint8Array, cs: CiphersuiteImpl): Promise<Uint8Array> {
76✔
1040
  return cs.hpke.importSecret(
76✔
1041
    await cs.hpke.importPrivateKey(privateKey),
76✔
1042
    new TextEncoder().encode("MLS 1.0 external init secret"),
76✔
1043
    kemOutput,
76✔
1044
    cs.kdf.size,
76✔
1045
    new Uint8Array(),
76✔
1046
  )
76✔
1047
}
76✔
1048

1049
async function applyTreeMutations(
4,887✔
1050
  ratchetTree: RatchetTree,
4,887✔
1051
  grouped: Proposals,
4,887✔
1052
  gc: GroupContext,
4,887✔
1053
  sentByClient: boolean,
4,887✔
1054
  authService: AuthenticationService,
4,887✔
1055
  lifetimeConfig: LifetimeConfig,
4,887✔
1056
  s: Signature,
4,887✔
1057
): Promise<[RatchetTree, [LeafIndex, KeyPackage][]]> {
4,887✔
1058
  const treeAfterUpdate = await grouped.update.reduce(async (acc, { senderLeafIndex, proposal }) => {
4,887✔
1059
    if (senderLeafIndex === undefined) throw new InternalError("No sender index found for update proposal")
14!
1060

1061
    throwIfDefined(await validateLeafNodeUpdateOrCommit(proposal.update.leafNode, senderLeafIndex, gc, authService, s))
14✔
1062
    throwIfDefined(
14✔
1063
      await validateLeafNodeCredentialAndKeyUniqueness(ratchetTree, proposal.update.leafNode, senderLeafIndex),
14✔
1064
    )
14✔
1065

1066
    return updateLeafNode(await acc, proposal.update.leafNode, toLeafIndex(senderLeafIndex))
14✔
1067
  }, Promise.resolve(ratchetTree))
4,887✔
1068

1069
  const treeAfterRemove = grouped.remove.reduce((acc, { proposal }) => {
4,887✔
1070
    throwIfDefined(validateRemove(proposal.remove, ratchetTree))
2,425✔
1071

1072
    return removeLeafNode(acc, toLeafIndex(proposal.remove.removed))
2,425✔
1073
  }, treeAfterUpdate)
4,887✔
1074

1075
  const [treeAfterAdd, addedLeafNodes] = await grouped.add.reduce(
4,887✔
1076
    async (acc, { proposal }) => {
4,887✔
1077
      throwIfDefined(
3,109✔
1078
        await validateKeyPackage(
3,109✔
1079
          proposal.add.keyPackage,
3,109✔
1080
          gc,
3,109✔
1081
          ratchetTree,
3,109✔
1082
          sentByClient,
3,109✔
1083
          lifetimeConfig,
3,109✔
1084
          authService,
3,109✔
1085
          s,
3,109✔
1086
        ),
3,109✔
1087
      )
3,109✔
1088

1089
      const [tree, ws] = await acc
3,109✔
1090
      const [updatedTree, leafNodeIndex] = addLeafNode(tree, proposal.add.keyPackage.leafNode)
3,030✔
1091
      return [
3,030✔
1092
        updatedTree,
3,030✔
1093
        [...ws, [nodeToLeafIndex(leafNodeIndex), proposal.add.keyPackage] as [LeafIndex, KeyPackage]],
3,030✔
1094
      ]
3,030✔
1095
    },
3,109✔
1096
    Promise.resolve([treeAfterRemove, []] as [RatchetTree, [LeafIndex, KeyPackage][]]),
4,887✔
1097
  )
4,887✔
1098

1099
  return [treeAfterAdd, addedLeafNodes]
4,808✔
1100
}
4,808✔
1101

1102
export async function processProposal(
1,835✔
1103
  state: ClientState,
1,835✔
1104
  content: AuthenticatedContent,
1,835✔
1105
  proposal: Proposal,
1,835✔
1106
  h: Hash,
1,835✔
1107
): Promise<ClientState> {
1,835✔
1108
  const ref = await makeProposalRef(content, h)
1,835✔
1109
  return {
1,835✔
1110
    ...state,
1,835✔
1111
    unappliedProposals: addUnappliedProposal(
1,835✔
1112
      ref,
1,835✔
1113
      state.unappliedProposals,
1,835✔
1114
      proposal,
1,835✔
1115
      getSenderLeafNodeIndex(content.content.sender),
1,835✔
1116
    ),
1,835✔
1117
  }
1,835✔
1118
}
1,835✔
1119

1120
export function addHistoricalReceiverData(state: ClientState): Map<bigint, EpochReceiverData> {
1✔
1121
  const withNew = addToMap(state.historicalReceiverData, state.groupContext.epoch, {
5,150✔
1122
    secretTree: state.secretTree,
5,150✔
1123
    ratchetTree: state.ratchetTree,
5,150✔
1124
    senderDataSecret: state.keySchedule.senderDataSecret,
5,150✔
1125
    groupContext: state.groupContext,
5,150✔
1126
    resumptionPsk: state.keySchedule.resumptionPsk,
5,150✔
1127
  })
5,150✔
1128

1129
  const epochs = [...withNew.keys()]
5,150✔
1130

1131
  const result =
5,150✔
1132
    epochs.length >= state.clientConfig.keyRetentionConfig.retainKeysForEpochs
5,150✔
1133
      ? removeOldHistoricalReceiverData(withNew, state.clientConfig.keyRetentionConfig.retainKeysForEpochs)
2,762✔
1134
      : withNew
2,388✔
1135

1136
  return result
5,150✔
1137
}
5,150✔
1138

1139
function removeOldHistoricalReceiverData(
2,762✔
1140
  historicalReceiverData: Map<bigint, EpochReceiverData>,
2,762✔
1141
  max: number,
2,762✔
1142
): Map<bigint, EpochReceiverData> {
2,762✔
1143
  const sortedEpochs = [...historicalReceiverData.keys()].sort((a, b) => (a < b ? -1 : 1))
2,762✔
1144

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