lemon-tls 0.9.0 → 0.11.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
@@ -273,6 +273,52 @@ function decryptRecordWithAadView(aadView, ciphertext, key, nonce, algo) {
273
273
  return pt;
274
274
  }
275
275
 
276
+ /**
277
+ * Anti-replay (RFC 6347 §4.1.2.6, RFC 9147 §4.5.1): has a record with this
278
+ * sequence number been accepted in this epoch before? `win` is the epoch's
279
+ * receive state — its read-key object — which this keeps: maxReadSeq and a
280
+ * 64-record window below it (replayLo = bits 0..31, replayHi = 32..63, bit k
281
+ * = maxReadSeq - k; no BigInt per record). True, and recorded, for a new one;
282
+ * false for a replay or one older than the window.
283
+ *
284
+ * Asked only AFTER the record authenticated: a forged record number must not
285
+ * move the window. DTLS 1.3 and DTLS 1.2 alike — 1.2 had no window at all,
286
+ * so a record received twice (replayed, or duplicated on the way) counted as
287
+ * the peer retransmitting its flight, and ours went out again (BoGo
288
+ * DTLS-Replay-TLS12: the cookie ClientHello resent).
289
+ */
290
+ function acceptRecordSeq(win, seq) {
291
+ if (win.maxReadSeq === undefined || win.maxReadSeq < 0) {
292
+ // First accepted record in this epoch: seed the window with this seq as
293
+ // the high-water mark (bit 0 set), so an immediate replay of it is
294
+ // caught rather than sailing through an unset window.
295
+ win.maxReadSeq = seq;
296
+ win.replayLo = 1; win.replayHi = 0;
297
+ return true;
298
+ }
299
+ if (seq > win.maxReadSeq) {
300
+ // Advance: shift the window up by the gap, then set bit 0.
301
+ let shift = seq - win.maxReadSeq;
302
+ let lo = win.replayLo, hi = win.replayHi;
303
+ if (shift >= 64) { lo = 0; hi = 0; }
304
+ else if (shift >= 32) { hi = (lo << (shift - 32)) >>> 0; lo = 0; }
305
+ else { hi = ((hi << shift) | (lo >>> (32 - shift))) >>> 0; lo = (lo << shift) >>> 0; }
306
+ win.replayLo = (lo | 1) >>> 0; win.replayHi = hi;
307
+ win.maxReadSeq = seq;
308
+ return true;
309
+ }
310
+ let back = win.maxReadSeq - seq;
311
+ if (back >= 64) return false; // too old — outside window
312
+ if (back < 32) {
313
+ if ((win.replayLo >>> back) & 1) return false; // already seen — replay
314
+ win.replayLo = (win.replayLo | (1 << back)) >>> 0;
315
+ } else {
316
+ if ((win.replayHi >>> (back - 32)) & 1) return false;
317
+ win.replayHi = (win.replayHi | (1 << (back - 32))) >>> 0;
318
+ }
319
+ return true;
320
+ }
321
+
276
322
  /** Strip trailing zeros and extract content_type from TLSInnerPlaintext. */
277
323
  /**
278
324
  * Strip trailing zeros and extract content_type from TLSInnerPlaintext.
@@ -284,7 +330,9 @@ function decryptRecordWithAadView(aadView, ciphertext, key, nonce, algo) {
284
330
  function parseInnerPlaintext(data) {
285
331
  let j = data.length - 1;
286
332
  while (j >= 0 && data[j] === 0) j--;
287
- if (j < 0) throw new Error('Malformed TLSInnerPlaintext');
333
+ // RFC 8446 §5.4: no non-zero octet — unexpected_message. The record was
334
+ // authenticated (the tag verified), so this is the peer's doing.
335
+ if (j < 0) throw recordError('Malformed TLSInnerPlaintext: padding only', 10);
288
336
  return { type: data[j], content: data.subarray(0, j) };
289
337
  }
290
338
 
@@ -665,6 +713,21 @@ function readU48(buf, off) {
665
713
  }
666
714
 
667
715
  /** AES-ECB encrypt a single 16-byte block (for DTLS 1.3 record number encryption). */
716
+ // One AES-ECB cipher per sn_key, reused for every record's number mask
717
+ // (RFC 9147 §4.2.3) instead of creating one per record. ECB keeps no state
718
+ // between blocks, so update() on 16 bytes always returns exactly that block's
719
+ // encryption. Keyed by the key object, which lives as long as its epoch's keys.
720
+ var _snEcbCache = new WeakMap();
721
+ function _snKeyEcb(key) {
722
+ var c = _snEcbCache.get(key);
723
+ if (!c) {
724
+ c = crypto.createCipheriv(key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb', key, null);
725
+ c.setAutoPadding(false);
726
+ _snEcbCache.set(key, c);
727
+ }
728
+ return c;
729
+ }
730
+
668
731
  function aesEcbEncrypt(key, block) {
669
732
  let algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
670
733
  let cipher = crypto.createCipheriv(algo, key, null);
@@ -721,106 +784,136 @@ function parseDtlsPlaintext(data) {
721
784
 
722
785
 
723
786
  // ===================== DTLS 1.2 encrypted records =====================
724
-
725
- /** DTLS 1.2 AAD: epoch(2) + seq(6) + type(1) + version(2) + plaintext_length(2). */
726
- function buildDtlsAad12(epoch, seq, type, plaintextLen) {
727
- let aad = new Uint8Array(13);
728
- let off = 0;
787
+ //
788
+ // With a Connection ID (RFC 9146) the peer asked for, a protected record is
789
+ // a tls12_cid one: outer type 25, the CID after the sequence number, and the
790
+ // real type inside, after the content (DTLSInnerPlaintext; zeros may pad it
791
+ // — we send none). Its AAD is §5.3's: the 0xff placeholder, 25, the CID
792
+ // length, 25, version, epoch, sequence number, the CID, and the length of
793
+ // the inner plaintext. `cid` below: that CID — null or empty, no CID.
794
+
795
+ const TLS12_CID = 25;
796
+ // The longest a record's body may be on the wire: TLSCiphertext's
797
+ // 2^14 + 256 in (D)TLS 1.3 (RFC 8446 §5.2), 2^14 + 2048 in 1.2 (RFC 5246
798
+ // §6.2.3) — and 1.2's for an unprotected record too, the most anything
799
+ // before the version is known can be. A DTLS record past it is dropped
800
+ // before anything is done with it: it is not the peer's, or the peer is
801
+ // broken, and which cannot be told before it is authenticated — RFC 9147
802
+ // §4.5.2, RFC 6347 §4.1.2.7: invalid records are discarded. It was
803
+ // decrypted, and its plaintext handed on whatever its size (BoGo
804
+ // LargeCiphertext-DTLS).
805
+ const DTLS13_MAX_CIPHERTEXT = 16384 + 256;
806
+ const DTLS12_MAX_RECORD = 16384 + 2048;
807
+ const SEQ_NUM_PLACEHOLDER = new Uint8Array(8).fill(0xff);
808
+
809
+ function hasCid(cid) { return !!cid && cid.length > 0; }
810
+
811
+ /** DTLS 1.2 AAD: epoch(2) + seq(6) + type(1) + version(2) + plaintext_length(2);
812
+ * with a CID, RFC 9146 §5.3's (the type is then tls12_cid). */
813
+ function buildDtlsAad12(epoch, seq, type, plaintextLen, cid) {
814
+ if (!hasCid(cid)) {
815
+ let aad = new Uint8Array(13);
816
+ let off = 0;
817
+ off = w_u16(aad, off, epoch);
818
+ off = w_u48(aad, off, seq);
819
+ off = w_u8(aad, off, type);
820
+ off = w_u16(aad, off, 0xFEFD);
821
+ off = w_u16(aad, off, plaintextLen);
822
+ return aad;
823
+ }
824
+ let aad = new Uint8Array(23 + cid.length);
825
+ aad.set(SEQ_NUM_PLACEHOLDER, 0);
826
+ let off = 8;
827
+ off = w_u8(aad, off, TLS12_CID);
828
+ off = w_u8(aad, off, cid.length);
829
+ off = w_u8(aad, off, TLS12_CID);
830
+ off = w_u16(aad, off, 0xFEFD);
729
831
  off = w_u16(aad, off, epoch);
730
832
  off = w_u48(aad, off, seq);
731
- off = w_u8(aad, off, type);
732
- off = w_u16(aad, off, 0xFEFD);
733
- off = w_u16(aad, off, plaintextLen);
833
+ aad.set(cid, off); off += cid.length;
834
+ w_u16(aad, off, plaintextLen);
734
835
  return aad;
735
836
  }
736
837
 
737
- /** Encrypt DTLS 1.2 record payload. Returns: [explicit_nonce(8) ||] ciphertext || tag(16).
738
- *
739
- * AES-GCM: prepends an 8-byte explicit nonce (the record seq); nonce = salt(4) || seq(8).
740
- * ChaCha20-Poly1305 (RFC 7905): NO explicit nonce on the wire; nonce = iv(12) XOR
741
- * (epoch(16) || seq(48)). Detected by ivSalt length (12 = ChaCha, 4 = GCM).
742
- */
743
- function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
744
- let aad = buildDtlsAad12(epoch, seq, type, pt.length);
745
-
746
- // ChaCha20-Poly1305: full 12-byte IV, no explicit nonce (RFC 7905).
838
+ // The AEAD of a DTLS 1.2 record: the cipher (or decipher) and, for AES-GCM,
839
+ // the 8-byte explicit nonce (the record seq). ChaCha20-Poly1305 has none on
840
+ // the wire (RFC 7905): its nonce is iv(12) XOR (epoch(16) || seq(48)) —
841
+ // the suite told apart by the IV's length (12 for ChaCha, a 4-byte salt for
842
+ // GCM). One place for both directions.
843
+ function dtls12Aead(decrypt, key, ivSalt, epoch, seq, aad, ptLen, explicit) {
844
+ let make = decrypt ? crypto.createDecipheriv : crypto.createCipheriv;
845
+ let c;
747
846
  if (ivSalt.length === 12) {
748
- // DTLS folds the 16-bit epoch above the 48-bit seq to form the 64-bit
749
- // sequence input, matching the AAD's epoch||seq (RFC 6347 / RFC 9147).
750
- let seq64 = epoch * 0x1000000000000 + seq;
751
- let nonce = getChachaNonce12(ivSalt, seq64);
752
- let cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
753
- cipher.setAAD(aad, { plaintextLength: pt.length });
754
- let ct = cipher.update(pt);
755
- cipher.final();
756
- let tag = cipher.getAuthTag();
757
- // No explicit nonce prefix for ChaCha.
758
- let out = new Uint8Array(ct.length + tag.length);
759
- out.set(new Uint8Array(ct), 0);
760
- out.set(new Uint8Array(tag), ct.length);
761
- return out;
847
+ c = make('chacha20-poly1305', key, getChachaNonce12(ivSalt, epoch * 0x1000000000000 + seq), { authTagLength: 16 });
848
+ c.setAAD(aad, { plaintextLength: ptLen });
849
+ } else {
850
+ c = make(key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm', key, getNonce12(ivSalt, explicit));
851
+ c.setAAD(aad);
762
852
  }
853
+ return c;
854
+ }
763
855
 
764
- // AES-GCM: explicit nonce (= record seq) prepended.
765
- let explicit = seqToBytes(seq);
766
- let nonce = getNonce12(ivSalt, explicit);
767
- let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
768
- let cipher = crypto.createCipheriv(algo, key, nonce);
769
- cipher.setAAD(aad);
770
- let ct = cipher.update(pt);
771
- cipher.final();
772
- let tag = cipher.getAuthTag();
773
- let out = new Uint8Array(8 + ct.length + tag.length);
774
- out.set(explicit, 0);
775
- out.set(new Uint8Array(ct), 8);
776
- out.set(new Uint8Array(tag), 8 + ct.length);
856
+ /** Build a complete protected DTLS 1.2 record: the header, then
857
+ * [explicit_nonce(8) ||] ciphertext || tag(16) — with `cid`, a tls12_cid
858
+ * record (RFC 9146 §4). */
859
+ function buildEncryptedDtls12(type, epoch, seq, plaintext, keys, cid) {
860
+ let key = keys.key, ivSalt = keys.iv;
861
+ let withCid = hasCid(cid);
862
+ let explicitLen = ivSalt.length === 12 ? 0 : 8;
863
+ let innerLen = plaintext.length + (withCid ? 1 : 0);
864
+ let fragLen = explicitLen + innerLen + 16;
865
+ let hdrLen = 13 + (withCid ? cid.length : 0);
866
+ let out = new Uint8Array(hdrLen + fragLen);
867
+ let off = 0;
868
+ off = w_u8(out, off, withCid ? TLS12_CID : type);
869
+ off = w_u16(out, off, 0xFEFD);
870
+ off = w_u16(out, off, epoch);
871
+ off = w_u48(out, off, seq);
872
+ if (withCid) { out.set(cid, off); off += cid.length; }
873
+ off = w_u16(out, off, fragLen);
874
+ let explicit = null;
875
+ if (explicitLen) { explicit = seqToBytes(seq); out.set(explicit, off); off += 8; }
876
+ let cipher = dtls12Aead(false, key, ivSalt, epoch, seq, buildDtlsAad12(epoch, seq, type, innerLen, cid), innerLen, explicit);
877
+ let ct = cipher.update(plaintext);
878
+ out.set(ct, off); off += ct.length;
879
+ if (withCid) { let t = cipher.update(_contentTypeByte(type)); out.set(t, off); off += t.length; }
880
+ let fin = cipher.final();
881
+ if (fin.length) { out.set(fin, off); off += fin.length; }
882
+ out.set(cipher.getAuthTag(), off);
777
883
  return out;
778
884
  }
779
885
 
780
- /** Decrypt DTLS 1.2 record fragment.
781
- * AES-GCM input: explicit_nonce(8) || ciphertext || tag(16).
782
- * ChaCha20 input: ciphertext || tag(16) (no explicit nonce — RFC 7905).
783
- */
784
- function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
785
- // ChaCha20-Poly1305: no explicit nonce; nonce = iv(12) XOR (epoch||seq).
786
- if (ivSalt.length === 12) {
787
- if (fragment.length < 16) throw new Error('DTLS 1.2 ChaCha fragment too short');
788
- let ct = fragment.slice(0, fragment.length - 16);
789
- let tag = fragment.slice(fragment.length - 16);
790
- let seq64 = epoch * 0x1000000000000 + seq;
791
- let nonce = getChachaNonce12(ivSalt, seq64);
792
- let aad = buildDtlsAad12(epoch, seq, type, ct.length);
793
- let decipher = crypto.createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
794
- decipher.setAAD(aad, { plaintextLength: ct.length });
795
- decipher.setAuthTag(tag);
796
- let pt = decipher.update(ct);
797
- decipher.final();
798
- // A view, not a copy: pt is a fresh Buffer that owns its whole
799
- // ArrayBuffer and nothing else holds it. Same Uint8Array type as before.
800
- return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
801
- }
886
+ /** The protected part of a DTLS 1.2 record alone: [explicit_nonce(8) ||]
887
+ * ciphertext || tag(16). */
888
+ function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
889
+ return buildEncryptedDtls12(type, epoch, seq, pt, { key: key, iv: ivSalt }).subarray(13);
890
+ }
802
891
 
803
- // AES-GCM: strip the 8-byte explicit nonce.
804
- if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
805
- let explicit = fragment.slice(0, 8);
806
- let tag = fragment.slice(fragment.length - 16);
807
- let ct = fragment.slice(8, fragment.length - 16);
808
- let nonce = getNonce12(ivSalt, explicit);
809
- let aad = buildDtlsAad12(epoch, seq, type, ct.length);
810
- let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
811
- let decipher = crypto.createDecipheriv(algo, key, nonce);
812
- decipher.setAAD(aad);
813
- decipher.setAuthTag(tag);
892
+ /** Decrypt a DTLS 1.2 record fragment ([explicit_nonce(8) ||] ciphertext ||
893
+ * tag(16)). Throws when it does not open. `type` is the record's (outer)
894
+ * type; with `cid` the AAD is RFC 9146's, and what comes back is the
895
+ * DTLSInnerPlaintext still — openDtls12 takes it apart. */
896
+ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type, cid) {
897
+ let explicitLen = ivSalt.length === 12 ? 0 : 8;
898
+ if (fragment.length < explicitLen + 16) throw new Error('DTLS 1.2 fragment too short');
899
+ // Views: only read; the plaintext comes out of the decipher as a fresh
900
+ // Buffer (a Uint8Array), handed on as it is, up to 'data' — no copy.
901
+ let explicit = explicitLen ? fragment.subarray(0, 8) : null;
902
+ let ct = fragment.subarray(explicitLen, fragment.length - 16);
903
+ let decipher = dtls12Aead(true, key, ivSalt, epoch, seq, buildDtlsAad12(epoch, seq, type, ct.length, cid), ct.length, explicit);
904
+ decipher.setAuthTag(fragment.subarray(fragment.length - 16));
814
905
  let pt = decipher.update(ct);
815
906
  decipher.final();
816
- // A view, not a copy — see the ChaCha branch above.
817
- return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
907
+ return pt;
818
908
  }
819
909
 
820
- /** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
821
- function buildEncryptedDtls12(type, epoch, seq, plaintext, keys) {
822
- let encrypted = encryptDtls12(plaintext, keys.key, keys.iv, epoch, seq, type);
823
- return buildDtlsPlaintext(type, epoch, seq, encrypted);
910
+ /** Open a protected DTLS 1.2 record: { type, content }. A tls12_cid record
911
+ * — under the CID we asked for (`cid`) — gives its real type from inside
912
+ * (RFC 9146 §4: the last non-zero byte; none at all is unexpected_message,
913
+ * as in TLS 1.3). Throws when it does not open. */
914
+ function openDtls12(fragment, keys, epoch, seq, type, cid) {
915
+ if (type !== TLS12_CID) return { type: type, content: decryptDtls12(fragment, keys.key, keys.iv, epoch, seq, type) };
916
+ return parseInnerPlaintext(decryptDtls12(fragment, keys.key, keys.iv, epoch, seq, type, cid));
824
917
  }
825
918
 
826
919
 
@@ -864,6 +957,17 @@ function parseUnifiedHdr(b) {
864
957
  /** Check if a byte is a DTLS 1.3 unified header (0x20..0x3F). */
865
958
  function isUnifiedHdr(b) { return (b & 0xE0) === UNIFIED_HDR_FIXED; }
866
959
 
960
+ /** The Connection ID of a datagram's first record, `cidLen` bytes, or null
961
+ * when it carries none: a tls12_cid record's (after the 11-byte prefix),
962
+ * or a unified header's with the C bit (after its first byte). What a
963
+ * server finds a connection by, whatever address it comes from. */
964
+ function datagramCid(data, cidLen) {
965
+ if (!data || cidLen <= 0) return null;
966
+ if (data[0] === TLS12_CID) return data.length >= 13 + cidLen ? data.subarray(11, 11 + cidLen) : null;
967
+ if (isUnifiedHdr(data[0]) && parseUnifiedHdr(data[0]).hasCid) return data.length > cidLen ? data.subarray(1, 1 + cidLen) : null;
968
+ return null;
969
+ }
970
+
867
971
 
868
972
  // ===================== DTLS 1.3 record number encryption =====================
869
973
 
@@ -898,7 +1002,7 @@ function maskRecordNumber(snKey, rnBytes, ciphertext, algo) {
898
1002
  let c = crypto.createCipheriv('chacha20', snKey, iv);
899
1003
  mask = new Uint8Array(c.update(Buffer.alloc(16)));
900
1004
  } else {
901
- mask = aesEcbEncrypt(snKey, ciphertext.subarray(0, 16));
1005
+ mask = _snKeyEcb(snKey).update(ciphertext.subarray(0, 16));
902
1006
  }
903
1007
 
904
1008
  let out = new Uint8Array(rnBytes.length);
@@ -917,51 +1021,59 @@ function maskRecordNumber(snKey, rnBytes, ciphertext, algo) {
917
1021
  * seq: record sequence number
918
1022
  * epoch: low 2 bits (2=handshake, 3=application)
919
1023
  * keys: { key, iv, snKey, algo? }
1024
+ * cid: the peer's Connection ID (RFC 9147 §4: C bit, the CID after
1025
+ * the first byte; its length is the one negotiated, not on the
1026
+ * wire), or null / empty for none
920
1027
  */
921
- function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
1028
+ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys, cid) {
922
1029
  let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
923
1030
  let isChaCha = algo === 'chacha20-poly1305';
1031
+ let cidLen = cid ? cid.length : 0;
924
1032
 
925
- // Unified header: 2-byte seq, with length, no CID
926
- let info = buildUnifiedHdr(epoch, true, true, false);
1033
+ // Unified header: [CID], 2-byte seq, with length
1034
+ let info = buildUnifiedHdr(epoch, true, true, cidLen > 0);
1035
+ let rnOff = 1 + cidLen, hdrLen = rnOff + 4;
927
1036
 
928
1037
  // Plaintext record number
929
- let rn = new Uint8Array(2);
930
- rn[0] = (seq >>> 8) & 0xFF;
931
- rn[1] = seq & 0xFF;
932
-
933
- // Inner plaintext: content + content_type
934
- let inner = new Uint8Array(plaintext.length + 1);
935
- inner.set(plaintext, 0);
936
- inner[plaintext.length] = innerType;
1038
+ let rn0 = (seq >>> 8) & 0xFF, rn1 = seq & 0xFF;
937
1039
 
938
- let encLen = inner.length + 16;
1040
+ // Inner plaintext = content || content_type. Fed to the cipher in two
1041
+ // pieces rather than first copied into one buffer.
1042
+ let innerLen = plaintext.length + 1;
1043
+ let encLen = innerLen + 16;
939
1044
 
940
- // AAD = header with PLAINTEXT record number (before RN encryption)
941
- let aad = new Uint8Array(5);
1045
+ // AAD = the header with the PLAINTEXT record number (before RN
1046
+ // encryption) — the CID in it (§4.2.3: the whole header).
1047
+ let aad = new Uint8Array(hdrLen);
942
1048
  aad[0] = info;
943
- aad[1] = rn[0];
944
- aad[2] = rn[1];
945
- aad[3] = (encLen >>> 8) & 0xFF;
946
- aad[4] = encLen & 0xFF;
1049
+ if (cidLen) aad.set(cid, 1);
1050
+ aad[rnOff] = rn0;
1051
+ aad[rnOff + 1] = rn1;
1052
+ aad[rnOff + 2] = (encLen >>> 8) & 0xFF;
1053
+ aad[rnOff + 3] = encLen & 0xFF;
947
1054
 
948
1055
  // Nonce (reuse TLS 1.3 nonce construction)
949
1056
  let nonce = getNonce(keys.iv, seq);
950
1057
 
951
- // AEAD encrypt
1058
+ // AEAD encrypt, written straight into the record: info(1) + encrypted
1059
+ // rn(2) + length(2) + ciphertext. (It used to pass through three buffers:
1060
+ // inner plaintext, ciphertext+tag, then the record.)
952
1061
  let cipher = crypto.createCipheriv(algo, keys.key, nonce,
953
1062
  isChaCha ? { authTagLength: 16 } : undefined);
954
- cipher.setAAD(aad, isChaCha ? { plaintextLength: inner.length } : undefined);
955
- let ct = cipher.update(inner);
956
- cipher.final();
957
- let tag = cipher.getAuthTag();
958
-
959
- let ciphertext = new Uint8Array(ct.length + tag.length);
960
- ciphertext.set(new Uint8Array(ct), 0);
961
- ciphertext.set(new Uint8Array(tag), ct.length);
1063
+ cipher.setAAD(aad, isChaCha ? { plaintextLength: innerLen } : undefined);
1064
+ let record = new Uint8Array(hdrLen + encLen);
1065
+ let off = hdrLen;
1066
+ let ct1 = cipher.update(plaintext);
1067
+ record.set(ct1, off); off += ct1.length;
1068
+ let ct2 = cipher.update(_contentTypeByte(innerType));
1069
+ record.set(ct2, off); off += ct2.length;
1070
+ let ct3 = cipher.final();
1071
+ if (ct3.length) { record.set(ct3, off); off += ct3.length; }
1072
+ record.set(cipher.getAuthTag(), off);
1073
+ let ciphertext = record.subarray(hdrLen);
962
1074
 
963
1075
  // Encrypt record number
964
- let encRn = maskRecordNumber(keys.snKey, rn, ciphertext, algo);
1076
+ let encRn = maskRecordNumber(keys.snKey, [rn0, rn1], ciphertext, algo);
965
1077
 
966
1078
  // ── DIAGNOSTIC (LEMON_DEBUG=1 / WEBRTC_DEBUG=1) ──────────────────────
967
1079
  // Everything a peer needs to PARSE this record, printed as we build it.
@@ -971,34 +1083,37 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
971
1083
  // present), the sequence number before and after masking, and the
972
1084
  // declared length against the actual ciphertext size. If two records
973
1085
  // are accepted and the third is not, the difference between them shows
974
- // up here.
975
- try {
976
- if (typeof process !== 'undefined' && process.env &&
977
- (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1')) {
1086
+ // up here. (Flag read once at load: process.env is slow to read per record.)
1087
+ if (_REC_DEBUG) {
1088
+ try {
978
1089
  var _h = function (b) { return ('0' + (b & 0xFF).toString(16)).slice(-2); };
979
1090
  console.log('[lemon-rec] TX info=0x' + _h(info) +
980
1091
  ' [C=' + ((info >> 4) & 1) + ' S=' + ((info >> 3) & 1) +
981
1092
  ' L=' + ((info >> 2) & 1) + ' epoch=' + (info & 0x03) + ']' +
982
- ' seqPlain=' + _h(rn[0]) + _h(rn[1]) +
1093
+ ' seqPlain=' + _h(rn0) + _h(rn1) +
983
1094
  ' seqMasked=' + _h(encRn[0]) + _h(encRn[1]) +
984
1095
  ' declaredLen=' + ciphertext.length +
985
- ' aadLen=' + (aad ? aad.length : 'null') +
1096
+ ' aadLen=' + aad.length +
986
1097
  ' snKey=' + (keys.snKey ? 'set' : 'MISSING'));
987
- }
988
- } catch (_e) {}
1098
+ } catch (_e) {}
1099
+ }
989
1100
  // ─────────────────────────────────────────────────────────────────────
990
1101
 
991
- // Assemble: info(1) + encrypted_rn(2) + length(2) + ciphertext
992
- let record = new Uint8Array(5 + ciphertext.length);
993
- record[0] = info;
994
- record[1] = encRn[0];
995
- record[2] = encRn[1];
996
- record[3] = (ciphertext.length >>> 8) & 0xFF;
997
- record[4] = ciphertext.length & 0xFF;
998
- record.set(ciphertext, 5);
1102
+ record.set(aad, 0);
1103
+ record[rnOff] = encRn[0];
1104
+ record[rnOff + 1] = encRn[1];
999
1105
  return record;
1000
1106
  }
1001
1107
 
1108
+ var _REC_DEBUG = typeof process !== 'undefined' && process.env &&
1109
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1');
1110
+
1111
+ // The one-byte content type appended to a DTLS 1.3 inner plaintext.
1112
+ var _CT_BYTES = [];
1113
+ function _contentTypeByte(t) {
1114
+ return _CT_BYTES[t] || (_CT_BYTES[t] = new Uint8Array([t]));
1115
+ }
1116
+
1002
1117
  /**
1003
1118
  * Reconstruct a full DTLS 1.3 record sequence number from its truncated wire
1004
1119
  * encoding (RFC 9147 §4.2.2).
@@ -1068,15 +1183,27 @@ function resolveReadEpoch(epochBits, keysByEpoch) {
1068
1183
  * Decrypt a DTLS 1.3 encrypted record.
1069
1184
  * data: full record bytes (starting with unified header byte)
1070
1185
  * keys: { key, iv, snKey, algo?, maxReadSeq? }
1186
+ * cid: the Connection ID we asked the peer for, or null / empty. A record
1187
+ * with a CID when there is none, or another, does not open;
1188
+ * cidRequired: the peer agreed to it — then nor does one without it (§4:
1189
+ * "it MUST be contained in all datagrams"), but in epoch 1, early data
1190
+ * sent before the CID was known.
1071
1191
  * Returns { epoch, seq, type, content } or null on failure.
1072
1192
  */
1073
- function decryptEncryptedDtls13(data, keys, fullEpoch) {
1193
+ function decryptEncryptedDtls13(data, keys, fullEpoch, cid, cidRequired) {
1074
1194
  if (data.length < 1) return null;
1075
1195
 
1076
1196
  let hdr = parseUnifiedHdr(data[0]);
1077
1197
  let off = 1;
1078
-
1079
- if (hdr.hasCid) return null; // CID not supported yet
1198
+ let cidLen = cid ? cid.length : 0;
1199
+
1200
+ if (hdr.hasCid) {
1201
+ if (cidLen === 0 || data.length < 1 + cidLen) return null;
1202
+ for (let i = 0; i < cidLen; i++) if (data[1 + i] !== cid[i]) return null;
1203
+ off += cidLen;
1204
+ } else if (cidRequired && fullEpoch !== 1) {
1205
+ return null;
1206
+ }
1080
1207
 
1081
1208
  let rnLen = hdr.seqLen2 ? 2 : 1;
1082
1209
  if (off + rnLen > data.length) return null;
@@ -1093,7 +1220,10 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1093
1220
  }
1094
1221
 
1095
1222
  if (off + ctLen > data.length) return null;
1096
- let ciphertext = data.slice(off, off + ctLen);
1223
+ if (ctLen > DTLS13_MAX_CIPHERTEXT) return null;
1224
+ // A view: only read here (record-number mask, AEAD input); the plaintext
1225
+ // comes out of the decipher as a fresh buffer.
1226
+ let ciphertext = data.subarray(off, off + ctLen);
1097
1227
 
1098
1228
  // Resolve the AEAD before unmasking: the record-number mask is built with
1099
1229
  // the same primitive as the AEAD (AES-ECB vs ChaCha20), so `algo` is needed
@@ -1122,13 +1252,13 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1122
1252
  // Rebuild AAD with plaintext RN. AAD depends only on the wire bytes (the
1123
1253
  // decrypted record number + length), NOT on the reconstructed full seq —
1124
1254
  // so it's computed once and reused across seq candidates below.
1125
- let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
1126
- let aad = new Uint8Array(hdrLen);
1127
- aad[0] = data[0];
1128
- for (let i = 0; i < rnLen; i++) aad[1 + i] = rn[i];
1255
+ let rnOff = off - rnLen - (hdr.hasLength ? 2 : 0);
1256
+ let aad = new Uint8Array(off);
1257
+ aad.set(data.subarray(0, rnOff), 0); // the first byte, and the CID
1258
+ for (let i = 0; i < rnLen; i++) aad[rnOff + i] = rn[i];
1129
1259
  if (hdr.hasLength) {
1130
- aad[1 + rnLen] = (ctLen >>> 8) & 0xFF;
1131
- aad[1 + rnLen + 1] = ctLen & 0xFF;
1260
+ aad[rnOff + rnLen] = (ctLen >>> 8) & 0xFF;
1261
+ aad[rnOff + rnLen + 1] = ctLen & 0xFF;
1132
1262
  }
1133
1263
 
1134
1264
  let ct = ciphertext.subarray(0, ciphertext.length - 16);
@@ -1168,34 +1298,13 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1168
1298
  seq = acceptedSeq;
1169
1299
 
1170
1300
  {
1171
- // A view, not a copy: pt is the decipher's fresh Buffer, owned by no one else.
1172
- let inner = parseInnerPlaintext(new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength));
1173
-
1174
- // RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
1175
- // and update the sliding window. Checking before deprotection would let a
1176
- // forged record number poison the window; checking after means only
1177
- // genuine records move it. The window lives on the per-epoch key object,
1178
- // next to maxReadSeq, because both are per-epoch receive state.
1179
- // replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
1180
- // been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
1181
- if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
1182
- // First accepted record in this epoch: seed the window with this seq as
1183
- // the high-water mark (bit 0 set), so an immediate replay of it is
1184
- // caught below rather than sailing through an unset window.
1185
- keys.maxReadSeq = seq;
1186
- keys.replayWindow = 1n;
1187
- } else if (seq > keys.maxReadSeq) {
1188
- // Advance: shift the window up by the gap, then set bit 0.
1189
- let shift = BigInt(seq - keys.maxReadSeq);
1190
- keys.replayWindow = shift >= 64n ? 1n : ((keys.replayWindow << shift) | 1n) & ((1n << 64n) - 1n);
1191
- keys.maxReadSeq = seq;
1192
- } else {
1193
- let back = BigInt(keys.maxReadSeq - seq);
1194
- if (back >= 64n) return null; // too old — outside window
1195
- let bit = 1n << back;
1196
- if (keys.replayWindow & bit) return null; // already seen — replay
1197
- keys.replayWindow |= bit;
1198
- }
1301
+ // pt is the decipher's fresh Buffer, owned by no one else: the content is
1302
+ // a subarray of it — a Buffer, all the way up to 'data', with no copy.
1303
+ let inner = parseInnerPlaintext(pt);
1304
+
1305
+ // RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies (see
1306
+ // acceptRecordSeq).
1307
+ if (!acceptRecordSeq(keys, seq)) return null;
1199
1308
 
1200
1309
  return {
1201
1310
  // The caller resolves the full epoch (resolveReadEpoch); hdr.epoch is
@@ -1217,17 +1326,26 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1217
1326
  * Dispatches between plaintext (classic header) and encrypted (unified header).
1218
1327
  *
1219
1328
  * keysByEpoch: { [epoch]: { key, iv, snKey, algo } } — null for plaintext only.
1329
+ * cid: the Connection ID we asked the peer for (RFC 9146, RFC 9147
1330
+ * §9), or null / empty — from the moment it is asked for: its
1331
+ * length is what finds the boundaries of a record carrying it,
1332
+ * which may come in the datagram that says it was agreed to. A
1333
+ * datagram of CID records when we asked for none is not one of
1334
+ * ours, and ends here.
1335
+ * cidRequired: the peer agreed to it: every protected record carries it.
1220
1336
  * Returns array of { type, epoch, seq, content, encrypted }.
1221
1337
  */
1222
- function parseDtlsDatagram(data, keysByEpoch) {
1338
+ function parseDtlsDatagram(data, keysByEpoch, cid, cidRequired) {
1223
1339
  let records = [];
1224
1340
  let off = 0;
1341
+ let cidLen = cid ? cid.length : 0;
1225
1342
 
1226
1343
  while (off < data.length) {
1227
1344
  let first = data[off];
1228
1345
 
1229
1346
  if (isUnifiedHdr(first)) {
1230
1347
  let hdr = parseUnifiedHdr(first);
1348
+ if (hdr.hasCid && cidLen === 0) break;
1231
1349
  // Header carries 2 epoch bits; map them back to a real epoch first.
1232
1350
  let fullEpoch = resolveReadEpoch(hdr.epoch, keysByEpoch);
1233
1351
  let keys = (fullEpoch === null) ? null : keysByEpoch[fullEpoch];
@@ -1243,8 +1361,7 @@ function parseDtlsDatagram(data, keysByEpoch) {
1243
1361
  // Keep the raw bytes and mark the record deferred; the caller retries
1244
1362
  // it after processing each earlier record, once the keys exist. This
1245
1363
  // generalises the CCS-only re-decrypt that DTLS 1.2 already had.
1246
- let rnLen = hdr.seqLen2 ? 2 : 1;
1247
- let skip = 1 + rnLen;
1364
+ let skip = 1 + (hdr.hasCid ? cidLen : 0) + (hdr.seqLen2 ? 2 : 1);
1248
1365
  let total;
1249
1366
  if (hdr.hasLength && off + skip + 2 <= data.length) {
1250
1367
  total = skip + 2 + readU16(data, off + skip);
@@ -1269,7 +1386,7 @@ function parseDtlsDatagram(data, keysByEpoch) {
1269
1386
  continue;
1270
1387
  }
1271
1388
 
1272
- let result = decryptEncryptedDtls13(data.subarray(off), keys, fullEpoch);
1389
+ let result = decryptEncryptedDtls13(data.subarray(off), keys, fullEpoch, cid, cidRequired);
1273
1390
  if (result) {
1274
1391
  records.push({
1275
1392
  type: result.type,
@@ -1284,27 +1401,56 @@ function parseDtlsDatagram(data, keysByEpoch) {
1284
1401
  }
1285
1402
 
1286
1403
  } else if (first <= 63) {
1287
- // Classic DTLS record
1288
- if (off + 13 > data.length) break;
1404
+ // Classic DTLS record — or a tls12_cid one (RFC 9146 §4), the CID
1405
+ // between the sequence number and the length.
1406
+ let withCid = first === TLS12_CID;
1407
+ if (withCid && cidLen === 0) break;
1408
+ let hdrLen = 13 + (withCid ? cidLen : 0);
1409
+ if (off + hdrLen > data.length) break;
1289
1410
  let type = data[off];
1290
1411
  let epoch = readU16(data, off + 3);
1291
1412
  let seq = readU48(data, off + 5);
1292
- let length = readU16(data, off + 11);
1293
- if (off + 13 + length > data.length) break;
1413
+ let length = readU16(data, off + hdrLen - 2);
1414
+ if (off + hdrLen + length > data.length) break;
1415
+ let fragment = data.subarray(off + hdrLen, off + hdrLen + length);
1416
+ let at = off;
1417
+ off += hdrLen + length;
1418
+ if (length > DTLS12_MAX_RECORD) continue;
1419
+
1420
+ // Ours, by its CID — or not. And with a CID asked for, every protected
1421
+ // record carries it (§3: "MUST send with the record format"); one
1422
+ // without is not this connection's.
1423
+ if (withCid) {
1424
+ let mine = true;
1425
+ for (let i = 0; i < cidLen; i++) if (data[at + 11 + i] !== cid[i]) { mine = false; break; }
1426
+ if (!mine) continue;
1427
+ } else if (cidRequired && epoch > 0) {
1428
+ continue;
1429
+ }
1294
1430
 
1295
- let payload = data.slice(off + 13, off + 13 + length);
1431
+ let payload = null;
1296
1432
  let encrypted = false;
1297
1433
 
1298
- // DTLS 1.2: decrypt if epoch > 0 and keys available
1434
+ // DTLS 1.2: decrypt if epoch > 0 and keys available. The ciphertext is
1435
+ // only read (the plaintext comes out of the decipher as a fresh
1436
+ // buffer), so it is passed as a view; a record kept as it is — not
1437
+ // decrypted — gets its own copy, as before.
1299
1438
  if (epoch > 0 && keysByEpoch && keysByEpoch[epoch]) {
1300
1439
  let keys = keysByEpoch[epoch];
1301
- try {
1302
- payload = decryptDtls12(payload, keys.key, keys.iv, epoch, seq, type);
1303
- encrypted = true;
1304
- } catch(e) {
1305
- // Decryption failed — return raw
1440
+ let opened = null;
1441
+ try { opened = openDtls12(fragment, keys, epoch, seq, type, cid); }
1442
+ catch (e) {
1443
+ // Opened, and malformed inside: the peer's (alertDesc). Otherwise
1444
+ // it did not open — kept raw.
1445
+ if (e && e.alertDesc) throw e;
1446
+ }
1447
+ if (opened) {
1448
+ // Authenticated, and seen before: a replay — dropped, as in 1.3.
1449
+ if (!acceptRecordSeq(keys, seq)) continue;
1450
+ type = opened.type; payload = opened.content; encrypted = true;
1306
1451
  }
1307
1452
  }
1453
+ if (payload === null) payload = fragment.slice();
1308
1454
 
1309
1455
  records.push({
1310
1456
  type: type,
@@ -1313,7 +1459,6 @@ function parseDtlsDatagram(data, keysByEpoch) {
1313
1459
  content: payload,
1314
1460
  encrypted: encrypted,
1315
1461
  });
1316
- off += 13 + length;
1317
1462
  } else {
1318
1463
  break;
1319
1464
  }
@@ -1358,16 +1503,31 @@ function buildDtlsAck(acks) {
1358
1503
  }
1359
1504
 
1360
1505
  /** Parse ACK payload. Returns [{ epoch, seq }, ...]. */
1506
+ // A malformed record body from an authenticated record: decode_error (or
1507
+ // the given alert), carried as .alertDesc like wire.js's parse errors.
1508
+ function recordError(msg, alertDesc) {
1509
+ let e = new Error(msg);
1510
+ e.alertDesc = alertDesc === undefined ? 50 : alertDesc;
1511
+ return e;
1512
+ }
1513
+
1514
+ // Strict (RFC 9147 §7): the vector fills the record exactly, in 16-byte
1515
+ // RecordNumbers, each within what a DTLS record number can be (an epoch
1516
+ // carried as a u16 on the wire of every record, a 48-bit sequence number).
1517
+ // A short body, a length that does not match, a partial entry or a number
1518
+ // beyond those was read as far as it went, or past it.
1361
1519
  function parseDtlsAck(data) {
1520
+ if (!data || data.length < 2) throw recordError('ACK: truncated');
1362
1521
  let bodyLen = readU16(data, 0);
1363
- let off = 2;
1364
- let end = off + bodyLen;
1522
+ if (2 + bodyLen !== data.length) throw recordError('ACK: record_numbers of ' + bodyLen + ' bytes in a body of ' + (data.length - 2));
1523
+ if (bodyLen % 16 !== 0) throw recordError('ACK: record_numbers not a whole number of entries');
1365
1524
  let acks = [];
1366
- // 16 bytes per RecordNumber — see buildDtlsAck.
1367
- while (off + 16 <= end && off + 16 <= data.length) {
1368
- let epoch = readU64(data, off); off += 8;
1369
- let seq = readU64(data, off); off += 8;
1370
- acks.push({ epoch, seq });
1525
+ for (let off = 2; off < data.length; off += 16) {
1526
+ if (data[off] | data[off + 1] | data[off + 2] | data[off + 3] | data[off + 4] | data[off + 5] ||
1527
+ data[off + 8] | data[off + 9]) {
1528
+ throw recordError('ACK: a record number out of range');
1529
+ }
1530
+ acks.push({ epoch: readU64(data, off), seq: readU48(data, off + 10) });
1371
1531
  }
1372
1532
  return acks;
1373
1533
  }
@@ -1409,6 +1569,7 @@ export {
1409
1569
  // DTLS helpers
1410
1570
  aesEcbEncrypt,
1411
1571
  isUnifiedHdr,
1572
+ datagramCid,
1412
1573
 
1413
1574
  // DTLS plaintext records
1414
1575
  buildDtlsPlaintext,
@@ -1416,9 +1577,12 @@ export {
1416
1577
 
1417
1578
  // DTLS 1.2 encrypted records
1418
1579
  buildDtlsAad12,
1580
+ acceptRecordSeq,
1419
1581
  encryptDtls12,
1420
1582
  decryptDtls12,
1583
+ openDtls12,
1421
1584
  buildEncryptedDtls12,
1585
+ TLS12_CID,
1422
1586
 
1423
1587
  // DTLS 1.3 unified header
1424
1588
  buildUnifiedHdr,