lemon-tls 0.2.1 → 0.2.2
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/index.js +21 -6
- package/package.json +3 -10
- package/src/crypto.js +12 -1
- package/src/dtls_session.js +865 -0
- package/src/dtls_socket.js +263 -0
- package/src/record.js +486 -4
- package/src/session/message.js +6 -3
- package/src/tls_session.js +166 -57
- package/src/wire.js +142 -11
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dtls_session.js — DTLS 1.2 + 1.3 session (client + server).
|
|
3
|
+
*
|
|
4
|
+
* Wraps TLSSession (composition) and adds:
|
|
5
|
+
* - DTLS record layer (plaintext + encrypted via record.js)
|
|
6
|
+
* - Reconstruction headers (add on output, strip on input)
|
|
7
|
+
* - Fragmentation and reassembly
|
|
8
|
+
* - Flight-based retransmission
|
|
9
|
+
* - HelloVerifyRequest (DTLS 1.2)
|
|
10
|
+
* - ACK records (DTLS 1.3)
|
|
11
|
+
* - Epoch + key management
|
|
12
|
+
* - Version mapping (TLSSession works internally, DTLS on the wire)
|
|
13
|
+
*
|
|
14
|
+
* Events emitted:
|
|
15
|
+
* 'packet' (Uint8Array) — DTLS datagram ready to send via UDP
|
|
16
|
+
* 'connect' () — handshake complete
|
|
17
|
+
* 'data' (Uint8Array) — decrypted application data received
|
|
18
|
+
* 'error' (Error)
|
|
19
|
+
* 'close' ()
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* let dtls = new DTLSSession({ isServer: false, servername: 'example.com' });
|
|
23
|
+
* dtls.on('packet', (data) => udpSocket.send(data, remotePort, remoteAddr));
|
|
24
|
+
* dtls.on('connect', () => dtls.send(new TextEncoder().encode('hello')));
|
|
25
|
+
* dtls.on('data', (data) => console.log('received:', data));
|
|
26
|
+
* dtls.feedDatagram(incomingUdpPayload);
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { EventEmitter } from 'node:events';
|
|
30
|
+
|
|
31
|
+
import TLSSession from './tls_session.js';
|
|
32
|
+
import createSecureContext from './secure_context.js';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
DTLS_VERSION,
|
|
36
|
+
build_dtls_handshake,
|
|
37
|
+
parse_dtls_handshake,
|
|
38
|
+
build_message,
|
|
39
|
+
build_hello_verify_request,
|
|
40
|
+
parse_hello_verify_request,
|
|
41
|
+
} from './wire.js';
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
TLS_CIPHER_SUITES,
|
|
45
|
+
hkdf_expand_label,
|
|
46
|
+
derive_sn_key,
|
|
47
|
+
} from './crypto.js';
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
CT,
|
|
51
|
+
getAeadAlgo,
|
|
52
|
+
deriveKeys12,
|
|
53
|
+
decryptDtls12,
|
|
54
|
+
buildDtlsPlaintext,
|
|
55
|
+
buildEncryptedDtls12,
|
|
56
|
+
buildEncryptedDtls13,
|
|
57
|
+
parseDtlsDatagram,
|
|
58
|
+
buildDtlsAck,
|
|
59
|
+
parseDtlsAck,
|
|
60
|
+
} from './record.js';
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Helpers
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// DTLSSession
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Insert a DTLS cookie into a ClientHello TLS message.
|
|
73
|
+
* CH1 format: type(1) + length(3) + version(2) + random(32) + sid_len(1) + sid + cookie_len(1) + cookie(0) + ...
|
|
74
|
+
* Returns new Uint8Array with cookie inserted.
|
|
75
|
+
*/
|
|
76
|
+
function insertCookieIntoClientHello(ch1, cookie) {
|
|
77
|
+
let off = 4; // skip type(1)+length(3)
|
|
78
|
+
off += 2; // version
|
|
79
|
+
off += 32; // random
|
|
80
|
+
let sidLen = ch1[off];
|
|
81
|
+
off += 1 + sidLen; // session_id
|
|
82
|
+
|
|
83
|
+
// Now at cookie_len position
|
|
84
|
+
let oldCookieLen = ch1[off];
|
|
85
|
+
let cookiePos = off;
|
|
86
|
+
|
|
87
|
+
// Build CH2: [before cookie_len] + [new cookie_len + cookie] + [after old cookie]
|
|
88
|
+
let before = ch1.subarray(0, cookiePos);
|
|
89
|
+
let after = ch1.subarray(cookiePos + 1 + oldCookieLen);
|
|
90
|
+
|
|
91
|
+
let ch2 = new Uint8Array(before.length + 1 + cookie.length + after.length);
|
|
92
|
+
ch2.set(before, 0);
|
|
93
|
+
ch2[before.length] = cookie.length;
|
|
94
|
+
ch2.set(cookie, before.length + 1);
|
|
95
|
+
ch2.set(after, before.length + 1 + cookie.length);
|
|
96
|
+
|
|
97
|
+
// Update handshake length field (bytes 1-3)
|
|
98
|
+
let newBodyLen = ch2.length - 4;
|
|
99
|
+
ch2[1] = (newBodyLen >> 16) & 0xff;
|
|
100
|
+
ch2[2] = (newBodyLen >> 8) & 0xff;
|
|
101
|
+
ch2[3] = newBodyLen & 0xff;
|
|
102
|
+
|
|
103
|
+
return ch2;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function DTLSSession(options) {
|
|
107
|
+
if (!(this instanceof DTLSSession)) return new DTLSSession(options);
|
|
108
|
+
options = options || {};
|
|
109
|
+
|
|
110
|
+
let ev = new EventEmitter();
|
|
111
|
+
let isServer = !!options.isServer;
|
|
112
|
+
|
|
113
|
+
// ---- Internal TLSSession ----
|
|
114
|
+
let tls = new TLSSession({
|
|
115
|
+
isServer: isServer,
|
|
116
|
+
servername: options.servername,
|
|
117
|
+
SNICallback: options.SNICallback,
|
|
118
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
119
|
+
ca: options.ca,
|
|
120
|
+
noTickets: options.noTickets,
|
|
121
|
+
requestCert: options.requestCert,
|
|
122
|
+
cert: options.cert,
|
|
123
|
+
key: options.key,
|
|
124
|
+
ticketKeys: options.ticketKeys,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---- DTLS context ----
|
|
128
|
+
let ctx = {
|
|
129
|
+
state: 'idle', // idle → handshaking → connected → closed
|
|
130
|
+
isServer: isServer,
|
|
131
|
+
mtu: options.mtu || 1200,
|
|
132
|
+
|
|
133
|
+
// Version (determined after negotiation)
|
|
134
|
+
selectedVersion: null, // DTLS version (0xFEFC or 0xFEFD)
|
|
135
|
+
|
|
136
|
+
// Keys by DTLS epoch
|
|
137
|
+
// epoch 0: cleartext
|
|
138
|
+
// DTLS 1.2: epoch 1 = after CCS
|
|
139
|
+
// DTLS 1.3: epoch 2 = handshake, epoch 3 = application
|
|
140
|
+
keys: {},
|
|
141
|
+
|
|
142
|
+
// Record sequence numbers (per epoch, for sending)
|
|
143
|
+
writeSeq: {},
|
|
144
|
+
|
|
145
|
+
// Cipher info (set when handshake secrets arrive)
|
|
146
|
+
cipherSuite: null,
|
|
147
|
+
hashName: null,
|
|
148
|
+
|
|
149
|
+
// Fragment reassembly: msg_seq → { totalLength, parts: { offset: Uint8Array } }
|
|
150
|
+
fragments: {},
|
|
151
|
+
|
|
152
|
+
// Incoming handshake msg_seq tracking
|
|
153
|
+
nextReadMsgSeq: 0,
|
|
154
|
+
|
|
155
|
+
// Flight tracking for retransmission
|
|
156
|
+
currentFlight: [], // array of { epoch, data: Uint8Array (complete datagram) }
|
|
157
|
+
retransmitTimer: null,
|
|
158
|
+
retransmitCount: 0,
|
|
159
|
+
retransmitTimeout: 1000,
|
|
160
|
+
maxRetransmits: 6,
|
|
161
|
+
|
|
162
|
+
// DTLS 1.2: CCS tracking
|
|
163
|
+
localCcsSent: false,
|
|
164
|
+
remoteCcsSeen: false,
|
|
165
|
+
|
|
166
|
+
// HelloVerifyRequest (DTLS 1.2 server)
|
|
167
|
+
hvrCookie: null, // cookie sent in HVR
|
|
168
|
+
hvrDone: false, // true after HVR exchange complete
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// ============================================================
|
|
173
|
+
// Setup TLSSession for DTLS
|
|
174
|
+
// ============================================================
|
|
175
|
+
|
|
176
|
+
// Set DTLS versions and cookie (triggers DTLS format in build_hello)
|
|
177
|
+
let versions = [];
|
|
178
|
+
let minVer = options.minVersion || 'DTLSv1.2';
|
|
179
|
+
let maxVer = options.maxVersion || 'DTLSv1.3';
|
|
180
|
+
if (maxVer === 'DTLSv1.3' || maxVer === 'TLSv1.3') versions.push(DTLS_VERSION.DTLS1_3);
|
|
181
|
+
if (minVer === 'DTLSv1.2' || minVer === 'TLSv1.2' || maxVer === 'DTLSv1.2') versions.push(DTLS_VERSION.DTLS1_2);
|
|
182
|
+
if (versions.length === 0) versions = [DTLS_VERSION.DTLS1_3, DTLS_VERSION.DTLS1_2];
|
|
183
|
+
|
|
184
|
+
let tlsSetup = {
|
|
185
|
+
local_supported_versions: versions,
|
|
186
|
+
dtls_cookie: new Uint8Array(0), // empty cookie — triggers DTLS format
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Pass through cipher/group/alpn preferences
|
|
190
|
+
if (options.cipherSuites) tlsSetup.local_supported_cipher_suites = options.cipherSuites;
|
|
191
|
+
|
|
192
|
+
// Client cipher defaults: TLSSession auto-populates both TLS 1.3+1.2 ciphers,
|
|
193
|
+
// but if we only support DTLS 1.2, we must not offer TLS 1.3 ciphers
|
|
194
|
+
// (the server might erroneously select one for a 1.2 connection).
|
|
195
|
+
if (!isServer && !options.cipherSuites && versions.indexOf(DTLS_VERSION.DTLS1_3) < 0) {
|
|
196
|
+
tlsSetup.local_supported_cipher_suites = [0xC02F, 0xC030, 0xC02B, 0xC02C, 0xCCA8];
|
|
197
|
+
}
|
|
198
|
+
if (options.groups) tlsSetup.local_supported_groups = options.groups;
|
|
199
|
+
if (options.alpnProtocols) tlsSetup.local_supported_alpns = options.alpnProtocols;
|
|
200
|
+
|
|
201
|
+
// Server defaults — TLSSession client auto-populates these, but server expects them set externally
|
|
202
|
+
if (isServer) {
|
|
203
|
+
if (!tlsSetup.local_supported_cipher_suites) {
|
|
204
|
+
let ciphers = [];
|
|
205
|
+
if (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0) {
|
|
206
|
+
ciphers.push(0x1301, 0x1302, 0x1303); // TLS 1.3 AEAD ciphers
|
|
207
|
+
}
|
|
208
|
+
if (versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0) {
|
|
209
|
+
ciphers.push(0xC02F, 0xC030, 0xC02B, 0xC02C); // TLS 1.2 ECDHE ciphers
|
|
210
|
+
}
|
|
211
|
+
tlsSetup.local_supported_cipher_suites = ciphers;
|
|
212
|
+
}
|
|
213
|
+
if (!tlsSetup.local_supported_groups) {
|
|
214
|
+
tlsSetup.local_supported_groups = [0x001d, 0x0017, 0x0018];
|
|
215
|
+
}
|
|
216
|
+
tlsSetup.local_supported_signature_algorithms = [
|
|
217
|
+
0x0804, 0x0805, 0x0806, // RSA-PSS
|
|
218
|
+
0x0403, 0x0503, 0x0603, // ECDSA
|
|
219
|
+
0x0807, 0x0808, // EdDSA
|
|
220
|
+
0x0401, 0x0501, 0x0601, // PKCS#1
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Server cert/key
|
|
225
|
+
if (options.cert && options.key) {
|
|
226
|
+
let sctx = createSecureContext({ key: options.key, cert: options.cert });
|
|
227
|
+
tlsSetup.local_cert_chain = sctx.certificateChain;
|
|
228
|
+
tlsSetup.cert_private_key = sctx.privateKey;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
tls.set_context(tlsSetup);
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
// ============================================================
|
|
235
|
+
// DTLS 1.2 transcript hook (RFC 6347 §4.2.6)
|
|
236
|
+
//
|
|
237
|
+
// DTLS 1.2 requires the handshake hash to include DTLS-specific
|
|
238
|
+
// reconstruction data (msg_seq + frag_offset + frag_length).
|
|
239
|
+
// We hook into TLSSession's transcript to transparently convert
|
|
240
|
+
// TLS-format entries to DTLS-format when the version is DTLS 1.2.
|
|
241
|
+
//
|
|
242
|
+
// For DTLS 1.3 and TLS, the transcript uses standard TLS format.
|
|
243
|
+
// ============================================================
|
|
244
|
+
|
|
245
|
+
let transcriptMsgSeqs = []; // parallel array: msg_seq for each transcript entry
|
|
246
|
+
|
|
247
|
+
tls.context.transcriptHook = function(data) {
|
|
248
|
+
// Determine msg_seq: incoming uses _incomingMsgSeq (set by deliverHandshakeMessage),
|
|
249
|
+
// outgoing uses message_sent_seq (TLSSession's counter).
|
|
250
|
+
let msgSeq;
|
|
251
|
+
if (ctx._incomingMsgSeq !== undefined) {
|
|
252
|
+
msgSeq = ctx._incomingMsgSeq;
|
|
253
|
+
ctx._incomingMsgSeq = undefined; // one-shot: only applies to the first push
|
|
254
|
+
} else {
|
|
255
|
+
msgSeq = tls.context.message_sent_seq;
|
|
256
|
+
}
|
|
257
|
+
transcriptMsgSeqs.push(msgSeq);
|
|
258
|
+
|
|
259
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
|
|
260
|
+
return build_dtls_handshake(data, msgSeq);
|
|
261
|
+
}
|
|
262
|
+
return data; // TLS format for DTLS 1.3 or unknown version
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Called when selectedVersion is first determined as DTLS 1.2.
|
|
267
|
+
* Retroactively converts all existing transcript entries to DTLS format.
|
|
268
|
+
*/
|
|
269
|
+
function fixTranscriptForDtls12() {
|
|
270
|
+
let t = tls.context.transcript;
|
|
271
|
+
for (let i = 0; i < t.length; i++) {
|
|
272
|
+
if (i < transcriptMsgSeqs.length) {
|
|
273
|
+
t[i] = build_dtls_handshake(t[i], transcriptMsgSeqs[i]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
// ============================================================
|
|
280
|
+
// Key derivation
|
|
281
|
+
// ============================================================
|
|
282
|
+
|
|
283
|
+
function deriveEpochKeys(secret) {
|
|
284
|
+
let cs = TLS_CIPHER_SUITES[ctx.cipherSuite];
|
|
285
|
+
let empty = new Uint8Array(0);
|
|
286
|
+
let key = hkdf_expand_label(cs.hash, secret, 'key', empty, cs.keylen);
|
|
287
|
+
let iv = hkdf_expand_label(cs.hash, secret, 'iv', empty, 12);
|
|
288
|
+
|
|
289
|
+
let result = { key, iv };
|
|
290
|
+
|
|
291
|
+
// DTLS 1.3: derive sn_key for record number encryption
|
|
292
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
293
|
+
result.snKey = derive_sn_key(cs.hash, secret, ctx.cipherSuite);
|
|
294
|
+
result.algo = getAeadAlgo(ctx.cipherSuite);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
tls.on('handshakeSecrets', function(localSecret, remoteSecret) {
|
|
301
|
+
// Ensure version is detected before key derivation
|
|
302
|
+
if (ctx.selectedVersion === null) ctx.selectedVersion = tls.context.selected_version;
|
|
303
|
+
ctx.cipherSuite = tls.getCipher();
|
|
304
|
+
ctx.hashName = TLS_CIPHER_SUITES[ctx.cipherSuite].hash;
|
|
305
|
+
|
|
306
|
+
let dtlsEpoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 2 : 1;
|
|
307
|
+
|
|
308
|
+
ctx.keys[dtlsEpoch] = {
|
|
309
|
+
write: deriveEpochKeys(localSecret),
|
|
310
|
+
read: deriveEpochKeys(remoteSecret),
|
|
311
|
+
};
|
|
312
|
+
ctx.writeSeq[dtlsEpoch] = 0;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
tls.on('appSecrets', function(localSecret, remoteSecret) {
|
|
316
|
+
let dtlsEpoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1;
|
|
317
|
+
|
|
318
|
+
ctx.keys[dtlsEpoch] = {
|
|
319
|
+
write: deriveEpochKeys(localSecret),
|
|
320
|
+
read: deriveEpochKeys(remoteSecret),
|
|
321
|
+
};
|
|
322
|
+
ctx.writeSeq[dtlsEpoch] = 0;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
// ============================================================
|
|
327
|
+
// Outgoing: TLSSession message → DTLS datagram
|
|
328
|
+
// ============================================================
|
|
329
|
+
|
|
330
|
+
tls.on('message', function(tlsEpoch, seq, type, data) {
|
|
331
|
+
// Ensure version is detected
|
|
332
|
+
if (ctx.selectedVersion === null && tls.context.selected_version) {
|
|
333
|
+
ctx.selectedVersion = tls.context.selected_version;
|
|
334
|
+
// If just determined as DTLS 1.2, retroactively fix transcript entries
|
|
335
|
+
// that were pushed before the version was known.
|
|
336
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
|
|
337
|
+
fixTranscriptForDtls12();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Save client's CH1 for potential HVR retry
|
|
342
|
+
if (type === 'hello' && !isServer) {
|
|
343
|
+
ctx.savedClientHello = data;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Map TLS epoch → DTLS epoch
|
|
347
|
+
let dtlsEpoch;
|
|
348
|
+
if (tlsEpoch === 0) {
|
|
349
|
+
dtlsEpoch = 0;
|
|
350
|
+
} else if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
351
|
+
dtlsEpoch = tlsEpoch === 1 ? 2 : 3; // epoch 2=handshake, 3=app
|
|
352
|
+
} else {
|
|
353
|
+
dtlsEpoch = 1; // DTLS 1.2: epoch 1 for all encrypted
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (type === 'alert') {
|
|
357
|
+
sendAlertRecord(dtlsEpoch, data);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// DTLS 1.2: derive keys before first encrypted message
|
|
362
|
+
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && tlsEpoch === 1 && !ctx.keys[1]) {
|
|
363
|
+
let ts = tls.getTrafficSecrets();
|
|
364
|
+
if (ts.masterSecret) {
|
|
365
|
+
let d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, tls.getCipher(), isServer);
|
|
366
|
+
ctx.keys[1] = {
|
|
367
|
+
write: { key: d12.writeKey, iv: d12.writeIv },
|
|
368
|
+
read: { key: d12.readKey, iv: d12.readIv },
|
|
369
|
+
};
|
|
370
|
+
ctx.writeSeq[1] = 0;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// DTLS 1.2: send CCS before first encrypted message (Finished)
|
|
375
|
+
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && tlsEpoch === 1 && !ctx.localCcsSent) {
|
|
376
|
+
sendCCS();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Convert TLS handshake message → DTLS handshake message (add reconstruction header)
|
|
380
|
+
let dtlsMsg = build_dtls_handshake(data, seq);
|
|
381
|
+
|
|
382
|
+
// Fragment if needed
|
|
383
|
+
let frags = fragmentMessage(dtlsMsg, data, seq);
|
|
384
|
+
|
|
385
|
+
// Build DTLS records and emit
|
|
386
|
+
for (let i = 0; i < frags.length; i++) {
|
|
387
|
+
let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
|
|
388
|
+
ctx.currentFlight.push(record);
|
|
389
|
+
ev.emit('packet', record);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Start retransmit timer after sending flight messages
|
|
393
|
+
if (ctx.state === 'handshaking') {
|
|
394
|
+
startRetransmitTimer();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Build a DTLS record (plaintext or encrypted depending on epoch and key availability).
|
|
400
|
+
*/
|
|
401
|
+
function buildRecord(epoch, contentType, payload) {
|
|
402
|
+
let epochKeys = ctx.keys[epoch];
|
|
403
|
+
|
|
404
|
+
if (!epochKeys) {
|
|
405
|
+
// No keys → plaintext record
|
|
406
|
+
return buildDtlsPlaintext(contentType, epoch, nextWriteSeq(epoch), payload);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
410
|
+
// DTLS 1.3 encrypted (unified header)
|
|
411
|
+
return buildEncryptedDtls13(contentType, payload, nextWriteSeq(epoch), epoch, epochKeys.write);
|
|
412
|
+
} else {
|
|
413
|
+
// DTLS 1.2 encrypted (classic header)
|
|
414
|
+
return buildEncryptedDtls12(contentType, epoch, nextWriteSeq(epoch), payload, epochKeys.write);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function nextWriteSeq(epoch) {
|
|
419
|
+
if (!(epoch in ctx.writeSeq)) ctx.writeSeq[epoch] = 0;
|
|
420
|
+
return ctx.writeSeq[epoch]++;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Fragment a DTLS handshake message if it exceeds MTU.
|
|
425
|
+
* Returns array of DTLS handshake message fragments (each with reconstruction header).
|
|
426
|
+
*/
|
|
427
|
+
function fragmentMessage(dtlsMsg, tlsMsg, msgSeq) {
|
|
428
|
+
let overhead = 13 + 16; // record header + AEAD tag (approximate)
|
|
429
|
+
let maxFragment = ctx.mtu - overhead;
|
|
430
|
+
|
|
431
|
+
if (dtlsMsg.length <= maxFragment) return [dtlsMsg];
|
|
432
|
+
|
|
433
|
+
// Need to fragment: split the TLS body into chunks
|
|
434
|
+
let body = tlsMsg.subarray(4); // skip TLS header (type+length)
|
|
435
|
+
let totalLength = body.length;
|
|
436
|
+
let fragments = [];
|
|
437
|
+
let offset = 0;
|
|
438
|
+
|
|
439
|
+
while (offset < totalLength) {
|
|
440
|
+
let chunkLen = Math.min(maxFragment - 12, totalLength - offset); // 12 = DTLS handshake header
|
|
441
|
+
let frag = build_dtls_handshake(tlsMsg, msgSeq, offset, chunkLen);
|
|
442
|
+
fragments.push(frag);
|
|
443
|
+
offset += chunkLen;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return fragments;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Send an alert record.
|
|
451
|
+
*/
|
|
452
|
+
function sendAlertRecord(epoch, alertData) {
|
|
453
|
+
let record = buildRecord(epoch, CT.ALERT, alertData);
|
|
454
|
+
ev.emit('packet', record);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Send CCS record (DTLS 1.2 only).
|
|
459
|
+
*/
|
|
460
|
+
function sendCCS() {
|
|
461
|
+
if (ctx.localCcsSent) return;
|
|
462
|
+
ctx.localCcsSent = true;
|
|
463
|
+
let record = buildDtlsPlaintext(CT.CHANGE_CIPHER_SPEC, 0, nextWriteSeq(0), new Uint8Array([1]));
|
|
464
|
+
ev.emit('packet', record);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
// ============================================================
|
|
469
|
+
// Incoming: UDP datagram → parse → feed TLSSession
|
|
470
|
+
// ============================================================
|
|
471
|
+
|
|
472
|
+
function feedDatagram(data) {
|
|
473
|
+
if (ctx.state === 'closed') return;
|
|
474
|
+
if (ctx.state === 'idle') ctx.state = 'handshaking';
|
|
475
|
+
|
|
476
|
+
// Build key lookup for decryption
|
|
477
|
+
let keysByEpoch = {};
|
|
478
|
+
for (let ep in ctx.keys) {
|
|
479
|
+
if (ctx.keys[ep] && ctx.keys[ep].read) {
|
|
480
|
+
keysByEpoch[Number(ep)] = ctx.keys[ep].read;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let records = parseDtlsDatagram(data, keysByEpoch);
|
|
485
|
+
|
|
486
|
+
for (let i = 0; i < records.length; i++) {
|
|
487
|
+
processRecord(records[i]);
|
|
488
|
+
|
|
489
|
+
// After CCS, keys are newly available — re-decrypt remaining epoch>0 records
|
|
490
|
+
if (records[i].type === CT.CHANGE_CIPHER_SPEC && ctx.keys[1]) {
|
|
491
|
+
let readKeys = ctx.keys[1].read;
|
|
492
|
+
for (let j = i + 1; j < records.length; j++) {
|
|
493
|
+
if (!records[j].encrypted && records[j].epoch > 0 && readKeys) {
|
|
494
|
+
try {
|
|
495
|
+
records[j].content = decryptDtls12(records[j].content, readKeys.key, readKeys.iv, records[j].epoch, records[j].seq, records[j].type);
|
|
496
|
+
records[j].encrypted = true;
|
|
497
|
+
} catch(e) {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function processRecord(record) {
|
|
506
|
+
if (record.type === CT.HANDSHAKE) {
|
|
507
|
+
processHandshakeRecord(record.content, record.epoch, record.encrypted);
|
|
508
|
+
} else if (record.type === CT.APPLICATION_DATA) {
|
|
509
|
+
if (ctx.state === 'connected') {
|
|
510
|
+
ev.emit('data', record.content);
|
|
511
|
+
}
|
|
512
|
+
} else if (record.type === CT.CHANGE_CIPHER_SPEC) {
|
|
513
|
+
// DTLS 1.2: peer sent CCS — derive keys if not yet done
|
|
514
|
+
ctx.remoteCcsSeen = true;
|
|
515
|
+
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && !ctx.keys[1]) {
|
|
516
|
+
let ts = tls.getTrafficSecrets();
|
|
517
|
+
if (ts.masterSecret) {
|
|
518
|
+
let d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, tls.getCipher(), isServer);
|
|
519
|
+
ctx.keys[1] = {
|
|
520
|
+
write: { key: d12.writeKey, iv: d12.writeIv },
|
|
521
|
+
read: { key: d12.readKey, iv: d12.readIv },
|
|
522
|
+
};
|
|
523
|
+
ctx.writeSeq[1] = 0;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} else if (record.type === CT.ACK) {
|
|
527
|
+
processAck(record.content);
|
|
528
|
+
} else if (record.type === CT.ALERT) {
|
|
529
|
+
let level = record.content[0];
|
|
530
|
+
let desc = record.content[1];
|
|
531
|
+
if (desc === 0) {
|
|
532
|
+
// close_notify
|
|
533
|
+
ctx.state = 'closed';
|
|
534
|
+
ev.emit('close');
|
|
535
|
+
} else {
|
|
536
|
+
ev.emit('error', new Error('DTLS alert: level=' + level + ' desc=' + desc));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Process a handshake record. Handles reassembly and feeds to TLSSession.
|
|
543
|
+
*/
|
|
544
|
+
function processHandshakeRecord(data, epoch, encrypted) {
|
|
545
|
+
// A handshake record may contain multiple handshake messages
|
|
546
|
+
let off = 0;
|
|
547
|
+
while (off + 12 <= data.length) {
|
|
548
|
+
let parsed = parse_dtls_handshake(data.subarray(off));
|
|
549
|
+
|
|
550
|
+
let msgSeq = parsed.msg_seq;
|
|
551
|
+
let totalLen = parsed.length;
|
|
552
|
+
let fragOffset = parsed.frag_offset;
|
|
553
|
+
let fragLen = parsed.frag_length;
|
|
554
|
+
|
|
555
|
+
// Is this a complete message or a fragment?
|
|
556
|
+
if (fragOffset === 0 && fragLen === totalLen) {
|
|
557
|
+
// Complete message — feed directly
|
|
558
|
+
deliverHandshakeMessage(parsed.type, parsed.body, msgSeq);
|
|
559
|
+
} else {
|
|
560
|
+
// Fragment — reassemble
|
|
561
|
+
reassembleFragment(parsed);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
off += 12 + fragLen;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Received handshake data → cancel retransmit (implicit ACK)
|
|
568
|
+
cancelRetransmit();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Reassemble a fragmented handshake message.
|
|
573
|
+
*/
|
|
574
|
+
function reassembleFragment(parsed) {
|
|
575
|
+
let key = parsed.msg_seq;
|
|
576
|
+
if (!(key in ctx.fragments)) {
|
|
577
|
+
ctx.fragments[key] = { totalLength: parsed.length, type: parsed.type, parts: {} };
|
|
578
|
+
}
|
|
579
|
+
let frag = ctx.fragments[key];
|
|
580
|
+
frag.parts[parsed.frag_offset] = parsed.body;
|
|
581
|
+
|
|
582
|
+
// Check if complete
|
|
583
|
+
let assembled = new Uint8Array(frag.totalLength);
|
|
584
|
+
let filled = 0;
|
|
585
|
+
let offsets = Object.keys(frag.parts).map(Number).sort((a, b) => a - b);
|
|
586
|
+
|
|
587
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
588
|
+
let part = frag.parts[offsets[i]];
|
|
589
|
+
if (offsets[i] !== filled) return; // gap — not complete yet
|
|
590
|
+
assembled.set(part, offsets[i]);
|
|
591
|
+
filled = offsets[i] + part.length;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (filled < frag.totalLength) return; // not complete
|
|
595
|
+
|
|
596
|
+
// Complete — deliver
|
|
597
|
+
delete ctx.fragments[key];
|
|
598
|
+
deliverHandshakeMessage(frag.type, assembled, key);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Deliver a complete handshake message to TLSSession.
|
|
603
|
+
* Strips DTLS reconstruction → builds TLS format.
|
|
604
|
+
*/
|
|
605
|
+
function deliverHandshakeMessage(type, body, msgSeq) {
|
|
606
|
+
// Skip if we've already processed this msg_seq
|
|
607
|
+
if (msgSeq < ctx.nextReadMsgSeq) return;
|
|
608
|
+
ctx.nextReadMsgSeq = msgSeq + 1;
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
// Check for HelloVerifyRequest (DTLS 1.2 server→client, type=3)
|
|
612
|
+
if (type === 3 && !isServer) {
|
|
613
|
+
let hvr = parse_hello_verify_request(body);
|
|
614
|
+
triggerClientHelloWithCookie(hvr.cookie);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Build TLS-format message: type(1) + length(3) + body
|
|
619
|
+
let tlsMsg = build_message(type, body);
|
|
620
|
+
|
|
621
|
+
// Set incoming msg_seq for the transcriptHook (one-shot — cleared after first push)
|
|
622
|
+
ctx._incomingMsgSeq = msgSeq;
|
|
623
|
+
|
|
624
|
+
// Feed to TLSSession (transcriptHook will convert to DTLS format if needed)
|
|
625
|
+
tls.message(tlsMsg);
|
|
626
|
+
|
|
627
|
+
// Update selectedVersion if just negotiated
|
|
628
|
+
if (ctx.selectedVersion === null && tls.context.selected_version) {
|
|
629
|
+
ctx.selectedVersion = tls.context.selected_version;
|
|
630
|
+
if (ctx.selectedVersion === DTLS_VERSION.DTLS1_2) {
|
|
631
|
+
fixTranscriptForDtls12();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Trigger a new ClientHello with cookie (DTLS 1.2 HVR response).
|
|
638
|
+
*/
|
|
639
|
+
function triggerClientHelloWithCookie(cookie) {
|
|
640
|
+
if (!ctx.savedClientHello) return;
|
|
641
|
+
|
|
642
|
+
// Build CH2 by inserting cookie into saved CH1
|
|
643
|
+
// RFC 6347: "the client MUST use the same parameter values"
|
|
644
|
+
let ch2 = insertCookieIntoClientHello(ctx.savedClientHello, cookie);
|
|
645
|
+
|
|
646
|
+
// Reset state
|
|
647
|
+
tls.context.transcript = [];
|
|
648
|
+
tls.context.hello_sent = true;
|
|
649
|
+
tls.context.dtls_cookie = cookie;
|
|
650
|
+
tls.context.message_sent_seq = 0;
|
|
651
|
+
ctx.nextReadMsgSeq = 0;
|
|
652
|
+
ctx.currentFlight = [];
|
|
653
|
+
transcriptMsgSeqs = [];
|
|
654
|
+
|
|
655
|
+
// Push CH2 to transcript (transcriptHook will store TLS format for now,
|
|
656
|
+
// and fixTranscriptForDtls12() will convert when version is determined)
|
|
657
|
+
ctx._incomingMsgSeq = undefined; // not incoming — use message_sent_seq
|
|
658
|
+
tls.context.transcript.push(
|
|
659
|
+
tls.context.transcriptHook ? tls.context.transcriptHook(ch2) : ch2
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Build DTLS message and send
|
|
663
|
+
let dtlsMsg = build_dtls_handshake(ch2, 0);
|
|
664
|
+
let record = buildRecord(0, CT.HANDSHAKE, dtlsMsg);
|
|
665
|
+
ctx.currentFlight = [record];
|
|
666
|
+
ev.emit('packet', record);
|
|
667
|
+
|
|
668
|
+
tls.context.message_sent_seq = 1;
|
|
669
|
+
startRetransmitTimer();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
// ============================================================
|
|
674
|
+
// Flight tracking + retransmission
|
|
675
|
+
// ============================================================
|
|
676
|
+
|
|
677
|
+
function startRetransmitTimer() {
|
|
678
|
+
cancelRetransmit();
|
|
679
|
+
ctx.retransmitTimer = setTimeout(function() {
|
|
680
|
+
if (ctx.retransmitCount >= ctx.maxRetransmits) {
|
|
681
|
+
ev.emit('error', new Error('DTLS handshake timeout — max retransmits exceeded'));
|
|
682
|
+
ctx.state = 'closed';
|
|
683
|
+
ev.emit('close');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Retransmit entire current flight
|
|
688
|
+
for (let i = 0; i < ctx.currentFlight.length; i++) {
|
|
689
|
+
ev.emit('packet', ctx.currentFlight[i]);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
ctx.retransmitCount++;
|
|
693
|
+
ctx.retransmitTimeout = Math.min(ctx.retransmitTimeout * 2, 60000);
|
|
694
|
+
startRetransmitTimer();
|
|
695
|
+
}, ctx.retransmitTimeout);
|
|
696
|
+
|
|
697
|
+
if (ctx.retransmitTimer.unref) ctx.retransmitTimer.unref();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function cancelRetransmit() {
|
|
701
|
+
if (ctx.retransmitTimer !== null) {
|
|
702
|
+
clearTimeout(ctx.retransmitTimer);
|
|
703
|
+
ctx.retransmitTimer = null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function startNewFlight() {
|
|
708
|
+
ctx.currentFlight = [];
|
|
709
|
+
ctx.retransmitCount = 0;
|
|
710
|
+
ctx.retransmitTimeout = 1000;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
// ============================================================
|
|
715
|
+
// ACK (DTLS 1.3)
|
|
716
|
+
// ============================================================
|
|
717
|
+
|
|
718
|
+
function processAck(content) {
|
|
719
|
+
let acks = parseDtlsAck(content);
|
|
720
|
+
// ACK received — flight was acknowledged
|
|
721
|
+
cancelRetransmit();
|
|
722
|
+
startNewFlight();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function sendAck(epoch, recordsToAck) {
|
|
726
|
+
if (ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) return;
|
|
727
|
+
let payload = buildDtlsAck(recordsToAck);
|
|
728
|
+
let record = buildRecord(epoch, CT.ACK, payload);
|
|
729
|
+
ev.emit('packet', record);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
// ============================================================
|
|
734
|
+
// Handshake completion
|
|
735
|
+
// ============================================================
|
|
736
|
+
|
|
737
|
+
tls.on('secureConnect', function() {
|
|
738
|
+
ctx.state = 'connected';
|
|
739
|
+
cancelRetransmit();
|
|
740
|
+
startNewFlight();
|
|
741
|
+
ev.emit('connect');
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
tls.on('error', function(e) {
|
|
745
|
+
ev.emit('error', e);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
tls.on('session', function(ticket) {
|
|
749
|
+
ev.emit('session', ticket);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
// ============================================================
|
|
754
|
+
// Application data
|
|
755
|
+
// ============================================================
|
|
756
|
+
|
|
757
|
+
function send(data) {
|
|
758
|
+
if (ctx.state !== 'connected') {
|
|
759
|
+
ev.emit('error', new Error('Cannot send before handshake complete'));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (typeof data === 'string') data = new TextEncoder().encode(data);
|
|
763
|
+
|
|
764
|
+
let epoch = ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1;
|
|
765
|
+
let record = buildRecord(epoch, CT.APPLICATION_DATA, data);
|
|
766
|
+
ev.emit('packet', record);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
// ============================================================
|
|
771
|
+
// Close
|
|
772
|
+
// ============================================================
|
|
773
|
+
|
|
774
|
+
function close() {
|
|
775
|
+
if (ctx.state === 'closed') return;
|
|
776
|
+
let epoch = ctx.state === 'connected' ? (ctx.selectedVersion === DTLS_VERSION.DTLS1_3 ? 3 : 1) : 0;
|
|
777
|
+
sendAlertRecord(epoch, new Uint8Array([1, 0])); // warning, close_notify
|
|
778
|
+
ctx.state = 'closed';
|
|
779
|
+
cancelRetransmit();
|
|
780
|
+
ev.emit('close');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
// ============================================================
|
|
785
|
+
// Server: HelloVerifyRequest (DTLS 1.2)
|
|
786
|
+
// ============================================================
|
|
787
|
+
|
|
788
|
+
tls.on('hello', function() {
|
|
789
|
+
// Version is detected lazily in handshakeSecrets and message handlers
|
|
790
|
+
|
|
791
|
+
// DTLS 1.2 server: optionally send HelloVerifyRequest
|
|
792
|
+
if (isServer && ctx.selectedVersion !== DTLS_VERSION.DTLS1_3 && !ctx.hvrDone && options.useCookies === true) {
|
|
793
|
+
ctx.hvrDone = true;
|
|
794
|
+
let cookie = new Uint8Array(32);
|
|
795
|
+
for (let i = 0; i < 32; i++) cookie[i] = Math.floor(Math.random() * 256);
|
|
796
|
+
ctx.hvrCookie = cookie;
|
|
797
|
+
|
|
798
|
+
let hvrBody = build_hello_verify_request({ cookie: cookie });
|
|
799
|
+
let hvrMsg = build_dtls_handshake(
|
|
800
|
+
build_message(3, hvrBody),
|
|
801
|
+
0
|
|
802
|
+
);
|
|
803
|
+
let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
|
|
804
|
+
ev.emit('packet', record);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
// ============================================================
|
|
810
|
+
// Public API
|
|
811
|
+
// ============================================================
|
|
812
|
+
|
|
813
|
+
let api = {
|
|
814
|
+
/** Feed an incoming UDP datagram. */
|
|
815
|
+
feedDatagram: feedDatagram,
|
|
816
|
+
|
|
817
|
+
/** Send application data (after connect). */
|
|
818
|
+
send: send,
|
|
819
|
+
|
|
820
|
+
/** Close the DTLS session. */
|
|
821
|
+
close: close,
|
|
822
|
+
|
|
823
|
+
/** Configure the session (passes through to TLSSession). */
|
|
824
|
+
set_context: function(opts) { tls.set_context(opts); },
|
|
825
|
+
|
|
826
|
+
/** Register event listener. */
|
|
827
|
+
on: function(name, fn) { ev.on(name, fn); },
|
|
828
|
+
off: function(name, fn) { ev.off(name, fn); },
|
|
829
|
+
|
|
830
|
+
/** Access to internal TLSSession (for advanced use). */
|
|
831
|
+
get tls() { return tls; },
|
|
832
|
+
|
|
833
|
+
/** Current DTLS state. */
|
|
834
|
+
get state() { return ctx.state; },
|
|
835
|
+
|
|
836
|
+
/** Selected DTLS version (0xFEFC or 0xFEFD). */
|
|
837
|
+
get version() { return ctx.selectedVersion; },
|
|
838
|
+
|
|
839
|
+
/** Whether handshake is complete. */
|
|
840
|
+
get connected() { return ctx.state === 'connected'; },
|
|
841
|
+
|
|
842
|
+
/** Full negotiation result. */
|
|
843
|
+
getNegotiationResult: function() { return tls.getNegotiationResult(); },
|
|
844
|
+
|
|
845
|
+
/** Negotiated ALPN. */
|
|
846
|
+
getALPN: function() { return tls.getALPN(); },
|
|
847
|
+
|
|
848
|
+
/** Peer certificate. */
|
|
849
|
+
getPeerCertificate: function() { return tls.getPeerCertificate(); },
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
for (let k in api) {
|
|
853
|
+
if (Object.prototype.hasOwnProperty.call(api, k)) {
|
|
854
|
+
if (typeof Object.getOwnPropertyDescriptor(api, k).get === 'function') {
|
|
855
|
+
Object.defineProperty(this, k, Object.getOwnPropertyDescriptor(api, k));
|
|
856
|
+
} else {
|
|
857
|
+
this[k] = api[k];
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return this;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export default DTLSSession;
|