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
package/src/record.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* record.js —
|
|
2
|
+
* record.js — Record-layer primitives for TLS 1.2, TLS 1.3, DTLS 1.2, and DTLS 1.3.
|
|
3
3
|
*
|
|
4
|
-
* Used by TLSSocket, DTLSSocket, and test harnesses.
|
|
4
|
+
* Used by TLSSocket, DTLSSession, DTLSSocket, and test harnesses.
|
|
5
5
|
* Handles AEAD encryption/decryption, nonce construction, key derivation,
|
|
6
|
-
* and
|
|
6
|
+
* raw record framing, and DTLS-specific record number encryption.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import crypto from 'node:crypto';
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
hkdf_expand_label,
|
|
13
13
|
tls_derive_from_master_secret_tls12
|
|
14
14
|
} from './crypto.js';
|
|
15
|
+
import {
|
|
16
|
+
w_u8,
|
|
17
|
+
w_u16,
|
|
18
|
+
w_u48,
|
|
19
|
+
} from './wire.js';
|
|
15
20
|
|
|
16
21
|
// ===================== AEAD algorithm resolution =====================
|
|
17
22
|
|
|
@@ -191,7 +196,7 @@ function deriveKeys12(masterSecret, localRandom, remoteRandom, cipherSuite, isSe
|
|
|
191
196
|
// ===================== Record framing =====================
|
|
192
197
|
|
|
193
198
|
/** Content type constants. */
|
|
194
|
-
const CT = { CHANGE_CIPHER_SPEC: 20, ALERT: 21, HANDSHAKE: 22, APPLICATION_DATA: 23 };
|
|
199
|
+
const CT = { CHANGE_CIPHER_SPEC: 20, ALERT: 21, HANDSHAKE: 22, APPLICATION_DATA: 23, ACK: 26 };
|
|
195
200
|
|
|
196
201
|
/** Write a raw TLS record to a writable stream. */
|
|
197
202
|
function writeRecord(transport, type, payload, version) {
|
|
@@ -205,6 +210,453 @@ function writeRecord(transport, type, payload, version) {
|
|
|
205
210
|
transport.write(rec);
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
// ===================== DTLS binary helpers =====================
|
|
214
|
+
// w_u8, w_u16, w_u48 imported from wire.js
|
|
215
|
+
// readU16, readU48 return value only (no offset tracking), unlike wire.js r_u16 which returns [value, offset]
|
|
216
|
+
|
|
217
|
+
function readU16(buf, off) { return ((buf[off] << 8) | buf[off+1]) >>> 0; }
|
|
218
|
+
function readU48(buf, off) {
|
|
219
|
+
let hi = ((buf[off] << 8) | buf[off+1]) >>> 0;
|
|
220
|
+
let lo = ((buf[off+2] << 24) | (buf[off+3] << 16) | (buf[off+4] << 8) | buf[off+5]) >>> 0;
|
|
221
|
+
return hi * 0x100000000 + lo;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** AES-ECB encrypt a single 16-byte block (for DTLS 1.3 record number encryption). */
|
|
225
|
+
function aesEcbEncrypt(key, block) {
|
|
226
|
+
let algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
|
|
227
|
+
let cipher = crypto.createCipheriv(algo, key, null);
|
|
228
|
+
cipher.setAutoPadding(false);
|
|
229
|
+
let out = cipher.update(block);
|
|
230
|
+
cipher.final();
|
|
231
|
+
return new Uint8Array(out);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// ===================== DTLS plaintext record (13-byte header) =====================
|
|
236
|
+
//
|
|
237
|
+
// Used for: DTLS 1.2 all records, DTLS 1.3 epoch 0 (cleartext handshake).
|
|
238
|
+
//
|
|
239
|
+
// struct {
|
|
240
|
+
// ContentType type; // 1 byte
|
|
241
|
+
// ProtocolVersion version; // 2 bytes (0xFEFD)
|
|
242
|
+
// uint16 epoch; // 2 bytes
|
|
243
|
+
// uint48 sequence_number; // 6 bytes
|
|
244
|
+
// uint16 length; // 2 bytes
|
|
245
|
+
// opaque fragment[length];
|
|
246
|
+
// } DTLSPlaintext;
|
|
247
|
+
|
|
248
|
+
/** Build a plaintext DTLS record (classic 13-byte header). */
|
|
249
|
+
function buildDtlsPlaintext(type, epoch, seq, payload) {
|
|
250
|
+
let out = new Uint8Array(13 + payload.length);
|
|
251
|
+
let off = 0;
|
|
252
|
+
off = w_u8(out, off, type);
|
|
253
|
+
off = w_u16(out, off, 0xFEFD);
|
|
254
|
+
off = w_u16(out, off, epoch);
|
|
255
|
+
off = w_u48(out, off, seq);
|
|
256
|
+
off = w_u16(out, off, payload.length);
|
|
257
|
+
out.set(payload, off);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Parse plaintext DTLS records from a datagram. Returns array of { type, version, epoch, seq, payload, total_length }. */
|
|
262
|
+
function parseDtlsPlaintext(data) {
|
|
263
|
+
let records = [];
|
|
264
|
+
let off = 0;
|
|
265
|
+
while (off + 13 <= data.length) {
|
|
266
|
+
let type = data[off];
|
|
267
|
+
let version = readU16(data, off + 1);
|
|
268
|
+
let epoch = readU16(data, off + 3);
|
|
269
|
+
let seq = readU48(data, off + 5);
|
|
270
|
+
let length = readU16(data, off + 11);
|
|
271
|
+
if (off + 13 + length > data.length) break;
|
|
272
|
+
let payload = data.slice(off + 13, off + 13 + length);
|
|
273
|
+
records.push({ type, version, epoch, seq, payload, total_length: 13 + length });
|
|
274
|
+
off += 13 + length;
|
|
275
|
+
}
|
|
276
|
+
return records;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
// ===================== DTLS 1.2 encrypted records =====================
|
|
281
|
+
|
|
282
|
+
/** DTLS 1.2 AAD: epoch(2) + seq(6) + type(1) + version(2) + plaintext_length(2). */
|
|
283
|
+
function buildDtlsAad12(epoch, seq, type, plaintextLen) {
|
|
284
|
+
let aad = new Uint8Array(13);
|
|
285
|
+
let off = 0;
|
|
286
|
+
off = w_u16(aad, off, epoch);
|
|
287
|
+
off = w_u48(aad, off, seq);
|
|
288
|
+
off = w_u8(aad, off, type);
|
|
289
|
+
off = w_u16(aad, off, 0xFEFD);
|
|
290
|
+
off = w_u16(aad, off, plaintextLen);
|
|
291
|
+
return aad;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Encrypt DTLS 1.2 record payload. Returns: explicit_nonce(8) || ciphertext || tag(16). */
|
|
295
|
+
function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
|
|
296
|
+
let explicit = seqToBytes(seq);
|
|
297
|
+
let nonce = getNonce12(ivSalt, explicit);
|
|
298
|
+
let aad = buildDtlsAad12(epoch, seq, type, pt.length);
|
|
299
|
+
let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
300
|
+
let cipher = crypto.createCipheriv(algo, key, nonce);
|
|
301
|
+
cipher.setAAD(aad);
|
|
302
|
+
let ct = cipher.update(pt);
|
|
303
|
+
cipher.final();
|
|
304
|
+
let tag = cipher.getAuthTag();
|
|
305
|
+
let out = new Uint8Array(8 + ct.length + tag.length);
|
|
306
|
+
out.set(explicit, 0);
|
|
307
|
+
out.set(new Uint8Array(ct), 8);
|
|
308
|
+
out.set(new Uint8Array(tag), 8 + ct.length);
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Decrypt DTLS 1.2 record fragment. Input: explicit_nonce(8) || ciphertext || tag(16). */
|
|
313
|
+
function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
|
|
314
|
+
if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
|
|
315
|
+
let explicit = fragment.slice(0, 8);
|
|
316
|
+
let tag = fragment.slice(fragment.length - 16);
|
|
317
|
+
let ct = fragment.slice(8, fragment.length - 16);
|
|
318
|
+
let nonce = getNonce12(ivSalt, explicit);
|
|
319
|
+
let aad = buildDtlsAad12(epoch, seq, type, ct.length);
|
|
320
|
+
let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
321
|
+
let decipher = crypto.createDecipheriv(algo, key, nonce);
|
|
322
|
+
decipher.setAAD(aad);
|
|
323
|
+
decipher.setAuthTag(tag);
|
|
324
|
+
let pt = decipher.update(ct);
|
|
325
|
+
decipher.final();
|
|
326
|
+
return new Uint8Array(pt);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
|
|
330
|
+
function buildEncryptedDtls12(type, epoch, seq, plaintext, keys) {
|
|
331
|
+
let encrypted = encryptDtls12(plaintext, keys.key, keys.iv, epoch, seq, type);
|
|
332
|
+
return buildDtlsPlaintext(type, epoch, seq, encrypted);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
// ===================== DTLS 1.3 unified header =====================
|
|
337
|
+
//
|
|
338
|
+
// struct {
|
|
339
|
+
// uint8 header_info;
|
|
340
|
+
// bits 7-5: 001 (fixed)
|
|
341
|
+
// bit 4: connection_id present
|
|
342
|
+
// bit 3: sequence_number_length (0=1byte, 1=2bytes)
|
|
343
|
+
// bit 2: length_present
|
|
344
|
+
// bits 1-0: epoch (low 2 bits)
|
|
345
|
+
// [ConnectionID cid;]
|
|
346
|
+
// uint8/uint16 record_number; // 1 or 2 bytes
|
|
347
|
+
// [uint16 length;]
|
|
348
|
+
// opaque encrypted_record[];
|
|
349
|
+
// } DTLSCiphertext;
|
|
350
|
+
|
|
351
|
+
const UNIFIED_HDR_FIXED = 0x20; // 001xxxxx
|
|
352
|
+
|
|
353
|
+
/** Build unified header info byte. */
|
|
354
|
+
function buildUnifiedHdr(epoch, seqLen2, hasLength, hasCid) {
|
|
355
|
+
let b = UNIFIED_HDR_FIXED;
|
|
356
|
+
if (hasCid) b |= 0x10;
|
|
357
|
+
if (seqLen2) b |= 0x08;
|
|
358
|
+
if (hasLength) b |= 0x04;
|
|
359
|
+
b |= (epoch & 0x03);
|
|
360
|
+
return b;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Parse unified header info byte. */
|
|
364
|
+
function parseUnifiedHdr(b) {
|
|
365
|
+
return {
|
|
366
|
+
hasCid: !!(b & 0x10),
|
|
367
|
+
seqLen2: !!(b & 0x08),
|
|
368
|
+
hasLength: !!(b & 0x04),
|
|
369
|
+
epoch: b & 0x03,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Check if a byte is a DTLS 1.3 unified header (0x20..0x3F). */
|
|
374
|
+
function isUnifiedHdr(b) { return (b & 0xE0) === UNIFIED_HDR_FIXED; }
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
// ===================== DTLS 1.3 record number encryption =====================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Encrypt/decrypt record number (XOR with AES-ECB mask — symmetric operation).
|
|
381
|
+
* mask = AES-ECB(snKey, ciphertext[0..15])
|
|
382
|
+
* result = rnBytes XOR mask[0..len-1]
|
|
383
|
+
*/
|
|
384
|
+
function maskRecordNumber(snKey, rnBytes, ciphertext) {
|
|
385
|
+
let sample = new Uint8Array(16);
|
|
386
|
+
sample.set(ciphertext.subarray(0, Math.min(16, ciphertext.length)), 0);
|
|
387
|
+
let mask = aesEcbEncrypt(snKey, sample);
|
|
388
|
+
let out = new Uint8Array(rnBytes.length);
|
|
389
|
+
for (let i = 0; i < rnBytes.length; i++) out[i] = rnBytes[i] ^ mask[i];
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// ===================== DTLS 1.3 encrypted record build/decrypt =====================
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Build a DTLS 1.3 encrypted record (unified header).
|
|
398
|
+
*
|
|
399
|
+
* innerType: content type (22=handshake, 23=app_data, 26=ACK)
|
|
400
|
+
* plaintext: content to encrypt
|
|
401
|
+
* seq: record sequence number
|
|
402
|
+
* epoch: low 2 bits (2=handshake, 3=application)
|
|
403
|
+
* keys: { key, iv, snKey, algo? }
|
|
404
|
+
*/
|
|
405
|
+
function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
406
|
+
let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
|
|
407
|
+
let isChaCha = algo === 'chacha20-poly1305';
|
|
408
|
+
|
|
409
|
+
// Unified header: 2-byte seq, with length, no CID
|
|
410
|
+
let info = buildUnifiedHdr(epoch, true, true, false);
|
|
411
|
+
|
|
412
|
+
// Plaintext record number
|
|
413
|
+
let rn = new Uint8Array(2);
|
|
414
|
+
rn[0] = (seq >>> 8) & 0xFF;
|
|
415
|
+
rn[1] = seq & 0xFF;
|
|
416
|
+
|
|
417
|
+
// Inner plaintext: content + content_type
|
|
418
|
+
let inner = new Uint8Array(plaintext.length + 1);
|
|
419
|
+
inner.set(plaintext, 0);
|
|
420
|
+
inner[plaintext.length] = innerType;
|
|
421
|
+
|
|
422
|
+
let encLen = inner.length + 16;
|
|
423
|
+
|
|
424
|
+
// AAD = header with PLAINTEXT record number (before RN encryption)
|
|
425
|
+
let aad = new Uint8Array(5);
|
|
426
|
+
aad[0] = info;
|
|
427
|
+
aad[1] = rn[0];
|
|
428
|
+
aad[2] = rn[1];
|
|
429
|
+
aad[3] = (encLen >>> 8) & 0xFF;
|
|
430
|
+
aad[4] = encLen & 0xFF;
|
|
431
|
+
|
|
432
|
+
// Nonce (reuse TLS 1.3 nonce construction)
|
|
433
|
+
let nonce = getNonce(keys.iv, seq);
|
|
434
|
+
|
|
435
|
+
// AEAD encrypt
|
|
436
|
+
let cipher = crypto.createCipheriv(algo, keys.key, nonce,
|
|
437
|
+
isChaCha ? { authTagLength: 16 } : undefined);
|
|
438
|
+
cipher.setAAD(aad, isChaCha ? { plaintextLength: inner.length } : undefined);
|
|
439
|
+
let ct = cipher.update(inner);
|
|
440
|
+
cipher.final();
|
|
441
|
+
let tag = cipher.getAuthTag();
|
|
442
|
+
|
|
443
|
+
let ciphertext = new Uint8Array(ct.length + tag.length);
|
|
444
|
+
ciphertext.set(new Uint8Array(ct), 0);
|
|
445
|
+
ciphertext.set(new Uint8Array(tag), ct.length);
|
|
446
|
+
|
|
447
|
+
// Encrypt record number
|
|
448
|
+
let encRn = maskRecordNumber(keys.snKey, rn, ciphertext);
|
|
449
|
+
|
|
450
|
+
// Assemble: info(1) + encrypted_rn(2) + length(2) + ciphertext
|
|
451
|
+
let record = new Uint8Array(5 + ciphertext.length);
|
|
452
|
+
record[0] = info;
|
|
453
|
+
record[1] = encRn[0];
|
|
454
|
+
record[2] = encRn[1];
|
|
455
|
+
record[3] = (ciphertext.length >>> 8) & 0xFF;
|
|
456
|
+
record[4] = ciphertext.length & 0xFF;
|
|
457
|
+
record.set(ciphertext, 5);
|
|
458
|
+
return record;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Decrypt a DTLS 1.3 encrypted record.
|
|
463
|
+
* data: full record bytes (starting with unified header byte)
|
|
464
|
+
* keys: { key, iv, snKey, algo? }
|
|
465
|
+
* Returns { epoch, seq, type, content } or null on failure.
|
|
466
|
+
*/
|
|
467
|
+
function decryptEncryptedDtls13(data, keys) {
|
|
468
|
+
if (data.length < 1) return null;
|
|
469
|
+
|
|
470
|
+
let hdr = parseUnifiedHdr(data[0]);
|
|
471
|
+
let off = 1;
|
|
472
|
+
|
|
473
|
+
if (hdr.hasCid) return null; // CID not supported yet
|
|
474
|
+
|
|
475
|
+
let rnLen = hdr.seqLen2 ? 2 : 1;
|
|
476
|
+
if (off + rnLen > data.length) return null;
|
|
477
|
+
let encRn = data.slice(off, off + rnLen);
|
|
478
|
+
off += rnLen;
|
|
479
|
+
|
|
480
|
+
let ctLen;
|
|
481
|
+
if (hdr.hasLength) {
|
|
482
|
+
if (off + 2 > data.length) return null;
|
|
483
|
+
ctLen = readU16(data, off);
|
|
484
|
+
off += 2;
|
|
485
|
+
} else {
|
|
486
|
+
ctLen = data.length - off;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (off + ctLen > data.length) return null;
|
|
490
|
+
let ciphertext = data.slice(off, off + ctLen);
|
|
491
|
+
|
|
492
|
+
// Decrypt record number
|
|
493
|
+
let rn = maskRecordNumber(keys.snKey, encRn, ciphertext);
|
|
494
|
+
let seq = hdr.seqLen2 ? ((rn[0] << 8) | rn[1]) : rn[0];
|
|
495
|
+
|
|
496
|
+
// Rebuild AAD with plaintext RN
|
|
497
|
+
let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
|
|
498
|
+
let aad = new Uint8Array(hdrLen);
|
|
499
|
+
aad[0] = data[0];
|
|
500
|
+
for (let i = 0; i < rnLen; i++) aad[1 + i] = rn[i];
|
|
501
|
+
if (hdr.hasLength) {
|
|
502
|
+
aad[1 + rnLen] = (ctLen >>> 8) & 0xFF;
|
|
503
|
+
aad[1 + rnLen + 1] = ctLen & 0xFF;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let nonce = getNonce(keys.iv, seq);
|
|
507
|
+
let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
|
|
508
|
+
let isChaCha = algo === 'chacha20-poly1305';
|
|
509
|
+
|
|
510
|
+
if (ciphertext.length < 16) return null;
|
|
511
|
+
let ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
512
|
+
let tag = ciphertext.subarray(ciphertext.length - 16);
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
let decipher = crypto.createDecipheriv(algo, keys.key, nonce,
|
|
516
|
+
isChaCha ? { authTagLength: 16 } : undefined);
|
|
517
|
+
decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
|
|
518
|
+
decipher.setAuthTag(tag);
|
|
519
|
+
let pt = decipher.update(ct);
|
|
520
|
+
decipher.final();
|
|
521
|
+
|
|
522
|
+
let inner = parseInnerPlaintext(new Uint8Array(pt));
|
|
523
|
+
return {
|
|
524
|
+
epoch: hdr.epoch,
|
|
525
|
+
seq: seq,
|
|
526
|
+
type: inner.type,
|
|
527
|
+
content: inner.content,
|
|
528
|
+
total_length: off + ctLen,
|
|
529
|
+
};
|
|
530
|
+
} catch (e) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
// ===================== DTLS datagram parsing =====================
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Parse a DTLS datagram containing one or more records.
|
|
540
|
+
* Dispatches between plaintext (classic header) and encrypted (unified header).
|
|
541
|
+
*
|
|
542
|
+
* keysByEpoch: { [epoch]: { key, iv, snKey, algo } } — null for plaintext only.
|
|
543
|
+
* Returns array of { type, epoch, seq, content, encrypted }.
|
|
544
|
+
*/
|
|
545
|
+
function parseDtlsDatagram(data, keysByEpoch) {
|
|
546
|
+
let records = [];
|
|
547
|
+
let off = 0;
|
|
548
|
+
|
|
549
|
+
while (off < data.length) {
|
|
550
|
+
let first = data[off];
|
|
551
|
+
|
|
552
|
+
if (isUnifiedHdr(first)) {
|
|
553
|
+
let hdr = parseUnifiedHdr(first);
|
|
554
|
+
let keys = keysByEpoch ? keysByEpoch[hdr.epoch] : null;
|
|
555
|
+
|
|
556
|
+
if (!keys) {
|
|
557
|
+
// Can't decrypt — try to skip
|
|
558
|
+
let rnLen = hdr.seqLen2 ? 2 : 1;
|
|
559
|
+
let skip = 1 + rnLen;
|
|
560
|
+
if (hdr.hasLength && off + skip + 2 <= data.length) {
|
|
561
|
+
skip += 2 + readU16(data, off + skip);
|
|
562
|
+
} else {
|
|
563
|
+
skip = data.length - off;
|
|
564
|
+
}
|
|
565
|
+
off += skip;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let result = decryptEncryptedDtls13(data.subarray(off), keys);
|
|
570
|
+
if (result) {
|
|
571
|
+
records.push({
|
|
572
|
+
type: result.type,
|
|
573
|
+
epoch: result.epoch,
|
|
574
|
+
seq: result.seq,
|
|
575
|
+
content: result.content,
|
|
576
|
+
encrypted: true,
|
|
577
|
+
});
|
|
578
|
+
off += result.total_length;
|
|
579
|
+
} else {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
} else if (first <= 63) {
|
|
584
|
+
// Classic DTLS record
|
|
585
|
+
if (off + 13 > data.length) break;
|
|
586
|
+
let type = data[off];
|
|
587
|
+
let epoch = readU16(data, off + 3);
|
|
588
|
+
let seq = readU48(data, off + 5);
|
|
589
|
+
let length = readU16(data, off + 11);
|
|
590
|
+
if (off + 13 + length > data.length) break;
|
|
591
|
+
|
|
592
|
+
let payload = data.slice(off + 13, off + 13 + length);
|
|
593
|
+
let encrypted = false;
|
|
594
|
+
|
|
595
|
+
// DTLS 1.2: decrypt if epoch > 0 and keys available
|
|
596
|
+
if (epoch > 0 && keysByEpoch && keysByEpoch[epoch]) {
|
|
597
|
+
let keys = keysByEpoch[epoch];
|
|
598
|
+
try {
|
|
599
|
+
payload = decryptDtls12(payload, keys.key, keys.iv, epoch, seq, type);
|
|
600
|
+
encrypted = true;
|
|
601
|
+
} catch(e) {
|
|
602
|
+
// Decryption failed — return raw
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
records.push({
|
|
607
|
+
type: type,
|
|
608
|
+
epoch: epoch,
|
|
609
|
+
seq: seq,
|
|
610
|
+
content: payload,
|
|
611
|
+
encrypted: encrypted,
|
|
612
|
+
});
|
|
613
|
+
off += 13 + length;
|
|
614
|
+
} else {
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return records;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
// ===================== DTLS 1.3 ACK (RFC 9147 §7) =====================
|
|
623
|
+
//
|
|
624
|
+
// struct {
|
|
625
|
+
// RecordNumber record_numbers<0..2^16-1>;
|
|
626
|
+
// }
|
|
627
|
+
// struct {
|
|
628
|
+
// uint16 epoch;
|
|
629
|
+
// uint48 sequence_number;
|
|
630
|
+
// } RecordNumber; // 8 bytes
|
|
631
|
+
|
|
632
|
+
/** Build ACK payload. acks: [{ epoch, seq }, ...] */
|
|
633
|
+
function buildDtlsAck(acks) {
|
|
634
|
+
let bodyLen = acks.length * 8;
|
|
635
|
+
let out = new Uint8Array(2 + bodyLen);
|
|
636
|
+
let off = 0;
|
|
637
|
+
off = w_u16(out, off, bodyLen);
|
|
638
|
+
for (let i = 0; i < acks.length; i++) {
|
|
639
|
+
off = w_u16(out, off, acks[i].epoch);
|
|
640
|
+
off = w_u48(out, off, acks[i].seq);
|
|
641
|
+
}
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Parse ACK payload. Returns [{ epoch, seq }, ...]. */
|
|
646
|
+
function parseDtlsAck(data) {
|
|
647
|
+
let bodyLen = readU16(data, 0);
|
|
648
|
+
let off = 2;
|
|
649
|
+
let end = off + bodyLen;
|
|
650
|
+
let acks = [];
|
|
651
|
+
while (off + 8 <= end) {
|
|
652
|
+
let epoch = readU16(data, off); off += 2;
|
|
653
|
+
let seq = readU48(data, off); off += 6;
|
|
654
|
+
acks.push({ epoch, seq });
|
|
655
|
+
}
|
|
656
|
+
return acks;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
|
|
208
660
|
// ===================== Exports =====================
|
|
209
661
|
|
|
210
662
|
export {
|
|
@@ -229,4 +681,34 @@ export {
|
|
|
229
681
|
// Record framing
|
|
230
682
|
CT,
|
|
231
683
|
writeRecord,
|
|
684
|
+
|
|
685
|
+
// DTLS helpers
|
|
686
|
+
aesEcbEncrypt,
|
|
687
|
+
isUnifiedHdr,
|
|
688
|
+
|
|
689
|
+
// DTLS plaintext records
|
|
690
|
+
buildDtlsPlaintext,
|
|
691
|
+
parseDtlsPlaintext,
|
|
692
|
+
|
|
693
|
+
// DTLS 1.2 encrypted records
|
|
694
|
+
buildDtlsAad12,
|
|
695
|
+
encryptDtls12,
|
|
696
|
+
decryptDtls12,
|
|
697
|
+
buildEncryptedDtls12,
|
|
698
|
+
|
|
699
|
+
// DTLS 1.3 unified header
|
|
700
|
+
buildUnifiedHdr,
|
|
701
|
+
parseUnifiedHdr,
|
|
702
|
+
maskRecordNumber,
|
|
703
|
+
|
|
704
|
+
// DTLS 1.3 encrypted records
|
|
705
|
+
buildEncryptedDtls13,
|
|
706
|
+
decryptEncryptedDtls13,
|
|
707
|
+
|
|
708
|
+
// DTLS datagram parsing
|
|
709
|
+
parseDtlsDatagram,
|
|
710
|
+
|
|
711
|
+
// DTLS 1.3 ACK
|
|
712
|
+
buildDtlsAck,
|
|
713
|
+
parseDtlsAck,
|
|
232
714
|
};
|
package/src/session/message.js
CHANGED
|
@@ -97,10 +97,12 @@ function build_tls_message(params) {
|
|
|
97
97
|
|
|
98
98
|
if (params.type == 'server_hello') {
|
|
99
99
|
type = wire.TLS_MESSAGE_TYPE.SERVER_HELLO;
|
|
100
|
-
|
|
100
|
+
params.kind = 'server';
|
|
101
|
+
body = wire.build_hello(params);
|
|
101
102
|
} else if (params.type == 'client_hello') {
|
|
102
103
|
type = wire.TLS_MESSAGE_TYPE.CLIENT_HELLO;
|
|
103
|
-
|
|
104
|
+
params.kind = 'client';
|
|
105
|
+
body = wire.build_hello(params);
|
|
104
106
|
} else if (params.type == 'server_key_exchange') {
|
|
105
107
|
type = wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE;
|
|
106
108
|
body = wire.build_server_key_exchange_ecdhe(params);
|
|
@@ -145,7 +147,8 @@ function parse_tls_message(data) {
|
|
|
145
147
|
let message = wire.parse_message(data);
|
|
146
148
|
|
|
147
149
|
if (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO || message.type == wire.TLS_MESSAGE_TYPE.SERVER_HELLO) {
|
|
148
|
-
let
|
|
150
|
+
let kind = (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO) ? 'client' : 'server';
|
|
151
|
+
let hello = wire.parse_hello({ kind: kind, body: message.body });
|
|
149
152
|
out = normalize_hello(hello);
|
|
150
153
|
|
|
151
154
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE) {
|