UNPKG

lemon-tls

Version:

JavaScript TLS 1.3/1.2 implementation for Node.js, with full control over cryptographic keys and record layer

454 lines (333 loc) 15.6 kB
import TLSSession from './tls_session.js'; import { AES } from '@stablelib/aes'; import { GCM } from '@stablelib/gcm'; import { TLS_CIPHER_SUITES, hkdf_expand_label } from './crypto.js'; function Emitter(){ var listeners = {}; return { on: function(name, fn){ (listeners[name] = listeners[name] || []).push(fn); }, emit: function(name){ var args = Array.prototype.slice.call(arguments, 1); var arr = listeners[name] || []; for (var i=0;i<arr.length;i++){ try{ arr[i].apply(null, args); }catch(e){} } } }; } // TLS ContentType var CT = { CHANGE_CIPHER_SPEC:20, ALERT:21, HANDSHAKE:22, APPLICATION_DATA:23 }; // legacy_record_version בשדה כותרת הרשומה (TLS 1.3 שומר 0x0303) var REC_VERSION = 0x0303; // ==== עזרי המרה ==== function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); } function toU8(buf){ return (buf instanceof Uint8Array) ? buf : new Uint8Array(buf || []); } function tls_derive_from_tls_secrets(traffic_secret, cipher_suite){ var empty = new Uint8Array(0); var key = hkdf_expand_label(TLS_CIPHER_SUITES[cipher_suite].hash, traffic_secret, 'key', empty, TLS_CIPHER_SUITES[cipher_suite].keylen); var iv = hkdf_expand_label(TLS_CIPHER_SUITES[cipher_suite].hash, traffic_secret, 'iv', empty, 12); return { key: key, iv: iv, }; } function get_nonce(iv,seq) { var seq_buf = new Uint8Array(12); // all zero var view = new DataView(seq_buf.buffer); view.setBigUint64(4, BigInt(seq)); // offset 4, 64-bit BE var nonce = new Uint8Array(12); for (var i = 0; i < 12; i++) { nonce[i] = iv[i] ^ seq_buf[i]; } return nonce; } function encrypt_tls_record(innerType, plaintext, key, nonce) { const aes = new AES(key); const gcm = new GCM(aes); // TLSInnerPlaintext = content || content_type || padding(0x00…) const full_plaintext = new Uint8Array(plaintext.length + 1); full_plaintext.set(plaintext); full_plaintext[plaintext.length] = innerType; // ← אל תשכח לבחור נכון // AAD = [ 0x17, 0x03, 0x03, len_hi, len_lo ] // len = ciphertext.length כולל tag = full_plaintext.length + 16 (ב-GCM) const aad = new Uint8Array(5); aad[0] = 0x17; aad[1] = 0x03; aad[2] = 0x03; const recLen = full_plaintext.length + 16; // tag=16 aad[3] = (recLen >>> 8) & 0xff; aad[4] = (recLen ) & 0xff; // הצפנה אחת עם AAD הנכון const ciphertext = gcm.seal(nonce, full_plaintext, aad); // אמור להחזיר ct||tag return ciphertext; } function decrypt_tls_record(ciphertext, key, nonce) { // הקמה: AES + GCM var aes = new AES(key); var gcm = new GCM(aes); // AAD לפי TLS 1.3: 0x17 (application_data), גרסה 0x0303, ואורך ה-ciphertext var aad = new Uint8Array(5); aad[0] = 0x17; // record type תמיד 0x17 אחרי הצפנה aad[1] = 0x03; aad[2] = 0x03; // "גרסת" הרשומה (TLS 1.2 בפועל לשכבת הרשומה) var len = ciphertext.length; aad[3] = (len >> 8) & 0xff; aad[4] = len & 0xff; // פתיחה (אימות + פענוח). אם ה-tag לא תקף, תחזור null/undefined לפי המימוש var full_plaintext = gcm.open(nonce, ciphertext, aad); if (!full_plaintext) { throw new Error('GCM authentication failed (bad tag)'); } return full_plaintext; } function parse_tls_inner_plaintext(full_plaintext) { var j = full_plaintext.length - 1; while (j >= 0 && full_plaintext[j] === 0x00) { j--; } if (j < 0) throw new Error('Malformed TLSInnerPlaintext (no content type)'); var content_type = full_plaintext[j]; var content = full_plaintext.slice(0, j); // חיתוך אמיתי return { content_type: content_type, content: content }; } // ==== TLSSocket ==== function TLSSocket(duplex, options){ if (!(this instanceof TLSSocket)) return new TLSSocket(duplex, options); options = options || {}; var ev = Emitter(); var context = { options: options, // transport (Duplex) שמחובר מבחוץ transport: (duplex && typeof duplex.write === 'function') ? duplex : null, // TLSSession פנימי בלבד session: new TLSSession({ isServer: !!options.isServer, servername: options.servername, ALPNProtocols: options.ALPNProtocols || null, SNICallback: options.SNICallback || null }), // Handshake write handshake_write_key: null, handshake_write_iv: null, handshake_write_seq: 0, handshake_write_aead: null, // Handshake read handshake_read_key: null, handshake_read_iv: null, handshake_read_seq: 0, handshake_read_aead: null, // Application write app_write_key: null, app_write_iv: null, app_write_seq: 0, app_write_aead: null, // Application read app_read_key: null, app_read_iv: null, app_read_seq: 0, app_read_aead: null, using_app_keys: false, // באפרים ותורים readBuffer: Buffer.alloc(0), appWriteQueue: [], // מצבים כלליים destroyed: false, secureEstablished: false, // legacy record version (TLS1.3) rec_version: 0x0303 }; // === שכבת הרשומות (Record Layer) === function writeRecord(type, payload){ if (!context.transport) throw new Error('No transport attached to TLSSocket'); var rec = Buffer.allocUnsafe(5 + payload.length); rec.writeUInt8(type, 0); rec.writeUInt16BE(context.rec_version, 1); rec.writeUInt16BE(payload.length, 3); payload.copy(rec, 5); try { context.transport.write(rec); } catch(e){ ev.emit('error', e); } } function writeAppData(plain){ //console.log('...'); if(context.session.context.server_app_traffic_secret!==null){ if(context.app_write_key==null || context.app_write_iv==null){ var d=tls_derive_from_tls_secrets(context.session.context.server_app_traffic_secret,context.session.context.selected_cipher_suite); context.app_write_key=d.key; context.app_write_iv=d.iv; } }else{ //console.log('no key yet...'); } var enc1 = encrypt_tls_record(CT.APPLICATION_DATA,plain, context.app_write_key, get_nonce(context.app_write_iv,context.app_write_seq)); context.app_write_seq++; try { //console.log(enc1); writeRecord(CT.APPLICATION_DATA, Buffer.from(enc1)); } catch(e){ ev.emit('error', e); } } function processCiphertext(body){ var out=null; if(context.using_app_keys==true){ if(context.session.context.client_app_traffic_secret!==null){ if(context.app_read_key==null || context.app_read_iv==null){ var d=tls_derive_from_tls_secrets(context.session.context.client_app_traffic_secret,context.session.context.selected_cipher_suite); context.app_read_key=d.key; context.app_read_iv=d.iv; } out = decrypt_tls_record(body, context.app_read_key, get_nonce(context.app_read_iv,context.app_read_seq)); context.app_read_seq++; }else{ //... } }else{ if(context.session.context.client_handshake_traffic_secret!==null){ if(context.handshake_read_key==null || context.handshake_read_iv==null){ var d=tls_derive_from_tls_secrets(context.session.context.client_handshake_traffic_secret,context.session.context.selected_cipher_suite); context.handshake_read_key=d.key; context.handshake_read_iv=d.iv; } out = decrypt_tls_record(body, context.handshake_read_key, get_nonce(context.handshake_read_iv,context.handshake_read_seq)); context.handshake_read_seq++; }else{ //... } } if(out!==null){ var {content_type, content} = parse_tls_inner_plaintext(out); if (content_type === CT.HANDSHAKE || content_type === CT.ALERT) { //var cls = usingApp ? 2 : 1; // 1=handshake-keys, 2=app-keys try { context.session.message(new Uint8Array(content)); } catch(e){ ev.emit('error', e); } return; } if (content_type === CT.APPLICATION_DATA) { ev.emit('data', content); return; } if (content_type === CT.CHANGE_CIPHER_SPEC) { return; } } } function parseRecordsAndDispatch(){ while (context.readBuffer.length >= 5) { var type = context.readBuffer.readUInt8(0); var ver = context.readBuffer.readUInt16BE(1); var len = context.readBuffer.readUInt16BE(3); if (context.readBuffer.length < 5 + len) break; var body = context.readBuffer.slice(5, 5+len); context.readBuffer = context.readBuffer.slice(5+len); if (type === CT.APPLICATION_DATA) { try { processCiphertext(body); } catch(e){ ev.emit('error', e); } continue; } if (type === CT.HANDSHAKE || type === CT.ALERT || type === CT.CHANGE_CIPHER_SPEC) { try { context.session.message(new Uint8Array(body)); } catch(e){ ev.emit('error', e); } continue; } } } function bindTransport(){ if (!context.transport) return; context.transport.on('data', function(chunk){ context.readBuffer = Buffer.concat([context.readBuffer, chunk]); parseRecordsAndDispatch(); }); context.transport.on('error', function(err){ ev.emit('error', err); }); context.transport.on('close', function(){ ev.emit('close'); }); } context.session.on('message', function(epoch, seq, type, data){ var buf = toBuf(data || []); if (epoch === 0) { // ברור (ClientHello/ServerHello/CCS/Alert מוקדם) writeRecord(CT.HANDSHAKE, buf); return; } if (epoch === 1) { //need to create it... if(context.session.context.server_handshake_traffic_secret!==null){ if(context.handshake_write_key==null || context.handshake_write_iv==null){ var d=tls_derive_from_tls_secrets(context.session.context.server_handshake_traffic_secret,context.session.context.selected_cipher_suite); context.handshake_write_key=d.key; context.handshake_write_iv=d.iv; } var enc1 = encrypt_tls_record(CT.HANDSHAKE, buf, context.handshake_write_key, get_nonce(context.handshake_write_iv,context.handshake_write_seq)); context.handshake_write_seq++; try { //var enc1 = aeadEncrypt(context.handshake_write_key, context.handshake_write_iv, TLS_CIPHER_SUITES[context.session.context.selected_cipher_suite].cipher, context.handshake_write_seq, 0x0304, CT.HANDSHAKE, buf); //console.log(enc1); writeRecord(CT.APPLICATION_DATA, Buffer.from(enc1)); } catch(e){ ev.emit('error', e); } }else{ ev.emit('error', new Error('Missing handshake write keys')); } } if (epoch === 2) { // Post-Handshake מוצפן (inner_type=HANDSHAKE) תחת מפתחות Application if (!context.application_write) { ev.emit('error', new Error('Missing application write keys')); return; } try { var enc2 = aeadEncrypt(context.application_write, CT.HANDSHAKE, buf); writeRecord(CT.APPLICATION_DATA, enc2); } catch(e){ ev.emit('error', e); } return; } }); context.session.on('hello', function(info){ context.rec_version = 0x0303; // TLS 1.3 legacy record version context.session.set_context({ local_versions: [0x0304], local_alpns: ['http/1.1'], local_groups: [0x001d, 0x0017, 0x0018], local_cipher_suites: [ 0x1301, 0x1302, 0xC02F, // ECDHE_RSA_WITH_AES_128_GCM_SHA256 0xC030, // ECDHE_RSA_WITH_AES_256_GCM_SHA384 0xCCA8 // ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (אם מימשת) ], // ---- אלגוריתמי חתימה (TLS 1.2 → RSA-PKCS1, לא PSS) ---- // 0x0401 = rsa_pkcs1_sha256, 0x0501 = rsa_pkcs1_sha384, 0x0601 = rsa_pkcs1_sha512 local_signature_algorithms: [0x0401, 0x0501, 0x0601], // אופציונלי (לטובת חלק מהלקוחות): אותו דבר גם ל-signature_algorithms_cert local_signature_algorithms_cert: [0x0401, 0x0501, 0x0601], //local_cert_chain: [{ cert: new Uint8Array(cert.raw)}], //cert_private_key: new Uint8Array(private_key_der) }); }); context.session.on('secureConnect', function(){ context.using_app_keys=true; ev.emit('secureConnect'); }); // אם הועבר duplex בבנאי — להתחיל לקלוט if (context.transport) { bindTransport(); } // === API ציבורי (ללא חשיפת session) === var api = { on: function(name, fn){ ev.on(name, fn); }, setSocket: function(duplex2){ if (!duplex2 || typeof duplex2.write !== 'function') throw new Error('setSocket expects a Duplex-like stream'); context.transport = duplex2; bindTransport(); }, write: function(data){ if (context.destroyed) return false; var buf = toBuf(data); if (!context.using_app_keys) { context.appWriteQueue.push(buf); return true; } return writeAppData(buf); }, end: function(data){ if (context.destroyed) return; if (typeof data !== 'undefined' && data !== null) api.write(data); try { context.transport && context.transport.end && context.transport.end(); } catch(e){} }, destroy: function(){ if (context.destroyed) return; context.destroyed = true; try { context.transport && context.transport.destroy && context.transport.destroy(); } catch(e){} }, getCipher: function(){ var cs = context.session && context.session.context && context.session.context.selected_cipher_suite; return { name: cs || 'TLS_AES_128_GCM_SHA256', version: 'TLSv1.3' }; }, getPeerCertificate: function(){ return null; }, authorized: function(){ return true; } }; for (var k in api) if (Object.prototype.hasOwnProperty.call(api,k)) this[k] = api[k]; return this; } export default TLSSocket;