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.
@@ -0,0 +1,865 @@
1
+ /**
2
+ * dtls_session.js — DTLS 1.2 + 1.3 session (client + server).
3
+ *
4
+ * Wraps TLSSession (composition) and adds:
5
+ * - DTLS record layer (plaintext + encrypted via record.js)
6
+ * - Reconstruction headers (add on output, strip on input)
7
+ * - Fragmentation and reassembly
8
+ * - Flight-based retransmission
9
+ * - HelloVerifyRequest (DTLS 1.2)
10
+ * - ACK records (DTLS 1.3)
11
+ * - Epoch + key management
12
+ * - Version mapping (TLSSession works internally, DTLS on the wire)
13
+ *
14
+ * Events emitted:
15
+ * 'packet' (Uint8Array) — DTLS datagram ready to send via UDP
16
+ * 'connect' () — handshake complete
17
+ * 'data' (Uint8Array) — decrypted application data received
18
+ * 'error' (Error)
19
+ * 'close' ()
20
+ *
21
+ * Usage:
22
+ * let dtls = new DTLSSession({ isServer: false, servername: 'example.com' });
23
+ * dtls.on('packet', (data) => udpSocket.send(data, remotePort, remoteAddr));
24
+ * dtls.on('connect', () => dtls.send(new TextEncoder().encode('hello')));
25
+ * dtls.on('data', (data) => console.log('received:', data));
26
+ * dtls.feedDatagram(incomingUdpPayload);
27
+ */
28
+
29
+ import { EventEmitter } from 'node:events';
30
+
31
+ import TLSSession from './tls_session.js';
32
+ import createSecureContext from './secure_context.js';
33
+
34
+ import {
35
+ DTLS_VERSION,
36
+ build_dtls_handshake,
37
+ parse_dtls_handshake,
38
+ build_message,
39
+ build_hello_verify_request,
40
+ parse_hello_verify_request,
41
+ } from './wire.js';
42
+
43
+ import {
44
+ TLS_CIPHER_SUITES,
45
+ hkdf_expand_label,
46
+ derive_sn_key,
47
+ } from './crypto.js';
48
+
49
+ import {
50
+ CT,
51
+ getAeadAlgo,
52
+ deriveKeys12,
53
+ decryptDtls12,
54
+ buildDtlsPlaintext,
55
+ buildEncryptedDtls12,
56
+ buildEncryptedDtls13,
57
+ parseDtlsDatagram,
58
+ buildDtlsAck,
59
+ parseDtlsAck,
60
+ } from './record.js';
61
+
62
+
63
+ // ============================================================
64
+ // Helpers
65
+ // ============================================================
66
+
67
+ // ============================================================
68
+ // DTLSSession
69
+ // ============================================================
70
+
71
+ /**
72
+ * Insert a DTLS cookie into a ClientHello TLS message.
73
+ * CH1 format: type(1) + length(3) + version(2) + random(32) + sid_len(1) + sid + cookie_len(1) + cookie(0) + ...
74
+ * Returns new Uint8Array with cookie inserted.
75
+ */
76
+ function insertCookieIntoClientHello(ch1, cookie) {
77
+ let off = 4; // skip type(1)+length(3)
78
+ off += 2; // version
79
+ off += 32; // random
80
+ let sidLen = ch1[off];
81
+ off += 1 + sidLen; // session_id
82
+
83
+ // Now at cookie_len position
84
+ let oldCookieLen = ch1[off];
85
+ let cookiePos = off;
86
+
87
+ // Build CH2: [before cookie_len] + [new cookie_len + cookie] + [after old cookie]
88
+ let before = ch1.subarray(0, cookiePos);
89
+ let after = ch1.subarray(cookiePos + 1 + oldCookieLen);
90
+
91
+ let ch2 = new Uint8Array(before.length + 1 + cookie.length + after.length);
92
+ ch2.set(before, 0);
93
+ ch2[before.length] = cookie.length;
94
+ ch2.set(cookie, before.length + 1);
95
+ ch2.set(after, before.length + 1 + cookie.length);
96
+
97
+ // Update handshake length field (bytes 1-3)
98
+ let newBodyLen = ch2.length - 4;
99
+ ch2[1] = (newBodyLen >> 16) & 0xff;
100
+ ch2[2] = (newBodyLen >> 8) & 0xff;
101
+ ch2[3] = newBodyLen & 0xff;
102
+
103
+ return ch2;
104
+ }
105
+
106
+ function DTLSSession(options) {
107
+ if (!(this instanceof DTLSSession)) return new DTLSSession(options);
108
+ options = options || {};
109
+
110
+ let ev = new EventEmitter();
111
+ let isServer = !!options.isServer;
112
+
113
+ // ---- Internal TLSSession ----
114
+ let tls = new TLSSession({
115
+ isServer: isServer,
116
+ servername: options.servername,
117
+ SNICallback: options.SNICallback,
118
+ rejectUnauthorized: options.rejectUnauthorized,
119
+ ca: options.ca,
120
+ noTickets: options.noTickets,
121
+ requestCert: options.requestCert,
122
+ cert: options.cert,
123
+ key: options.key,
124
+ ticketKeys: options.ticketKeys,
125
+ });
126
+
127
+ // ---- DTLS context ----
128
+ let ctx = {
129
+ state: 'idle', // idle → handshaking → connected → closed
130
+ isServer: isServer,
131
+ mtu: options.mtu || 1200,
132
+
133
+ // Version (determined after negotiation)
134
+ selectedVersion: null, // DTLS version (0xFEFC or 0xFEFD)
135
+
136
+ // Keys by DTLS epoch
137
+ // epoch 0: cleartext
138
+ // DTLS 1.2: epoch 1 = after CCS
139
+ // DTLS 1.3: epoch 2 = handshake, epoch 3 = application
140
+ keys: {},
141
+
142
+ // Record sequence numbers (per epoch, for sending)
143
+ writeSeq: {},
144
+
145
+ // Cipher info (set when handshake secrets arrive)
146
+ cipherSuite: null,
147
+ hashName: null,
148
+
149
+ // Fragment reassembly: msg_seq → { totalLength, parts: { offset: Uint8Array } }
150
+ fragments: {},
151
+
152
+ // Incoming handshake msg_seq tracking
153
+ nextReadMsgSeq: 0,
154
+
155
+ // Flight tracking for retransmission
156
+ currentFlight: [], // array of { epoch, data: Uint8Array (complete datagram) }
157
+ retransmitTimer: null,
158
+ retransmitCount: 0,
159
+ retransmitTimeout: 1000,
160
+ maxRetransmits: 6,
161
+
162
+ // DTLS 1.2: CCS tracking
163
+ localCcsSent: false,
164
+ remoteCcsSeen: false,
165
+
166
+ // HelloVerifyRequest (DTLS 1.2 server)
167
+ hvrCookie: null, // cookie sent in HVR
168
+ hvrDone: false, // true after HVR exchange complete
169
+ };
170
+
171
+
172
+ // ============================================================
173
+ // Setup TLSSession for DTLS
174
+ // ============================================================
175
+
176
+ // Set DTLS versions and cookie (triggers DTLS format in build_hello)
177
+ let versions = [];
178
+ let minVer = options.minVersion || 'DTLSv1.2';
179
+ let maxVer = options.maxVersion || 'DTLSv1.3';
180
+ if (maxVer === 'DTLSv1.3' || maxVer === 'TLSv1.3') versions.push(DTLS_VERSION.DTLS1_3);
181
+ if (minVer === 'DTLSv1.2' || minVer === 'TLSv1.2' || maxVer === 'DTLSv1.2') versions.push(DTLS_VERSION.DTLS1_2);
182
+ if (versions.length === 0) versions = [DTLS_VERSION.DTLS1_3, DTLS_VERSION.DTLS1_2];
183
+
184
+ let tlsSetup = {
185
+ local_supported_versions: versions,
186
+ dtls_cookie: new Uint8Array(0), // empty cookie — triggers DTLS format
187
+ };
188
+
189
+ // Pass through cipher/group/alpn preferences
190
+ if (options.cipherSuites) tlsSetup.local_supported_cipher_suites = options.cipherSuites;
191
+
192
+ // Client cipher defaults: TLSSession auto-populates both TLS 1.3+1.2 ciphers,
193
+ // but if we only support DTLS 1.2, we must not offer TLS 1.3 ciphers
194
+ // (the server might erroneously select one for a 1.2 connection).
195
+ if (!isServer && !options.cipherSuites && versions.indexOf(DTLS_VERSION.DTLS1_3) < 0) {
196
+ tlsSetup.local_supported_cipher_suites = [0xC02F, 0xC030, 0xC02B, 0xC02C, 0xCCA8];
197
+ }
198
+ if (options.groups) tlsSetup.local_supported_groups = options.groups;
199
+ if (options.alpnProtocols) tlsSetup.local_supported_alpns = options.alpnProtocols;
200
+
201
+ // Server defaults — TLSSession client auto-populates these, but server expects them set externally
202
+ if (isServer) {
203
+ if (!tlsSetup.local_supported_cipher_suites) {
204
+ let ciphers = [];
205
+ if (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0) {
206
+ ciphers.push(0x1301, 0x1302, 0x1303); // TLS 1.3 AEAD ciphers
207
+ }
208
+ if (versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0) {
209
+ ciphers.push(0xC02F, 0xC030, 0xC02B, 0xC02C); // TLS 1.2 ECDHE ciphers
210
+ }
211
+ tlsSetup.local_supported_cipher_suites = ciphers;
212
+ }
213
+ if (!tlsSetup.local_supported_groups) {
214
+ tlsSetup.local_supported_groups = [0x001d, 0x0017, 0x0018];
215
+ }
216
+ tlsSetup.local_supported_signature_algorithms = [
217
+ 0x0804, 0x0805, 0x0806, // RSA-PSS
218
+ 0x0403, 0x0503, 0x0603, // ECDSA
219
+ 0x0807, 0x0808, // EdDSA
220
+ 0x0401, 0x0501, 0x0601, // PKCS#1
221
+ ];
222
+ }
223
+
224
+ // Server cert/key
225
+ if (options.cert && options.key) {
226
+ let sctx = createSecureContext({ key: options.key, cert: options.cert });
227
+ tlsSetup.local_cert_chain = sctx.certificateChain;
228
+ tlsSetup.cert_private_key = sctx.privateKey;
229
+ }
230
+
231
+ tls.set_context(tlsSetup);
232
+
233
+
234
+ // ============================================================
235
+ // DTLS 1.2 transcript hook (RFC 6347 §4.2.6)
236
+ //
237
+ // DTLS 1.2 requires the handshake hash to include DTLS-specific
238
+ // reconstruction data (msg_seq + frag_offset + frag_length).
239
+ // We hook into TLSSession's transcript to transparently convert
240
+ // TLS-format entries to DTLS-format when the version is DTLS 1.2.
241
+ //
242
+ // For DTLS 1.3 and TLS, the transcript uses standard TLS format.
243
+ // ============================================================
244
+
245
+ let transcriptMsgSeqs = []; // parallel array: msg_seq for each transcript entry
246
+
247
+ tls.context.transcriptHook = function(data) {
248
+ // Determine msg_seq: incoming uses _incomingMsgSeq (set by deliverHandshakeMessage),
249
+ // outgoing uses message_sent_seq (TLSSession's counter).
250
+ let msgSeq;
251
+ if (ctx._incomingMsgSeq !== undefined) {
252
+ msgSeq = ctx._incomingMsgSeq;
253
+ ctx._incomingMsgSeq = undefined; // one-shot: only applies to the first push
254
+ } else {
255
+ msgSeq = tls.context.message_sent_seq;
256
+ }
257
+ transcriptMsgSeqs.push(msgSeq);
258
+
259
+ if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
260
+ return build_dtls_handshake(data, msgSeq);
261
+ }
262
+ return data; // TLS format for DTLS 1.3 or unknown version
263
+ };
264
+
265
+ /**
266
+ * Called when selectedVersion is first determined as DTLS 1.2.
267
+ * Retroactively converts all existing transcript entries to DTLS format.
268
+ */
269
+ function fixTranscriptForDtls12() {
270
+ let t = tls.context.transcript;
271
+ for (let i = 0; i < t.length; i++) {
272
+ if (i < transcriptMsgSeqs.length) {
273
+ t[i] = build_dtls_handshake(t[i], transcriptMsgSeqs[i]);
274
+ }
275
+ }
276
+ }
277
+
278
+
279
+ // ============================================================
280
+ // Key derivation
281
+ // ============================================================
282
+
283
+ function deriveEpochKeys(secret) {
284
+ let cs = TLS_CIPHER_SUITES[ctx.cipherSuite];
285
+ let empty = new Uint8Array(0);
286
+ let key = hkdf_expand_label(cs.hash, secret, 'key', empty, cs.keylen);
287
+ let iv = hkdf_expand_label(cs.hash, secret, 'iv', empty, 12);
288
+
289
+ let result = { key, iv };
290
+
291
+ // DTLS 1.3: derive sn_key for record number encryption
292
+ if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
293
+ result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite);
294
+ result.algo = getAeadAlgo(ctx.cipherSuite);
295
+ }
296
+
297
+ return result;
298
+ }
299
+
300
+ tls.on('handshakeSecrets', function(localSecret, remoteSecret) {
301
+ // Ensure version is detected before key derivation
302
+ if (ctx.selectedVersion === null) ctx.selectedVersion = tls.context.selected_version;
303
+ ctx.cipherSuite = tls.getCipher();
304
+ ctx.hashName = TLS_CIPHER_SUITES[ctx.cipherSuite].hash;
305
+
306
+ let dtlsEpoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 2 : 1;
307
+
308
+ ctx.keys[dtlsEpoch] = {
309
+ write: deriveEpochKeys(localSecret),
310
+ read: deriveEpochKeys(remoteSecret),
311
+ };
312
+ ctx.writeSeq[dtlsEpoch] = 0;
313
+ });
314
+
315
+ tls.on('appSecrets', function(localSecret, remoteSecret) {
316
+ let dtlsEpoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1;
317
+
318
+ ctx.keys[dtlsEpoch] = {
319
+ write: deriveEpochKeys(localSecret),
320
+ read: deriveEpochKeys(remoteSecret),
321
+ };
322
+ ctx.writeSeq[dtlsEpoch] = 0;
323
+ });
324
+
325
+
326
+ // ============================================================
327
+ // Outgoing: TLSSession message → DTLS datagram
328
+ // ============================================================
329
+
330
+ tls.on('message', function(tlsEpoch, seq, type, data) {
331
+ // Ensure version is detected
332
+ if (ctx.selectedVersion === null && tls.context.selected_version) {
333
+ ctx.selectedVersion = tls.context.selected_version;
334
+ // If just determined as DTLS 1.2, retroactively fix transcript entries
335
+ // that were pushed before the version was known.
336
+ if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
337
+ fixTranscriptForDtls12();
338
+ }
339
+ }
340
+
341
+ // Save client's CH1 for potential HVR retry
342
+ if (type === 'hello' && !isServer) {
343
+ ctx.savedClientHello = data;
344
+ }
345
+
346
+ // Map TLS epoch → DTLS epoch
347
+ let dtlsEpoch;
348
+ if (tlsEpoch === 0) {
349
+ dtlsEpoch = 0;
350
+ } else if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
351
+ dtlsEpoch = tlsEpoch === 1 ? 2 : 3; // epoch 2=handshake, 3=app
352
+ } else {
353
+ dtlsEpoch = 1; // DTLS 1.2: epoch 1 for all encrypted
354
+ }
355
+
356
+ if (type === 'alert') {
357
+ sendAlertRecord(dtlsEpoch, data);
358
+ return;
359
+ }
360
+
361
+ // DTLS 1.2: derive keys before first encrypted message
362
+ if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && tlsEpoch === 1 && !ctx.keys[1]) {
363
+ let ts = tls.getTrafficSecrets();
364
+ if (ts.masterSecret) {
365
+ let d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, tls.getCipher(), isServer);
366
+ ctx.keys[1] = {
367
+ write: { key: d12.writeKey, iv: d12.writeIv },
368
+ read: { key: d12.readKey, iv: d12.readIv },
369
+ };
370
+ ctx.writeSeq[1] = 0;
371
+ }
372
+ }
373
+
374
+ // DTLS 1.2: send CCS before first encrypted message (Finished)
375
+ if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && tlsEpoch === 1 && !ctx.localCcsSent) {
376
+ sendCCS();
377
+ }
378
+
379
+ // Convert TLS handshake message → DTLS handshake message (add reconstruction header)
380
+ let dtlsMsg = build_dtls_handshake(data, seq);
381
+
382
+ // Fragment if needed
383
+ let frags = fragmentMessage(dtlsMsg, data, seq);
384
+
385
+ // Build DTLS records and emit
386
+ for (let i = 0; i < frags.length; i++) {
387
+ let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
388
+ ctx.currentFlight.push(record);
389
+ ev.emit('packet', record);
390
+ }
391
+
392
+ // Start retransmit timer after sending flight messages
393
+ if (ctx.state === 'handshaking') {
394
+ startRetransmitTimer();
395
+ }
396
+ });
397
+
398
+ /**
399
+ * Build a DTLS record (plaintext or encrypted depending on epoch and key availability).
400
+ */
401
+ function buildRecord(epoch, contentType, payload) {
402
+ let epochKeys = ctx.keys[epoch];
403
+
404
+ if (!epochKeys) {
405
+ // No keys → plaintext record
406
+ return buildDtlsPlaintext(contentType, epoch, nextWriteSeq(epoch), payload);
407
+ }
408
+
409
+ if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
410
+ // DTLS 1.3 encrypted (unified header)
411
+ return buildEncryptedDtls13(contentType, payload, nextWriteSeq(epoch), epoch, epochKeys.write);
412
+ } else {
413
+ // DTLS 1.2 encrypted (classic header)
414
+ return buildEncryptedDtls12(contentType, epoch, nextWriteSeq(epoch), payload, epochKeys.write);
415
+ }
416
+ }
417
+
418
+ function nextWriteSeq(epoch) {
419
+ if (!(epoch in ctx.writeSeq)) ctx.writeSeq[epoch] = 0;
420
+ return ctx.writeSeq[epoch]++;
421
+ }
422
+
423
+ /**
424
+ * Fragment a DTLS handshake message if it exceeds MTU.
425
+ * Returns array of DTLS handshake message fragments (each with reconstruction header).
426
+ */
427
+ function fragmentMessage(dtlsMsg, tlsMsg, msgSeq) {
428
+ let overhead = 13 + 16; // record header + AEAD tag (approximate)
429
+ let maxFragment = ctx.mtu - overhead;
430
+
431
+ if (dtlsMsg.length <= maxFragment) return [dtlsMsg];
432
+
433
+ // Need to fragment: split the TLS body into chunks
434
+ let body = tlsMsg.subarray(4); // skip TLS header (type+length)
435
+ let totalLength = body.length;
436
+ let fragments = [];
437
+ let offset = 0;
438
+
439
+ while (offset < totalLength) {
440
+ let chunkLen = Math.min(maxFragment - 12, totalLength - offset); // 12 = DTLS handshake header
441
+ let frag = build_dtls_handshake(tlsMsg, msgSeq, offset, chunkLen);
442
+ fragments.push(frag);
443
+ offset += chunkLen;
444
+ }
445
+
446
+ return fragments;
447
+ }
448
+
449
+ /**
450
+ * Send an alert record.
451
+ */
452
+ function sendAlertRecord(epoch, alertData) {
453
+ let record = buildRecord(epoch, CT.ALERT, alertData);
454
+ ev.emit('packet', record);
455
+ }
456
+
457
+ /**
458
+ * Send CCS record (DTLS 1.2 only).
459
+ */
460
+ function sendCCS() {
461
+ if (ctx.localCcsSent) return;
462
+ ctx.localCcsSent = true;
463
+ let record = buildDtlsPlaintext(CT.CHANGE_CIPHER_SPEC, 0, nextWriteSeq(0), new Uint8Array([1]));
464
+ ev.emit('packet', record);
465
+ }
466
+
467
+
468
+ // ============================================================
469
+ // Incoming: UDP datagram → parse → feed TLSSession
470
+ // ============================================================
471
+
472
+ function feedDatagram(data) {
473
+ if (ctx.state === 'closed') return;
474
+ if (ctx.state === 'idle') ctx.state = 'handshaking';
475
+
476
+ // Build key lookup for decryption
477
+ let keysByEpoch = {};
478
+ for (let ep in ctx.keys) {
479
+ if (ctx.keys[ep] && ctx.keys[ep].read) {
480
+ keysByEpoch[Number(ep)] = ctx.keys[ep].read;
481
+ }
482
+ }
483
+
484
+ let records = parseDtlsDatagram(data, keysByEpoch);
485
+
486
+ for (let i = 0; i < records.length; i++) {
487
+ processRecord(records[i]);
488
+
489
+ // After CCS, keys are newly available — re-decrypt remaining epoch>0 records
490
+ if (records[i].type === CT.CHANGE_CIPHER_SPEC && ctx.keys[1]) {
491
+ let readKeys = ctx.keys[1].read;
492
+ for (let j = i + 1; j < records.length; j++) {
493
+ if (!records[j].encrypted && records[j].epoch > 0 && readKeys) {
494
+ try {
495
+ records[j].content = decryptDtls12(records[j].content, readKeys.key, readKeys.iv, records[j].epoch, records[j].seq, records[j].type);
496
+ records[j].encrypted = true;
497
+ } catch(e) {
498
+ }
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+
505
+ function processRecord(record) {
506
+ if (record.type === CT.HANDSHAKE) {
507
+ processHandshakeRecord(record.content, record.epoch, record.encrypted);
508
+ } else if (record.type === CT.APPLICATION_DATA) {
509
+ if (ctx.state === 'connected') {
510
+ ev.emit('data', record.content);
511
+ }
512
+ } else if (record.type === CT.CHANGE_CIPHER_SPEC) {
513
+ // DTLS 1.2: peer sent CCS — derive keys if not yet done
514
+ ctx.remoteCcsSeen = true;
515
+ if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && !ctx.keys[1]) {
516
+ let ts = tls.getTrafficSecrets();
517
+ if (ts.masterSecret) {
518
+ let d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, tls.getCipher(), isServer);
519
+ ctx.keys[1] = {
520
+ write: { key: d12.writeKey, iv: d12.writeIv },
521
+ read: { key: d12.readKey, iv: d12.readIv },
522
+ };
523
+ ctx.writeSeq[1] = 0;
524
+ }
525
+ }
526
+ } else if (record.type === CT.ACK) {
527
+ processAck(record.content);
528
+ } else if (record.type === CT.ALERT) {
529
+ let level = record.content[0];
530
+ let desc = record.content[1];
531
+ if (desc === 0) {
532
+ // close_notify
533
+ ctx.state = 'closed';
534
+ ev.emit('close');
535
+ } else {
536
+ ev.emit('error', new Error('DTLS alert: level=' + level + ' desc=' + desc));
537
+ }
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Process a handshake record. Handles reassembly and feeds to TLSSession.
543
+ */
544
+ function processHandshakeRecord(data, epoch, encrypted) {
545
+ // A handshake record may contain multiple handshake messages
546
+ let off = 0;
547
+ while (off + 12 <= data.length) {
548
+ let parsed = parse_dtls_handshake(data.subarray(off));
549
+
550
+ let msgSeq = parsed.msg_seq;
551
+ let totalLen = parsed.length;
552
+ let fragOffset = parsed.frag_offset;
553
+ let fragLen = parsed.frag_length;
554
+
555
+ // Is this a complete message or a fragment?
556
+ if (fragOffset === 0 && fragLen === totalLen) {
557
+ // Complete message — feed directly
558
+ deliverHandshakeMessage(parsed.type, parsed.body, msgSeq);
559
+ } else {
560
+ // Fragment — reassemble
561
+ reassembleFragment(parsed);
562
+ }
563
+
564
+ off += 12 + fragLen;
565
+ }
566
+
567
+ // Received handshake data → cancel retransmit (implicit ACK)
568
+ cancelRetransmit();
569
+ }
570
+
571
+ /**
572
+ * Reassemble a fragmented handshake message.
573
+ */
574
+ function reassembleFragment(parsed) {
575
+ let key = parsed.msg_seq;
576
+ if (!(key in ctx.fragments)) {
577
+ ctx.fragments[key] = { totalLength: parsed.length, type: parsed.type, parts: {} };
578
+ }
579
+ let frag = ctx.fragments[key];
580
+ frag.parts[parsed.frag_offset] = parsed.body;
581
+
582
+ // Check if complete
583
+ let assembled = new Uint8Array(frag.totalLength);
584
+ let filled = 0;
585
+ let offsets = Object.keys(frag.parts).map(Number).sort((a, b) => a - b);
586
+
587
+ for (let i = 0; i < offsets.length; i++) {
588
+ let part = frag.parts[offsets[i]];
589
+ if (offsets[i] !== filled) return; // gap — not complete yet
590
+ assembled.set(part, offsets[i]);
591
+ filled = offsets[i] + part.length;
592
+ }
593
+
594
+ if (filled < frag.totalLength) return; // not complete
595
+
596
+ // Complete — deliver
597
+ delete ctx.fragments[key];
598
+ deliverHandshakeMessage(frag.type, assembled, key);
599
+ }
600
+
601
+ /**
602
+ * Deliver a complete handshake message to TLSSession.
603
+ * Strips DTLS reconstruction → builds TLS format.
604
+ */
605
+ function deliverHandshakeMessage(type, body, msgSeq) {
606
+ // Skip if we've already processed this msg_seq
607
+ if (msgSeq < ctx.nextReadMsgSeq) return;
608
+ ctx.nextReadMsgSeq = msgSeq + 1;
609
+
610
+
611
+ // Check for HelloVerifyRequest (DTLS 1.2 server→client, type=3)
612
+ if (type === 3 && !isServer) {
613
+ let hvr = parse_hello_verify_request(body);
614
+ triggerClientHelloWithCookie(hvr.cookie);
615
+ return;
616
+ }
617
+
618
+ // Build TLS-format message: type(1) + length(3) + body
619
+ let tlsMsg = build_message(type, body);
620
+
621
+ // Set incoming msg_seq for the transcriptHook (one-shot — cleared after first push)
622
+ ctx._incomingMsgSeq = msgSeq;
623
+
624
+ // Feed to TLSSession (transcriptHook will convert to DTLS format if needed)
625
+ tls.message(tlsMsg);
626
+
627
+ // Update selectedVersion if just negotiated
628
+ if (ctx.selectedVersion === null && tls.context.selected_version) {
629
+ ctx.selectedVersion = tls.context.selected_version;
630
+ if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
631
+ fixTranscriptForDtls12();
632
+ }
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Trigger a new ClientHello with cookie (DTLS 1.2 HVR response).
638
+ */
639
+ function triggerClientHelloWithCookie(cookie) {
640
+ if (!ctx.savedClientHello) return;
641
+
642
+ // Build CH2 by inserting cookie into saved CH1
643
+ // RFC 6347: "the client MUST use the same parameter values"
644
+ let ch2 = insertCookieIntoClientHello(ctx.savedClientHello, cookie);
645
+
646
+ // Reset state
647
+ tls.context.transcript = [];
648
+ tls.context.hello_sent = true;
649
+ tls.context.dtls_cookie = cookie;
650
+ tls.context.message_sent_seq = 0;
651
+ ctx.nextReadMsgSeq = 0;
652
+ ctx.currentFlight = [];
653
+ transcriptMsgSeqs = [];
654
+
655
+ // Push CH2 to transcript (transcriptHook will store TLS format for now,
656
+ // and fixTranscriptForDtls12() will convert when version is determined)
657
+ ctx._incomingMsgSeq = undefined; // not incoming — use message_sent_seq
658
+ tls.context.transcript.push(
659
+ tls.context.transcriptHook ? tls.context.transcriptHook(ch2) : ch2
660
+ );
661
+
662
+ // Build DTLS message and send
663
+ let dtlsMsg = build_dtls_handshake(ch2, 0);
664
+ let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
665
+ ctx.currentFlight = [record];
666
+ ev.emit('packet', record);
667
+
668
+ tls.context.message_sent_seq = 1;
669
+ startRetransmitTimer();
670
+ }
671
+
672
+
673
+ // ============================================================
674
+ // Flight tracking + retransmission
675
+ // ============================================================
676
+
677
+ function startRetransmitTimer() {
678
+ cancelRetransmit();
679
+ ctx.retransmitTimer = setTimeout(function() {
680
+ if (ctx.retransmitCount >= ctx.maxRetransmits) {
681
+ ev.emit('error', new Error('DTLS handshake timeout — max retransmits exceeded'));
682
+ ctx.state = 'closed';
683
+ ev.emit('close');
684
+ return;
685
+ }
686
+
687
+ // Retransmit entire current flight
688
+ for (let i = 0; i < ctx.currentFlight.length; i++) {
689
+ ev.emit('packet', ctx.currentFlight[i]);
690
+ }
691
+
692
+ ctx.retransmitCount++;
693
+ ctx.retransmitTimeout = Math.min(ctx.retransmitTimeout * 2, 60000);
694
+ startRetransmitTimer();
695
+ }, ctx.retransmitTimeout);
696
+
697
+ if (ctx.retransmitTimer.unref) ctx.retransmitTimer.unref();
698
+ }
699
+
700
+ function cancelRetransmit() {
701
+ if (ctx.retransmitTimer !== null) {
702
+ clearTimeout(ctx.retransmitTimer);
703
+ ctx.retransmitTimer = null;
704
+ }
705
+ }
706
+
707
+ function startNewFlight() {
708
+ ctx.currentFlight = [];
709
+ ctx.retransmitCount = 0;
710
+ ctx.retransmitTimeout = 1000;
711
+ }
712
+
713
+
714
+ // ============================================================
715
+ // ACK (DTLS 1.3)
716
+ // ============================================================
717
+
718
+ function processAck(content) {
719
+ let acks = parseDtlsAck(content);
720
+ // ACK received — flight was acknowledged
721
+ cancelRetransmit();
722
+ startNewFlight();
723
+ }
724
+
725
+ function sendAck(epoch, recordsToAck) {
726
+ if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return;
727
+ let payload = buildDtlsAck(recordsToAck);
728
+ let record = buildRecord(epoch, CT.ACK, payload);
729
+ ev.emit('packet', record);
730
+ }
731
+
732
+
733
+ // ============================================================
734
+ // Handshake completion
735
+ // ============================================================
736
+
737
+ tls.on('secureConnect', function() {
738
+ ctx.state = 'connected';
739
+ cancelRetransmit();
740
+ startNewFlight();
741
+ ev.emit('connect');
742
+ });
743
+
744
+ tls.on('error', function(e) {
745
+ ev.emit('error', e);
746
+ });
747
+
748
+ tls.on('session', function(ticket) {
749
+ ev.emit('session', ticket);
750
+ });
751
+
752
+
753
+ // ============================================================
754
+ // Application data
755
+ // ============================================================
756
+
757
+ function send(data) {
758
+ if (ctx.state !== 'connected') {
759
+ ev.emit('error', new Error('Cannot send before handshake complete'));
760
+ return;
761
+ }
762
+ if (typeof data === 'string') data = new TextEncoder().encode(data);
763
+
764
+ let epoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1;
765
+ let record = buildRecord(epoch, CT.APPLICATION_DATA, data);
766
+ ev.emit('packet', record);
767
+ }
768
+
769
+
770
+ // ============================================================
771
+ // Close
772
+ // ============================================================
773
+
774
+ function close() {
775
+ if (ctx.state === 'closed') return;
776
+ let epoch = ctx.state === 'connected' ? (ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1) : 0;
777
+ sendAlertRecord(epoch, new Uint8Array([1, 0])); // warning, close_notify
778
+ ctx.state = 'closed';
779
+ cancelRetransmit();
780
+ ev.emit('close');
781
+ }
782
+
783
+
784
+ // ============================================================
785
+ // Server: HelloVerifyRequest (DTLS 1.2)
786
+ // ============================================================
787
+
788
+ tls.on('hello', function() {
789
+ // Version is detected lazily in handshakeSecrets and message handlers
790
+
791
+ // DTLS 1.2 server: optionally send HelloVerifyRequest
792
+ if (isServer && ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && !ctx.hvrDone && options.useCookies === true) {
793
+ ctx.hvrDone = true;
794
+ let cookie = new Uint8Array(32);
795
+ for (let i = 0; i < 32; i++) cookie[i] = Math.floor(Math.random() * 256);
796
+ ctx.hvrCookie = cookie;
797
+
798
+ let hvrBody = build_hello_verify_request({ cookie: cookie });
799
+ let hvrMsg = build_dtls_handshake(
800
+ build_message(3, hvrBody),
801
+ 0
802
+ );
803
+ let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
804
+ ev.emit('packet', record);
805
+ }
806
+ });
807
+
808
+
809
+ // ============================================================
810
+ // Public API
811
+ // ============================================================
812
+
813
+ let api = {
814
+ /** Feed an incoming UDP datagram. */
815
+ feedDatagram: feedDatagram,
816
+
817
+ /** Send application data (after connect). */
818
+ send: send,
819
+
820
+ /** Close the DTLS session. */
821
+ close: close,
822
+
823
+ /** Configure the session (passes through to TLSSession). */
824
+ set_context: function(opts) { tls.set_context(opts); },
825
+
826
+ /** Register event listener. */
827
+ on: function(name, fn) { ev.on(name, fn); },
828
+ off: function(name, fn) { ev.off(name, fn); },
829
+
830
+ /** Access to internal TLSSession (for advanced use). */
831
+ get tls() { return tls; },
832
+
833
+ /** Current DTLS state. */
834
+ get state() { return ctx.state; },
835
+
836
+ /** Selected DTLS version (0xFEFC or 0xFEFD). */
837
+ get version() { return ctx.selectedVersion; },
838
+
839
+ /** Whether handshake is complete. */
840
+ get connected() { return ctx.state === 'connected'; },
841
+
842
+ /** Full negotiation result. */
843
+ getNegotiationResult: function() { return tls.getNegotiationResult(); },
844
+
845
+ /** Negotiated ALPN. */
846
+ getALPN: function() { return tls.getALPN(); },
847
+
848
+ /** Peer certificate. */
849
+ getPeerCertificate: function() { return tls.getPeerCertificate(); },
850
+ };
851
+
852
+ for (let k in api) {
853
+ if (Object.prototype.hasOwnProperty.call(api, k)) {
854
+ if (typeof Object.getOwnPropertyDescriptor(api, k).get === 'function') {
855
+ Object.defineProperty(this, k, Object.getOwnPropertyDescriptor(api, k));
856
+ } else {
857
+ this[k] = api[k];
858
+ }
859
+ }
860
+ }
861
+
862
+ return this;
863
+ }
864
+
865
+ export default DTLSSession;