lemon-tls 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/record.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * record.js — Shared record-layer primitives for TLS 1.2 and TLS 1.3.
2
+ * record.js — Record-layer primitives for TLS 1.2, TLS 1.3, DTLS 1.2, and DTLS 1.3.
3
3
  *
4
- * Used by TLSSocket, DTLSSocket, and test harnesses.
4
+ * Used by TLSSocket, DTLSSession, DTLSSocket, and test harnesses.
5
5
  * Handles AEAD encryption/decryption, nonce construction, key derivation,
6
- * and raw record framing.
6
+ * raw record framing, and DTLS-specific record number encryption.
7
7
  */
8
8
 
9
9
  import crypto from 'node:crypto';
@@ -12,6 +12,11 @@ import {
12
12
  hkdf_expand_label,
13
13
  tls_derive_from_master_secret_tls12
14
14
  } from './crypto.js';
15
+ import {
16
+ w_u8,
17
+ w_u16,
18
+ w_u48,
19
+ } from './wire.js';
15
20
 
16
21
  // ===================== AEAD algorithm resolution =====================
17
22
 
@@ -37,46 +42,189 @@ function deriveKeys(trafficSecret, cipherSuite) {
37
42
  };
38
43
  }
39
44
 
40
- /** TLS 1.3 nonce: IV XOR zero-padded 64-bit sequence number. */
45
+ /**
46
+ * TLS 1.3 nonce: IV XOR zero-padded 64-bit sequence number.
47
+ *
48
+ * Perf-critical: called on every encrypted record. Uses Number arithmetic
49
+ * (safe for seq < 2^53, vastly exceeds practical TLS limits) instead of BigInt,
50
+ * which is ~20x slower in V8. Single allocation, unrolled XOR loop.
51
+ */
41
52
  function getNonce(iv, seq) {
42
- const seqBuf = new Uint8Array(12);
43
- const view = new DataView(seqBuf.buffer);
44
- view.setBigUint64(4, BigInt(seq));
45
53
  const nonce = new Uint8Array(12);
46
- for (let i = 0; i < 12; i++) nonce[i] = iv[i] ^ seqBuf[i];
54
+ // High 4 bytes of IV are XOR'd with zeros (seq top bits) — just copy
55
+ nonce[0] = iv[0]; nonce[1] = iv[1]; nonce[2] = iv[2]; nonce[3] = iv[3];
56
+ // Split seq into hi/lo 32-bit halves (seq < 2^53 is safe)
57
+ const hi = (seq / 0x100000000) | 0;
58
+ const lo = seq >>> 0;
59
+ nonce[4] = iv[4] ^ ((hi >>> 24) & 0xff);
60
+ nonce[5] = iv[5] ^ ((hi >>> 16) & 0xff);
61
+ nonce[6] = iv[6] ^ ((hi >>> 8) & 0xff);
62
+ nonce[7] = iv[7] ^ ( hi & 0xff);
63
+ nonce[8] = iv[8] ^ ((lo >>> 24) & 0xff);
64
+ nonce[9] = iv[9] ^ ((lo >>> 16) & 0xff);
65
+ nonce[10] = iv[10] ^ ((lo >>> 8) & 0xff);
66
+ nonce[11] = iv[11] ^ ( lo & 0xff);
47
67
  return nonce;
48
68
  }
49
69
 
50
70
  /**
51
- * Encrypt a TLS 1.3 record (TLSInnerPlaintext).
71
+ * Same as getNonce but writes into a provided output buffer instead of
72
+ * allocating a fresh one. Returns the same buffer for chaining.
73
+ *
74
+ * Use this when you have a per-connection reusable nonce scratch buffer —
75
+ * TLS processing is synchronous per connection, and Node's crypto.createCipheriv
76
+ * copies the nonce into OpenSSL state, so we can safely reuse the same buffer
77
+ * across records.
78
+ *
79
+ * Saves 12 bytes × records of allocations. For a 10MB transfer (640 records)
80
+ * that's ~7.7KB of garbage eliminated.
81
+ */
82
+ function getNonceInto(out, iv, seq) {
83
+ out[0] = iv[0]; out[1] = iv[1]; out[2] = iv[2]; out[3] = iv[3];
84
+ const hi = (seq / 0x100000000) | 0;
85
+ const lo = seq >>> 0;
86
+ out[4] = iv[4] ^ ((hi >>> 24) & 0xff);
87
+ out[5] = iv[5] ^ ((hi >>> 16) & 0xff);
88
+ out[6] = iv[6] ^ ((hi >>> 8) & 0xff);
89
+ out[7] = iv[7] ^ ( hi & 0xff);
90
+ out[8] = iv[8] ^ ((lo >>> 24) & 0xff);
91
+ out[9] = iv[9] ^ ((lo >>> 16) & 0xff);
92
+ out[10] = iv[10] ^ ((lo >>> 8) & 0xff);
93
+ out[11] = iv[11] ^ ( lo & 0xff);
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * Encrypt a TLS 1.3 record (TLSInnerPlaintext = plaintext || inner_content_type).
52
99
  * Returns ciphertext || tag (without record header).
100
+ *
101
+ * Perf: uses two cipher.update() calls (plaintext, then 1-byte inner type) instead
102
+ * of allocating and copying a +1-byte buffer. For 16KB records this saves ~16KB
103
+ * of allocation and copy per encrypted record.
53
104
  */
54
105
  function encryptRecord(innerType, plaintext, key, nonce, algo) {
55
- const full = new Uint8Array(plaintext.length + 1);
56
- full.set(plaintext);
57
- full[plaintext.length] = innerType;
58
-
59
- const recLen = full.length + 16;
60
- const aad = new Uint8Array([0x17, 0x03, 0x03, (recLen >>> 8) & 0xff, recLen & 0xff]);
106
+ const ptLen = plaintext.length;
107
+ const recLen = ptLen + 1 + 16; // plaintext + inner_type + tag
108
+ const aad = new Uint8Array(5);
109
+ aad[0] = 0x17; aad[1] = 0x03; aad[2] = 0x03;
110
+ aad[3] = (recLen >>> 8) & 0xff;
111
+ aad[4] = recLen & 0xff;
61
112
 
62
113
  if (!algo) algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
63
- let isChaCha = algo === 'chacha20-poly1305';
64
- let cipher = crypto.createCipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
65
- cipher.setAAD(aad, isChaCha ? { plaintextLength: full.length } : undefined);
66
- let ct = cipher.update(full);
114
+ const isChaCha = algo === 'chacha20-poly1305';
115
+ const cipher = crypto.createCipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
116
+ cipher.setAAD(aad, isChaCha ? { plaintextLength: ptLen + 1 } : undefined);
117
+
118
+ // Stream: plaintext first, then the 1-byte inner content type
119
+ const ct1 = cipher.update(plaintext);
120
+ // Reuse a single-byte scratch for inner type — tiny allocation, unavoidable
121
+ const innerBuf = new Uint8Array(1);
122
+ innerBuf[0] = innerType;
123
+ const ct2 = cipher.update(innerBuf);
67
124
  cipher.final();
68
- let tag = cipher.getAuthTag();
125
+ const tag = cipher.getAuthTag();
69
126
 
70
- let out = new Uint8Array(ct.length + tag.length);
71
- out.set(ct, 0);
72
- out.set(tag, ct.length);
127
+ // AES-GCM and ChaCha20-Poly1305 are stream ciphers → ct1.length + ct2.length === ptLen + 1
128
+ const out = new Uint8Array(ct1.length + ct2.length + tag.length);
129
+ out.set(ct1, 0);
130
+ if (ct2.length > 0) out.set(ct2, ct1.length);
131
+ out.set(tag, ct1.length + ct2.length);
73
132
  return out;
74
133
  }
75
134
 
135
+ // Pre-allocated single-byte buffers for inner content type. Avoids allocating a
136
+ // new 1-byte Uint8Array for every encrypted record (25 bytes × records adds up
137
+ // in garbage — 640 records for 10MB = 16KB of pointless allocations).
138
+ const _INNER_TYPE_BUFS = new Array(256);
139
+ for (let i = 0; i < 256; i++) {
140
+ const b = new Uint8Array(1);
141
+ b[0] = i;
142
+ _INNER_TYPE_BUFS[i] = b;
143
+ }
144
+
145
+ /**
146
+ * Encrypt a TLS 1.3 record directly into a complete TLS record buffer (5-byte
147
+ * header + ciphertext + inner type + tag), ready to hand to transport.write().
148
+ *
149
+ * Single-allocation + single-update hot-path version — the biggest throughput
150
+ * optimization we have for TLS 1.3 bulk data.
151
+ *
152
+ * Strategy:
153
+ * 1. Allocate rec of the full final size upfront.
154
+ * 2. Write record header (5 bytes) — also serves as AAD (TLS 1.3 AAD == header).
155
+ * 3. Stage plaintext + inner_type byte into rec[5 .. 5+ptLen+1]. This is a
156
+ * 16KB copy we pay for up front but it lets us call cipher.update ONCE
157
+ * with a single contiguous input.
158
+ * 4. cipher.setAAD(rec.subarray(0, 5)) — view, no alloc.
159
+ * 5. cipher.update(rec.subarray(5, 5+ptLen+1)) — one call, returns ct of
160
+ * same size (AES-GCM is a stream cipher). We then copy ct back into rec,
161
+ * overwriting the staged plaintext with ciphertext.
162
+ * 6. Write the 16-byte auth tag at the end.
163
+ *
164
+ * Why single-update beats two updates (plaintext then [inner_type]):
165
+ * - Each cipher.update call crosses the JS↔C boundary (~1-3μs overhead).
166
+ * For 640 records per 10MB transfer, skipping one call per record saves
167
+ * ~0.6-2ms per transfer.
168
+ * - Avoids the small Buffer object wrapper Node creates for the 1-byte ct2.
169
+ *
170
+ * Why the extra plaintext→rec copy is cheap:
171
+ * - memcpy throughput is ~20GB/s → a 16KB copy is ~800ns.
172
+ * - For 640 records, total extra copy time is ~500μs, dominated by the
173
+ * 2-4ms of cipher.update overhead we avoid.
174
+ *
175
+ * Net savings per record: ~1-2μs. Per 10MB transfer: ~1-2ms on encryption
176
+ * hot path — meaningful on top of the ~25ms actual AES work.
177
+ *
178
+ * Note: outer record type for encrypted TLS 1.3 records is ALWAYS 0x17
179
+ * (application_data) regardless of inner type — the real type is encrypted
180
+ * in the inner byte.
181
+ */
182
+ function encryptCompleteRecord13(innerType, plaintext, key, nonce, algo, version) {
183
+ const ptLen = plaintext.length;
184
+ const payloadLen = ptLen + 1 + 16; // inner_type + tag
185
+ const ver = version || 0x0303;
186
+
187
+ const rec = Buffer.allocUnsafe(5 + payloadLen);
188
+ // Record header (also used as AAD)
189
+ rec[0] = 0x17; // outer type always application_data for encrypted records
190
+ rec[1] = (ver >>> 8) & 0xff;
191
+ rec[2] = ver & 0xff;
192
+ rec[3] = (payloadLen >>> 8) & 0xff;
193
+ rec[4] = payloadLen & 0xff;
194
+
195
+ // Stage plaintext + inner_type byte at rec[5 .. 5+ptLen+1].
196
+ // cipher.update will read from this view, and we'll overwrite it with
197
+ // ciphertext immediately after.
198
+ if (plaintext.length > 0) {
199
+ // Buffer.prototype.copy and Uint8Array.set both work; .set is faster for Uint8Array src.
200
+ if (plaintext.copy) plaintext.copy(rec, 5);
201
+ else rec.set(plaintext, 5);
202
+ }
203
+ rec[5 + ptLen] = innerType & 0xff;
204
+
205
+ if (!algo) algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
206
+ const isChaCha = algo === 'chacha20-poly1305';
207
+ const cipher = crypto.createCipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
208
+ cipher.setAAD(rec.subarray(0, 5), isChaCha ? { plaintextLength: ptLen + 1 } : undefined);
209
+
210
+ // Single update reading plaintext+innerType from rec, writing ciphertext
211
+ // back into rec (overwriting the staged plaintext). ct.length === ptLen + 1.
212
+ const ct = cipher.update(rec.subarray(5, 5 + ptLen + 1));
213
+ ct.copy(rec, 5);
214
+
215
+ cipher.final();
216
+ cipher.getAuthTag().copy(rec, 5 + ct.length);
217
+
218
+ return rec;
219
+ }
220
+
76
221
  /**
77
222
  * Decrypt a TLS 1.3 record.
78
223
  * Input: raw ciphertext || tag (without record header).
79
224
  * Returns full TLSInnerPlaintext (content || content_type || padding).
225
+ *
226
+ * Perf: returns Node Buffer directly (which IS a Uint8Array subclass) — avoids
227
+ * a redundant copy into a plain Uint8Array.
80
228
  */
81
229
  function decryptRecord(ciphertext, key, nonce, algo) {
82
230
  const aad = new Uint8Array(5);
@@ -84,25 +232,59 @@ function decryptRecord(ciphertext, key, nonce, algo) {
84
232
  aad[3] = (ciphertext.length >> 8) & 0xff;
85
233
  aad[4] = ciphertext.length & 0xff;
86
234
 
87
- let ct = ciphertext.subarray(0, ciphertext.length - 16);
88
- let tag = ciphertext.subarray(ciphertext.length - 16);
235
+ const ct = ciphertext.subarray(0, ciphertext.length - 16);
236
+ const tag = ciphertext.subarray(ciphertext.length - 16);
89
237
 
90
238
  if (!algo) algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
91
- let isChaCha = algo === 'chacha20-poly1305';
92
- let decipher = crypto.createDecipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
239
+ const isChaCha = algo === 'chacha20-poly1305';
240
+ const decipher = crypto.createDecipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
93
241
  decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
94
242
  decipher.setAuthTag(tag);
95
- let pt = decipher.update(ct);
243
+ const pt = decipher.update(ct);
96
244
  decipher.final();
97
- return new Uint8Array(pt);
245
+ return pt;
246
+ }
247
+
248
+ /**
249
+ * Decrypt a TLS 1.3 record using an AAD view taken directly from the record
250
+ * buffer's header — no fresh AAD allocation. Use this when the caller has
251
+ * access to the original record header bytes (e.g., parseRecordsAndDispatch
252
+ * has the full readBuffer and knows the offset of the record).
253
+ *
254
+ * aadView must be the 5 bytes that precede `ciphertext` in the original record:
255
+ * [type, version_hi, version_lo, length_hi, length_lo]
256
+ *
257
+ * Saves a 5-byte allocation per decrypt. For 640 records in a 10MB transfer,
258
+ * that's 3.2KB of avoided allocations — small on its own but part of the
259
+ * ongoing effort to reduce per-record GC pressure.
260
+ */
261
+ function decryptRecordWithAadView(aadView, ciphertext, key, nonce, algo) {
262
+ const ct = ciphertext.subarray(0, ciphertext.length - 16);
263
+ const tag = ciphertext.subarray(ciphertext.length - 16);
264
+
265
+ if (!algo) algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
266
+ const isChaCha = algo === 'chacha20-poly1305';
267
+ const decipher = crypto.createDecipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
268
+ decipher.setAAD(aadView, isChaCha ? { plaintextLength: ct.length } : undefined);
269
+ decipher.setAuthTag(tag);
270
+ const pt = decipher.update(ct);
271
+ decipher.final();
272
+ return pt;
98
273
  }
99
274
 
100
275
  /** Strip trailing zeros and extract content_type from TLSInnerPlaintext. */
276
+ /**
277
+ * Strip trailing zeros and extract content_type from TLSInnerPlaintext.
278
+ *
279
+ * Perf: returns a `subarray` (zero-copy view) rather than `slice`. When `data` is
280
+ * a Node Buffer these are equivalent, but being explicit makes the behaviour
281
+ * uniform across Buffer / Uint8Array inputs and documents the intent.
282
+ */
101
283
  function parseInnerPlaintext(data) {
102
284
  let j = data.length - 1;
103
285
  while (j >= 0 && data[j] === 0) j--;
104
286
  if (j < 0) throw new Error('Malformed TLSInnerPlaintext');
105
- return { type: data[j], content: data.slice(0, j) };
287
+ return { type: data[j], content: data.subarray(0, j) };
106
288
  }
107
289
 
108
290
  // ===================== TLS 1.2 primitives =====================
@@ -115,18 +297,41 @@ function getNonce12(salt4, explicit8) {
115
297
  return out;
116
298
  }
117
299
 
118
- /** Encode sequence number as 8-byte big-endian. */
300
+ /**
301
+ * Encode sequence number as 8-byte big-endian.
302
+ * Perf: uses Number arithmetic (seq < 2^53 is always safe for TLS).
303
+ */
119
304
  function seqToBytes(seq) {
120
305
  const buf = new Uint8Array(8);
121
- let bn = BigInt(seq);
122
- for (let i = 0; i < 8; i++) buf[7 - i] = Number((bn >> BigInt(8 * i)) & 0xffn);
306
+ const hi = (seq / 0x100000000) | 0;
307
+ const lo = seq >>> 0;
308
+ buf[0] = (hi >>> 24) & 0xff;
309
+ buf[1] = (hi >>> 16) & 0xff;
310
+ buf[2] = (hi >>> 8) & 0xff;
311
+ buf[3] = hi & 0xff;
312
+ buf[4] = (lo >>> 24) & 0xff;
313
+ buf[5] = (lo >>> 16) & 0xff;
314
+ buf[6] = (lo >>> 8) & 0xff;
315
+ buf[7] = lo & 0xff;
123
316
  return buf;
124
317
  }
125
318
 
126
- /** TLS 1.2 AAD: seq(8) || type(1) || version(2) || length(2). */
319
+ /**
320
+ * TLS 1.2 AAD: seq(8) || type(1) || version(2) || length(2).
321
+ * Perf: inlines seqToBytes to avoid an extra allocation + copy.
322
+ */
127
323
  function buildAad12(seqNum, recordType, plaintextLen) {
128
324
  const aad = new Uint8Array(13);
129
- aad.set(seqToBytes(seqNum), 0);
325
+ const hi = (seqNum / 0x100000000) | 0;
326
+ const lo = seqNum >>> 0;
327
+ aad[0] = (hi >>> 24) & 0xff;
328
+ aad[1] = (hi >>> 16) & 0xff;
329
+ aad[2] = (hi >>> 8) & 0xff;
330
+ aad[3] = hi & 0xff;
331
+ aad[4] = (lo >>> 24) & 0xff;
332
+ aad[5] = (lo >>> 16) & 0xff;
333
+ aad[6] = (lo >>> 8) & 0xff;
334
+ aad[7] = lo & 0xff;
130
335
  aad[8] = recordType & 0xff;
131
336
  aad[9] = 0x03;
132
337
  aad[10] = 0x03;
@@ -135,44 +340,171 @@ function buildAad12(seqNum, recordType, plaintextLen) {
135
340
  return aad;
136
341
  }
137
342
 
138
- /** Encrypt a TLS 1.2 GCM record fragment. Returns explicit_nonce(8) || ciphertext || tag(16). */
343
+ /**
344
+ * Encrypt a TLS 1.2 GCM record fragment. Returns explicit_nonce(8) || ciphertext || tag(16).
345
+ * Perf: computes seq-bytes once for both explicit nonce and AAD (was computed twice).
346
+ */
139
347
  function encrypt12(pt, key, salt4, seqNum, recordType) {
140
- let explicit = seqToBytes(seqNum);
141
- let nonce = getNonce12(salt4, explicit);
142
- let aad = buildAad12(seqNum, recordType, pt.length);
348
+ // Shared big-endian seq encoding (used as both explicit_nonce and AAD[0..8])
349
+ const hi = (seqNum / 0x100000000) | 0;
350
+ const lo = seqNum >>> 0;
143
351
 
144
- let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
145
- let cipher = crypto.createCipheriv(algo, key, nonce);
352
+ // Nonce = salt4(4) || seq(8)
353
+ const nonce = new Uint8Array(12);
354
+ nonce[0] = salt4[0]; nonce[1] = salt4[1]; nonce[2] = salt4[2]; nonce[3] = salt4[3];
355
+ nonce[4] = (hi >>> 24) & 0xff;
356
+ nonce[5] = (hi >>> 16) & 0xff;
357
+ nonce[6] = (hi >>> 8) & 0xff;
358
+ nonce[7] = hi & 0xff;
359
+ nonce[8] = (lo >>> 24) & 0xff;
360
+ nonce[9] = (lo >>> 16) & 0xff;
361
+ nonce[10] = (lo >>> 8) & 0xff;
362
+ nonce[11] = lo & 0xff;
363
+
364
+ // AAD = seq(8) || type(1) || 03 03 || length(2)
365
+ const aad = new Uint8Array(13);
366
+ aad[0] = nonce[4]; aad[1] = nonce[5]; aad[2] = nonce[6]; aad[3] = nonce[7];
367
+ aad[4] = nonce[8]; aad[5] = nonce[9]; aad[6] = nonce[10]; aad[7] = nonce[11];
368
+ aad[8] = recordType & 0xff;
369
+ aad[9] = 0x03;
370
+ aad[10] = 0x03;
371
+ aad[11] = (pt.length >>> 8) & 0xff;
372
+ aad[12] = pt.length & 0xff;
373
+
374
+ const algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
375
+ const cipher = crypto.createCipheriv(algo, key, nonce);
146
376
  cipher.setAAD(aad);
147
- let ct = cipher.update(pt);
377
+ const ct = cipher.update(pt);
148
378
  cipher.final();
149
- let tag = cipher.getAuthTag();
379
+ const tag = cipher.getAuthTag();
150
380
 
151
- let out = new Uint8Array(8 + ct.length + tag.length);
152
- out.set(explicit, 0);
381
+ // Output: explicit_nonce(8) || ct || tag(16)
382
+ const out = new Uint8Array(8 + ct.length + tag.length);
383
+ // explicit_nonce = seq bytes (nonce[4..12])
384
+ out[0] = nonce[4]; out[1] = nonce[5]; out[2] = nonce[6]; out[3] = nonce[7];
385
+ out[4] = nonce[8]; out[5] = nonce[9]; out[6] = nonce[10]; out[7] = nonce[11];
153
386
  out.set(ct, 8);
154
387
  out.set(tag, 8 + ct.length);
155
388
  return out;
156
389
  }
157
390
 
158
- /** Decrypt a TLS 1.2 GCM record fragment. Input: explicit_nonce(8) || ciphertext || tag(16). */
391
+ /**
392
+ * Encrypt a TLS 1.2 GCM record directly into a complete TLS record buffer
393
+ * (5-byte header + 8-byte explicit_iv + ciphertext + 16-byte tag).
394
+ *
395
+ * Same "single allocation, single bulk copy" principle as encryptCompleteRecord13,
396
+ * but TLS 1.2 has extra complications:
397
+ * - AAD is NOT the record header (it's seq || type || version || length)
398
+ * - Record body has an 8-byte explicit_iv prefix before the ciphertext
399
+ *
400
+ * The seq bytes used in nonce are the SAME bytes written as explicit_iv —
401
+ * so we write them once into rec[5..13] and reference them from there, and
402
+ * we copy them into the nonce inline. Old flow needed a separate 'out' buffer
403
+ * with explicit_iv || ct || tag plus an 8-byte out[0..7] copy; this eliminates
404
+ * both of those.
405
+ */
406
+ function encryptCompleteRecord12(pt, key, salt4, seqNum, recordType, version) {
407
+ const ptLen = pt.length;
408
+ // Record body: 8 explicit_iv + ptLen ciphertext + 16 tag
409
+ const bodyLen = 8 + ptLen + 16;
410
+ const ver = version || 0x0303;
411
+
412
+ const rec = Buffer.allocUnsafe(5 + bodyLen);
413
+ // Record header
414
+ rec[0] = recordType & 0xff;
415
+ rec[1] = (ver >>> 8) & 0xff;
416
+ rec[2] = ver & 0xff;
417
+ rec[3] = (bodyLen >>> 8) & 0xff;
418
+ rec[4] = bodyLen & 0xff;
419
+
420
+ // Write explicit_iv (= seq bytes) into rec[5..13]
421
+ const hi = (seqNum / 0x100000000) | 0;
422
+ const lo = seqNum >>> 0;
423
+ rec[5] = (hi >>> 24) & 0xff;
424
+ rec[6] = (hi >>> 16) & 0xff;
425
+ rec[7] = (hi >>> 8) & 0xff;
426
+ rec[8] = hi & 0xff;
427
+ rec[9] = (lo >>> 24) & 0xff;
428
+ rec[10] = (lo >>> 16) & 0xff;
429
+ rec[11] = (lo >>> 8) & 0xff;
430
+ rec[12] = lo & 0xff;
431
+
432
+ // Nonce = salt4(4) || seq(8) — use the seq bytes we just wrote
433
+ const nonce = new Uint8Array(12);
434
+ nonce[0] = salt4[0]; nonce[1] = salt4[1]; nonce[2] = salt4[2]; nonce[3] = salt4[3];
435
+ nonce[4] = rec[5]; nonce[5] = rec[6]; nonce[6] = rec[7]; nonce[7] = rec[8];
436
+ nonce[8] = rec[9]; nonce[9] = rec[10]; nonce[10] = rec[11]; nonce[11] = rec[12];
437
+
438
+ // AAD = seq(8) || type(1) || 03 03 || plaintextLen(2)
439
+ // Note: AAD uses the PLAINTEXT length, not the record body length.
440
+ const aad = new Uint8Array(13);
441
+ aad[0] = rec[5]; aad[1] = rec[6]; aad[2] = rec[7]; aad[3] = rec[8];
442
+ aad[4] = rec[9]; aad[5] = rec[10]; aad[6] = rec[11]; aad[7] = rec[12];
443
+ aad[8] = recordType & 0xff;
444
+ aad[9] = 0x03;
445
+ aad[10] = 0x03;
446
+ aad[11] = (ptLen >>> 8) & 0xff;
447
+ aad[12] = ptLen & 0xff;
448
+
449
+ const algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
450
+ const cipher = crypto.createCipheriv(algo, key, nonce);
451
+ cipher.setAAD(aad);
452
+ const ct = cipher.update(pt);
453
+ cipher.final();
454
+
455
+ // Copy ciphertext into rec[13 : 13+ptLen]
456
+ ct.copy(rec, 13);
457
+ // Copy tag into rec[13+ptLen : 13+ptLen+16]
458
+ cipher.getAuthTag().copy(rec, 13 + ct.length);
459
+
460
+ return rec;
461
+ }
462
+
463
+ /**
464
+ * Decrypt a TLS 1.2 GCM record fragment. Input: explicit_nonce(8) || ciphertext || tag(16).
465
+ * Perf: subarray (zero-copy views) instead of slice (copies); returns Node Buffer
466
+ * (which IS a Uint8Array subclass) instead of re-copying to a plain Uint8Array.
467
+ */
159
468
  function decrypt12(fragment, key, salt4, seqNum, recordType) {
160
469
  if (fragment.length < 24) throw new Error('TLS 1.2 fragment too short');
161
470
 
162
- let explicit = fragment.slice(0, 8);
163
- let tag = fragment.slice(fragment.length - 16);
164
- let ct = fragment.slice(8, fragment.length - 16);
471
+ // Zero-copy views over the input
472
+ const explicit = fragment.subarray(0, 8);
473
+ const ct = fragment.subarray(8, fragment.length - 16);
474
+ const tag = fragment.subarray(fragment.length - 16);
165
475
 
166
- let nonce = getNonce12(salt4, explicit);
167
- let aad = buildAad12(seqNum, recordType, ct.length);
476
+ // Nonce = salt4 || explicit
477
+ const nonce = new Uint8Array(12);
478
+ nonce[0] = salt4[0]; nonce[1] = salt4[1]; nonce[2] = salt4[2]; nonce[3] = salt4[3];
479
+ nonce[4] = explicit[0]; nonce[5] = explicit[1]; nonce[6] = explicit[2]; nonce[7] = explicit[3];
480
+ nonce[8] = explicit[4]; nonce[9] = explicit[5]; nonce[10] = explicit[6]; nonce[11] = explicit[7];
168
481
 
169
- let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
170
- let decipher = crypto.createDecipheriv(algo, key, nonce);
482
+ // AAD = seq(8) || type(1) || 03 03 || ct.length(2)
483
+ const aad = new Uint8Array(13);
484
+ const hi = (seqNum / 0x100000000) | 0;
485
+ const lo = seqNum >>> 0;
486
+ aad[0] = (hi >>> 24) & 0xff;
487
+ aad[1] = (hi >>> 16) & 0xff;
488
+ aad[2] = (hi >>> 8) & 0xff;
489
+ aad[3] = hi & 0xff;
490
+ aad[4] = (lo >>> 24) & 0xff;
491
+ aad[5] = (lo >>> 16) & 0xff;
492
+ aad[6] = (lo >>> 8) & 0xff;
493
+ aad[7] = lo & 0xff;
494
+ aad[8] = recordType & 0xff;
495
+ aad[9] = 0x03;
496
+ aad[10] = 0x03;
497
+ aad[11] = (ct.length >>> 8) & 0xff;
498
+ aad[12] = ct.length & 0xff;
499
+
500
+ const algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
501
+ const decipher = crypto.createDecipheriv(algo, key, nonce);
171
502
  decipher.setAAD(aad);
172
503
  decipher.setAuthTag(tag);
173
- let pt = decipher.update(ct);
504
+ const pt = decipher.update(ct);
174
505
  decipher.final();
175
- return new Uint8Array(pt);
506
+ // Node Buffer extends Uint8Array, so callers that expect Uint8Array work unmodified.
507
+ return pt;
176
508
  }
177
509
 
178
510
  // ===================== Shared: TLS 1.2 key_block =====================
@@ -191,20 +523,483 @@ function deriveKeys12(masterSecret, localRandom, remoteRandom, cipherSuite, isSe
191
523
  // ===================== Record framing =====================
192
524
 
193
525
  /** Content type constants. */
194
- const CT = { CHANGE_CIPHER_SPEC: 20, ALERT: 21, HANDSHAKE: 22, APPLICATION_DATA: 23 };
526
+ const CT = { CHANGE_CIPHER_SPEC: 20, ALERT: 21, HANDSHAKE: 22, APPLICATION_DATA: 23, ACK: 26 };
195
527
 
196
- /** Write a raw TLS record to a writable stream. */
528
+ /**
529
+ * Write a raw TLS record to a writable stream.
530
+ *
531
+ * Returns the transport's backpressure signal (true = ready for more, false = buffer
532
+ * full, wait for 'drain'). Callers should propagate this through the TLS write chain
533
+ * so user-level sock.write() can apply backpressure correctly.
534
+ *
535
+ * Perf: single-copy from payload into the allocated record buffer, using
536
+ * typed-array `.set()` (fast native memcpy) instead of
537
+ * `Buffer.from(payload).copy(rec, 5)` which allocates an intermediate Buffer
538
+ * and does two copies. Also writes the 5-byte header with direct byte
539
+ * assignments to avoid method call overhead.
540
+ */
197
541
  function writeRecord(transport, type, payload, version) {
198
- if (!transport || typeof transport.write !== 'function') return;
199
- let ver = version || 0x0303;
200
- let rec = Buffer.allocUnsafe(5 + payload.length);
201
- rec.writeUInt8(type, 0);
202
- rec.writeUInt16BE(ver, 1);
203
- rec.writeUInt16BE(payload.length, 3);
204
- Buffer.from(payload).copy(rec, 5);
205
- transport.write(rec);
542
+ if (!transport || typeof transport.write !== 'function') return false;
543
+ const ver = version || 0x0303;
544
+ const plen = payload.length;
545
+ const rec = Buffer.allocUnsafe(5 + plen);
546
+ rec[0] = type;
547
+ rec[1] = (ver >>> 8) & 0xff;
548
+ rec[2] = ver & 0xff;
549
+ rec[3] = (plen >>> 8) & 0xff;
550
+ rec[4] = plen & 0xff;
551
+ // Buffer extends Uint8Array → .set() copies in a single pass from any TypedArray/Buffer.
552
+ rec.set(payload, 5);
553
+ return transport.write(rec);
554
+ }
555
+
556
+ // ===================== DTLS binary helpers =====================
557
+ // w_u8, w_u16, w_u48 imported from wire.js
558
+ // readU16, readU48 return value only (no offset tracking), unlike wire.js r_u16 which returns [value, offset]
559
+
560
+ function readU16(buf, off) { return ((buf[off] << 8) | buf[off+1]) >>> 0; }
561
+ function readU48(buf, off) {
562
+ let hi = ((buf[off] << 8) | buf[off+1]) >>> 0;
563
+ let lo = ((buf[off+2] << 24) | (buf[off+3] << 16) | (buf[off+4] << 8) | buf[off+5]) >>> 0;
564
+ return hi * 0x100000000 + lo;
565
+ }
566
+
567
+ /** AES-ECB encrypt a single 16-byte block (for DTLS 1.3 record number encryption). */
568
+ function aesEcbEncrypt(key, block) {
569
+ let algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
570
+ let cipher = crypto.createCipheriv(algo, key, null);
571
+ cipher.setAutoPadding(false);
572
+ let out = cipher.update(block);
573
+ cipher.final();
574
+ return new Uint8Array(out);
575
+ }
576
+
577
+
578
+ // ===================== DTLS plaintext record (13-byte header) =====================
579
+ //
580
+ // Used for: DTLS 1.2 all records, DTLS 1.3 epoch 0 (cleartext handshake).
581
+ //
582
+ // struct {
583
+ // ContentType type; // 1 byte
584
+ // ProtocolVersion version; // 2 bytes (0xFEFD)
585
+ // uint16 epoch; // 2 bytes
586
+ // uint48 sequence_number; // 6 bytes
587
+ // uint16 length; // 2 bytes
588
+ // opaque fragment[length];
589
+ // } DTLSPlaintext;
590
+
591
+ /** Build a plaintext DTLS record (classic 13-byte header). */
592
+ function buildDtlsPlaintext(type, epoch, seq, payload) {
593
+ let out = new Uint8Array(13 + payload.length);
594
+ let off = 0;
595
+ off = w_u8(out, off, type);
596
+ off = w_u16(out, off, 0xFEFD);
597
+ off = w_u16(out, off, epoch);
598
+ off = w_u48(out, off, seq);
599
+ off = w_u16(out, off, payload.length);
600
+ out.set(payload, off);
601
+ return out;
602
+ }
603
+
604
+ /** Parse plaintext DTLS records from a datagram. Returns array of { type, version, epoch, seq, payload, total_length }. */
605
+ function parseDtlsPlaintext(data) {
606
+ let records = [];
607
+ let off = 0;
608
+ while (off + 13 <= data.length) {
609
+ let type = data[off];
610
+ let version = readU16(data, off + 1);
611
+ let epoch = readU16(data, off + 3);
612
+ let seq = readU48(data, off + 5);
613
+ let length = readU16(data, off + 11);
614
+ if (off + 13 + length > data.length) break;
615
+ let payload = data.slice(off + 13, off + 13 + length);
616
+ records.push({ type, version, epoch, seq, payload, total_length: 13 + length });
617
+ off += 13 + length;
618
+ }
619
+ return records;
620
+ }
621
+
622
+
623
+ // ===================== DTLS 1.2 encrypted records =====================
624
+
625
+ /** DTLS 1.2 AAD: epoch(2) + seq(6) + type(1) + version(2) + plaintext_length(2). */
626
+ function buildDtlsAad12(epoch, seq, type, plaintextLen) {
627
+ let aad = new Uint8Array(13);
628
+ let off = 0;
629
+ off = w_u16(aad, off, epoch);
630
+ off = w_u48(aad, off, seq);
631
+ off = w_u8(aad, off, type);
632
+ off = w_u16(aad, off, 0xFEFD);
633
+ off = w_u16(aad, off, plaintextLen);
634
+ return aad;
635
+ }
636
+
637
+ /** Encrypt DTLS 1.2 record payload. Returns: explicit_nonce(8) || ciphertext || tag(16). */
638
+ function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
639
+ let explicit = seqToBytes(seq);
640
+ let nonce = getNonce12(ivSalt, explicit);
641
+ let aad = buildDtlsAad12(epoch, seq, type, pt.length);
642
+ let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
643
+ let cipher = crypto.createCipheriv(algo, key, nonce);
644
+ cipher.setAAD(aad);
645
+ let ct = cipher.update(pt);
646
+ cipher.final();
647
+ let tag = cipher.getAuthTag();
648
+ let out = new Uint8Array(8 + ct.length + tag.length);
649
+ out.set(explicit, 0);
650
+ out.set(new Uint8Array(ct), 8);
651
+ out.set(new Uint8Array(tag), 8 + ct.length);
652
+ return out;
653
+ }
654
+
655
+ /** Decrypt DTLS 1.2 record fragment. Input: explicit_nonce(8) || ciphertext || tag(16). */
656
+ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
657
+ if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
658
+ let explicit = fragment.slice(0, 8);
659
+ let tag = fragment.slice(fragment.length - 16);
660
+ let ct = fragment.slice(8, fragment.length - 16);
661
+ let nonce = getNonce12(ivSalt, explicit);
662
+ let aad = buildDtlsAad12(epoch, seq, type, ct.length);
663
+ let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
664
+ let decipher = crypto.createDecipheriv(algo, key, nonce);
665
+ decipher.setAAD(aad);
666
+ decipher.setAuthTag(tag);
667
+ let pt = decipher.update(ct);
668
+ decipher.final();
669
+ return new Uint8Array(pt);
670
+ }
671
+
672
+ /** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
673
+ function buildEncryptedDtls12(type, epoch, seq, plaintext, keys) {
674
+ let encrypted = encryptDtls12(plaintext, keys.key, keys.iv, epoch, seq, type);
675
+ return buildDtlsPlaintext(type, epoch, seq, encrypted);
676
+ }
677
+
678
+
679
+ // ===================== DTLS 1.3 unified header =====================
680
+ //
681
+ // struct {
682
+ // uint8 header_info;
683
+ // bits 7-5: 001 (fixed)
684
+ // bit 4: connection_id present
685
+ // bit 3: sequence_number_length (0=1byte, 1=2bytes)
686
+ // bit 2: length_present
687
+ // bits 1-0: epoch (low 2 bits)
688
+ // [ConnectionID cid;]
689
+ // uint8/uint16 record_number; // 1 or 2 bytes
690
+ // [uint16 length;]
691
+ // opaque encrypted_record[];
692
+ // } DTLSCiphertext;
693
+
694
+ const UNIFIED_HDR_FIXED = 0x20; // 001xxxxx
695
+
696
+ /** Build unified header info byte. */
697
+ function buildUnifiedHdr(epoch, seqLen2, hasLength, hasCid) {
698
+ let b = UNIFIED_HDR_FIXED;
699
+ if (hasCid) b |= 0x10;
700
+ if (seqLen2) b |= 0x08;
701
+ if (hasLength) b |= 0x04;
702
+ b |= (epoch & 0x03);
703
+ return b;
704
+ }
705
+
706
+ /** Parse unified header info byte. */
707
+ function parseUnifiedHdr(b) {
708
+ return {
709
+ hasCid: !!(b & 0x10),
710
+ seqLen2: !!(b & 0x08),
711
+ hasLength: !!(b & 0x04),
712
+ epoch: b & 0x03,
713
+ };
206
714
  }
207
715
 
716
+ /** Check if a byte is a DTLS 1.3 unified header (0x20..0x3F). */
717
+ function isUnifiedHdr(b) { return (b & 0xE0) === UNIFIED_HDR_FIXED; }
718
+
719
+
720
+ // ===================== DTLS 1.3 record number encryption =====================
721
+
722
+ /**
723
+ * Encrypt/decrypt record number (XOR with AES-ECB mask — symmetric operation).
724
+ * mask = AES-ECB(snKey, ciphertext[0..15])
725
+ * result = rnBytes XOR mask[0..len-1]
726
+ */
727
+ function maskRecordNumber(snKey, rnBytes, ciphertext) {
728
+ let sample = new Uint8Array(16);
729
+ sample.set(ciphertext.subarray(0, Math.min(16, ciphertext.length)), 0);
730
+ let mask = aesEcbEncrypt(snKey, sample);
731
+ let out = new Uint8Array(rnBytes.length);
732
+ for (let i = 0; i < rnBytes.length; i++) out[i] = rnBytes[i] ^ mask[i];
733
+ return out;
734
+ }
735
+
736
+
737
+ // ===================== DTLS 1.3 encrypted record build/decrypt =====================
738
+
739
+ /**
740
+ * Build a DTLS 1.3 encrypted record (unified header).
741
+ *
742
+ * innerType: content type (22=handshake, 23=app_data, 26=ACK)
743
+ * plaintext: content to encrypt
744
+ * seq: record sequence number
745
+ * epoch: low 2 bits (2=handshake, 3=application)
746
+ * keys: { key, iv, snKey, algo? }
747
+ */
748
+ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
749
+ let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
750
+ let isChaCha = algo === 'chacha20-poly1305';
751
+
752
+ // Unified header: 2-byte seq, with length, no CID
753
+ let info = buildUnifiedHdr(epoch, true, true, false);
754
+
755
+ // Plaintext record number
756
+ let rn = new Uint8Array(2);
757
+ rn[0] = (seq >>> 8) & 0xFF;
758
+ rn[1] = seq & 0xFF;
759
+
760
+ // Inner plaintext: content + content_type
761
+ let inner = new Uint8Array(plaintext.length + 1);
762
+ inner.set(plaintext, 0);
763
+ inner[plaintext.length] = innerType;
764
+
765
+ let encLen = inner.length + 16;
766
+
767
+ // AAD = header with PLAINTEXT record number (before RN encryption)
768
+ let aad = new Uint8Array(5);
769
+ aad[0] = info;
770
+ aad[1] = rn[0];
771
+ aad[2] = rn[1];
772
+ aad[3] = (encLen >>> 8) & 0xFF;
773
+ aad[4] = encLen & 0xFF;
774
+
775
+ // Nonce (reuse TLS 1.3 nonce construction)
776
+ let nonce = getNonce(keys.iv, seq);
777
+
778
+ // AEAD encrypt
779
+ let cipher = crypto.createCipheriv(algo, keys.key, nonce,
780
+ isChaCha ? { authTagLength: 16 } : undefined);
781
+ cipher.setAAD(aad, isChaCha ? { plaintextLength: inner.length } : undefined);
782
+ let ct = cipher.update(inner);
783
+ cipher.final();
784
+ let tag = cipher.getAuthTag();
785
+
786
+ let ciphertext = new Uint8Array(ct.length + tag.length);
787
+ ciphertext.set(new Uint8Array(ct), 0);
788
+ ciphertext.set(new Uint8Array(tag), ct.length);
789
+
790
+ // Encrypt record number
791
+ let encRn = maskRecordNumber(keys.snKey, rn, ciphertext);
792
+
793
+ // Assemble: info(1) + encrypted_rn(2) + length(2) + ciphertext
794
+ let record = new Uint8Array(5 + ciphertext.length);
795
+ record[0] = info;
796
+ record[1] = encRn[0];
797
+ record[2] = encRn[1];
798
+ record[3] = (ciphertext.length >>> 8) & 0xFF;
799
+ record[4] = ciphertext.length & 0xFF;
800
+ record.set(ciphertext, 5);
801
+ return record;
802
+ }
803
+
804
+ /**
805
+ * Decrypt a DTLS 1.3 encrypted record.
806
+ * data: full record bytes (starting with unified header byte)
807
+ * keys: { key, iv, snKey, algo? }
808
+ * Returns { epoch, seq, type, content } or null on failure.
809
+ */
810
+ function decryptEncryptedDtls13(data, keys) {
811
+ if (data.length < 1) return null;
812
+
813
+ let hdr = parseUnifiedHdr(data[0]);
814
+ let off = 1;
815
+
816
+ if (hdr.hasCid) return null; // CID not supported yet
817
+
818
+ let rnLen = hdr.seqLen2 ? 2 : 1;
819
+ if (off + rnLen > data.length) return null;
820
+ let encRn = data.slice(off, off + rnLen);
821
+ off += rnLen;
822
+
823
+ let ctLen;
824
+ if (hdr.hasLength) {
825
+ if (off + 2 > data.length) return null;
826
+ ctLen = readU16(data, off);
827
+ off += 2;
828
+ } else {
829
+ ctLen = data.length - off;
830
+ }
831
+
832
+ if (off + ctLen > data.length) return null;
833
+ let ciphertext = data.slice(off, off + ctLen);
834
+
835
+ // Decrypt record number
836
+ let rn = maskRecordNumber(keys.snKey, encRn, ciphertext);
837
+ let seq = hdr.seqLen2 ? ((rn[0] << 8) | rn[1]) : rn[0];
838
+
839
+ // Rebuild AAD with plaintext RN
840
+ let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
841
+ let aad = new Uint8Array(hdrLen);
842
+ aad[0] = data[0];
843
+ for (let i = 0; i < rnLen; i++) aad[1 + i] = rn[i];
844
+ if (hdr.hasLength) {
845
+ aad[1 + rnLen] = (ctLen >>> 8) & 0xFF;
846
+ aad[1 + rnLen + 1] = ctLen & 0xFF;
847
+ }
848
+
849
+ let nonce = getNonce(keys.iv, seq);
850
+ let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
851
+ let isChaCha = algo === 'chacha20-poly1305';
852
+
853
+ if (ciphertext.length < 16) return null;
854
+ let ct = ciphertext.subarray(0, ciphertext.length - 16);
855
+ let tag = ciphertext.subarray(ciphertext.length - 16);
856
+
857
+ try {
858
+ let decipher = crypto.createDecipheriv(algo, keys.key, nonce,
859
+ isChaCha ? { authTagLength: 16 } : undefined);
860
+ decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
861
+ decipher.setAuthTag(tag);
862
+ let pt = decipher.update(ct);
863
+ decipher.final();
864
+
865
+ let inner = parseInnerPlaintext(new Uint8Array(pt));
866
+ return {
867
+ epoch: hdr.epoch,
868
+ seq: seq,
869
+ type: inner.type,
870
+ content: inner.content,
871
+ total_length: off + ctLen,
872
+ };
873
+ } catch (e) {
874
+ return null;
875
+ }
876
+ }
877
+
878
+
879
+ // ===================== DTLS datagram parsing =====================
880
+
881
+ /**
882
+ * Parse a DTLS datagram containing one or more records.
883
+ * Dispatches between plaintext (classic header) and encrypted (unified header).
884
+ *
885
+ * keysByEpoch: { [epoch]: { key, iv, snKey, algo } } — null for plaintext only.
886
+ * Returns array of { type, epoch, seq, content, encrypted }.
887
+ */
888
+ function parseDtlsDatagram(data, keysByEpoch) {
889
+ let records = [];
890
+ let off = 0;
891
+
892
+ while (off < data.length) {
893
+ let first = data[off];
894
+
895
+ if (isUnifiedHdr(first)) {
896
+ let hdr = parseUnifiedHdr(first);
897
+ let keys = keysByEpoch ? keysByEpoch[hdr.epoch] : null;
898
+
899
+ if (!keys) {
900
+ // Can't decrypt — try to skip
901
+ let rnLen = hdr.seqLen2 ? 2 : 1;
902
+ let skip = 1 + rnLen;
903
+ if (hdr.hasLength && off + skip + 2 <= data.length) {
904
+ skip += 2 + readU16(data, off + skip);
905
+ } else {
906
+ skip = data.length - off;
907
+ }
908
+ off += skip;
909
+ continue;
910
+ }
911
+
912
+ let result = decryptEncryptedDtls13(data.subarray(off), keys);
913
+ if (result) {
914
+ records.push({
915
+ type: result.type,
916
+ epoch: result.epoch,
917
+ seq: result.seq,
918
+ content: result.content,
919
+ encrypted: true,
920
+ });
921
+ off += result.total_length;
922
+ } else {
923
+ break;
924
+ }
925
+
926
+ } else if (first <= 63) {
927
+ // Classic DTLS record
928
+ if (off + 13 > data.length) break;
929
+ let type = data[off];
930
+ let epoch = readU16(data, off + 3);
931
+ let seq = readU48(data, off + 5);
932
+ let length = readU16(data, off + 11);
933
+ if (off + 13 + length > data.length) break;
934
+
935
+ let payload = data.slice(off + 13, off + 13 + length);
936
+ let encrypted = false;
937
+
938
+ // DTLS 1.2: decrypt if epoch > 0 and keys available
939
+ if (epoch > 0 && keysByEpoch && keysByEpoch[epoch]) {
940
+ let keys = keysByEpoch[epoch];
941
+ try {
942
+ payload = decryptDtls12(payload, keys.key, keys.iv, epoch, seq, type);
943
+ encrypted = true;
944
+ } catch(e) {
945
+ // Decryption failed — return raw
946
+ }
947
+ }
948
+
949
+ records.push({
950
+ type: type,
951
+ epoch: epoch,
952
+ seq: seq,
953
+ content: payload,
954
+ encrypted: encrypted,
955
+ });
956
+ off += 13 + length;
957
+ } else {
958
+ break;
959
+ }
960
+ }
961
+ return records;
962
+ }
963
+
964
+
965
+ // ===================== DTLS 1.3 ACK (RFC 9147 §7) =====================
966
+ //
967
+ // struct {
968
+ // RecordNumber record_numbers<0..2^16-1>;
969
+ // }
970
+ // struct {
971
+ // uint16 epoch;
972
+ // uint48 sequence_number;
973
+ // } RecordNumber; // 8 bytes
974
+
975
+ /** Build ACK payload. acks: [{ epoch, seq }, ...] */
976
+ function buildDtlsAck(acks) {
977
+ let bodyLen = acks.length * 8;
978
+ let out = new Uint8Array(2 + bodyLen);
979
+ let off = 0;
980
+ off = w_u16(out, off, bodyLen);
981
+ for (let i = 0; i < acks.length; i++) {
982
+ off = w_u16(out, off, acks[i].epoch);
983
+ off = w_u48(out, off, acks[i].seq);
984
+ }
985
+ return out;
986
+ }
987
+
988
+ /** Parse ACK payload. Returns [{ epoch, seq }, ...]. */
989
+ function parseDtlsAck(data) {
990
+ let bodyLen = readU16(data, 0);
991
+ let off = 2;
992
+ let end = off + bodyLen;
993
+ let acks = [];
994
+ while (off + 8 <= end) {
995
+ let epoch = readU16(data, off); off += 2;
996
+ let seq = readU48(data, off); off += 6;
997
+ acks.push({ epoch, seq });
998
+ }
999
+ return acks;
1000
+ }
1001
+
1002
+
208
1003
  // ===================== Exports =====================
209
1004
 
210
1005
  export {
@@ -214,8 +1009,11 @@ export {
214
1009
  // TLS 1.3
215
1010
  deriveKeys,
216
1011
  getNonce,
1012
+ getNonceInto, // write nonce into a caller-provided buffer (avoid alloc per record)
217
1013
  encryptRecord,
1014
+ encryptCompleteRecord13, // fused encrypt + header → single allocation hot path
218
1015
  decryptRecord,
1016
+ decryptRecordWithAadView, // decrypt using caller-provided header view as AAD (no alloc)
219
1017
  parseInnerPlaintext,
220
1018
 
221
1019
  // TLS 1.2
@@ -223,10 +1021,41 @@ export {
223
1021
  seqToBytes,
224
1022
  buildAad12,
225
1023
  encrypt12,
1024
+ encryptCompleteRecord12, // fused encrypt + header → single allocation hot path
226
1025
  decrypt12,
227
1026
  deriveKeys12,
228
1027
 
229
1028
  // Record framing
230
1029
  CT,
231
1030
  writeRecord,
1031
+
1032
+ // DTLS helpers
1033
+ aesEcbEncrypt,
1034
+ isUnifiedHdr,
1035
+
1036
+ // DTLS plaintext records
1037
+ buildDtlsPlaintext,
1038
+ parseDtlsPlaintext,
1039
+
1040
+ // DTLS 1.2 encrypted records
1041
+ buildDtlsAad12,
1042
+ encryptDtls12,
1043
+ decryptDtls12,
1044
+ buildEncryptedDtls12,
1045
+
1046
+ // DTLS 1.3 unified header
1047
+ buildUnifiedHdr,
1048
+ parseUnifiedHdr,
1049
+ maskRecordNumber,
1050
+
1051
+ // DTLS 1.3 encrypted records
1052
+ buildEncryptedDtls13,
1053
+ decryptEncryptedDtls13,
1054
+
1055
+ // DTLS datagram parsing
1056
+ parseDtlsDatagram,
1057
+
1058
+ // DTLS 1.3 ACK
1059
+ buildDtlsAck,
1060
+ parseDtlsAck,
232
1061
  };