From 748ebe9aaa21ed8bdb1fd8c8efe0c8641024f57d Mon Sep 17 00:00:00 2001 From: Micooz Date: Wed, 25 Apr 2018 22:49:13 +0800 Subject: [PATCH] presets: use increment* helpers to increase nonce --- src/presets/aead-random-cipher.js | 38 +++++++++------- src/presets/ss-aead-cipher.js | 74 ++++++++++++++++++------------- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/presets/aead-random-cipher.js b/src/presets/aead-random-cipher.js index 0d57060..c9336af 100644 --- a/src/presets/aead-random-cipher.js +++ b/src/presets/aead-random-cipher.js @@ -1,11 +1,11 @@ import crypto from 'crypto'; import { IPreset } from './defs'; import { + AdvancedBuffer, HKDF, getRandomChunks, numberToBuffer, - BYTE_ORDER_LE, - AdvancedBuffer, + incrementLE, } from '../utils'; const NONCE_LEN = 12; @@ -88,14 +88,14 @@ export default class AeadRandomCipherPreset extends IPreset { _decipherKey = null; - _cipherNonce = 0; + _cipherNonce = null; - _decipherNonce = 0; + _decipherNonce = null; // sorry for bad naming, // this is used for determining if the current chunk had dropped random padding. // please check out onReceiving() - _nextExpectDecipherNonce = 0; + _nextExpectDecipherNonce = null; _adBuf = null; @@ -126,6 +126,9 @@ export default class AeadRandomCipherPreset extends IPreset { this._keySaltSize = ciphers[method]; this._adBuf = new 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() { @@ -133,9 +136,9 @@ export default class AeadRandomCipherPreset extends IPreset { this._adBuf = null; this._cipherKey = null; this._decipherKey = null; - this._cipherNonce = 0; - this._decipherNonce = 0; - this._nextExpectDecipherNonce = 0; + this._cipherNonce = null; + this._decipherNonce = null; + this._nextExpectDecipherNonce = null; } beforeOut({ buffer }) { @@ -180,12 +183,14 @@ export default class AeadRandomCipherPreset extends IPreset { } // 2. determine padding length then drop it - if (this._decipherNonce === this._nextExpectDecipherNonce) { + if (this._decipherNonce.equals(this._nextExpectDecipherNonce)) { const paddingLen = this.getPaddingLength(this._decipherKey, this._decipherNonce); if (buffer.length < paddingLen) { return; // too short to drop padding } - this._nextExpectDecipherNonce += 2; // because each chunk increases the nonce twice + // because each chunk increases the nonce twice + incrementLE(this._nextExpectDecipherNonce); + incrementLE(this._nextExpectDecipherNonce); return buffer.slice(paddingLen); // drop random padding } @@ -220,9 +225,8 @@ export default class AeadRandomCipherPreset extends IPreset { } getPaddingLength(key, nonce) { - const nonceBuffer = numberToBuffer(nonce, NONCE_LEN, BYTE_ORDER_LE); - const cipher = crypto.createCipheriv(this._cipherName, key, nonceBuffer); - cipher.update(nonceBuffer); + const cipher = crypto.createCipheriv(this._cipherName, key, nonce); + cipher.update(nonce); cipher.final(); return cipher.getAuthTag()[0] * this._factor; } @@ -231,11 +235,11 @@ export default class AeadRandomCipherPreset extends IPreset { const cipher = crypto.createCipheriv( this._cipherName, this._cipherKey, - numberToBuffer(this._cipherNonce, NONCE_LEN, BYTE_ORDER_LE) + this._cipherNonce, ); const encrypted = Buffer.concat([cipher.update(message), cipher.final()]); const tag = cipher.getAuthTag(); - this._cipherNonce += 1; + incrementLE(this._cipherNonce); return [encrypted, tag]; } @@ -243,12 +247,12 @@ export default class AeadRandomCipherPreset extends IPreset { const decipher = crypto.createDecipheriv( this._cipherName, this._decipherKey, - numberToBuffer(this._decipherNonce, NONCE_LEN, BYTE_ORDER_LE) + this._decipherNonce, ); decipher.setAuthTag(tag); try { const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); - this._decipherNonce += 1; + incrementLE(this._decipherNonce); return decrypted; } catch (err) { return null; diff --git a/src/presets/ss-aead-cipher.js b/src/presets/ss-aead-cipher.js index 200149a..91288a7 100644 --- a/src/presets/ss-aead-cipher.js +++ b/src/presets/ss-aead-cipher.js @@ -1,6 +1,14 @@ import crypto from 'crypto'; import { IPreset } from './defs'; -import { EVP_BytesToKey, HKDF, getRandomChunks, numberToBuffer, BYTE_ORDER_LE, AdvancedBuffer } from '../utils'; +import { + AdvancedBuffer, + dumpHex, + EVP_BytesToKey, + HKDF, + getRandomChunks, + numberToBuffer, + incrementLE, +} from '../utils'; const TAG_SIZE = 16; const MIN_CHUNK_LEN = TAG_SIZE * 2 + 3; @@ -14,24 +22,25 @@ const ciphers = { 'aes-256-gcm': [32, 32, 12], 'chacha20-poly1305': [32, 32, 8], 'chacha20-ietf-poly1305': [32, 32, 12], - 'xchacha20-ietf-poly1305': [32, 32, 24] + 'xchacha20-ietf-poly1305': [32, 32, 24], }; const libsodium_functions = { 'chacha20-poly1305': [ 'crypto_aead_chacha20poly1305_encrypt_detached', - 'crypto_aead_chacha20poly1305_decrypt_detached' + 'crypto_aead_chacha20poly1305_decrypt_detached', ], 'chacha20-ietf-poly1305': [ 'crypto_aead_chacha20poly1305_ietf_encrypt_detached', - 'crypto_aead_chacha20poly1305_ietf_decrypt_detached' + 'crypto_aead_chacha20poly1305_ietf_decrypt_detached', ], 'xchacha20-ietf-poly1305': [ 'crypto_aead_xchacha20poly1305_ietf_encrypt_detached', - 'crypto_aead_xchacha20poly1305_ietf_decrypt_detached' - ] + 'crypto_aead_xchacha20poly1305_ietf_decrypt_detached', + ], }; +const DEFAULT_METHOD = 'aes-256-gcm'; const HKDF_HASH_ALGORITHM = 'sha1'; const HKDF_INFO = 'ss-subkey'; @@ -100,9 +109,7 @@ export default class SsAeadCipherPreset extends IPreset { _info = Buffer.from(HKDF_INFO); _keySize = 0; - _saltSize = 0; - _nonceSize = 0; _evpKey = null; @@ -110,23 +117,21 @@ export default class SsAeadCipherPreset extends IPreset { _isUseLibSodium = false; _cipherKey = null; - _decipherKey = null; - _cipherNonce = 0; - - _decipherNonce = 0; + _cipherNonce = null; + _decipherNonce = null; _adBuf = null; - static onCheckParams({ method }) { + static onCheckParams({ method = DEFAULT_METHOD }) { const cipherNames = Object.keys(ciphers); if (!cipherNames.includes(method)) { throw Error(`'method' must be one of [${cipherNames}]`); } } - onInit({ method }) { + onInit({ method = DEFAULT_METHOD }) { const [keySize, saltSize, nonceSize] = ciphers[method]; this._cipherName = method; this._keySize = keySize; @@ -136,6 +141,13 @@ export default class SsAeadCipherPreset extends IPreset { this._isUseLibSodium = Object.keys(libsodium_functions).includes(method); this._adBuf = new AdvancedBuffer({ getPacketLength: this.onReceiving.bind(this) }); this._adBuf.on('data', this.onChunkReceived.bind(this)); + this._cipherNonce = Buffer.alloc(nonceSize); + this._decipherNonce = Buffer.alloc(nonceSize); + // TODO: prefer to use openssl in Node.js v10. + // if (this._cipherName === 'chacha20-ietf-poly1305' && process.version.startsWith('v10')) { + // this._cipherName = 'chacha20-poly1305'; + // this._isUseLibSodium = false; + // } } onDestroy() { @@ -143,8 +155,8 @@ export default class SsAeadCipherPreset extends IPreset { this._adBuf = null; this._cipherKey = null; this._decipherKey = null; - this._cipherNonce = 0; - this._decipherNonce = 0; + this._cipherNonce = null; + this._decipherNonce = null; } // tcp @@ -191,12 +203,12 @@ export default class SsAeadCipherPreset extends IPreset { const [encLen, lenTag] = [buffer.slice(0, 2), buffer.slice(2, 2 + TAG_SIZE)]; 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')}`); + fail(`unexpected DataLen_TAG=${dumpHex(lenTag)} when verify DataLen=${dumpHex(encLen)}, dump=${dumpHex(buffer)}`); 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')}`); + fail(`invalid DataLen=${dataLen} is over ${MAX_CHUNK_SPLIT_LEN}, dump=${dumpHex(buffer)}`); return -1; } return 2 + TAG_SIZE + dataLen + TAG_SIZE; @@ -207,7 +219,7 @@ export default class SsAeadCipherPreset extends IPreset { const [encData, dataTag] = [chunk.slice(2 + TAG_SIZE, -TAG_SIZE), chunk.slice(-TAG_SIZE)]; const data = this.decrypt(encData, dataTag); if (data === null) { - return fail(`unexpected Data_TAG=${dataTag.toString('hex')} when verify Data=${encData.slice(0, 60).toString('hex')}, dump=${chunk.slice(0, 60).toString('hex')}`); + return fail(`unexpected Data_TAG=${dumpHex(dataTag)} when verify Data=${dumpHex(encData)}, dump=${dumpHex(chunk)}`); } next(data); } @@ -215,14 +227,14 @@ export default class SsAeadCipherPreset extends IPreset { encrypt(message) { const cipherName = this._cipherName; const cipherKey = this._cipherKey; - const nonce = numberToBuffer(this._cipherNonce, this._nonceSize, BYTE_ORDER_LE); + const nonce = this._cipherNonce; let ciphertext = null; let tag = null; if (this._isUseLibSodium) { const noop = Buffer.alloc(0); // eslint-disable-next-line const result = libsodium[libsodium_functions[cipherName][0]]( - message, noop, noop, nonce, cipherKey + message, noop, noop, nonce, cipherKey, ); ciphertext = Buffer.from(result.ciphertext); tag = Buffer.from(result.mac); @@ -231,22 +243,22 @@ export default class SsAeadCipherPreset extends IPreset { ciphertext = Buffer.concat([cipher.update(message), cipher.final()]); tag = cipher.getAuthTag(); } - this._cipherNonce += 1; + incrementLE(nonce); return [ciphertext, tag]; } decrypt(ciphertext, tag) { const cipherName = this._cipherName; const decipherKey = this._decipherKey; - const nonce = numberToBuffer(this._decipherNonce, this._nonceSize, BYTE_ORDER_LE); + const nonce = this._decipherNonce; if (this._isUseLibSodium) { const noop = Buffer.alloc(0); try { // eslint-disable-next-line const plaintext = libsodium[libsodium_functions[cipherName][1]]( - noop, ciphertext, tag, noop, nonce, decipherKey + noop, ciphertext, tag, noop, nonce, decipherKey, ); - this._decipherNonce += 1; + incrementLE(nonce); return Buffer.from(plaintext); } catch (err) { return null; @@ -256,7 +268,7 @@ export default class SsAeadCipherPreset extends IPreset { decipher.setAuthTag(tag); try { const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]); - this._decipherNonce += 1; + incrementLE(nonce); return plaintext; } catch (err) { return null; @@ -269,7 +281,7 @@ export default class SsAeadCipherPreset extends IPreset { beforeOutUdp({ buffer }) { const salt = crypto.randomBytes(this._saltSize); this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize); - this._cipherNonce = 0; + this._cipherNonce = Buffer.alloc(this._nonceSize); const [ciphertext, tag] = this.encrypt(buffer); return Buffer.concat([salt, ciphertext, tag]); } @@ -277,18 +289,18 @@ export default class SsAeadCipherPreset extends IPreset { beforeInUdp({ buffer, fail }) { const saltSize = this._saltSize; if (buffer.length < saltSize) { - return fail(`too short to get salt, len=${buffer.length} dump=${buffer.toString('hex')}`); + return fail(`too short to get salt, len=${buffer.length} dump=${dumpHex(buffer)}`); } const salt = buffer.slice(0, saltSize); this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize); - this._decipherNonce = 0; + this._decipherNonce = Buffer.alloc(this._nonceSize); if (buffer.length < saltSize + TAG_SIZE + 1) { - return fail(`too short to verify Data, len=${buffer.length} dump=${buffer.toString('hex')}`); + return fail(`too short to verify Data, len=${buffer.length} dump=${dumpHex(buffer)}`); } const [encData, dataTag] = [buffer.slice(saltSize, -TAG_SIZE), buffer.slice(-TAG_SIZE)]; const data = this.decrypt(encData, dataTag); if (data === null) { - return fail(`unexpected Data_TAG=${dataTag.toString('hex')} when verify Data=${encData.slice(0, 60).toString('hex')}, dump=${buffer.slice(0, 60).toString('hex')}`); + return fail(`unexpected Data_TAG=${dumpHex(dataTag)} when verify Data=${dumpHex(encData)}, dump=${dumpHex(buffer)}`); } return data; }