lemon-tls 0.4.1 → 0.5.5

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/tls_socket.js CHANGED
@@ -331,6 +331,18 @@ function TLSSocket(duplex, options){
331
331
  ? context.isTls13
332
332
  : session.getVersion() === 0x0304; // early records (pre-secureConnect)
333
333
 
334
+ // AEAD decryption can throw on any forged/corrupted record (GCM/ChaCha
335
+ // tag mismatch surfaces as an exception from decipher.final()). Left
336
+ // unhandled, a single injected bad segment would throw out of the
337
+ // transport 'data' handler — an unhandled exception that tears down the
338
+ // connection or the process: a trivial remote DoS.
339
+ //
340
+ // RFC 8446 §5.2: in TLS (unlike DTLS, which silently drops), a record
341
+ // that fails to decrypt MUST be treated as a fatal error. So we catch,
342
+ // send a fatal bad_record_mac alert, destroy the connection, and stop.
343
+ // The read sequence number is intentionally NOT advanced on failure —
344
+ // the rejected record never counts.
345
+ try {
334
346
  if (!isTls13 && context.remote_ccs_seen === true) {
335
347
  // TLS 1.2: decrypt with key_block derived keys
336
348
  if (context.app_read_key === null) {
@@ -394,7 +406,14 @@ function TLSSocket(duplex, options){
394
406
  }
395
407
  context.handshake_read_seq++;
396
408
  }
397
-
409
+ } catch (e) {
410
+ // Record failed authentication → fatal bad_record_mac (RFC 8446 §5.2).
411
+ try { session.sendAlert ? session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC) : null; } catch (_) {}
412
+ self.emit('error', new Error('TLS record decryption failed (bad_record_mac)'));
413
+ self.destroy();
414
+ return;
415
+ }
416
+
398
417
 
399
418
  if (out !== null) {
400
419
  // Reuse cached isTls13 computed at the top of this function
@@ -1427,6 +1446,30 @@ function TLSSocket(duplex, options){
1427
1446
  /** Full negotiation result — all selected parameters in one object. */
1428
1447
  self.getNegotiationResult = function(){ return session.getNegotiationResult ? session.getNegotiationResult() : null; };
1429
1448
 
1449
+ /**
1450
+ * Export keying material (RFC 5705 for TLS 1.2, RFC 8446 §7.5 for
1451
+ * TLS 1.3). Mirrors Node's tls.TLSSocket#exportKeyingMaterial —
1452
+ * same argument order (length, label, context). One deliberate
1453
+ * difference: Node THROWS before the handshake completes; we return
1454
+ * null (matching this library's null-until-ready convention used by
1455
+ * getTrafficSecrets etc.).
1456
+ */
1457
+ self.exportKeyingMaterial = function(length, label, context_value){
1458
+ return session.exportKeyingMaterial
1459
+ ? session.exportKeyingMaterial(length, label, context_value)
1460
+ : null;
1461
+ };
1462
+
1463
+ /** Peer hello extensions: [{ type, name, data, value }]. */
1464
+ self.getRemoteExtensions = function(){
1465
+ return session.getRemoteExtensions ? session.getRemoteExtensions() : [];
1466
+ };
1467
+
1468
+ /** One peer extension by numeric type, or null. */
1469
+ self.getRemoteExtension = function(type){
1470
+ return session.getRemoteExtension ? session.getRemoteExtension(type) : null;
1471
+ };
1472
+
1430
1473
  /** Request Key Update — refresh outgoing encryption keys (TLS 1.3 only). */
1431
1474
  self.rekeySend = function(){ if (session.requestKeyUpdate) session.requestKeyUpdate(false); };
1432
1475
 
package/src/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
 
2
3
  function concatUint8Arrays(arrays) {
3
4
  let totalLength = 0;
@@ -93,9 +94,35 @@ function uint8Equal(a, b) {
93
94
  }
94
95
 
95
96
 
97
+ /**
98
+ * Constant-time equality for two byte arrays.
99
+ *
100
+ * Unlike uint8Equal (which short-circuits on the first differing byte and
101
+ * therefore leaks — via timing — how many leading bytes matched), this always
102
+ * compares the full length. Use it wherever a mismatch would otherwise be
103
+ * exploitable as an oracle: Finished verify_data, PSK binders, ticket key_name,
104
+ * and any MAC/tag comparison.
105
+ *
106
+ * Returns false immediately (and safely) on length mismatch — that's public
107
+ * information, not a secret, so short-circuiting there is fine.
108
+ *
109
+ * Note: crypto.timingSafeEqual requires two equal-length Buffers. We wrap the
110
+ * inputs as Buffer views over the same memory (no copy) via byteOffset/length.
111
+ */
112
+ function timingSafeEqualU8(a, b) {
113
+ if (a == null || b == null) return false;
114
+ if (a.length !== b.length) return false;
115
+ if (a.length === 0) return true;
116
+ let ba = Buffer.isBuffer(a) ? a : Buffer.from(a.buffer, a.byteOffset, a.length);
117
+ let bb = Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.length);
118
+ return crypto.timingSafeEqual(ba, bb);
119
+ }
120
+
121
+
96
122
  export {
97
123
  concatUint8Arrays,
98
124
  arraybufferEqual,
99
125
  arraysEqual,
100
- uint8Equal
126
+ uint8Equal,
127
+ timingSafeEqualU8
101
128
  };