lemon-tls 0.4.0 → 0.5.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.
@@ -27,6 +27,7 @@
27
27
  */
28
28
 
29
29
  import { EventEmitter } from 'node:events';
30
+ import crypto from 'node:crypto';
30
31
 
31
32
  import TLSSession from './tls_session.js';
32
33
  import createSecureContext from './secure_context.js';
@@ -38,6 +39,7 @@ import {
38
39
  build_message,
39
40
  build_hello_verify_request,
40
41
  parse_hello_verify_request,
42
+ parse_hello,
41
43
  } from './wire.js';
42
44
 
43
45
  import {
@@ -59,6 +61,8 @@ import {
59
61
  parseDtlsAck,
60
62
  } from './record.js';
61
63
 
64
+ import { timingSafeEqualU8 } from './utils.js';
65
+
62
66
 
63
67
  // ============================================================
64
68
  // Helpers
@@ -130,6 +134,15 @@ function DTLSSession(options) {
130
134
  isServer: isServer,
131
135
  mtu: options.mtu || 1200,
132
136
 
137
+ // Upper bound on a single reassembled handshake message. The DTLS handshake
138
+ // header carries a 24-bit length (up to 16MB), which is attacker-controlled
139
+ // on the very first fragment. Without a cap, a peer could send one fragment
140
+ // claiming length 0xFFFFFF for several msg_seqs and force large allocations
141
+ // before anything is authenticated. 256KB comfortably fits a real
142
+ // certificate-chain flight while bounding the damage. Configurable via
143
+ // options.maxHandshakeMessageSize.
144
+ maxHandshakeMessageSize: options.maxHandshakeMessageSize || (256 * 1024),
145
+
133
146
  // Version (determined after negotiation)
134
147
  selectedVersion: null, // DTLS version (0xFEFC or 0xFEFD)
135
148
 
@@ -146,12 +159,17 @@ function DTLSSession(options) {
146
159
  cipherSuite: null,
147
160
  hashName: null,
148
161
 
149
- // Fragment reassembly: msg_seq → { totalLength, parts: { offset: Uint8Array } }
162
+ // Fragment reassembly: msg_seq → { totalLength, type, chunks:[{off,end,body}], covered }
150
163
  fragments: {},
151
164
 
152
165
  // Incoming handshake msg_seq tracking
153
166
  nextReadMsgSeq: 0,
154
167
 
168
+ // Out-of-order handshake buffering (RFC 6347 §4.2.4 / RFC 9147 §5.2):
169
+ // complete messages that arrive ahead of nextReadMsgSeq are held here,
170
+ // keyed by msg_seq, and drained in order once the gap is filled.
171
+ pendingMessages: {}, // msg_seq → { type, body }
172
+
155
173
  // Flight tracking for retransmission
156
174
  currentFlight: [], // array of { epoch, data: Uint8Array (complete datagram) }
157
175
  retransmitTimer: null,
@@ -425,9 +443,18 @@ function DTLSSession(options) {
425
443
  * Returns array of DTLS handshake message fragments (each with reconstruction header).
426
444
  */
427
445
  function fragmentMessage(dtlsMsg, tlsMsg, msgSeq) {
428
- let overhead = 13 + 16; // record header + AEAD tag (approximate)
429
- let maxFragment = ctx.mtu - overhead;
430
-
446
+ // Record-layer overhead we must leave room for inside one datagram/MTU:
447
+ // DTLS 1.2: 13-byte classic record header + 16-byte AEAD tag (+8 explicit
448
+ // nonce for GCM, but that lives inside the encrypted payload the
449
+ // record layer builds, so we keep a small safety margin).
450
+ // DTLS 1.3: 5-byte unified header (2-byte seq + length) + 16-byte AEAD tag.
451
+ // Using the correct, smaller 1.3 overhead avoids over-fragmenting.
452
+ let isDtls13 = ctx.selectedVersion === DTLS_VERSION.DTLS1_3;
453
+ let recordOverhead = isDtls13 ? (5 + 16) : (13 + 16 + 8);
454
+ let maxFragment = ctx.mtu - recordOverhead;
455
+
456
+ // The DTLS handshake message header is 12 bytes; the body is what we chunk.
457
+ const HS_HDR = 12;
431
458
  if (dtlsMsg.length <= maxFragment) return [dtlsMsg];
432
459
 
433
460
  // Need to fragment: split the TLS body into chunks
@@ -436,8 +463,11 @@ function DTLSSession(options) {
436
463
  let fragments = [];
437
464
  let offset = 0;
438
465
 
466
+ // Each fragment carries its own 12-byte handshake header, so the payload
467
+ // budget per fragment is maxFragment - HS_HDR.
468
+ let maxChunk = Math.max(1, maxFragment - HS_HDR);
439
469
  while (offset < totalLength) {
440
- let chunkLen = Math.min(maxFragment - 12, totalLength - offset); // 12 = DTLS handshake header
470
+ let chunkLen = Math.min(maxChunk, totalLength - offset);
441
471
  let frag = build_dtls_handshake(tlsMsg, msgSeq, offset, chunkLen);
442
472
  fragments.push(frag);
443
473
  offset += chunkLen;
@@ -483,7 +513,18 @@ function DTLSSession(options) {
483
513
 
484
514
  let records = parseDtlsDatagram(data, keysByEpoch);
485
515
 
516
+ // RFC 9147 §7: collect the record numbers of handshake records we received
517
+ // in this datagram so we can ACK them once, after processing. Only for
518
+ // DTLS 1.3, and only while we're still handshaking (post-handshake ACKs of
519
+ // NewSessionTicket etc. are optional and skipped here for simplicity).
520
+ let ackable = [];
521
+
486
522
  for (let i = 0; i < records.length; i++) {
523
+ if (records[i].type === CT.HANDSHAKE &&
524
+ ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
525
+ ackable.push({ epoch: records[i].epoch, seq: records[i].seq });
526
+ }
527
+
487
528
  processRecord(records[i]);
488
529
 
489
530
  // After CCS, keys are newly available — re-decrypt remaining epoch>0 records
@@ -500,6 +541,14 @@ function DTLSSession(options) {
500
541
  }
501
542
  }
502
543
  }
544
+
545
+ // Send a single ACK covering all handshake records received in this datagram.
546
+ // We ACK from the highest epoch for which we already have write keys (so the
547
+ // ACK itself is protected once handshake keys exist; epoch 0 before that).
548
+ if (ackable.length > 0) {
549
+ let ackEpoch = ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0);
550
+ sendAck(ackEpoch, ackable);
551
+ }
503
552
  }
504
553
 
505
554
  function processRecord(record) {
@@ -569,33 +618,68 @@ function DTLSSession(options) {
569
618
  }
570
619
 
571
620
  /**
572
- * Reassemble a fragmented handshake message.
621
+ * Reassemble a (possibly fragmented) handshake message.
622
+ *
623
+ * RFC 6347 §4.2.3 / RFC 9147 §5.5: fragments may overlap and arrive out of
624
+ * order, because a retransmission can re-fragment the same message with
625
+ * different boundaries (e.g. after a PMTU change). We therefore copy each
626
+ * fragment's bytes into a full-length buffer and track coverage as a set of
627
+ * merged byte-ranges, rather than requiring each fragment to start exactly
628
+ * where the previous one ended.
573
629
  */
574
630
  function reassembleFragment(parsed) {
575
631
  let key = parsed.msg_seq;
576
- if (!(key in ctx.fragments)) {
577
- ctx.fragments[key] = { totalLength: parsed.length, type: parsed.type, parts: {} };
632
+
633
+ // Ignore fragments for messages we've already delivered.
634
+ if (key < ctx.nextReadMsgSeq) return;
635
+
636
+ // DoS guard: the 24-bit `length` is attacker-controlled on the first
637
+ // fragment. Reject anything claiming more than our cap BEFORE allocating,
638
+ // so a peer can't force huge buffers with a single unauthenticated packet.
639
+ if (parsed.length > ctx.maxHandshakeMessageSize) {
640
+ ev.emit('error', new Error('DTLS handshake message exceeds maxHandshakeMessageSize (' +
641
+ parsed.length + ' > ' + ctx.maxHandshakeMessageSize + ')'));
642
+ return;
578
643
  }
644
+
579
645
  let frag = ctx.fragments[key];
580
- frag.parts[parsed.frag_offset] = parsed.body;
646
+ if (!frag) {
647
+ frag = ctx.fragments[key] = {
648
+ totalLength: parsed.length,
649
+ type: parsed.type,
650
+ buf: new Uint8Array(parsed.length),
651
+ ranges: [], // sorted, merged [start, end) covered ranges
652
+ };
653
+ }
581
654
 
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);
655
+ let start = parsed.frag_offset;
656
+ let end = parsed.frag_offset + parsed.frag_length;
586
657
 
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
- }
658
+ // Bounds guard against a malformed fragment claiming to exceed totalLength.
659
+ if (end > frag.totalLength) return;
593
660
 
594
- if (filled < frag.totalLength) return; // not complete
661
+ // Copy the fragment bytes in (overlaps simply overwrite with identical data).
662
+ frag.buf.set(parsed.body.subarray(0, parsed.frag_length), start);
595
663
 
596
- // Complete — deliver
597
- delete ctx.fragments[key];
598
- deliverHandshakeMessage(frag.type, assembled, key);
664
+ // Merge [start, end) into the covered-range set.
665
+ frag.ranges.push([start, end]);
666
+ frag.ranges.sort((a, b) => a[0] - b[0]);
667
+ let merged = [];
668
+ for (let r of frag.ranges) {
669
+ if (merged.length && r[0] <= merged[merged.length - 1][1]) {
670
+ // Overlapping/adjacent → extend the previous range.
671
+ if (r[1] > merged[merged.length - 1][1]) merged[merged.length - 1][1] = r[1];
672
+ } else {
673
+ merged.push([r[0], r[1]]);
674
+ }
675
+ }
676
+ frag.ranges = merged;
677
+
678
+ // Complete when a single range covers [0, totalLength).
679
+ if (merged.length === 1 && merged[0][0] === 0 && merged[0][1] === frag.totalLength) {
680
+ delete ctx.fragments[key];
681
+ deliverHandshakeMessage(frag.type, frag.buf, key);
682
+ }
599
683
  }
600
684
 
601
685
  /**
@@ -603,18 +687,80 @@ function DTLSSession(options) {
603
687
  * Strips DTLS reconstruction → builds TLS format.
604
688
  */
605
689
  function deliverHandshakeMessage(type, body, msgSeq) {
606
- // Skip if we've already processed this msg_seq
690
+ // Skip if we've already processed this msg_seq (duplicate / retransmit).
607
691
  if (msgSeq < ctx.nextReadMsgSeq) return;
608
- ctx.nextReadMsgSeq = msgSeq + 1;
609
-
610
692
 
611
693
  // Check for HelloVerifyRequest (DTLS 1.2 server→client, type=3)
612
694
  if (type === 3 && !isServer) {
695
+ ctx.nextReadMsgSeq = msgSeq + 1;
613
696
  let hvr = parse_hello_verify_request(body);
614
697
  triggerClientHelloWithCookie(hvr.cookie);
615
698
  return;
616
699
  }
617
700
 
701
+ // ---- Server-side HelloVerifyRequest cookie exchange (RFC 6347 §4.2.1) ----
702
+ // For DTLS 1.2 with cookies enabled, the first ClientHello carries no cookie
703
+ // (or an empty one). We answer with a HelloVerifyRequest and do NOT process
704
+ // the ClientHello — that proves return-routability (the client must be at the
705
+ // address it claims before we allocate any handshake state). Only a second
706
+ // ClientHello echoing the correct cookie is processed.
707
+ //
708
+ // Note: msg_seq is NOT advanced for a rejected/HVR-triggering ClientHello, so
709
+ // the client's retried CH (which reuses msg_seq per RFC 6347) is still accepted.
710
+ if (type === 1 && isServer && options.useCookies === true &&
711
+ ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) {
712
+
713
+ let ch = null;
714
+ try { ch = parse_hello({ kind: 'client', body: body }); } catch (e) { ch = null; }
715
+ let incomingCookie = (ch && ch.dtls_cookie) ? ch.dtls_cookie : new Uint8Array(0);
716
+
717
+ if (incomingCookie.length === 0) {
718
+ // First ClientHello → send HVR with a fresh cookie. Don't process the CH,
719
+ // don't advance msg_seq (the retry reuses the same msg_seq).
720
+ sendHelloVerifyRequest();
721
+ return;
722
+ }
723
+
724
+ // Second ClientHello: the cookie MUST match what we issued.
725
+ if (!ctx.hvrCookie || !timingSafeEqualU8(incomingCookie, ctx.hvrCookie)) {
726
+ // Cookie mismatch → silently drop (RFC 6347: server discards). Don't
727
+ // advance msg_seq; a correct retry can still arrive.
728
+ return;
729
+ }
730
+ // Cookie verified → fall through and process the ClientHello normally.
731
+ }
732
+
733
+ // ---- In-order delivery (RFC 6347 §4.2.4 / RFC 9147 §5.2) ----
734
+ // DTLS handshake messages must be handed to the TLS state machine strictly
735
+ // in msg_seq order. If this message is ahead of what we expect, buffer it
736
+ // and wait for the gap to fill. If it's the next expected one, feed it, then
737
+ // drain any consecutive messages that were buffered earlier.
738
+ if (msgSeq > ctx.nextReadMsgSeq) {
739
+ // Out of order — hold it (ignore a duplicate already buffered).
740
+ if (!(msgSeq in ctx.pendingMessages)) {
741
+ ctx.pendingMessages[msgSeq] = { type: type, body: body };
742
+ }
743
+ return;
744
+ }
745
+
746
+ // msgSeq === nextReadMsgSeq → feed it and advance.
747
+ feedHandshakeToTls(type, body, msgSeq);
748
+ ctx.nextReadMsgSeq = msgSeq + 1;
749
+
750
+ // Drain consecutively-numbered buffered messages.
751
+ while (ctx.nextReadMsgSeq in ctx.pendingMessages) {
752
+ let next = ctx.pendingMessages[ctx.nextReadMsgSeq];
753
+ delete ctx.pendingMessages[ctx.nextReadMsgSeq];
754
+ feedHandshakeToTls(next.type, next.body, ctx.nextReadMsgSeq);
755
+ ctx.nextReadMsgSeq++;
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Feed one in-order handshake message to the TLSSession, converting from DTLS
761
+ * to TLS wire format and driving version detection.
762
+ */
763
+ function feedHandshakeToTls(type, body, msgSeq) {
618
764
  // Build TLS-format message: type(1) + length(3) + body
619
765
  let tlsMsg = build_message(type, body);
620
766
 
@@ -633,6 +779,23 @@ function DTLSSession(options) {
633
779
  }
634
780
  }
635
781
 
782
+ /**
783
+ * Server: build and send a HelloVerifyRequest with a fresh cookie (DTLS 1.2).
784
+ * The cookie is a CSPRNG value stored on the context; a matching cookie must
785
+ * come back in the client's second ClientHello (verified in
786
+ * deliverHandshakeMessage) before we process the handshake.
787
+ */
788
+ function sendHelloVerifyRequest() {
789
+ let cookie = new Uint8Array(crypto.randomBytes(32));
790
+ ctx.hvrCookie = cookie;
791
+ ctx.hvrDone = true;
792
+
793
+ // HVR always uses msg_seq 0 and is sent in the clear (epoch 0).
794
+ let hvrMsg = build_dtls_handshake(build_message(3, build_hello_verify_request({ cookie: cookie })), 0);
795
+ let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
796
+ ev.emit('packet', record);
797
+ }
798
+
636
799
  /**
637
800
  * Trigger a new ClientHello with cookie (DTLS 1.2 HVR response).
638
801
  */
@@ -650,6 +813,8 @@ function DTLSSession(options) {
650
813
  tls.context.message_sent_seq = 0;
651
814
  ctx.nextReadMsgSeq = 0;
652
815
  ctx.currentFlight = [];
816
+ ctx.fragments = {}; // discard any partial reassembly from CH1 flight
817
+ ctx.pendingMessages = {}; // discard any out-of-order buffer from CH1 flight
653
818
  transcriptMsgSeqs = [];
654
819
 
655
820
  // Push CH2 to transcript (transcriptHook will store TLS format for now,
@@ -734,6 +899,16 @@ function DTLSSession(options) {
734
899
  // Handshake completion
735
900
  // ============================================================
736
901
 
902
+ // Forward 'clienthello' so server-side callers can inspect the peer's
903
+ // offered extensions (tls.getRemoteExtension / message.extensions) and
904
+ // set answering extensions via set_context({ local_extensions }) BEFORE
905
+ // the ServerHello is built — the emit is synchronous within ClientHello
906
+ // processing, ahead of hello construction. This is how DTLS-SRTP's
907
+ // use_srtp answer (RFC 5764 §4.1) gets negotiated generically.
908
+ tls.on('clienthello', function(data, message) {
909
+ ev.emit('clienthello', data, message);
910
+ });
911
+
737
912
  tls.on('secureConnect', function() {
738
913
  ctx.state = 'connected';
739
914
  cancelRetransmit();
@@ -786,23 +961,12 @@ function DTLSSession(options) {
786
961
  // ============================================================
787
962
 
788
963
  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
- }
964
+ // Version is detected lazily in handshakeSecrets and message handlers.
965
+ //
966
+ // Note: DTLS 1.2 HelloVerifyRequest is handled earlier, in
967
+ // deliverHandshakeMessage — the first (cookieless) ClientHello never reaches
968
+ // TLSSession, so by the time 'hello' fires here the cookie has already been
969
+ // verified. Nothing to do for HVR at this point.
806
970
  });
807
971
 
808
972
 
@@ -830,6 +994,21 @@ function DTLSSession(options) {
830
994
  /** Access to internal TLSSession (for advanced use). */
831
995
  get tls() { return tls; },
832
996
 
997
+ /**
998
+ * Export keying material (RFC 5705 / RFC 8446 §7.5).
999
+ * DTLS-SRTP: exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp') — no context.
1000
+ * Returns Buffer, or null before secrets are available.
1001
+ */
1002
+ exportKeyingMaterial: function(length, label, context_value) {
1003
+ return tls.exportKeyingMaterial(length, label, context_value);
1004
+ },
1005
+
1006
+ /** Peer's hello extensions: [{ type, name, data, value }]. */
1007
+ getRemoteExtensions: function() { return tls.getRemoteExtensions(); },
1008
+
1009
+ /** One peer extension by numeric type (e.g. 14 = use_srtp), or null. */
1010
+ getRemoteExtension: function(type) { return tls.getRemoteExtension(type); },
1011
+
833
1012
  /** Current DTLS state. */
834
1013
  get state() { return ctx.state; },
835
1014
 
@@ -81,6 +81,13 @@ function DTLSSocket(udpSocket, options) {
81
81
  ev.emit('session', ticket);
82
82
  });
83
83
 
84
+ // Server-side callers use this to inspect the peer's offered extensions
85
+ // (session.getRemoteExtension) and answer via set_context BEFORE the
86
+ // ServerHello is built — e.g. DTLS-SRTP's use_srtp (RFC 5764).
87
+ session.on('clienthello', function(data, message) {
88
+ ev.emit('clienthello', data, message);
89
+ });
90
+
84
91
  /**
85
92
  * Feed an incoming UDP datagram (filtered by remote address/port).
86
93
  * Called by the UDP socket's message handler or by DTLSServer.
@@ -122,6 +129,21 @@ function DTLSSocket(udpSocket, options) {
122
129
  self.getALPN = function() { return session.getALPN(); };
123
130
  self.getPeerCertificate = function() { return session.getPeerCertificate(); };
124
131
 
132
+ /** RFC 5705 / RFC 8446 §7.5 exporter (Node tls.TLSSocket parity).
133
+ * DTLS-SRTP: exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp') — no context.
134
+ * Returns Buffer, or null before secrets are available. */
135
+ self.exportKeyingMaterial = function(length, label, context_value) {
136
+ return session.exportKeyingMaterial(length, label, context_value);
137
+ };
138
+
139
+ /** Peer hello extensions / single extension by numeric type. */
140
+ self.getRemoteExtensions = function() { return session.getRemoteExtensions(); };
141
+ self.getRemoteExtension = function(type) { return session.getRemoteExtension(type); };
142
+
143
+ /** Pass config through to the underlying session (e.g. local_extensions
144
+ * from a 'clienthello' handler). */
145
+ self.set_context = function(opts) { session.set_context(opts); };
146
+
125
147
  /** Access to internal DTLSSession (for advanced use). */
126
148
  self.session = session;
127
149
 
package/src/record.js CHANGED
@@ -297,6 +297,37 @@ function getNonce12(salt4, explicit8) {
297
297
  return out;
298
298
  }
299
299
 
300
+ /**
301
+ * TLS/DTLS 1.2 ChaCha20-Poly1305 nonce (RFC 7905 §2).
302
+ *
303
+ * Unlike AES-GCM, ChaCha20-Poly1305 in TLS 1.2 has NO explicit per-record nonce
304
+ * on the wire. The 12-byte nonce is the record's 64-bit sequence number, left-
305
+ * padded to 12 bytes, XORed with the 12-byte client/server write IV derived from
306
+ * the key block. This is the same construction TLS 1.3 uses — the difference from
307
+ * GCM (salt(4) || explicit(8)) is why the two must be handled separately.
308
+ *
309
+ * For DTLS the "sequence number" input is the 16-bit epoch concatenated with the
310
+ * 48-bit record sequence number, i.e. the same 64-bit value used in the AAD, so
311
+ * callers pass the full record seq and (for DTLS) fold the epoch into it.
312
+ */
313
+ function getChachaNonce12(iv12, seq) {
314
+ const nonce = new Uint8Array(12);
315
+ nonce.set(iv12, 0);
316
+ // XOR the 64-bit seq into the LAST 8 bytes (big-endian), leaving the first 4
317
+ // bytes of the IV untouched — identical layout to TLS 1.3's getNonce.
318
+ const hi = (seq / 0x100000000) | 0;
319
+ const lo = seq >>> 0;
320
+ nonce[4] ^= (hi >>> 24) & 0xff;
321
+ nonce[5] ^= (hi >>> 16) & 0xff;
322
+ nonce[6] ^= (hi >>> 8) & 0xff;
323
+ nonce[7] ^= hi & 0xff;
324
+ nonce[8] ^= (lo >>> 24) & 0xff;
325
+ nonce[9] ^= (lo >>> 16) & 0xff;
326
+ nonce[10] ^= (lo >>> 8) & 0xff;
327
+ nonce[11] ^= lo & 0xff;
328
+ return nonce;
329
+ }
330
+
300
331
  /**
301
332
  * Encode sequence number as 8-byte big-endian.
302
333
  * Perf: uses Number arithmetic (seq < 2^53 is always safe for TLS).
@@ -634,11 +665,36 @@ function buildDtlsAad12(epoch, seq, type, plaintextLen) {
634
665
  return aad;
635
666
  }
636
667
 
637
- /** Encrypt DTLS 1.2 record payload. Returns: explicit_nonce(8) || ciphertext || tag(16). */
668
+ /** Encrypt DTLS 1.2 record payload. Returns: [explicit_nonce(8) ||] ciphertext || tag(16).
669
+ *
670
+ * AES-GCM: prepends an 8-byte explicit nonce (the record seq); nonce = salt(4) || seq(8).
671
+ * ChaCha20-Poly1305 (RFC 7905): NO explicit nonce on the wire; nonce = iv(12) XOR
672
+ * (epoch(16) || seq(48)). Detected by ivSalt length (12 = ChaCha, 4 = GCM).
673
+ */
638
674
  function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
675
+ let aad = buildDtlsAad12(epoch, seq, type, pt.length);
676
+
677
+ // ChaCha20-Poly1305: full 12-byte IV, no explicit nonce (RFC 7905).
678
+ if (ivSalt.length === 12) {
679
+ // DTLS folds the 16-bit epoch above the 48-bit seq to form the 64-bit
680
+ // sequence input, matching the AAD's epoch||seq (RFC 6347 / RFC 9147).
681
+ let seq64 = epoch * 0x1000000000000 + seq;
682
+ let nonce = getChachaNonce12(ivSalt, seq64);
683
+ let cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
684
+ cipher.setAAD(aad, { plaintextLength: pt.length });
685
+ let ct = cipher.update(pt);
686
+ cipher.final();
687
+ let tag = cipher.getAuthTag();
688
+ // No explicit nonce prefix for ChaCha.
689
+ let out = new Uint8Array(ct.length + tag.length);
690
+ out.set(new Uint8Array(ct), 0);
691
+ out.set(new Uint8Array(tag), ct.length);
692
+ return out;
693
+ }
694
+
695
+ // AES-GCM: explicit nonce (= record seq) prepended.
639
696
  let explicit = seqToBytes(seq);
640
697
  let nonce = getNonce12(ivSalt, explicit);
641
- let aad = buildDtlsAad12(epoch, seq, type, pt.length);
642
698
  let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
643
699
  let cipher = crypto.createCipheriv(algo, key, nonce);
644
700
  cipher.setAAD(aad);
@@ -652,8 +708,28 @@ function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
652
708
  return out;
653
709
  }
654
710
 
655
- /** Decrypt DTLS 1.2 record fragment. Input: explicit_nonce(8) || ciphertext || tag(16). */
711
+ /** Decrypt DTLS 1.2 record fragment.
712
+ * AES-GCM input: explicit_nonce(8) || ciphertext || tag(16).
713
+ * ChaCha20 input: ciphertext || tag(16) (no explicit nonce — RFC 7905).
714
+ */
656
715
  function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
716
+ // ChaCha20-Poly1305: no explicit nonce; nonce = iv(12) XOR (epoch||seq).
717
+ if (ivSalt.length === 12) {
718
+ if (fragment.length < 16) throw new Error('DTLS 1.2 ChaCha fragment too short');
719
+ let ct = fragment.slice(0, fragment.length - 16);
720
+ let tag = fragment.slice(fragment.length - 16);
721
+ let seq64 = epoch * 0x1000000000000 + seq;
722
+ let nonce = getChachaNonce12(ivSalt, seq64);
723
+ let aad = buildDtlsAad12(epoch, seq, type, ct.length);
724
+ let decipher = crypto.createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
725
+ decipher.setAAD(aad, { plaintextLength: ct.length });
726
+ decipher.setAuthTag(tag);
727
+ let pt = decipher.update(ct);
728
+ decipher.final();
729
+ return new Uint8Array(pt);
730
+ }
731
+
732
+ // AES-GCM: strip the 8-byte explicit nonce.
657
733
  if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
658
734
  let explicit = fragment.slice(0, 8);
659
735
  let tag = fragment.slice(fragment.length - 16);
@@ -801,10 +877,42 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
801
877
  return record;
802
878
  }
803
879
 
880
+ /**
881
+ * Reconstruct a full DTLS 1.3 record sequence number from its truncated wire
882
+ * encoding (RFC 9147 §4.2.2).
883
+ *
884
+ * @param truncated the low `bits` bits of the seq as sent on the wire
885
+ * @param bits 8 or 16 (how many low bits were transmitted)
886
+ * @param maxSeq highest full seq already accepted in this epoch (-1 if none)
887
+ *
888
+ * Returns the full seq: the value congruent to `truncated` (mod 2^bits) that is
889
+ * closest to maxSeq+1. This tolerates reordering within half a window in each
890
+ * direction, which is what the sliding receive window expects.
891
+ */
892
+ function reconstructDtlsSeq(truncated, bits, maxSeq) {
893
+ let window = Math.pow(2, bits); // 256 or 65536
894
+ let expected = maxSeq + 1; // where we expect the next record
895
+ let high = Math.floor(expected / window) * window;
896
+ let candidate = high + truncated;
897
+
898
+ // Pick whichever of {candidate-window, candidate, candidate+window} is nearest
899
+ // to `expected`, without going negative.
900
+ let best = candidate;
901
+ let bestDist = Math.abs(candidate - expected);
902
+ for (let delta of [-window, window]) {
903
+ let alt = candidate + delta;
904
+ if (alt < 0) continue;
905
+ let dist = Math.abs(alt - expected);
906
+ if (dist < bestDist) { best = alt; bestDist = dist; }
907
+ }
908
+ return best;
909
+ }
910
+
911
+
804
912
  /**
805
913
  * Decrypt a DTLS 1.3 encrypted record.
806
914
  * data: full record bytes (starting with unified header byte)
807
- * keys: { key, iv, snKey, algo? }
915
+ * keys: { key, iv, snKey, algo?, maxReadSeq? }
808
916
  * Returns { epoch, seq, type, content } or null on failure.
809
917
  */
810
918
  function decryptEncryptedDtls13(data, keys) {
@@ -834,7 +942,19 @@ function decryptEncryptedDtls13(data, keys) {
834
942
 
835
943
  // Decrypt record number
836
944
  let rn = maskRecordNumber(keys.snKey, encRn, ciphertext);
837
- let seq = hdr.seqLen2 ? ((rn[0] << 8) | rn[1]) : rn[0];
945
+ let truncated = hdr.seqLen2 ? ((rn[0] << 8) | rn[1]) : rn[0];
946
+
947
+ // RFC 9147 §4.2.2: the wire carries only the low 8 or 16 bits of the record
948
+ // sequence number. Reconstruct the full value by choosing the candidate
949
+ // congruent to `truncated` (mod window) that is closest to the highest seq
950
+ // we've already accepted in this epoch. Without this, seq silently wraps to 0
951
+ // after 2^16 records and every nonce/AAD past that point is wrong.
952
+ //
953
+ // keys.maxReadSeq holds the highest full seq accepted so far (per-epoch key
954
+ // object). It starts undefined → treated as -1 so the first record maps to
955
+ // its own truncated value.
956
+ let seq = reconstructDtlsSeq(truncated, hdr.seqLen2 ? 16 : 8,
957
+ (keys.maxReadSeq === undefined ? -1 : keys.maxReadSeq));
838
958
 
839
959
  // Rebuild AAD with plaintext RN
840
960
  let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
@@ -863,6 +983,13 @@ function decryptEncryptedDtls13(data, keys) {
863
983
  decipher.final();
864
984
 
865
985
  let inner = parseInnerPlaintext(new Uint8Array(pt));
986
+
987
+ // Advance the per-epoch high-water mark only after the tag verified, so a
988
+ // forged/replayed record number can't poison future reconstruction.
989
+ if (keys.maxReadSeq === undefined || seq > keys.maxReadSeq) {
990
+ keys.maxReadSeq = seq;
991
+ }
992
+
866
993
  return {
867
994
  epoch: hdr.epoch,
868
995
  seq: seq,
@@ -1018,6 +1145,7 @@ export {
1018
1145
 
1019
1146
  // TLS 1.2
1020
1147
  getNonce12,
1148
+ getChachaNonce12,
1021
1149
  seqToBytes,
1022
1150
  buildAad12,
1023
1151
  encrypt12,
@@ -28,7 +28,22 @@ function x25519_get_shared_secret(localPrivateRaw, remotePublicRaw) {
28
28
  let pubDer = Buffer.concat([X25519_SPKI_PREFIX, Buffer.from(remotePublicRaw)]);
29
29
  let privObj = crypto.createPrivateKey({ key: privDer, format: 'der', type: 'pkcs8' });
30
30
  let pubObj = crypto.createPublicKey({ key: pubDer, format: 'der', type: 'spki' });
31
- return new Uint8Array(crypto.diffieHellman({ privateKey: privObj, publicKey: pubObj }));
31
+ let secret = new Uint8Array(crypto.diffieHellman({ privateKey: privObj, publicKey: pubObj }));
32
+
33
+ // RFC 7748 §6.1: implementations MUST reject an all-zero shared secret. A peer
34
+ // that sends a low-order/small-order public key can force the X25519 result to
35
+ // be all zeros — a value it knows in advance — which would let it choose the
36
+ // key material. The `break` here is safe (does not leak a secret): the peer's
37
+ // public key is public, so an early exit reveals nothing exploitable.
38
+ let allZero = true;
39
+ for (let i = 0; i < secret.length; i++) {
40
+ if (secret[i] !== 0) { allZero = false; break; }
41
+ }
42
+ if (allZero) {
43
+ throw new Error('X25519: all-zero shared secret — invalid peer public key (RFC 7748 §6.1)');
44
+ }
45
+
46
+ return secret;
32
47
  }
33
48
 
34
49
  /**