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/src/record.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * record.js — Shared record-layer primitives for TLS 1.2 and TLS 1.3.
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 raw record framing.
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
  };
@@ -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
- body = wire.build_hello('server', params);
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
- body = wire.build_hello('client', params);
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 hello = wire.parse_hello(message.type, message.body);
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) {