2017-03-03 14:16:06 +00:00
|
|
|
import crypto from 'crypto';
|
2018-02-17 04:19:31 +00:00
|
|
|
import { IPreset } from './defs';
|
|
|
|
import { dumpHex, EVP_BytesToKey, hash } from '../utils';
|
2017-03-03 14:16:06 +00:00
|
|
|
|
2017-10-14 04:26:25 +00:00
|
|
|
// available ciphers and [key size, iv size]
|
|
|
|
const ciphers = {
|
|
|
|
'aes-128-ctr': [16, 16], 'aes-192-ctr': [24, 16], 'aes-256-ctr': [32, 16],
|
|
|
|
'aes-128-cfb': [16, 16], 'aes-192-cfb': [24, 16], 'aes-256-cfb': [32, 16],
|
2018-04-25 04:10:27 +00:00
|
|
|
'camellia-128-cfb': [16, 16],
|
|
|
|
'camellia-192-cfb': [24, 16],
|
|
|
|
'camellia-256-cfb': [32, 16],
|
|
|
|
'rc4-md5': [16, 16],
|
|
|
|
'rc4-md5-6': [16, 6],
|
2017-12-13 03:51:08 +00:00
|
|
|
|
|
|
|
// NOTE: "none" cipher is just prepared for "ssr-auth-chain-*" presets.
|
|
|
|
// DO NOT use "none" without "ssr-auth-chain-*".
|
2018-04-25 04:10:27 +00:00
|
|
|
'none': [16, 0],
|
2017-12-13 03:51:08 +00:00
|
|
|
|
2018-04-25 04:10:27 +00:00
|
|
|
// require Node.js v10.x
|
|
|
|
'chacha20-ietf': [32, 12],
|
2017-10-14 04:26:25 +00:00
|
|
|
};
|
2017-03-03 14:16:06 +00:00
|
|
|
|
2018-04-25 04:10:27 +00:00
|
|
|
const DEFAULT_METHOD = 'aes-256-ctr';
|
2017-11-04 09:24:41 +00:00
|
|
|
const NOOP = Buffer.alloc(0);
|
|
|
|
|
2017-02-23 13:41:43 +00:00
|
|
|
/**
|
|
|
|
* @description
|
2017-10-14 04:26:25 +00:00
|
|
|
* Perform stream encrypt/decrypt.
|
2017-02-23 13:41:43 +00:00
|
|
|
*
|
|
|
|
* @params
|
2017-10-14 04:26:25 +00:00
|
|
|
* method: The cipher name.
|
2017-02-23 13:41:43 +00:00
|
|
|
*
|
|
|
|
* @examples
|
2017-04-13 08:43:54 +00:00
|
|
|
* {
|
|
|
|
* "name": "ss-stream-cipher",
|
|
|
|
* "params": {
|
|
|
|
* "method": "aes-256-cfb"
|
|
|
|
* }
|
|
|
|
* }
|
2017-02-23 13:41:43 +00:00
|
|
|
*
|
|
|
|
* @protocol
|
|
|
|
*
|
2017-06-15 07:05:57 +00:00
|
|
|
* # TCP stream
|
|
|
|
* +-------+---------------------+---------+
|
|
|
|
* | IV | PAYLOAD | ... |
|
|
|
|
* +-------+---------------------+---------+
|
|
|
|
* | Fixed | Variable | ... |
|
|
|
|
* +-------+---------------------+---------+
|
2017-02-23 13:41:43 +00:00
|
|
|
*
|
2017-06-15 07:05:57 +00:00
|
|
|
* # TCP chunks
|
|
|
|
* +---------------------+
|
|
|
|
* | PAYLOAD |
|
|
|
|
* +---------------------+
|
|
|
|
* | Variable |
|
|
|
|
* +---------------------+
|
2017-03-23 13:51:25 +00:00
|
|
|
*
|
2017-10-26 09:59:09 +00:00
|
|
|
* # UDP packet
|
|
|
|
* +-------+---------------------+
|
|
|
|
* | IV | PAYLOAD |
|
|
|
|
* +-------+---------------------+
|
|
|
|
* | Fixed | Variable |
|
|
|
|
* +-------+---------------------+
|
|
|
|
*
|
2017-03-23 13:51:25 +00:00
|
|
|
* @explain
|
|
|
|
* 1. Key derivation function is EVP_BytesToKey.
|
2017-10-14 04:26:25 +00:00
|
|
|
* 2. IV is randomly generated.
|
2017-03-23 13:51:25 +00:00
|
|
|
* 3. Client Cipher IV = Server Decipher IV, vice versa.
|
|
|
|
*
|
|
|
|
* @reference
|
|
|
|
* [1] EVP_BytesToKey
|
|
|
|
* https://www.openssl.org/docs/man1.0.2/crypto/EVP_BytesToKey.html
|
|
|
|
* https://github.com/shadowsocks/shadowsocks/blob/master/shadowsocks/cryptor.py#L53
|
2017-02-23 13:41:43 +00:00
|
|
|
*/
|
2017-08-08 07:53:00 +00:00
|
|
|
export default class SsStreamCipherPreset extends IPreset {
|
2017-02-23 13:41:43 +00:00
|
|
|
|
2017-11-04 09:24:41 +00:00
|
|
|
_algorithm = '';
|
2017-02-23 13:41:43 +00:00
|
|
|
|
2017-11-04 09:24:41 +00:00
|
|
|
_key = null;
|
|
|
|
_iv = null;
|
2017-02-23 13:41:43 +00:00
|
|
|
|
2017-11-04 09:24:41 +00:00
|
|
|
_ivSize = 0;
|
2017-10-14 04:26:25 +00:00
|
|
|
|
2017-03-23 13:51:25 +00:00
|
|
|
_cipher = null;
|
|
|
|
_decipher = null;
|
|
|
|
|
2018-02-15 03:01:24 +00:00
|
|
|
get key() {
|
|
|
|
return this._key;
|
|
|
|
}
|
|
|
|
|
|
|
|
get iv() {
|
|
|
|
return this._iv;
|
|
|
|
}
|
|
|
|
|
2018-04-25 04:10:27 +00:00
|
|
|
static onCheckParams({ method = DEFAULT_METHOD }) {
|
2017-04-13 08:43:54 +00:00
|
|
|
if (typeof method !== 'string' || method === '') {
|
|
|
|
throw Error('\'method\' must be set');
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
2017-10-14 04:26:25 +00:00
|
|
|
const cipherNames = Object.keys(ciphers);
|
|
|
|
if (!cipherNames.includes(method)) {
|
|
|
|
throw Error(`'method' must be one of [${cipherNames}]`);
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
2018-04-25 04:10:27 +00:00
|
|
|
if (method === 'chacha20-ietf' && !process.version.startsWith('v10')) {
|
|
|
|
throw Error('require Node.js v10.x to run "chacha20-ietf"');
|
|
|
|
}
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
|
|
|
|
2018-04-25 04:10:27 +00:00
|
|
|
onInit({ method = DEFAULT_METHOD }) {
|
2017-11-04 09:24:41 +00:00
|
|
|
const [keySize, ivSize] = ciphers[method];
|
|
|
|
const iv = crypto.randomBytes(ivSize);
|
2018-04-25 04:10:27 +00:00
|
|
|
this._algorithm = method;
|
2017-11-04 09:24:41 +00:00
|
|
|
this._ivSize = ivSize;
|
2018-02-15 03:01:24 +00:00
|
|
|
this._key = EVP_BytesToKey(this._config.key, keySize, ivSize);
|
2018-04-25 04:10:27 +00:00
|
|
|
this._iv = iv;
|
|
|
|
if (this._algorithm.startsWith('rc4')) {
|
|
|
|
this._algorithm = 'rc4';
|
|
|
|
if (this._algorithm === 'rc4-md5-6') {
|
|
|
|
this._iv = this._iv.slice(0, 6);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this._algorithm === 'chacha20-ietf') {
|
|
|
|
this._algorithm = 'chacha20';
|
|
|
|
}
|
2017-10-31 14:26:40 +00:00
|
|
|
}
|
|
|
|
|
2017-09-08 02:47:36 +00:00
|
|
|
onDestroy() {
|
2017-11-04 09:24:41 +00:00
|
|
|
this._key = null;
|
|
|
|
this._iv = null;
|
2017-09-08 02:47:36 +00:00
|
|
|
this._cipher = null;
|
|
|
|
this._decipher = null;
|
2017-11-04 09:24:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
createCipher(key, iv) {
|
|
|
|
const algorithm = this._algorithm;
|
|
|
|
let _key = key;
|
|
|
|
let _iv = iv;
|
|
|
|
if (algorithm === 'rc4') {
|
|
|
|
_key = hash('md5', Buffer.concat([_key, _iv]));
|
|
|
|
_iv = NOOP;
|
|
|
|
}
|
2017-12-13 03:51:08 +00:00
|
|
|
else if (algorithm === 'none') {
|
|
|
|
return {
|
2018-04-25 04:10:27 +00:00
|
|
|
update: (buffer) => buffer,
|
2017-12-13 03:51:08 +00:00
|
|
|
};
|
|
|
|
}
|
2018-04-25 04:10:27 +00:00
|
|
|
else if (algorithm === 'chacha20') {
|
|
|
|
// 4 bytes counter + 12 bytes nonce
|
|
|
|
_iv = Buffer.concat([Buffer.alloc(4), _iv]);
|
|
|
|
}
|
2017-11-04 09:24:41 +00:00
|
|
|
return crypto.createCipheriv(algorithm, _key, _iv);
|
|
|
|
}
|
|
|
|
|
|
|
|
createDecipher(key, iv) {
|
|
|
|
const algorithm = this._algorithm;
|
|
|
|
let _key = key;
|
|
|
|
let _iv = iv;
|
|
|
|
if (algorithm === 'rc4') {
|
|
|
|
_key = hash('md5', Buffer.concat([_key, _iv]));
|
|
|
|
_iv = NOOP;
|
|
|
|
}
|
2017-12-13 03:51:08 +00:00
|
|
|
else if (algorithm === 'none') {
|
|
|
|
return {
|
2018-04-25 04:10:27 +00:00
|
|
|
update: (buffer) => buffer,
|
2017-12-13 03:51:08 +00:00
|
|
|
};
|
|
|
|
}
|
2018-04-25 04:10:27 +00:00
|
|
|
else if (algorithm === 'chacha20') {
|
|
|
|
// 4 bytes counter + 12 bytes nonce
|
|
|
|
_iv = Buffer.concat([Buffer.alloc(4), _iv]);
|
|
|
|
}
|
2017-11-04 09:24:41 +00:00
|
|
|
return crypto.createDecipheriv(algorithm, _key, _iv);
|
2017-09-08 02:47:36 +00:00
|
|
|
}
|
|
|
|
|
2017-10-26 09:59:09 +00:00
|
|
|
// tcp
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
beforeOut({ buffer }) {
|
2017-03-23 13:51:25 +00:00
|
|
|
if (!this._cipher) {
|
2017-11-04 09:24:41 +00:00
|
|
|
this._cipher = this.createCipher(this._key, this._iv);
|
2017-10-31 14:26:40 +00:00
|
|
|
return Buffer.concat([this._iv, this._cipher.update(buffer)]);
|
2017-02-23 13:41:43 +00:00
|
|
|
} else {
|
2017-10-26 09:59:09 +00:00
|
|
|
return this._cipher.update(buffer);
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
beforeIn({ buffer, fail }) {
|
2017-03-23 13:51:25 +00:00
|
|
|
if (!this._decipher) {
|
2018-02-17 04:19:31 +00:00
|
|
|
const { _ivSize } = this;
|
2017-11-04 09:24:41 +00:00
|
|
|
if (buffer.length < _ivSize) {
|
|
|
|
return fail(`buffer is too short to get iv, len=${buffer.length} dump=${dumpHex(buffer)}`);
|
2017-08-09 06:54:31 +00:00
|
|
|
}
|
2017-11-04 09:24:41 +00:00
|
|
|
this._iv = buffer.slice(0, _ivSize);
|
|
|
|
this._decipher = this.createDecipher(this._key, this._iv);
|
|
|
|
return this._decipher.update(buffer.slice(_ivSize));
|
2017-02-23 13:41:43 +00:00
|
|
|
} else {
|
2017-10-26 09:59:09 +00:00
|
|
|
return this._decipher.update(buffer);
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-26 09:59:09 +00:00
|
|
|
// udp
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
beforeOutUdp({ buffer }) {
|
2017-11-04 09:24:41 +00:00
|
|
|
this._iv = crypto.randomBytes(this._ivSize);
|
|
|
|
this._cipher = this.createCipher(this._key, this._iv);
|
|
|
|
return Buffer.concat([this._iv, this._cipher.update(buffer)]);
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
|
|
|
|
2018-02-17 04:19:31 +00:00
|
|
|
beforeInUdp({ buffer, fail }) {
|
|
|
|
const { _ivSize } = this;
|
2017-11-04 09:24:41 +00:00
|
|
|
if (buffer.length < _ivSize) {
|
|
|
|
return fail(`buffer is too short to get iv, len=${buffer.length} dump=${dumpHex(buffer)}`);
|
2017-10-26 09:59:09 +00:00
|
|
|
}
|
2017-11-04 09:24:41 +00:00
|
|
|
this._iv = buffer.slice(0, _ivSize);
|
|
|
|
this._decipher = this.createDecipher(this._key, this._iv);
|
|
|
|
return this._decipher.update(buffer.slice(_ivSize));
|
2017-02-23 13:41:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|