MysticBoy 4 years ago
parent
commit
eec839b3f8
19 changed files with 807 additions and 538 deletions
  1. +12
    -1
      appveyor.yml
  2. +9
    -14
      shadowsocks-csharp/Controller/Service/TCPRelay.cs
  3. +109
    -0
      shadowsocks-csharp/Encryption/AEAD/AEADAesGcmNativeEncryptor.cs
  4. +41
    -22
      shadowsocks-csharp/Encryption/AEAD/AEADBouncyCastleEncryptor.cs
  5. +152
    -86
      shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs
  6. +81
    -0
      shadowsocks-csharp/Encryption/AEAD/AEADNaClEncryptor.cs
  7. +0
    -48
      shadowsocks-csharp/Encryption/AEAD/AEADNativeEncryptor.cs
  8. +91
    -0
      shadowsocks-csharp/Encryption/CipherInfo.cs
  9. +3
    -64
      shadowsocks-csharp/Encryption/EncryptorBase.cs
  10. +48
    -14
      shadowsocks-csharp/Encryption/EncryptorFactory.cs
  11. +3
    -2
      shadowsocks-csharp/Encryption/IEncryptor.cs
  12. +37
    -25
      shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs
  13. +116
    -0
      shadowsocks-csharp/Encryption/Stream/StreamRc4NativeEncryptor.cs
  14. +11
    -103
      shadowsocks-csharp/Encryption/Stream/StreamTableNativeEncryptor.cs
  15. +4
    -90
      shadowsocks-csharp/View/ConfigForm.cs
  16. +5
    -0
      shadowsocks-csharp/packages.config
  17. +11
    -4
      shadowsocks-csharp/shadowsocks-csharp.csproj
  18. +70
    -65
      test/CryptographyTest.cs
  19. +4
    -0
      test/ShadowsocksTest.csproj

+ 12
- 1
appveyor.yml View File

@@ -121,14 +121,21 @@ after_build:
$WorkingFolder = "$env:APPVEYOR_BUILD_FOLDER\working"
$ExeFileName = "Shadowsocks-$env:APPVEYOR_BUILD_VERSION-$env:CONFIGURATION.exe"
$DllFileName = "Shadowsocks.dll"
$ExeFile = "$WorkingFolder\$DNVer\$ExeFileName"
$DllFile = "$WorkingFolder\$DNVer\$DllFileName"
$ExeHashFile = "$Exefile.hash"
$DllHashFile = "$DllFile.hash"
New-Item $WorkingFolder -ItemType Directory -Force
Copy-Item $env:APPVEYOR_BUILD_FOLDER\shadowsocks-csharp\bin\$env:CONFIGURATION\net472\Shadowsocks.exe $WorkingFolder\Shadowsocks.exe
Copy-Item $env:APPVEYOR_BUILD_FOLDER\shadowsocks-csharp\bin\$env:CONFIGURATION\netcoreapp3.1\Shadowsocks.exe $WorkingFolder\Shadowsocks.exe
Copy-Item $env:APPVEYOR_BUILD_FOLDER\shadowsocks-csharp\bin\$env:CONFIGURATION\netcoreapp3.1\Shadowsocks.dll $WorkingFolder\Shadowsocks.dll
Copy-Item $WorkingFolder\Shadowsocks.exe $ExeFile
Copy-Item $WorkingFolder\Shadowsocks.dll $DllFile
CalculateHash -file $Exefile | Out-File -FilePath $ExeHashFile
CalculateHash -file $DllFile | Out-File -FilePath $DllHashFile
Push-AppveyorArtifact $ExeFile
Push-AppveyorArtifact $ExeHashFile
@@ -137,13 +144,17 @@ after_build:
if ($env:configuration -eq 'Release')
{
$ReleaseFile = "$WorkingFolder\Shadowsocks.exe"
$ReleaseFile2 = "$WorkingFolder\Shadowsocks.dll"
$HashFile = "$ReleaseFile.hash"
$HashFile2 = "$ReleaseFile2.hash"
$ZipFile = "$WorkingFolder\Shadowsocks-$env:APPVEYOR_BUILD_VERSION.zip"
$ZipHashFile = "$ZipFile.hash"
# Calculate exe Hash and archieve both exe and hash to zip
CalculateHash -file $ReleaseFile | Out-File -FilePath $hashFile
7z a $ZipFile $ReleaseFile
7z a $ZipFile $ReleaseFile2
7z a $ZipFile $HashFile
7z a $ZipFile $HashFile2
Push-AppveyorArtifact $ZipFile
# Calculate zip Hash
CalculateHash -file $ZipFile | Out-File -FilePath $ZipHashFile


+ 9
- 14
shadowsocks-csharp/Controller/Service/TCPRelay.cs View File

@@ -148,7 +148,9 @@ namespace Shadowsocks.Controller
private TCPRelay _tcprelay;
private Socket _connection;
private IEncryptor _encryptor;
private IEncryptor encryptor;
// workaround
private IEncryptor decryptor;
private Server _server;
private AsyncSession _currentRemoteSession;
@@ -216,13 +218,14 @@ namespace Shadowsocks.Controller
if (server == null || server.server == "")
throw new ArgumentException("No server configured");
_encryptor = EncryptorFactory.GetEncryptor(server.method, server.password);
encryptor = EncryptorFactory.GetEncryptor(server.method, server.password);
decryptor = EncryptorFactory.GetEncryptor(server.method, server.password);
this._server = server;
/* prepare address buffer length for AEAD */
Logger.Debug($"_addrBufLength={_addrBufLength}");
_encryptor.AddrBufLength = _addrBufLength;
encryptor.AddressBufferLength = _addrBufLength;
decryptor.AddressBufferLength = _addrBufLength;
}
public void Start(byte[] firstPacket, int length)
@@ -272,14 +275,6 @@ namespace Shadowsocks.Controller
Logger.LogUsefulException(e);
}
}
lock (_encryptionLock)
{
lock (_decryptionLock)
{
_encryptor?.Dispose();
}
}
}
private void HandshakeReceive()
@@ -825,7 +820,7 @@ namespace Shadowsocks.Controller
{
try
{
_encryptor.Decrypt(_remoteRecvBuffer, bytesRead, _remoteSendBuffer, out bytesToSend);
decryptor.Decrypt(_remoteRecvBuffer, bytesRead, _remoteSendBuffer, out bytesToSend);
}
catch (CryptoErrorException)
{
@@ -898,7 +893,7 @@ namespace Shadowsocks.Controller
{
try
{
_encryptor.Encrypt(_connetionRecvBuffer, length, _connetionSendBuffer, out bytesToSend);
encryptor.Encrypt(_connetionRecvBuffer, length, _connetionSendBuffer, out bytesToSend);
}
catch (CryptoErrorException)
{


+ 109
- 0
shadowsocks-csharp/Encryption/AEAD/AEADAesGcmNativeEncryptor.cs View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Shadowsocks.Encryption.AEAD
{
public class AEADAesGcmNativeEncryptor : AEADEncryptor
{

public AEADAesGcmNativeEncryptor(string method, string password) : base(method, password)
{
}

private static readonly Dictionary<string, CipherInfo> _ciphers = new Dictionary<string, CipherInfo>
{
{"aes-128-gcm", new CipherInfo("aes-128-gcm", 16, 16, 12, 16, CipherFamily.AesGcm)},
{"aes-192-gcm", new CipherInfo("aes-192-gcm", 24, 24, 12, 16, CipherFamily.AesGcm)},
{"aes-256-gcm", new CipherInfo("aes-256-gcm", 32, 32, 12, 16, CipherFamily.AesGcm)},
};

protected override Dictionary<string, CipherInfo> getCiphers()
{
return _ciphers;
}

public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp)
{
base.InitCipher(salt, isEncrypt, isUdp);

DeriveSessionKey(isEncrypt ? encryptSalt : decryptSalt,
_Masterkey, sessionKey);
}

public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen)
{
using (var aes = new AesGcm(sessionKey))
{
byte[] tag = new byte[tagLen];
byte[] cipherWithoutTag = new byte[clen];
Array.Copy(ciphertext, 0, cipherWithoutTag, 0, clen - tagLen);
Array.Copy(ciphertext, clen - tagLen, tag, 0, tagLen);
aes.Decrypt(decNonce, ciphertext, cipherWithoutTag, tag);
}
/*var cipher = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(_sessionKey), tagLen * 8, _decNonce);

cipher.Init(false, parameters);
var plaintextBC = new byte[cipher.GetOutputSize((int)clen)];
var len = cipher.ProcessBytes(ciphertext, 0, (int)clen, plaintextBC, 0);
cipher.DoFinal(plaintextBC, len);
plen = (uint)(plaintextBC.Length);
Array.Copy(plaintextBC, 0, plaintext, 0, plen);*/
}

public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen)
{
using (var aes = new AesGcm(sessionKey))
{
byte[] tag = new byte[tagLen];
byte[] cipherWithoutTag = new byte[clen];
aes.Encrypt(encNonce, plaintext, cipherWithoutTag, tag);
cipherWithoutTag.CopyTo(ciphertext, 0);
tag.CopyTo(ciphertext, clen);
}

/*
var cipher = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(_sessionKey), tagLen * 8, _encNonce);

cipher.Init(true, parameters);
var ciphertextBC = new byte[cipher.GetOutputSize((int)plen)];
var len = cipher.ProcessBytes(plaintext, 0, (int)plen, ciphertextBC, 0);
cipher.DoFinal(ciphertextBC, len);
clen = (uint)(ciphertextBC.Length);
Array.Copy(ciphertextBC, 0, ciphertext, 0, clen);*/
}

public override byte[] CipherDecrypt2(byte[] cipher)
{
var (cipherMem, tagMem) = GetCipherTextAndTagMem(cipher);
Span<byte> plainMem = new Span<byte>(new byte[cipherMem.Length]);

using var aes = new AesGcm(sessionKey);
aes.Decrypt(decNonce.AsSpan(), cipherMem.Span, tagMem.Span, plainMem);
return plainMem.ToArray();
}

public override byte[] CipherEncrypt2(byte[] plain)
{
Span<byte> d = new Span<byte>(new byte[plain.Length + tagLen]);

//Span<byte> cipherMem = new Span<byte>(new byte[plain.Length]);
//Span<byte> tagMem = new Span<byte>(new byte[tagLen]);

using var aes = new AesGcm(sessionKey);
aes.Encrypt(encNonce.AsSpan(), plain.AsSpan(), d.Slice(0, plain.Length), d.Slice(plain.Length));

return d.ToArray();
}

public static Dictionary<string, CipherInfo> SupportedCiphers()
{
return _ciphers;
}
}
}

+ 41
- 22
shadowsocks-csharp/Encryption/AEAD/AEADBouncyCastleEncryptor.cs View File

@@ -9,24 +9,21 @@ using System.Threading.Tasks;
namespace Shadowsocks.Encryption.AEAD
{
public class AEADBouncyCastleEncryptor : AEADEncryptor, IDisposable
public class AEADBouncyCastleEncryptor : AEADEncryptor
{
static int CIPHER_AES = 1; // dummy
public AEADBouncyCastleEncryptor(string method, string password)
: base(method, password)
public AEADBouncyCastleEncryptor(string method, string password) : base(method, password)
{
}
private static readonly Dictionary<string, EncryptorInfo> _ciphers = new Dictionary<string, EncryptorInfo>
private static readonly Dictionary<string, CipherInfo> _ciphers = new Dictionary<string, CipherInfo>
{
{"aes-128-gcm", new EncryptorInfo("AES-128-GCM", 16, 16, 12, 16, CIPHER_AES)},
{"aes-192-gcm", new EncryptorInfo("AES-192-GCM", 24, 24, 12, 16, CIPHER_AES)},
{"aes-256-gcm", new EncryptorInfo("AES-256-GCM", 32, 32, 12, 16, CIPHER_AES)},
{"aes-128-gcm", new CipherInfo("aes-128-gcm", 16, 16, 12, 16, CipherFamily.AesGcm)},
{"aes-192-gcm", new CipherInfo("aes-192-gcm", 24, 24, 12, 16, CipherFamily.AesGcm)},
{"aes-256-gcm", new CipherInfo("aes-256-gcm", 32, 32, 12, 16, CipherFamily.AesGcm)},
};
protected override Dictionary<string, EncryptorInfo> getCiphers()
protected override Dictionary<string, CipherInfo> getCiphers()
{
return _ciphers;
}
@@ -35,27 +32,27 @@ namespace Shadowsocks.Encryption.AEAD
{
base.InitCipher(salt, isEncrypt, isUdp);
DeriveSessionKey(isEncrypt ? _encryptSalt : _decryptSalt,
_Masterkey, _sessionKey);
DeriveSessionKey(isEncrypt ? encryptSalt : decryptSalt,
_Masterkey, sessionKey);
}
public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen)
{
var cipher = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(_sessionKey), tagLen * 8, _decNonce);
AeadParameters parameters = new AeadParameters(new KeyParameter(sessionKey), tagLen * 8, decNonce);
cipher.Init(false, parameters);
var plaintextBC = new byte[cipher.GetOutputSize((int)clen)];
var len = cipher.ProcessBytes(ciphertext, 0, (int)clen, plaintextBC, 0);
cipher.DoFinal(plaintextBC, len);
plen = (uint)(plaintextBC.Length);
Array.Copy(plaintextBC, 0, plaintext, 0, plen);
//var plaintextBC = new byte[cipher.GetOutputSize(ciphertext.Length)];
var len = cipher.ProcessBytes(ciphertext, 0, ciphertext.Length, plaintext, 0);
cipher.DoFinal(plaintext, len);
//plen = (uint)(plaintext.Length);
//Array.Copy(plaintextBC, 0, plaintext, 0, plaintext.Length);
}
public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen)
{
var cipher = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(_sessionKey), tagLen * 8, _encNonce);
AeadParameters parameters = new AeadParameters(new KeyParameter(sessionKey), tagLen * 8, encNonce);
cipher.Init(true, parameters);
var ciphertextBC = new byte[cipher.GetOutputSize((int)plen)];
@@ -65,13 +62,35 @@ namespace Shadowsocks.Encryption.AEAD
Array.Copy(ciphertextBC, 0, ciphertext, 0, clen);
}
public static List<string> SupportedCiphers()
public override byte[] CipherDecrypt2(byte[] cipher)
{
return new List<string>(_ciphers.Keys);
var aes = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(sessionKey), tagLen * 8, decNonce);
aes.Init(false, parameters);
byte[] plain = new byte[aes.GetOutputSize(cipher.Length)];
var len = aes.ProcessBytes(cipher, 0, cipher.Length, plain, 0);
aes.DoFinal(plain, len);
return plain;
}
public override void Dispose()
public override byte[] CipherEncrypt2(byte[] plain)
{
var aes = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(sessionKey), tagLen * 8, encNonce);
aes.Init(true, parameters);
var cipher = new byte[aes.GetOutputSize(plain.Length)];
var len = aes.ProcessBytes(plain, 0, plain.Length, cipher, 0);
aes.DoFinal(cipher, len);
return cipher;
}
public static Dictionary<string, CipherInfo> SupportedCiphers()
{
return _ciphers;
}
}
}

+ 152
- 86
shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs View File

@@ -11,8 +11,7 @@ using Shadowsocks.Encryption.Stream;
namespace Shadowsocks.Encryption.AEAD
{
public abstract class AEADEncryptor
: EncryptorBase
public abstract class AEADEncryptor : EncryptorBase
{
private static Logger logger = LogManager.GetCurrentClassLogger();
// We are using the same saltLen and keyLen
@@ -29,26 +28,27 @@ namespace Shadowsocks.Encryption.AEAD
public const int CHUNK_LEN_BYTES = 2;
public const uint CHUNK_LEN_MASK = 0x3FFFu;
protected Dictionary<string, EncryptorInfo> ciphers;
protected Dictionary<string, CipherInfo> ciphers;
protected string _method;
protected int _cipher;
protected CipherFamily _cipher;
// internal name in the crypto library
protected string _innerLibName;
protected EncryptorInfo CipherInfo;
protected CipherInfo CipherInfo;
protected static byte[] _Masterkey = null;
protected byte[] _sessionKey;
protected byte[] sessionKey;
protected int keyLen;
protected int saltLen;
protected int tagLen;
protected int nonceLen;
protected byte[] _encryptSalt;
protected byte[] _decryptSalt;
protected byte[] encryptSalt;
protected byte[] decryptSalt;
protected object _nonceIncrementLock = new object();
protected byte[] _encNonce;
protected byte[] _decNonce;
protected byte[] encNonce;
protected byte[] decNonce;
// Is first packet
protected bool _decryptSaltReceived;
protected bool _encryptSaltSent;
@@ -62,11 +62,11 @@ namespace Shadowsocks.Encryption.AEAD
InitEncryptorInfo(method);
InitKey(password);
// Initialize all-zero nonce for each connection
_encNonce = new byte[nonceLen];
_decNonce = new byte[nonceLen];
encNonce = new byte[nonceLen];
decNonce = new byte[nonceLen];
}
protected abstract Dictionary<string, EncryptorInfo> getCiphers();
protected abstract Dictionary<string, CipherInfo> getCiphers();
protected void InitEncryptorInfo(string method)
{
@@ -74,15 +74,12 @@ namespace Shadowsocks.Encryption.AEAD
_method = method;
ciphers = getCiphers();
CipherInfo = ciphers[_method];
_innerLibName = CipherInfo.InnerLibName;
_cipher = CipherInfo.Type;
if (_cipher == 0) {
throw new System.Exception("method not found");
}
keyLen = CipherInfo.KeySize;
saltLen = CipherInfo.SaltSize;
tagLen = CipherInfo.TagSize;
nonceLen = CipherInfo.NonceSize;
var parameter = (AEADCipherParameter)CipherInfo.CipherParameter;
keyLen = parameter.KeySize;
saltLen = parameter.SaltSize;
tagLen = parameter.TagSize;
nonceLen = parameter.NonceSize;
}
protected void InitKey(string password)
@@ -93,7 +90,7 @@ namespace Shadowsocks.Encryption.AEAD
if (_Masterkey.Length != keyLen) Array.Resize(ref _Masterkey, keyLen);
DeriveKey(passbuf, _Masterkey, keyLen);
// init session key
if (_sessionKey == null) _sessionKey = new byte[keyLen];
if (sessionKey == null) sessionKey = new byte[keyLen];
}
public void DeriveKey(byte[] password, byte[] key, int keylen)
@@ -103,32 +100,69 @@ namespace Shadowsocks.Encryption.AEAD
public void DeriveSessionKey(byte[] salt, byte[] masterKey, byte[] sessionKey)
{
CryptoUtils.HKDF(keyLen, masterKey, salt, InfoBytes).CopyTo(sessionKey,0);
CryptoUtils.HKDF(keyLen, masterKey, salt, InfoBytes).CopyTo(sessionKey, 0);
}
protected void IncrementNonce(bool isEncrypt)
{
lock (_nonceIncrementLock) {
CryptoUtils.SodiumIncrement(isEncrypt ? _encNonce : _decNonce);
lock (_nonceIncrementLock)
{
CryptoUtils.SodiumIncrement(isEncrypt ? encNonce : decNonce);
}
}
public virtual void InitCipher(byte[] salt, bool isEncrypt, bool isUdp)
{
if (isEncrypt) {
_encryptSalt = new byte[saltLen];
Array.Copy(salt, _encryptSalt, saltLen);
} else {
_decryptSalt = new byte[saltLen];
Array.Copy(salt, _decryptSalt, saltLen);
if (isEncrypt)
{
encryptSalt = new byte[saltLen];
Array.Copy(salt, encryptSalt, saltLen);
}
else
{
decryptSalt = new byte[saltLen];
Array.Copy(salt, decryptSalt, saltLen);
}
logger.Dump("Salt", salt, saltLen);
}
public static void randBytes(byte[] buf, int length) { RNG.GetBytes(buf, length); }
/// <summary>
///
/// </summary>
/// <param name="plaintext">Input, plain text</param>
/// <param name="plen">plaintext.Length</param>
/// <param name="ciphertext">Output, allocated by caller, tag space included,
/// length = plaintext.Length + tagLen, [enc][tag] order</param>
/// <param name="clen">Should be same as ciphertext.Length</param>
public abstract void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen);
// plain -> cipher + tag
public abstract byte[] CipherEncrypt2(byte[] plain);
// cipher + tag -> plain
public abstract byte[] CipherDecrypt2(byte[] cipher);
public (Memory<byte>, Memory<byte>) GetCipherTextAndTagMem(byte[] cipher)
{
var mc = cipher.AsMemory();
var clen = mc.Length - tagLen;
var c = mc.Slice(0, clen);
var t = mc.Slice(clen);
return (c, t);
}
public (byte[], byte[]) GetCipherTextAndTag(byte[] cipher)
{
var (c, t) = GetCipherTextAndTagMem(cipher);
return (c.ToArray(), t.ToArray());
}
/// <summary>
///
/// </summary>
/// <param name="ciphertext">Cipher text in [enc][tag] order</param>
/// <param name="clen">ciphertext.Length</param>
/// <param name="plaintext">Output plain text may with additional data allocated by caller</param>
/// <param name="plen">Output, should be used plain text length</param>
public abstract void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen);
#region TCP
@@ -140,32 +174,35 @@ namespace Shadowsocks.Encryption.AEAD
_encCircularBuffer.Put(buf, 0, length);
outlength = 0;
logger.Debug("---Start Encryption");
if (! _encryptSaltSent) {
if (!_encryptSaltSent)
{
_encryptSaltSent = true;
// Generate salt
byte[] saltBytes = new byte[saltLen];
randBytes(saltBytes, saltLen);
RNG.GetBytes(saltBytes, saltLen);
InitCipher(saltBytes, true, false);
Array.Copy(saltBytes, 0, outbuf, 0, saltLen);
outlength = saltLen;
logger.Debug($"_encryptSaltSent outlength {outlength}");
}
if (! _tcpRequestSent) {
if (!_tcpRequestSent)
{
_tcpRequestSent = true;
// The first TCP request
int encAddrBufLength;
byte[] encAddrBufBytes = new byte[AddrBufLength + tagLen * 2 + CHUNK_LEN_BYTES];
byte[] addrBytes = _encCircularBuffer.Get(AddrBufLength);
ChunkEncrypt(addrBytes, AddrBufLength, encAddrBufBytes, out encAddrBufLength);
Debug.Assert(encAddrBufLength == AddrBufLength + tagLen * 2 + CHUNK_LEN_BYTES);
byte[] encAddrBufBytes = new byte[AddressBufferLength + tagLen * 2 + CHUNK_LEN_BYTES];
byte[] addrBytes = _encCircularBuffer.Get(AddressBufferLength);
ChunkEncrypt(addrBytes, AddressBufferLength, encAddrBufBytes, out encAddrBufLength);
Debug.Assert(encAddrBufLength == AddressBufferLength + tagLen * 2 + CHUNK_LEN_BYTES);
Array.Copy(encAddrBufBytes, 0, outbuf, outlength, encAddrBufLength);
outlength += encAddrBufLength;
logger.Debug($"_tcpRequestSent outlength {outlength}");
}
// handle other chunks
while (true) {
while (true)
{
uint bufSize = (uint)_encCircularBuffer.Size;
if (bufSize <= 0) return;
var chunklength = (int)Math.Min(bufSize, CHUNK_LEN_MASK);
@@ -178,12 +215,14 @@ namespace Shadowsocks.Encryption.AEAD
outlength += encChunkLength;
logger.Debug("chunks enc outlength " + outlength);
// check if we have enough space for outbuf
if (outlength + TCPHandler.ChunkOverheadSize > TCPHandler.BufferSize) {
if (outlength + TCPHandler.ChunkOverheadSize > TCPHandler.BufferSize)
{
logger.Debug("enc outbuf almost full, giving up");
return;
}
bufSize = (uint)_encCircularBuffer.Size;
if (bufSize <= 0) {
if (bufSize <= 0)
{
logger.Debug("No more data to encrypt, leaving");
return;
}
@@ -200,10 +239,12 @@ namespace Shadowsocks.Encryption.AEAD
_decCircularBuffer.Put(buf, 0, length);
logger.Debug("---Start Decryption");
if (! _decryptSaltReceived) {
if (!_decryptSaltReceived)
{
bufSize = _decCircularBuffer.Size;
// check if we get the leading salt
if (bufSize <= saltLen) {
if (bufSize <= saltLen)
{
// need more
return;
}
@@ -214,16 +255,19 @@ namespace Shadowsocks.Encryption.AEAD
}
// handle chunks
while (true) {
while (true)
{
bufSize = _decCircularBuffer.Size;
// check if we have any data
if (bufSize <= 0) {
if (bufSize <= 0)
{
logger.Debug("No data in _decCircularBuffer");
return;
}
// first get chunk length
if (bufSize <= CHUNK_LEN_BYTES + tagLen) {
if (bufSize <= CHUNK_LEN_BYTES + tagLen)
{
// so we only have chunk length and its tag?
return;
}
@@ -231,13 +275,16 @@ namespace Shadowsocks.Encryption.AEAD
#region Chunk Decryption
byte[] encLenBytes = _decCircularBuffer.Peek(CHUNK_LEN_BYTES + tagLen);
uint decChunkLenLength = 0;
byte[] decChunkLenBytes = new byte[CHUNK_LEN_BYTES];
//uint decChunkLenLength = 0;
//byte[] decChunkLenBytes = new byte[CHUNK_LEN_BYTES];
// try to dec chunk len
cipherDecrypt(encLenBytes, CHUNK_LEN_BYTES + (uint)tagLen, decChunkLenBytes, ref decChunkLenLength);
Debug.Assert(decChunkLenLength == CHUNK_LEN_BYTES);
//cipherDecrypt(encLenBytes, CHUNK_LEN_BYTES + (uint)tagLen, decChunkLenBytes, ref decChunkLenLength);
byte[] decChunkLenBytes = CipherDecrypt2(encLenBytes);
// Debug.Assert(decChunkLenLength == CHUNK_LEN_BYTES);
// finally we get the real chunk len
ushort chunkLen = (ushort) IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(decChunkLenBytes, 0));
ushort chunkLen = (ushort)IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(decChunkLenBytes, 0));
if (chunkLen > CHUNK_LEN_MASK)
{
// we get invalid chunk
@@ -246,7 +293,8 @@ namespace Shadowsocks.Encryption.AEAD
}
logger.Debug("Get the real chunk len:" + chunkLen);
bufSize = _decCircularBuffer.Size;
if (bufSize < CHUNK_LEN_BYTES + tagLen /* we haven't remove them */+ chunkLen + tagLen) {
if (bufSize < CHUNK_LEN_BYTES + tagLen /* we haven't remove them */+ chunkLen + tagLen)
{
logger.Debug("No more data to decrypt one chunk");
return;
}
@@ -256,17 +304,19 @@ namespace Shadowsocks.Encryption.AEAD
// drop chunk len and its tag from buffer
_decCircularBuffer.Skip(CHUNK_LEN_BYTES + tagLen);
byte[] encChunkBytes = _decCircularBuffer.Get(chunkLen + tagLen);
byte[] decChunkBytes = new byte[chunkLen];
uint decChunkLen = 0;
cipherDecrypt(encChunkBytes, chunkLen + (uint)tagLen, decChunkBytes, ref decChunkLen);
Debug.Assert(decChunkLen == chunkLen);
//byte[] decChunkBytes = new byte[chunkLen];
//uint decChunkLen = 0;
//cipherDecrypt(encChunkBytes, chunkLen + (uint)tagLen, decChunkBytes, ref decChunkLen);
byte[] decChunkBytes = CipherDecrypt2(encChunkBytes);
//Debug.Assert(decChunkLen == chunkLen);
IncrementNonce(false);
#endregion
// output to outbuf
Buffer.BlockCopy(decChunkBytes, 0, outbuf, outlength, (int) decChunkLen);
outlength += (int)decChunkLen;
decChunkBytes.CopyTo(outbuf, outlength);
// Buffer.BlockCopy(decChunkBytes, 0, outbuf, outlength, (int)decChunkLen);
outlength += decChunkBytes.Length;
logger.Debug("aead dec outlength " + outlength);
if (outlength + 100 > TCPHandler.BufferSize)
{
@@ -275,7 +325,8 @@ namespace Shadowsocks.Encryption.AEAD
}
bufSize = _decCircularBuffer.Size;
// check if we already done all of them
if (bufSize <= 0) {
if (bufSize <= 0)
{
logger.Debug("No data in _decCircularBuffer, already all done");
return;
}
@@ -289,59 +340,74 @@ namespace Shadowsocks.Encryption.AEAD
public override void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength)
{
// Generate salt
randBytes(outbuf, saltLen);
RNG.GetBytes(outbuf, saltLen);
InitCipher(outbuf, true, true);
uint olen = 0;
lock (_udpTmpBuf) {
cipherEncrypt(buf, (uint) length, _udpTmpBuf, ref olen);
Debug.Assert(olen == length + tagLen);
Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, saltLen, (int) olen);
outlength = (int) (saltLen + olen);
//uint olen = 0;
lock (_udpTmpBuf)
{
//cipherEncrypt(buf, (uint)length, _udpTmpBuf, ref olen);
var plain = buf.AsSpan().Slice(0, length).ToArray(); // mmp
var cipher = CipherEncrypt2(plain);
//Debug.Assert(olen == length + tagLen);
Buffer.BlockCopy(cipher, 0, outbuf, saltLen, length + tagLen);
//Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, saltLen, (int)olen);
outlength = (int)(saltLen + cipher.Length);
}
}
public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength)
{
InitCipher(buf, false, true);
uint olen = 0;
lock (_udpTmpBuf) {
//uint olen = 0;
lock (_udpTmpBuf)
{
// copy remaining data to first pos
Buffer.BlockCopy(buf, saltLen, buf, 0, length - saltLen);
cipherDecrypt(buf, (uint) (length - saltLen), _udpTmpBuf, ref olen);
Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, 0, (int) olen);
outlength = (int) olen;
byte[] b = buf.AsSpan().Slice(0, length - saltLen).ToArray();
byte[] o = CipherDecrypt2(b);
//cipherDecrypt(buf, (uint)(length - saltLen), _udpTmpBuf, ref olen);
Buffer.BlockCopy(o, 0, outbuf, 0, o.Length);
outlength = o.Length;
}
}
#endregion
// we know the plaintext length before encryption, so we can do it in one operation
// plain -> [len][data]
private void ChunkEncrypt(byte[] plaintext, int plainLen, byte[] ciphertext, out int cipherLen)
{
if (plainLen > CHUNK_LEN_MASK) {
if (plainLen > CHUNK_LEN_MASK)
{
logger.Error("enc chunk too big");
throw new CryptoErrorException();
}
// encrypt len
byte[] encLenBytes = new byte[CHUNK_LEN_BYTES + tagLen];
uint encChunkLenLength = 0;
byte[] lenbuf = BitConverter.GetBytes((ushort) IPAddress.HostToNetworkOrder((short)plainLen));
cipherEncrypt(lenbuf, CHUNK_LEN_BYTES, encLenBytes, ref encChunkLenLength);
Debug.Assert(encChunkLenLength == CHUNK_LEN_BYTES + tagLen);
//byte[] encLenBytes = new byte[CHUNK_LEN_BYTES + tagLen];
//uint encChunkLenLength = 0;
// always 2 byte
byte[] lenbuf = BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)plainLen));
//cipherEncrypt(lenbuf, CHUNK_LEN_BYTES, encLenBytes, ref encChunkLenLength);
byte[] encLenBytes = CipherEncrypt2(lenbuf);
//Debug.Assert(encChunkLenLength == CHUNK_LEN_BYTES + tagLen);
IncrementNonce(true);
// encrypt corresponding data
byte[] encBytes = new byte[plainLen + tagLen];
uint encBufLength = 0;
cipherEncrypt(plaintext, (uint) plainLen, encBytes, ref encBufLength);
Debug.Assert(encBufLength == plainLen + tagLen);
//byte[] encBytes = new byte[plainLen + tagLen];
//uint encBufLength = 0;
//cipherEncrypt(plaintext, (uint)plainLen, encBytes, ref encBufLength);
byte[] encBytes = CipherEncrypt2(plaintext);
//Debug.Assert(encBufLength == plainLen + tagLen);
IncrementNonce(true);
// construct outbuf
Array.Copy(encLenBytes, 0, ciphertext, 0, (int) encChunkLenLength);
Buffer.BlockCopy(encBytes, 0, ciphertext, (int) encChunkLenLength, (int) encBufLength);
cipherLen = (int) (encChunkLenLength + encBufLength);
encLenBytes.CopyTo(ciphertext, 0);
encBytes.CopyTo(ciphertext, encLenBytes.Length);
//Array.Copy(encLenBytes, 0, ciphertext, 0, (int)encChunkLenLength);
//Buffer.BlockCopy(encBytes, 0, ciphertext, (int)encChunkLenLength, (int)encBufLength);
cipherLen = encLenBytes.Length + encBytes.Length;
}
}
}

+ 81
- 0
shadowsocks-csharp/Encryption/AEAD/AEADNaClEncryptor.cs View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NaCl.Core;
using NaCl.Core.Base;

namespace Shadowsocks.Encryption.AEAD
{
public class AEADNaClEncryptor : AEADEncryptor
{

SnufflePoly1305 enc;
SnufflePoly1305 dec;
public AEADNaClEncryptor(string method, string password) : base(method, password)
{
}

public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp)
{
base.InitCipher(salt, isEncrypt, isUdp);
DeriveSessionKey(isEncrypt ? encryptSalt : decryptSalt,
_Masterkey, sessionKey);

SnufflePoly1305 tmp;
switch (_cipher)
{
default:
case CipherFamily.Chacha20Poly1305:
tmp = new ChaCha20Poly1305(sessionKey);
break;
case CipherFamily.XChacha20Poly1305:
tmp = new XChaCha20Poly1305(sessionKey);
break;
}
if (isEncrypt) enc = tmp;
else dec = tmp;
}

public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen)
{
var pt = dec.Decrypt(ciphertext, null, decNonce);
pt.CopyTo(plaintext, 0);
plen = (uint)pt.Length;
}

public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen)
{
var ct = enc.Encrypt(plaintext, null, encNonce);
ct.CopyTo(ciphertext, 0);
clen = (uint)ct.Length;
}

private static readonly Dictionary<string, CipherInfo> _ciphers = new Dictionary<string, CipherInfo>
{
{"chacha20-ietf-poly1305", new CipherInfo("chacha20-ietf-poly1305",32, 32, 12, 16, CipherFamily.Chacha20Poly1305)},
{"xchacha20-ietf-poly1305", new CipherInfo("xchacha20-ietf-poly1305",32, 32, 24, 16, CipherFamily.XChacha20Poly1305)},
};

protected override Dictionary<string, CipherInfo> getCiphers()
{
return _ciphers;
}

public static Dictionary<string, CipherInfo> SupportedCiphers()
{
return _ciphers;
}

public override byte[] CipherEncrypt2(byte[] plain)
{
return enc.Encrypt(plain, null, encNonce);
}

public override byte[] CipherDecrypt2(byte[] cipher)
{
return dec.Decrypt(cipher, null, decNonce);
}
}
}

+ 0
- 48
shadowsocks-csharp/Encryption/AEAD/AEADNativeEncryptor.cs View File

@@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;

namespace Shadowsocks.Encryption.AEAD
{
public class AEADNativeEncryptor : AEADEncryptor
{
public AEADNativeEncryptor(string method, string password)
: base(method, password)
{
}

public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen)
{
Array.Copy(ciphertext, plaintext, 0);
plen = clen;

}

public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen)
{
Array.Copy(plaintext, ciphertext, 0);
clen = plen;
}

public override void Dispose()
{
return;
}

private static Dictionary<string, EncryptorInfo> _ciphers = new Dictionary<string, EncryptorInfo>()
{
{"plain-fake-aead",new EncryptorInfo("PLAIN_AEAD",0,0,0,0,0) }
};


protected override Dictionary<string, EncryptorInfo> getCiphers()
{
return _ciphers;
}

public static IEnumerable<string> SupportedCiphers()
{
return _ciphers.Keys;
}

}
}

+ 91
- 0
shadowsocks-csharp/Encryption/CipherInfo.cs View File

@@ -0,0 +1,91 @@
using Shadowsocks.Controller;
using System;

namespace Shadowsocks.Encryption
{
public enum CipherFamily
{
Plain,
Table,

AesGcm,

AesCfb,
AesCtr,

Chacha20,
Chacha20Poly1305,
XChacha20Poly1305,

Rc4,
Rc4Md5,
}

public enum CipherStandardState
{
InUse,
Deprecated,
Hidden,
}

public class CipherParameter
{
public int KeySize;
}
public class StreamCipherParameter : CipherParameter
{
public int IvSize;
}

public class AEADCipherParameter : CipherParameter
{
public int SaltSize;
public int TagSize;
public int NonceSize;
}

public class CipherInfo
{
public string Name;
public CipherFamily Type;
public CipherParameter CipherParameter;

public CipherStandardState StandardState = CipherStandardState.InUse;

#region Stream ciphers
public CipherInfo(string name, int keySize, int ivSize, CipherFamily type)
{
Type = type;
Name = name;
StandardState = CipherStandardState.Hidden;
CipherParameter = new StreamCipherParameter
{
KeySize = keySize,
IvSize = ivSize,
};
}

#endregion

#region AEAD ciphers
public CipherInfo(string name, int keySize, int saltSize, int nonceSize, int tagSize, CipherFamily type)
{
Type = type;
Name = name;

CipherParameter = new AEADCipherParameter
{
KeySize = keySize,
SaltSize = saltSize,
NonceSize = nonceSize,
TagSize = tagSize,
};
}
#endregion

public override string ToString()
{
return StandardState == CipherStandardState.InUse ? Name : $"{Name} ({I18N.GetString("deprecated")})";
}
}
}

+ 3
- 64
shadowsocks-csharp/Encryption/EncryptorBase.cs View File

@@ -1,65 +1,6 @@
namespace Shadowsocks.Encryption
{
public class EncryptorInfo
{
public int KeySize;
public int IvSize;
public int SaltSize;
public int TagSize;
public int NonceSize;
public int Type;
public string InnerLibName;
// For those who make use of internal crypto method name
// e.g. mbed TLS
#region Stream ciphers
public EncryptorInfo(string innerLibName, int keySize, int ivSize, int type)
{
this.KeySize = keySize;
this.IvSize = ivSize;
this.Type = type;
this.InnerLibName = innerLibName;
}
public EncryptorInfo(int keySize, int ivSize, int type)
{
this.KeySize = keySize;
this.IvSize = ivSize;
this.Type = type;
this.InnerLibName = string.Empty;
}
#endregion
#region AEAD ciphers
public EncryptorInfo(string innerLibName, int keySize, int saltSize, int nonceSize, int tagSize, int type)
{
this.KeySize = keySize;
this.SaltSize = saltSize;
this.NonceSize = nonceSize;
this.TagSize = tagSize;
this.Type = type;
this.InnerLibName = innerLibName;
}
public EncryptorInfo(int keySize, int saltSize, int nonceSize, int tagSize, int type)
{
this.KeySize = keySize;
this.SaltSize = saltSize;
this.NonceSize = nonceSize;
this.TagSize = tagSize;
this.Type = type;
this.InnerLibName = string.Empty;
}
#endregion
}
public abstract class EncryptorBase
: IEncryptor
public abstract class EncryptorBase : IEncryptor
{
public const int MAX_INPUT_SIZE = 32768;
@@ -81,7 +22,7 @@
protected string Method;
protected string Password;
public abstract void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength);
public abstract void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength);
@@ -90,8 +31,6 @@
public abstract void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength);
public abstract void Dispose();
public int AddrBufLength { get; set; } = - 1;
public int AddressBufferLength { get; set; } = -1;
}
}

+ 48
- 14
shadowsocks-csharp/Encryption/EncryptorFactory.cs View File

@@ -10,27 +10,51 @@ namespace Shadowsocks.Encryption
public static class EncryptorFactory
{
private static Dictionary<string, Type> _registeredEncryptors = new Dictionary<string, Type>();
private static Dictionary<string, CipherInfo> ciphers = new Dictionary<string, CipherInfo>();
private static readonly Type[] ConstructorTypes = { typeof(string), typeof(string) };
static EncryptorFactory()
{
foreach (string method in StreamNativeEncryptor.SupportedCiphers())
foreach (var method in StreamTableNativeEncryptor.SupportedCiphers())
{
if (!_registeredEncryptors.ContainsKey(method))
_registeredEncryptors.Add(method, typeof(StreamNativeEncryptor));
if (!_registeredEncryptors.ContainsKey(method.Key))
{
ciphers.Add(method.Key, method.Value);
_registeredEncryptors.Add(method.Key, typeof(StreamTableNativeEncryptor));
}
}
foreach (string method in AEADNativeEncryptor.SupportedCiphers())
foreach (var method in StreamRc4NativeEncryptor.SupportedCiphers())
{
if (!_registeredEncryptors.ContainsKey(method))
_registeredEncryptors.Add(method, typeof(AEADNativeEncryptor));
if (!_registeredEncryptors.ContainsKey(method.Key))
{
ciphers.Add(method.Key, method.Value);
_registeredEncryptors.Add(method.Key, typeof(StreamRc4NativeEncryptor));
}
}
foreach (string method in AEADBouncyCastleEncryptor.SupportedCiphers())
foreach (var method in AEADAesGcmNativeEncryptor.SupportedCiphers())
{
if (!_registeredEncryptors.ContainsKey(method.Key))
{
ciphers.Add(method.Key, method.Value);
_registeredEncryptors.Add(method.Key, typeof(AEADAesGcmNativeEncryptor));
}
}
foreach (var method in AEADNaClEncryptor.SupportedCiphers())
{
if (!_registeredEncryptors.ContainsKey(method))
_registeredEncryptors.Add(method, typeof(AEADBouncyCastleEncryptor));
if (!_registeredEncryptors.ContainsKey(method.Key))
{
ciphers.Add(method.Key, method.Value);
_registeredEncryptors.Add(method.Key, typeof(AEADNaClEncryptor));
}
}
foreach (var method in StreamRc4NativeEncryptor.SupportedCiphers())
{
if (!_registeredEncryptors.ContainsKey(method.Key))
{
ciphers.Add(method.Key, method.Value);
_registeredEncryptors.Add(method.Key, typeof(StreamRc4NativeEncryptor));
}
}
}
@@ -55,15 +79,25 @@ namespace Shadowsocks.Encryption
{
var sb = new StringBuilder();
sb.Append(Environment.NewLine);
sb.AppendLine("=========================");
sb.AppendLine("-------------------------");
sb.AppendLine("Registered Encryptor Info");
foreach (var encryptor in _registeredEncryptors)
{
sb.AppendLine(String.Format("{0}=>{1}", encryptor.Key, encryptor.Value.Name));
}
sb.AppendLine("=========================");
// use ----- instead of =======, so when user paste it to Github, it won't became title
sb.AppendLine("-------------------------");
return sb.ToString();
}
public static CipherInfo GetCipherInfo(string name)
{
return ciphers[name];
}
public static IEnumerable<CipherInfo> ListAvaliableCiphers()
{
return ciphers.Values;
}
}
}

+ 3
- 2
shadowsocks-csharp/Encryption/IEncryptor.cs View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
namespace Shadowsocks.Encryption
{
public interface IEncryptor : IDisposable
public interface IEncryptor
{
/* length == -1 means not used */
int AddrBufLength { set; get; }
int AddressBufferLength { set; get; }
void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength);
void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength);
void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength);


+ 37
- 25
shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs View File

@@ -7,8 +7,7 @@ using Shadowsocks.Controller;
namespace Shadowsocks.Encryption.Stream
{
public abstract class StreamEncryptor
: EncryptorBase
public abstract class StreamEncryptor : EncryptorBase
{
// for UDP only
protected static byte[] _udpTmpBuf = new byte[65536];
@@ -17,7 +16,7 @@ namespace Shadowsocks.Encryption.Stream
private ByteCircularBuffer _encCircularBuffer = new ByteCircularBuffer(TCPHandler.BufferSize * 2);
private ByteCircularBuffer _decCircularBuffer = new ByteCircularBuffer(TCPHandler.BufferSize * 2);
protected Dictionary<string, EncryptorInfo> ciphers;
protected Dictionary<string, CipherInfo> ciphers;
protected byte[] _encryptIV;
protected byte[] _decryptIV;
@@ -27,10 +26,10 @@ namespace Shadowsocks.Encryption.Stream
protected bool _encryptIVSent;
protected string _method;
protected int _cipher;
protected CipherFamily _cipher;
// internal name in the crypto library
protected string _innerLibName;
protected EncryptorInfo CipherInfo;
protected CipherInfo CipherInfo;
// long-time master key
protected static byte[] _key = null;
protected int keyLen;
@@ -43,7 +42,7 @@ namespace Shadowsocks.Encryption.Stream
InitKey(password);
}
protected abstract Dictionary<string, EncryptorInfo> getCiphers();
protected abstract Dictionary<string, CipherInfo> getCiphers();
private void InitEncryptorInfo(string method)
{
@@ -51,13 +50,10 @@ namespace Shadowsocks.Encryption.Stream
_method = method;
ciphers = getCiphers();
CipherInfo = ciphers[_method];
_innerLibName = CipherInfo.InnerLibName;
_cipher = CipherInfo.Type;
if (_cipher == 0) {
throw new System.Exception("method not found");
}
keyLen = CipherInfo.KeySize;
ivLen = CipherInfo.IvSize;
var parameter = (StreamCipherParameter)CipherInfo.CipherParameter;
keyLen = parameter.KeySize;
ivLen = parameter.IvSize;
}
private void InitKey(string password)
@@ -73,10 +69,14 @@ namespace Shadowsocks.Encryption.Stream
byte[] result = new byte[password.Length + MD5_LEN];
int i = 0;
byte[] md5sum = null;
while (i < keylen) {
if (i == 0) {
while (i < keylen)
{
if (i == 0)
{
md5sum = CryptoUtils.MD5(password);
} else {
}
else
{
Array.Copy(md5sum, 0, result, 0, MD5_LEN);
Array.Copy(password, 0, result, MD5_LEN, password.Length);
md5sum = CryptoUtils.MD5(result);
@@ -88,10 +88,13 @@ namespace Shadowsocks.Encryption.Stream
protected virtual void initCipher(byte[] iv, bool isEncrypt)
{
if (isEncrypt) {
if (isEncrypt)
{
_encryptIV = new byte[ivLen];
Array.Copy(iv, _encryptIV, ivLen);
} else {
}
else
{
_decryptIV = new byte[ivLen];
Array.Copy(iv, _decryptIV, ivLen);
}
@@ -108,12 +111,13 @@ namespace Shadowsocks.Encryption.Stream
int cipherOffset = 0;
Debug.Assert(_encCircularBuffer != null, "_encCircularBuffer != null");
_encCircularBuffer.Put(buf, 0, length);
if (! _encryptIVSent) {
if (!_encryptIVSent)
{
// Generate IV
byte[] ivBytes = new byte[ivLen];
randBytes(ivBytes, ivLen);
initCipher(ivBytes, true);
Array.Copy(ivBytes, 0, outbuf, 0, ivLen);
cipherOffset = ivLen;
_encryptIVSent = true;
@@ -130,16 +134,22 @@ namespace Shadowsocks.Encryption.Stream
{
Debug.Assert(_decCircularBuffer != null, "_circularBuffer != null");
_decCircularBuffer.Put(buf, 0, length);
if (! _decryptIVReceived) {
if (_decCircularBuffer.Size <= ivLen) {
if (!_decryptIVReceived)
{
if (_decCircularBuffer.Size <= ivLen)
{
// we need more data
outlength = 0;
return;
}
// start decryption
_decryptIVReceived = true;
byte[] iv = _decCircularBuffer.Get(ivLen);
initCipher(iv, false);
if (ivLen > 0)
{
byte[] iv = _decCircularBuffer.Get(ivLen);
initCipher(iv, false);
}
else initCipher(Array.Empty<byte>(), false);
}
byte[] cipher = _decCircularBuffer.ToArray();
cipherUpdate(false, cipher.Length, cipher, outbuf);
@@ -158,7 +168,8 @@ namespace Shadowsocks.Encryption.Stream
// Generate IV
randBytes(outbuf, ivLen);
initCipher(outbuf, true);
lock (_udpTmpBuf) {
lock (_udpTmpBuf)
{
cipherUpdate(true, length, buf, _udpTmpBuf);
outlength = length + ivLen;
Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, ivLen, length);
@@ -170,7 +181,8 @@ namespace Shadowsocks.Encryption.Stream
// Get IV from first pos
initCipher(buf, false);
outlength = length - ivLen;
lock (_udpTmpBuf) {
lock (_udpTmpBuf)
{
// C# could be multi-threaded
Buffer.BlockCopy(buf, ivLen, _udpTmpBuf, 0, length - ivLen);
cipherUpdate(false, length - ivLen, _udpTmpBuf, outbuf);


+ 116
- 0
shadowsocks-csharp/Encryption/Stream/StreamRc4NativeEncryptor.cs View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Shadowsocks.Encryption.Stream
{
public class StreamRc4NativeEncryptor : StreamEncryptor
{
byte[] realkey = new byte[256];
byte[] sbox = new byte[256];
public StreamRc4NativeEncryptor(string method, string password) : base(method, password)
{
}

protected override void initCipher(byte[] iv, bool isEncrypt)
{
base.initCipher(iv, isEncrypt);
if (_cipher == CipherFamily.Rc4Md5)
{
byte[] temp = new byte[keyLen + ivLen];
Array.Copy(_key, 0, temp, 0, keyLen);
Array.Copy(iv, 0, temp, keyLen, ivLen);
realkey = CryptoUtils.MD5(temp);
}
else
{
realkey = _key;
}
sbox = SBox(realkey);

}

protected override void cipherUpdate(bool isEncrypt, int length, byte[] buf, byte[] outbuf)
{
var ctx = isEncrypt ? enc_ctx : dec_ctx;

byte[] t = new byte[length];
Array.Copy(buf, t, length);

RC4(ctx, sbox, t, length);
Array.Copy(t, outbuf, length);
}

private static readonly Dictionary<string, CipherInfo> _ciphers = new Dictionary<string, CipherInfo>
{
// original RC4 doesn't use IV
{ "rc4", new CipherInfo("rc4", 16, 0, CipherFamily.Rc4) },
{ "rc4-md5", new CipherInfo("rc4-md5", 16, 16, CipherFamily.Rc4Md5) },
};

public static Dictionary<string, CipherInfo> SupportedCiphers()
{
return _ciphers;
}

protected override Dictionary<string, CipherInfo> getCiphers()
{
return _ciphers;
}

#region RC4
class Context
{
public int index1 = 0;
public int index2 = 0;
}

private Context enc_ctx = new Context();
private Context dec_ctx = new Context();

private byte[] SBox(byte[] key)
{
byte[] s = new byte[256];

for (int i = 0; i < 256; i++)
{
s[i] = (byte)i;
}

for (int i = 0, j = 0; i < 256; i++)
{
j = (j + key[i % key.Length] + s[i]) & 255;

Swap(s, i, j);
}

return s;
}

private void RC4(Context ctx, byte[] s, byte[] data, int length)
{
for (int n = 0; n < length; n++)
{
byte b = data[n];

ctx.index1 = (ctx.index1 + 1) & 255;
ctx.index2 = (ctx.index2 + s[ctx.index1]) & 255;

Swap(s, ctx.index1, ctx.index2);

data[n] = (byte)(b ^ s[(s[ctx.index1] + s[ctx.index2]) & 255]);
}
}

private static void Swap(byte[] s, int i, int j)
{
byte c = s[i];

s[i] = s[j];
s[j] = c;
}
#endregion
}
}

shadowsocks-csharp/Encryption/Stream/StreamNativeEncryptor.cs → shadowsocks-csharp/Encryption/Stream/StreamTableNativeEncryptor.cs View File

@@ -6,46 +6,19 @@ using System.Threading.Tasks;

namespace Shadowsocks.Encryption.Stream
{
public class StreamNativeEncryptor : StreamEncryptor
public class StreamTableNativeEncryptor : StreamEncryptor
{
const int Plain = 0;
const int Table = 1;
const int Rc4 = 2;
const int Rc4Md5 = 3;

string _password;

byte[] realkey;
byte[] sbox;
public StreamNativeEncryptor(string method, string password) : base(method, password)
public StreamTableNativeEncryptor(string method, string password) : base(method, password)
{
_password = password;
}

public override void Dispose()
{
return;
}

protected override void initCipher(byte[] iv, bool isEncrypt)
{
base.initCipher(iv, isEncrypt);
if (_cipher >= Rc4)
{
if (_cipher == Rc4Md5)
{
byte[] temp = new byte[keyLen + ivLen];
Array.Copy(_key, 0, temp, 0, keyLen);
Array.Copy(iv, 0, temp, keyLen, ivLen);
realkey = CryptoUtils.MD5(temp);
}
else
{
realkey = _key;
}
sbox = SBox(realkey);
}
else if (_cipher == Table)
if (_cipher == CipherFamily.Table)
{
ulong a = BitConverter.ToUInt64(CryptoUtils.MD5(Encoding.UTF8.GetBytes(_password)), 0);
for (int i = 0; i < 256; i++)
@@ -65,7 +38,7 @@ namespace Shadowsocks.Encryption.Stream

protected override void cipherUpdate(bool isEncrypt, int length, byte[] buf, byte[] outbuf)
{
if (_cipher == Table)
if (_cipher == CipherFamily.Table)
{
var table = isEncrypt ? _encryptTable : _decryptTable;
for (int i = 0; i < length; i++)
@@ -73,36 +46,24 @@ namespace Shadowsocks.Encryption.Stream
outbuf[i] = table[buf[i]];
}
}
else if (_cipher == Plain)
else if (_cipher == CipherFamily.Plain)
{
Array.Copy(buf, outbuf, length);
}
else
{
var ctx = isEncrypt ? enc_ctx : dec_ctx;

byte[] t = new byte[length];
Array.Copy(buf, t, length);

RC4(ctx, sbox, t, length);
Array.Copy(t, outbuf, length);
}
}

private static readonly Dictionary<string, EncryptorInfo> _ciphers = new Dictionary<string, EncryptorInfo>
private static readonly Dictionary<string, CipherInfo> _ciphers = new Dictionary<string, CipherInfo>
{
{"plain", new EncryptorInfo("PLAIN", 0, 0, Plain) },
{"table", new EncryptorInfo("TABLE", 0, 0, Table) },
{ "rc4", new EncryptorInfo("RC4", 16, 0, Rc4) }, // original RC4 doesn't use IV
{ "rc4-md5", new EncryptorInfo("RC4", 16, 16, Rc4Md5) },
{"plain", new CipherInfo("plain", 0, 0, CipherFamily.Plain) },
{"table", new CipherInfo("table", 0, 0, CipherFamily.Table) },
};

public static IEnumerable<string> SupportedCiphers()
public static Dictionary<string, CipherInfo> SupportedCiphers()
{
return _ciphers.Keys;
return _ciphers;
}

protected override Dictionary<string, EncryptorInfo> getCiphers()
protected override Dictionary<string, CipherInfo> getCiphers()
{
return _ciphers;
}
@@ -156,58 +117,5 @@ namespace Shadowsocks.Encryption.Stream
return sorted;
}
#endregion

#region RC4
class Context
{
public int index1 = 0;
public int index2 = 0;
}

private Context enc_ctx = new Context();
private Context dec_ctx = new Context();

private byte[] SBox(byte[] key)
{
byte[] s = new byte[256];

for (int i = 0; i < 256; i++)
{
s[i] = (byte)i;
}

for (int i = 0, j = 0; i < 256; i++)
{
j = (j + key[i % key.Length] + s[i]) & 255;

Swap(s, i, j);
}

return s;
}

private void RC4(Context ctx, byte[] s, byte[] data, int length)
{
for (int n = 0; n < length; n++)
{
byte b = data[n];

ctx.index1 = (ctx.index1 + 1) & 255;
ctx.index2 = (ctx.index2 + s[ctx.index1]) & 255;

Swap(s, ctx.index1, ctx.index2);

data[n] = (byte)(b ^ s[(s[ctx.index1] + s[ctx.index2]) & 255]);
}
}

private static void Swap(byte[] s, int i, int j)
{
byte c = s[i];

s[i] = s[j];
s[j] = c;
}
#endregion
}
}

+ 4
- 90
shadowsocks-csharp/View/ConfigForm.cs View File

@@ -1,4 +1,5 @@
using Shadowsocks.Controller;
using Shadowsocks.Encryption;
using Shadowsocks.Model;
using Shadowsocks.Properties;
using System;
@@ -19,98 +20,11 @@ namespace Shadowsocks.View
private bool isChange = false;
private class EncryptionMethod
{
public readonly string name;
public readonly bool deprecated;
// Edit here to add/delete encryption method displayed in UI
private static string[] deprecatedMethod = new string[]
{
"rc4-md5",
"salsa20",
"chacha20",
"bf-cfb",
"rc4",
"plain",
"table",
};
private static string[] inuseMethod = new string[]
{
"aes-256-gcm",
"aes-192-gcm",
"aes-128-gcm",
"chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305",
"chacha20-ietf",
"aes-256-cfb",
"aes-192-cfb",
"aes-128-cfb",
"aes-256-ctr",
"aes-192-ctr",
"aes-128-ctr",
"camellia-256-cfb",
"camellia-192-cfb",
"camellia-128-cfb",
};
public static EncryptionMethod[] AllMethods
{
get
{
if (!init) Init();
return allMethods;
}
}
private static bool init = false;
private static EncryptionMethod[] allMethods;
private static Dictionary<string, EncryptionMethod> methodByName = new Dictionary<string, EncryptionMethod>();
private static void Init()
{
var all = new List<EncryptionMethod>();
all.AddRange(inuseMethod.Select(i => new EncryptionMethod(i, false)));
all.AddRange(deprecatedMethod.Select(d => new EncryptionMethod(d, true)));
allMethods = all.ToArray();
foreach (var item in all)
{
methodByName[item.name] = item;
}
init = true;
}
public static EncryptionMethod GetMethod(string name)
{
if (!init) Init();
bool success = methodByName.TryGetValue(name, out EncryptionMethod method);
if (!success)
{
string defaultMethod = Server.DefaultMethod;
MessageBox.Show(I18N.GetString("Encryption method {0} not exist, will replace with {1}", name, defaultMethod), I18N.GetString("Shadowsocks"));
return methodByName[defaultMethod];
}
return method;
}
private EncryptionMethod(string name, bool deprecated)
{
this.name = name;
this.deprecated = deprecated;
}
public override string ToString()
{
return deprecated ? $"{name} ({I18N.GetString("deprecated")})" : name;
}
}
public ConfigForm(ShadowsocksController controller)
{
Font = SystemFonts.MessageBoxFont;
InitializeComponent();
EncryptionSelect.Items.AddRange(EncryptionMethod.AllMethods);
EncryptionSelect.Items.AddRange(EncryptorFactory.ListAvaliableCiphers().ToArray());
// a dirty hack
ServersListBox.Dock = DockStyle.Fill;
@@ -205,7 +119,7 @@ namespace Shadowsocks.View
server = address,
server_port = addressPort.Value,
password = serverPassword,
method = ((EncryptionMethod)EncryptionSelect.SelectedItem).name,
method = ((CipherInfo)EncryptionSelect.SelectedItem).Name,
plugin = PluginTextBox.Text,
plugin_opts = PluginOptionsTextBox.Text,
plugin_args = PluginArgumentsTextBox.Text,
@@ -406,7 +320,7 @@ namespace Shadowsocks.View
IPTextBox.Text = server.server;
ServerPortTextBox.Text = server.server_port.ToString();
PasswordTextBox.Text = server.password;
EncryptionSelect.SelectedItem = EncryptionMethod.GetMethod(server.method ?? Server.DefaultMethod);
EncryptionSelect.SelectedItem = EncryptorFactory.GetCipherInfo(server.method ?? Server.DefaultMethod);
PluginTextBox.Text = server.plugin;
PluginOptionsTextBox.Text = server.plugin_opts;
PluginArgumentsTextBox.Text = server.plugin_args;


+ 5
- 0
shadowsocks-csharp/packages.config View File

@@ -5,8 +5,13 @@
<package id="Costura.Fody" version="3.3.3" targetFramework="net472" />
<package id="Fody" version="4.2.1" targetFramework="net472" developmentDependency="true" />
<package id="GlobalHotKey" version="1.1.0" targetFramework="net472" />
<package id="NaCl.Core" version="1.2.0" targetFramework="net472" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
<package id="NLog" version="4.6.8" targetFramework="net472" />
<package id="StringEx.CS" version="0.3.1" targetFramework="net472" developmentDependency="true" />
<package id="System.Buffers" version="4.4.0" targetFramework="net472" />
<package id="System.Memory" version="4.5.2" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.4.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.2" targetFramework="net472" />
<package id="ZXing.Net" version="0.16.5" targetFramework="net472" />
</packages>

+ 11
- 4
shadowsocks-csharp/shadowsocks-csharp.csproj View File

@@ -11,7 +11,12 @@
<Version>4.1.9.2</Version>
<AssemblyName>Shadowsocks</AssemblyName>
<ApplicationIcon>shadowsocks.ico</ApplicationIcon>
<StartupObject />
<StartupObject>Shadowsocks.Program</StartupObject>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<ItemGroup>
@@ -58,6 +63,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.5" />
<!--TODO: Remove Fody related stuff, as they're either not actually used or has NET Core built-in alternate-->
<PackageReference Include="Caseless.Fody" Version="1.9.0" />
<PackageReference Include="Costura.Fody" Version="4.1.0" />
<PackageReference Include="Fody" Version="6.1.1">
@@ -66,16 +72,17 @@
</PackageReference>
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
<PackageReference Include="NaCl.Core" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.6.8" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
<PackageReference Include="ZXing.Net" Version="0.16.5" />
<PackageReference Include="StringEx.CS" Version="0.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
<PackageReference Include="System.Management" Version="4.7.0" />
<PackageReference Include="ZXing.Net" Version="0.16.5" />
</ItemGroup>
<ItemGroup>


+ 70
- 65
test/CryptographyTest.cs View File

@@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shadowsocks.Encryption;
using Shadowsocks.Encryption.Stream;
using Shadowsocks.Encryption.AEAD;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -10,7 +11,7 @@ namespace Shadowsocks.Test
[TestClass]
public class CryptographyTest
{
Random random = new Random();
[TestMethod]
public void TestMD5()
{
@@ -18,7 +19,6 @@ namespace Shadowsocks.Test
{
System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
byte[] bytes = new byte[len];
var random = new Random();
random.NextBytes(bytes);
string md5str = Convert.ToBase64String(md5.ComputeHash(bytes));
string md5str2 = Convert.ToBase64String(CryptoUtils.MD5(bytes));
@@ -32,12 +32,12 @@ namespace Shadowsocks.Test
byte[] plain = new byte[16384];
byte[] cipher = new byte[plain.Length + 100];// AEAD with IPv4 address type needs +100
byte[] plain2 = new byte[plain.Length + 16];
int outLen = 0;
int outLen2 = 0;
var random = new Random();
//random = new Random();
random.NextBytes(plain);
encryptor.Encrypt(plain, plain.Length, cipher, out outLen);
decryptor.Decrypt(cipher, outLen, plain2, out outLen2);
encryptor.Encrypt(plain, plain.Length, cipher, out int outLen);
decryptor.Decrypt(cipher, outLen, plain2, out int outLen2);
Assert.AreEqual(plain.Length, outLen2);
for (int j = 0; j < plain.Length; j++)
{
@@ -59,40 +59,48 @@ namespace Shadowsocks.Test
}
}
private static bool encryptionFailed = false;
private static object locker = new object();
const string password = "barfoo!";
[TestMethod]
public void TestBouncyCastleAEADEncryption()
private void RunSingleEncryptionThread(Type enc, Type dec, string method)
{
encryptionFailed = false;
// run it once before the multi-threading test to initialize global tables
RunSingleBouncyCastleAEADEncryptionThread();
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 10; i++)
var ector = enc.GetConstructor(new Type[] { typeof(string), typeof(string) });
var dctor = dec.GetConstructor(new Type[] { typeof(string), typeof(string) });
try
{
Thread t = new Thread(new ThreadStart(RunSingleBouncyCastleAEADEncryptionThread)); threads.Add(t);
t.Start();
IEncryptor encryptor = (IEncryptor)ector.Invoke(new object[] { method, password });
IEncryptor decryptor = (IEncryptor)dctor.Invoke(new object[] { method, password });
encryptor.AddressBufferLength = 1 + 4 + 2;// ADDR_ATYP_LEN + 4 + ADDR_PORT_LEN;
decryptor.AddressBufferLength = 1 + 4 + 2;// ADDR_ATYP_LEN + 4 + ADDR_PORT_LEN;
for (int i = 0; i < 16; i++)
{
RunEncryptionRound(encryptor, decryptor);
}
}
foreach (Thread t in threads)
catch
{
t.Join();
encryptionFailed = true;
throw;
}
RNG.Close();
Assert.IsFalse(encryptionFailed);
}
[TestMethod]
public void TestNativeEncryption()
private static bool encryptionFailed = false;
private void TestEncryptionMethod(Type enc, string method)
{
TestEncryptionMethod(enc, enc, method);
}
private void TestEncryptionMethod(Type enc, Type dec, string method)
{
encryptionFailed = false;
// run it once before the multi-threading test to initialize global tables
RunSingleNativeEncryptionThread();
RunSingleEncryptionThread(enc, dec, method);
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 10; i++)
for (int i = 0; i < 8; i++)
{
Thread t = new Thread(new ThreadStart(RunSingleNativeEncryptionThread));
threads.Add(t);
Thread t = new Thread(new ThreadStart(() => RunSingleEncryptionThread(enc, dec, method))); threads.Add(t);
t.Start();
}
foreach (Thread t in threads)
@@ -103,47 +111,44 @@ namespace Shadowsocks.Test
Assert.IsFalse(encryptionFailed);
}
private void RunSingleNativeEncryptionThread()
[TestMethod]
public void TestBouncyCastleAEADEncryption()
{
try
{
for (int i = 0; i < 100; i++)
{
IEncryptor encryptorN;
IEncryptor decryptorN;
encryptorN = new StreamNativeEncryptor("rc4-md5", "barfoo!");
decryptorN = new StreamNativeEncryptor("rc4-md5", "barfoo!");
RunEncryptionRound(encryptorN, decryptorN);
}
}
catch
{
encryptionFailed = true;
throw;
}
TestEncryptionMethod(typeof(AEADBouncyCastleEncryptor), "aes-128-gcm");
TestEncryptionMethod(typeof(AEADBouncyCastleEncryptor), "aes-192-gcm");
TestEncryptionMethod(typeof(AEADBouncyCastleEncryptor), "aes-256-gcm");
}
private void RunSingleBouncyCastleAEADEncryptionThread()
[TestMethod]
public void TestAesGcmNativeAEADEncryption()
{
try
{
for (int i = 0; i < 100; i++)
{
var random = new Random();
IEncryptor encryptor;
IEncryptor decryptor;
encryptor = new Encryption.AEAD.AEADBouncyCastleEncryptor("aes-256-gcm", "barfoo!");
encryptor.AddrBufLength = 1 + 4 + 2;// ADDR_ATYP_LEN + 4 + ADDR_PORT_LEN;
decryptor = new Encryption.AEAD.AEADBouncyCastleEncryptor("aes-256-gcm", "barfoo!");
decryptor.AddrBufLength = 1 + 4 + 2;// ADDR_ATYP_LEN + 4 + ADDR_PORT_LEN;
RunEncryptionRound(encryptor, decryptor);
}
}
catch
{
encryptionFailed = true;
throw;
}
TestEncryptionMethod(typeof(AEADAesGcmNativeEncryptor), "aes-128-gcm");
TestEncryptionMethod(typeof(AEADAesGcmNativeEncryptor), "aes-192-gcm");
TestEncryptionMethod(typeof(AEADAesGcmNativeEncryptor), "aes-256-gcm");
}
[TestMethod]
public void TestNaClAEADEncryption()
{
TestEncryptionMethod(typeof(AEADNaClEncryptor), "chacha20-ietf-poly1305");
TestEncryptionMethod(typeof(AEADNaClEncryptor), "xchacha20-ietf-poly1305");
}
[TestMethod]
public void TestNativeEncryption()
{
TestEncryptionMethod(typeof(StreamTableNativeEncryptor), "plain");
TestEncryptionMethod(typeof(StreamRc4NativeEncryptor), "rc4");
TestEncryptionMethod(typeof(StreamRc4NativeEncryptor), "rc4-md5");
}
[TestMethod]
public void TestNativeTableEncryption()
{
// Too slow, run once to save CPU
var enc = new StreamTableNativeEncryptor("table", "barfoo!");
var dec = new StreamTableNativeEncryptor("table", "barfoo!");
RunEncryptionRound(enc, dec);
}
}
}

+ 4
- 0
test/ShadowsocksTest.csproj View File

@@ -6,6 +6,10 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GlobalHotKeyCore" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />


Loading…
Cancel
Save