188 lines
6.2 KiB
JavaScript
188 lines
6.2 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _crypto = require('crypto');
|
|
|
|
var _crypto2 = _interopRequireDefault(_crypto);
|
|
|
|
var _defs = require('./defs');
|
|
|
|
var _utils = require('../utils');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
const NONCE_LEN = 12;
|
|
const TAG_LEN = 16;
|
|
const MIN_CHUNK_LEN = TAG_LEN * 2 + 3;
|
|
const MIN_CHUNK_SPLIT_LEN = 0x0800;
|
|
const MAX_CHUNK_SPLIT_LEN = 0x3FFF;
|
|
const DEFAULT_INFO = 'bs-subkey';
|
|
const DEFAULT_FACTOR = 2;
|
|
|
|
const ciphers = {
|
|
'aes-128-gcm': 16,
|
|
'aes-192-gcm': 24,
|
|
'aes-256-gcm': 32
|
|
};
|
|
|
|
const HKDF_HASH_ALGORITHM = 'sha1';
|
|
|
|
class AeadRandomCipherPreset extends _defs.IPreset {
|
|
constructor(...args) {
|
|
var _temp;
|
|
|
|
return _temp = super(...args), this._cipherName = '', this._info = null, this._factor = DEFAULT_FACTOR, this._rawKey = null, this._keySaltSize = 0, this._cipherKey = null, this._decipherKey = null, this._cipherNonce = null, this._decipherNonce = null, this._nextExpectDecipherNonce = null, this._adBuf = null, _temp;
|
|
}
|
|
|
|
static onCheckParams({ method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR }) {
|
|
if (method === undefined || method === '') {
|
|
throw Error('\'method\' must be set');
|
|
}
|
|
const cipherNames = Object.keys(ciphers);
|
|
if (!cipherNames.includes(method)) {
|
|
throw Error(`'method' must be one of [${cipherNames}]`);
|
|
}
|
|
if (typeof info !== 'string' || info.length <= 0) {
|
|
throw Error('\'info\' must be a non-empty string');
|
|
}
|
|
if (!Number.isInteger(factor)) {
|
|
throw Error('\'factor\' must be an integer');
|
|
}
|
|
if (factor < 1 || factor > 10) {
|
|
throw Error('\'factor\' must be in [1, 10]');
|
|
}
|
|
}
|
|
|
|
onInit({ method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR }) {
|
|
this._cipherName = method;
|
|
this._info = Buffer.from(info);
|
|
this._factor = factor;
|
|
this._rawKey = Buffer.from(this._config.key);
|
|
this._keySaltSize = ciphers[method];
|
|
this._adBuf = new _utils.AdvancedBuffer({ getPacketLength: this.onReceiving.bind(this) });
|
|
this._adBuf.on('data', this.onChunkReceived.bind(this));
|
|
this._cipherNonce = Buffer.alloc(NONCE_LEN);
|
|
this._decipherNonce = Buffer.alloc(NONCE_LEN);
|
|
this._nextExpectDecipherNonce = Buffer.alloc(NONCE_LEN);
|
|
}
|
|
|
|
onDestroy() {
|
|
this._adBuf.clear();
|
|
this._adBuf = null;
|
|
this._cipherKey = null;
|
|
this._decipherKey = null;
|
|
this._cipherNonce = null;
|
|
this._decipherNonce = null;
|
|
this._nextExpectDecipherNonce = null;
|
|
}
|
|
|
|
beforeOut({ buffer }) {
|
|
let salt = null;
|
|
if (this._cipherKey === null) {
|
|
const size = this._keySaltSize;
|
|
salt = _crypto2.default.randomBytes(size);
|
|
this._cipherKey = (0, _utils.HKDF)(HKDF_HASH_ALGORITHM, salt, this._rawKey, this._info, size);
|
|
}
|
|
const chunks = (0, _utils.getRandomChunks)(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map(chunk => {
|
|
const paddingLen = this.getPaddingLength(this._cipherKey, this._cipherNonce);
|
|
const padding = _crypto2.default.randomBytes(paddingLen);
|
|
|
|
const dataLen = (0, _utils.numberToBuffer)(chunk.length);
|
|
const [encLen, lenTag] = this.encrypt(dataLen);
|
|
|
|
const [encData, dataTag] = this.encrypt(chunk);
|
|
return Buffer.concat([padding, encLen, lenTag, encData, dataTag]);
|
|
});
|
|
if (salt) {
|
|
return Buffer.concat([salt, ...chunks]);
|
|
} else {
|
|
return Buffer.concat(chunks);
|
|
}
|
|
}
|
|
|
|
beforeIn({ buffer, next, fail }) {
|
|
this._adBuf.put(buffer, { next, fail });
|
|
}
|
|
|
|
onReceiving(buffer, { fail }) {
|
|
if (this._decipherKey === null) {
|
|
const size = this._keySaltSize;
|
|
if (buffer.length < size) {
|
|
return;
|
|
}
|
|
const salt = buffer.slice(0, size);
|
|
this._decipherKey = (0, _utils.HKDF)(HKDF_HASH_ALGORITHM, salt, this._rawKey, this._info, size);
|
|
return buffer.slice(size);
|
|
}
|
|
|
|
if (this._decipherNonce.equals(this._nextExpectDecipherNonce)) {
|
|
const paddingLen = this.getPaddingLength(this._decipherKey, this._decipherNonce);
|
|
if (buffer.length < paddingLen) {
|
|
return;
|
|
}
|
|
|
|
(0, _utils.incrementLE)(this._nextExpectDecipherNonce);
|
|
(0, _utils.incrementLE)(this._nextExpectDecipherNonce);
|
|
return buffer.slice(paddingLen);
|
|
}
|
|
|
|
if (buffer.length < MIN_CHUNK_LEN) {
|
|
return;
|
|
}
|
|
|
|
const [encLen, lenTag] = [buffer.slice(0, 2), buffer.slice(2, 2 + TAG_LEN)];
|
|
const dataLenBuf = this.decrypt(encLen, lenTag);
|
|
if (dataLenBuf === null) {
|
|
fail(`unexpected DataLen_TAG=${lenTag.toString('hex')} when verify DataLen=${encLen.toString('hex')}, dump=${buffer.slice(0, 60).toString('hex')}`);
|
|
return -1;
|
|
}
|
|
const dataLen = dataLenBuf.readUInt16BE(0);
|
|
if (dataLen > MAX_CHUNK_SPLIT_LEN) {
|
|
fail(`invalid DataLen=${dataLen} is over ${MAX_CHUNK_SPLIT_LEN}, dump=${buffer.slice(0, 60).toString('hex')}`);
|
|
return -1;
|
|
}
|
|
return 2 + TAG_LEN + dataLen + TAG_LEN;
|
|
}
|
|
|
|
onChunkReceived(chunk, { next, fail }) {
|
|
const [encData, dataTag] = [chunk.slice(2 + TAG_LEN, -TAG_LEN), chunk.slice(-TAG_LEN)];
|
|
const data = this.decrypt(encData, dataTag);
|
|
if (data === null) {
|
|
fail(`unexpected Data_TAG=${dataTag.toString('hex')} when verify Data=${encData.slice(0, 60).toString('hex')}, dump=${chunk.slice(0, 60).toString('hex')}`);
|
|
return;
|
|
}
|
|
next(data);
|
|
}
|
|
|
|
getPaddingLength(key, nonce) {
|
|
const cipher = _crypto2.default.createCipheriv(this._cipherName, key, nonce);
|
|
cipher.update(nonce);
|
|
cipher.final();
|
|
return cipher.getAuthTag()[0] * this._factor;
|
|
}
|
|
|
|
encrypt(message) {
|
|
const cipher = _crypto2.default.createCipheriv(this._cipherName, this._cipherKey, this._cipherNonce);
|
|
const encrypted = Buffer.concat([cipher.update(message), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
(0, _utils.incrementLE)(this._cipherNonce);
|
|
return [encrypted, tag];
|
|
}
|
|
|
|
decrypt(ciphertext, tag) {
|
|
const decipher = _crypto2.default.createDecipheriv(this._cipherName, this._decipherKey, this._decipherNonce);
|
|
decipher.setAuthTag(tag);
|
|
try {
|
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
(0, _utils.incrementLE)(this._decipherNonce);
|
|
return decrypted;
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
}
|
|
exports.default = AeadRandomCipherPreset; |