lemon-tls 0.2.1 → 0.3.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.
@@ -68,6 +68,15 @@ function normalize_hello(hello) {
68
68
  out.heartbeat = value;
69
69
  } else if (name === 'USE_SRTP') {
70
70
  out.use_srtp = value;
71
+ } else if (name === 'SESSION_TICKET') {
72
+ // RFC 5077: opaque ticket bytes (TLS 1.2)
73
+ // Empty in ClientHello = client supports tickets / non-empty = resume using this ticket
74
+ // Empty in ServerHello = server will send NewSessionTicket
75
+ out.session_ticket = value;
76
+ out.session_ticket_supported = true;
77
+ } else if (name === 'EXTENDED_MASTER_SECRET') {
78
+ // RFC 7627: presence signals EMS support
79
+ out.extended_master_secret = true;
71
80
  } else {
72
81
  if (!('unknown' in out)) out.unknown = [];
73
82
  out.unknown.push(e);
@@ -97,10 +106,12 @@ function build_tls_message(params) {
97
106
 
98
107
  if (params.type == 'server_hello') {
99
108
  type = wire.TLS_MESSAGE_TYPE.SERVER_HELLO;
100
- body = wire.build_hello('server', params);
109
+ params.kind = 'server';
110
+ body = wire.build_hello(params);
101
111
  } else if (params.type == 'client_hello') {
102
112
  type = wire.TLS_MESSAGE_TYPE.CLIENT_HELLO;
103
- body = wire.build_hello('client', params);
113
+ params.kind = 'client';
114
+ body = wire.build_hello(params);
104
115
  } else if (params.type == 'server_key_exchange') {
105
116
  type = wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE;
106
117
  body = wire.build_server_key_exchange_ecdhe(params);
@@ -131,6 +142,12 @@ function build_tls_message(params) {
131
142
  } else if (params.type == 'hello_retry_request') {
132
143
  type = wire.TLS_MESSAGE_TYPE.SERVER_HELLO; // HRR uses ServerHello type with magic random
133
144
  body = wire.build_hello_retry_request(params);
145
+ } else if (params.type == 'new_session_ticket_tls12') {
146
+ type = wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET;
147
+ body = wire.build_new_session_ticket_tls12(params);
148
+ } else if (params.type == 'new_session_ticket') {
149
+ type = wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET;
150
+ body = wire.build_new_session_ticket(params);
134
151
  }
135
152
 
136
153
  return wire.build_message(type, body);
@@ -139,13 +156,17 @@ function build_tls_message(params) {
139
156
 
140
157
  /**
141
158
  * Parse a raw TLS handshake message into a typed object.
159
+ * @param {Uint8Array} data — handshake message bytes
160
+ * @param {number} [negotiatedVersion] — optional; 0x0303 for TLS 1.2, 0x0304 for TLS 1.3.
161
+ * Needed to disambiguate NewSessionTicket wire format.
142
162
  */
143
- function parse_tls_message(data) {
163
+ function parse_tls_message(data, negotiatedVersion) {
144
164
  let out = {};
145
165
  let message = wire.parse_message(data);
146
166
 
147
167
  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);
168
+ let kind = (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO) ? 'client' : 'server';
169
+ let hello = wire.parse_hello({ kind: kind, body: message.body });
149
170
  out = normalize_hello(hello);
150
171
 
151
172
  } else if (message.type == wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE) {
@@ -180,7 +201,14 @@ function parse_tls_message(data) {
180
201
  out.body = message.body;
181
202
 
182
203
  } else if (message.type == wire.TLS_MESSAGE_TYPE.NEW_SESSION_TICKET) {
183
- out = wire.parse_new_session_ticket(message.body);
204
+ // TLS 1.2 and 1.3 have different wire formats for this message.
205
+ // TLS 1.3: ticket_lifetime | ticket_age_add | ticket_nonce | ticket | extensions
206
+ // TLS 1.2: ticket_lifetime_hint | ticket (RFC 5077)
207
+ if (negotiatedVersion === 0x0303) {
208
+ out = wire.parse_new_session_ticket_tls12(message.body);
209
+ } else {
210
+ out = wire.parse_new_session_ticket(message.body);
211
+ }
184
212
  out.type = 'new_session_ticket';
185
213
 
186
214
  } else if (message.type == wire.TLS_MESSAGE_TYPE.KEY_UPDATE) {
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Session ticket / session blob encoding & decoding.
3
+ *
4
+ * Two distinct blob types:
5
+ *
6
+ * 1. SERVER blob (encrypted) — issued by server to client (TLS 1.3 NewSessionTicket,
7
+ * TLS 1.2 NewSessionTicket), or to user's store (TLS 1.2 Session IDs via 'newSession' event).
8
+ * Client/store returns it back; server decrypts with its ticketKeys.
9
+ *
10
+ * Format: key_name(16) | IV(12) | Ciphertext(AES-256-GCM) | Tag(16)
11
+ * ticketKeys layout: [0:16]=key_name, [16:48]=AES-256-GCM key
12
+ *
13
+ * 2. CLIENT blob (plaintext) — returned by TLSSocket to the user via 'session' event;
14
+ * user passes it back in tls.connect({ session }). JSON-encoded, not encrypted —
15
+ * user is responsible for secure storage (same convention as Node.js).
16
+ */
17
+
18
+ import * as crypto from 'node:crypto';
19
+ import { uint8Equal } from '../utils.js';
20
+
21
+ /* =========================================================================
22
+ * SERVER-SIDE ENCRYPTED BLOB
23
+ * ========================================================================= */
24
+
25
+ const KEY_NAME_LEN = 16;
26
+ const IV_LEN = 12;
27
+ const TAG_LEN = 16;
28
+
29
+ /**
30
+ * Split ticketKeys (48 bytes) into key_name (16) + aes key (32).
31
+ * Throws if ticketKeys is malformed.
32
+ */
33
+ function split_ticket_keys(ticketKeys) {
34
+ if (!ticketKeys) throw new Error('ticketKeys is required');
35
+ let buf = Buffer.isBuffer(ticketKeys) ? ticketKeys : Buffer.from(ticketKeys);
36
+ if (buf.length !== 48) throw new Error('ticketKeys must be exactly 48 bytes');
37
+ return {
38
+ key_name: buf.slice(0, 16),
39
+ aes_key: buf.slice(16, 48),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Encrypt a server-side session state into an opaque blob.
45
+ *
46
+ * @param {Object} state — session state (version, cipher, master_secret, ...)
47
+ * @param {Buffer} ticketKeys — 48 bytes (key_name + aes_key)
48
+ * @returns {Uint8Array} — key_name(16) | IV(12) | CT | Tag(16)
49
+ */
50
+ function encrypt_session_blob(state, ticketKeys) {
51
+ let { key_name, aes_key } = split_ticket_keys(ticketKeys);
52
+
53
+ // Serialize state → JSON with Uint8Array fields base64-encoded
54
+ let serialized = Buffer.from(JSON.stringify(serialize_state(state)));
55
+
56
+ let iv = crypto.randomBytes(IV_LEN);
57
+ let cipher = crypto.createCipheriv('aes-256-gcm', aes_key, iv);
58
+ let ct = cipher.update(serialized);
59
+ cipher.final();
60
+ let tag = cipher.getAuthTag();
61
+
62
+ let out = Buffer.concat([key_name, iv, ct, tag]);
63
+ return new Uint8Array(out);
64
+ }
65
+
66
+ /**
67
+ * Decrypt a server-side session blob.
68
+ *
69
+ * @param {Uint8Array} blob — encrypted blob
70
+ * @param {Buffer} ticketKeys — 48 bytes. Must match the key_name embedded in blob.
71
+ * @returns {Object|null} — state, or null if decryption failed (wrong key_name, tampered data, etc).
72
+ */
73
+ function decrypt_session_blob(blob, ticketKeys) {
74
+ try {
75
+ if (!blob || blob.length < KEY_NAME_LEN + IV_LEN + TAG_LEN) return null;
76
+ let buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
77
+
78
+ let blob_key_name = buf.slice(0, KEY_NAME_LEN);
79
+ let iv = buf.slice(KEY_NAME_LEN, KEY_NAME_LEN + IV_LEN);
80
+ let tag = buf.slice(buf.length - TAG_LEN);
81
+ let ct = buf.slice(KEY_NAME_LEN + IV_LEN, buf.length - TAG_LEN);
82
+
83
+ let { key_name, aes_key } = split_ticket_keys(ticketKeys);
84
+
85
+ // Verify key_name matches (allows rotation — caller may try multiple keys)
86
+ if (!uint8Equal(blob_key_name, key_name)) return null;
87
+
88
+ let decipher = crypto.createDecipheriv('aes-256-gcm', aes_key, iv);
89
+ decipher.setAuthTag(tag);
90
+ let pt = decipher.update(ct);
91
+ decipher.final(); // throws on tag mismatch
92
+
93
+ let state = deserialize_state(JSON.parse(pt.toString('utf8')));
94
+ return state;
95
+ } catch (e) {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /* =========================================================================
101
+ * CLIENT-SIDE PLAINTEXT BLOB
102
+ * ========================================================================= */
103
+
104
+ /**
105
+ * Encode a client-side session blob. JSON-serialized, NOT encrypted.
106
+ * User is responsible for secure storage (this contains the master_secret).
107
+ *
108
+ * @param {Object} state
109
+ * @returns {Uint8Array} — utf-8 encoded JSON
110
+ */
111
+ function encode_client_session(state) {
112
+ let serialized = JSON.stringify(serialize_state(state));
113
+ return new Uint8Array(Buffer.from(serialized, 'utf8'));
114
+ }
115
+
116
+ /**
117
+ * Decode a client-side session blob.
118
+ *
119
+ * @param {Uint8Array|Buffer} blob
120
+ * @returns {Object|null}
121
+ */
122
+ function decode_client_session(blob) {
123
+ try {
124
+ if (!blob || blob.length === 0) return null;
125
+ let buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
126
+ return deserialize_state(JSON.parse(buf.toString('utf8')));
127
+ } catch (e) {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /* =========================================================================
133
+ * SERIALIZATION HELPERS
134
+ * ========================================================================= */
135
+
136
+ /**
137
+ * Convert state object to JSON-safe form.
138
+ * Uint8Array/Buffer fields → { $b: base64 }
139
+ */
140
+ function serialize_state(state) {
141
+ 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
+ }
154
+ return out;
155
+ }
156
+
157
+ /**
158
+ * Convert JSON-decoded form back to state object.
159
+ * { $b: base64 } → Uint8Array
160
+ */
161
+ function deserialize_state(obj) {
162
+ if (obj == null) return null;
163
+ 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
+ }
176
+ return out;
177
+ }
178
+
179
+ export {
180
+ encrypt_session_blob,
181
+ decrypt_session_blob,
182
+ encode_client_session,
183
+ decode_client_session,
184
+ split_ticket_keys,
185
+ };