lemon-tls 0.2.2 → 0.4.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.
- package/README.md +258 -203
- package/index.d.ts +145 -14
- package/index.js +12 -0
- package/package.json +2 -2
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/dtls_socket.js +3 -0
- package/src/record.js +408 -61
- package/src/session/message.js +28 -3
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +820 -99
- package/src/tls_socket.js +815 -249
- package/src/wire.js +25 -0
|
@@ -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
|
+
};
|