Skip to content

Commit 3dc3fc8

Browse files
scott-xuRob-Hague
andauthored
Add support for AEAD AES 128/256 GCM Ciphers (.NET 6.0 onward only) (#1369)
* Init AeadCipher * Move AeadCipher to parent folder. Move EncryptBlock/DecryptBlock from SymmetricCipher to BlockCipher * simplify parameter name * Implement AesGcmCipher * Update README * Remove protected IV from AeadCipher; Set offset to outbound sequence just like other ciphers * Rename associatedData to packetLengthField * Use Span<byte> to avoid unnecessary allocations * Use `Span` to improve performance when `IncrementCounter()` * Add `IsAead` property to `CipherInfo`. Include packet length field and tag field in offset and length when call AesGcm's `Decrypt(...)` method. Do not determine HMAC if cipher is AesGcm during kex. * Fix build * Fix UT * Check `AesGcm.IsSupported` before add to the `Encryptions` collection. Guard AES-GCM with `NET6_0_OR_GREATER`. Insert AES-GCM ciphers right after AES-CTR ciphers but before AES-CBC ciphers, which is similar with OpenSSH: ``` debug2: ciphers ctos: [email protected],aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected] debug2: ciphers stoc: [email protected],aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected] ``` Although Dictionary's order is not defined, from observation, it is in the same order with add. Anyway that would be another topic. * Suppress CA1859 "Use concrete types when possible for improved performance" for `ConnectionInfo.Encryptions`. Test `Aes128Gcm` and `Aes256Gcm` only when `NET6_0_OR_GREATER` * Update xml doc comments. Do not treat AesGcmCipher separately in Session.cs * Fix build * Fix build * Update the comment as ChaCha20Poly1305 uses a separated key to encypt the packet length and the size it 4. * Update src/Renci.SshNet/Security/Cryptography/Cipher.cs Co-authored-by: Rob Hague <[email protected]> * Make `AesGcmCipher` internal. Assert offset when decrypt. * Fix nullable error in build * typos --------- Co-authored-by: Rob Hague <[email protected]>
1 parent 953e136 commit 3dc3fc8

File tree

17 files changed

+381
-181
lines changed

17 files changed

+381
-181
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ the missing test once you figure things out. 🤓
5050
* aes128-cbc
5151
* aes192-cbc
5252
* aes256-cbc
53+
* aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
54+
* aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
5355
* blowfish-cbc
5456
* twofish-cbc
5557
* twofish192-cbc

src/Renci.SshNet/CipherInfo.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ public class CipherInfo
1717
/// </value>
1818
public int KeySize { get; private set; }
1919

20+
/// <summary>
21+
/// Gets a value indicating whether the cipher is AEAD (Authenticated Encryption with Associated data).
22+
/// </summary>
23+
/// <value>
24+
/// <see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.
25+
/// </value>
26+
public bool IsAead { get; private set; }
27+
2028
/// <summary>
2129
/// Gets the cipher.
2230
/// </summary>
@@ -27,10 +35,12 @@ public class CipherInfo
2735
/// </summary>
2836
/// <param name="keySize">Size of the key.</param>
2937
/// <param name="cipher">The cipher.</param>
30-
public CipherInfo(int keySize, Func<byte[], byte[], Cipher> cipher)
38+
/// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.</param>
39+
public CipherInfo(int keySize, Func<byte[], byte[], Cipher> cipher, bool isAead = false)
3140
{
3241
KeySize = keySize;
3342
Cipher = (key, iv) => cipher(key.Take(KeySize / 8), iv);
43+
IsAead = isAead;
3444
}
3545
}
3646
}

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ public class ConnectionInfo : IConnectionInfoInternal
5555
/// <summary>
5656
/// Gets supported encryptions for this connection.
5757
/// </summary>
58+
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
5859
public IDictionary<string, CipherInfo> Encryptions { get; private set; }
60+
#pragma warning restore CA1859 // Use concrete types when possible for improved performance
5961

6062
/// <summary>
6163
/// Gets supported hash algorithms for this connection.
@@ -380,25 +382,30 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
380382
{ "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() },
381383
};
382384

383-
Encryptions = new Dictionary<string, CipherInfo>
384-
{
385-
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
386-
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
387-
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
388-
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
389-
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
390-
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
391-
{ "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)) },
392-
{ "blowfish-cbc", new CipherInfo(128, (key, iv) => new BlowfishCipher(key, new CbcCipherMode(iv), padding: null)) },
393-
{ "twofish-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
394-
{ "twofish192-cbc", new CipherInfo(192, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
395-
{ "twofish128-cbc", new CipherInfo(128, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
396-
{ "twofish256-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
397-
{ "arcfour", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: false)) },
398-
{ "arcfour128", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)) },
399-
{ "arcfour256", new CipherInfo(256, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)) },
400-
{ "cast128-cbc", new CipherInfo(128, (key, iv) => new CastCipher(key, new CbcCipherMode(iv), padding: null)) },
401-
};
385+
Encryptions = new Dictionary<string, CipherInfo>();
386+
Encryptions.Add("aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
387+
Encryptions.Add("aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
388+
Encryptions.Add("aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
389+
#if NET6_0_OR_GREATER
390+
if (AesGcm.IsSupported)
391+
{
392+
Encryptions.Add("[email protected]", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
393+
Encryptions.Add("[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
394+
}
395+
#endif
396+
Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
397+
Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
398+
Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
399+
Encryptions.Add("3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)));
400+
Encryptions.Add("blowfish-cbc", new CipherInfo(128, (key, iv) => new BlowfishCipher(key, new CbcCipherMode(iv), padding: null)));
401+
Encryptions.Add("twofish-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
402+
Encryptions.Add("twofish192-cbc", new CipherInfo(192, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
403+
Encryptions.Add("twofish128-cbc", new CipherInfo(128, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
404+
Encryptions.Add("twofish256-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
405+
Encryptions.Add("arcfour", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: false)));
406+
Encryptions.Add("arcfour128", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)));
407+
Encryptions.Add("arcfour256", new CipherInfo(256, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)));
408+
Encryptions.Add("cast128-cbc", new CipherInfo(128, (key, iv) => new CastCipher(key, new CbcCipherMode(iv), padding: null)));
402409

403410
#pragma warning disable IDE0200 // Remove unnecessary lambda expression; We want to prevent instantiating the HashAlgorithm objects.
404411
HmacAlgorithms = new Dictionary<string, HashInfo>

src/Renci.SshNet/Messages/Message.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected override void WriteBytes(SshDataStream stream)
3737
base.WriteBytes(stream);
3838
}
3939

40-
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool isEncryptThenMAC = false)
40+
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool excludePacketLengthFieldWhenPadding = false)
4141
{
4242
const int outboundPacketSequenceSize = 4;
4343

@@ -78,9 +78,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool is
7878
var packetLength = messageLength + 4 + 1;
7979

8080
// determine the padding length
81-
// in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
81+
// in Encrypt-then-MAC mode or AEAD, the length field is not encrypted, so we should keep it out of the
8282
// padding length calculation
83-
var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
83+
var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);
8484

8585
// add padding bytes
8686
var paddingBytes = new byte[paddingLength];
@@ -106,9 +106,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool is
106106
var packetLength = messageLength + 4 + 1;
107107

108108
// determine the padding length
109-
// in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
109+
// in Encrypt-then-MAC mode or AEAD, the length field is not encrypted, so we should keep it out of the
110110
// padding length calculation
111-
var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
111+
var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);
112112

113113
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
114114

src/Renci.SshNet/Security/Cryptography/BlockCipher.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,6 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
110110
return output;
111111
}
112112

113-
/// <summary>
114-
/// Decrypts the specified data.
115-
/// </summary>
116-
/// <param name="input">The data.</param>
117-
/// <returns>
118-
/// The decrypted data.
119-
/// </returns>
120-
public override byte[] Decrypt(byte[] input)
121-
{
122-
return Decrypt(input, 0, input.Length);
123-
}
124-
125113
/// <summary>
126114
/// Decrypts the specified input.
127115
/// </summary>
@@ -167,5 +155,31 @@ public override byte[] Decrypt(byte[] input, int offset, int length)
167155

168156
return output;
169157
}
158+
159+
/// <summary>
160+
/// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
161+
/// </summary>
162+
/// <param name="inputBuffer">The input data to encrypt.</param>
163+
/// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
164+
/// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
165+
/// <param name="outputBuffer">The output to which to write encrypted data.</param>
166+
/// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
167+
/// <returns>
168+
/// The number of bytes encrypted.
169+
/// </returns>
170+
public abstract int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
171+
172+
/// <summary>
173+
/// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
174+
/// </summary>
175+
/// <param name="inputBuffer">The input data to decrypt.</param>
176+
/// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
177+
/// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
178+
/// <param name="outputBuffer">The output to which to write decrypted data.</param>
179+
/// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
180+
/// <returns>
181+
/// The number of bytes decrypted.
182+
/// </returns>
183+
public abstract int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
170184
}
171185
}

src/Renci.SshNet/Security/Cryptography/Cipher.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ public abstract class Cipher
1313
/// </value>
1414
public abstract byte MinimumSize { get; }
1515

16+
/// <summary>
17+
/// Gets the size of the authentication tag for ciphers which implement Authenticated Encryption (AE).
18+
/// </summary>
19+
/// <returns>
20+
/// When this <see cref="Cipher"/> implements Authenticated Encryption, the size, in bytes,
21+
/// of the authentication tag included in the encrypted message.
22+
/// </returns>
23+
public virtual int TagSize { get; }
24+
1625
/// <summary>
1726
/// Encrypts the specified input.
1827
/// </summary>
@@ -41,7 +50,10 @@ public byte[] Encrypt(byte[] input)
4150
/// <returns>
4251
/// The decrypted data.
4352
/// </returns>
44-
public abstract byte[] Decrypt(byte[] input);
53+
public byte[] Decrypt(byte[] input)
54+
{
55+
return Decrypt(input, 0, input.Length);
56+
}
4557

4658
/// <summary>
4759
/// Decrypts the specified input.

src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipherMode.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,22 @@
55
/// </summary>
66
public enum AesCipherMode
77
{
8-
/// <summary>CBC Mode.</summary>
8+
/// <summary>Cipher Block Chain Mode.</summary>
99
CBC = 1,
1010

11-
/// <summary>ECB Mode.</summary>
11+
/// <summary>Electronic Codebook Mode.</summary>
1212
ECB = 2,
1313

14-
/// <summary>OFB Mode.</summary>
14+
/// <summary>Output Feedback Mode.</summary>
1515
OFB = 3,
1616

17-
/// <summary>CFB Mode.</summary>
17+
/// <summary>Cipher Feedback Mode.</summary>
1818
CFB = 4,
1919

20-
/// <summary>CTS Mode.</summary>
20+
/// <summary>Cipher Text Stealing Mode.</summary>
2121
CTS = 5,
2222

23-
/// <summary>CTR Mode.</summary>
23+
/// <summary>Counter Mode.</summary>
2424
CTR = 6
2525
}
2626
}

0 commit comments

Comments
 (0)