lemon-tls 0.2.2 → 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 +12 -0
- package/package.json +1 -1
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/record.js +408 -61
- package/src/session/message.js +27 -2
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +780 -94
- package/src/tls_socket.js +815 -249
- package/src/wire.js +25 -0
package/src/record.js
CHANGED
|
@@ -42,46 +42,189 @@ function deriveKeys(trafficSecret, cipherSuite) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
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
|
+
*/
|
|
46
52
|
function getNonce(iv, seq) {
|
|
47
|
-
const seqBuf = new Uint8Array(12);
|
|
48
|
-
const view = new DataView(seqBuf.buffer);
|
|
49
|
-
view.setBigUint64(4, BigInt(seq));
|
|
50
53
|
const nonce = new Uint8Array(12);
|
|
51
|
-
|
|
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);
|
|
52
67
|
return nonce;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
56
|
-
*
|
|
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).
|
|
57
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.
|
|
58
104
|
*/
|
|
59
105
|
function encryptRecord(innerType, plaintext, key, nonce, algo) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
112
|
|
|
67
113
|
if (!algo) algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
cipher.setAAD(aad, isChaCha ? { plaintextLength:
|
|
71
|
-
|
|
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);
|
|
72
124
|
cipher.final();
|
|
73
|
-
|
|
125
|
+
const tag = cipher.getAuthTag();
|
|
74
126
|
|
|
75
|
-
|
|
76
|
-
out.
|
|
77
|
-
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);
|
|
78
132
|
return out;
|
|
79
133
|
}
|
|
80
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
|
+
|
|
81
221
|
/**
|
|
82
222
|
* Decrypt a TLS 1.3 record.
|
|
83
223
|
* Input: raw ciphertext || tag (without record header).
|
|
84
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.
|
|
85
228
|
*/
|
|
86
229
|
function decryptRecord(ciphertext, key, nonce, algo) {
|
|
87
230
|
const aad = new Uint8Array(5);
|
|
@@ -89,25 +232,59 @@ function decryptRecord(ciphertext, key, nonce, algo) {
|
|
|
89
232
|
aad[3] = (ciphertext.length >> 8) & 0xff;
|
|
90
233
|
aad[4] = ciphertext.length & 0xff;
|
|
91
234
|
|
|
92
|
-
|
|
93
|
-
|
|
235
|
+
const ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
236
|
+
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
94
237
|
|
|
95
238
|
if (!algo) algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
96
|
-
|
|
97
|
-
|
|
239
|
+
const isChaCha = algo === 'chacha20-poly1305';
|
|
240
|
+
const decipher = crypto.createDecipheriv(algo, key, nonce, isChaCha ? { authTagLength: 16 } : undefined);
|
|
98
241
|
decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
|
|
99
242
|
decipher.setAuthTag(tag);
|
|
100
|
-
|
|
243
|
+
const pt = decipher.update(ct);
|
|
101
244
|
decipher.final();
|
|
102
|
-
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;
|
|
103
273
|
}
|
|
104
274
|
|
|
105
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
|
+
*/
|
|
106
283
|
function parseInnerPlaintext(data) {
|
|
107
284
|
let j = data.length - 1;
|
|
108
285
|
while (j >= 0 && data[j] === 0) j--;
|
|
109
286
|
if (j < 0) throw new Error('Malformed TLSInnerPlaintext');
|
|
110
|
-
return { type: data[j], content: data.
|
|
287
|
+
return { type: data[j], content: data.subarray(0, j) };
|
|
111
288
|
}
|
|
112
289
|
|
|
113
290
|
// ===================== TLS 1.2 primitives =====================
|
|
@@ -120,18 +297,41 @@ function getNonce12(salt4, explicit8) {
|
|
|
120
297
|
return out;
|
|
121
298
|
}
|
|
122
299
|
|
|
123
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Encode sequence number as 8-byte big-endian.
|
|
302
|
+
* Perf: uses Number arithmetic (seq < 2^53 is always safe for TLS).
|
|
303
|
+
*/
|
|
124
304
|
function seqToBytes(seq) {
|
|
125
305
|
const buf = new Uint8Array(8);
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
128
316
|
return buf;
|
|
129
317
|
}
|
|
130
318
|
|
|
131
|
-
/**
|
|
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
|
+
*/
|
|
132
323
|
function buildAad12(seqNum, recordType, plaintextLen) {
|
|
133
324
|
const aad = new Uint8Array(13);
|
|
134
|
-
|
|
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;
|
|
135
335
|
aad[8] = recordType & 0xff;
|
|
136
336
|
aad[9] = 0x03;
|
|
137
337
|
aad[10] = 0x03;
|
|
@@ -140,44 +340,171 @@ function buildAad12(seqNum, recordType, plaintextLen) {
|
|
|
140
340
|
return aad;
|
|
141
341
|
}
|
|
142
342
|
|
|
143
|
-
/**
|
|
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
|
+
*/
|
|
144
347
|
function encrypt12(pt, key, salt4, seqNum, recordType) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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;
|
|
148
351
|
|
|
149
|
-
|
|
150
|
-
|
|
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);
|
|
151
376
|
cipher.setAAD(aad);
|
|
152
|
-
|
|
377
|
+
const ct = cipher.update(pt);
|
|
153
378
|
cipher.final();
|
|
154
|
-
|
|
379
|
+
const tag = cipher.getAuthTag();
|
|
155
380
|
|
|
156
|
-
|
|
157
|
-
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];
|
|
158
386
|
out.set(ct, 8);
|
|
159
387
|
out.set(tag, 8 + ct.length);
|
|
160
388
|
return out;
|
|
161
389
|
}
|
|
162
390
|
|
|
163
|
-
/**
|
|
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
|
+
*/
|
|
164
468
|
function decrypt12(fragment, key, salt4, seqNum, recordType) {
|
|
165
469
|
if (fragment.length < 24) throw new Error('TLS 1.2 fragment too short');
|
|
166
470
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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);
|
|
475
|
+
|
|
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];
|
|
170
481
|
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
499
|
|
|
174
|
-
|
|
175
|
-
|
|
500
|
+
const algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
501
|
+
const decipher = crypto.createDecipheriv(algo, key, nonce);
|
|
176
502
|
decipher.setAAD(aad);
|
|
177
503
|
decipher.setAuthTag(tag);
|
|
178
|
-
|
|
504
|
+
const pt = decipher.update(ct);
|
|
179
505
|
decipher.final();
|
|
180
|
-
|
|
506
|
+
// Node Buffer extends Uint8Array, so callers that expect Uint8Array work unmodified.
|
|
507
|
+
return pt;
|
|
181
508
|
}
|
|
182
509
|
|
|
183
510
|
// ===================== Shared: TLS 1.2 key_block =====================
|
|
@@ -198,16 +525,32 @@ function deriveKeys12(masterSecret, localRandom, remoteRandom, cipherSuite, isSe
|
|
|
198
525
|
/** Content type constants. */
|
|
199
526
|
const CT = { CHANGE_CIPHER_SPEC: 20, ALERT: 21, HANDSHAKE: 22, APPLICATION_DATA: 23, ACK: 26 };
|
|
200
527
|
|
|
201
|
-
/**
|
|
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
|
+
*/
|
|
202
541
|
function writeRecord(transport, type, payload, version) {
|
|
203
|
-
if (!transport || typeof transport.write !== 'function') return;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
rec.
|
|
207
|
-
rec
|
|
208
|
-
rec
|
|
209
|
-
|
|
210
|
-
|
|
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);
|
|
211
554
|
}
|
|
212
555
|
|
|
213
556
|
// ===================== DTLS binary helpers =====================
|
|
@@ -666,8 +1009,11 @@ export {
|
|
|
666
1009
|
// TLS 1.3
|
|
667
1010
|
deriveKeys,
|
|
668
1011
|
getNonce,
|
|
1012
|
+
getNonceInto, // write nonce into a caller-provided buffer (avoid alloc per record)
|
|
669
1013
|
encryptRecord,
|
|
1014
|
+
encryptCompleteRecord13, // fused encrypt + header → single allocation hot path
|
|
670
1015
|
decryptRecord,
|
|
1016
|
+
decryptRecordWithAadView, // decrypt using caller-provided header view as AAD (no alloc)
|
|
671
1017
|
parseInnerPlaintext,
|
|
672
1018
|
|
|
673
1019
|
// TLS 1.2
|
|
@@ -675,6 +1021,7 @@ export {
|
|
|
675
1021
|
seqToBytes,
|
|
676
1022
|
buildAad12,
|
|
677
1023
|
encrypt12,
|
|
1024
|
+
encryptCompleteRecord12, // fused encrypt + header → single allocation hot path
|
|
678
1025
|
decrypt12,
|
|
679
1026
|
deriveKeys12,
|
|
680
1027
|
|
package/src/session/message.js
CHANGED
|
@@ -68,6 +68,15 @@ function normalize_hello(hello) {
|
|
|
68
68
|
out.heartbeat = value;
|
|
69
69
|
} else if (name === 'USE_SRTP') {
|
|
70
70
|
out.use_srtp = value;
|
|
71
|
+
} else if (name === 'SESSION_TICKET') {
|
|
72
|
+
// RFC 5077: opaque ticket bytes (TLS 1.2)
|
|
73
|
+
// Empty in ClientHello = client supports tickets / non-empty = resume using this ticket
|
|
74
|
+
// Empty in ServerHello = server will send NewSessionTicket
|
|
75
|
+
out.session_ticket = value;
|
|
76
|
+
out.session_ticket_supported = true;
|
|
77
|
+
} else if (name === 'EXTENDED_MASTER_SECRET') {
|
|
78
|
+
// RFC 7627: presence signals EMS support
|
|
79
|
+
out.extended_master_secret = true;
|
|
71
80
|
} else {
|
|
72
81
|
if (!('unknown' in out)) out.unknown = [];
|
|
73
82
|
out.unknown.push(e);
|
|
@@ -133,6 +142,12 @@ function build_tls_message(params) {
|
|
|
133
142
|
} else if (params.type == 'hello_retry_request') {
|
|
134
143
|
type = wire.TLS_MESSAGE_TYPE.SERVER_HELLO; // HRR uses ServerHello type with magic random
|
|
135
144
|
body = wire.build_hello_retry_request(params);
|
|
145
|
+
} else if (params.type == 'new_session_ticket_tls12') {
|
|
146
|
+
type = wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET;
|
|
147
|
+
body = wire.build_new_session_ticket_tls12(params);
|
|
148
|
+
} else if (params.type == 'new_session_ticket') {
|
|
149
|
+
type = wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET;
|
|
150
|
+
body = wire.build_new_session_ticket(params);
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
return wire.build_message(type, body);
|
|
@@ -141,8 +156,11 @@ function build_tls_message(params) {
|
|
|
141
156
|
|
|
142
157
|
/**
|
|
143
158
|
* Parse a raw TLS handshake message into a typed object.
|
|
159
|
+
* @param {Uint8Array} data — handshake message bytes
|
|
160
|
+
* @param {number} [negotiatedVersion] — optional; 0x0303 for TLS 1.2, 0x0304 for TLS 1.3.
|
|
161
|
+
* Needed to disambiguate NewSessionTicket wire format.
|
|
144
162
|
*/
|
|
145
|
-
function parse_tls_message(data) {
|
|
163
|
+
function parse_tls_message(data, negotiatedVersion) {
|
|
146
164
|
let out = {};
|
|
147
165
|
let message = wire.parse_message(data);
|
|
148
166
|
|
|
@@ -183,7 +201,14 @@ function parse_tls_message(data) {
|
|
|
183
201
|
out.body = message.body;
|
|
184
202
|
|
|
185
203
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET) {
|
|
186
|
-
|
|
204
|
+
// TLS 1.2 and 1.3 have different wire formats for this message.
|
|
205
|
+
// TLS 1.3: ticket_lifetime | ticket_age_add | ticket_nonce | ticket | extensions
|
|
206
|
+
// TLS 1.2: ticket_lifetime_hint | ticket (RFC 5077)
|
|
207
|
+
if (negotiatedVersion === 0x0303) {
|
|
208
|
+
out = wire.parse_new_session_ticket_tls12(message.body);
|
|
209
|
+
} else {
|
|
210
|
+
out = wire.parse_new_session_ticket(message.body);
|
|
211
|
+
}
|
|
187
212
|
out.type = 'new_session_ticket';
|
|
188
213
|
|
|
189
214
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.KEY_UPDATE) {
|