2017-10-20 02:56:03 +00:00
|
|
|
import crypto from 'crypto';
|
2018-05-05 07:51:13 +00:00
|
|
|
import { EVP_BytesToKey, numberToBuffer, hmac, hash, dumpHex } from '../utils';
|
2018-02-17 04:19:31 +00:00
|
|
|
import { IPresetAddressing } from './defs';
|
2017-10-20 02:56:03 +00:00
|
|
|
|
|
|
|
// available HMACs and length
|
|
|
|
const HMAC_METHODS = {
|
2018-02-17 04:19:31 +00:00
|
|
|
'md5': 16, 'sha1': 20, 'sha256': 32,
|
2017-10-20 02:56:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const DEFAULT_HASH_METHOD = 'sha1';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description
|
|
|
|
* Deliver destination address and verify it using HMAC as well.
|
|
|
|
*
|
|
|
|
* @params
|
|
|
|
* method: The hash algorithm for HMAC, default is "sha1".
|
|
|
|
*
|
|
|
|
* @examples
|
|
|
|
* {
|
|
|
|
* "name": "base-auth",
|
|
|
|
* "params": {
|
|
|
|
* "method": "sha1"
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @protocol
|
|
|
|
*
|
2017-10-26 14:28:47 +00:00
|
|
|
* # TCP stream (client -> server)
|
2017-10-20 02:56:03 +00:00
|
|
|
* +------+----------+----------+----------+----------+---------+
|
|
|
|
* | ALEN | DST.ADDR | DST.PORT | HMAC | DATA | ... |
|
|
|
|
* +------+----------+----------+----------+----------+---------+
|
|
|
|
* | 1 | Variable | 2 | Fixed | Variable | ... |
|
|
|
|
* +------+----------+----------+----------+----------+---------+
|
|
|
|
* |<------ aes-128-cfb ------->|
|
|
|
|
*
|
2017-10-26 14:28:47 +00:00
|
|
|
* # UDP packet (client -> server)
|
|
|
|
* +------+----------+----------+----------+----------+
|
|
|
|
* | ALEN | DST.ADDR | DST.PORT | HMAC | DATA |
|
|
|
|
* +------+----------+----------+----------+----------+
|
|
|
|
* | 1 | Variable | 2 | Fixed | Variable |
|
|
|
|
* +------+----------+----------+----------+----------+
|
|
|
|
*
|
|
|
|
* # any others of TCP and UDP
|
2017-10-20 02:56:03 +00:00
|
|
|
* +----------+
|
|
|
|
* | DATA |
|
|
|
|
* +----------+
|
|
|
|
* | Variable |
|
|
|
|
* +----------+
|
|
|
|
*
|
|
|
|
* @explain
|
|
|
|
* 1. ALEN = len(DST.ADDR).
|
|
|
|
* 2. HMAC = HMAC(AES-128-CFB(ALEN + DST.ADDR + DST.PORT)).
|
|
|
|
* 3. IV for encryption is md5(user_key + 'base-auth').
|
|
|
|
* 4. key for encryption and HMAC are derived from EVP_BytesToKey.
|
|
|
|
*/
|
2017-12-25 08:42:01 +00:00
|
|
|
export default class BaseAuthPreset extends IPresetAddressing {
|
2017-10-20 02:56:03 +00:00
|
|
|
|
2018-02-15 03:01:24 +00:00
|
|
|
_hmacMethod = DEFAULT_HASH_METHOD;
|
2017-10-20 02:56:03 +00:00
|
|
|
|
2018-02-15 03:01:24 +00:00
|
|
|
_hmacLen = null;
|
2017-10-20 02:56:03 +00:00
|
|
|
|
2018-02-15 03:01:24 +00:00
|
|
|
_hmacKey = null;
|
2017-10-20 02:56:03 +00:00
|
|
|
|
2017-10-20 03:03:02 +00:00
|
|
|
_cipher = null;
|
2017-10-20 02:56:03 +00:00
|
|
|
|
2017-10-20 03:03:02 +00:00
|
|
|
_decipher = null;
|
2017-10-20 02:56:03 +00:00
|
|
|
|
|
|
|
_isConnecting = false;
|
|
|
|
|
|
|
|
_isHeaderSent = false;
|
|
|
|
|
|
|
|
_isHeaderRecv = false;
|
|
|
|
|
|
|
|
_pending = Buffer.alloc(0);
|
|
|
|
|
|
|
|
_host = null; // buffer
|
|
|
|
|
|
|
|
_port = null; // buffer
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
static onCheckParams({ method = DEFAULT_HASH_METHOD }) {
|
2017-10-20 02:56:03 +00:00
|
|
|
const methods = Object.keys(HMAC_METHODS);
|
|
|
|
if (!methods.includes(method)) {
|
|
|
|
throw Error(`base-auth 'method' must be one of [${methods}]`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
onInit({ method = DEFAULT_HASH_METHOD }) {
|
2018-02-15 03:01:24 +00:00
|
|
|
const key = EVP_BytesToKey(this._config.key, 16, 16);
|
|
|
|
const iv = hash('md5', Buffer.from(this._config.key + 'base-auth'));
|
|
|
|
this._hmacMethod = method;
|
|
|
|
this._hmacLen = HMAC_METHODS[method];
|
|
|
|
this._hmacKey = key;
|
|
|
|
if (this._config.is_client) {
|
2017-10-20 03:03:02 +00:00
|
|
|
this._cipher = crypto.createCipheriv('aes-128-cfb', key, iv);
|
2017-10-20 02:56:03 +00:00
|
|
|
} else {
|
2017-10-20 03:03:02 +00:00
|
|
|
this._decipher = crypto.createDecipheriv('aes-128-cfb', key, iv);
|
2017-10-20 02:56:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-15 11:59:39 +00:00
|
|
|
onInitTargetAddress({ host, port }) {
|
|
|
|
this._host = Buffer.from(host);
|
|
|
|
this._port = numberToBuffer(port);
|
|
|
|
}
|
|
|
|
|
2017-10-20 02:56:03 +00:00
|
|
|
onDestroy() {
|
2017-10-20 03:03:02 +00:00
|
|
|
this._cipher = null;
|
|
|
|
this._decipher = null;
|
2017-10-20 02:56:03 +00:00
|
|
|
this._pending = null;
|
|
|
|
this._host = null;
|
|
|
|
this._port = null;
|
|
|
|
}
|
|
|
|
|
2017-10-26 09:59:09 +00:00
|
|
|
encodeHeader() {
|
|
|
|
const header = Buffer.concat([numberToBuffer(this._host.length, 1), this._host, this._port]);
|
|
|
|
const encHeader = this._cipher.update(header);
|
2018-02-15 03:01:24 +00:00
|
|
|
const mac = hmac(this._hmacMethod, this._hmacKey, encHeader);
|
2017-10-26 09:59:09 +00:00
|
|
|
return Buffer.concat([encHeader, mac]);
|
|
|
|
}
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
decodeHeader({ buffer, fail }) {
|
2018-02-15 03:01:24 +00:00
|
|
|
const hmacLen = this._hmacLen;
|
2017-10-26 09:59:09 +00:00
|
|
|
|
|
|
|
// minimal length required
|
|
|
|
if (buffer.length < 31) {
|
2018-05-05 07:51:13 +00:00
|
|
|
return fail(`length is too short: ${buffer.length}, dump=${dumpHex(buffer)}`);
|
2017-10-26 09:59:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// decrypt the first byte and check length overflow
|
|
|
|
const alen = this._decipher.update(buffer.slice(0, 1))[0];
|
|
|
|
if (buffer.length <= 1 + alen + 2 + hmacLen) {
|
2018-05-05 07:51:13 +00:00
|
|
|
return fail(`unexpected length: ${buffer.length}, dump=${dumpHex(buffer)}`);
|
2017-10-26 09:59:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// check hmac
|
|
|
|
const givenHmac = buffer.slice(1 + alen + 2, 1 + alen + 2 + hmacLen);
|
2018-02-15 03:01:24 +00:00
|
|
|
const expHmac = hmac(this._hmacMethod, this._hmacKey, buffer.slice(0, 1 + alen + 2));
|
2017-10-26 09:59:09 +00:00
|
|
|
if (!givenHmac.equals(expHmac)) {
|
2018-05-05 07:51:13 +00:00
|
|
|
return fail(`unexpected HMAC=${dumpHex(givenHmac)} want=${dumpHex(expHmac)} dump=${dumpHex(buffer)}`);
|
2017-10-26 09:59:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// decrypt the following bytes
|
|
|
|
const plaintext = this._decipher.update(buffer.slice(1, 1 + alen + 2));
|
|
|
|
|
|
|
|
// addr, port, data
|
|
|
|
const addr = plaintext.slice(0, alen).toString();
|
|
|
|
const port = plaintext.slice(alen, alen + 2).readUInt16BE(0);
|
|
|
|
const data = buffer.slice(1 + alen + 2 + hmacLen);
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
return { host: addr, port, data };
|
2017-10-26 09:59:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// tcp
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
clientOut({ buffer }) {
|
2017-10-20 02:56:03 +00:00
|
|
|
if (!this._isHeaderSent) {
|
|
|
|
this._isHeaderSent = true;
|
2017-10-26 09:59:09 +00:00
|
|
|
return Buffer.concat([this.encodeHeader(), buffer]);
|
2017-10-20 02:56:03 +00:00
|
|
|
} else {
|
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-15 12:34:59 +00:00
|
|
|
serverIn({ buffer, next, fail }) {
|
2017-10-20 02:56:03 +00:00
|
|
|
if (!this._isHeaderRecv) {
|
|
|
|
|
|
|
|
if (this._isConnecting) {
|
|
|
|
this._pending = Buffer.concat([this._pending, buffer]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
const decoded = this.decodeHeader({ buffer, fail });
|
2017-10-26 09:59:09 +00:00
|
|
|
if (!decoded) {
|
|
|
|
return;
|
2017-10-20 02:56:03 +00:00
|
|
|
}
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
const { host, port, data } = decoded;
|
2017-10-20 02:56:03 +00:00
|
|
|
|
|
|
|
// notify to connect to the real server
|
|
|
|
this._isConnecting = true;
|
2018-04-15 12:34:59 +00:00
|
|
|
this.resolveTargetAddress({ host, port }, () => {
|
|
|
|
next(Buffer.concat([data, this._pending]));
|
|
|
|
this._isHeaderRecv = true;
|
|
|
|
this._isConnecting = false;
|
|
|
|
this._pending = null;
|
2017-10-20 02:56:03 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-26 09:59:09 +00:00
|
|
|
// udp
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
clientOutUdp({ buffer }) {
|
2017-10-26 09:59:09 +00:00
|
|
|
return Buffer.concat([this.encodeHeader(), buffer]);
|
|
|
|
}
|
|
|
|
|
2018-04-15 12:34:59 +00:00
|
|
|
serverInUdp({ buffer, next, fail }) {
|
2018-02-17 04:19:31 +00:00
|
|
|
const decoded = this.decodeHeader({ buffer, fail });
|
2017-10-26 09:59:09 +00:00
|
|
|
if (!decoded) {
|
|
|
|
return;
|
|
|
|
}
|
2018-02-17 04:19:31 +00:00
|
|
|
const { host, port, data } = decoded;
|
2018-04-15 12:34:59 +00:00
|
|
|
this.resolveTargetAddress({ host, port }, () => next(data));
|
2017-10-26 09:59:09 +00:00
|
|
|
}
|
|
|
|
|
2017-10-20 02:56:03 +00:00
|
|
|
}
|