lemon-tls 0.6.0 → 0.9.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.
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import * as crypto from 'node:crypto';
19
- import { uint8Equal } from '../utils.js';
19
+ import { timingSafeEqualU8 } from '../utils.js';
20
20
 
21
21
  /* =========================================================================
22
22
  * SERVER-SIDE ENCRYPTED BLOB
@@ -82,8 +82,11 @@ function decrypt_session_blob(blob, ticketKeys) {
82
82
 
83
83
  let { key_name, aes_key } = split_ticket_keys(ticketKeys);
84
84
 
85
- // Verify key_name matches (allows rotation — caller may try multiple keys)
86
- if (!uint8Equal(blob_key_name, key_name)) return null;
85
+ // Verify key_name matches (allows rotation — caller may try multiple keys).
86
+ // Constant-time: utils.js names "ticket key_name" explicitly in the list
87
+ // of comparisons that must not short-circuit, but this call site was
88
+ // still using uint8Equal, which returns on the first differing byte.
89
+ if (!timingSafeEqualU8(blob_key_name, key_name)) return null;
87
90
 
88
91
  let decipher = crypto.createDecipheriv('aes-256-gcm', aes_key, iv);
89
92
  decipher.setAuthTag(tag);
@@ -119,6 +122,25 @@ function encode_client_session(state) {
119
122
  * @param {Uint8Array|Buffer} blob
120
123
  * @returns {Object|null}
121
124
  */
125
+ /**
126
+ * What an application may know of a client session blob ('session' event)
127
+ * without its secrets: { version, servername, alpn, created, expiresAt (ms),
128
+ * earlyData (whether its ticket allows 0-RTT) } — or null for a blob that is
129
+ * not one. For a cache to drop what has expired.
130
+ */
131
+ function client_session_info(blob) {
132
+ let s = decode_client_session(blob);
133
+ if (!s) return null;
134
+ return {
135
+ version: s.v === 13 ? 'TLSv1.3' : s.v === 12 ? 'TLSv1.2' : null,
136
+ servername: s.sni != null ? s.sni : null,
137
+ alpn: s.alpn != null ? s.alpn : null,
138
+ created: typeof s.created === 'number' ? s.created : null,
139
+ expiresAt: typeof s.created === 'number' && typeof s.lifetime === 'number' ? s.created + s.lifetime * 1000 : null,
140
+ earlyData: !!(s.max_early_data > 0),
141
+ };
142
+ }
143
+
122
144
  function decode_client_session(blob) {
123
145
  try {
124
146
  if (!blob || blob.length === 0) return null;
@@ -135,22 +157,19 @@ function decode_client_session(blob) {
135
157
 
136
158
  /**
137
159
  * Convert state object to JSON-safe form.
138
- * Uint8Array/Buffer fields → { $b: base64 }
160
+ * Uint8Array/Buffer values → { $b: base64 }, inside objects and arrays alike
161
+ * (a client blob holds the server's certificate chain: an array of DER).
139
162
  */
163
+ function serialize_value(v) {
164
+ if (v == null) return null;
165
+ if (v instanceof Uint8Array || Buffer.isBuffer(v)) return { $b: Buffer.from(v).toString('base64') };
166
+ if (Array.isArray(v)) return v.map(serialize_value);
167
+ if (typeof v === 'object') return serialize_state(v);
168
+ return v;
169
+ }
140
170
  function serialize_state(state) {
141
171
  let out = {};
142
- for (let k in state) {
143
- let v = state[k];
144
- if (v == null) {
145
- out[k] = null;
146
- } else if (v instanceof Uint8Array || Buffer.isBuffer(v)) {
147
- out[k] = { $b: Buffer.from(v).toString('base64') };
148
- } else if (typeof v === 'object' && !Array.isArray(v)) {
149
- out[k] = serialize_state(v);
150
- } else {
151
- out[k] = v;
152
- }
153
- }
172
+ for (let k in state) out[k] = serialize_value(state[k]);
154
173
  return out;
155
174
  }
156
175
 
@@ -158,21 +177,17 @@ function serialize_state(state) {
158
177
  * Convert JSON-decoded form back to state object.
159
178
  * { $b: base64 } → Uint8Array
160
179
  */
180
+ function deserialize_value(v) {
181
+ if (v == null) return null;
182
+ if (Array.isArray(v)) return v.map(deserialize_value);
183
+ if (typeof v === 'object' && typeof v.$b === 'string' && Object.keys(v).length === 1) return new Uint8Array(Buffer.from(v.$b, 'base64'));
184
+ if (typeof v === 'object') return deserialize_state(v);
185
+ return v;
186
+ }
161
187
  function deserialize_state(obj) {
162
188
  if (obj == null) return null;
163
189
  let out = {};
164
- for (let k in obj) {
165
- let v = obj[k];
166
- if (v == null) {
167
- out[k] = null;
168
- } else if (typeof v === 'object' && typeof v.$b === 'string' && Object.keys(v).length === 1) {
169
- out[k] = new Uint8Array(Buffer.from(v.$b, 'base64'));
170
- } else if (typeof v === 'object' && !Array.isArray(v)) {
171
- out[k] = deserialize_state(v);
172
- } else {
173
- out[k] = v;
174
- }
175
- }
190
+ for (let k in obj) out[k] = deserialize_value(obj[k]);
176
191
  return out;
177
192
  }
178
193
 
@@ -180,6 +195,7 @@ export {
180
195
  encrypt_session_blob,
181
196
  decrypt_session_blob,
182
197
  encode_client_session,
198
+ client_session_info,
183
199
  decode_client_session,
184
200
  split_ticket_keys,
185
201
  };