From 47083a0e5d282502faaf8bd6ebeb408531fd2138 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Sun, 8 Nov 2020 14:05:51 +0800 Subject: [PATCH] Add Extensions --- Shadowsocks.Net/Crypto/Extensions/Check.cs | 23 + .../Crypto/Extensions/MyCfbBlockCipher.cs | 264 ++++++++ .../Crypto/Extensions/MyChaChaEngine.cs | 183 ++++++ .../Crypto/Extensions/MySalsa20Engine.cs | 363 +++++++++++ Shadowsocks.Net/Crypto/Extensions/Pack.cs | 56 ++ .../Crypto/Extensions/XChaCha20Engine.cs | 98 +++ .../Crypto/Extensions/XChaCha20Poly1305.cs | 588 ++++++++++++++++++ 7 files changed, 1575 insertions(+) create mode 100644 Shadowsocks.Net/Crypto/Extensions/Check.cs create mode 100644 Shadowsocks.Net/Crypto/Extensions/MyCfbBlockCipher.cs create mode 100644 Shadowsocks.Net/Crypto/Extensions/MyChaChaEngine.cs create mode 100644 Shadowsocks.Net/Crypto/Extensions/MySalsa20Engine.cs create mode 100644 Shadowsocks.Net/Crypto/Extensions/Pack.cs create mode 100644 Shadowsocks.Net/Crypto/Extensions/XChaCha20Engine.cs create mode 100644 Shadowsocks.Net/Crypto/Extensions/XChaCha20Poly1305.cs diff --git a/Shadowsocks.Net/Crypto/Extensions/Check.cs b/Shadowsocks.Net/Crypto/Extensions/Check.cs new file mode 100644 index 00000000..691efe31 --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/Check.cs @@ -0,0 +1,23 @@ +using Org.BouncyCastle.Crypto; + +namespace Shadowsocks.Net.Crypto.Extensions +{ + internal static class Check + { + internal static void DataLength(byte[] buf, int off, int len, string msg) + { + if (off > buf.Length - len) + { + throw new DataLengthException(msg); + } + } + + internal static void OutputLength(byte[] buf, int off, int len, string msg) + { + if (off > buf.Length - len) + { + throw new OutputLengthException(msg); + } + } + } +} \ No newline at end of file diff --git a/Shadowsocks.Net/Crypto/Extensions/MyCfbBlockCipher.cs b/Shadowsocks.Net/Crypto/Extensions/MyCfbBlockCipher.cs new file mode 100644 index 00000000..c108d5b1 --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/MyCfbBlockCipher.cs @@ -0,0 +1,264 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using System; +using System.IO; + +namespace Shadowsocks.Net.Crypto.Extensions +{ + /** + * implements a Cipher-FeedBack (CFB) mode on top of a simple cipher. + */ + public class MyCfbBlockCipher + : IBlockCipher + { + private byte[] IV; + private byte[] cfbV; + private byte[] cfbOutV; + private byte[] buff; + private int _offset; + private bool encrypting; + + private readonly int blockSize; + private readonly IBlockCipher cipher; + + /** + * Basic constructor. + * + * @param cipher the block cipher to be used as the basis of the + * feedback mode. + * @param blockSize the block size in bits (note: a multiple of 8) + */ + public MyCfbBlockCipher( + IBlockCipher cipher, + int bitBlockSize) + { + this.cipher = cipher; + blockSize = bitBlockSize / 8; + IV = new byte[cipher.GetBlockSize()]; + cfbV = new byte[cipher.GetBlockSize()]; + cfbOutV = new byte[cipher.GetBlockSize()]; + buff = new byte[cipher.GetBlockSize()]; + _offset = 0; + } + /** + * return the underlying block cipher that we are wrapping. + * + * @return the underlying block cipher that we are wrapping. + */ + public IBlockCipher GetUnderlyingCipher() + { + return cipher; + } + /** + * Initialise the cipher and, possibly, the initialisation vector (IV). + * If an IV isn't passed as part of the parameter, the IV will be all zeros. + * An IV which is too short is handled in FIPS compliant fashion. + * + * @param forEncryption if true the cipher is initialised for + * encryption, if false for decryption. + * @param param the key and other data required by the cipher. + * @exception ArgumentException if the parameters argument is + * inappropriate. + */ + public void Init( + bool forEncryption, + ICipherParameters parameters) + { + encrypting = forEncryption; + if (parameters is ParametersWithIV ivParam) + { + var iv = ivParam.GetIV(); + var diff = IV.Length - iv.Length; + Array.Copy(iv, 0, IV, diff, iv.Length); + Array.Clear(IV, 0, diff); + + parameters = ivParam.Parameters; + } + Reset(); + + // if it's null, key is to be reused. + if (parameters != null) + { + cipher.Init(true, parameters); + } + } + + /** + * return the algorithm name and mode. + * + * @return the name of the underlying algorithm followed by "/CFB" + * and the block size in bits. + */ + public string AlgorithmName => $@"{cipher.AlgorithmName}/CFB{blockSize * 8}"; + + public bool IsPartialBlockOkay => true; + + /** + * return the block size we are operating at. + * + * @return the block size we are operating at (in bytes). + */ + public int GetBlockSize() + { + return blockSize; + } + + /** + * Process one block of input from the array in and write it to + * the out array. + * + * @param in the array containing the input data. + * @param inOff offset into the in array the data starts at. + * @param out the array the output data will be copied into. + * @param outOff the offset into the out array the output will start at. + * @exception DataLengthException if there isn't enough data in in, or + * space in out. + * @exception InvalidOperationException if the cipher isn't initialised. + * @return the number of bytes processed and produced. + */ + private int ProcessBlock( + byte[] input, + int inOff, + byte[] output, + int outOff, + bool change) + { + return encrypting + ? EncryptBlock(input, inOff, output, outOff, change) + : DecryptBlock(input, inOff, output, outOff, change); + } + + public int ProcessBlock(byte[] inBuf, int inOff, byte[] outBuf, int outOff) + { + using var m = new MemoryStream(inBuf, inOff, inBuf.Length); + var tmp = new byte[blockSize]; + var o = new byte[outBuf.Length - outOff + blockSize * 8]; + using var outStream = new MemoryStream(o); + + var ptr = _offset; + int read; + while ((read = m.Read(buff, _offset, buff.Length - _offset)) > 0) + { + if (read + _offset < buff.Length) + { + var len = ProcessBlock(buff, 0, tmp, 0, false); + outStream.Write(tmp, 0, len); + _offset += read; + break; + } + else + { + var len = ProcessBlock(buff, 0, tmp, 0, true); + outStream.Write(tmp, 0, len); + _offset = 0; + } + } + + outStream.Seek(ptr, SeekOrigin.Begin); + var res = inBuf.Length; + outStream.Read(outBuf, outOff, res); + return res; + } + + /** + * Do the appropriate processing for CFB mode encryption. + * + * @param in the array containing the data to be encrypted. + * @param inOff offset into the in array the data starts at. + * @param out the array the encrypted data will be copied into. + * @param outOff the offset into the out array the output will start at. + * @exception DataLengthException if there isn't enough data in in, or + * space in out. + * @exception InvalidOperationException if the cipher isn't initialised. + * @return the number of bytes processed and produced. + */ + public int EncryptBlock( + byte[] input, + int inOff, + byte[] outBytes, + int outOff, + bool change) + { + if (inOff + blockSize > input.Length) + { + throw new DataLengthException("input buffer too short"); + } + if (outOff + blockSize > outBytes.Length) + { + throw new DataLengthException("output buffer too short"); + } + cipher.ProcessBlock(cfbV, 0, cfbOutV, 0); + // + // XOR the cfbV with the plaintext producing the ciphertext + // + for (var i = 0; i < blockSize; i++) + { + outBytes[outOff + i] = (byte)(cfbOutV[i] ^ input[inOff + i]); + } + // + // change over the input block. + // + if (change) + { + Array.Copy(cfbV, blockSize, cfbV, 0, cfbV.Length - blockSize); + Array.Copy(outBytes, outOff, cfbV, cfbV.Length - blockSize, blockSize); + } + return blockSize; + } + /** + * Do the appropriate processing for CFB mode decryption. + * + * @param in the array containing the data to be decrypted. + * @param inOff offset into the in array the data starts at. + * @param out the array the encrypted data will be copied into. + * @param outOff the offset into the out array the output will start at. + * @exception DataLengthException if there isn't enough data in in, or + * space in out. + * @exception InvalidOperationException if the cipher isn't initialised. + * @return the number of bytes processed and produced. + */ + public int DecryptBlock( + byte[] input, + int inOff, + byte[] outBytes, + int outOff, + bool change) + { + if (inOff + blockSize > input.Length) + { + throw new DataLengthException("input buffer too short"); + } + if (outOff + blockSize > outBytes.Length) + { + throw new DataLengthException("output buffer too short"); + } + cipher.ProcessBlock(cfbV, 0, cfbOutV, 0); + // + // change over the input block. + // + if (change) + { + Array.Copy(cfbV, blockSize, cfbV, 0, cfbV.Length - blockSize); + Array.Copy(input, inOff, cfbV, cfbV.Length - blockSize, blockSize); + } + // + // XOR the cfbV with the ciphertext producing the plaintext + // + for (var i = 0; i < blockSize; i++) + { + outBytes[outOff + i] = (byte)(cfbOutV[i] ^ input[inOff + i]); + } + return blockSize; + } + /** + * reset the chaining vector back to the IV and reset the underlying + * cipher. + */ + public void Reset() + { + Array.Copy(IV, 0, cfbV, 0, IV.Length); + _offset = 0; + cipher.Reset(); + } + } +} diff --git a/Shadowsocks.Net/Crypto/Extensions/MyChaChaEngine.cs b/Shadowsocks.Net/Crypto/Extensions/MyChaChaEngine.cs new file mode 100644 index 00000000..34569dfa --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/MyChaChaEngine.cs @@ -0,0 +1,183 @@ +using System; + +namespace Shadowsocks.Net.Crypto.Extensions +{ + /// + /// Implementation of Daniel J. Bernstein's ChaCha stream cipher. + /// + public class MyChaChaEngine : MySalsa20Engine + { + /// + /// Creates a ChaCha engine with a specific number of rounds. + /// + /// the number of rounds (must be an even number). + protected MyChaChaEngine(int rounds) : base(rounds) { } + + public override string AlgorithmName => $@"ChaCha{rounds}"; + + protected override void AdvanceCounter() + { + if (++engineState[12] == 0) + { + ++engineState[13]; + } + } + + protected override void ResetCounter() + { + engineState[12] = engineState[13] = 0; + } + + protected override void SetKey(byte[] keyBytes, byte[] ivBytes) + { + if (keyBytes != null) + { + if (keyBytes.Length != 16 && keyBytes.Length != 32) + { + throw new ArgumentException($@"{AlgorithmName} requires 128 bit or 256 bit key"); + } + + PackTauOrSigma(keyBytes.Length, engineState, 0); + + // Key + Pack.LE_To_UInt32(keyBytes, 0, engineState, 4, 4); + Pack.LE_To_UInt32(keyBytes, keyBytes.Length - 16, engineState, 8, 4); + } + + // IV + Pack.LE_To_UInt32(ivBytes, 0, engineState, 14, 2); + } + + protected override void GenerateKeyStream(byte[] output) + { + ChachaCore(rounds, engineState, x); + Pack.UInt32_To_LE(x, output, 0); + } + + /// + /// ChaCha function. + /// + /// The number of ChaCha rounds to execute + /// The input words. + /// The ChaCha state to modify. + private static void ChachaCore(int rounds, uint[] input, uint[] x) + { + if (input.Length != 16) + { + throw new ArgumentException(); + } + + if (x.Length != 16) + { + throw new ArgumentException(); + } + + if (rounds % 2 != 0) + { + throw new ArgumentException(@"Number of rounds must be even"); + } + + var x00 = input[0]; + var x01 = input[1]; + var x02 = input[2]; + var x03 = input[3]; + var x04 = input[4]; + var x05 = input[5]; + var x06 = input[6]; + var x07 = input[7]; + var x08 = input[8]; + var x09 = input[9]; + var x10 = input[10]; + var x11 = input[11]; + var x12 = input[12]; + var x13 = input[13]; + var x14 = input[14]; + var x15 = input[15]; + + for (var i = rounds; i > 0; i -= 2) + { + x00 += x04; + x12 = R(x12 ^ x00, 16); + x08 += x12; + x04 = R(x04 ^ x08, 12); + x00 += x04; + x12 = R(x12 ^ x00, 8); + x08 += x12; + x04 = R(x04 ^ x08, 7); + x01 += x05; + x13 = R(x13 ^ x01, 16); + x09 += x13; + x05 = R(x05 ^ x09, 12); + x01 += x05; + x13 = R(x13 ^ x01, 8); + x09 += x13; + x05 = R(x05 ^ x09, 7); + x02 += x06; + x14 = R(x14 ^ x02, 16); + x10 += x14; + x06 = R(x06 ^ x10, 12); + x02 += x06; + x14 = R(x14 ^ x02, 8); + x10 += x14; + x06 = R(x06 ^ x10, 7); + x03 += x07; + x15 = R(x15 ^ x03, 16); + x11 += x15; + x07 = R(x07 ^ x11, 12); + x03 += x07; + x15 = R(x15 ^ x03, 8); + x11 += x15; + x07 = R(x07 ^ x11, 7); + x00 += x05; + x15 = R(x15 ^ x00, 16); + x10 += x15; + x05 = R(x05 ^ x10, 12); + x00 += x05; + x15 = R(x15 ^ x00, 8); + x10 += x15; + x05 = R(x05 ^ x10, 7); + x01 += x06; + x12 = R(x12 ^ x01, 16); + x11 += x12; + x06 = R(x06 ^ x11, 12); + x01 += x06; + x12 = R(x12 ^ x01, 8); + x11 += x12; + x06 = R(x06 ^ x11, 7); + x02 += x07; + x13 = R(x13 ^ x02, 16); + x08 += x13; + x07 = R(x07 ^ x08, 12); + x02 += x07; + x13 = R(x13 ^ x02, 8); + x08 += x13; + x07 = R(x07 ^ x08, 7); + x03 += x04; + x14 = R(x14 ^ x03, 16); + x09 += x14; + x04 = R(x04 ^ x09, 12); + x03 += x04; + x14 = R(x14 ^ x03, 8); + x09 += x14; + x04 = R(x04 ^ x09, 7); + } + + x[0] = x00 + input[0]; + x[1] = x01 + input[1]; + x[2] = x02 + input[2]; + x[3] = x03 + input[3]; + x[4] = x04 + input[4]; + x[5] = x05 + input[5]; + x[6] = x06 + input[6]; + x[7] = x07 + input[7]; + x[8] = x08 + input[8]; + x[9] = x09 + input[9]; + x[10] = x10 + input[10]; + x[11] = x11 + input[11]; + x[12] = x12 + input[12]; + x[13] = x13 + input[13]; + x[14] = x14 + input[14]; + x[15] = x15 + input[15]; + } + } +} diff --git a/Shadowsocks.Net/Crypto/Extensions/MySalsa20Engine.cs b/Shadowsocks.Net/Crypto/Extensions/MySalsa20Engine.cs new file mode 100644 index 00000000..c45f2e18 --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/MySalsa20Engine.cs @@ -0,0 +1,363 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities; +using System; + +namespace Shadowsocks.Net.Crypto.Extensions +{ + /// + /// Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005 + /// + public class MySalsa20Engine : IStreamCipher + { + public static readonly int DefaultRounds = 20; + + /** Constants */ + private const int StateSize = 16; // 16, 32 bit ints = 64 bytes + + private static readonly uint[] TauSigma = Pack.LE_To_UInt32(Strings.ToAsciiByteArray("expand 16-byte k" + "expand 32-byte k"), 0, 8); + + internal static void PackTauOrSigma(int keyLength, uint[] state, int stateOffset) + { + var tsOff = (keyLength - 16) / 4; + state[stateOffset] = TauSigma[tsOff]; + state[stateOffset + 1] = TauSigma[tsOff + 1]; + state[stateOffset + 2] = TauSigma[tsOff + 2]; + state[stateOffset + 3] = TauSigma[tsOff + 3]; + } + + protected int rounds; + + /* + * variables to hold the state of the engine + * during encryption and decryption + */ + private int index = 0; + internal uint[] engineState = new uint[StateSize]; // state + internal uint[] x = new uint[StateSize]; // internal buffer + private byte[] keyStream = new byte[StateSize * 4]; // expanded state, 64 bytes + private bool initialised = false; + + /* + * internal counter + */ + private uint cW0, cW1, cW2; + + /// + /// Creates a Salsa20 engine with a specific number of rounds. + /// + /// the number of rounds (must be an even number). + protected MySalsa20Engine(int rounds) + { + if (rounds <= 0 || (rounds & 1) != 0) + { + throw new ArgumentException("'rounds' must be a positive, even number"); + } + + this.rounds = rounds; + } + + public virtual void Init( + bool forEncryption, + ICipherParameters parameters) + { + /* + * Salsa20 encryption and decryption is completely + * symmetrical, so the 'forEncryption' is + * irrelevant. (Like 90% of stream ciphers) + */ + + var ivParams = parameters as ParametersWithIV; + if (ivParams == null) + { + throw new ArgumentException(AlgorithmName + " Init requires an IV", "parameters"); + } + + var iv = ivParams.GetIV(); + if (iv == null || iv.Length != NonceSize) + { + throw new ArgumentException(AlgorithmName + " requires exactly " + NonceSize + " bytes of IV"); + } + + var keyParam = ivParams.Parameters; + if (keyParam == null) + { + if (!initialised) + { + throw new InvalidOperationException(AlgorithmName + " KeyParameter can not be null for first initialisation"); + } + + SetKey(null, iv); + } + else if (keyParam is KeyParameter keyParameter) + { + SetKey(keyParameter.GetKey(), iv); + } + else + { + throw new ArgumentException(AlgorithmName + " Init parameters must contain a KeyParameter (or null for re-init)"); + } + + Reset(); + initialised = true; + } + + protected virtual int NonceSize => 8; + + public virtual string AlgorithmName + { + get + { + var name = @"Salsa20"; + if (rounds != DefaultRounds) + { + name += $"/{rounds}"; + } + return name; + } + } + + public virtual byte ReturnByte( + byte input) + { + if (LimitExceeded()) + { + throw new MaxBytesExceededException("2^70 byte limit per IV; Change IV"); + } + + if (index == 0) + { + GenerateKeyStream(keyStream); + AdvanceCounter(); + } + + var output = (byte)(keyStream[index] ^ input); + index = (index + 1) & 63; + + return output; + } + + protected virtual void AdvanceCounter() + { + if (++engineState[8] == 0) + { + ++engineState[9]; + } + } + + public virtual void ProcessBytes( + byte[] inBytes, + int inOff, + int len, + byte[] outBytes, + int outOff) + { + if (!initialised) + { + throw new InvalidOperationException(AlgorithmName + " not initialised"); + } + + Check.DataLength(inBytes, inOff, len, "input buffer too short"); + Check.OutputLength(outBytes, outOff, len, "output buffer too short"); + + if (LimitExceeded((uint)len)) + { + throw new MaxBytesExceededException("2^70 byte limit per IV would be exceeded; Change IV"); + } + + for (var i = 0; i < len; i++) + { + if (index == 0) + { + GenerateKeyStream(keyStream); + AdvanceCounter(); + } + outBytes[i + outOff] = (byte)(keyStream[index] ^ inBytes[i + inOff]); + index = (index + 1) & 63; + } + } + + public virtual void Reset() + { + index = 0; + ResetLimitCounter(); + ResetCounter(); + } + + protected virtual void ResetCounter() + { + engineState[8] = engineState[9] = 0; + } + + protected virtual void SetKey(byte[] keyBytes, byte[] ivBytes) + { + if (keyBytes != null) + { + if ((keyBytes.Length != 16) && (keyBytes.Length != 32)) + { + throw new ArgumentException(AlgorithmName + " requires 128 bit or 256 bit key"); + } + + var tsOff = (keyBytes.Length - 16) / 4; + engineState[0] = TauSigma[tsOff]; + engineState[5] = TauSigma[tsOff + 1]; + engineState[10] = TauSigma[tsOff + 2]; + engineState[15] = TauSigma[tsOff + 3]; + + // Key + Pack.LE_To_UInt32(keyBytes, 0, engineState, 1, 4); + Pack.LE_To_UInt32(keyBytes, keyBytes.Length - 16, engineState, 11, 4); + } + + // IV + Pack.LE_To_UInt32(ivBytes, 0, engineState, 6, 2); + } + + protected virtual void GenerateKeyStream(byte[] output) + { + SalsaCore(rounds, engineState, x); + Pack.UInt32_To_LE(x, output, 0); + } + + internal static void SalsaCore(int rounds, uint[] input, uint[] x) + { + if (input.Length != 16) + { + throw new ArgumentException(); + } + + if (x.Length != 16) + { + throw new ArgumentException(); + } + + if (rounds % 2 != 0) + { + throw new ArgumentException("Number of rounds must be even"); + } + + var x00 = input[0]; + var x01 = input[1]; + var x02 = input[2]; + var x03 = input[3]; + var x04 = input[4]; + var x05 = input[5]; + var x06 = input[6]; + var x07 = input[7]; + var x08 = input[8]; + var x09 = input[9]; + var x10 = input[10]; + var x11 = input[11]; + var x12 = input[12]; + var x13 = input[13]; + var x14 = input[14]; + var x15 = input[15]; + + for (var i = rounds; i > 0; i -= 2) + { + x04 ^= R((x00 + x12), 7); + x08 ^= R((x04 + x00), 9); + x12 ^= R((x08 + x04), 13); + x00 ^= R((x12 + x08), 18); + x09 ^= R((x05 + x01), 7); + x13 ^= R((x09 + x05), 9); + x01 ^= R((x13 + x09), 13); + x05 ^= R((x01 + x13), 18); + x14 ^= R((x10 + x06), 7); + x02 ^= R((x14 + x10), 9); + x06 ^= R((x02 + x14), 13); + x10 ^= R((x06 + x02), 18); + x03 ^= R((x15 + x11), 7); + x07 ^= R((x03 + x15), 9); + x11 ^= R((x07 + x03), 13); + x15 ^= R((x11 + x07), 18); + + x01 ^= R((x00 + x03), 7); + x02 ^= R((x01 + x00), 9); + x03 ^= R((x02 + x01), 13); + x00 ^= R((x03 + x02), 18); + x06 ^= R((x05 + x04), 7); + x07 ^= R((x06 + x05), 9); + x04 ^= R((x07 + x06), 13); + x05 ^= R((x04 + x07), 18); + x11 ^= R((x10 + x09), 7); + x08 ^= R((x11 + x10), 9); + x09 ^= R((x08 + x11), 13); + x10 ^= R((x09 + x08), 18); + x12 ^= R((x15 + x14), 7); + x13 ^= R((x12 + x15), 9); + x14 ^= R((x13 + x12), 13); + x15 ^= R((x14 + x13), 18); + } + + x[0] = x00 + input[0]; + x[1] = x01 + input[1]; + x[2] = x02 + input[2]; + x[3] = x03 + input[3]; + x[4] = x04 + input[4]; + x[5] = x05 + input[5]; + x[6] = x06 + input[6]; + x[7] = x07 + input[7]; + x[8] = x08 + input[8]; + x[9] = x09 + input[9]; + x[10] = x10 + input[10]; + x[11] = x11 + input[11]; + x[12] = x12 + input[12]; + x[13] = x13 + input[13]; + x[14] = x14 + input[14]; + x[15] = x15 + input[15]; + } + + /** + * Rotate left + * + * @param x value to rotate + * @param y amount to rotate x + * + * @return rotated x + */ + internal static uint R(uint x, int y) + { + return (x << y) | (x >> (32 - y)); + } + + private void ResetLimitCounter() + { + cW0 = 0; + cW1 = 0; + cW2 = 0; + } + + private bool LimitExceeded() + { + if (++cW0 == 0) + { + if (++cW1 == 0) + { + return (++cW2 & 0x20) != 0; // 2^(32 + 32 + 6) + } + } + + return false; + } + + /* + * this relies on the fact len will always be positive. + */ + private bool LimitExceeded( + uint len) + { + var old = cW0; + cW0 += len; + if (cW0 < old) + { + if (++cW1 == 0) + { + return (++cW2 & 0x20) != 0; // 2^(32 + 32 + 6) + } + } + + return false; + } + } +} diff --git a/Shadowsocks.Net/Crypto/Extensions/Pack.cs b/Shadowsocks.Net/Crypto/Extensions/Pack.cs new file mode 100644 index 00000000..6f851455 --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/Pack.cs @@ -0,0 +1,56 @@ +namespace Shadowsocks.Net.Crypto.Extensions +{ + internal static class Pack + { + private static void UInt32_To_LE(uint n, byte[] bs, int off) + { + bs[off] = (byte)n; + bs[off + 1] = (byte)(n >> 8); + bs[off + 2] = (byte)(n >> 16); + bs[off + 3] = (byte)(n >> 24); + } + + internal static void UInt32_To_LE(uint[] ns, byte[] bs, int off) + { + foreach (var nsb in ns) + { + UInt32_To_LE(nsb, bs, off); + off += 4; + } + } + + private static uint LE_To_UInt32(byte[] bs, int off) + { + return bs[off] + | (uint)bs[off + 1] << 8 + | (uint)bs[off + 2] << 16 + | (uint)bs[off + 3] << 24; + } + + internal static void LE_To_UInt32(byte[] bs, int bOff, uint[] ns, int nOff, int count) + { + for (var i = 0; i < count; ++i) + { + ns[nOff + i] = LE_To_UInt32(bs, bOff); + bOff += 4; + } + } + + internal static uint[] LE_To_UInt32(byte[] bs, int off, int count) + { + var ns = new uint[count]; + for (var i = 0; i < ns.Length; ++i) + { + ns[i] = LE_To_UInt32(bs, off); + off += 4; + } + return ns; + } + + internal static void UInt64_To_LE(ulong n, byte[] bs, int off) + { + UInt32_To_LE((uint)n, bs, off); + UInt32_To_LE((uint)(n >> 32), bs, off + 4); + } + } +} diff --git a/Shadowsocks.Net/Crypto/Extensions/XChaCha20Engine.cs b/Shadowsocks.Net/Crypto/Extensions/XChaCha20Engine.cs new file mode 100644 index 00000000..f3fbd319 --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/XChaCha20Engine.cs @@ -0,0 +1,98 @@ +using Org.BouncyCastle.Utilities; +using System; + +namespace Shadowsocks.Net.Crypto.Extensions +{ + public class XChaCha20Engine : MyChaChaEngine + { + public XChaCha20Engine() : base(20) + { + } + + protected override int NonceSize => 24; + + public override string AlgorithmName => @"XChaCha20"; + + private static readonly uint[] Sigma = Pack.LE_To_UInt32(Strings.ToAsciiByteArray("expand 32-byte k"), 0, 4); + + protected override void SetKey(byte[] keyBytes, byte[] ivBytes) + { + base.SetKey(keyBytes, ivBytes); + + if (keyBytes == null || keyBytes.Length != 32) + { + throw new ArgumentException($@"{AlgorithmName} requires a 256 bit key"); + } + + if (ivBytes == null || ivBytes.Length != NonceSize) + { + throw new ArgumentException($@"{AlgorithmName} requires a 192 bit nonce"); + } + + var nonceInt = Pack.LE_To_UInt32(ivBytes, 0, 6); + + var chachaKey = HChaCha20Internal(keyBytes, nonceInt); + SetSigma(engineState); + SetKey(engineState, chachaKey); + engineState[12] = 1; // Counter + engineState[13] = 0; + engineState[14] = nonceInt[4]; + engineState[15] = nonceInt[5]; + } + + private static uint[] HChaCha20Internal(byte[] key, uint[] nonceInt) + { + var x = new uint[16]; + var intKey = Pack.LE_To_UInt32(key, 0, 8); + + SetSigma(x); + SetKey(x, intKey); + SetIntNonce(x, nonceInt); + DoubleRound(x); + Array.Copy(x, 12, x, 4, 4); + return x; + } + + private static void SetSigma(uint[] state) + { + Array.Copy(Sigma, 0, state, 0, Sigma.Length); + } + + private static void SetKey(uint[] state, uint[] key) + { + Array.Copy(key, 0, state, 4, 8); + } + + private static void SetIntNonce(uint[] state, uint[] nonce) + { + Array.Copy(nonce, 0, state, 12, 4); + } + + private static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d) + { + x[a] += x[b]; + x[d] = R(x[d] ^ x[a], 16); + x[c] += x[d]; + x[b] = R(x[b] ^ x[c], 12); + x[a] += x[b]; + x[d] = R(x[d] ^ x[a], 8); + x[c] += x[d]; + x[b] = R(x[b] ^ x[c], 7); + } + + private static void DoubleRound(uint[] state) + { + for (var i = 0; i < 10; ++i) + { + QuarterRound(state, 0, 4, 8, 12); + QuarterRound(state, 1, 5, 9, 13); + QuarterRound(state, 2, 6, 10, 14); + QuarterRound(state, 3, 7, 11, 15); + QuarterRound(state, 0, 5, 10, 15); + QuarterRound(state, 1, 6, 11, 12); + QuarterRound(state, 2, 7, 8, 13); + QuarterRound(state, 3, 4, 9, 14); + } + } + } +} diff --git a/Shadowsocks.Net/Crypto/Extensions/XChaCha20Poly1305.cs b/Shadowsocks.Net/Crypto/Extensions/XChaCha20Poly1305.cs new file mode 100644 index 00000000..e28b95be --- /dev/null +++ b/Shadowsocks.Net/Crypto/Extensions/XChaCha20Poly1305.cs @@ -0,0 +1,588 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities; +using System; + +namespace Shadowsocks.Net.Crypto.Extensions +{ + public sealed class XChaCha20Poly1305 : IAeadCipher + { + private enum State + { + Uninitialized = 0, + EncInit = 1, + EncAad = 2, + EncData = 3, + EncFinal = 4, + DecInit = 5, + DecAad = 6, + DecData = 7, + DecFinal = 8, + } + + private const int BufSize = 64; + private const int KeySize = 32; + private const int NonceSize = 24; + private const int MacSize = 16; + private static readonly byte[] Zeroes = new byte[MacSize - 1]; + + private const ulong AadLimit = ulong.MaxValue; + private const ulong DataLimit = ((1UL << 32) - 1) * 64; + + private readonly XChaCha20Engine mChacha20; + private readonly IMac mPoly1305; + + private readonly byte[] mKey = new byte[KeySize]; + private readonly byte[] mNonce = new byte[NonceSize]; + private readonly byte[] mBuf = new byte[BufSize + MacSize]; + private readonly byte[] mMac = new byte[MacSize]; + + private byte[] mInitialAad; + + private ulong mAadCount; + private ulong mDataCount; + private State mState = State.Uninitialized; + private int mBufPos; + + public XChaCha20Poly1305() : this(new Poly1305()) + { + } + + public XChaCha20Poly1305(IMac poly1305) + { + if (null == poly1305) + { + throw new ArgumentNullException(nameof(poly1305)); + } + + if (MacSize != poly1305.GetMacSize()) + { + throw new ArgumentException("must be a 128-bit MAC", nameof(poly1305)); + } + + mChacha20 = new XChaCha20Engine(); + mPoly1305 = poly1305; + } + + public string AlgorithmName => @"XChaCha20Poly1305"; + + public void Init(bool forEncryption, ICipherParameters parameters) + { + KeyParameter initKeyParam; + byte[] initNonce; + ICipherParameters chacha20Params; + + if (parameters is AeadParameters aeadParams) + { + var macSizeBits = aeadParams.MacSize; + if (MacSize * 8 != macSizeBits) + { + throw new ArgumentException("Invalid value for MAC size: " + macSizeBits); + } + + initKeyParam = aeadParams.Key; + initNonce = aeadParams.GetNonce(); + chacha20Params = new ParametersWithIV(initKeyParam, initNonce); + + mInitialAad = aeadParams.GetAssociatedText(); + } + else if (parameters is ParametersWithIV ivParams) + { + initKeyParam = (KeyParameter)ivParams.Parameters; + initNonce = ivParams.GetIV(); + chacha20Params = ivParams; + + mInitialAad = null; + } + else + { + throw new ArgumentException("invalid parameters passed to ChaCha20Poly1305", nameof(parameters)); + } + + // Validate key + if (null == initKeyParam) + { + if (State.Uninitialized == mState) + { + throw new ArgumentException("Key must be specified in initial init"); + } + } + else + { + if (KeySize != initKeyParam.GetKey().Length) + { + throw new ArgumentException("Key must be 256 bits"); + } + } + + // Validate nonce + if (null == initNonce || NonceSize != initNonce.Length) + { + throw new ArgumentException("Nonce must be 192 bits"); + } + + // Check for encryption with reused nonce + if (State.Uninitialized != mState && forEncryption && Arrays.AreEqual(mNonce, initNonce)) + { + if (null == initKeyParam || Arrays.AreEqual(mKey, initKeyParam.GetKey())) + { + throw new ArgumentException("cannot reuse nonce for ChaCha20Poly1305 encryption"); + } + } + + if (null != initKeyParam) + { + Array.Copy(initKeyParam.GetKey(), 0, mKey, 0, KeySize); + } + + Array.Copy(initNonce, 0, mNonce, 0, NonceSize); + + mChacha20.Init(true, chacha20Params); + + mState = forEncryption ? State.EncInit : State.DecInit; + + Reset(true, false); + } + + public int GetOutputSize(int len) + { + var total = Math.Max(0, len) + mBufPos; + + switch (mState) + { + case State.DecInit: + case State.DecAad: + case State.DecData: + return Math.Max(0, total - MacSize); + case State.EncInit: + case State.EncAad: + case State.EncData: + return total + MacSize; + default: + throw new InvalidOperationException(); + } + } + + public int GetUpdateOutputSize(int len) + { + var total = Math.Max(0, len) + mBufPos; + + switch (mState) + { + case State.DecInit: + case State.DecAad: + case State.DecData: + total = Math.Max(0, total - MacSize); + break; + case State.EncInit: + case State.EncAad: + case State.EncData: + break; + default: + throw new InvalidOperationException(); + } + + return total - total % BufSize; + } + + public void ProcessAadByte(byte input) + { + CheckAad(); + + mAadCount = IncrementCount(mAadCount, 1, AadLimit); + mPoly1305.Update(input); + } + + public void ProcessAadBytes(byte[] inBytes, int inOff, int len) + { + if (null == inBytes) + { + throw new ArgumentNullException(nameof(inBytes)); + } + + if (inOff < 0) + { + throw new ArgumentException("cannot be negative", nameof(inOff)); + } + + if (len < 0) + { + throw new ArgumentException("cannot be negative", nameof(len)); + } + + Check.DataLength(inBytes, inOff, len, "input buffer too short"); + + CheckAad(); + + if (len > 0) + { + mAadCount = IncrementCount(mAadCount, (uint)len, AadLimit); + mPoly1305.BlockUpdate(inBytes, inOff, len); + } + } + + public int ProcessByte(byte input, byte[] outBytes, int outOff) + { + CheckData(); + + switch (mState) + { + case State.DecData: + { + mBuf[mBufPos] = input; + if (++mBufPos == mBuf.Length) + { + mPoly1305.BlockUpdate(mBuf, 0, BufSize); + ProcessData(mBuf, 0, BufSize, outBytes, outOff); + Array.Copy(mBuf, BufSize, mBuf, 0, MacSize); + mBufPos = MacSize; + return BufSize; + } + + return 0; + } + case State.EncData: + { + mBuf[mBufPos] = input; + if (++mBufPos == BufSize) + { + ProcessData(mBuf, 0, BufSize, outBytes, outOff); + mPoly1305.BlockUpdate(outBytes, outOff, BufSize); + mBufPos = 0; + return BufSize; + } + + return 0; + } + default: + throw new InvalidOperationException(); + } + } + + public int ProcessBytes(byte[] inBytes, int inOff, int len, byte[] outBytes, int outOff) + { + if (null == inBytes) + { + throw new ArgumentNullException(nameof(inBytes)); + } + + if (null == outBytes) + { + throw new ArgumentNullException(nameof(outBytes)); + } + + if (inOff < 0) + { + throw new ArgumentException("cannot be negative", nameof(inOff)); + } + + if (len < 0) + { + throw new ArgumentException("cannot be negative", nameof(len)); + } + + Check.DataLength(inBytes, inOff, len, "input buffer too short"); + if (outOff < 0) + { + throw new ArgumentException("cannot be negative", nameof(outOff)); + } + + CheckData(); + + var resultLen = 0; + + switch (mState) + { + case State.DecData: + { + for (var i = 0; i < len; ++i) + { + mBuf[mBufPos] = inBytes[inOff + i]; + if (++mBufPos == mBuf.Length) + { + mPoly1305.BlockUpdate(mBuf, 0, BufSize); + ProcessData(mBuf, 0, BufSize, outBytes, outOff + resultLen); + Array.Copy(mBuf, BufSize, mBuf, 0, MacSize); + mBufPos = MacSize; + resultLen += BufSize; + } + } + break; + } + case State.EncData: + { + if (mBufPos != 0) + { + while (len > 0) + { + --len; + mBuf[mBufPos] = inBytes[inOff++]; + if (++mBufPos == BufSize) + { + ProcessData(mBuf, 0, BufSize, outBytes, outOff); + mPoly1305.BlockUpdate(outBytes, outOff, BufSize); + mBufPos = 0; + resultLen = BufSize; + break; + } + } + } + + while (len >= BufSize) + { + ProcessData(inBytes, inOff, BufSize, outBytes, outOff + resultLen); + mPoly1305.BlockUpdate(outBytes, outOff + resultLen, BufSize); + inOff += BufSize; + len -= BufSize; + resultLen += BufSize; + } + + if (len > 0) + { + Array.Copy(inBytes, inOff, mBuf, 0, len); + mBufPos = len; + } + break; + } + default: + throw new InvalidOperationException(); + } + + return resultLen; + } + + public int DoFinal(byte[] outBytes, int outOff) + { + if (null == outBytes) + { + throw new ArgumentNullException(nameof(outBytes)); + } + + if (outOff < 0) + { + throw new ArgumentException("cannot be negative", nameof(outOff)); + } + + CheckData(); + + Array.Clear(mMac, 0, MacSize); + + int resultLen; + + switch (mState) + { + case State.DecData: + { + if (mBufPos < MacSize) + { + throw new InvalidCipherTextException("data too short"); + } + + resultLen = mBufPos - MacSize; + + Check.OutputLength(outBytes, outOff, resultLen, "output buffer too short"); + + if (resultLen > 0) + { + mPoly1305.BlockUpdate(mBuf, 0, resultLen); + ProcessData(mBuf, 0, resultLen, outBytes, outOff); + } + + FinishData(State.DecFinal); + + if (!Arrays.ConstantTimeAreEqual(MacSize, mMac, 0, mBuf, resultLen)) + { + throw new InvalidCipherTextException("mac check in ChaCha20Poly1305 failed"); + } + + break; + } + case State.EncData: + { + resultLen = mBufPos + MacSize; + + Check.OutputLength(outBytes, outOff, resultLen, "output buffer too short"); + + if (mBufPos > 0) + { + ProcessData(mBuf, 0, mBufPos, outBytes, outOff); + mPoly1305.BlockUpdate(outBytes, outOff, mBufPos); + } + + FinishData(State.EncFinal); + + Array.Copy(mMac, 0, outBytes, outOff + mBufPos, MacSize); + break; + } + default: + throw new InvalidOperationException(); + } + + Reset(false, true); + + return resultLen; + } + + public byte[] GetMac() + { + return Arrays.Clone(mMac); + } + + public void Reset() + { + Reset(true, true); + } + + private void CheckAad() + { + switch (mState) + { + case State.DecInit: + mState = State.DecAad; + break; + case State.EncInit: + mState = State.EncAad; + break; + case State.DecAad: + case State.EncAad: + break; + case State.EncFinal: + throw new InvalidOperationException("ChaCha20Poly1305 cannot be reused for encryption"); + default: + throw new InvalidOperationException(); + } + } + + private void CheckData() + { + switch (mState) + { + case State.DecInit: + case State.DecAad: + FinishAad(State.DecData); + break; + case State.EncInit: + case State.EncAad: + FinishAad(State.EncData); + break; + case State.DecData: + case State.EncData: + break; + case State.EncFinal: + throw new InvalidOperationException("ChaCha20Poly1305 cannot be reused for encryption"); + default: + throw new InvalidOperationException(); + } + } + + private void FinishAad(State nextState) + { + PadMac(mAadCount); + + mState = nextState; + } + + private void FinishData(State nextState) + { + PadMac(mDataCount); + + var lengths = new byte[16]; + Pack.UInt64_To_LE(mAadCount, lengths, 0); + Pack.UInt64_To_LE(mDataCount, lengths, 8); + mPoly1305.BlockUpdate(lengths, 0, 16); + + mPoly1305.DoFinal(mMac, 0); + + mState = nextState; + } + + private ulong IncrementCount(ulong count, uint increment, ulong limit) + { + if (count > limit - increment) + { + throw new InvalidOperationException("Limit exceeded"); + } + + return count + increment; + } + + private void InitMac() + { + var firstBlock = new byte[64]; + try + { + mChacha20.ProcessBytes(firstBlock, 0, 64, firstBlock, 0); + mPoly1305.Init(new KeyParameter(firstBlock, 0, 32)); + } + finally + { + Array.Clear(firstBlock, 0, 64); + } + } + + private void PadMac(ulong count) + { + var partial = (int)count % MacSize; + if (0 != partial) + { + mPoly1305.BlockUpdate(Zeroes, 0, MacSize - partial); + } + } + + private void ProcessData(byte[] inBytes, int inOff, int inLen, byte[] outBytes, int outOff) + { + Check.OutputLength(outBytes, outOff, inLen, "output buffer too short"); + + mChacha20.ProcessBytes(inBytes, inOff, inLen, outBytes, outOff); + + mDataCount = IncrementCount(mDataCount, (uint)inLen, DataLimit); + } + + private void Reset(bool clearMac, bool resetCipher) + { + Array.Clear(mBuf, 0, mBuf.Length); + + if (clearMac) + { + Array.Clear(mMac, 0, mMac.Length); + } + + mAadCount = 0UL; + mDataCount = 0UL; + mBufPos = 0; + + switch (mState) + { + case State.DecInit: + case State.EncInit: + break; + case State.DecAad: + case State.DecData: + case State.DecFinal: + mState = State.DecInit; + break; + case State.EncAad: + case State.EncData: + case State.EncFinal: + mState = State.EncFinal; + return; + default: + throw new InvalidOperationException(); + } + + if (resetCipher) + { + mChacha20.Reset(); + } + + InitMac(); + + if (null != mInitialAad) + { + ProcessAadBytes(mInitialAad, 0, mInitialAad.Length); + } + } + } +}