diff --git a/shadowsocks-csharp/Controller/LoggerExtension.cs b/shadowsocks-csharp/Controller/LoggerExtension.cs index b3fc6e4c..c2b04057 100644 --- a/shadowsocks-csharp/Controller/LoggerExtension.cs +++ b/shadowsocks-csharp/Controller/LoggerExtension.cs @@ -12,6 +12,10 @@ namespace NLog public static class LoggerExtension { // for key, iv, etc... + public static void Dump(this Logger logger, string tag, ReadOnlySpan arr) + { + logger.Dump(tag, arr.ToArray(), arr.Length); + } public static void Dump(this Logger logger, string tag, byte[] arr, int length = -1) { if (length == -1) length = arr.Length; @@ -26,6 +30,10 @@ namespace NLog logger.Trace(content); } // for cipher and plain text, so we can use openssl to test + public static void DumpBase64(this Logger logger, string tag, ReadOnlySpan arr) + { + logger.DumpBase64(tag, arr.ToArray(), arr.Length); + } public static void DumpBase64(this Logger logger, string tag, byte[] arr, int length = -1) { if (length == -1) length = arr.Length; diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs index 29a18ceb..4698dcd4 100644 --- a/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs +++ b/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs @@ -188,6 +188,70 @@ namespace Shadowsocks.Encryption.AEAD } } + public override int Encrypt(ReadOnlySpan plain, Span cipher) + { + // push data + Span tmp = sharedBuffer.AsSpan(0, plain.Length + bufPtr); + plain.CopyTo(tmp.Slice(bufPtr)); + + int outlength = 0; + if (!saltReady) + { + saltReady = true; + // Generate salt + byte[] saltBytes = RNG.GetBytes(saltLen); + InitCipher(saltBytes, true, false); + saltBytes.CopyTo(cipher); + outlength = saltLen; + } + + if (!tcpRequestSent) + { + tcpRequestSent = true; + + // read addr byte to encrypt + int encAddrBufLength = ChunkEncrypt(tmp.Slice(0, AddressBufferLength), cipher.Slice(outlength)); + tmp = tmp.Slice(AddressBufferLength); + outlength += encAddrBufLength; + } + + // handle other chunks + while (true) + { + // calculate next chunk size + int bufSize = tmp.Length; + if (bufSize <= 0) + { + return outlength; + } + + int chunklength = (int)Math.Min(bufSize, ChunkLengthMask); + // read next chunk + int encChunkLength = ChunkEncrypt(tmp.Slice(0, chunklength), cipher.Slice(outlength)); + tmp = tmp.Slice(chunklength); + outlength += encChunkLength; + + // check if we have enough space for outbuf + // if not, keep buf for next run, at this condition, buffer is not empty + if (outlength + TCPHandler.ChunkOverheadSize > TCPHandler.BufferSize) + { + logger.Debug("enc outbuf almost full, giving up"); + + // write rest data to head of shared buffer + tmp.CopyTo(sharedBuffer); + bufPtr = tmp.Length; + + return outlength; + } + // check if buffer empty + bufSize = tmp.Length; + if (bufSize <= 0) + { + logger.Debug("No more data to encrypt, leaving"); + return outlength; + } + } + } public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) { @@ -239,61 +303,112 @@ namespace Shadowsocks.Encryption.AEAD return; } - #region Chunk Decryption + int len = ChunkDecrypt(outbuf.AsSpan(outlength), tmp); + if (len <= 0) + { + tmp.CopyTo(sharedBuffer); + bufPtr = tmp.Length; + return; + } - // byte[] encLenBytes = buffer.Peek(ChunkLengthBytes + tagLen); - byte[] encLenBytes = tmp.Slice(0, ChunkLengthBytes + tagLen).ToArray(); + tmp = tmp.Slice(ChunkLengthBytes + tagLen + len + tagLen); + // output to outbuf + outlength += len; - // try to dec chunk len - byte[] decChunkLenBytes = new byte[ChunkLengthBytes]; - CipherDecrypt(decChunkLenBytes, encLenBytes); - ushort chunkLen = (ushort)IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(decChunkLenBytes, 0)); - if (chunkLen > ChunkLengthMask) + logger.Debug("aead dec outlength " + outlength); + if (outlength + 100 > TCPHandler.BufferSize) { - // we get invalid chunk - logger.Error($"Invalid chunk length: {chunkLen}"); - throw new CryptoErrorException(); + logger.Debug("dec outbuf almost full, giving up"); + tmp.CopyTo(sharedBuffer); + bufPtr = tmp.Length; + return; } - logger.Debug("Get the real chunk len:" + chunkLen); bufSize = tmp.Length; - if (bufSize < ChunkLengthBytes + tagLen /* we haven't remove them */+ chunkLen + tagLen) + // check if we already done all of them + if (bufSize <= 0) { - logger.Debug("No more data to decrypt one chunk"); - // write back length data + logger.Debug("No data in _decCircularBuffer, already all done"); + return; + } + } + } + + public override int Decrypt(Span plain, ReadOnlySpan cipher) + { + int outlength = 0; + // drop all into buffer + Span tmp = sharedBuffer.AsSpan(0, cipher.Length + bufPtr); + cipher.CopyTo(tmp.Slice(bufPtr)); + int bufSize = tmp.Length; + + logger.Debug("---Start Decryption"); + if (!saltReady) + { + // check if we get the leading salt + if (bufSize <= saltLen) + { + // need more, write back cache tmp.CopyTo(sharedBuffer); bufPtr = tmp.Length; - return; + return outlength; } - IncrementNonce(); - // we have enough data to decrypt one chunk - // drop chunk len and its tag from buffer - // buffer.Skip(ChunkLengthBytes + tagLen); - tmp = tmp.Slice(ChunkLengthBytes + tagLen); - // byte[] encChunkBytes = buffer.Get(chunkLen + tagLen); - byte[] encChunkBytes = tmp.Slice(0, chunkLen + tagLen).ToArray(); - tmp = tmp.Slice(chunkLen + tagLen); + saltReady = true; - int len = CipherDecrypt(outbuf.AsSpan(outlength), encChunkBytes); - IncrementNonce(); + // buffer.Get(saltLen); + byte[] salt = tmp.Slice(0, saltLen).ToArray(); + tmp = tmp.Slice(saltLen); - #endregion + InitCipher(salt, false, false); + logger.Debug("get salt len " + saltLen); + } - // output to outbuf + // handle chunks + while (true) + { + bufSize = tmp.Length; + // check if we have any data + if (bufSize <= 0) + { + logger.Debug("No data in buffer"); + return outlength; + } + + // first get chunk length + if (bufSize <= ChunkLengthBytes + tagLen) + { + // so we only have chunk length and its tag? + // wait more + tmp.CopyTo(sharedBuffer); + bufPtr = tmp.Length; + return outlength; + } + + int len = ChunkDecrypt(plain.Slice(outlength), tmp); + if (len <= 0) + { + // no chunk decrypted + tmp.CopyTo(sharedBuffer); + bufPtr = tmp.Length; + return outlength; + } + // drop decrypted data + tmp = tmp.Slice(ChunkLengthBytes + tagLen + len + tagLen); outlength += len; + logger.Debug("aead dec outlength " + outlength); if (outlength + 100 > TCPHandler.BufferSize) { logger.Debug("dec outbuf almost full, giving up"); tmp.CopyTo(sharedBuffer); bufPtr = tmp.Length; - return; + return outlength; } bufSize = tmp.Length; // check if we already done all of them if (bufSize <= 0) { logger.Debug("No data in _decCircularBuffer, already all done"); - return; + return outlength; } } } @@ -301,14 +416,6 @@ namespace Shadowsocks.Encryption.AEAD #endregion #region UDP - /// - /// Perform AEAD UDP packet encryption - /// - /// payload => [salt][encrypted payload][tag] - /// - /// - /// - /// public override void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) { // Generate salt @@ -317,7 +424,13 @@ namespace Shadowsocks.Encryption.AEAD outlength = saltLen + CipherEncrypt( buf.AsSpan(0, length), outbuf.AsSpan(saltLen, length + tagLen)); + } + public override int EncryptUDP(ReadOnlySpan plain, Span cipher) + { + RNG.GetSpan(cipher.Slice(0, saltLen)); + InitCipher(cipher.Slice(0, saltLen).ToArray(), true, true); + return saltLen + CipherEncrypt(plain, cipher.Slice(saltLen)); } public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) @@ -326,6 +439,12 @@ namespace Shadowsocks.Encryption.AEAD outlength = CipherDecrypt(outbuf, buf.AsSpan(saltLen, length - saltLen)); } + public override int DecryptUDP(Span plain, ReadOnlySpan cipher) + { + InitCipher(cipher.Slice(0, saltLen).ToArray(), false, true); + return CipherDecrypt(plain, cipher.Slice(saltLen)); + } + #endregion private int ChunkEncrypt(ReadOnlySpan plain, Span cipher) @@ -345,24 +464,31 @@ namespace Shadowsocks.Encryption.AEAD return cipherLenSize + cipherDataSize; } - public override int Encrypt(ReadOnlySpan plain, Span cipher) + private int ChunkDecrypt(Span plain, ReadOnlySpan cipher) { - throw new NotImplementedException(); - } - - public override int Decrypt(Span plain, ReadOnlySpan cipher) - { - throw new NotImplementedException(); - } - - public override int EncryptUDP(ReadOnlySpan plain, Span cipher) - { - throw new NotImplementedException(); - } - - public override int DecryptUDP(Span plain, ReadOnlySpan cipher) - { - throw new NotImplementedException(); + // try to dec chunk len + byte[] chunkLengthByte = new byte[ChunkLengthBytes]; + CipherDecrypt(chunkLengthByte, cipher.Slice(0, ChunkLengthBytes + tagLen)); + ushort chunkLength = (ushort)IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(chunkLengthByte, 0)); + if (chunkLength > ChunkLengthMask) + { + // we get invalid chunk + logger.Error($"Invalid chunk length: {chunkLength}"); + throw new CryptoErrorException(); + } + logger.Debug("Get the real chunk len:" + chunkLength); + int bufSize = cipher.Length; + if (bufSize < ChunkLengthBytes + tagLen /* we haven't remove them */+ chunkLength + tagLen) + { + logger.Debug("No data to decrypt one chunk"); + return 0; + } + IncrementNonce(); + // we have enough data to decrypt one chunk + // drop chunk len and its tag from buffer + int len = CipherDecrypt(plain, cipher.Slice(ChunkLengthBytes + tagLen, chunkLength + tagLen)); + IncrementNonce(); + return len; } } } \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs b/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs index 4d1c97e6..9399480c 100644 --- a/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs +++ b/shadowsocks-csharp/Encryption/Stream/StreamEncryptor.cs @@ -117,6 +117,28 @@ namespace Shadowsocks.Encryption.Stream outlength = size + cipherOffset; } + public override int Encrypt(ReadOnlySpan plain, Span cipher) + { + int cipherOffset = 0; + logger.Trace($"{instanceId} encrypt TCP, generate iv: {!ivReady}"); + if (!ivReady) + { + // Generate IV + byte[] ivBytes = RNG.GetBytes(ivLen); + initCipher(ivBytes, true); + ivBytes.CopyTo(cipher); + cipherOffset = ivLen; + cipher = cipher.Slice(cipherOffset); + ivReady = true; + } + int clen = CipherEncrypt(plain, cipher); + + logger.DumpBase64($"plain {instanceId}", plain); + logger.DumpBase64($"cipher {instanceId}", cipher); + logger.Dump($"iv {instanceId}", iv, ivLen); + return clen + cipherOffset; + } + private int recieveCtr = 0; [MethodImpl(MethodImplOptions.Synchronized)] public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) @@ -161,6 +183,48 @@ namespace Shadowsocks.Encryption.Stream outlength = tmp.Length; } + public override int Decrypt(Span plain, ReadOnlySpan cipher) + { + Span tmp;// = cipher; + logger.Trace($"{instanceId} decrypt TCP, read iv: {!ivReady}"); + + int cipherOffset = 0; + // is first packet, need read iv + if (!ivReady) + { + // push to buffer in case of not enough data + cipher.CopyTo(sharedBuffer.AsSpan(recieveCtr)); + recieveCtr += cipher.Length; + + // not enough data for read iv, return 0 byte data + if (recieveCtr <= ivLen) + { + return 0; + } + // start decryption + ivReady = true; + if (ivLen > 0) + { + // read iv + byte[] iv = sharedBuffer.AsSpan(0, ivLen).ToArray(); + initCipher(iv, false); + } + else + { + initCipher(Array.Empty(), false); + } + cipherOffset += ivLen; + tmp = sharedBuffer.AsSpan(ivLen, recieveCtr - ivLen); + } + + // read all data from buffer + int len = CipherDecrypt(plain, cipher.Slice(cipherOffset)); + logger.DumpBase64($"cipher {instanceId}", cipher.Slice(cipherOffset)); + logger.DumpBase64($"plain {instanceId}", plain); + logger.Dump($"iv {instanceId}", iv, ivLen); + return len; + } + #endregion #region UDP @@ -178,6 +242,14 @@ namespace Shadowsocks.Encryption.Stream } } + public override int EncryptUDP(ReadOnlySpan plain, Span cipher) + { + byte[] iv = RNG.GetBytes(ivLen); + iv.CopyTo(cipher); + initCipher(iv, true); + return ivLen + CipherEncrypt(plain, cipher.Slice(ivLen)); + } + [MethodImpl(MethodImplOptions.Synchronized)] public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) { @@ -192,26 +264,12 @@ namespace Shadowsocks.Encryption.Stream } } - #endregion - - public override int Encrypt(ReadOnlySpan plain, Span cipher) - { - throw new NotImplementedException(); - } - - public override int Decrypt(Span plain, ReadOnlySpan cipher) - { - throw new NotImplementedException(); - } - - public override int EncryptUDP(ReadOnlySpan plain, Span cipher) - { - throw new NotImplementedException(); - } - public override int DecryptUDP(Span plain, ReadOnlySpan cipher) { - throw new NotImplementedException(); + initCipher(cipher.Slice(0, ivLen).ToArray(), false); + return CipherDecrypt(plain, cipher.Slice(ivLen)); } + + #endregion } } \ No newline at end of file diff --git a/test/CryptographyTest.cs b/test/CryptographyTest.cs index 6b8dc48c..13459069 100644 --- a/test/CryptographyTest.cs +++ b/test/CryptographyTest.cs @@ -37,8 +37,10 @@ namespace Shadowsocks.Test byte[] plain2 = new byte[plain.Length + 16]; random.NextBytes(plain); - encryptor.Encrypt(plain, length, cipher, out int outLen); - decryptor.Decrypt(cipher, outLen, plain2, out int outLen2); + int outLen = encryptor.Encrypt(plain, cipher); + int outLen2 = decryptor.Decrypt(plain2, cipher.AsSpan(0, outLen)); + //encryptor.Encrypt(plain, length, cipher, out int outLen); + //decryptor.Decrypt(cipher, outLen, plain2, out int outLen2); Assert.AreEqual(length, outLen2); ArrayEqual(plain.AsSpan(0, length).ToArray(), plain2.AsSpan(0, length).ToArray()); } @@ -123,16 +125,16 @@ namespace Shadowsocks.Test Assert.IsFalse(encryptionFailed); } #endregion - + // encryption test cases private void RunEncryptionRound(IEncryptor encryptor, IEncryptor decryptor) { - SingleEncryptionTestCase(encryptor, decryptor, 16384); SingleEncryptionTestCase(encryptor, decryptor, 7); // for not aligned data SingleEncryptionTestCase(encryptor, decryptor, 1000); SingleEncryptionTestCase(encryptor, decryptor, 12333); + SingleEncryptionTestCase(encryptor, decryptor, 16384); } - + [TestMethod] public void TestAEADAesGcmNativeEncryption() {