presets: use increment* helpers to increase nonce

This commit is contained in:
Micooz 2018-04-25 22:49:13 +08:00
parent 26041f5659
commit 748ebe9aaa
2 changed files with 64 additions and 48 deletions

@ -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;

@ -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;
}