blinksocks/src/presets/ssr-auth-chain.js
2018-02-17 12:19:31 +08:00

478 lines
17 KiB
JavaScript

import crypto from 'crypto';
import Long from 'long';
import { IPreset } from './defs';
import {
hmac,
xor,
EVP_BytesToKey,
dumpHex,
getChunks,
getRandomInt,
getCurrentTimestampInt,
numberToBuffer as ntb, BYTE_ORDER_LE,
AdvancedBuffer,
} from '../utils';
const DEFAULT_SALT = 'auth_chain_a';
const MAX_TIME_DIFF = 30; // seconds
const NOOP = Buffer.alloc(0);
/**
* seedable random number generator implement in pure js
* @returns {{next: next, init: init}}
*/
export function xorshift128plus() {
const max_int = Long.fromString('18446744073709551615', true);
const mov_mask = Long.fromString('2199023255551', true);
let v0 = 0;
let v1 = 0;
return {
next: function next() {
let x = v0;
const y = v1;
v0 = y;
x = x.xor(x.and(mov_mask).shiftLeft(23));
x = x.xor(y.xor(x.shiftRightUnsigned(17)).xor(y.shiftRightUnsigned(26)).and(max_int));
v1 = x;
return x.add(y).and(max_int);
},
init_from_bin: function init(bin) {
const buf = Buffer.concat([bin, Buffer.alloc(16)]);
v0 = Long.fromBits(buf.readUInt32LE(0), buf.readUInt32LE(4), true);
v1 = Long.fromBits(buf.readUInt32LE(8), buf.readUInt32LE(12), true);
},
init_from_bin_datalen: function init_from_bin_datalen(bin, datalen) {
let buf = Buffer.concat([bin, Buffer.alloc(16)]);
buf = Buffer.concat([ntb(datalen, 2, BYTE_ORDER_LE), buf.slice(2)]);
v0 = Long.fromBits(buf.readUInt32LE(0), buf.readUInt32LE(4), true);
v1 = Long.fromBits(buf.readUInt32LE(8), buf.readUInt32LE(12), true);
[0, 0, 0, 0].forEach(() => void this.next());
}
};
}
/**
* @description
* shadowsocksr "auth_chain_xxx" implementation.
*
* @protocol
*
* # TCP handshake request (client -> server)
* +--------+--------+
* | part 1 | part 2 |
* +--------+--------+
* | 12 | 24 |
* +--------+--------+
*
* part 1
* +--------+----------+
* | Random | HMAC-MD5 |
* +--------+----------+
* | 4 | 8 |
* +--------+----------+
*
* part 2
* +-----+----------------------------+----------+
* | UID | AES-128-CBC encrypted data | HMAC-MD5 |
* +-----+----------------------------+----------+
* | 4 | 16 | 4 |
* +-----+----------------------------+----------+
*
* AES-128-CBC encrypted data
* +-----+-----+---------------+----------+---------+
* | UTC | CID | Connection ID | overhead | reserve |
* +-----+---------------------+----------+---------+
* | 4 | 4 | 4 | 2 | 2 |
* +-----+-----+---------------+----------+---------+
*
* # TCP chunks
* +------+--------------+---------+--------------+----------+
* | size | Random bytes | Payload | Random bytes | HMAC-MD5 |
* +------+--------------+---------+--------------+----------+
* | 2 | Variable | size | Variable | 2 |
* +------+--------------+---------+--------------+----------+
*
* # UDP (client -> server)
* +---------+--------------+--------+-----+----------+
* | Payload | Random bytes | Random | UID | HMAC-MD5 |
* +---------+--------------+--------+-----+----------+
* |Variable | Variable | 3 | 4 | 1 |
* +---------+--------------+--------+-----+----------+
*
* # UDP (server -> client)
* +---------+--------------+--------+----------+
* | Payload | Random bytes | Random | HMAC-MD5 |
* +---------+--------------+--------+----------+
* |Variable | Variable | 7 | 1 |
* +---------+--------------+--------+----------+
*
* @reference
* https://github.com/shadowsocksr-rm/shadowsocks-rss/blob/master/doc/auth_chain_a.md
*/
export default class SsrAuthChainPreset extends IPreset {
_clientId = null;
_connectionId = null;
_userKey = null;
_salt = DEFAULT_SALT; // overwrite by subclass
_isHeaderSent = false;
_isHeaderRecv = false;
// chaining hash
_lastClientHash = null;
_lastServerHash = null;
// xorshift128plus pseudo-random number generator
_rngClient = null;
_rngServer = null;
// chunk counters
_encodeChunkId = 1;
_decodeChunkId = 1;
// chunk payload cipher/decipher
_cipher = null;
_decipher = null;
_tcpMss = 1460;
_overhead = 4;
_adBuf = null;
onInit() {
this._clientId = crypto.randomBytes(4);
this._connectionId = getRandomInt(0, 0x00ffffff);
this._rngClient = xorshift128plus();
this._rngServer = xorshift128plus();
this._adBuf = new AdvancedBuffer({ getPacketLength: this.onReceiving.bind(this) });
this._adBuf.on('data', this.onChunkReceived.bind(this));
}
onDestroy() {
this._userKey = null;
this._lastClientHash = null;
this._lastServerHash = null;
this._rngClient = null;
this._rngServer = null;
this._cipher = null;
this._decipher = null;
this._adBuf.clear();
this._adBuf = null;
}
// implement in subclasses
getRandomBytesLengthForTcp(/* seed, base, rng */) {
return 0;
}
getRandomBytesLengthForUdp(seed, rng) {
rng.init_from_bin(seed);
return rng.next().mod(127).toNumber();
}
createRequest() {
const clientId = this._clientId;
const connectionId = this._connectionId;
const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key');
const iv = this.readProperty('ss-stream-cipher', 'iv');
const part1_hmac_key = Buffer.concat([iv, userKey]);
// part 1
const random = crypto.randomBytes(4);
this._lastClientHash = hmac('md5', part1_hmac_key, random);
const random_hmac = this._lastClientHash.slice(0, 8);
const part1 = Buffer.concat([random, random_hmac]);
// part 2
const uid = xor(crypto.randomBytes(4), this._lastClientHash.slice(8, 12));
// prepare input data for part2 encryption
const utc = ntb(getCurrentTimestampInt(), 4, BYTE_ORDER_LE);
let client_id = clientId;
let connection_id = connectionId;
if (connectionId > 0xff000000) {
connection_id = getRandomInt(0, 0x00ffffff);
client_id = crypto.randomBytes(4);
this._connectionId = connection_id;
} else {
connection_id = ++this._connectionId;
}
const overhead = ntb(this._overhead, 2, BYTE_ORDER_LE);
const reserve = Buffer.alloc(2);
const header = Buffer.concat([utc, client_id, ntb(connection_id, 4, BYTE_ORDER_LE), overhead, reserve]);
// prepare cipher for part 2 encryption
const cipher_key = EVP_BytesToKey(userKey.toString('base64') + this._salt, 16, 16);
const cipher = crypto.createCipheriv('aes-128-cbc', cipher_key, Buffer.alloc(16));
const cbc_enc_header = cipher.update(header);
let part2 = Buffer.concat([uid, cbc_enc_header]);
this._lastServerHash = hmac('md5', userKey, part2);
const part2_hmac = this._lastServerHash.slice(0, 4);
part2 = Buffer.concat([part2, part2_hmac]);
// initialize data cipher
const data_cipher_key = EVP_BytesToKey(Buffer.from(userKey.toString('base64') + this._lastClientHash.toString('base64')), 16, 16);
this._cipher = crypto.createCipheriv('rc4', data_cipher_key, NOOP);
this._decipher = crypto.createDecipheriv('rc4', data_cipher_key, NOOP);
return Buffer.concat([part1, part2]);
}
createChunks(buffer) {
const userKey = this._userKey;
const max_payload_size = this._config.is_client ? 2800 : (this._tcpMss - this._overhead);
return getChunks(buffer, max_payload_size).map((payload) => {
let _payload = payload;
if (this._config.is_server && this._encodeChunkId === 1) {
_payload = Buffer.concat([ntb(this._tcpMss, 2, BYTE_ORDER_LE), payload]);
}
const rc4_enc_payload = this._cipher.update(_payload);
const hash = this._config.is_client ? this._lastClientHash : this._lastServerHash;
const size = rc4_enc_payload.length ^ hash.slice(-2).readUInt16LE(0);
// generate two pieces of random bytes
const rng = this._config.is_client ? this._rngClient : this._rngServer;
const random_bytes_len = this.getRandomBytesLengthForTcp(hash, _payload.length, rng);
const random_bytes = crypto.randomBytes(random_bytes_len);
const random_divide_pos = random_bytes_len > 0 ? rng.next().mod(8589934609).mod(random_bytes_len).toNumber() : 0;
const random_bytes_a = random_bytes.slice(0, random_divide_pos);
const random_bytes_b = random_bytes.slice(random_divide_pos);
// assemble chunk
let chunk = Buffer.concat([ntb(size, 2, BYTE_ORDER_LE), random_bytes_a, rc4_enc_payload, random_bytes_b]);
const hmac_key = Buffer.concat([userKey, ntb(this._encodeChunkId, 4, BYTE_ORDER_LE)]);
const chunk_hmac = hmac('md5', hmac_key, chunk);
chunk = Buffer.concat([chunk, chunk_hmac.slice(0, 2)]);
if (this._config.is_client) {
this._lastClientHash = chunk_hmac;
} else {
this._lastServerHash = chunk_hmac;
}
this._encodeChunkId += 1;
return chunk;
});
}
// tcp
clientOut({ buffer }) {
if (!this._isHeaderSent) {
this._isHeaderSent = true;
return Buffer.concat([this.createRequest(), Buffer.concat(this.createChunks(buffer))]);
} else {
return Buffer.concat(this.createChunks(buffer));
}
}
serverOut({ buffer }) {
return Buffer.concat(this.createChunks(buffer));
}
serverIn({ buffer, next, fail }) {
if (!this._isHeaderRecv) {
if (buffer.length < 36) {
return fail(`handshake request is too short to parse, request=${dumpHex(buffer)}`);
}
const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key');
const iv = this.readProperty('ss-stream-cipher', 'iv');
const part12_hmac_key = Buffer.concat([iv, userKey]);
// part 1
const part1_random = buffer.slice(0, 4);
const part1_hmac = buffer.slice(4, 12);
const part1_hmac_calc = hmac('md5', part12_hmac_key, part1_random);
if (!part1_hmac_calc.slice(0, 8).equals(part1_hmac)) {
return fail(`unexpected hmac in part 1, dump=${dumpHex(buffer)}`);
}
this._lastClientHash = part1_hmac_calc;
// part 2
const part2_hmac = buffer.slice(32, 36);
const part2_hmac_calc = hmac('md5', userKey, buffer.slice(12, 32));
if (!part2_hmac_calc.slice(0, 4).equals(part2_hmac)) {
return fail(`unexpected hmac in part 2, dump=${dumpHex(buffer)}`);
}
this._lastServerHash = part2_hmac_calc;
// const uid = xor(buffer.slice(12, 16), part1_hmac.slice(8, 12));
const cbc_enc_header = buffer.slice(16, 32);
const decipher_key = EVP_BytesToKey(userKey.toString('base64') + this._salt, 16, 16);
const decipher = crypto.createDecipheriv('aes-128-cbc', decipher_key, Buffer.alloc(16));
const header = decipher.update(Buffer.concat([
cbc_enc_header,
Buffer.alloc(1) // we need one more byte to get plaintext from the second block
]));
const utc = header.slice(0, 4);
const time_diff = Math.abs(utc.readUInt32LE(0) - getCurrentTimestampInt());
if (time_diff > MAX_TIME_DIFF) {
return fail(`timestamp diff is over ${MAX_TIME_DIFF}s, dump=${dumpHex(buffer)}`);
}
// NOTE: blinksocks's implementation doesn't support multiple user, so client_id and connection_id are useless.
// const client_id = header.slice(4, 8);
// const connection_id = header.slice(8, 12);
this._overhead = header.slice(12, 14).readUInt16LE(0);
// initialize data cipher
const data_cipher_key = EVP_BytesToKey(Buffer.from(userKey.toString('base64') + this._lastClientHash.toString('base64')), 16, 16);
this._cipher = crypto.createCipheriv('rc4', data_cipher_key, NOOP);
this._decipher = crypto.createDecipheriv('rc4', data_cipher_key, NOOP);
this._isHeaderRecv = true;
if (buffer.length > 36) {
this._adBuf.put(buffer.slice(36), { next, fail });
}
} else {
this._adBuf.put(buffer, { next, fail });
}
}
clientIn({ buffer, next, fail }) {
this._adBuf.put(buffer, { next, fail });
}
onReceiving(buffer, { fail }) {
if (buffer.length < 2 || this._adBuf === null) {
return; // too short to get size
}
const hash = this._config.is_client ? this._lastServerHash : this._lastClientHash;
const payload_len = buffer.readUInt16LE(0) ^ hash.readUInt16LE(14);
const rng = this._config.is_client ? this._rngServer : this._rngClient;
const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng);
const chunk_size = 2 + random_bytes_len + payload_len + 2;
if (chunk_size >= 4096) {
fail(`invalid chunk, chunk size=${chunk_size} is greater than 4096, dump=${dumpHex(buffer)}`);
return -1;
}
return chunk_size;
}
onChunkReceived(chunk, { next, fail }) {
const userKey = this._userKey;
if (chunk.length < 2) {
return fail(`invalid chunk, size=${chunk.length} dump=${dumpHex(chunk)}`);
}
// check chunk
const hmac_key = Buffer.concat([userKey, ntb(this._decodeChunkId, 4, BYTE_ORDER_LE)]);
const new_hash = hmac('md5', hmac_key, chunk.slice(0, -2));
const chunk_hmac_calc = new_hash.slice(0, 2);
const chunk_hmac = chunk.slice(-2);
if (!chunk_hmac_calc.equals(chunk_hmac)) {
return fail(`unexpected chunk hmac, chunk=${dumpHex(chunk)}`);
}
// drop random_bytes, get encrypted payload
const hash = this._config.is_client ? this._lastServerHash : this._lastClientHash;
const payload_len = chunk.readUInt16LE(0) ^ hash.readUInt16LE(14);
const rng = this._config.is_client ? this._rngServer : this._rngClient;
const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng);
let enc_payload = null;
if (random_bytes_len > 0) {
const random_divide_pos = rng.next().mod(8589934609).mod(random_bytes_len).toNumber();
enc_payload = chunk.slice(2 + random_divide_pos, 2 + random_divide_pos + payload_len);
} else {
enc_payload = chunk.slice(2, 2 + payload_len);
}
// decrypt payload
let payload = this._decipher.update(enc_payload);
// update hash
if (this._config.is_client) {
this._lastServerHash = new_hash;
} else {
this._lastClientHash = new_hash;
}
if (this._config.is_client && this._decodeChunkId === 1) {
this._tcpMss = payload.readUInt16LE(0);
payload = payload.slice(2);
}
this._decodeChunkId += 1;
next(payload);
}
// udp
clientOutUdp({ buffer }) {
const userKey = this.readProperty('ss-stream-cipher', 'key');
const random = crypto.randomBytes(3);
const tmp_mac = hmac('md5', userKey, random);
const uid = xor(crypto.randomBytes(4), tmp_mac.slice(0, 4));
const random_bytes = crypto.randomBytes(this.getRandomBytesLengthForUdp(tmp_mac, this._rngClient));
const cipher_key = EVP_BytesToKey(Buffer.from(userKey.toString('base64') + tmp_mac.toString('base64')), 16, 16);
const cipher = crypto.createCipheriv('rc4', cipher_key, NOOP);
const enc_payload = cipher.update(buffer);
const packet = Buffer.concat([enc_payload, random_bytes, random, uid]);
const packet_hmac = hmac('md5', userKey, packet).slice(0, 1);
return Buffer.concat([packet, packet_hmac]);
}
serverInUdp({ buffer, fail }) {
const userKey = this.readProperty('ss-stream-cipher', 'key');
const packet = buffer.slice(0, -1);
const packet_hmac = buffer.slice(-1);
const packet_hmac_calc = hmac('md5', userKey, packet).slice(0, 1);
if (!packet_hmac_calc.equals(packet_hmac)) {
return fail(`unexpected hmac when verify client udp packet, dump=${dumpHex(buffer)}`);
}
const random = buffer.slice(-8, -5);
const tmp_mac = hmac('md5', userKey, random);
// const uid = xor(buffer.slice(-5, -1), tmp_mac.slice(0, 4));
const random_bytes_len = this.getRandomBytesLengthForUdp(tmp_mac, this._rngClient);
const decipher_key = EVP_BytesToKey(Buffer.from(userKey.toString('base64') + tmp_mac.toString('base64')), 16, 16);
const decipher = crypto.createDecipheriv('rc4', decipher_key, NOOP);
const enc_payload = buffer.slice(0, -8 - random_bytes_len);
return decipher.update(enc_payload);
}
serverOutUdp({ buffer }) {
const userKey = this.readProperty('ss-stream-cipher', 'key');
const random = crypto.randomBytes(7);
const tmp_mac = hmac('md5', userKey, random);
const random_bytes = crypto.randomBytes(this.getRandomBytesLengthForUdp(tmp_mac, this._rngServer));
const cipher_key = EVP_BytesToKey(Buffer.from(userKey.toString('base64') + tmp_mac.toString('base64')), 16, 16);
const cipher = crypto.createCipheriv('rc4', cipher_key, NOOP);
const enc_payload = cipher.update(buffer);
const packet = Buffer.concat([enc_payload, random_bytes, random]);
const packet_hmac = hmac('md5', userKey, packet).slice(0, 1);
return Buffer.concat([packet, packet_hmac]);
}
clientInUdp({ buffer, fail }) {
const userKey = this.readProperty('ss-stream-cipher', 'key');
const packet = buffer.slice(0, -1);
const packet_hmac = buffer.slice(-1);
const packet_hmac_calc = hmac('md5', userKey, packet).slice(0, 1);
if (!packet_hmac_calc.equals(packet_hmac)) {
return fail(`unexpected hmac when verify server udp packet, dump=${dumpHex(buffer)}`);
}
const random = buffer.slice(-8, -1);
const tmp_mac = hmac('md5', userKey, random);
const random_bytes_len = this.getRandomBytesLengthForUdp(tmp_mac, this._rngServer);
const decipher_key = EVP_BytesToKey(Buffer.from(userKey.toString('base64') + tmp_mac.toString('base64')), 16, 16);
const decipher = crypto.createDecipheriv('rc4', decipher_key, NOOP);
const enc_payload = buffer.slice(0, -8 - random_bytes_len);
return decipher.update(enc_payload);
}
}