Skip to content

Commit 4ce18d3

Browse files
zybexXLPedro FonsecaRob-Hague
authored
Use hardware-accelerated AES CryptoServiceProvider (#865)
* 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]>
1 parent 18cf71b commit 4ce18d3

File tree

12 files changed

+740
-180
lines changed

12 files changed

+740
-180
lines changed

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ dotnet_diagnostic.SA1520.severity = none
291291
# We do not use file headers.
292292
dotnet_diagnostic.SA1633.severity = none
293293

294+
# SA1601: Partial elements should be documented
295+
dotnet_diagnostic.SA1601.severity = none
296+
294297
# SA1648: <inheritdoc> must be used with inheriting class
295298
#
296299
# This rule is disabled by default, hence we need to explicitly enable it.
@@ -555,6 +558,18 @@ dotnet_code_quality.CA1859.api_surface = all
555558
# This is similar to, but less powerful than, MA0015.
556559
dotnet_diagnostic.CA2208.severity = none
557560

561+
# CA5358: Do Not Use Unsafe Cipher Modes / Review cipher mode usage with cryptography experts
562+
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca5358
563+
#
564+
# We use ECB mode as the basis for other modes (e.g. CTR)
565+
dotnet_diagnostic.CA5358.severity = none
566+
567+
# CA5401: Do not use CreateEncryptor with non-default IV
568+
# https://learn.microsoft.com/en-gb/dotnet/fundamentals/code-analysis/quality-rules/ca5401
569+
#
570+
# We need to specify the IV.
571+
dotnet_diagnostic.CA5401.severity = none
572+
558573
#### Roslyn IDE analyser rules ####
559574

560575
# IDE0028: Simplify collection initialization; and

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,13 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
360360

361361
Encryptions = new Dictionary<string, CipherInfo>
362362
{
363-
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, new CtrCipherMode(iv), padding: null)) },
363+
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
364+
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
365+
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
366+
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
367+
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
368+
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
364369
{ "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)) },
365-
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), padding: null)) },
366-
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), padding: null)) },
367-
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), padding: null)) },
368370
{ "blowfish-cbc", new CipherInfo(128, (key, iv) => new BlowfishCipher(key, new CbcCipherMode(iv), padding: null)) },
369371
{ "twofish-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
370372
{ "twofish192-cbc", new CipherInfo(192, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
@@ -374,8 +376,6 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
374376
{ "arcfour128", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)) },
375377
{ "arcfour256", new CipherInfo(256, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)) },
376378
{ "cast128-cbc", new CipherInfo(128, (key, iv) => new CastCipher(key, new CbcCipherMode(iv), padding: null)) },
377-
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, new CtrCipherMode(iv), padding: null)) },
378-
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, new CtrCipherMode(iv), padding: null)) },
379379
};
380380

381381
HmacAlgorithms = new Dictionary<string, HashInfo>

src/Renci.SshNet/PrivateKeyFile.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ private void Open(Stream privateKey, string passPhrase)
226226
cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
227227
break;
228228
case "AES-128-CBC":
229-
cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
229+
cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
230230
break;
231231
case "AES-192-CBC":
232-
cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
232+
cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
233233
break;
234234
case "AES-256-CBC":
235-
cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
235+
cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
236236
break;
237237
default:
238238
throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
@@ -522,10 +522,10 @@ private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
522522
switch (cipherName)
523523
{
524524
case "aes256-cbc":
525-
cipher = new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding());
525+
cipher = new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false);
526526
break;
527527
case "aes256-ctr":
528-
cipher = new AesCipher(key, new CtrCipherMode(iv), new PKCS7Padding());
528+
cipher = new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false);
529529
break;
530530
default:
531531
throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
4+
using Renci.SshNet.Common;
5+
6+
namespace Renci.SshNet.Security.Cryptography.Ciphers
7+
{
8+
public partial class AesCipher
9+
{
10+
private sealed class BclImpl : BlockCipher, IDisposable
11+
{
12+
private readonly Aes _aes;
13+
private readonly ICryptoTransform _encryptor;
14+
private readonly ICryptoTransform _decryptor;
15+
16+
public BclImpl(
17+
byte[] key,
18+
byte[] iv,
19+
System.Security.Cryptography.CipherMode cipherMode,
20+
PaddingMode paddingMode)
21+
: base(key, 16, mode: null, padding: null)
22+
{
23+
var aes = Aes.Create();
24+
aes.Key = key;
25+
26+
if (cipherMode != System.Security.Cryptography.CipherMode.ECB)
27+
{
28+
if (iv is null)
29+
{
30+
throw new ArgumentNullException(nameof(iv));
31+
}
32+
33+
aes.IV = iv.Take(16);
34+
}
35+
36+
aes.Mode = cipherMode;
37+
aes.Padding = paddingMode;
38+
aes.FeedbackSize = 128; // We use CFB128
39+
_aes = aes;
40+
_encryptor = aes.CreateEncryptor();
41+
_decryptor = aes.CreateDecryptor();
42+
}
43+
44+
public override byte[] Encrypt(byte[] input, int offset, int length)
45+
{
46+
if (_aes.Padding != PaddingMode.None)
47+
{
48+
// If padding has been specified, call TransformFinalBlock to apply
49+
// the padding and reset the state.
50+
return _encryptor.TransformFinalBlock(input, offset, length);
51+
}
52+
53+
// Otherwise, (the most important case) assume this instance is
54+
// used for one direction of an SSH connection, whereby the
55+
// encrypted data in all packets are considered a single data
56+
// stream i.e. we do not want to reset the state between calls to Encrypt.
57+
var output = new byte[length];
58+
_ = _encryptor.TransformBlock(input, offset, length, output, 0);
59+
return output;
60+
}
61+
62+
public override byte[] Decrypt(byte[] input, int offset, int length)
63+
{
64+
if (_aes.Padding != PaddingMode.None)
65+
{
66+
// If padding has been specified, call TransformFinalBlock to apply
67+
// the padding and reset the state.
68+
return _decryptor.TransformFinalBlock(input, offset, length);
69+
}
70+
71+
// Otherwise, (the most important case) assume this instance is
72+
// used for one direction of an SSH connection, whereby the
73+
// encrypted data in all packets are considered a single data
74+
// stream i.e. we do not want to reset the state between calls to Decrypt.
75+
var output = new byte[length];
76+
_ = _decryptor.TransformBlock(input, offset, length, output, 0);
77+
return output;
78+
}
79+
80+
public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
81+
{
82+
throw new NotImplementedException($"Invalid usage of {nameof(EncryptBlock)}.");
83+
}
84+
85+
public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
86+
{
87+
throw new NotImplementedException($"Invalid usage of {nameof(DecryptBlock)}.");
88+
}
89+
90+
private void Dispose(bool disposing)
91+
{
92+
if (disposing)
93+
{
94+
_aes.Dispose();
95+
_encryptor.Dispose();
96+
_decryptor.Dispose();
97+
}
98+
}
99+
100+
public void Dispose()
101+
{
102+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
103+
Dispose(disposing: true);
104+
GC.SuppressFinalize(this);
105+
}
106+
}
107+
}
108+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
4+
namespace Renci.SshNet.Security.Cryptography.Ciphers
5+
{
6+
public partial class AesCipher
7+
{
8+
private sealed class BlockImpl : BlockCipher, IDisposable
9+
{
10+
private readonly Aes _aes;
11+
private readonly ICryptoTransform _encryptor;
12+
private readonly ICryptoTransform _decryptor;
13+
14+
public BlockImpl(byte[] key, CipherMode mode, CipherPadding padding)
15+
: base(key, 16, mode, padding)
16+
{
17+
var aes = Aes.Create();
18+
aes.Key = key;
19+
aes.Mode = System.Security.Cryptography.CipherMode.ECB;
20+
aes.Padding = PaddingMode.None;
21+
_aes = aes;
22+
_encryptor = aes.CreateEncryptor();
23+
_decryptor = aes.CreateDecryptor();
24+
}
25+
26+
public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
27+
{
28+
return _encryptor.TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
29+
}
30+
31+
public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
32+
{
33+
return _decryptor.TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
34+
}
35+
36+
private void Dispose(bool disposing)
37+
{
38+
if (disposing)
39+
{
40+
_aes.Dispose();
41+
_encryptor.Dispose();
42+
_decryptor.Dispose();
43+
}
44+
}
45+
46+
public void Dispose()
47+
{
48+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
49+
Dispose(disposing: true);
50+
GC.SuppressFinalize(this);
51+
}
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)