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

LukaJCB / ts-mls / 20887934276

11 Jan 2026 02:12AM UTC coverage: 95.727% (+0.06%) from 95.665%
20887934276

push

github

LukaJCB
Update to vitest v4

409 of 417 branches covered (98.08%)

Branch coverage included in aggregate %.

2369 of 2485 relevant lines covered (95.33%)

80736.06 hits per line

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

97.67
/src/messageProtection.ts
1
import { AuthenticatedContent, makeProposalRef } from "./authenticatedContent.js"
2
import { CiphersuiteImpl } from "./crypto/ciphersuite.js"
3
import {
4
  FramedContentTBSApplicationOrProposal,
5
  signFramedContentApplicationOrProposal,
6
  verifyFramedContentSignature,
7
} from "./framedContent.js"
8
import { GroupContext } from "./groupContext.js"
9
import { Proposal } from "./proposal.js"
10
import {
11
  decodePrivateMessageContent,
12
  decryptSenderData,
13
  encodePrivateMessageContent,
14
  encryptSenderData,
15
  PrivateContentAAD,
16
  privateContentAADEncoder,
17
  PrivateMessage,
18
  PrivateMessageContent,
19
  toAuthenticatedContent,
20
} from "./privateMessage.js"
21
import { consumeRatchet, ratchetToGeneration, SecretTree } from "./secretTree.js"
22
import { getSignaturePublicKeyFromLeafIndex, RatchetTree } from "./ratchetTree.js"
23
import { SenderData, SenderDataAAD } from "./sender.js"
24
import { leafToNodeIndex, toLeafIndex } from "./treemath.js"
25
import { KeyRetentionConfig } from "./keyRetentionConfig.js"
26
import { CryptoVerificationError, CodecError, ValidationError, MlsError, InternalError } from "./mlsError.js"
27
import { PaddingConfig } from "./paddingConfig.js"
28
import { encode } from "./codec/tlsEncoder.js"
29

30
export interface ProtectApplicationDataResult {
31
  privateMessage: PrivateMessage
32
  newSecretTree: SecretTree
33
  consumed: Uint8Array[]
34
}
35

36
export async function protectApplicationData(
37
  signKey: Uint8Array,
38
  senderDataSecret: Uint8Array,
39
  applicationData: Uint8Array,
40
  authenticatedData: Uint8Array,
41
  groupContext: GroupContext,
42
  secretTree: SecretTree,
43
  leafIndex: number,
44
  paddingConfig: PaddingConfig,
45
  cs: CiphersuiteImpl,
46
): Promise<ProtectApplicationDataResult> {
47
  const tbs: FramedContentTBSApplicationOrProposal = {
2,458✔
48
    protocolVersion: groupContext.version,
49
    wireformat: "mls_private_message",
50
    content: {
51
      contentType: "application",
52
      applicationData,
53
      groupId: groupContext.groupId,
54
      epoch: groupContext.epoch,
55
      sender: {
56
        senderType: "member",
57
        leafIndex: leafIndex,
58
      },
59
      authenticatedData,
60
    },
61
    senderType: "member",
62
    context: groupContext,
63
  }
64

65
  const auth = await signFramedContentApplicationOrProposal(signKey, tbs, cs)
2,458✔
66

67
  const content = {
2,458✔
68
    ...tbs.content,
69
    auth,
70
  }
71

72
  const result = await protect(
2,458✔
73
    senderDataSecret,
74
    authenticatedData,
75
    groupContext,
76
    secretTree,
77
    content,
78
    leafIndex,
79
    paddingConfig,
80
    cs,
81
  )
82

83
  return { newSecretTree: result.tree, privateMessage: result.privateMessage, consumed: result.consumed }
2,458✔
84
}
85

86
export interface ProtectProposalResult {
87
  privateMessage: PrivateMessage
88
  newSecretTree: SecretTree
89
  proposalRef: Uint8Array
90
  consumed: Uint8Array[]
91
}
92

93
export async function protectProposal(
94
  signKey: Uint8Array,
95
  senderDataSecret: Uint8Array,
96
  p: Proposal,
97
  authenticatedData: Uint8Array,
98
  groupContext: GroupContext,
99
  secretTree: SecretTree,
100
  leafIndex: number,
101
  paddingConfig: PaddingConfig,
102
  cs: CiphersuiteImpl,
103
): Promise<ProtectProposalResult> {
104
  const tbs = {
83✔
105
    protocolVersion: groupContext.version,
106
    wireformat: "mls_private_message" as const,
107
    content: {
108
      contentType: "proposal" as const,
109
      proposal: p,
110
      groupId: groupContext.groupId,
111
      epoch: groupContext.epoch,
112
      sender: {
113
        senderType: "member" as const,
114
        leafIndex,
115
      },
116
      authenticatedData,
117
    },
118
    senderType: "member" as const,
119
    context: groupContext,
120
  }
121

122
  const auth = await signFramedContentApplicationOrProposal(signKey, tbs, cs)
83✔
123
  const content = { ...tbs.content, auth }
83✔
124

125
  const protectResult = await protect(
83✔
126
    senderDataSecret,
127
    authenticatedData,
128
    groupContext,
129
    secretTree,
130
    content,
131
    leafIndex,
132
    paddingConfig,
133
    cs,
134
  )
135

136
  const newSecretTree = protectResult.tree
83✔
137

138
  const authenticatedContent = {
83✔
139
    wireformat: "mls_private_message" as const,
140
    content,
141
    auth,
142
  }
143
  const proposalRef = await makeProposalRef(authenticatedContent, cs.hash)
83✔
144

145
  return { privateMessage: protectResult.privateMessage, newSecretTree, proposalRef, consumed: protectResult.consumed }
83✔
146
}
147

148
export interface ProtectResult {
149
  privateMessage: PrivateMessage
150
  tree: SecretTree
151
  consumed: Uint8Array[]
152
}
153

154
export async function protect(
155
  senderDataSecret: Uint8Array,
156
  authenticatedData: Uint8Array,
157
  groupContext: GroupContext,
158
  secretTree: SecretTree,
159
  content: PrivateMessageContent,
160
  leafIndex: number,
161
  config: PaddingConfig,
162
  cs: CiphersuiteImpl,
163
): Promise<ProtectResult> {
164
  const node = secretTree[leafToNodeIndex(toLeafIndex(leafIndex))]
4,321✔
165
  if (node === undefined) throw new InternalError("Bad node index for secret tree")
4,321✔
166

167
  const { newTree, generation, reuseGuard, nonce, key, consumed } = await consumeRatchet(
4,321✔
168
    secretTree,
169
    leafToNodeIndex(toLeafIndex(leafIndex)),
170
    content.contentType,
171
    cs,
172
  )
173

174
  const aad: PrivateContentAAD = {
4,321✔
175
    groupId: groupContext.groupId,
176
    epoch: groupContext.epoch,
177
    contentType: content.contentType,
178
    authenticatedData: authenticatedData,
179
  }
180

181
  const ciphertext = await cs.hpke.encryptAead(
4,321✔
182
    key,
183
    nonce,
184
    encode(privateContentAADEncoder)(aad),
185
    encodePrivateMessageContent(config)(content),
186
  )
187

188
  const senderData: SenderData = {
4,321✔
189
    leafIndex,
190
    generation,
191
    reuseGuard,
192
  }
193

194
  const senderAad: SenderDataAAD = {
4,321✔
195
    groupId: groupContext.groupId,
196
    epoch: groupContext.epoch,
197
    contentType: content.contentType,
198
  }
199

200
  const encryptedSenderData = await encryptSenderData(senderDataSecret, senderData, senderAad, ciphertext, cs)
4,321✔
201

202
  return {
4,321✔
203
    privateMessage: {
204
      groupId: groupContext.groupId,
205
      epoch: groupContext.epoch,
206
      encryptedSenderData,
207
      contentType: content.contentType,
208
      authenticatedData,
209
      ciphertext,
210
    },
211
    tree: newTree,
212
    consumed,
213
  }
214
}
215

216
export interface UnprotectResult {
217
  content: AuthenticatedContent
218
  tree: SecretTree
219
  consumed: Uint8Array[]
220
}
221

222
export async function unprotectPrivateMessage(
223
  senderDataSecret: Uint8Array,
224
  msg: PrivateMessage,
225
  secretTree: SecretTree,
226
  ratchetTree: RatchetTree,
227
  groupContext: GroupContext,
228
  config: KeyRetentionConfig,
229
  cs: CiphersuiteImpl,
230
  overrideSignatureKey?: Uint8Array,
231
): Promise<UnprotectResult> {
232
  const senderData = await decryptSenderData(msg, senderDataSecret, cs)
9,020✔
233

234
  if (senderData === undefined) throw new CodecError("Could not decode senderdata")
9,020✔
235

236
  validateSenderData(senderData, ratchetTree)
9,020✔
237

238
  const { key, nonce, newTree, consumed } = await ratchetToGeneration(
9,020✔
239
    secretTree,
240
    senderData,
241
    msg.contentType,
242
    config,
243
    cs,
244
  )
245

246
  const aad: PrivateContentAAD = {
9,001✔
247
    groupId: msg.groupId,
248
    epoch: msg.epoch,
249
    contentType: msg.contentType,
250
    authenticatedData: msg.authenticatedData,
251
  }
252

253
  const decrypted = await cs.hpke.decryptAead(key, nonce, encode(privateContentAADEncoder)(aad), msg.ciphertext)
9,001✔
254

255
  const pmc = decodePrivateMessageContent(msg.contentType)(decrypted, 0)?.[0]
8,963✔
256

257
  if (pmc === undefined) throw new CodecError("Could not decode PrivateMessageContent")
9,020✔
258

259
  const content = toAuthenticatedContent(pmc, msg, senderData.leafIndex)
8,963✔
260

261
  const signaturePublicKey =
262
    overrideSignatureKey !== undefined
8,963✔
263
      ? overrideSignatureKey
264
      : getSignaturePublicKeyFromLeafIndex(ratchetTree, toLeafIndex(senderData.leafIndex))
265

266
  const signatureValid = await verifyFramedContentSignature(
9,020✔
267
    signaturePublicKey,
268
    "mls_private_message",
269
    content.content,
270
    content.auth,
271
    groupContext,
272
    cs.signature,
273
  )
274

275
  if (!signatureValid) throw new CryptoVerificationError("Signature invalid")
8,963✔
276

277
  return { tree: newTree, content, consumed }
8,963✔
278
}
279

280
export function validateSenderData(senderData: SenderData, tree: RatchetTree): MlsError | undefined {
281
  if (tree[leafToNodeIndex(toLeafIndex(senderData.leafIndex))]?.nodeType !== "leaf")
9,020✔
282
    return new ValidationError("SenderData did not point to a non-blank leaf node")
×
283
}
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