Skip to content

Commit

Permalink
Use hardware-accelerated AES CryptoServiceProvider (#865)
Browse files Browse the repository at this point in the history
* Add FEATURE_AES_CSP to use hardware-accelerated AesCryptoServiceProvider
Reduces CPU usage dramatically, allowing more performance on slower machines

* Restructure, move most of the feature code to AesCipher.cs
Fix padding for non-AES blockciphers
Fix IV exception for non-AES blockciphers

* Fix the AES Padding
It looks like the legacy code doesn't correctly remove padding, so this code needs to do the same.

* fix rebase issues
restructure AES CSP code into its own class

* Minor fixes

* Rework based on suggestions

* Move all changes to AesCypher.cs, as per Rob-Hague suggestion
Remove FEATURE_AES_CSP conditional
Fix OFB CipherMode

* update AesCipherTest.cs generator

* Fix continuous session encrypt/decrypt
(preserve IV between calls when Padding is None)

* Reduce CTR memory usage in Net 6+
Small performance increase in CTR buffer mode
Cosmetic changes

* Factor out the implementations and re-add the existing constructor

* remove ctor; revert tests; remove unused _iv member

* Reorder Encryption cipher preference list

* Remove redundant AES tests
Add tests for stream cipher state preservation

* Refactor ArrayXOR()

* Add test for IV overflow

* Performance bump for AES CTR (thanks @robhague)

* fix merge conflict

* Move AesCipherMode enum to its own file

---------

Co-authored-by: Pedro Fonseca <[email protected]>
Co-authored-by: Rob Hague <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2023
1 parent 18cf71b commit 4ce18d3
Show file tree
Hide file tree
Showing 12 changed files with 740 additions and 180 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ dotnet_diagnostic.SA1520.severity = none
# We do not use file headers.
dotnet_diagnostic.SA1633.severity = none

# SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none

# SA1648: <inheritdoc> must be used with inheriting class
#
# This rule is disabled by default, hence we need to explicitly enable it.
Expand Down Expand Up @@ -555,6 +558,18 @@ dotnet_code_quality.CA1859.api_surface = all
# This is similar to, but less powerful than, MA0015.
dotnet_diagnostic.CA2208.severity = none

# CA5358: Do Not Use Unsafe Cipher Modes / Review cipher mode usage with cryptography experts
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca5358
#
# We use ECB mode as the basis for other modes (e.g. CTR)
dotnet_diagnostic.CA5358.severity = none

# CA5401: Do not use CreateEncryptor with non-default IV
# https://learn.microsoft.com/en-gb/dotnet/fundamentals/code-analysis/quality-rules/ca5401
#
# We need to specify the IV.
dotnet_diagnostic.CA5401.severity = none

#### Roslyn IDE analyser rules ####

# IDE0028: Simplify collection initialization; and
Expand Down
12 changes: 6 additions & 6 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,13 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy

Encryptions = new Dictionary<string, CipherInfo>
{
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, new CtrCipherMode(iv), padding: null)) },
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
{ "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "blowfish-cbc", new CipherInfo(128, (key, iv) => new BlowfishCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "twofish-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "twofish192-cbc", new CipherInfo(192, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
Expand All @@ -374,8 +376,6 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
{ "arcfour128", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)) },
{ "arcfour256", new CipherInfo(256, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)) },
{ "cast128-cbc", new CipherInfo(128, (key, iv) => new CastCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, new CtrCipherMode(iv), padding: null)) },
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, new CtrCipherMode(iv), padding: null)) },
};

HmacAlgorithms = new Dictionary<string, HashInfo>
Expand Down
10 changes: 5 additions & 5 deletions src/Renci.SshNet/PrivateKeyFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,13 @@ private void Open(Stream privateKey, string passPhrase)
cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
break;
case "AES-128-CBC":
cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
break;
case "AES-192-CBC":
cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
break;
case "AES-256-CBC":
cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
break;
default:
throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
Expand Down Expand Up @@ -522,10 +522,10 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
switch (cipherName)
{
case "aes256-cbc":
cipher = new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding());
cipher = new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false);
break;
case "aes256-ctr":
cipher = new AesCipher(key, new CtrCipherMode(iv), new PKCS7Padding());
cipher = new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false);
break;
default:
throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
Expand Down
108 changes: 108 additions & 0 deletions src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.BclImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Security.Cryptography;

using Renci.SshNet.Common;

namespace Renci.SshNet.Security.Cryptography.Ciphers
{
public partial class AesCipher
{
private sealed class BclImpl : BlockCipher, IDisposable
{
private readonly Aes _aes;
private readonly ICryptoTransform _encryptor;
private readonly ICryptoTransform _decryptor;

public BclImpl(
byte[] key,
byte[] iv,
System.Security.Cryptography.CipherMode cipherMode,
PaddingMode paddingMode)
: base(key, 16, mode: null, padding: null)
{
var aes = Aes.Create();
aes.Key = key;

if (cipherMode != System.Security.Cryptography.CipherMode.ECB)
{
if (iv is null)
{
throw new ArgumentNullException(nameof(iv));
}

aes.IV = iv.Take(16);
}

aes.Mode = cipherMode;
aes.Padding = paddingMode;
aes.FeedbackSize = 128; // We use CFB128
_aes = aes;
_encryptor = aes.CreateEncryptor();
_decryptor = aes.CreateDecryptor();
}

public override byte[] Encrypt(byte[] input, int offset, int length)
{
if (_aes.Padding != PaddingMode.None)
{
// If padding has been specified, call TransformFinalBlock to apply
// the padding and reset the state.
return _encryptor.TransformFinalBlock(input, offset, length);
}

// Otherwise, (the most important case) assume this instance is
// used for one direction of an SSH connection, whereby the
// encrypted data in all packets are considered a single data
// stream i.e. we do not want to reset the state between calls to Encrypt.
var output = new byte[length];
_ = _encryptor.TransformBlock(input, offset, length, output, 0);
return output;
}

public override byte[] Decrypt(byte[] input, int offset, int length)
{
if (_aes.Padding != PaddingMode.None)
{
// If padding has been specified, call TransformFinalBlock to apply
// the padding and reset the state.
return _decryptor.TransformFinalBlock(input, offset, length);
}

// Otherwise, (the most important case) assume this instance is
// used for one direction of an SSH connection, whereby the
// encrypted data in all packets are considered a single data
// stream i.e. we do not want to reset the state between calls to Decrypt.
var output = new byte[length];
_ = _decryptor.TransformBlock(input, offset, length, output, 0);
return output;
}

public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
throw new NotImplementedException($"Invalid usage of {nameof(EncryptBlock)}.");
}

public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
throw new NotImplementedException($"Invalid usage of {nameof(DecryptBlock)}.");
}

private void Dispose(bool disposing)
{
if (disposing)
{
_aes.Dispose();
_encryptor.Dispose();
_decryptor.Dispose();
}
}

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Security.Cryptography;

namespace Renci.SshNet.Security.Cryptography.Ciphers
{
public partial class AesCipher
{
private sealed class BlockImpl : BlockCipher, IDisposable
{
private readonly Aes _aes;
private readonly ICryptoTransform _encryptor;
private readonly ICryptoTransform _decryptor;

public BlockImpl(byte[] key, CipherMode mode, CipherPadding padding)
: base(key, 16, mode, padding)
{
var aes = Aes.Create();
aes.Key = key;
aes.Mode = System.Security.Cryptography.CipherMode.ECB;
aes.Padding = PaddingMode.None;
_aes = aes;
_encryptor = aes.CreateEncryptor();
_decryptor = aes.CreateDecryptor();
}

public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
return _encryptor.TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
}

public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
return _decryptor.TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
}

private void Dispose(bool disposing)
{
if (disposing)
{
_aes.Dispose();
_encryptor.Dispose();
_decryptor.Dispose();
}
}

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
}
Loading

0 comments on commit 4ce18d3

Please sign in to comment.