lemon-tls 0.10.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/README.md +164 -42
- package/index.d.ts +399 -15
- package/package.json +7 -2
- package/src/ca_store.js +31 -0
- package/src/compat.js +64 -112
- package/src/crypto.js +21 -114
- package/src/dtls_session.js +1238 -412
- package/src/dtls_socket.js +318 -96
- package/src/node_options.js +154 -17
- package/src/record.js +293 -197
- package/src/secure_context.js +86 -42
- package/src/session/ecdh.js +140 -122
- package/src/session/ech.js +92 -32
- package/src/session/hpke.js +12 -44
- package/src/session/message.js +29 -9
- package/src/session/signing.js +80 -22
- package/src/session/ticket.js +5 -3
- package/src/session/x509.js +385 -0
- package/src/tls_session.js +2214 -895
- package/src/tls_socket.js +451 -452
- package/src/wire.js +271 -158
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
|
-
|
|
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
|
|
|
@@ -736,138 +784,138 @@ function parseDtlsPlaintext(data) {
|
|
|
736
784
|
|
|
737
785
|
|
|
738
786
|
// ===================== DTLS 1.2 encrypted records =====================
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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);
|
|
744
831
|
off = w_u16(aad, off, epoch);
|
|
745
832
|
off = w_u48(aad, off, seq);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
off = w_u16(aad, off, plaintextLen);
|
|
833
|
+
aad.set(cid, off); off += cid.length;
|
|
834
|
+
w_u16(aad, off, plaintextLen);
|
|
749
835
|
return aad;
|
|
750
836
|
}
|
|
751
837
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
let
|
|
760
|
-
|
|
761
|
-
// ChaCha20-Poly1305: full 12-byte IV, no explicit nonce (RFC 7905).
|
|
762
|
-
if (ivSalt.length === 12) {
|
|
763
|
-
// DTLS folds the 16-bit epoch above the 48-bit seq to form the 64-bit
|
|
764
|
-
// sequence input, matching the AAD's epoch||seq (RFC 6347 / RFC 9147).
|
|
765
|
-
let seq64 = epoch * 0x1000000000000 + seq;
|
|
766
|
-
let nonce = getChachaNonce12(ivSalt, seq64);
|
|
767
|
-
let cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
768
|
-
cipher.setAAD(aad, { plaintextLength: pt.length });
|
|
769
|
-
let ct = cipher.update(pt);
|
|
770
|
-
cipher.final();
|
|
771
|
-
let tag = cipher.getAuthTag();
|
|
772
|
-
// No explicit nonce prefix for ChaCha.
|
|
773
|
-
let out = new Uint8Array(ct.length + tag.length);
|
|
774
|
-
out.set(new Uint8Array(ct), 0);
|
|
775
|
-
out.set(new Uint8Array(tag), ct.length);
|
|
776
|
-
return out;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// AES-GCM: explicit nonce (= record seq) prepended.
|
|
780
|
-
let explicit = seqToBytes(seq);
|
|
781
|
-
let nonce = getNonce12(ivSalt, explicit);
|
|
782
|
-
let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
783
|
-
let cipher = crypto.createCipheriv(algo, key, nonce);
|
|
784
|
-
cipher.setAAD(aad);
|
|
785
|
-
let ct = cipher.update(pt);
|
|
786
|
-
cipher.final();
|
|
787
|
-
let tag = cipher.getAuthTag();
|
|
788
|
-
let out = new Uint8Array(8 + ct.length + tag.length);
|
|
789
|
-
out.set(explicit, 0);
|
|
790
|
-
out.set(new Uint8Array(ct), 8);
|
|
791
|
-
out.set(new Uint8Array(tag), 8 + ct.length);
|
|
792
|
-
return out;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** Decrypt DTLS 1.2 record fragment.
|
|
796
|
-
* AES-GCM input: explicit_nonce(8) || ciphertext || tag(16).
|
|
797
|
-
* ChaCha20 input: ciphertext || tag(16) (no explicit nonce — RFC 7905).
|
|
798
|
-
*/
|
|
799
|
-
function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
|
|
800
|
-
// ChaCha20-Poly1305: no explicit nonce; nonce = iv(12) XOR (epoch||seq).
|
|
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;
|
|
801
846
|
if (ivSalt.length === 12) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
let aad = buildDtlsAad12(epoch, seq, type, ct.length);
|
|
808
|
-
let decipher = crypto.createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
809
|
-
decipher.setAAD(aad, { plaintextLength: ct.length });
|
|
810
|
-
decipher.setAuthTag(tag);
|
|
811
|
-
let pt = decipher.update(ct);
|
|
812
|
-
decipher.final();
|
|
813
|
-
// A view, not a copy: pt is a fresh Buffer that owns its whole
|
|
814
|
-
// ArrayBuffer and nothing else holds it. Same Uint8Array type as before.
|
|
815
|
-
return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
|
|
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);
|
|
816
852
|
}
|
|
817
|
-
|
|
818
|
-
// AES-GCM: strip the 8-byte explicit nonce.
|
|
819
|
-
if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
|
|
820
|
-
let explicit = fragment.subarray(0, 8); // views: only read
|
|
821
|
-
let tag = fragment.subarray(fragment.length - 16);
|
|
822
|
-
let ct = fragment.subarray(8, fragment.length - 16);
|
|
823
|
-
let nonce = getNonce12(ivSalt, explicit);
|
|
824
|
-
let aad = buildDtlsAad12(epoch, seq, type, ct.length);
|
|
825
|
-
let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
826
|
-
let decipher = crypto.createDecipheriv(algo, key, nonce);
|
|
827
|
-
decipher.setAAD(aad);
|
|
828
|
-
decipher.setAuthTag(tag);
|
|
829
|
-
let pt = decipher.update(ct);
|
|
830
|
-
decipher.final();
|
|
831
|
-
// A view, not a copy — see the ChaCha branch above.
|
|
832
|
-
return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
|
|
853
|
+
return c;
|
|
833
854
|
}
|
|
834
855
|
|
|
835
|
-
/** Build complete
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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) {
|
|
839
860
|
let key = keys.key, ivSalt = keys.iv;
|
|
840
|
-
let
|
|
841
|
-
let explicitLen =
|
|
842
|
-
let
|
|
843
|
-
let
|
|
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);
|
|
844
867
|
let off = 0;
|
|
845
|
-
off = w_u8(out, off, type);
|
|
868
|
+
off = w_u8(out, off, withCid ? TLS12_CID : type);
|
|
846
869
|
off = w_u16(out, off, 0xFEFD);
|
|
847
870
|
off = w_u16(out, off, epoch);
|
|
848
871
|
off = w_u48(out, off, seq);
|
|
872
|
+
if (withCid) { out.set(cid, off); off += cid.length; }
|
|
849
873
|
off = w_u16(out, off, fragLen);
|
|
850
|
-
let
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
let nonce = getChachaNonce12(ivSalt, epoch * 0x1000000000000 + seq);
|
|
854
|
-
cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
855
|
-
cipher.setAAD(aad, { plaintextLength: plaintext.length });
|
|
856
|
-
} else {
|
|
857
|
-
// AES-GCM: explicit nonce (= record seq) prepended.
|
|
858
|
-
let explicit = seqToBytes(seq);
|
|
859
|
-
out.set(explicit, off); off += 8;
|
|
860
|
-
cipher = crypto.createCipheriv(key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm', key, getNonce12(ivSalt, explicit));
|
|
861
|
-
cipher.setAAD(aad);
|
|
862
|
-
}
|
|
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);
|
|
863
877
|
let ct = cipher.update(plaintext);
|
|
864
878
|
out.set(ct, off); off += ct.length;
|
|
879
|
+
if (withCid) { let t = cipher.update(_contentTypeByte(type)); out.set(t, off); off += t.length; }
|
|
865
880
|
let fin = cipher.final();
|
|
866
881
|
if (fin.length) { out.set(fin, off); off += fin.length; }
|
|
867
882
|
out.set(cipher.getAuthTag(), off);
|
|
868
883
|
return out;
|
|
869
884
|
}
|
|
870
885
|
|
|
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
|
+
}
|
|
891
|
+
|
|
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));
|
|
905
|
+
let pt = decipher.update(ct);
|
|
906
|
+
decipher.final();
|
|
907
|
+
return pt;
|
|
908
|
+
}
|
|
909
|
+
|
|
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));
|
|
917
|
+
}
|
|
918
|
+
|
|
871
919
|
|
|
872
920
|
// ===================== DTLS 1.3 unified header =====================
|
|
873
921
|
//
|
|
@@ -909,6 +957,17 @@ function parseUnifiedHdr(b) {
|
|
|
909
957
|
/** Check if a byte is a DTLS 1.3 unified header (0x20..0x3F). */
|
|
910
958
|
function isUnifiedHdr(b) { return (b & 0xE0) === UNIFIED_HDR_FIXED; }
|
|
911
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
|
+
|
|
912
971
|
|
|
913
972
|
// ===================== DTLS 1.3 record number encryption =====================
|
|
914
973
|
|
|
@@ -962,13 +1021,18 @@ function maskRecordNumber(snKey, rnBytes, ciphertext, algo) {
|
|
|
962
1021
|
* seq: record sequence number
|
|
963
1022
|
* epoch: low 2 bits (2=handshake, 3=application)
|
|
964
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
|
|
965
1027
|
*/
|
|
966
|
-
function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
1028
|
+
function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys, cid) {
|
|
967
1029
|
let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
|
|
968
1030
|
let isChaCha = algo === 'chacha20-poly1305';
|
|
1031
|
+
let cidLen = cid ? cid.length : 0;
|
|
969
1032
|
|
|
970
|
-
// Unified header: 2-byte seq, with length
|
|
971
|
-
let info = buildUnifiedHdr(epoch, true, true,
|
|
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;
|
|
972
1036
|
|
|
973
1037
|
// Plaintext record number
|
|
974
1038
|
let rn0 = (seq >>> 8) & 0xFF, rn1 = seq & 0xFF;
|
|
@@ -978,13 +1042,15 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
|
978
1042
|
let innerLen = plaintext.length + 1;
|
|
979
1043
|
let encLen = innerLen + 16;
|
|
980
1044
|
|
|
981
|
-
// AAD = header with PLAINTEXT record number (before RN
|
|
982
|
-
|
|
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);
|
|
983
1048
|
aad[0] = info;
|
|
984
|
-
aad
|
|
985
|
-
aad[
|
|
986
|
-
aad[
|
|
987
|
-
aad[
|
|
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;
|
|
988
1054
|
|
|
989
1055
|
// Nonce (reuse TLS 1.3 nonce construction)
|
|
990
1056
|
let nonce = getNonce(keys.iv, seq);
|
|
@@ -995,8 +1061,8 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
|
995
1061
|
let cipher = crypto.createCipheriv(algo, keys.key, nonce,
|
|
996
1062
|
isChaCha ? { authTagLength: 16 } : undefined);
|
|
997
1063
|
cipher.setAAD(aad, isChaCha ? { plaintextLength: innerLen } : undefined);
|
|
998
|
-
let record = new Uint8Array(
|
|
999
|
-
let off =
|
|
1064
|
+
let record = new Uint8Array(hdrLen + encLen);
|
|
1065
|
+
let off = hdrLen;
|
|
1000
1066
|
let ct1 = cipher.update(plaintext);
|
|
1001
1067
|
record.set(ct1, off); off += ct1.length;
|
|
1002
1068
|
let ct2 = cipher.update(_contentTypeByte(innerType));
|
|
@@ -1004,7 +1070,7 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
|
1004
1070
|
let ct3 = cipher.final();
|
|
1005
1071
|
if (ct3.length) { record.set(ct3, off); off += ct3.length; }
|
|
1006
1072
|
record.set(cipher.getAuthTag(), off);
|
|
1007
|
-
let ciphertext = record.subarray(
|
|
1073
|
+
let ciphertext = record.subarray(hdrLen);
|
|
1008
1074
|
|
|
1009
1075
|
// Encrypt record number
|
|
1010
1076
|
let encRn = maskRecordNumber(keys.snKey, [rn0, rn1], ciphertext, algo);
|
|
@@ -1033,11 +1099,9 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
|
1033
1099
|
}
|
|
1034
1100
|
// ─────────────────────────────────────────────────────────────────────
|
|
1035
1101
|
|
|
1036
|
-
record
|
|
1037
|
-
record[
|
|
1038
|
-
record[
|
|
1039
|
-
record[3] = (ciphertext.length >>> 8) & 0xFF;
|
|
1040
|
-
record[4] = ciphertext.length & 0xFF;
|
|
1102
|
+
record.set(aad, 0);
|
|
1103
|
+
record[rnOff] = encRn[0];
|
|
1104
|
+
record[rnOff + 1] = encRn[1];
|
|
1041
1105
|
return record;
|
|
1042
1106
|
}
|
|
1043
1107
|
|
|
@@ -1119,15 +1183,27 @@ function resolveReadEpoch(epochBits, keysByEpoch) {
|
|
|
1119
1183
|
* Decrypt a DTLS 1.3 encrypted record.
|
|
1120
1184
|
* data: full record bytes (starting with unified header byte)
|
|
1121
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.
|
|
1122
1191
|
* Returns { epoch, seq, type, content } or null on failure.
|
|
1123
1192
|
*/
|
|
1124
|
-
function decryptEncryptedDtls13(data, keys, fullEpoch) {
|
|
1193
|
+
function decryptEncryptedDtls13(data, keys, fullEpoch, cid, cidRequired) {
|
|
1125
1194
|
if (data.length < 1) return null;
|
|
1126
1195
|
|
|
1127
1196
|
let hdr = parseUnifiedHdr(data[0]);
|
|
1128
1197
|
let off = 1;
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
+
}
|
|
1131
1207
|
|
|
1132
1208
|
let rnLen = hdr.seqLen2 ? 2 : 1;
|
|
1133
1209
|
if (off + rnLen > data.length) return null;
|
|
@@ -1144,6 +1220,7 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
|
|
|
1144
1220
|
}
|
|
1145
1221
|
|
|
1146
1222
|
if (off + ctLen > data.length) return null;
|
|
1223
|
+
if (ctLen > DTLS13_MAX_CIPHERTEXT) return null;
|
|
1147
1224
|
// A view: only read here (record-number mask, AEAD input); the plaintext
|
|
1148
1225
|
// comes out of the decipher as a fresh buffer.
|
|
1149
1226
|
let ciphertext = data.subarray(off, off + ctLen);
|
|
@@ -1175,13 +1252,13 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
|
|
|
1175
1252
|
// Rebuild AAD with plaintext RN. AAD depends only on the wire bytes (the
|
|
1176
1253
|
// decrypted record number + length), NOT on the reconstructed full seq —
|
|
1177
1254
|
// so it's computed once and reused across seq candidates below.
|
|
1178
|
-
let
|
|
1179
|
-
let aad = new Uint8Array(
|
|
1180
|
-
aad
|
|
1181
|
-
for (let i = 0; i < rnLen; i++) aad[
|
|
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];
|
|
1182
1259
|
if (hdr.hasLength) {
|
|
1183
|
-
aad[
|
|
1184
|
-
aad[
|
|
1260
|
+
aad[rnOff + rnLen] = (ctLen >>> 8) & 0xFF;
|
|
1261
|
+
aad[rnOff + rnLen + 1] = ctLen & 0xFF;
|
|
1185
1262
|
}
|
|
1186
1263
|
|
|
1187
1264
|
let ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
@@ -1221,45 +1298,13 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
|
|
|
1221
1298
|
seq = acceptedSeq;
|
|
1222
1299
|
|
|
1223
1300
|
{
|
|
1224
|
-
//
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1231
|
-
// next to maxReadSeq, because both are per-epoch receive state.
|
|
1232
|
-
// replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
|
|
1233
|
-
// been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
|
|
1234
|
-
// The 64-bit window is kept as two 32-bit numbers (replayLo = bits
|
|
1235
|
-
// 0..31, replayHi = 32..63): same window, no BigInt allocations per
|
|
1236
|
-
// record.
|
|
1237
|
-
if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
|
|
1238
|
-
// First accepted record in this epoch: seed the window with this seq as
|
|
1239
|
-
// the high-water mark (bit 0 set), so an immediate replay of it is
|
|
1240
|
-
// caught below rather than sailing through an unset window.
|
|
1241
|
-
keys.maxReadSeq = seq;
|
|
1242
|
-
keys.replayLo = 1; keys.replayHi = 0;
|
|
1243
|
-
} else if (seq > keys.maxReadSeq) {
|
|
1244
|
-
// Advance: shift the window up by the gap, then set bit 0.
|
|
1245
|
-
let shift = seq - keys.maxReadSeq;
|
|
1246
|
-
let lo = keys.replayLo, hi = keys.replayHi;
|
|
1247
|
-
if (shift >= 64) { lo = 0; hi = 0; }
|
|
1248
|
-
else if (shift >= 32) { hi = (lo << (shift - 32)) >>> 0; lo = 0; }
|
|
1249
|
-
else { hi = ((hi << shift) | (lo >>> (32 - shift))) >>> 0; lo = (lo << shift) >>> 0; }
|
|
1250
|
-
keys.replayLo = (lo | 1) >>> 0; keys.replayHi = hi;
|
|
1251
|
-
keys.maxReadSeq = seq;
|
|
1252
|
-
} else {
|
|
1253
|
-
let back = keys.maxReadSeq - seq;
|
|
1254
|
-
if (back >= 64) return null; // too old — outside window
|
|
1255
|
-
if (back < 32) {
|
|
1256
|
-
if ((keys.replayLo >>> back) & 1) return null; // already seen — replay
|
|
1257
|
-
keys.replayLo = (keys.replayLo | (1 << back)) >>> 0;
|
|
1258
|
-
} else {
|
|
1259
|
-
if ((keys.replayHi >>> (back - 32)) & 1) return null;
|
|
1260
|
-
keys.replayHi = (keys.replayHi | (1 << (back - 32))) >>> 0;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
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;
|
|
1263
1308
|
|
|
1264
1309
|
return {
|
|
1265
1310
|
// The caller resolves the full epoch (resolveReadEpoch); hdr.epoch is
|
|
@@ -1281,17 +1326,26 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
|
|
|
1281
1326
|
* Dispatches between plaintext (classic header) and encrypted (unified header).
|
|
1282
1327
|
*
|
|
1283
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.
|
|
1284
1336
|
* Returns array of { type, epoch, seq, content, encrypted }.
|
|
1285
1337
|
*/
|
|
1286
|
-
function parseDtlsDatagram(data, keysByEpoch) {
|
|
1338
|
+
function parseDtlsDatagram(data, keysByEpoch, cid, cidRequired) {
|
|
1287
1339
|
let records = [];
|
|
1288
1340
|
let off = 0;
|
|
1341
|
+
let cidLen = cid ? cid.length : 0;
|
|
1289
1342
|
|
|
1290
1343
|
while (off < data.length) {
|
|
1291
1344
|
let first = data[off];
|
|
1292
1345
|
|
|
1293
1346
|
if (isUnifiedHdr(first)) {
|
|
1294
1347
|
let hdr = parseUnifiedHdr(first);
|
|
1348
|
+
if (hdr.hasCid && cidLen === 0) break;
|
|
1295
1349
|
// Header carries 2 epoch bits; map them back to a real epoch first.
|
|
1296
1350
|
let fullEpoch = resolveReadEpoch(hdr.epoch, keysByEpoch);
|
|
1297
1351
|
let keys = (fullEpoch === null) ? null : keysByEpoch[fullEpoch];
|
|
@@ -1307,8 +1361,7 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1307
1361
|
// Keep the raw bytes and mark the record deferred; the caller retries
|
|
1308
1362
|
// it after processing each earlier record, once the keys exist. This
|
|
1309
1363
|
// generalises the CCS-only re-decrypt that DTLS 1.2 already had.
|
|
1310
|
-
let
|
|
1311
|
-
let skip = 1 + rnLen;
|
|
1364
|
+
let skip = 1 + (hdr.hasCid ? cidLen : 0) + (hdr.seqLen2 ? 2 : 1);
|
|
1312
1365
|
let total;
|
|
1313
1366
|
if (hdr.hasLength && off + skip + 2 <= data.length) {
|
|
1314
1367
|
total = skip + 2 + readU16(data, off + skip);
|
|
@@ -1333,7 +1386,7 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1333
1386
|
continue;
|
|
1334
1387
|
}
|
|
1335
1388
|
|
|
1336
|
-
let result = decryptEncryptedDtls13(data.subarray(off), keys, fullEpoch);
|
|
1389
|
+
let result = decryptEncryptedDtls13(data.subarray(off), keys, fullEpoch, cid, cidRequired);
|
|
1337
1390
|
if (result) {
|
|
1338
1391
|
records.push({
|
|
1339
1392
|
type: result.type,
|
|
@@ -1348,13 +1401,32 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1348
1401
|
}
|
|
1349
1402
|
|
|
1350
1403
|
} else if (first <= 63) {
|
|
1351
|
-
// Classic DTLS record
|
|
1352
|
-
|
|
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;
|
|
1353
1410
|
let type = data[off];
|
|
1354
1411
|
let epoch = readU16(data, off + 3);
|
|
1355
1412
|
let seq = readU48(data, off + 5);
|
|
1356
|
-
let length = readU16(data, off +
|
|
1357
|
-
if (off +
|
|
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
|
+
}
|
|
1358
1430
|
|
|
1359
1431
|
let payload = null;
|
|
1360
1432
|
let encrypted = false;
|
|
@@ -1365,14 +1437,20 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1365
1437
|
// decrypted — gets its own copy, as before.
|
|
1366
1438
|
if (epoch > 0 && keysByEpoch && keysByEpoch[epoch]) {
|
|
1367
1439
|
let keys = keysByEpoch[epoch];
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
//
|
|
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;
|
|
1373
1451
|
}
|
|
1374
1452
|
}
|
|
1375
|
-
if (payload === null) payload =
|
|
1453
|
+
if (payload === null) payload = fragment.slice();
|
|
1376
1454
|
|
|
1377
1455
|
records.push({
|
|
1378
1456
|
type: type,
|
|
@@ -1381,7 +1459,6 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1381
1459
|
content: payload,
|
|
1382
1460
|
encrypted: encrypted,
|
|
1383
1461
|
});
|
|
1384
|
-
off += 13 + length;
|
|
1385
1462
|
} else {
|
|
1386
1463
|
break;
|
|
1387
1464
|
}
|
|
@@ -1426,16 +1503,31 @@ function buildDtlsAck(acks) {
|
|
|
1426
1503
|
}
|
|
1427
1504
|
|
|
1428
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.
|
|
1429
1519
|
function parseDtlsAck(data) {
|
|
1520
|
+
if (!data || data.length < 2) throw recordError('ACK: truncated');
|
|
1430
1521
|
let bodyLen = readU16(data, 0);
|
|
1431
|
-
|
|
1432
|
-
|
|
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');
|
|
1433
1524
|
let acks = [];
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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) });
|
|
1439
1531
|
}
|
|
1440
1532
|
return acks;
|
|
1441
1533
|
}
|
|
@@ -1477,6 +1569,7 @@ export {
|
|
|
1477
1569
|
// DTLS helpers
|
|
1478
1570
|
aesEcbEncrypt,
|
|
1479
1571
|
isUnifiedHdr,
|
|
1572
|
+
datagramCid,
|
|
1480
1573
|
|
|
1481
1574
|
// DTLS plaintext records
|
|
1482
1575
|
buildDtlsPlaintext,
|
|
@@ -1484,9 +1577,12 @@ export {
|
|
|
1484
1577
|
|
|
1485
1578
|
// DTLS 1.2 encrypted records
|
|
1486
1579
|
buildDtlsAad12,
|
|
1580
|
+
acceptRecordSeq,
|
|
1487
1581
|
encryptDtls12,
|
|
1488
1582
|
decryptDtls12,
|
|
1583
|
+
openDtls12,
|
|
1489
1584
|
buildEncryptedDtls12,
|
|
1585
|
+
TLS12_CID,
|
|
1490
1586
|
|
|
1491
1587
|
// DTLS 1.3 unified header
|
|
1492
1588
|
buildUnifiedHdr,
|