Browse Source

Add Extensions

pull/3006/head
Bruce Wayne 3 years ago
parent
commit
47083a0e5d
No known key found for this signature in database GPG Key ID: 6D396097F05051BF
7 changed files with 1575 additions and 0 deletions
  1. +23
    -0
      Shadowsocks.Net/Crypto/Extensions/Check.cs
  2. +264
    -0
      Shadowsocks.Net/Crypto/Extensions/MyCfbBlockCipher.cs
  3. +183
    -0
      Shadowsocks.Net/Crypto/Extensions/MyChaChaEngine.cs
  4. +363
    -0
      Shadowsocks.Net/Crypto/Extensions/MySalsa20Engine.cs
  5. +56
    -0
      Shadowsocks.Net/Crypto/Extensions/Pack.cs
  6. +98
    -0
      Shadowsocks.Net/Crypto/Extensions/XChaCha20Engine.cs
  7. +588
    -0
      Shadowsocks.Net/Crypto/Extensions/XChaCha20Poly1305.cs

+ 23
- 0
Shadowsocks.Net/Crypto/Extensions/Check.cs View File

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

+ 264
- 0
Shadowsocks.Net/Crypto/Extensions/MyCfbBlockCipher.cs View File

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

+ 183
- 0
Shadowsocks.Net/Crypto/Extensions/MyChaChaEngine.cs View File

@@ -0,0 +1,183 @@
using System;

namespace Shadowsocks.Net.Crypto.Extensions
{
/// <summary>
/// Implementation of Daniel J. Bernstein's ChaCha stream cipher.
/// </summary>
public class MyChaChaEngine : MySalsa20Engine
{
/// <summary>
/// Creates a ChaCha engine with a specific number of rounds.
/// </summary>
/// <param name="rounds">the number of rounds (must be an even number).</param>
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);
}

/// <summary>
/// ChaCha function.
/// </summary>
/// <param name="rounds">The number of ChaCha rounds to execute</param>
/// <param name="input">The input words.</param>
/// <param name="x">The ChaCha state to modify.</param>
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];
}
}
}

+ 363
- 0
Shadowsocks.Net/Crypto/Extensions/MySalsa20Engine.cs View File

@@ -0,0 +1,363 @@
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Utilities;
using System;

namespace Shadowsocks.Net.Crypto.Extensions
{
/// <summary>
/// Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005
/// </summary>
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;

/// <summary>
/// Creates a Salsa20 engine with a specific number of rounds.
/// </summary>
/// <param name="rounds">the number of rounds (must be an even number).</param>
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;
}
}
}

+ 56
- 0
Shadowsocks.Net/Crypto/Extensions/Pack.cs View File

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

+ 98
- 0
Shadowsocks.Net/Crypto/Extensions/XChaCha20Engine.cs View File

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

+ 588
- 0
Shadowsocks.Net/Crypto/Extensions/XChaCha20Poly1305.cs View File

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

Loading…
Cancel
Save