import crypto from 'crypto'; import { IPreset } from './defs'; import { numberToBuffer, getCurrentTimestampInt, getRandomInt, getRandomChunks, AdvancedBuffer } from '../utils'; const TLS_STAGE_HELLO = 1; const TLS_STAGE_CHANGE_CIPHER_SPEC = 2; const TLS_STAGE_APPLICATION_DATA = 3; const MIN_AD_PAYLOAD_LEN = 0x0800; const MAX_AD_PAYLOAD_LEN = 0x3FFF; /** * convert string to buffer * @param str * @returns {Buffer} */ function stb(str) { return Buffer.from(str, 'hex'); } /** * return UTC timestamp as buffer * @returns {Buffer} */ function getUTC() { return numberToBuffer(getCurrentTimestampInt(), 4); } /** * wrap buffer to Application Data * @param buffer * @returns {Buffer} * @constructor */ function ApplicationData(buffer) { const len = numberToBuffer(buffer.length); return Buffer.concat([stb('170303'), len, buffer]); } /** * @description * Do TLS handshake using SessionTicket TLS mechanism, transfer data inside of Application Data. * * @params * sni: Server Name Indication. * * @examples * { * "name": "obfs-tls1.2-ticket", * "params": { * "sni": ["www.bing.com"] * } * } * * @protocol * C ---- Client Hello ---> S * C <--- Server Hello, New Session Ticket, Change Cipher Spec, Finished --- S * C ---- Change Cipher Spec, Finished, Application Data, Application Data, ... ---> S * C <--- Application Data, Application Data, ... ---> S * * @reference * [1] SNI * https://en.wikipedia.org/wiki/Server_Name_Indication */ export default class ObfsTls12TicketPreset extends IPreset { _sni = []; _stage = TLS_STAGE_HELLO; _staging = Buffer.alloc(0); _adBuf = null; static onCheckParams({ sni }) { if (typeof sni === 'undefined') { throw Error('\'sni\' must be set'); } if (!Array.isArray(sni)) { sni = [sni]; } if (sni.some((s) => typeof s !== 'string' || s.length < 1)) { throw Error('\'sni\' must be a non-empty string or an array without empty strings'); } } onInit({ sni }) { this._sni = Array.isArray(sni) ? sni : [sni]; this._adBuf = new AdvancedBuffer({ getPacketLength: this.onReceiving.bind(this) }); this._adBuf.on('data', this.onChunkReceived.bind(this)); } onDestroy() { this._adBuf.clear(); this._adBuf = null; this._staging = null; this._sni = null; } getRandomSNI() { const index = crypto.randomBytes(1)[0] % this._sni.length; return Buffer.from(this._sni[index]); } clientOut({ buffer, next }) { if (this._stage === TLS_STAGE_HELLO) { this._stage = TLS_STAGE_CHANGE_CIPHER_SPEC; this._staging = buffer; // Send Client Hello const sni = this.getRandomSNI(); // Random const random = [ ...getUTC(), // GMT Unix Time ...crypto.randomBytes(28), // Random Bytes ]; // Session const session = [ ...stb('20'), // Session ID Length ...crypto.randomBytes(0x20), // Session ID ]; // Cipher Suites const cipher_suites = [ ...stb('001a'), // Cipher Suites Length ...stb('c02b'), // Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) ...stb('c02f'), // Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) ...stb('c02c'), // Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) ...stb('c030'), // Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) ...stb('cc14'), // Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc14) ...stb('cc13'), // Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc13) ...stb('c013'), // Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013) ...stb('c014'), // Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014) ...stb('009c'), // Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) ...stb('009d'), // Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) ...stb('002f'), // Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) ...stb('0035'), // Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) ...stb('000a'), // Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a) ]; // Extension: server_name const ext_server_name = [ ...stb('0000'), // Type: server_name ...numberToBuffer(2 + 1 + 2 + sni.length), // Length ...numberToBuffer(1 + 2 + sni.length), // Server Name List length ...stb('00'), // Server Name Type: host_name(0) ...numberToBuffer(sni.length), // Server Name length ...sni, // Server Name ]; // Extension: SessionTicket TLS const ticketLen = getRandomInt(200, 400); const session_ticket = [ ...stb('0023'), // Type: SessionTicket TLS ...numberToBuffer(ticketLen), // Length ...crypto.randomBytes(ticketLen), // Data ]; // Extensions const exts = [ ...stb('ff01000100'), // Extension: renegotiation_info ...ext_server_name, // Extension: server_name ...stb('00170000'), // Extension: Extended Master Secret ...session_ticket, // Extension: SessionTicket TLS ...stb('000d00140012040308040401050308050501080606010201'), // Extension: signature_algorithms ...stb('000500050100000000'), // Extension: status_request ...stb('00120000'), // Extension: signed_certificate_timestamp ...stb('75500000'), // Extension: channel_id ...stb('000b00020100'), // Extension: ec_point_formats ...stb('000a0006000400170018') // Extension: elliptic_curves ]; const body = [ ...stb('0303'), // Version: TLS 1.2 ...random, // Random ...session, // Session ...cipher_suites, // Cipher Suites ...stb('01'), // Compression Methods Length ...stb('00'), // Compression Methods = [null] ...numberToBuffer(exts.length), // Extension Length ...exts // Extensions ]; const header = [ ...stb('16'), // Content Type: Handshake ...stb('0301'), // Version: TLS 1.0 ...numberToBuffer(1 + 3 + body.length), // Length ...stb('01'), // Handshake Type: ClientHello ...numberToBuffer(body.length, 3) // Length ]; return next(Buffer.from([...header, ...body])); } if (this._stage === TLS_STAGE_CHANGE_CIPHER_SPEC) { this._staging = Buffer.concat([this._staging, buffer]); } if (this._stage === TLS_STAGE_APPLICATION_DATA) { // Send Application Data const chunks = getRandomChunks(buffer, MIN_AD_PAYLOAD_LEN, MAX_AD_PAYLOAD_LEN) .map((chunk) => ApplicationData(chunk)); return Buffer.concat(chunks); } } serverIn({ buffer, next, fail }) { if (this._stage === TLS_STAGE_HELLO) { this._stage = TLS_STAGE_CHANGE_CIPHER_SPEC; // 1. Check Client Hello if (buffer.length < 200) { fail(`TLS handshake header is too short, length=${buffer.length} dump=${buffer.slice(0, 100).toString('hex')}`); return; } if (!buffer.slice(0, 3).equals(stb('160301'))) { fail(`invalid TLS handshake header=${buffer.slice(0, 3).toString('hex')}, want=160301, dump=${buffer.slice(0, 100).toString('hex')}`); return; } const tlsLen = buffer.slice(3, 5).readUInt16BE(0); if (tlsLen !== buffer.length - 5) { fail(`unexpected TLS handshake body length=${buffer.length - 5}, want=${tlsLen}, dump=${buffer.slice(0, 100).toString('hex')}`); return; } // 2. Send Server Hello, New Session Ticket, Change Cipher Spec, Finished // [Server Hello] // Random const random = [ ...getUTC(), // GMT Unix Time ...crypto.randomBytes(28), // Random Bytes ]; // Session const session = [ ...stb('20'), // Session ID Length ...crypto.randomBytes(0x20), // Session ID ]; // Extensions const exts = [ ...stb('ff01000100'), // Extension: renegotiation_info ...stb('00050000'), // Extension: status_request ...stb('00170000') // Extension: Extended Master Secret ]; const body = [ ...stb('0303'), // Version: TLS 1.2 ...random, // Random ...session, // Session ...stb('c02f'), // Cipher Suite ...stb('00'), // Compression Method ...numberToBuffer(exts.length), // Extension Length ...exts // Extensions ]; const header = [ ...stb('16'), // Content Type: Handshake ...stb('0303'), // Version: TLS 1.2 ...numberToBuffer(1 + 3 + body.length), // Length ...stb('02'), // Handshake Type: Server Hello ...numberToBuffer(body.length, 3) // Length ]; const server_hello = [...header, ...body]; // [New Session Ticket] const ticket = crypto.randomBytes(getRandomInt(200, 255)); const session_ticket = [ ...stb('000004b0'), // Session Ticket Lifetime Hint: 1200 sec, 32-bit unsigned integer in network byte order ...numberToBuffer(ticket.length), // Session Ticket Length ...ticket // Session Ticket ]; const new_session_ticket_body = [ ...stb('04'), // New Session Ticket ...numberToBuffer(session_ticket.length, 3), // New Session Ticket Length, 3 bytes ...session_ticket ]; const new_session_ticket = [ ...stb('160303'), ...numberToBuffer(new_session_ticket_body.length), // Length ...new_session_ticket_body ]; // [Change Cipher Spec] const change_cipher_spec = [ ...stb('140303000101') ]; // [Finished] const finishedLen = getRandomInt(32, 40); const finished = [ ...stb('16'), // Content Type: Handshake ...stb('0303'), // Version: TLS 1.2 ...numberToBuffer(finishedLen), // Length ...crypto.randomBytes(finishedLen) ]; return next(Buffer.from([...server_hello, ...new_session_ticket, ...change_cipher_spec, ...finished]), true); } let _buffer = buffer; if (this._stage === TLS_STAGE_CHANGE_CIPHER_SPEC) { this._stage = TLS_STAGE_APPLICATION_DATA; // TODO: 1. Check Client Change Cipher Spec // 2. Drop Client Change Cipher Spec _buffer = buffer.slice(43); } this._adBuf.put(_buffer, { next, fail }); } serverOut({ buffer }) { // Send Application Data const chunks = getRandomChunks(buffer, MIN_AD_PAYLOAD_LEN, MAX_AD_PAYLOAD_LEN) .map((chunk) => ApplicationData(chunk)); return Buffer.concat(chunks); } clientIn({ buffer, next, fail }) { if (this._stage === TLS_STAGE_CHANGE_CIPHER_SPEC) { this._stage = TLS_STAGE_APPLICATION_DATA; // TODO: 1. Check Server Hello // 2. Send Change Cipher Spec(43 bytes fixed) and Pending Data // Change Cipher Spec const change_cipher_spec = [ ...stb('140303000101') ]; // Finished const finished = [ ...stb('16'), // Content Type: Handshake ...stb('0303'), // Version: TLS 1.2 ...stb('0020'), // Length: 32 ...crypto.randomBytes(0x20), ]; // Application Data const chunks = getRandomChunks(this._staging, MIN_AD_PAYLOAD_LEN, MAX_AD_PAYLOAD_LEN) .map((chunk) => ApplicationData(chunk)); this._staging = null; return next(Buffer.from([...change_cipher_spec, ...finished, ...Buffer.concat(chunks)]), true); } this._adBuf.put(buffer, { next, fail }); } onReceiving(buffer) { if (buffer.length < 5) { // fail(`Application Data is too short: ${buffer.length} bytes, ${buffer.toString('hex')}`); return; } return 5 + buffer.readUInt16BE(3); } onChunkReceived(chunk, { next }) { // Drop TLS Application Data header next(chunk.slice(5)); } }