lemon-tls 0.1.0 → 0.2.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 +432 -122
- package/index.cjs +28 -0
- package/index.d.ts +283 -0
- package/index.js +70 -15
- package/lemontls.svg +1 -1
- package/package.json +120 -62
- package/src/compat.js +235 -0
- package/src/crypto.js +580 -0
- package/src/record.js +232 -0
- package/{secure_context.js → src/secure_context.js} +196 -196
- package/src/session/ecdh.js +61 -0
- package/src/session/message.js +203 -0
- package/src/session/signing.js +129 -0
- package/src/tls_session.js +2204 -0
- package/src/tls_socket.js +877 -0
- package/{utils.js → src/utils.js} +100 -87
- package/{wire.js → src/wire.js} +1499 -1672
- package/crypto.js +0 -383
- package/tls_server.js +0 -0
- package/tls_session.js +0 -1441
- package/tls_socket.js +0 -456
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
function writeClientRandomKeyLog(clientRandom, masterSecret, filePath) {
|
|
5
|
+
function toHex(u8) {
|
|
6
|
+
return Array.from(u8)
|
|
7
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
8
|
+
.join('');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const line = `CLIENT_RANDOM ${toHex(clientRandom)} ${toHex(masterSecret)}\n`;
|
|
12
|
+
|
|
13
|
+
fs.appendFileSync(filePath, line, 'utf8');
|
|
14
|
+
console.log(`✅ Added CLIENT_RANDOM line to ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import TLSSession from './tls_session.js';
|
|
18
|
+
|
|
19
|
+
import crypto from 'node:crypto';
|
|
20
|
+
import { Duplex } from 'node:stream';
|
|
21
|
+
|
|
22
|
+
import { TLS_CIPHER_SUITES } from './crypto.js';
|
|
23
|
+
import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT } from './wire.js';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
getAeadAlgo,
|
|
27
|
+
deriveKeys as tls_derive_from_tls_secrets,
|
|
28
|
+
getNonce as get_nonce,
|
|
29
|
+
encryptRecord as encrypt_tls_record,
|
|
30
|
+
decryptRecord as decrypt_tls_record,
|
|
31
|
+
parseInnerPlaintext as parse_tls_inner_plaintext,
|
|
32
|
+
encrypt12 as encrypt_tls12_gcm_fragment,
|
|
33
|
+
decrypt12 as decrypt_tls12_gcm_fragment,
|
|
34
|
+
deriveKeys12,
|
|
35
|
+
writeRecord as writeRawRecord,
|
|
36
|
+
} from './record.js';
|
|
37
|
+
|
|
38
|
+
// legacy_record_version (TLS 1.3 uses 0x0303 in record header)
|
|
39
|
+
const REC_VERSION = 0x0303;
|
|
40
|
+
|
|
41
|
+
// ==== עזרי המרה ====
|
|
42
|
+
function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); }
|
|
43
|
+
function toU8(buf){ return (buf instanceof Uint8Array) ? buf : new Uint8Array(buf || []); }
|
|
44
|
+
|
|
45
|
+
function parseVersion(v) {
|
|
46
|
+
if (typeof v === 'number') return v;
|
|
47
|
+
if (typeof v === 'string') {
|
|
48
|
+
let s = v.toUpperCase().replace(/[^0-9.]/g, '');
|
|
49
|
+
if (s === '1.3' || s === '13') return 0x0304;
|
|
50
|
+
if (s === '1.2' || s === '12') return 0x0303;
|
|
51
|
+
if (s === '1.1' || s === '11') return 0x0302;
|
|
52
|
+
if (s === '1.0' || s === '10') return 0x0301;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// ==== TLSSocket ====
|
|
66
|
+
function TLSSocket(duplex, options){
|
|
67
|
+
if (!(this instanceof TLSSocket)) return new TLSSocket(duplex, options);
|
|
68
|
+
options = options || {};
|
|
69
|
+
|
|
70
|
+
// Inherit from Duplex stream
|
|
71
|
+
Duplex.call(this, { allowHalfOpen: true, readableObjectMode: false, writableObjectMode: false });
|
|
72
|
+
const self = this;
|
|
73
|
+
|
|
74
|
+
let _ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
|
|
75
|
+
|
|
76
|
+
let context = {
|
|
77
|
+
|
|
78
|
+
options: options,
|
|
79
|
+
|
|
80
|
+
// External transport (Duplex)
|
|
81
|
+
transport: (duplex && typeof duplex.write === 'function') ? duplex : null,
|
|
82
|
+
|
|
83
|
+
// Internal TLSSession
|
|
84
|
+
session: new TLSSession({
|
|
85
|
+
isServer: !!options.isServer,
|
|
86
|
+
servername: options.servername,
|
|
87
|
+
ALPNProtocols: options.ALPNProtocols || null,
|
|
88
|
+
SNICallback: options.SNICallback || null,
|
|
89
|
+
ticketKeys: _ticketKeys,
|
|
90
|
+
session: options.session || null,
|
|
91
|
+
psk: options.psk || null,
|
|
92
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
93
|
+
ca: options.ca || null,
|
|
94
|
+
noTickets: !!options.noTickets,
|
|
95
|
+
maxHandshakeSize: options.maxHandshakeSize || 0,
|
|
96
|
+
customExtensions: options.customExtensions || [],
|
|
97
|
+
requestCert: !!options.requestCert,
|
|
98
|
+
cert: options.cert || null,
|
|
99
|
+
key: options.key || null,
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
// Handshake write
|
|
103
|
+
handshake_write_key: null,
|
|
104
|
+
handshake_write_iv: null,
|
|
105
|
+
handshake_write_seq: 0,
|
|
106
|
+
handshake_write_aead: null,
|
|
107
|
+
|
|
108
|
+
// Handshake read
|
|
109
|
+
handshake_read_key: null,
|
|
110
|
+
handshake_read_iv: null,
|
|
111
|
+
handshake_read_seq: 0,
|
|
112
|
+
handshake_read_aead: null,
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// Application write
|
|
116
|
+
app_write_key: null,
|
|
117
|
+
app_write_iv: null,
|
|
118
|
+
app_write_seq: 0,
|
|
119
|
+
app_write_aead: null,
|
|
120
|
+
|
|
121
|
+
// Application read
|
|
122
|
+
app_read_key: null,
|
|
123
|
+
app_read_iv: null,
|
|
124
|
+
app_read_seq: 0,
|
|
125
|
+
app_read_aead: null,
|
|
126
|
+
|
|
127
|
+
using_app_keys: false,
|
|
128
|
+
|
|
129
|
+
remote_ccs_seen: false,
|
|
130
|
+
local_ccs_sent: false,
|
|
131
|
+
|
|
132
|
+
tls12_read_seq: 0,
|
|
133
|
+
|
|
134
|
+
// Buffers and queues
|
|
135
|
+
readBuffer: Buffer.alloc(0),
|
|
136
|
+
appWriteQueue: [],
|
|
137
|
+
pendingHandshake: [],
|
|
138
|
+
|
|
139
|
+
// General state
|
|
140
|
+
destroyed: false,
|
|
141
|
+
secureEstablished: false,
|
|
142
|
+
aeadAlgo: null, // set when cipher suite is negotiated
|
|
143
|
+
|
|
144
|
+
// Session ticket keys for PSK resumption
|
|
145
|
+
ticketKeys: _ticketKeys,
|
|
146
|
+
|
|
147
|
+
// Advanced options
|
|
148
|
+
maxRecordSize: options.maxRecordSize || 16384,
|
|
149
|
+
noTickets: !!options.noTickets,
|
|
150
|
+
pins: options.pins || null, // ['sha256/AAAA...'] certificate pinning
|
|
151
|
+
handshakeTimeout: options.handshakeTimeout || 0, // ms, 0 = no timeout
|
|
152
|
+
allowedCipherSuites: options.allowedCipherSuites || null, // [0x1301, ...] whitelist
|
|
153
|
+
certificateCallback: options.certificateCallback || null, // (info, cb) => cb(null, ctx)
|
|
154
|
+
handshakeTimer: null,
|
|
155
|
+
|
|
156
|
+
// legacy record version
|
|
157
|
+
rec_version: 0x0303
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let session = context.session;
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
// === Record Layer ===
|
|
164
|
+
function writeRecord(type, payload){
|
|
165
|
+
if (!context.transport) throw new Error('No transport attached to TLSSocket');
|
|
166
|
+
if (context.destroyed || context.transport.destroyed || context.transport.writableEnded) return;
|
|
167
|
+
try { writeRawRecord(context.transport, type, payload, context.rec_version); }
|
|
168
|
+
catch(e){ self.emit('error', e); }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const MAX_RECORD_PLAINTEXT = 16384; // TLS max record size (2^14)
|
|
172
|
+
|
|
173
|
+
function writeAppData(plain){
|
|
174
|
+
// Fragment large writes into multiple TLS records
|
|
175
|
+
let maxSize = context.maxRecordSize || 16384;
|
|
176
|
+
if (plain.length > maxSize) {
|
|
177
|
+
for (let off = 0; off < plain.length; off += maxSize) {
|
|
178
|
+
let chunk = plain.slice(off, Math.min(off + maxSize, plain.length));
|
|
179
|
+
writeAppDataSingle(chunk);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
writeAppDataSingle(plain);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeAppDataSingle(plain){
|
|
187
|
+
let isTls13 = session.getVersion() === 0x0304;
|
|
188
|
+
|
|
189
|
+
if(isTls13){
|
|
190
|
+
if(session.getTrafficSecrets().localAppSecret!==null){
|
|
191
|
+
if(context.app_write_key==null || context.app_write_iv==null){
|
|
192
|
+
let d=tls_derive_from_tls_secrets(session.getTrafficSecrets().localAppSecret,session.getCipher());
|
|
193
|
+
|
|
194
|
+
context.app_write_key=d.key;
|
|
195
|
+
context.app_write_iv=d.iv;
|
|
196
|
+
}
|
|
197
|
+
}else{
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let enc1 = encrypt_tls_record(CT.APPLICATION_DATA, plain, context.app_write_key, get_nonce(context.app_write_iv,context.app_write_seq), context.aeadAlgo || getAeadAlgo(session.getCipher()));
|
|
202
|
+
|
|
203
|
+
context.app_write_seq++;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
writeRecord(CT.APPLICATION_DATA, Buffer.from(enc1));
|
|
207
|
+
} catch(e){
|
|
208
|
+
self.emit('error', e);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
}else{
|
|
212
|
+
// TLS 1.2: derive keys if needed, then encrypt with GCM
|
|
213
|
+
if(context.app_write_key==null || context.app_write_iv==null){
|
|
214
|
+
let d12 = deriveKeys12(session.getTrafficSecrets().masterSecret, session.getTrafficSecrets().localRandom, session.getTrafficSecrets().remoteRandom, session.getCipher(), session.isServer);
|
|
215
|
+
context.app_write_key = d12.writeKey;
|
|
216
|
+
context.app_write_iv = d12.writeIv;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let fragment = encrypt_tls12_gcm_fragment(
|
|
220
|
+
plain,
|
|
221
|
+
context.app_write_key,
|
|
222
|
+
context.app_write_iv,
|
|
223
|
+
context.app_write_seq,
|
|
224
|
+
CT.APPLICATION_DATA
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
context.app_write_seq++;
|
|
228
|
+
|
|
229
|
+
writeRecord(CT.APPLICATION_DATA, Buffer.from(fragment));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function processCiphertext(body){
|
|
235
|
+
|
|
236
|
+
let out=null;
|
|
237
|
+
let isTls13 = session.getVersion() === 0x0304;
|
|
238
|
+
|
|
239
|
+
if(!isTls13 && context.remote_ccs_seen==true){
|
|
240
|
+
// TLS 1.2: decrypt with key_block derived keys
|
|
241
|
+
if(context.app_read_key==null || context.app_read_iv==null){
|
|
242
|
+
|
|
243
|
+
let d12 = deriveKeys12(session.getTrafficSecrets().masterSecret, session.getTrafficSecrets().localRandom, session.getTrafficSecrets().remoteRandom, session.getCipher(), session.isServer);
|
|
244
|
+
context.app_read_key = d12.readKey;
|
|
245
|
+
context.app_read_iv = d12.readIv;
|
|
246
|
+
context.app_write_key = d12.writeKey;
|
|
247
|
+
context.app_write_iv = d12.writeIv;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let recordType = context.using_app_keys ? 0x17 : 0x16;
|
|
251
|
+
out = decrypt_tls12_gcm_fragment(new Uint8Array(body), context.app_read_key, context.app_read_iv, context.tls12_read_seq, recordType);
|
|
252
|
+
|
|
253
|
+
}else if(isTls13 && context.using_app_keys==true){
|
|
254
|
+
// TLS 1.3: decrypt app data with app traffic secret
|
|
255
|
+
if(session.getTrafficSecrets().remoteAppSecret!==null){
|
|
256
|
+
if(context.app_read_key==null || context.app_read_iv==null){
|
|
257
|
+
let d=tls_derive_from_tls_secrets(session.getTrafficSecrets().remoteAppSecret,session.getCipher());
|
|
258
|
+
context.app_read_key=d.key;
|
|
259
|
+
context.app_read_iv=d.iv;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
out = decrypt_tls_record(body, context.app_read_key, get_nonce(context.app_read_iv,context.app_read_seq), context.aeadAlgo || getAeadAlgo(session.getCipher()));
|
|
263
|
+
context.app_read_seq++;
|
|
264
|
+
}
|
|
265
|
+
}else if(isTls13){
|
|
266
|
+
// TLS 1.3: decrypt handshake with handshake traffic secret
|
|
267
|
+
if(session.getHandshakeSecrets().remoteSecret!==null && session.getCipher()!==null){
|
|
268
|
+
if(context.handshake_read_key==null || context.handshake_read_iv==null){
|
|
269
|
+
let d=tls_derive_from_tls_secrets(session.getHandshakeSecrets().remoteSecret,session.getCipher());
|
|
270
|
+
context.handshake_read_key=d.key;
|
|
271
|
+
context.handshake_read_iv=d.iv;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
out = decrypt_tls_record(body, context.handshake_read_key, get_nonce(context.handshake_read_iv,context.handshake_read_seq), context.aeadAlgo || getAeadAlgo(session.getCipher()));
|
|
275
|
+
context.handshake_read_seq++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if(out!==null){
|
|
281
|
+
let isTls13 = session.getVersion() === 0x0304;
|
|
282
|
+
|
|
283
|
+
if(isTls13){
|
|
284
|
+
let {type: content_type, content} = parse_tls_inner_plaintext(out);
|
|
285
|
+
|
|
286
|
+
if(content_type === CT.APPLICATION_DATA){
|
|
287
|
+
self.push(Buffer.from(content));
|
|
288
|
+
|
|
289
|
+
}else if(content_type === CT.HANDSHAKE){
|
|
290
|
+
session.message(new Uint8Array(content));
|
|
291
|
+
|
|
292
|
+
}else if(content_type === CT.ALERT){
|
|
293
|
+
// TODO: handle alert
|
|
294
|
+
}
|
|
295
|
+
}else{
|
|
296
|
+
// TLS 1.2: no inner plaintext wrapping
|
|
297
|
+
if(context.using_app_keys==true){
|
|
298
|
+
self.push(Buffer.from(out));
|
|
299
|
+
}else{
|
|
300
|
+
session.message(new Uint8Array(out));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
function parseRecordsAndDispatch(){
|
|
311
|
+
while (context.readBuffer.length >= 5) {
|
|
312
|
+
let type = context.readBuffer.readUInt8(0);
|
|
313
|
+
let ver = context.readBuffer.readUInt16BE(1);
|
|
314
|
+
let len = context.readBuffer.readUInt16BE(3);
|
|
315
|
+
if (context.readBuffer.length < 5 + len) break;
|
|
316
|
+
|
|
317
|
+
let body = context.readBuffer.slice(5, 5+len);
|
|
318
|
+
context.readBuffer = context.readBuffer.slice(5+len);
|
|
319
|
+
|
|
320
|
+
if (type === CT.APPLICATION_DATA) {
|
|
321
|
+
processCiphertext(body);
|
|
322
|
+
|
|
323
|
+
context.tls12_read_seq++;
|
|
324
|
+
|
|
325
|
+
}else if(type === CT.HANDSHAKE){
|
|
326
|
+
if(context.remote_ccs_seen==true){
|
|
327
|
+
processCiphertext(body);
|
|
328
|
+
}else{
|
|
329
|
+
session.message(new Uint8Array(body));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
context.tls12_read_seq++;
|
|
333
|
+
|
|
334
|
+
}else if(type === CT.CHANGE_CIPHER_SPEC){
|
|
335
|
+
|
|
336
|
+
context.tls12_read_seq=0;
|
|
337
|
+
context.remote_ccs_seen=true;
|
|
338
|
+
|
|
339
|
+
}else if(type === CT.ALERT ){
|
|
340
|
+
// Alert: 2 bytes — level (1=warning, 2=fatal), description
|
|
341
|
+
if(body.length >= 2){
|
|
342
|
+
let level = body[0];
|
|
343
|
+
let desc = body[1];
|
|
344
|
+
self.emit('alert', { level: level, description: desc });
|
|
345
|
+
if(desc === TLS_ALERT.CLOSE_NOTIFY){
|
|
346
|
+
// Peer is closing — send close_notify back and close
|
|
347
|
+
session.close();
|
|
348
|
+
if(context.transport && typeof context.transport.end === 'function'){
|
|
349
|
+
context.transport.end();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if(level === TLS_ALERT_LEVEL.FATAL){
|
|
353
|
+
// Fatal alert — close immediately
|
|
354
|
+
if(context.transport && typeof context.transport.destroy === 'function'){
|
|
355
|
+
context.transport.destroy();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
function bindTransport(){
|
|
368
|
+
if (!context.transport) return;
|
|
369
|
+
context.transport.on('data', function(chunk){
|
|
370
|
+
context.readBuffer = Buffer.concat([context.readBuffer, chunk]);
|
|
371
|
+
parseRecordsAndDispatch();
|
|
372
|
+
});
|
|
373
|
+
context.transport.on('error', function(err){ self.emit('error', err); });
|
|
374
|
+
context.transport.on('close', function(){ self.emit('close'); });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
session.on('message', function(epoch, seq, type, data){
|
|
378
|
+
let buf = toBuf(data || []);
|
|
379
|
+
|
|
380
|
+
// Alert messages — send as ALERT record type
|
|
381
|
+
if (type === 'alert') {
|
|
382
|
+
let isTls13 = session.getVersion() === 0x0304;
|
|
383
|
+
if (isTls13 && context.using_app_keys && context.app_write_key) {
|
|
384
|
+
// TLS 1.3: post-handshake alerts are encrypted
|
|
385
|
+
let enc = encrypt_tls_record(CT.ALERT, buf, context.app_write_key, get_nonce(context.app_write_iv, context.app_write_seq), context.aeadAlgo || getAeadAlgo(session.getCipher()));
|
|
386
|
+
context.app_write_seq++;
|
|
387
|
+
writeRecord(CT.APPLICATION_DATA, Buffer.from(enc));
|
|
388
|
+
} else {
|
|
389
|
+
writeRecord(CT.ALERT, buf);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (epoch === 0) {
|
|
395
|
+
// Cleartext handshake (ClientHello/ServerHello)
|
|
396
|
+
if (!context.transport) {
|
|
397
|
+
context.pendingHandshake.push({ type: CT.HANDSHAKE, data: buf });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
writeRecord(CT.HANDSHAKE, buf);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (epoch === 1) {
|
|
405
|
+
let isTls13 = session.getVersion() === 0x0304;
|
|
406
|
+
|
|
407
|
+
if(isTls13){
|
|
408
|
+
// TLS 1.3: encrypt handshake messages with handshake traffic secret
|
|
409
|
+
if(session.getHandshakeSecrets().localSecret!==null){
|
|
410
|
+
|
|
411
|
+
if(context.handshake_write_key==null || context.handshake_write_iv==null){
|
|
412
|
+
let d=tls_derive_from_tls_secrets(session.getHandshakeSecrets().localSecret,session.getCipher());
|
|
413
|
+
|
|
414
|
+
context.handshake_write_key=d.key;
|
|
415
|
+
context.handshake_write_iv=d.iv;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let enc1 = encrypt_tls_record(CT.HANDSHAKE, buf, context.handshake_write_key, get_nonce(context.handshake_write_iv,context.handshake_write_seq), context.aeadAlgo || getAeadAlgo(session.getCipher()));
|
|
419
|
+
|
|
420
|
+
context.handshake_write_seq++;
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
writeRecord(CT.APPLICATION_DATA, Buffer.from(enc1));
|
|
424
|
+
} catch(e){
|
|
425
|
+
self.emit('error', e);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
}else{
|
|
429
|
+
self.emit('error', new Error('Missing handshake write keys'));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
}else{
|
|
433
|
+
// TLS 1.2: send CCS first, then encrypt Finished with key_block keys
|
|
434
|
+
if(context.local_ccs_sent==false){
|
|
435
|
+
writeRecord(CT.CHANGE_CIPHER_SPEC, Buffer.from([0x01]));
|
|
436
|
+
context.local_ccs_sent=true;
|
|
437
|
+
// Reset write seq after CCS (TLS 1.2 spec)
|
|
438
|
+
context.app_write_seq=0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Derive keys if not yet done
|
|
442
|
+
if(context.app_write_key==null || context.app_write_iv==null){
|
|
443
|
+
let d12 = deriveKeys12(session.getTrafficSecrets().masterSecret, session.getTrafficSecrets().localRandom, session.getTrafficSecrets().remoteRandom, session.getCipher(), session.isServer);
|
|
444
|
+
context.app_read_key = d12.readKey;
|
|
445
|
+
context.app_read_iv = d12.readIv;
|
|
446
|
+
context.app_write_key = d12.writeKey;
|
|
447
|
+
context.app_write_iv = d12.writeIv;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let fragment = encrypt_tls12_gcm_fragment(
|
|
451
|
+
buf,
|
|
452
|
+
context.app_write_key,
|
|
453
|
+
context.app_write_iv,
|
|
454
|
+
context.app_write_seq,
|
|
455
|
+
CT.HANDSHAKE
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
context.app_write_seq++;
|
|
459
|
+
|
|
460
|
+
writeRecord(CT.HANDSHAKE, Buffer.from(fragment));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (epoch === 2) {
|
|
466
|
+
// Post-handshake message encrypted with app keys (e.g. NewSessionTicket)
|
|
467
|
+
if (!context.app_write_key) {
|
|
468
|
+
// Derive app write keys if not yet done
|
|
469
|
+
let ts = session.getTrafficSecrets();
|
|
470
|
+
if (ts.localAppSecret) {
|
|
471
|
+
let d = tls_derive_from_tls_secrets(ts.localAppSecret, session.getCipher());
|
|
472
|
+
context.app_write_key = d.key;
|
|
473
|
+
context.app_write_iv = d.iv;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (context.app_write_key) {
|
|
477
|
+
let algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
478
|
+
let enc = encrypt_tls_record(CT.HANDSHAKE, buf, context.app_write_key, get_nonce(context.app_write_iv, context.app_write_seq), algo);
|
|
479
|
+
context.app_write_seq++;
|
|
480
|
+
writeRecord(CT.APPLICATION_DATA, Buffer.from(enc));
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
session.on('hello', function(){
|
|
488
|
+
context.rec_version = 0x0303; // legacy record version (both 1.2 and 1.3)
|
|
489
|
+
|
|
490
|
+
// Client already sent its preferences in ClientHello — don't override
|
|
491
|
+
if (!session.isServer) return;
|
|
492
|
+
|
|
493
|
+
// Parse version range from options
|
|
494
|
+
let maxVer = parseVersion(options.maxVersion) || 0x0304;
|
|
495
|
+
let minVer = parseVersion(options.minVersion) || 0x0303;
|
|
496
|
+
|
|
497
|
+
let versions = [];
|
|
498
|
+
if (maxVer >= 0x0304 && minVer <= 0x0304) versions.push(0x0304);
|
|
499
|
+
if (maxVer >= 0x0303 && minVer <= 0x0303) versions.push(0x0303);
|
|
500
|
+
if (versions.length === 0) versions.push(0x0303); // fallback
|
|
501
|
+
|
|
502
|
+
// Cipher suites based on supported versions
|
|
503
|
+
let ciphers = [];
|
|
504
|
+
if (versions.includes(0x0304)) {
|
|
505
|
+
ciphers.push(0x1301, 0x1302, 0x1303); // TLS_AES_128_GCM, TLS_AES_256_GCM, TLS_CHACHA20
|
|
506
|
+
}
|
|
507
|
+
if (versions.includes(0x0303)) {
|
|
508
|
+
ciphers.push(
|
|
509
|
+
0xC02F, // ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
|
510
|
+
0xC030, // ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
|
511
|
+
0xC02B, // ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
|
512
|
+
0xCCA8 // ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Signature algorithms: override or default
|
|
517
|
+
let sigalgs = [];
|
|
518
|
+
if (options.signatureAlgorithms) {
|
|
519
|
+
sigalgs = options.signatureAlgorithms;
|
|
520
|
+
} else {
|
|
521
|
+
if (versions.includes(0x0304)) {
|
|
522
|
+
sigalgs.push(0x0804, 0x0805, 0x0806); // RSA-PSS
|
|
523
|
+
sigalgs.push(0x0403, 0x0503, 0x0603); // ECDSA
|
|
524
|
+
}
|
|
525
|
+
if (versions.includes(0x0303)) {
|
|
526
|
+
sigalgs.push(0x0401, 0x0501, 0x0601); // RSA-PKCS1
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Groups: override or default
|
|
531
|
+
let groups = options.groups || [0x001d, 0x0017]; // X25519, P-256
|
|
532
|
+
|
|
533
|
+
// prioritizeChaCha: move ChaCha20 to front of cipher list
|
|
534
|
+
if (options.prioritizeChaCha) {
|
|
535
|
+
let chacha = ciphers.filter(c => c === 0x1303 || c === 0xCCA8);
|
|
536
|
+
let rest = ciphers.filter(c => c !== 0x1303 && c !== 0xCCA8);
|
|
537
|
+
ciphers = [...chacha, ...rest];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// allowedCipherSuites: whitelist filter
|
|
541
|
+
if (context.allowedCipherSuites) {
|
|
542
|
+
ciphers = ciphers.filter(c => context.allowedCipherSuites.includes(c));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ALPN from options
|
|
546
|
+
let alpns = Array.isArray(options.ALPNProtocols) ? options.ALPNProtocols : [];
|
|
547
|
+
|
|
548
|
+
session.set_context({
|
|
549
|
+
local_supported_versions: versions,
|
|
550
|
+
local_supported_alpns: alpns,
|
|
551
|
+
local_supported_groups: groups,
|
|
552
|
+
local_supported_cipher_suites: ciphers,
|
|
553
|
+
local_supported_signature_algorithms: sigalgs,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Resolve AEAD algorithm once cipher is negotiated
|
|
557
|
+
if (session.getCipher()) context.aeadAlgo = getAeadAlgo(session.getCipher());
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
session.on('secureConnect', function(){
|
|
561
|
+
context.using_app_keys=true;
|
|
562
|
+
context.secureEstablished=true;
|
|
563
|
+
context.aeadAlgo = getAeadAlgo(session.getCipher());
|
|
564
|
+
|
|
565
|
+
// Clear handshake timeout
|
|
566
|
+
if (context.handshakeTimer) { clearTimeout(context.handshakeTimer); context.handshakeTimer = null; }
|
|
567
|
+
|
|
568
|
+
// Certificate pinning
|
|
569
|
+
if (context.pins && context.pins.length > 0) {
|
|
570
|
+
try {
|
|
571
|
+
let cert = session.getPeerCertificate();
|
|
572
|
+
if (cert && cert.raw) {
|
|
573
|
+
let hash = 'sha256/' + crypto.createHash('sha256').update(cert.raw).digest('base64');
|
|
574
|
+
if (!context.pins.includes(hash)) {
|
|
575
|
+
self.emit('error', new Error('Certificate pin mismatch: ' + hash));
|
|
576
|
+
self.destroy();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch(e) { /* pinning is best-effort if no raw cert */ }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Emit keylog lines (SSLKEYLOGFILE compatible)
|
|
584
|
+
try {
|
|
585
|
+
let ts = session.getTrafficSecrets();
|
|
586
|
+
let clientRandom = Buffer.from(session.context.local_random || session.context.remote_random || []).toString('hex');
|
|
587
|
+
if (session.isServer) clientRandom = Buffer.from(session.context.remote_random || []).toString('hex');
|
|
588
|
+
|
|
589
|
+
if (session.getVersion() === 0x0304) {
|
|
590
|
+
// TLS 1.3 key log
|
|
591
|
+
let hs = session.getHandshakeSecrets();
|
|
592
|
+
if (hs.localSecret) self.emit('keylog', Buffer.from(`SERVER_HANDSHAKE_TRAFFIC_SECRET ${clientRandom} ${Buffer.from(session.isServer ? hs.localSecret : hs.remoteSecret).toString('hex')}\n`));
|
|
593
|
+
if (hs.remoteSecret) self.emit('keylog', Buffer.from(`CLIENT_HANDSHAKE_TRAFFIC_SECRET ${clientRandom} ${Buffer.from(session.isServer ? hs.remoteSecret : hs.localSecret).toString('hex')}\n`));
|
|
594
|
+
if (ts.localAppSecret) self.emit('keylog', Buffer.from(`SERVER_TRAFFIC_SECRET_0 ${clientRandom} ${Buffer.from(session.isServer ? ts.localAppSecret : ts.remoteAppSecret).toString('hex')}\n`));
|
|
595
|
+
if (ts.remoteAppSecret) self.emit('keylog', Buffer.from(`CLIENT_TRAFFIC_SECRET_0 ${clientRandom} ${Buffer.from(session.isServer ? ts.remoteAppSecret : ts.localAppSecret).toString('hex')}\n`));
|
|
596
|
+
} else {
|
|
597
|
+
// TLS 1.2 key log
|
|
598
|
+
if (ts.masterSecret) self.emit('keylog', Buffer.from(`CLIENT_RANDOM ${clientRandom} ${Buffer.from(ts.masterSecret).toString('hex')}\n`));
|
|
599
|
+
}
|
|
600
|
+
} catch(e) { /* keylog is best-effort */ }
|
|
601
|
+
|
|
602
|
+
// Flush any queued writes
|
|
603
|
+
while (context.appWriteQueue.length > 0) {
|
|
604
|
+
writeAppData(context.appWriteQueue.shift());
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
self.emit('secureConnect');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Forward session ticket event (Node.js compatible)
|
|
611
|
+
session.on('session', function(ticketData) {
|
|
612
|
+
self.emit('session', ticketData);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Forward handshakeMessage event (for debugging/inspection)
|
|
616
|
+
session.on('handshakeMessage', function(type, raw, parsed) {
|
|
617
|
+
self.emit('handshakeMessage', type, raw, parsed);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Forward raw clienthello event (server-side, for JA3/inspection)
|
|
621
|
+
session.on('clienthello', function(raw, parsed) {
|
|
622
|
+
self.emit('clienthello', raw, parsed);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Handshake timeout
|
|
626
|
+
if (context.handshakeTimeout > 0) {
|
|
627
|
+
context.handshakeTimer = setTimeout(function() {
|
|
628
|
+
if (!context.secureEstablished) {
|
|
629
|
+
self.emit('error', new Error('Handshake timeout after ' + context.handshakeTimeout + 'ms'));
|
|
630
|
+
self.destroy();
|
|
631
|
+
}
|
|
632
|
+
}, context.handshakeTimeout);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// certificateCallback: dynamic cert selection (extends SNICallback)
|
|
636
|
+
if (context.certificateCallback && options.isServer) {
|
|
637
|
+
session.on('hello', function() {
|
|
638
|
+
let info = {
|
|
639
|
+
servername: session.context.remote_sni,
|
|
640
|
+
version: session.context.selected_version,
|
|
641
|
+
ciphers: session.context.remote_supported_cipher_suites,
|
|
642
|
+
sigalgs: session.context.remote_supported_signature_algorithms,
|
|
643
|
+
groups: session.context.remote_supported_groups,
|
|
644
|
+
alpns: session.context.remote_supported_alpns,
|
|
645
|
+
};
|
|
646
|
+
context.certificateCallback(info, function(err, ctx) {
|
|
647
|
+
if (!err && ctx) {
|
|
648
|
+
session.set_context({
|
|
649
|
+
local_cert_chain: ctx.certificateChain,
|
|
650
|
+
cert_private_key: ctx.privateKey,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// KeyUpdate: swap record layer keys when TLSSession derives new secrets
|
|
658
|
+
session.on('keyUpdate', function(info) {
|
|
659
|
+
if (info.direction === 'send') {
|
|
660
|
+
// We updated our send secret → derive new write keys, reset seq
|
|
661
|
+
let d = tls_derive_from_tls_secrets(info.secret, session.getCipher());
|
|
662
|
+
context.app_write_key = d.key;
|
|
663
|
+
context.app_write_iv = d.iv;
|
|
664
|
+
context.app_write_seq = 0;
|
|
665
|
+
} else if (info.direction === 'receive') {
|
|
666
|
+
// Peer updated their send secret → derive new read keys, reset seq
|
|
667
|
+
let d = tls_derive_from_tls_secrets(info.secret, session.getCipher());
|
|
668
|
+
context.app_read_key = d.key;
|
|
669
|
+
context.app_read_iv = d.iv;
|
|
670
|
+
context.app_read_seq = 0;
|
|
671
|
+
}
|
|
672
|
+
self.emit('keyUpdate', info.direction);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Forward certificateRequest event
|
|
676
|
+
session.on('certificateRequest', function(msg) {
|
|
677
|
+
self.emit('certificateRequest', msg);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Server: automatic PSK handler (decrypts tickets with ticketKeys)
|
|
681
|
+
if (options.isServer) {
|
|
682
|
+
session.on('psk', function(identity, callback) {
|
|
683
|
+
// First check if user has a custom handler
|
|
684
|
+
if (self.listenerCount('psk') > 0) {
|
|
685
|
+
// Let user handle it
|
|
686
|
+
self.emit('psk', identity, callback);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Auto-decrypt ticket using ticketKeys
|
|
691
|
+
try {
|
|
692
|
+
let tk = context.ticketKeys;
|
|
693
|
+
let ticket_enc_key = tk.slice(0, 32);
|
|
694
|
+
let id = Buffer.from(identity);
|
|
695
|
+
if (id.length < 12 + 16) { callback(null); return; }
|
|
696
|
+
|
|
697
|
+
let iv = id.slice(0, 12);
|
|
698
|
+
let tag = id.slice(id.length - 16);
|
|
699
|
+
let ct = id.slice(12, id.length - 16);
|
|
700
|
+
|
|
701
|
+
let decipher = crypto.createDecipheriv('aes-256-gcm', ticket_enc_key, iv);
|
|
702
|
+
decipher.setAuthTag(tag);
|
|
703
|
+
let pt = decipher.update(ct);
|
|
704
|
+
decipher.final();
|
|
705
|
+
|
|
706
|
+
let data = JSON.parse(pt.toString());
|
|
707
|
+
callback({
|
|
708
|
+
psk: new Uint8Array(Buffer.from(data.psk, 'base64')),
|
|
709
|
+
cipher: data.cipher,
|
|
710
|
+
});
|
|
711
|
+
} catch(e) {
|
|
712
|
+
callback(null); // Decryption failed → full handshake
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// If duplex was passed in constructor, start reading
|
|
718
|
+
if (context.transport) {
|
|
719
|
+
bindTransport();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// === Duplex stream implementation ===
|
|
723
|
+
|
|
724
|
+
/** Duplex _write — called by stream.write() */
|
|
725
|
+
self._write = function(chunk, encoding, callback) {
|
|
726
|
+
if (context.destroyed) { callback(new Error('Socket destroyed')); return; }
|
|
727
|
+
let buf = toBuf(chunk);
|
|
728
|
+
if (!context.using_app_keys) {
|
|
729
|
+
context.appWriteQueue.push(buf);
|
|
730
|
+
} else {
|
|
731
|
+
writeAppData(buf);
|
|
732
|
+
}
|
|
733
|
+
callback();
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
/** Duplex _read — data is pushed when received, no pull needed */
|
|
737
|
+
self._read = function() {};
|
|
738
|
+
|
|
739
|
+
// === Node-compatible API ===
|
|
740
|
+
|
|
741
|
+
/** Attach a raw transport (TCP socket) after construction. */
|
|
742
|
+
self.setSocket = function(duplex2){
|
|
743
|
+
if (!duplex2 || typeof duplex2.write !== 'function') throw new Error('setSocket expects a Duplex-like stream');
|
|
744
|
+
context.transport = duplex2;
|
|
745
|
+
bindTransport();
|
|
746
|
+
// Flush any pending handshake messages (e.g. ClientHello queued before transport was ready)
|
|
747
|
+
while (context.pendingHandshake.length > 0) {
|
|
748
|
+
let msg = context.pendingHandshake.shift();
|
|
749
|
+
writeRecord(msg.type, Buffer.from(msg.data));
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
/** Send close_notify and gracefully close. */
|
|
754
|
+
self.end = (function(originalEnd) {
|
|
755
|
+
return function(data, encoding, callback) {
|
|
756
|
+
if (context.destroyed) return this;
|
|
757
|
+
session.close(); // sends close_notify alert
|
|
758
|
+
try { context.transport && context.transport.end && context.transport.end(); } catch(e){}
|
|
759
|
+
return originalEnd.call(this, data, encoding, callback);
|
|
760
|
+
};
|
|
761
|
+
})(self.end);
|
|
762
|
+
|
|
763
|
+
self.destroy = (function(originalDestroy) {
|
|
764
|
+
return function(err) {
|
|
765
|
+
if (context.destroyed) return this;
|
|
766
|
+
context.destroyed = true;
|
|
767
|
+
try { context.transport && context.transport.destroy && context.transport.destroy(); } catch(e){}
|
|
768
|
+
return originalDestroy.call(this, err);
|
|
769
|
+
};
|
|
770
|
+
})(self.destroy);
|
|
771
|
+
|
|
772
|
+
/** Access the underlying TLSSession (for QUIC/advanced consumers). */
|
|
773
|
+
self.getSession = function(){ return session; };
|
|
774
|
+
|
|
775
|
+
/** Whether this connection used PSK resumption (true after secureConnect). */
|
|
776
|
+
Object.defineProperty(self, 'isResumed', { get: function(){ return session.isResumed; } });
|
|
777
|
+
|
|
778
|
+
/** Returns negotiated protocol string: 'TLSv1.3', 'TLSv1.2', etc. */
|
|
779
|
+
self.getProtocol = function(){
|
|
780
|
+
let v = session.getVersion();
|
|
781
|
+
if (v === 0x0304) return 'TLSv1.3';
|
|
782
|
+
if (v === 0x0303) return 'TLSv1.2';
|
|
783
|
+
if (v === 0x0302) return 'TLSv1.1';
|
|
784
|
+
if (v === 0x0301) return 'TLSv1';
|
|
785
|
+
return null;
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
/** Returns cipher info: { name, standardName, version }. */
|
|
789
|
+
self.getCipher = function(){
|
|
790
|
+
let code = session.getCipher();
|
|
791
|
+
if (code == null) return null;
|
|
792
|
+
let info = TLS_CIPHER_SUITES[code];
|
|
793
|
+
if (!info) return { name: '0x' + code.toString(16), standardName: 'unknown', version: self.getProtocol() };
|
|
794
|
+
return {
|
|
795
|
+
name: info.name || info.cipher || '0x' + code.toString(16),
|
|
796
|
+
standardName: info.standardName || info.cipher || 'unknown',
|
|
797
|
+
version: self.getProtocol()
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
/** Returns negotiated ALPN protocol string, or false. */
|
|
802
|
+
Object.defineProperty(self, 'alpnProtocol', {
|
|
803
|
+
get: function(){ return session.getALPN() || false; },
|
|
804
|
+
enumerable: true
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
/** Whether the peer certificate was validated. */
|
|
808
|
+
Object.defineProperty(self, 'authorized', {
|
|
809
|
+
get: function(){ return session.authorized; },
|
|
810
|
+
enumerable: true
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
/** Authorization error string, or null. */
|
|
814
|
+
Object.defineProperty(self, 'authorizationError', {
|
|
815
|
+
get: function(){ return session.authorizationError; },
|
|
816
|
+
enumerable: true
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
/** Whether TLS is established. */
|
|
820
|
+
Object.defineProperty(self, 'encrypted', {
|
|
821
|
+
get: function(){ return context.secureEstablished; },
|
|
822
|
+
enumerable: true
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
self.getPeerCertificate = function(){
|
|
826
|
+
let chain = session.getPeerCertificate();
|
|
827
|
+
if (!chain || chain.length === 0) return null;
|
|
828
|
+
try {
|
|
829
|
+
let certDer = chain[0].cert;
|
|
830
|
+
let x509 = new crypto.X509Certificate(certDer);
|
|
831
|
+
return {
|
|
832
|
+
subject: x509.subject,
|
|
833
|
+
issuer: x509.issuer,
|
|
834
|
+
subjectaltname: x509.subjectAltName,
|
|
835
|
+
valid_from: x509.validFrom,
|
|
836
|
+
valid_to: x509.validTo,
|
|
837
|
+
fingerprint: x509.fingerprint,
|
|
838
|
+
fingerprint256: x509.fingerprint256,
|
|
839
|
+
serialNumber: x509.serialNumber,
|
|
840
|
+
raw: certDer
|
|
841
|
+
};
|
|
842
|
+
} catch(e) {
|
|
843
|
+
return { raw: chain[0].cert };
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// === LemonTLS-only extensions (not in Node.js tls) ===
|
|
848
|
+
|
|
849
|
+
/** Handshake duration in ms, or null if not completed. */
|
|
850
|
+
Object.defineProperty(self, 'handshakeDuration', {
|
|
851
|
+
get: function(){ return session.handshakeDuration; },
|
|
852
|
+
enumerable: true
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
/** JA3 fingerprint from ClientHello (server-side only). Returns { hash, raw } or null. */
|
|
856
|
+
self.getJA3 = function(){ return session.getJA3 ? session.getJA3() : null; };
|
|
857
|
+
|
|
858
|
+
/** ECDHE shared secret (Buffer), or null. For research/advanced use. */
|
|
859
|
+
self.getSharedSecret = function(){ return session.getSharedSecret ? session.getSharedSecret() : null; };
|
|
860
|
+
|
|
861
|
+
/** Full negotiation result — all selected parameters in one object. */
|
|
862
|
+
self.getNegotiationResult = function(){ return session.getNegotiationResult ? session.getNegotiationResult() : null; };
|
|
863
|
+
|
|
864
|
+
/** Request Key Update — refresh outgoing encryption keys (TLS 1.3 only). */
|
|
865
|
+
self.rekeySend = function(){ if (session.requestKeyUpdate) session.requestKeyUpdate(false); };
|
|
866
|
+
|
|
867
|
+
/** Request Key Update for both directions (TLS 1.3 only). */
|
|
868
|
+
self.rekeyBoth = function(){ if (session.requestKeyUpdate) session.requestKeyUpdate(true); };
|
|
869
|
+
|
|
870
|
+
return self;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Inherit from Duplex
|
|
874
|
+
Object.setPrototypeOf(TLSSocket.prototype, Duplex.prototype);
|
|
875
|
+
Object.setPrototypeOf(TLSSocket, Duplex);
|
|
876
|
+
|
|
877
|
+
export default TLSSocket;
|