Uh oh!
There was an error while loading. Please reload this page.
Use hardware-accelerated AES CryptoServiceProvider - #865
Conversation
jjxtra
commented
Aug 30, 2021
Well that's pretty good! |
IgorMilavec
commented
Sep 9, 2021
Here is the code I used: byte[]CTRArrayXOR(byte[]counter,byte[]data,intoffset,intlength){for(intloopOffset=0;length>0;length-=Vector<byte>.Count){varv=newVector<byte>(counter,loopOffset)^newVector<byte>(data,offset+loopOffset);if(length>=Vector<byte>.Count){v.CopyTo(counter,loopOffset);loopOffset+=Vector<byte>.Count;}else{for(inti=0;i<length;i++){counter[loopOffset++]=v[i];}}}returncounter;} |
I've tested your snippet by replacing the XOR on my AES-CTR benchmark, which processes chunks of 32KB of random data (similar to what happens when downloading a file) Note that my code copies the byte[] to uint[] so that the XOR is not done byte by byte. Does your benchmark take that into account? Is the "original" my PR code (accelerated), or the existing SSH-NET code?
I see that vector<T> is likely optimized by the compiler to use CPU registers. Even so, it seems slower than my current code. |
I recompiled the benchmark for NetCore 5.0 and added 3 versions of CTR:
Running it as Release instead of Debug also kicked performance by a lot, and changes the ranking. So here are the results:
So Vector is indeed the fastest, but available only since NetStandard 2.1 (like Span). Blockcopy is not that bad considering it's available for all platforms, and it's still 13x faster than base. We can add a separate PR with Vector/Span later on if needed. Here's the snippet for SpanXOR - it assumes the arrays are 4-byte aligned for simplification: privatebyte[]SpanXOR(byte[]data,intoffset,byte[]output){intuOffset=offset/4;Span<uint>uData=MemoryMarshal.Cast<byte,uint>(data);Span<uint>uOut=MemoryMarshal.Cast<byte,uint>(output);for(inti=0;i<uOut.Length;i++)uOut[i]=uOut[i]^uData[i+uOffset];returnoutput;} |
IgorMilavec
left a comment
There was a problem hiding this comment.
@drieseng This looks fine. I've been using it in production for a couple of months without any side effects. With 10x performance improvement, I propose to merge this before the next release.
A9G-Data-Droid
commented
Oct 11, 2022
I have tested this and it works! |
Reduces CPU usage dramatically, allowing more performance on slower machines
Fix padding for non-AES blockciphers Fix IV exception for non-AES blockciphers
It looks like the legacy code doesn't correctly remove padding, so this code needs to do the same.
restructure AES CSP code into its own class
zybexXL
commented
Nov 1, 2023
I've rebased this PR, please review and consider merging. |
Rob-Hague
commented
Nov 2, 2023
I'm very much in favour of this change, but I think the design needs some work. In fact, I think it can be much simpler as a starting point: I think we can just replace the implementation of That way, we still use the given If you have any thoughts on this approach, or would like to work on it, let me know. Otherwise, I will play with it. |
zybexXL
commented
Nov 5, 2023
All AES tests pass (including the new ones from #1232). The failed test is unrelated, seems spurious: Is there a way to force re-run of the test suite without doing a dummy commit? |
WojciechNagorski
commented
Nov 5, 2023
I've merged #1232 can you update this PR and check if all tests passed? |
zybexXL
commented
Nov 5, 2023
DST messed up the order of comments, my comment above was actually made after Wojciech's. |
@Rob-Hague With the removal of the legacy frameworks perhaps we can get rid of the legacy code and just use this new one. However, I think we should first merge this in, and then rework and remove the legacy code in a separate PR. |
Rob-Hague
commented
Nov 5, 2023
Let's get it right in this PR. My biggest concern is the subversion of logic. When passing a I have run some experiments with the following changes to the benchmarks: Detailsdiff --git a/test/Renci.SshNet.Benchmarks/Security/Cryptography/Ciphers/AesCipherBenchmarks.cs b/test/Renci.SshNet.Benchmarks/Security/Cryptography/Ciphers/AesCipherBenchmarks.cs
index ff414cc4..b91e3b8a 100644
--- a/test/Renci.SshNet.Benchmarks/Security/Cryptography/Ciphers/AesCipherBenchmarks.cs+++ b/test/Renci.SshNet.Benchmarks/Security/Cryptography/Ciphers/AesCipherBenchmarks.cs@@ -15,7 +15,7 @@ namespace Renci.SshNet.Benchmarks.Security.Cryptography.Ciphers
{
_key = new byte[32];
_iv = new byte[16];
- _data = new byte[256];+ _data = new byte[32 * 1024];
Random random = new(Seed: 12345);
random.NextBytes(_key);
@@ -34,5 +34,29 @@ namespace Renci.SshNet.Benchmarks.Security.Cryptography.Ciphers
{
return new AesCipher(_key, new CbcCipherMode(_iv), null).Decrypt(_data);
}
++ [Benchmark]+ public byte[] Encrypt_CFB()+ {+ return new AesCipher(_key, new CfbCipherMode(_iv), null).Encrypt(_data);+ }++ [Benchmark]+ public byte[] Decrypt_CFB()+ {+ return new AesCipher(_key, new CfbCipherMode(_iv), null).Decrypt(_data);+ }++ [Benchmark]+ public byte[] Encrypt_CTR()+ {+ return new AesCipher(_key, new CtrCipherMode(_iv), null).Encrypt(_data);+ }++ [Benchmark]+ public byte[] Decrypt_CTR()+ {+ return new AesCipher(_key, new CtrCipherMode(_iv), null).Decrypt(_data);+ }
}
}
Results on develop branch 826222f:
Results on my suggestion to replace the
Results on your branch zybexXL@a9f68fb
So my suggestion gets moderate gains but clearly we can do a lot better with an approach to delegate the entire encryption to the BCL (rather than block-by-block). So here is what I propose: We keep the behaviour of the existing constructor on We add a new constructor which will allow delegating to the BCL for the entire encryption process. This will achieve the much better performance and separation from the existing behaviour. publicAesCipher(System.Security.Cryptography.CipherMode cipherMode,System.Security.Cryptography.PaddingMode paddingMode,bool ctrMode = false){}Since In terms of the design, we define nested classes in publicsealedclassAesCipher:BlockCipher{privatereadonlyBlockCipher_impl;publicAesCipher(byte[]key,CipherModemode,CipherPaddingpadding):base(key,16,mode,padding){_impl=newSshNetCipherModeImpl(key,mode,padding);}publicAesCipher(byte[]key,byte[]iv,System.Security.Cryptography.CipherModecipherMode,System.Security.Cryptography.PaddingModepaddingMode,boolctrMode=false){_impl=newBclImpl(/* ... */);}// AesCipher just forwards all implementation to _implpublicoverrideintEncryptBlock(byte[]inputBuffer,intinputOffset,intinputCount,byte[]outputBuffer,intoutputOffset){return_impl.EncryptBlock(inputBuffer,inputOffset,inputCount,outputBuffer,outputOffset);}publicoverrideintDecryptBlock(byte[]inputBuffer,intinputOffset,intinputCount,byte[]outputBuffer,intoutputOffset){return_impl.DecryptBlock(inputBuffer,inputOffset,inputCount,outputBuffer,outputOffset);}publicoverridebyte[]Encrypt(byte[]input,intoffset,intlength){return_impl.Encrypt(input,offset,length);}publicoverridebyte[]Decrypt(byte[]input){return_impl.Decrypt(input);}publicoverridebyte[]Decrypt(byte[]input,intoffset,intlength){return_impl.Decrypt(input,offset,length);}// Implementations// The block-by-block implementation using instantiated SshNet.CipherMode, SshNet.CipherPadding objectsprivatesealedclassSshNetCipherModeImpl:BlockCipher{privatereadonlyAes_aes;privateICryptoTransform_encryptor;privateICryptoTransform_decryptor;publicSshNetCipherModeImpl(byte[]key,CipherModemode,CipherPaddingpadding):base(key,16,mode,padding){// Initialise _aes in ECB mode}publicoverrideintEncryptBlock(byte[]inputBuffer,intinputOffset,intinputCount,byte[]outputBuffer,intoutputOffset){_encryptor??=_aes.CreateEncryptor();return_encryptor.TransformBlock(inputBuffer,inputOffset,inputCount,outputBuffer,outputOffset);}publicoverrideintDecryptBlock(byte[]inputBuffer,intinputOffset,intinputCount,byte[]outputBuffer,intoutputOffset){_decryptor??=_aes.CreateDecryptor();return_decryptor.TransformBlock(inputBuffer,inputOffset,inputCount,outputBuffer,outputOffset);}}privatesealedclassBclImpl:BlockCipher{// This overrides Encrypt/Decrypt for full BCL perf.publicoverridebyte[]Decrypt(byte[]input){thrownewNotImplementedException();}publicoverridebyte[]Decrypt(byte[]input,intoffset,intlength){thrownewNotImplementedException();}publicoverrideintDecryptBlock(byte[]inputBuffer,intinputOffset,intinputCount,byte[]outputBuffer,intoutputOffset){thrownewNotImplementedException();}publicoverridebyte[]Encrypt(byte[]input,intoffset,intlength){thrownewNotImplementedException();}publicoverrideintEncryptBlock(byte[]inputBuffer,intinputOffset,intinputCount,byte[]outputBuffer,intoutputOffset){thrownewNotImplementedException();}}}Comments appreciated. Most important to me is that we do not subvert provided |
Rob-Hague
commented
Nov 5, 2023
One other thought: because the proposed new constructor is a bit awkward for CTR mode, I am in principle OK with subverting our own |
This feature reduces CPU usage dramatically, allowing higher SFTP performance on slower machines. It does so by using the AesCryptoServiceProvider for AES CBC/CTR/ECB encryption/decryption, which taps into AES-NI accelerated OS functions.
STFP download performance testcases:
Test case A: 4 year-old i5-7300U 2-core/4-threads laptop, 250Mbps internet connection:
Test case B: 4 year old I7-7700 4-core/8-threads, 500Mbps internet connection:
Note: all values above were taken with PR #866 also merged in. Tested with sftpClient.DownloadFile()
SSH.NET vs AES-NI benchmarks
SSH.NET implements AES as pure managed code. CPUs nowadays have hardware acceleration for AES via the AES-NI instruction set, and .NET provides APIs to the OS functions that make use of it. There are 3 .NET providers for AES:
AesManaged : older managed-only implementation, not accelerated. Still faster than the current SSH.Net
AesCryptoServiceProvider: call into the OS and makes use of AES-NI if available
AesCNG (CryptoNewGen): newer Crypto API, available since 2018 in Win10. Also makes use of AES-NI and has less overhead
Here are some benchmarks (executed on the dual-core laptop...):
As you can see, the accelerated APIs are about 25x faster than the current managed code!
Since AesCNG is too new I chose to use AesCryptoServiceProvider. This supports CBC and ECB natively, but not CFB or OFB (which are not listed in the SSH.Net protocol list anyway). For CTR (used by AWS SFTP Transfer Family), my code uses ECB followed by XOR. This would be extremely fast on C/C++ (or using unsafe tag in C# to allow memory pointers), but on pure C# I had to do a few memory Blockcopy to convert between byte[] and uint[], so the result is not ideal - however, it's still about 10x faster than the previous code.
Here's the new AES benchmarks with this PR:
These CTR and CBC values are now the new maximum theoretical SFTP performance of SSH.NET 😎