diff --git a/shadowsocks-csharp/Controller/Logging.cs b/shadowsocks-csharp/Controller/Logging.cs index 4ab82980..c705fdd9 100755 --- a/shadowsocks-csharp/Controller/Logging.cs +++ b/shadowsocks-csharp/Controller/Logging.cs @@ -4,7 +4,7 @@ using System.IO; using System.Net.Sockets; using System.Net; using System.Diagnostics; - +using System.Text; using Shadowsocks.Util; namespace Shadowsocks.Controller @@ -70,6 +70,18 @@ namespace Shadowsocks.Controller WriteToLogFile("[D] " + o); } + [Conditional("DEBUG")] + public static void Dump(string tag, byte[] arr, int length) + { + var sb = new StringBuilder($"{Environment.NewLine}{tag}: "); + for (int i = 0; i < length - 1; i++) { + sb.Append($"0x{arr[i]:X2}, "); + } + sb.Append($"0x{arr[length - 1]:X2}"); + sb.Append(Environment.NewLine); + Debug(sb.ToString()); + } + [Conditional("DEBUG")] public static void Debug(EndPoint local, EndPoint remote, int len, string header = null, string tailer = null) { diff --git a/shadowsocks-csharp/Controller/Service/TCPRelay.cs b/shadowsocks-csharp/Controller/Service/TCPRelay.cs index 2348ab6b..f90349b0 100644 --- a/shadowsocks-csharp/Controller/Service/TCPRelay.cs +++ b/shadowsocks-csharp/Controller/Service/TCPRelay.cs @@ -4,12 +4,14 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Timers; - using Shadowsocks.Controller.Strategy; using Shadowsocks.Encryption; +using Shadowsocks.Encryption.AEAD; +using Shadowsocks.Encryption.Exception; using Shadowsocks.Model; using Shadowsocks.Proxy; using Shadowsocks.Util.Sockets; +using static Shadowsocks.Encryption.EncryptorBase; namespace Shadowsocks.Controller { @@ -74,7 +76,7 @@ namespace Shadowsocks.Controller { handlersToClose.AddRange(Handlers); } - handlersToClose.ForEach(h=>h.Close()); + handlersToClose.ForEach(h => h.Close()); } public void UpdateInboundCounter(Server server, long n) @@ -93,7 +95,7 @@ namespace Shadowsocks.Controller } } - class TCPHandler + internal class TCPHandler { class AsyncSession { @@ -114,7 +116,7 @@ namespace Shadowsocks.Controller State = state; } - public AsyncSession(AsyncSession session, T state): base(session.Remote) + public AsyncSession(AsyncSession session, T state) : base(session.Remote) { State = state; } @@ -123,46 +125,66 @@ namespace Shadowsocks.Controller private readonly int _serverTimeout; private readonly int _proxyTimeout; - // Size of receive buffer. - public static readonly int RecvSize = 8192; - public static readonly int RecvReserveSize = IVEncryptor.ONETIMEAUTH_BYTES + IVEncryptor.AUTH_BYTES; // reserve for one-time auth - public static readonly int BufferSize = RecvSize + RecvReserveSize + 32; + // each recv size. + public const int RecvSize = 2048; + + // overhead of one chunk, reserved for AEAD ciphers + public const int ChunkOverheadSize = 16 * 2 /* two tags */ + AEADEncryptor.CHUNK_LEN_BYTES; + + // max chunk size + public const uint MaxChunkSize = AEADEncryptor.CHUNK_LEN_MASK + AEADEncryptor.CHUNK_LEN_BYTES + 16 * 2; + + // In general, the ciphertext length, we should take overhead into account + public const int BufferSize = RecvSize + (int)MaxChunkSize + 32 /* max salt len */; public DateTime lastActivity; - private ShadowsocksController _controller; - private Configuration _config; - private TCPRelay _tcprelay; - private Socket _connection; + private ShadowsocksController _controller; + private Configuration _config; + private TCPRelay _tcprelay; + private Socket _connection; - private IEncryptor _encryptor; - private Server _server; + private IEncryptor _encryptor; + private Server _server; private AsyncSession _currentRemoteSession; - private bool _proxyConnected; - private bool _destConnected; + private bool _proxyConnected; + private bool _destConnected; + + private byte _command; + private byte[] _firstPacket; + private int _firstPacketLength; - private byte _command; - private byte[] _firstPacket; - private int _firstPacketLength; + private const int CMD_CONNECT = 0x01; + private const int CMD_UDP_ASSOC = 0x03; - private int _totalRead = 0; - private int _totalWrite = 0; + private int _addrBufLength = -1; - private byte[] _remoteRecvBuffer = new byte[BufferSize]; - private byte[] _remoteSendBuffer = new byte[BufferSize]; - private byte[] _connetionRecvBuffer = new byte[BufferSize]; - private byte[] _connetionSendBuffer = new byte[BufferSize]; + private int _totalRead = 0; + private int _totalWrite = 0; - private bool _connectionShutdown = false; - private bool _remoteShutdown = false; - private bool _closed = false; + // remote -> local proxy (ciphertext, before decrypt) + private byte[] _remoteRecvBuffer = new byte[BufferSize]; + + // client -> local proxy (plaintext, before encrypt) + private byte[] _connetionRecvBuffer = new byte[BufferSize]; + + // local proxy -> remote (plaintext, after decrypt) + private byte[] _remoteSendBuffer = new byte[BufferSize]; + + // local proxy -> client (ciphertext, before decrypt) + private byte[] _connetionSendBuffer = new byte[BufferSize]; + + private bool _connectionShutdown = false; + private bool _remoteShutdown = false; + private bool _closed = false; // instance-based lock without static - private readonly object _encryptionLock = new object(); - private readonly object _decryptionLock = new object(); - private readonly object _closeConnLock = new object(); + private readonly object _encryptionLock = new object(); + + private readonly object _decryptionLock = new object(); + private readonly object _closeConnLock = new object(); private DateTime _startConnectTime; private DateTime _startReceivingTime; @@ -184,17 +206,18 @@ namespace Shadowsocks.Controller public void CreateRemote() { - Server server = _controller.GetAServer(IStrategyCallerType.TCP, (IPEndPoint)_connection.RemoteEndPoint, _destEndPoint); + Server server = _controller.GetAServer(IStrategyCallerType.TCP, (IPEndPoint)_connection.RemoteEndPoint, + _destEndPoint); if (server == null || server.server == "") throw new ArgumentException("No server configured"); - lock (_encryptionLock) - { - lock (_decryptionLock) - { - _encryptor = EncryptorFactory.GetEncryptor(server.method, server.password, server.auth, false); - } - } + + _encryptor = EncryptorFactory.GetEncryptor(server.method, server.password); + this._server = server; + + /* prepare address buffer length for AEAD */ + Logging.Debug($"_addrBufLength={_addrBufLength}"); + _encryptor.AddrBufLength = _addrBufLength; } public void Start(byte[] firstPacket, int length) @@ -269,7 +292,8 @@ namespace Shadowsocks.Controller response = new byte[] { 0, 91 }; Logging.Error("socks 5 protocol error"); } - _connection.BeginSend(response, 0, response.Length, SocketFlags.None, new AsyncCallback(HandshakeSendCallback), null); + _connection.BeginSend(response, 0, response.Length, SocketFlags.None, + HandshakeSendCallback, null); } else Close(); @@ -296,8 +320,8 @@ namespace Shadowsocks.Controller // Skip first 3 bytes, and read 2 more bytes to analysis the address. // 2 more bytes is designed if address is domain then we don't need to read once more to get the addr length. // TODO validate - _connection.BeginReceive(_connetionRecvBuffer, 0, 3 + 2, SocketFlags.None, - new AsyncCallback(handshakeReceive2Callback), null); + _connection.BeginReceive(_connetionRecvBuffer, 0, 3 + ADDR_ATYP_LEN + 1, SocketFlags.None, + HandshakeReceive2Callback, null); } catch (Exception e) { @@ -306,7 +330,7 @@ namespace Shadowsocks.Controller } } - private void handshakeReceive2Callback(IAsyncResult ar) + private void HandshakeReceive2Callback(IAsyncResult ar) { if (_closed) return; try @@ -315,20 +339,20 @@ namespace Shadowsocks.Controller if (bytesRead >= 5) { _command = _connetionRecvBuffer[1]; - if (_command != 1 && _command != 3) + if (_command != CMD_CONNECT && _command != CMD_UDP_ASSOC) { Logging.Debug("Unsupported CMD=" + _command); Close(); } else { - if (_command == 1) + if (_command == CMD_CONNECT) { byte[] response = { 5, 0, 0, 1, 0, 0, 0, 0, 0, 0 }; _connection.BeginSend(response, 0, response.Length, SocketFlags.None, - new AsyncCallback(ResponseCallback), null); + ResponseCallback, null); } - else if (_command == 3) + else if (_command == CMD_UDP_ASSOC) { ReadAddress(HandleUDPAssociate); } @@ -336,7 +360,8 @@ namespace Shadowsocks.Controller } else { - Logging.Debug("failed to recv data in Shadowsocks.Controller.TCPHandler.handshakeReceive2Callback()"); + Logging.Debug( + "failed to recv data in Shadowsocks.Controller.TCPHandler.handshakeReceive2Callback()"); Close(); } } @@ -368,15 +393,15 @@ namespace Shadowsocks.Controller switch (atyp) { - case 1: // IPv4 address, 4 bytes - ReadAddress(4 + 2 - 1, onSuccess); + case ATYP_IPv4: // IPv4 address, 4 bytes + ReadAddress(4 + ADDR_PORT_LEN - 1, onSuccess); break; - case 3: // domain name, length + str + case ATYP_DOMAIN: // domain name, length + str int len = _connetionRecvBuffer[4]; - ReadAddress(len + 2, onSuccess); + ReadAddress(len + ADDR_PORT_LEN, onSuccess); break; - case 4: // IPv6 address, 16 bytes - ReadAddress(16 + 2 - 1, onSuccess); + case ATYP_IPv6: // IPv6 address, 16 bytes + ReadAddress(16 + ADDR_PORT_LEN - 1, onSuccess); break; default: Logging.Debug("Unsupported ATYP=" + atyp); @@ -387,10 +412,12 @@ namespace Shadowsocks.Controller private void ReadAddress(int bytesRemain, Action onSuccess) { - Array.Copy(_connetionRecvBuffer, 3, _connetionRecvBuffer, 0, 2); + // drop [ VER | CMD | RSV ] + Array.Copy(_connetionRecvBuffer, 3, _connetionRecvBuffer, 0, ADDR_ATYP_LEN + 1); // Read the remain address bytes - _connection.BeginReceive(_connetionRecvBuffer, 2, RecvSize - 2, SocketFlags.None, OnAddressFullyRead, new object[] {bytesRemain, onSuccess}); + _connection.BeginReceive(_connetionRecvBuffer, 2, RecvSize - 2, SocketFlags.None, OnAddressFullyRead, + new object[] { bytesRemain, onSuccess }); } private void OnAddressFullyRead(IAsyncResult ar) @@ -400,10 +427,10 @@ namespace Shadowsocks.Controller { int bytesRead = _connection.EndReceive(ar); - var states = (object[]) ar.AsyncState; + var states = (object[])ar.AsyncState; int bytesRemain = (int)states[0]; - var onSuccess = (Action) states[1]; + var onSuccess = (Action)states[1]; if (bytesRead >= bytesRemain) { @@ -411,35 +438,39 @@ namespace Shadowsocks.Controller int atyp = _connetionRecvBuffer[0]; - string dst_addr = "Unknown"; - int dst_port = -1; + string dstAddr = "Unknown"; + int dstPort = -1; switch (atyp) { - case 1: // IPv4 address, 4 bytes - dst_addr = new IPAddress(_connetionRecvBuffer.Skip(1).Take(4).ToArray()).ToString(); - dst_port = (_connetionRecvBuffer[5] << 8) + _connetionRecvBuffer[6]; + case ATYP_IPv4: // IPv4 address, 4 bytes + dstAddr = new IPAddress(_connetionRecvBuffer.Skip(1).Take(4).ToArray()).ToString(); + dstPort = (_connetionRecvBuffer[5] << 8) + _connetionRecvBuffer[6]; + _addrBufLength = ADDR_ATYP_LEN + 4 + ADDR_PORT_LEN; break; - case 3: // domain name, length + str + case ATYP_DOMAIN: // domain name, length + str int len = _connetionRecvBuffer[1]; - dst_addr = System.Text.Encoding.UTF8.GetString(_connetionRecvBuffer, 2, len); - dst_port = (_connetionRecvBuffer[len + 2] << 8) + _connetionRecvBuffer[len + 3]; + dstAddr = System.Text.Encoding.UTF8.GetString(_connetionRecvBuffer, 2, len); + dstPort = (_connetionRecvBuffer[len + 2] << 8) + _connetionRecvBuffer[len + 3]; + _addrBufLength = ADDR_ATYP_LEN + 1 + len + ADDR_PORT_LEN; break; - case 4: // IPv6 address, 16 bytes - dst_addr = $"[{new IPAddress(_connetionRecvBuffer.Skip(1).Take(16).ToArray())}]"; - dst_port = (_connetionRecvBuffer[17] << 8) + _connetionRecvBuffer[18]; + case ATYP_IPv6: // IPv6 address, 16 bytes + dstAddr = $"[{new IPAddress(_connetionRecvBuffer.Skip(1).Take(16).ToArray())}]"; + dstPort = (_connetionRecvBuffer[17] << 8) + _connetionRecvBuffer[18]; + _addrBufLength = ADDR_ATYP_LEN + 16 + ADDR_PORT_LEN; break; } + if (_config.isVerboseLogging) { - Logging.Info($"connect to {dst_addr}:{dst_port}"); + Logging.Info($"connect to {dstAddr}:{dstPort}"); } - _destEndPoint = SocketUtil.GetEndPoint(dst_addr, dst_port); + _destEndPoint = SocketUtil.GetEndPoint(dstAddr, dstPort); - onSuccess.Invoke(); + onSuccess.Invoke(); /* StartConnect() */ } else { @@ -459,21 +490,21 @@ namespace Shadowsocks.Controller IPEndPoint endPoint = (IPEndPoint)_connection.LocalEndPoint; byte[] address = endPoint.Address.GetAddressBytes(); int port = endPoint.Port; - byte[] response = new byte[4 + address.Length + 2]; + byte[] response = new byte[4 + address.Length + ADDR_PORT_LEN]; response[0] = 5; switch (endPoint.AddressFamily) { case AddressFamily.InterNetwork: - response[3] = 1; + response[3] = ATYP_IPv4; break; case AddressFamily.InterNetworkV6: - response[3] = 4; + response[3] = ATYP_IPv6; break; } address.CopyTo(response, 4); response[response.Length - 1] = (byte)(port & 0xFF); response[response.Length - 2] = (byte)((port >> 8) & 0xFF); - _connection.BeginSend(response, 0, response.Length, SocketFlags.None, new AsyncCallback(ReadAll), true); + _connection.BeginSend(response, 0, response.Length, SocketFlags.None, ReadAll, true); } private void ReadAll(IAsyncResult ar) @@ -484,14 +515,16 @@ namespace Shadowsocks.Controller if (ar.AsyncState != null) { _connection.EndSend(ar); - _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, new AsyncCallback(ReadAll), null); + _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, + ReadAll, null); } else { int bytesRead = _connection.EndReceive(ar); if (bytesRead > 0) { - _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, new AsyncCallback(ReadAll), null); + _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, + ReadAll, null); } else Close(); @@ -522,7 +555,10 @@ namespace Shadowsocks.Controller public AsyncSession Session; public Server Server; - public ServerTimer(int p) : base(p) { } + + public ServerTimer(int p) : base(p) + { + } } private void StartConnect() @@ -533,7 +569,7 @@ namespace Shadowsocks.Controller // Setting up proxy IProxy remote; - EndPoint proxyEP; + EndPoint proxyEP = null; if (_config.proxy.useProxy) { switch (_config.proxy.proxyType) @@ -552,7 +588,6 @@ namespace Shadowsocks.Controller else { remote = new DirectConnect(); - proxyEP = null; } var session = new AsyncSession(remote); @@ -567,9 +602,8 @@ namespace Shadowsocks.Controller _currentRemoteSession = session; } - ProxyTimer proxyTimer = new ProxyTimer(_proxyTimeout); - proxyTimer.AutoReset = false; - proxyTimer.Elapsed += proxyConnectTimer_Elapsed; + ProxyTimer proxyTimer = new ProxyTimer(_proxyTimeout) { AutoReset = false }; + proxyTimer.Elapsed += ProxyConnectTimer_Elapsed; proxyTimer.Enabled = true; proxyTimer.Session = session; @@ -579,7 +613,8 @@ namespace Shadowsocks.Controller _proxyConnected = false; // Connect to the proxy server. - remote.BeginConnectProxy(proxyEP, new AsyncCallback(ProxyConnectCallback), new AsyncSession(remote, proxyTimer)); + remote.BeginConnectProxy(proxyEP, ProxyConnectCallback, + new AsyncSession(remote, proxyTimer)); } catch (Exception e) { @@ -588,10 +623,10 @@ namespace Shadowsocks.Controller } } - private void proxyConnectTimer_Elapsed(object sender, ElapsedEventArgs e) + private void ProxyConnectTimer_Elapsed(object sender, ElapsedEventArgs e) { - var timer = (ProxyTimer) sender; - timer.Elapsed -= proxyConnectTimer_Elapsed; + var timer = (ProxyTimer)sender; + timer.Elapsed -= ProxyConnectTimer_Elapsed; timer.Enabled = false; timer.Dispose(); @@ -609,18 +644,17 @@ namespace Shadowsocks.Controller private void ProxyConnectCallback(IAsyncResult ar) { - Server server = null; if (_closed) { return; } try { - var session = (AsyncSession) ar.AsyncState; + var session = (AsyncSession)ar.AsyncState; ProxyTimer timer = session.State; var destEndPoint = timer.DestEndPoint; - server = timer.Server; - timer.Elapsed -= proxyConnectTimer_Elapsed; + var server = timer.Server; + timer.Elapsed -= ProxyConnectTimer_Elapsed; timer.Enabled = false; timer.Dispose(); @@ -640,16 +674,16 @@ namespace Shadowsocks.Controller } _startConnectTime = DateTime.Now; - ServerTimer connectTimer = new ServerTimer(_serverTimeout); - connectTimer.AutoReset = false; - connectTimer.Elapsed += destConnectTimer_Elapsed; + ServerTimer connectTimer = new ServerTimer(_serverTimeout) { AutoReset = false }; + connectTimer.Elapsed += DestConnectTimer_Elapsed; connectTimer.Enabled = true; connectTimer.Session = session; connectTimer.Server = server; - + _destConnected = false; // Connect to the remote endpoint. - remote.BeginConnectDest(destEndPoint, new AsyncCallback(ConnectCallback), new AsyncSession(session, connectTimer)); + remote.BeginConnectDest(destEndPoint, ConnectCallback, + new AsyncSession(session, connectTimer)); } catch (ArgumentException) { @@ -661,10 +695,10 @@ namespace Shadowsocks.Controller } } - private void destConnectTimer_Elapsed(object sender, ElapsedEventArgs e) + private void DestConnectTimer_Elapsed(object sender, ElapsedEventArgs e) { var timer = (ServerTimer)sender; - timer.Elapsed -= destConnectTimer_Elapsed; + timer.Elapsed -= DestConnectTimer_Elapsed; timer.Enabled = false; timer.Dispose(); @@ -687,17 +721,17 @@ namespace Shadowsocks.Controller if (_closed) return; try { - var session = (AsyncSession) ar.AsyncState; + var session = (AsyncSession)ar.AsyncState; ServerTimer timer = session.State; _server = timer.Server; - timer.Elapsed -= destConnectTimer_Elapsed; + timer.Elapsed -= DestConnectTimer_Elapsed; timer.Enabled = false; timer.Dispose(); var remote = session.Remote; // Complete the connection. remote.EndConnectDest(ar); - + _destConnected = true; if (_config.isVerboseLogging) @@ -727,19 +761,11 @@ namespace Shadowsocks.Controller } } - // private static readonly Random Rnd = new Random(); - private void TryReadAvailableData() { int available = Math.Min(_connection.Available, RecvSize - _firstPacketLength); if (available > 0) { - // Pick a random chunk size, or is it truly necessary? Random packet size is some sort of 'characteristic' itself. - //lock (Rnd) - //{ - // available = Rnd.Next(available + 1); - //} - var size = _connection.Receive(_connetionRecvBuffer, _firstPacketLength, available, SocketFlags.None); @@ -753,7 +779,8 @@ namespace Shadowsocks.Controller try { _startReceivingTime = DateTime.Now; - session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, new AsyncCallback(PipeRemoteReceiveCallback), session); + session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeRemoteReceiveCallback, session); TryReadAvailableData(); Logging.Debug($"_firstPacketLength = {_firstPacketLength}"); @@ -771,19 +798,38 @@ namespace Shadowsocks.Controller if (_closed) return; try { - var session = (AsyncSession) ar.AsyncState; + var session = (AsyncSession)ar.AsyncState; int bytesRead = session.Remote.EndReceive(ar); _totalRead += bytesRead; _tcprelay.UpdateInboundCounter(_server, bytesRead); if (bytesRead > 0) { lastActivity = DateTime.Now; - int bytesToSend; + int bytesToSend = -1; lock (_decryptionLock) { - _encryptor.Decrypt(_remoteRecvBuffer, bytesRead, _remoteSendBuffer, out bytesToSend); + try + { + _encryptor.Decrypt(_remoteRecvBuffer, bytesRead, _remoteSendBuffer, out bytesToSend); + } + catch (CryptoErrorException) + { + Logging.Error("decryption error"); + Close(); + return; + } + } + if (bytesToSend == 0) + { + // need more to decrypt + Logging.Debug("Need more to decrypt"); + session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeRemoteReceiveCallback, session); + return; } - _connection.BeginSend(_remoteSendBuffer, 0, bytesToSend, SocketFlags.None, new AsyncCallback(PipeConnectionSendCallback), session); + Logging.Debug($"start sending {bytesToSend}"); + _connection.BeginSend(_remoteSendBuffer, 0, bytesToSend, SocketFlags.None, + PipeConnectionSendCallback, new object[] { session, bytesToSend }); IStrategy strategy = _controller.GetCurrentStrategy(); strategy?.UpdateLastRead(_server); } @@ -808,7 +854,7 @@ namespace Shadowsocks.Controller { int bytesRead = _connection.EndReceive(ar); - var session = (AsyncSession) ar.AsyncState; + var session = (AsyncSession)ar.AsyncState; var remote = session.Remote; if (bytesRead > 0) @@ -835,11 +881,21 @@ namespace Shadowsocks.Controller int bytesToSend; lock (_encryptionLock) { - _encryptor.Encrypt(_connetionRecvBuffer, length, _connetionSendBuffer, out bytesToSend); + try + { + _encryptor.Encrypt(_connetionRecvBuffer, length, _connetionSendBuffer, out bytesToSend); + } + catch (CryptoErrorException) + { + Logging.Debug("encryption error"); + Close(); + return; + } } _tcprelay.UpdateOutboundCounter(_server, bytesToSend); _startSendingTime = DateTime.Now; - session.Remote.BeginSend(_connetionSendBuffer, 0, bytesToSend, SocketFlags.None, new AsyncCallback(PipeRemoteSendCallback), session); + session.Remote.BeginSend(_connetionSendBuffer, 0, bytesToSend, SocketFlags.None, + PipeRemoteSendCallback, new object[] { session, bytesToSend }); IStrategy strategy = _controller.GetCurrentStrategy(); strategy?.UpdateLastWrite(_server); } @@ -849,9 +905,21 @@ namespace Shadowsocks.Controller if (_closed) return; try { - var session = (AsyncSession)ar.AsyncState; - session.Remote.EndSend(ar); - _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, new AsyncCallback(PipeConnectionReceiveCallback), session); + var container = (object[])ar.AsyncState; + var session = (AsyncSession)container[0]; + var bytesShouldSend = (int)container[1]; + int bytesSent = session.Remote.EndSend(ar); + int bytesRemaining = bytesShouldSend - bytesSent; + if (bytesRemaining > 0) + { + Logging.Info("reconstruct _connetionSendBuffer to re-send"); + Buffer.BlockCopy(_connetionSendBuffer, bytesSent, _connetionSendBuffer, 0, bytesRemaining); + session.Remote.BeginSend(_connetionSendBuffer, 0, bytesRemaining, SocketFlags.None, + PipeRemoteSendCallback, new object[] { session, bytesRemaining }); + return; + } + _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeConnectionReceiveCallback, session); } catch (Exception e) { @@ -860,13 +928,26 @@ namespace Shadowsocks.Controller } } + // In general, we assume there is no delay between local proxy and client, add this for sanity private void PipeConnectionSendCallback(IAsyncResult ar) { try { - var session = (AsyncSession)ar.AsyncState; - _connection.EndSend(ar); - session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, new AsyncCallback(PipeRemoteReceiveCallback), session); + var container = (object[])ar.AsyncState; + var session = (AsyncSession)container[0]; + var bytesShouldSend = (int)container[1]; + var bytesSent = _connection.EndSend(ar); + var bytesRemaining = bytesShouldSend - bytesSent; + if (bytesRemaining > 0) + { + Logging.Info("reconstruct _remoteSendBuffer to re-send"); + Buffer.BlockCopy(_remoteSendBuffer, bytesSent, _remoteSendBuffer, 0, bytesRemaining); + _connection.BeginSend(_remoteSendBuffer, 0, bytesRemaining, SocketFlags.None, + PipeConnectionSendCallback, new object[] { session, bytesRemaining }); + return; + } + session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeRemoteReceiveCallback, session); } catch (Exception e) { @@ -875,4 +956,4 @@ namespace Shadowsocks.Controller } } } -} +} \ No newline at end of file diff --git a/shadowsocks-csharp/Controller/Service/UDPRelay.cs b/shadowsocks-csharp/Controller/Service/UDPRelay.cs index 0d22f8fb..6f012739 100644 --- a/shadowsocks-csharp/Controller/Service/UDPRelay.cs +++ b/shadowsocks-csharp/Controller/Service/UDPRelay.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; - using Shadowsocks.Controller.Strategy; using Shadowsocks.Encryption; using Shadowsocks.Model; @@ -13,7 +12,9 @@ namespace Shadowsocks.Controller class UDPRelay : Listener.Service { private ShadowsocksController _controller; - private LRUCache _cache; + + // TODO: choose a smart number + private LRUCache _cache = new LRUCache(512); public long outbound = 0; public long inbound = 0; @@ -21,7 +22,6 @@ namespace Shadowsocks.Controller public UDPRelay(ShadowsocksController controller) { this._controller = controller; - this._cache = new LRUCache(512); // todo: choose a smart number } public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) @@ -78,12 +78,12 @@ namespace Shadowsocks.Controller public void Send(byte[] data, int length) { - IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password, _server.auth, true); - byte[] dataIn = new byte[length - 3 + IVEncryptor.ONETIMEAUTH_BYTES]; + IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); + byte[] dataIn = new byte[length - 3]; Array.Copy(data, 3, dataIn, 0, length - 3); - byte[] dataOut = new byte[length - 3 + 16 + IVEncryptor.ONETIMEAUTH_BYTES]; + byte[] dataOut = new byte[length - 3 + 16]; int outlen; - encryptor.Encrypt(dataIn, length - 3, dataOut, out outlen); + encryptor.EncryptUDP(dataIn, length - 3, dataOut, out outlen); Logging.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay"); _remote?.SendTo(dataOut, outlen, SocketFlags.None, _remoteEndPoint); } @@ -106,14 +106,15 @@ namespace Shadowsocks.Controller byte[] dataOut = new byte[bytesRead]; int outlen; - IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password, _server.auth, true); - encryptor.Decrypt(_buffer, bytesRead, dataOut, out outlen); + IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); + encryptor.DecryptUDP(_buffer, bytesRead, dataOut, out outlen); byte[] sendBuf = new byte[outlen + 3]; Array.Copy(dataOut, 0, sendBuf, 3, outlen); Logging.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay"); _local?.SendTo(sendBuf, outlen + 3, 0, _localEndPoint); + Receive(); } catch (ObjectDisposedException) @@ -126,6 +127,8 @@ namespace Shadowsocks.Controller } finally { + // No matter success or failed, we keep receiving + } } @@ -143,13 +146,12 @@ namespace Shadowsocks.Controller { // TODO: need more think about handle other Exceptions, or should remove this catch(). } - finally - { - } } } } + #region LRU cache + // cc by-sa 3.0 http://stackoverflow.com/a/3719378/1124054 class LRUCache where V : UDPRelay.UDPHandler { @@ -212,4 +214,6 @@ namespace Shadowsocks.Controller public K key; public V value; } + + #endregion } diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs new file mode 100644 index 00000000..698c11c6 --- /dev/null +++ b/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Text; +using Cyotek.Collections.Generic; +using Shadowsocks.Controller; +using Shadowsocks.Encryption.Exception; +using Shadowsocks.Encryption.Stream; + +namespace Shadowsocks.Encryption.AEAD +{ + public abstract class AEADEncryptor + : EncryptorBase + { + // We are using the same saltLen and keyLen + private const string Info = "ss-subkey"; + private static readonly byte[] InfoBytes = Encoding.ASCII.GetBytes(Info); + + // for UDP only + protected static byte[] _udpTmpBuf = new byte[MAX_INPUT_SIZE]; + + // every connection should create its own buffer + private CircularBuffer _encCircularBuffer = new CircularBuffer(MAX_INPUT_SIZE * 2, false); + private CircularBuffer _decCircularBuffer = new CircularBuffer(MAX_INPUT_SIZE * 2, false); + + public const int CHUNK_LEN_BYTES = 2; + public const uint CHUNK_LEN_MASK = 0x3FFFu; + + protected Dictionary ciphers; + + protected string _method; + protected int _cipher; + // internal name in the crypto library + protected string _innerLibName; + protected EncryptorInfo CipherInfo; + protected static byte[] _Masterkey = null; + protected byte[] _sessionKey; + protected int keyLen; + protected int saltLen; + protected int tagLen; + protected int nonceLen; + + protected byte[] _encryptSalt; + protected byte[] _decryptSalt; + + protected object _nonceIncrementLock = new object(); + protected byte[] _encNonce; + protected byte[] _decNonce; + // Is first packet + protected bool _decryptSaltReceived; + protected bool _encryptSaltSent; + + // Is first chunk(tcp request) + protected bool _tcpRequestSent; + + public AEADEncryptor(string method, string password) + : base(method, password) + { + InitEncryptorInfo(method); + InitKey(password); + // Initialize all-zero nonce for each connection + _encNonce = new byte[nonceLen]; + _decNonce = new byte[nonceLen]; + } + + protected abstract Dictionary getCiphers(); + + protected void InitEncryptorInfo(string method) + { + method = method.ToLower(); + _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; + } + + protected void InitKey(string password) + { + byte[] passbuf = Encoding.UTF8.GetBytes(password); + // init master key + if (_Masterkey == null) _Masterkey = new byte[keyLen]; + if (_Masterkey.Length < keyLen) Array.Resize(ref _Masterkey, keyLen); + DeriveKey(passbuf, _Masterkey); + // init session key + if (_sessionKey == null) _sessionKey = new byte[keyLen]; + } + + public void DeriveKey(byte[] password, byte[] key) + { + StreamEncryptor.LegacyDeriveKey(password, key); + } + + public void DeriveSessionKey(byte[] salt, byte[] masterKey, byte[] sessionKey) + { + int ret = MbedTLS.hkdf(salt, saltLen, masterKey, keyLen, InfoBytes, InfoBytes.Length, sessionKey, + keyLen); + if (ret != 0) throw new System.Exception("failed to generate session key"); + } + + protected void IncrementNonce(bool isEncrypt) + { + lock (_nonceIncrementLock) { + Sodium.sodium_increment(isEncrypt ? _encNonce : _decNonce, nonceLen); + } + } + + 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); + } + Logging.Dump("Salt", salt, saltLen); + } + + public static void randBytes(byte[] buf, int length) { RNG.GetBytes(buf, length); } + + public abstract int cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen); + + public abstract int cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen); + + #region TCP + + public override void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength) + { + Debug.Assert(_encCircularBuffer != null, "_encCircularBuffer != null"); + + _encCircularBuffer.Put(buf, 0, length); + outlength = 0; + Logging.Debug("---Start Encryption"); + if (! _encryptSaltSent) { + _encryptSaltSent = true; + // Generate salt + byte[] saltBytes = new byte[saltLen]; + randBytes(saltBytes, saltLen); + InitCipher(saltBytes, true, false); + Array.Copy(saltBytes, 0, outbuf, 0, saltLen); + outlength = saltLen; + Logging.Debug($"_encryptSaltSent outlength {outlength}"); + } + + 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); + Array.Copy(encAddrBufBytes, 0, outbuf, outlength, encAddrBufLength); + outlength += encAddrBufLength; + Logging.Debug($"_tcpRequestSent outlength {outlength}"); + } + + // handle other chunks + while (true) { + uint bufSize = (uint)_encCircularBuffer.Size; + if (bufSize <= 0) return; + var chunklength = (int)Math.Min(bufSize, CHUNK_LEN_MASK); + byte[] chunkBytes = _encCircularBuffer.Get(chunklength); + int encChunkLength; + byte[] encChunkBytes = new byte[chunklength + tagLen * 2 + CHUNK_LEN_BYTES]; + ChunkEncrypt(chunkBytes, chunklength, encChunkBytes, out encChunkLength); + Debug.Assert(encChunkLength == chunklength + tagLen * 2 + CHUNK_LEN_BYTES); + Buffer.BlockCopy(encChunkBytes, 0, outbuf, outlength, encChunkLength); + outlength += encChunkLength; + Logging.Debug("chunks enc outlength " + outlength); + // check if we have enough space for outbuf + if (outlength + TCPHandler.ChunkOverheadSize > TCPHandler.BufferSize) { + Logging.Debug("enc outbuf almost full, giving up"); + return; + } + bufSize = (uint)_encCircularBuffer.Size; + if (bufSize <= 0) { + Logging.Debug("No more data to encrypt, leaving"); + return; + } + } + } + + + public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) + { + Debug.Assert(_decCircularBuffer != null, "_decCircularBuffer != null"); + int bufSize; + outlength = 0; + // drop all into buffer + _decCircularBuffer.Put(buf, 0, length); + + Logging.Debug("---Start Decryption"); + if (! _decryptSaltReceived) { + bufSize = _decCircularBuffer.Size; + // check if we get the leading salt + if (bufSize <= saltLen) { + // need more + return; + } + _decryptSaltReceived = true; + byte[] salt = _decCircularBuffer.Get(saltLen); + InitCipher(salt, false, false); + Logging.Debug("get salt len " + saltLen); + } + + // handle chunks + while (true) { + bufSize = _decCircularBuffer.Size; + // check if we have any data + if (bufSize <= 0) { + Logging.Debug("No data in _decCircularBuffer"); + return; + } + + // first get chunk length + if (bufSize <= CHUNK_LEN_BYTES + tagLen) { + // so we only have chunk length and its tag? + return; + } + + #region Chunk Decryption + + byte[] encLenBytes = _decCircularBuffer.Peek(CHUNK_LEN_BYTES + tagLen); + 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); + // finally we get the real chunk len + ushort chunkLen = (ushort) IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(decChunkLenBytes, 0)); + if (chunkLen > CHUNK_LEN_MASK) + { + // we get invalid chunk + Logging.Error($"Invalid chunk length: {chunkLen}"); + throw new CryptoErrorException(); + } + Logging.Debug("Get the real chunk len:" + chunkLen); + bufSize = _decCircularBuffer.Size; + if (bufSize < CHUNK_LEN_BYTES + tagLen /* we haven't remove them */+ chunkLen + tagLen) { + Logging.Debug("No more data to decrypt one chunk"); + return; + } + IncrementNonce(false); + + // we have enough data to decrypt one chunk + // drop chunk len and its tag from buffer + _decCircularBuffer.Get(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); + IncrementNonce(false); + + #endregion + + // output to outbuf + Buffer.BlockCopy(decChunkBytes, 0, outbuf, outlength, (int) decChunkLen); + outlength += (int)decChunkLen; + Logging.Debug("aead dec outlength " + outlength); + if (outlength + 100 > TCPHandler.BufferSize) + { + Logging.Debug("dec outbuf almost full, giving up"); + return; + } + bufSize = _decCircularBuffer.Size; + // check if we already done all of them + if (bufSize <= 0) { + Logging.Debug("No data in _decCircularBuffer, already all done"); + return; + } + } + } + + #endregion + + #region UDP + + public override void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) + { + // Generate salt + randBytes(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); + } + } + + public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) + { + InitCipher(buf, false, true); + 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; + } + } + + #endregion + + // we know the plaintext length before encryption, so we can do it in one operation + private void ChunkEncrypt(byte[] plaintext, int plainLen, byte[] ciphertext, out int cipherLen) + { + if (plainLen > CHUNK_LEN_MASK) { + Logging.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); + 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); + 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); + } + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADMbedTLSEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADMbedTLSEncryptor.cs new file mode 100644 index 00000000..c445c691 --- /dev/null +++ b/shadowsocks-csharp/Encryption/AEAD/AEADMbedTLSEncryptor.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Shadowsocks.Encryption.Exception; + +namespace Shadowsocks.Encryption.AEAD +{ + public class AEADMbedTLSEncryptor + : AEADEncryptor, IDisposable + { + const int CIPHER_AES = 1; + + private IntPtr _encryptCtx = IntPtr.Zero; + private IntPtr _decryptCtx = IntPtr.Zero; + + public AEADMbedTLSEncryptor(string method, string password) + : base(method, password) + { + } + + private static Dictionary _ciphers = new Dictionary + { + {"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)}, + }; + + public static List SupportedCiphers() + { + return new List(_ciphers.Keys); + } + + protected override Dictionary getCiphers() + { + return _ciphers; + } + + public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) + { + base.InitCipher(salt, isEncrypt, isUdp); + IntPtr ctx = Marshal.AllocHGlobal(MbedTLS.cipher_get_size_ex()); + if (isEncrypt) + { + _encryptCtx = ctx; + } + else + { + _decryptCtx = ctx; + } + MbedTLS.cipher_init(ctx); + if (MbedTLS.cipher_setup(ctx, MbedTLS.cipher_info_from_string(_innerLibName)) != 0) + throw new System.Exception("Cannot initialize mbed TLS cipher context"); + + DeriveSessionKey(isEncrypt ? _encryptSalt : _decryptSalt, + _Masterkey, _sessionKey); + CipherSetKey(isEncrypt, _sessionKey); + } + + private void CipherSetKey(bool isEncrypt, byte[] key) + { + IntPtr ctx = isEncrypt ? _encryptCtx : _decryptCtx; + int ret = MbedTLS.cipher_setkey(ctx, key, keyLen * 8, + isEncrypt ? MbedTLS.MBEDTLS_ENCRYPT : MbedTLS.MBEDTLS_DECRYPT); + if (ret != 0) throw new System.Exception("failed to set key"); + ret = MbedTLS.cipher_reset(ctx); + if (ret != 0) throw new System.Exception("failed to finish preparation"); + } + + public override int cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen) + { + // buf: all plaintext + // outbuf: ciphertext + tag + int ret; + byte[] tagbuf = new byte[tagLen]; + uint olen = 0; + switch (_cipher) + { + case CIPHER_AES: + ret = MbedTLS.cipher_auth_encrypt(_encryptCtx, + /* nonce */ + _encNonce, (uint) nonceLen, + /* AD */ + IntPtr.Zero, 0, + /* plain */ + plaintext, plen, + /* cipher */ + ciphertext, ref olen, + tagbuf, (uint) tagLen); + if (ret != 0) throw new CryptoErrorException(); + Debug.Assert(olen == plen); + // attach tag to ciphertext + Array.Copy(tagbuf, 0, ciphertext, (int) plen, tagLen); + clen = olen + (uint) tagLen; + return ret; + default: + throw new System.Exception("not implemented"); + } + } + + public override int cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen) + { + // buf: ciphertext + tag + // outbuf: plaintext + int ret; + uint olen = 0; + // split tag + byte[] tagbuf = new byte[tagLen]; + Array.Copy(ciphertext, (int) (clen - tagLen), tagbuf, 0, tagLen); + switch (_cipher) + { + case CIPHER_AES: + ret = MbedTLS.cipher_auth_decrypt(_decryptCtx, + _decNonce, (uint) nonceLen, + IntPtr.Zero, 0, + ciphertext, (uint) (clen - tagLen), + plaintext, ref olen, + tagbuf, (uint) tagLen); + if (ret != 0) throw new CryptoErrorException(); + Debug.Assert(olen == clen - tagLen); + plen = olen; + return ret; + default: + throw new System.Exception("not implemented"); + } + } + + #region IDisposable + + private bool _disposed; + + // instance based lock + private readonly object _lock = new object(); + + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~AEADMbedTLSEncryptor() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + lock (_lock) + { + if (_disposed) return; + _disposed = true; + } + + if (disposing) + { + // free managed objects + } + + // free unmanaged objects + if (_encryptCtx != IntPtr.Zero) + { + MbedTLS.cipher_free(_encryptCtx); + Marshal.FreeHGlobal(_encryptCtx); + _encryptCtx = IntPtr.Zero; + } + if (_decryptCtx != IntPtr.Zero) + { + MbedTLS.cipher_free(_decryptCtx); + Marshal.FreeHGlobal(_decryptCtx); + _decryptCtx = IntPtr.Zero; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADSodiumEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADSodiumEncryptor.cs new file mode 100644 index 00000000..1cce0fa0 --- /dev/null +++ b/shadowsocks-csharp/Encryption/AEAD/AEADSodiumEncryptor.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Shadowsocks.Controller; +using Shadowsocks.Encryption.Exception; + +namespace Shadowsocks.Encryption.AEAD +{ + public class AEADSodiumEncryptor + : AEADEncryptor, IDisposable + { + private const int CIPHER_CHACHA20IETFPOLY1305 = 1; + + private byte[] _sodiumEncSubkey; + private byte[] _sodiumDecSubkey; + + public AEADSodiumEncryptor(string method, string password) + : base(method, password) + { + _sodiumEncSubkey = new byte[keyLen]; + _sodiumDecSubkey = new byte[keyLen]; + } + + private static Dictionary _ciphers = new Dictionary + { + {"chacha20-ietf-poly1305", new EncryptorInfo(32, 32, 12, 16, CIPHER_CHACHA20IETFPOLY1305)}, + }; + + public static List SupportedCiphers() + { + return new List(_ciphers.Keys); + } + + protected override Dictionary getCiphers() + { + return _ciphers; + } + + public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) + { + base.InitCipher(salt, isEncrypt, isUdp); + DeriveSessionKey(isEncrypt ? _encryptSalt : _decryptSalt, _Masterkey, + isEncrypt ? _sodiumEncSubkey : _sodiumDecSubkey); + } + + + public override int cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen) + { + Debug.Assert(_sodiumEncSubkey != null); + // buf: all plaintext + // outbuf: ciphertext + tag + int ret; + ulong encClen = 0; + Logging.Dump("_encNonce before enc", _encNonce, nonceLen); + Logging.Dump("_sodiumEncSubkey", _sodiumEncSubkey, keyLen); + Logging.Dump("before cipherEncrypt: plain", plaintext, (int) plen); + switch (_cipher) + { + case CIPHER_CHACHA20IETFPOLY1305: + ret = Sodium.crypto_aead_chacha20poly1305_ietf_encrypt(ciphertext, ref encClen, + plaintext, (ulong) plen, + null, 0, + null, _encNonce, + _sodiumEncSubkey); + break; + default: + throw new System.Exception("not implemented"); + } + if (ret != 0) throw new CryptoErrorException(); + Logging.Dump("after cipherEncrypt: cipher", ciphertext, (int) encClen); + clen = (uint) encClen; + return ret; + } + + public override int cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen) + { + Debug.Assert(_sodiumDecSubkey != null); + // buf: ciphertext + tag + // outbuf: plaintext + int ret; + ulong decPlen = 0; + Logging.Dump("_decNonce before dec", _decNonce, nonceLen); + Logging.Dump("_sodiumDecSubkey", _sodiumDecSubkey, keyLen); + Logging.Dump("before cipherDecrypt: cipher", ciphertext, (int) clen); + switch (_cipher) + { + case CIPHER_CHACHA20IETFPOLY1305: + ret = Sodium.crypto_aead_chacha20poly1305_ietf_decrypt(plaintext, ref decPlen, + null, + ciphertext, (ulong) clen, + null, 0, + _decNonce, _sodiumDecSubkey); + break; + default: + throw new System.Exception("not implemented"); + } + + if (ret != 0) throw new CryptoErrorException(); + Logging.Dump("after cipherDecrypt: plain", plaintext, (int) decPlen); + plen = (uint) decPlen; + return ret; + } + + public override void Dispose() + { + } + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/EncryptorBase.cs b/shadowsocks-csharp/Encryption/EncryptorBase.cs index d3692660..5b542094 100644 --- a/shadowsocks-csharp/Encryption/EncryptorBase.cs +++ b/shadowsocks-csharp/Encryption/EncryptorBase.cs @@ -1,16 +1,20 @@ -using System.Text; - -namespace Shadowsocks.Encryption +namespace Shadowsocks.Encryption { - public struct EncryptorInfo + 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; @@ -26,6 +30,32 @@ namespace Shadowsocks.Encryption 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 @@ -33,30 +63,33 @@ namespace Shadowsocks.Encryption { public const int MAX_INPUT_SIZE = 32768; - protected EncryptorBase(string method, string password, bool onetimeauth, bool isudp) + public const int MAX_DOMAIN_LEN = 255; + public const int ADDR_PORT_LEN = 2; + public const int ADDR_ATYP_LEN = 1; + + public const int ATYP_IPv4 = 0x01; + public const int ATYP_DOMAIN = 0x03; + public const int ATYP_IPv6 = 0x04; + + protected EncryptorBase(string method, string password) { Method = method; Password = password; - OnetimeAuth = onetimeauth; - IsUDP = isudp; } protected string Method; protected string Password; - protected bool OnetimeAuth; - protected bool IsUDP; - - protected byte[] GetPasswordHash() - { - byte[] inputBytes = Encoding.UTF8.GetBytes(Password); - byte[] hash = MbedTLS.MD5(inputBytes); - return hash; - } 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); + public abstract void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); + + public abstract void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); + public abstract void Dispose(); + + public int AddrBufLength { get; set; } = - 1; } -} +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/EncryptorFactory.cs b/shadowsocks-csharp/Encryption/EncryptorFactory.cs index f1091f98..331570fd 100644 --- a/shadowsocks-csharp/Encryption/EncryptorFactory.cs +++ b/shadowsocks-csharp/Encryption/EncryptorFactory.cs @@ -1,29 +1,38 @@ using System; using System.Collections.Generic; using System.Reflection; +using Shadowsocks.Encryption.AEAD; +using Shadowsocks.Encryption.Stream; namespace Shadowsocks.Encryption { public static class EncryptorFactory { - private static Dictionary _registeredEncryptors; + private static Dictionary _registeredEncryptors = new Dictionary(); - private static Type[] _constructorTypes = new Type[] { typeof(string), typeof(string), typeof(bool), typeof(bool) }; + private static readonly Type[] ConstructorTypes = {typeof(string), typeof(string)}; static EncryptorFactory() { - _registeredEncryptors = new Dictionary(); - foreach (string method in MbedTLSEncryptor.SupportedCiphers()) + foreach (string method in StreamMbedTLSEncryptor.SupportedCiphers()) { - _registeredEncryptors.Add(method, typeof(MbedTLSEncryptor)); + _registeredEncryptors.Add(method, typeof(StreamMbedTLSEncryptor)); } - foreach (string method in SodiumEncryptor.SupportedCiphers()) + foreach (string method in StreamSodiumEncryptor.SupportedCiphers()) { - _registeredEncryptors.Add(method, typeof(SodiumEncryptor)); + _registeredEncryptors.Add(method, typeof(StreamSodiumEncryptor)); + } + foreach (string method in AEADMbedTLSEncryptor.SupportedCiphers()) + { + _registeredEncryptors.Add(method, typeof(AEADMbedTLSEncryptor)); + } + foreach (string method in AEADSodiumEncryptor.SupportedCiphers()) + { + _registeredEncryptors.Add(method, typeof(AEADSodiumEncryptor)); } } - public static IEncryptor GetEncryptor(string method, string password, bool onetimeauth, bool isudp) + public static IEncryptor GetEncryptor(string method, string password) { if (method.IsNullOrEmpty()) { @@ -31,9 +40,10 @@ namespace Shadowsocks.Encryption } method = method.ToLowerInvariant(); Type t = _registeredEncryptors[method]; - ConstructorInfo c = t.GetConstructor(_constructorTypes); - IEncryptor result = (IEncryptor)c.Invoke(new object[] { method, password, onetimeauth, isudp }); + ConstructorInfo c = t.GetConstructor(ConstructorTypes); + if (c == null) throw new System.Exception("Invalid ctor"); + IEncryptor result = (IEncryptor) c.Invoke(new object[] {method, password}); return result; } } -} +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/Exception/CryptoException.cs b/shadowsocks-csharp/Encryption/Exception/CryptoException.cs new file mode 100644 index 00000000..2871affc --- /dev/null +++ b/shadowsocks-csharp/Encryption/Exception/CryptoException.cs @@ -0,0 +1,17 @@ +namespace Shadowsocks.Encryption.Exception +{ + public class CryptoErrorException : System.Exception + { + public CryptoErrorException() + { + } + + public CryptoErrorException(string msg) : base(msg) + { + } + + public CryptoErrorException(string message, System.Exception innerException) : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/IEncryptor.cs b/shadowsocks-csharp/Encryption/IEncryptor.cs index a1ddc7f0..d238083c 100644 --- a/shadowsocks-csharp/Encryption/IEncryptor.cs +++ b/shadowsocks-csharp/Encryption/IEncryptor.cs @@ -4,7 +4,11 @@ namespace Shadowsocks.Encryption { public interface IEncryptor : IDisposable { + /* length == -1 means not used */ + int AddrBufLength { 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); + void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); } } diff --git a/shadowsocks-csharp/Encryption/IVEncryptor.cs b/shadowsocks-csharp/Encryption/IVEncryptor.cs deleted file mode 100755 index cf673a66..00000000 --- a/shadowsocks-csharp/Encryption/IVEncryptor.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Concurrent; -using System.Text; -using System.Net; - -namespace Shadowsocks.Encryption -{ - public abstract class IVEncryptor - : EncryptorBase - { - public const int MAX_KEY_LENGTH = 64; - public const int MAX_IV_LENGTH = 16; - - protected static byte[] tempbuf = new byte[MAX_INPUT_SIZE]; - - protected Dictionary ciphers; - - private static readonly ConcurrentDictionary CachedKeys = new ConcurrentDictionary(); - protected byte[] _encryptIV; - protected byte[] _decryptIV; - protected bool _decryptIVReceived; - protected bool _encryptIVSent; - protected string _method; - protected int _cipher; - // internal name in the crypto library - protected string _innerLibName; - protected EncryptorInfo CipherInfo; - protected byte[] _key; - protected int keyLen; - protected int ivLen; - - public IVEncryptor(string method, string password, bool onetimeauth, bool isudp) - : base(method, password, onetimeauth, isudp) - { - InitKey(method, password); - } - - protected abstract Dictionary getCiphers(); - - private void InitKey(string method, string password) - { - method = method.ToLower(); - _method = method; - string k = method + ":" + password; - ciphers = getCiphers(); - CipherInfo = ciphers[_method]; - _innerLibName = CipherInfo.InnerLibName; - _cipher = CipherInfo.Type; - if (_cipher == 0) - { - throw new Exception("method not found"); - } - keyLen = CipherInfo.KeySize; - ivLen = CipherInfo.IvSize; - _key = CachedKeys.GetOrAdd(k, (nk) => - { - byte[] passbuf = Encoding.UTF8.GetBytes(password); - byte[] key = new byte[32]; - bytesToKey(passbuf, key); - return key; - }); - } - - protected void bytesToKey(byte[] password, byte[] key) - { - byte[] result = new byte[password.Length + 16]; - int i = 0; - byte[] md5sum = null; - while (i < key.Length) - { - if (i == 0) - { - md5sum = MbedTLS.MD5(password); - } - else - { - md5sum.CopyTo(result, 0); - password.CopyTo(result, md5sum.Length); - md5sum = MbedTLS.MD5(result); - } - md5sum.CopyTo(key, i); - i += md5sum.Length; - } - } - - protected virtual void initCipher(byte[] iv, bool isCipher) - { - if (ivLen > 0) - { - if (isCipher) - { - _encryptIV = new byte[ivLen]; - Array.Copy(iv, _encryptIV, ivLen); - } - else - { - _decryptIV = new byte[ivLen]; - Array.Copy(iv, _decryptIV, ivLen); - } - } - } - - protected abstract void cipherUpdate(bool isCipher, int length, byte[] buf, byte[] outbuf); - - #region OneTimeAuth - - public const int ONETIMEAUTH_FLAG = 0x10; - public const int ADDRTYPE_MASK = 0xEF; - - public const int ONETIMEAUTH_BYTES = 10; - - public const int CLEN_BYTES = 2; - public const int AUTH_BYTES = ONETIMEAUTH_BYTES + CLEN_BYTES; - - private uint _otaChunkCounter; - private byte[] _otaChunkKeyBuffer; - - protected int OtaGetHeadLen(byte[] buf, int length) - { - int len = 0; - int atyp = length > 0 ? (buf[0] & ADDRTYPE_MASK) : 0; - if (atyp == 1) - { - len = 7; // atyp (1 bytes) + ipv4 (4 bytes) + port (2 bytes) - } - else if (atyp == 3 && length > 1) - { - int nameLen = buf[1]; - len = 4 + nameLen; // atyp (1 bytes) + name length (1 bytes) + name (n bytes) + port (2 bytes) - } - else if (atyp == 4) - { - len = 19; // atyp (1 bytes) + ipv6 (16 bytes) + port (2 bytes) - } - if (len == 0 || len > length) - throw new Exception($"invalid header with addr type {atyp}"); - return len; - } - - private byte[] OtaGenHash(byte[] msg, int msg_len) - { - byte[] auth = new byte[ONETIMEAUTH_BYTES]; - byte[] hash = new byte[20]; - byte[] auth_key = new byte[MAX_IV_LENGTH + MAX_KEY_LENGTH]; - Buffer.BlockCopy(_encryptIV, 0, auth_key, 0, ivLen); - Buffer.BlockCopy(_key, 0, auth_key, ivLen, keyLen); - Sodium.ss_sha1_hmac_ex(auth_key, (uint)(ivLen + keyLen), - msg, 0, (uint)msg_len, hash); - Buffer.BlockCopy(hash, 0, auth, 0, ONETIMEAUTH_BYTES); - return auth; - } - - private void OtaUpdateKeyBuffer() - { - if (_otaChunkKeyBuffer == null) - { - _otaChunkKeyBuffer = new byte[MAX_IV_LENGTH + 4]; - Buffer.BlockCopy(_encryptIV, 0, _otaChunkKeyBuffer, 0, ivLen); - } - - byte[] counter_bytes = BitConverter.GetBytes((uint)IPAddress.HostToNetworkOrder((int)_otaChunkCounter)); - Buffer.BlockCopy(counter_bytes, 0, _otaChunkKeyBuffer, ivLen, 4); - _otaChunkCounter++; - } - - private byte[] OtaGenChunkHash(byte[] buf, int offset, int len) - { - byte[] hash = new byte[20]; - OtaUpdateKeyBuffer(); - Sodium.ss_sha1_hmac_ex(_otaChunkKeyBuffer, (uint)_otaChunkKeyBuffer.Length, - buf, offset, (uint)len, hash); - return hash; - } - - private void OtaAuthBuffer4Tcp(byte[] buf, ref int length) - { - if (!_encryptIVSent) - { - int headLen = OtaGetHeadLen(buf, length); - int dataLen = length - headLen; - buf[0] |= ONETIMEAUTH_FLAG; - byte[] hash = OtaGenHash(buf, headLen); - Buffer.BlockCopy(buf, headLen, buf, headLen + ONETIMEAUTH_BYTES + AUTH_BYTES, dataLen); - Buffer.BlockCopy(hash, 0, buf, headLen, ONETIMEAUTH_BYTES); - - if (dataLen == 0) - { - length = headLen + ONETIMEAUTH_BYTES; - } - else - { - hash = OtaGenChunkHash(buf, headLen + ONETIMEAUTH_BYTES + AUTH_BYTES, dataLen); - Buffer.BlockCopy(hash, 0, buf, headLen + ONETIMEAUTH_BYTES + CLEN_BYTES, ONETIMEAUTH_BYTES); - byte[] lenBytes = BitConverter.GetBytes((ushort) IPAddress.HostToNetworkOrder((short) dataLen)); - Buffer.BlockCopy(lenBytes, 0, buf, headLen + ONETIMEAUTH_BYTES, CLEN_BYTES); - length = headLen + ONETIMEAUTH_BYTES + AUTH_BYTES + dataLen; - } - } - else - { - byte[] hash = OtaGenChunkHash(buf, 0, length); - Buffer.BlockCopy(buf, 0, buf, AUTH_BYTES, length); - byte[] lenBytes = BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)length)); - Buffer.BlockCopy(lenBytes, 0, buf, 0, CLEN_BYTES); - Buffer.BlockCopy(hash, 0, buf, CLEN_BYTES, ONETIMEAUTH_BYTES); - length += AUTH_BYTES; - } - } - - private void OtaAuthBuffer4Udp(byte[] buf, ref int length) - { - buf[0] |= ONETIMEAUTH_FLAG; - byte[] hash = OtaGenHash(buf, length); - Buffer.BlockCopy(hash, 0, buf, length, ONETIMEAUTH_BYTES); - length += ONETIMEAUTH_BYTES; - } - - private void OtaAuthBuffer(byte[] buf, ref int length) - { - if (OnetimeAuth && ivLen > 0) - { - if (!IsUDP) - { - OtaAuthBuffer4Tcp(buf, ref length); - } - else - { - OtaAuthBuffer4Udp(buf, ref length); - } - } - } - - #endregion - - protected static void randBytes(byte[] buf, int length) - { - RNG.GetBytes(buf, length); - } - - public override void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength) - { - if (!_encryptIVSent) - { - // Generate IV - randBytes(outbuf, ivLen); - initCipher(outbuf, true); - outlength = length + ivLen; - OtaAuthBuffer(buf, ref length); - _encryptIVSent = true; - lock (tempbuf) - { - cipherUpdate(true, length, buf, tempbuf); - outlength = length + ivLen; - Buffer.BlockCopy(tempbuf, 0, outbuf, ivLen, length); - } - } - else - { - OtaAuthBuffer(buf, ref length); - outlength = length; - cipherUpdate(true, length, buf, outbuf); - } - } - - public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) - { - if (!_decryptIVReceived) - { - _decryptIVReceived = true; - initCipher(buf, false); - outlength = length - ivLen; - lock (tempbuf) - { - // C# could be multi-threaded - Buffer.BlockCopy(buf, ivLen, tempbuf, 0, length - ivLen); - cipherUpdate(false, length - ivLen, tempbuf, outbuf); - } - } - else - { - outlength = length; - cipherUpdate(false, length, buf, outbuf); - } - } - - } -} diff --git a/shadowsocks-csharp/Encryption/MbedTLS.cs b/shadowsocks-csharp/Encryption/MbedTLS.cs index 01f8fd37..b1d762d0 100644 --- a/shadowsocks-csharp/Encryption/MbedTLS.cs +++ b/shadowsocks-csharp/Encryption/MbedTLS.cs @@ -1,76 +1,105 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -using Shadowsocks.Controller; -using Shadowsocks.Properties; -using Shadowsocks.Util; - -namespace Shadowsocks.Encryption -{ - public class MbedTLS - { - const string DLLNAME = "libsscrypto"; - - public const int MBEDTLS_ENCRYPT = 1; - public const int MBEDTLS_DECRYPT = 0; - - static MbedTLS() - { - string dllPath = Utils.GetTempPath("libsscrypto.dll"); - try - { - FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll); - } - catch (IOException) - { - } - catch (Exception e) - { - Logging.LogUsefulException(e); - } - LoadLibrary(dllPath); - } - - public static byte[] MD5(byte[] input) - { - byte[] output = new byte[16]; - md5(input, (uint)input.Length, output); - return output; - } - - [DllImport("Kernel32.dll")] - private static extern IntPtr LoadLibrary(string path); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr cipher_info_from_string(string cipher_name); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void cipher_init(IntPtr ctx); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_setup(IntPtr ctx, IntPtr cipher_info); - - // XXX: Check operation before using it - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_setkey(IntPtr ctx, byte[] key, int key_bitlen, int operation); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_set_iv(IntPtr ctx, byte[] iv, int iv_len); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_reset(IntPtr ctx); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_update(IntPtr ctx, byte[] input, int ilen, byte[] output, ref int olen); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void cipher_free(IntPtr ctx); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void md5(byte[] input, uint ilen, byte[] output); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_get_size_ex(); - } -} +using System; +using System.IO; +using System.Runtime.InteropServices; +using Shadowsocks.Controller; +using Shadowsocks.Properties; +using Shadowsocks.Util; + +namespace Shadowsocks.Encryption +{ + public static class MbedTLS + { + private const string DLLNAME = "libsscrypto.dll"; + + public const int MBEDTLS_ENCRYPT = 1; + public const int MBEDTLS_DECRYPT = 0; + + static MbedTLS() + { + string dllPath = Utils.GetTempPath(DLLNAME); + try + { + FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll); + } + catch (IOException) + { + } + catch (System.Exception e) + { + Logging.LogUsefulException(e); + } + LoadLibrary(dllPath); + } + + public static byte[] MD5(byte[] input) + { + byte[] output = new byte[16]; + md5(input, (uint) input.Length, output); + return output; + } + + [DllImport("Kernel32.dll")] + private static extern IntPtr LoadLibrary(string path); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern void md5(byte[] input, uint ilen, byte[] output); + + /// + /// Get cipher ctx size for unmanaged memory allocation + /// + /// + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_get_size_ex(); + + #region Cipher layer wrappers + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr cipher_info_from_string(string cipher_name); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern void cipher_init(IntPtr ctx); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_setup(IntPtr ctx, IntPtr cipher_info); + + // XXX: Check operation before using it + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_setkey(IntPtr ctx, byte[] key, int key_bitlen, int operation); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_set_iv(IntPtr ctx, byte[] iv, int iv_len); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_reset(IntPtr ctx); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_update(IntPtr ctx, byte[] input, int ilen, byte[] output, ref int olen); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern void cipher_free(IntPtr ctx); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_auth_encrypt(IntPtr ctx, + byte[] iv, uint iv_len, + IntPtr ad, uint ad_len, + byte[] input, uint ilen, + byte[] output, ref uint olen, + byte[] tag, uint tag_len); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int cipher_auth_decrypt(IntPtr ctx, + byte[] iv, uint iv_len, + IntPtr ad, uint ad_len, + byte[] input, uint ilen, + byte[] output, ref uint olen, + byte[] tag, uint tag_len); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int hkdf(byte[] salt, + int salt_len, byte[] ikm, int ikm_len, + byte[] info, int info_len, byte[] okm, + int okm_len); + + #endregion + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/RNG.cs b/shadowsocks-csharp/Encryption/RNG.cs index 6e31f1f5..b44c51ac 100644 --- a/shadowsocks-csharp/Encryption/RNG.cs +++ b/shadowsocks-csharp/Encryption/RNG.cs @@ -28,16 +28,17 @@ namespace Shadowsocks.Encryption public static void GetBytes(byte[] buf) { - _rng.GetBytes(buf); + GetBytes(buf, buf.Length); } public static void GetBytes(byte[] buf, int len) { + if (_rng == null) Reload(); try { _rng.GetBytes(buf, 0, len); } - catch (Exception) + catch (System.Exception) { // the backup way byte[] tmp = new byte[len]; diff --git a/shadowsocks-csharp/Encryption/Sodium.cs b/shadowsocks-csharp/Encryption/Sodium.cs index 22a7b7a7..5cc853ad 100755 --- a/shadowsocks-csharp/Encryption/Sodium.cs +++ b/shadowsocks-csharp/Encryption/Sodium.cs @@ -1,20 +1,22 @@ using System; using System.IO; using System.Runtime.InteropServices; - using Shadowsocks.Controller; using Shadowsocks.Properties; using Shadowsocks.Util; namespace Shadowsocks.Encryption { - public class Sodium + public static class Sodium { - const string DLLNAME = "libsscrypto"; + private const string DLLNAME = "libsscrypto.dll"; + + private static bool _initialized = false; + private static readonly object _initLock = new object(); static Sodium() { - string dllPath = Utils.GetTempPath("libsscrypto.dll"); + string dllPath = Utils.GetTempPath(DLLNAME); try { FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll); @@ -22,29 +24,63 @@ namespace Shadowsocks.Encryption catch (IOException) { } - catch (Exception e) + catch (System.Exception e) { Logging.LogUsefulException(e); } LoadLibrary(dllPath); + + lock (_initLock) + { + if (!_initialized) + { + if (sodium_init() == -1) + { + throw new System.Exception("Failed to initialize sodium"); + } + else /* 1 means already initialized; 0 means success */ + { + _initialized = true; + } + } + } } [DllImport("Kernel32.dll")] private static extern IntPtr LoadLibrary(string path); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_stream_salsa20_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, ulong ic, byte[] k); + private static extern int sodium_init(); + + #region AEAD [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_stream_chacha20_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, ulong ic, byte[] k); + public static extern int sodium_increment(byte[] n, int nlen); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_stream_chacha20_ietf_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, uint ic, byte[] k); + public static extern int crypto_aead_chacha20poly1305_ietf_encrypt(byte[] c, ref ulong clen_p, byte[] m, + ulong mlen, byte[] ad, ulong adlen, byte[] nsec, byte[] npub, byte[] k); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void ss_sha1_hmac_ex(byte[] key, uint keylen, - byte[] input, int ioff, uint ilen, - byte[] output); - } -} + public static extern int crypto_aead_chacha20poly1305_ietf_decrypt(byte[] m, ref ulong mlen_p, + byte[] nsec, byte[] c, ulong clen, byte[] ad, ulong adlen, byte[] npub, byte[] k); + + #endregion + #region Stream + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int crypto_stream_salsa20_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, ulong ic, + byte[] k); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int crypto_stream_chacha20_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, ulong ic, + byte[] k); + + [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int crypto_stream_chacha20_ietf_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, uint ic, + byte[] k); + + #endregion + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs b/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs new file mode 100644 index 00000000..a85c1187 --- /dev/null +++ b/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Cyotek.Collections.Generic; +using Shadowsocks.Controller; + +namespace Shadowsocks.Encryption.Stream +{ + public abstract class StreamEncryptor + : EncryptorBase + { + // for UDP only + protected static byte[] _udpTmpBuf = new byte[MAX_INPUT_SIZE]; + + // every connection should create its own buffer + private CircularBuffer _encCircularBuffer = new CircularBuffer(TCPHandler.BufferSize * 2, false); + private CircularBuffer _decCircularBuffer = new CircularBuffer(TCPHandler.BufferSize * 2, false); + + protected Dictionary ciphers; + + protected byte[] _encryptIV; + protected byte[] _decryptIV; + + // Is first packet + protected bool _decryptIVReceived; + protected bool _encryptIVSent; + + protected string _method; + protected int _cipher; + // internal name in the crypto library + protected string _innerLibName; + protected EncryptorInfo CipherInfo; + // long-time master key + protected static byte[] _key = null; + protected int keyLen; + protected int ivLen; + + public StreamEncryptor(string method, string password) + : base(method, password) + { + InitEncryptorInfo(method); + InitKey(password); + } + + protected abstract Dictionary getCiphers(); + + private void InitEncryptorInfo(string method) + { + method = method.ToLower(); + _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; + } + + private void InitKey(string password) + { + byte[] passbuf = Encoding.UTF8.GetBytes(password); + if (_key == null) _key = new byte[keyLen]; + if (_key.Length < keyLen) Array.Resize(ref _key, keyLen); + LegacyDeriveKey(passbuf, _key); + } + + public static void LegacyDeriveKey(byte[] password, byte[] key) + { + byte[] result = new byte[password.Length + 16]; + int i = 0; + byte[] md5sum = null; + while (i < key.Length) { + if (i == 0) { + md5sum = MbedTLS.MD5(password); + } else { + md5sum.CopyTo(result, 0); + password.CopyTo(result, md5sum.Length); + md5sum = MbedTLS.MD5(result); + } + md5sum.CopyTo(key, i); + i += md5sum.Length; + } + } + + protected virtual void initCipher(byte[] iv, bool isEncrypt) + { + if (isEncrypt) { + _encryptIV = new byte[ivLen]; + Array.Copy(iv, _encryptIV, ivLen); + } else { + _decryptIV = new byte[ivLen]; + Array.Copy(iv, _decryptIV, ivLen); + } + } + + protected abstract void cipherUpdate(bool isEncrypt, int length, byte[] buf, byte[] outbuf); + + protected static void randBytes(byte[] buf, int length) { RNG.GetBytes(buf, length); } + + #region TCP + + public override void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength) + { + int cipherOffset = 0; + Debug.Assert(_encCircularBuffer != null, "_encCircularBuffer != null"); + _encCircularBuffer.Put(buf, 0, length); + 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; + } + int size = _encCircularBuffer.Size; + byte[] plain = _encCircularBuffer.Get(size); + byte[] cipher = new byte[size]; + cipherUpdate(true, size, plain, cipher); + Buffer.BlockCopy(cipher, 0, outbuf, cipherOffset, size); + outlength = size + cipherOffset; + } + + public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) + { + Debug.Assert(_decCircularBuffer != null, "_circularBuffer != null"); + _decCircularBuffer.Put(buf, 0, length); + 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); + } + byte[] cipher = _decCircularBuffer.ToArray(); + cipherUpdate(false, cipher.Length, cipher, outbuf); + _decCircularBuffer.Clear(); + outlength = cipher.Length; + // done the decryption + } + + #endregion + + #region UDP + + public override void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) + { + // Generate IV + randBytes(outbuf, ivLen); + initCipher(outbuf, true); + lock (_udpTmpBuf) { + cipherUpdate(true, length, buf, _udpTmpBuf); + outlength = length + ivLen; + Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, ivLen, length); + } + } + + public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) + { + // Get IV from first pos + initCipher(buf, false); + outlength = length - ivLen; + lock (_udpTmpBuf) { + // C# could be multi-threaded + Buffer.BlockCopy(buf, ivLen, _udpTmpBuf, 0, length - ivLen); + cipherUpdate(false, length - ivLen, _udpTmpBuf, outbuf); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/MbedTLSEncryptor.cs b/shadowsocks-csharp/Encryption/Stream/StreamMbedTLSEncryptor.cs similarity index 78% rename from shadowsocks-csharp/Encryption/MbedTLSEncryptor.cs rename to shadowsocks-csharp/Encryption/Stream/StreamMbedTLSEncryptor.cs index 033daf4c..9010aa8f 100644 --- a/shadowsocks-csharp/Encryption/MbedTLSEncryptor.cs +++ b/shadowsocks-csharp/Encryption/Stream/StreamMbedTLSEncryptor.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using Shadowsocks.Encryption.Exception; -namespace Shadowsocks.Encryption +namespace Shadowsocks.Encryption.Stream { - public class MbedTLSEncryptor - : IVEncryptor, IDisposable + public class StreamMbedTLSEncryptor + : StreamEncryptor, IDisposable { const int CIPHER_RC4 = 1; const int CIPHER_AES = 2; @@ -15,8 +16,8 @@ namespace Shadowsocks.Encryption private IntPtr _encryptCtx = IntPtr.Zero; private IntPtr _decryptCtx = IntPtr.Zero; - public MbedTLSEncryptor(string method, string password, bool onetimeauth, bool isudp) - : base(method, password, onetimeauth, isudp) + public StreamMbedTLSEncryptor(string method, string password) + : base(method, password) { } @@ -44,11 +45,11 @@ namespace Shadowsocks.Encryption return _ciphers; } - protected override void initCipher(byte[] iv, bool isCipher) + protected override void initCipher(byte[] iv, bool isEncrypt) { - base.initCipher(iv, isCipher); + base.initCipher(iv, isEncrypt); IntPtr ctx = Marshal.AllocHGlobal(MbedTLS.cipher_get_size_ex()); - if (isCipher) + if (isEncrypt) { _encryptCtx = ctx; } @@ -71,7 +72,7 @@ namespace Shadowsocks.Encryption } MbedTLS.cipher_init(ctx); if (MbedTLS.cipher_setup( ctx, MbedTLS.cipher_info_from_string( _innerLibName ) ) != 0 ) - throw new Exception("Cannot initialize mbed TLS cipher context"); + throw new System.Exception("Cannot initialize mbed TLS cipher context"); /* * MbedTLS takes key length by bit * cipher_setkey() will set the correct key schedule @@ -84,24 +85,24 @@ namespace Shadowsocks.Encryption * */ if (MbedTLS.cipher_setkey(ctx, realkey, keyLen * 8, - isCipher ? MbedTLS.MBEDTLS_ENCRYPT : MbedTLS.MBEDTLS_DECRYPT) != 0 ) - throw new Exception("Cannot set mbed TLS cipher key"); + isEncrypt ? MbedTLS.MBEDTLS_ENCRYPT : MbedTLS.MBEDTLS_DECRYPT) != 0 ) + throw new System.Exception("Cannot set mbed TLS cipher key"); if (MbedTLS.cipher_set_iv(ctx, iv, ivLen) != 0) - throw new Exception("Cannot set mbed TLS cipher IV"); + throw new System.Exception("Cannot set mbed TLS cipher IV"); if (MbedTLS.cipher_reset(ctx) != 0) - throw new Exception("Cannot finalize mbed TLS cipher context"); + throw new System.Exception("Cannot finalize mbed TLS cipher context"); } - protected override void cipherUpdate(bool isCipher, int length, byte[] buf, byte[] outbuf) + protected override void cipherUpdate(bool isEncrypt, int length, byte[] buf, byte[] outbuf) { // C# could be multi-threaded if (_disposed) { throw new ObjectDisposedException(this.ToString()); } - if (MbedTLS.cipher_update(isCipher ? _encryptCtx : _decryptCtx, + if (MbedTLS.cipher_update(isEncrypt ? _encryptCtx : _decryptCtx, buf, length, outbuf, ref length) != 0 ) - throw new Exception("Cannot update mbed TLS cipher context"); + throw new CryptoErrorException(); } #region IDisposable @@ -117,7 +118,7 @@ namespace Shadowsocks.Encryption GC.SuppressFinalize(this); } - ~MbedTLSEncryptor() + ~StreamMbedTLSEncryptor() { Dispose(false); } diff --git a/shadowsocks-csharp/Encryption/SodiumEncryptor.cs b/shadowsocks-csharp/Encryption/Stream/StreamSodiumEncryptor.cs old mode 100755 new mode 100644 similarity index 73% rename from shadowsocks-csharp/Encryption/SodiumEncryptor.cs rename to shadowsocks-csharp/Encryption/Stream/StreamSodiumEncryptor.cs index 7dd42d2e..492ca945 --- a/shadowsocks-csharp/Encryption/SodiumEncryptor.cs +++ b/shadowsocks-csharp/Encryption/Stream/StreamSodiumEncryptor.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; +using Shadowsocks.Encryption.Exception; -namespace Shadowsocks.Encryption +namespace Shadowsocks.Encryption.Stream { - public class SodiumEncryptor - : IVEncryptor, IDisposable + public class StreamSodiumEncryptor + : StreamEncryptor, IDisposable { const int CIPHER_SALSA20 = 1; const int CIPHER_CHACHA20 = 2; @@ -19,8 +20,8 @@ namespace Shadowsocks.Encryption protected byte[] _encryptBuf; protected byte[] _decryptBuf; - public SodiumEncryptor(string method, string password, bool onetimeauth, bool isudp) - : base(method, password, onetimeauth, isudp) + public StreamSodiumEncryptor(string method, string password) + : base(method, password) { _encryptBuf = new byte[MAX_INPUT_SIZE + SODIUM_BLOCK_SIZE]; _decryptBuf = new byte[MAX_INPUT_SIZE + SODIUM_BLOCK_SIZE]; @@ -42,15 +43,16 @@ namespace Shadowsocks.Encryption return new List(_ciphers.Keys); } - protected override void cipherUpdate(bool isCipher, int length, byte[] buf, byte[] outbuf) + protected override void cipherUpdate(bool isEncrypt, int length, byte[] buf, byte[] outbuf) { // TODO write a unidirection cipher so we don't have to if if if int bytesRemaining; ulong ic; byte[] sodiumBuf; byte[] iv; + int ret = -1; - if (isCipher) + if (isEncrypt) { bytesRemaining = _encryptBytesRemaining; ic = _encryptIC; @@ -70,21 +72,23 @@ namespace Shadowsocks.Encryption switch (_cipher) { case CIPHER_SALSA20: - Sodium.crypto_stream_salsa20_xor_ic(sodiumBuf, sodiumBuf, (ulong)(padding + length), iv, ic, _key); + ret = Sodium.crypto_stream_salsa20_xor_ic(sodiumBuf, sodiumBuf, (ulong)(padding + length), iv, ic, _key); break; case CIPHER_CHACHA20: - Sodium.crypto_stream_chacha20_xor_ic(sodiumBuf, sodiumBuf, (ulong)(padding + length), iv, ic, _key); + ret = Sodium.crypto_stream_chacha20_xor_ic(sodiumBuf, sodiumBuf, (ulong)(padding + length), iv, ic, _key); break; case CIPHER_CHACHA20_IETF: - Sodium.crypto_stream_chacha20_ietf_xor_ic(sodiumBuf, sodiumBuf, (ulong)(padding + length), iv, (uint)ic, _key); + ret = Sodium.crypto_stream_chacha20_ietf_xor_ic(sodiumBuf, sodiumBuf, (ulong)(padding + length), iv, (uint)ic, _key); break; } + if (ret != 0) throw new CryptoErrorException(); + Buffer.BlockCopy(sodiumBuf, padding, outbuf, 0, length); padding += length; ic += (ulong)padding / SODIUM_BLOCK_SIZE; bytesRemaining = padding % SODIUM_BLOCK_SIZE; - if (isCipher) + if (isEncrypt) { _encryptBytesRemaining = bytesRemaining; _encryptIC = ic; diff --git a/shadowsocks-csharp/packages.config b/shadowsocks-csharp/packages.config index 8701b3e8..2ca0b8da 100644 --- a/shadowsocks-csharp/packages.config +++ b/shadowsocks-csharp/packages.config @@ -2,6 +2,7 @@ + diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index 2b597142..4f354890 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -66,6 +66,9 @@ app.manifest + + 3rd\Cyotek.CircularBuffer.1.0.2\lib\net20\Cyotek.Collections.Generic.CircularBuffer.dll + 3rd\GlobalHotKey.1.1.0\lib\GlobalHotKey.dll True @@ -94,6 +97,19 @@ + + + + + + + + + + + + + @@ -116,14 +132,6 @@ - - - - - - - - @@ -144,7 +152,6 @@ - diff --git a/test/UnitTest.cs b/test/UnitTest.cs index 38f7dae2..350d71e7 100755 --- a/test/UnitTest.cs +++ b/test/UnitTest.cs @@ -7,6 +7,7 @@ using System.Windows.Input; using System.Threading; using System.Collections.Generic; using Shadowsocks.Controller.Hotkeys; +using Shadowsocks.Encryption.Stream; namespace test { @@ -70,7 +71,7 @@ namespace test { RNG.Reload(); byte[] plain = new byte[16384]; - byte[] cipher = new byte[plain.Length + 16 + IVEncryptor.ONETIMEAUTH_BYTES + IVEncryptor.AUTH_BYTES]; + byte[] cipher = new byte[plain.Length + 16]; byte[] plain2 = new byte[plain.Length + 16]; int outLen = 0; int outLen2 = 0; @@ -130,8 +131,8 @@ namespace test { IEncryptor encryptor; IEncryptor decryptor; - encryptor = new MbedTLSEncryptor("aes-256-cfb", "barfoo!", false, false); - decryptor = new MbedTLSEncryptor("aes-256-cfb", "barfoo!", false, false); + encryptor = new StreamMbedTLSEncryptor("aes-256-cfb", "barfoo!"); + decryptor = new StreamMbedTLSEncryptor("aes-256-cfb", "barfoo!"); RunEncryptionRound(encryptor, decryptor); } } @@ -171,8 +172,8 @@ namespace test var random = new Random(); IEncryptor encryptor; IEncryptor decryptor; - encryptor = new MbedTLSEncryptor("rc4-md5", "barfoo!", false, false); - decryptor = new MbedTLSEncryptor("rc4-md5", "barfoo!", false, false); + encryptor = new StreamMbedTLSEncryptor("rc4-md5", "barfoo!"); + decryptor = new StreamMbedTLSEncryptor("rc4-md5", "barfoo!"); RunEncryptionRound(encryptor, decryptor); } } @@ -212,8 +213,8 @@ namespace test var random = new Random(); IEncryptor encryptor; IEncryptor decryptor; - encryptor = new SodiumEncryptor("salsa20", "barfoo!", false, false); - decryptor = new SodiumEncryptor("salsa20", "barfoo!", false, false); + encryptor = new StreamSodiumEncryptor("salsa20", "barfoo!"); + decryptor = new StreamSodiumEncryptor("salsa20", "barfoo!"); RunEncryptionRound(encryptor, decryptor); } }