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