blinksocks/src/presets/obfs-tls1.2-ticket.js
2018-02-17 12:19:31 +08:00

368 lines
13 KiB
JavaScript

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));
}
}