diff --git a/README.md b/README.md
index a8c91ad98..519f5c5d5 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,8 @@ the missing test once you figure things out. 🤓
* aes128-cbc
* aes192-cbc
* aes256-cbc
+* aes128-gcm@openssh.com (.NET 6 and higher)
+* aes256-gcm@openssh.com (.NET 6 and higher)
* blowfish-cbc
* twofish-cbc
* twofish192-cbc
diff --git a/src/Renci.SshNet/CipherInfo.cs b/src/Renci.SshNet/CipherInfo.cs
index 2c9832a19..ed38e3067 100644
--- a/src/Renci.SshNet/CipherInfo.cs
+++ b/src/Renci.SshNet/CipherInfo.cs
@@ -17,6 +17,14 @@ public class CipherInfo
///
public int KeySize { get; private set; }
+ ///
+ /// Gets a value indicating whether the cipher is AEAD (Authenticated Encryption with Associated data).
+ ///
+ ///
+ /// to indicate the cipher is AEAD, to indicate the cipher is not AEAD.
+ ///
+ public bool IsAead { get; private set; }
+
///
/// Gets the cipher.
///
@@ -27,10 +35,12 @@ public class CipherInfo
///
/// Size of the key.
/// The cipher.
- public CipherInfo(int keySize, Func cipher)
+ /// to indicate the cipher is AEAD, to indicate the cipher is not AEAD.
+ public CipherInfo(int keySize, Func cipher, bool isAead = false)
{
KeySize = keySize;
Cipher = (key, iv) => cipher(key.Take(KeySize / 8), iv);
+ IsAead = isAead;
}
}
}
diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs
index 2a0a76a33..f5609dc58 100644
--- a/src/Renci.SshNet/ConnectionInfo.cs
+++ b/src/Renci.SshNet/ConnectionInfo.cs
@@ -55,7 +55,9 @@ public class ConnectionInfo : IConnectionInfoInternal
///
/// Gets supported encryptions for this connection.
///
+#pragma warning disable CA1859 // Use concrete types when possible for improved performance
public IDictionary Encryptions { get; private set; }
+#pragma warning restore CA1859 // Use concrete types when possible for improved performance
///
/// Gets supported hash algorithms for this connection.
@@ -380,25 +382,30 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
{ "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() },
};
- Encryptions = new Dictionary
- {
- { "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)) },
- { "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)) },
- { "twofish128-cbc", new CipherInfo(128, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
- { "twofish256-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
- { "arcfour", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: false)) },
- { "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)) },
- };
+ Encryptions = new Dictionary();
+ Encryptions.Add("aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
+ Encryptions.Add("aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
+ Encryptions.Add("aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
+#if NET6_0_OR_GREATER
+ if (AesGcm.IsSupported)
+ {
+ Encryptions.Add("aes128-gcm@openssh.com", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
+ Encryptions.Add("aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
+ }
+#endif
+ Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
+ Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
+ Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
+ Encryptions.Add("3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)));
+ Encryptions.Add("blowfish-cbc", new CipherInfo(128, (key, iv) => new BlowfishCipher(key, new CbcCipherMode(iv), padding: null)));
+ Encryptions.Add("twofish-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
+ Encryptions.Add("twofish192-cbc", new CipherInfo(192, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
+ Encryptions.Add("twofish128-cbc", new CipherInfo(128, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
+ Encryptions.Add("twofish256-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)));
+ Encryptions.Add("arcfour", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: false)));
+ Encryptions.Add("arcfour128", new CipherInfo(128, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)));
+ Encryptions.Add("arcfour256", new CipherInfo(256, (key, iv) => new Arc4Cipher(key, dischargeFirstBytes: true)));
+ Encryptions.Add("cast128-cbc", new CipherInfo(128, (key, iv) => new CastCipher(key, new CbcCipherMode(iv), padding: null)));
#pragma warning disable IDE0200 // Remove unnecessary lambda expression; We want to prevent instantiating the HashAlgorithm objects.
HmacAlgorithms = new Dictionary
diff --git a/src/Renci.SshNet/Messages/Message.cs b/src/Renci.SshNet/Messages/Message.cs
index fa3ba5f72..b5c43acaf 100644
--- a/src/Renci.SshNet/Messages/Message.cs
+++ b/src/Renci.SshNet/Messages/Message.cs
@@ -37,7 +37,7 @@ protected override void WriteBytes(SshDataStream stream)
base.WriteBytes(stream);
}
- internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool isEncryptThenMAC = false)
+ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool excludePacketLengthFieldWhenPadding = false)
{
const int outboundPacketSequenceSize = 4;
@@ -78,9 +78,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool is
var packetLength = messageLength + 4 + 1;
// determine the padding length
- // in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
+ // in Encrypt-then-MAC mode or AEAD, the length field is not encrypted, so we should keep it out of the
// padding length calculation
- var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
+ var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);
// add padding bytes
var paddingBytes = new byte[paddingLength];
@@ -106,9 +106,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool is
var packetLength = messageLength + 4 + 1;
// determine the padding length
- // in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
+ // in Encrypt-then-MAC mode or AEAD, the length field is not encrypted, so we should keep it out of the
// padding length calculation
- var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
+ var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
diff --git a/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs b/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs
index b9f7dde58..3e7e6541a 100644
--- a/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs
+++ b/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs
@@ -110,18 +110,6 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
return output;
}
- ///
- /// Decrypts the specified data.
- ///
- /// The data.
- ///
- /// The decrypted data.
- ///
- public override byte[] Decrypt(byte[] input)
- {
- return Decrypt(input, 0, input.Length);
- }
-
///
/// Decrypts the specified input.
///
@@ -167,5 +155,31 @@ public override byte[] Decrypt(byte[] input, int offset, int length)
return output;
}
+
+ ///
+ /// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
+ ///
+ /// The input data to encrypt.
+ /// The offset into the input byte array from which to begin using data.
+ /// The number of bytes in the input byte array to use as data.
+ /// The output to which to write encrypted data.
+ /// The offset into the output byte array from which to begin writing data.
+ ///
+ /// The number of bytes encrypted.
+ ///
+ public abstract int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
+
+ ///
+ /// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
+ ///
+ /// The input data to decrypt.
+ /// The offset into the input byte array from which to begin using data.
+ /// The number of bytes in the input byte array to use as data.
+ /// The output to which to write decrypted data.
+ /// The offset into the output byte array from which to begin writing data.
+ ///
+ /// The number of bytes decrypted.
+ ///
+ public abstract int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
}
}
diff --git a/src/Renci.SshNet/Security/Cryptography/Cipher.cs b/src/Renci.SshNet/Security/Cryptography/Cipher.cs
index e624bbba3..d96a214a9 100644
--- a/src/Renci.SshNet/Security/Cryptography/Cipher.cs
+++ b/src/Renci.SshNet/Security/Cryptography/Cipher.cs
@@ -13,6 +13,15 @@ public abstract class Cipher
///
public abstract byte MinimumSize { get; }
+ ///
+ /// Gets the size of the authentication tag for ciphers which implement Authenticated Encryption (AE).
+ ///
+ ///
+ /// When this implements Authenticated Encryption, the size, in bytes,
+ /// of the authentication tag included in the encrypted message.
+ ///
+ public virtual int TagSize { get; }
+
///
/// Encrypts the specified input.
///
@@ -41,7 +50,10 @@ public byte[] Encrypt(byte[] input)
///
/// The decrypted data.
///
- public abstract byte[] Decrypt(byte[] input);
+ public byte[] Decrypt(byte[] input)
+ {
+ return Decrypt(input, 0, input.Length);
+ }
///
/// Decrypts the specified input.
diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipherMode.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipherMode.cs
index 51ebfdd14..9f948b3cf 100644
--- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipherMode.cs
+++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipherMode.cs
@@ -5,22 +5,22 @@
///
public enum AesCipherMode
{
- /// CBC Mode.
+ /// Cipher Block Chain Mode.
CBC = 1,
- /// ECB Mode.
+ /// Electronic Codebook Mode.
ECB = 2,
- /// OFB Mode.
+ /// Output Feedback Mode.
OFB = 3,
- /// CFB Mode.
+ /// Cipher Feedback Mode.
CFB = 4,
- /// CTS Mode.
+ /// Cipher Text Stealing Mode.
CTS = 5,
- /// CTR Mode.
+ /// Counter Mode.
CTR = 6
}
}
diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs
new file mode 100644
index 000000000..cd673d04a
--- /dev/null
+++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs
@@ -0,0 +1,174 @@
+#if NET6_0_OR_GREATER
+using System;
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Security.Cryptography;
+
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Security.Cryptography.Ciphers
+{
+ ///
+ /// AES GCM cipher implementation.
+ /// .
+ ///
+ internal sealed class AesGcmCipher : SymmetricCipher, IDisposable
+ {
+ private readonly byte[] _iv;
+ private readonly AesGcm _aesGcm;
+
+ ///
+ /// Gets the minimun block size.
+ /// The reader is reminded that SSH requires that the data to be
+ /// encrypted MUST be padded out to a multiple of the block size
+ /// (16-octets for AES-GCM).
+ /// .
+ ///
+ public override byte MinimumSize
+ {
+ get
+ {
+ return 16;
+ }
+ }
+
+ ///
+ /// Gets the tag size in bytes.
+ /// Both AEAD_AES_128_GCM and AEAD_AES_256_GCM produce a 16-octet
+ /// Authentication Tag
+ /// .
+ ///
+ public override int TagSize
+ {
+ get
+ {
+ return 16;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The key.
+ /// The IV.
+ public AesGcmCipher(byte[] key, byte[] iv)
+ : base(key)
+ {
+ // SSH AES-GCM requires a 12-octet Initial IV
+ _iv = iv.Take(12);
+#if NET8_0_OR_GREATER
+ _aesGcm = new AesGcm(key, TagSize);
+#else
+ _aesGcm = new AesGcm(key);
+#endif
+ }
+
+ ///
+ /// Encrypts the specified input.
+ ///
+ ///
+ /// The input data with below format:
+ ///
+ /// [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
+ /// [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
+ ///
+ ///
+ /// The zero-based offset in at which to begin encrypting.
+ /// The number of bytes to encrypt from .
+ ///
+ /// The encrypted data with below format:
+ ///
+ /// [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
+ /// [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
+ ///
+ ///
+ public override byte[] Encrypt(byte[] input, int offset, int length)
+ {
+ var packetLengthField = new ReadOnlySpan(input, offset, 4);
+ var plainText = new ReadOnlySpan(input, offset + 4, length - 4);
+
+ var output = new byte[length + TagSize];
+ packetLengthField.CopyTo(output);
+ var cipherText = new Span(output, 4, length - 4);
+ var tag = new Span(output, length, TagSize);
+
+ _aesGcm.Encrypt(nonce: _iv, plainText, cipherText, tag, associatedData: packetLengthField);
+
+ IncrementCounter();
+
+ return output;
+ }
+
+ ///
+ /// Decrypts the specified input.
+ ///
+ ///
+ /// The input data with below format:
+ ///
+ /// [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
+ /// [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
+ ///
+ ///
+ /// The zero-based offset in at which to begin decrypting and authenticating.
+ /// The number of bytes to decrypt and authenticate from .
+ ///
+ /// The decrypted data with below format:
+ ///
+ /// [padding length field sz][payload][random paddings]
+ /// [--------------------Plain Text-------------------]
+ ///
+ ///
+ public override byte[] Decrypt(byte[] input, int offset, int length)
+ {
+ Debug.Assert(offset == 8, "The offset must be 8");
+
+ var packetLengthField = new ReadOnlySpan(input, 4, 4);
+ var cipherText = new ReadOnlySpan(input, offset, length);
+ var tag = new ReadOnlySpan(input, offset + length, TagSize);
+
+ var output = new byte[length];
+ var plainText = new Span(output);
+
+ _aesGcm.Decrypt(nonce: _iv, cipherText, tag, plainText, associatedData: packetLengthField);
+
+ IncrementCounter();
+
+ return output;
+ }
+
+ ///
+ /// With AES-GCM, the 12-octet IV is broken into two fields: a 4-octet
+ /// fixed field and an 8 - octet invocation counter field.The invocation
+ /// field is treated as a 64 - bit integer and is incremented after each
+ /// invocation of AES - GCM to process a binary packet.
+ /// .
+ ///
+ private void IncrementCounter()
+ {
+ var invocationCounter = new Span(_iv, 4, 8);
+ var count = BinaryPrimitives.ReadUInt64BigEndian(invocationCounter);
+ BinaryPrimitives.WriteUInt64BigEndian(invocationCounter, count + 1);
+ }
+
+ ///
+ /// Dispose the instance.
+ ///
+ /// Set to True to dispose of resouces.
+ public void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _aesGcm.Dispose();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
+#endif
diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/Arc4Cipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/Arc4Cipher.cs
index 41387ee02..aed9683b8 100644
--- a/src/Renci.SshNet/Security/Cryptography/Ciphers/Arc4Cipher.cs
+++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/Arc4Cipher.cs
@@ -50,38 +50,6 @@ public Arc4Cipher(byte[] key, bool dischargeFirstBytes)
}
}
- ///
- /// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
- ///
- /// The input data to encrypt.
- /// The offset into the input byte array from which to begin using data.
- /// The number of bytes in the input byte array to use as data.
- /// The output to which to write encrypted data.
- /// The offset into the output byte array from which to begin writing data.
- ///
- /// The number of bytes encrypted.
- ///
- public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
- {
- return ProcessBytes(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
- }
-
- ///
- /// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
- ///
- /// The input data to decrypt.
- /// The offset into the input byte array from which to begin using data.
- /// The number of bytes in the input byte array to use as data.
- /// The output to which to write decrypted data.
- /// The offset into the output byte array from which to begin writing data.
- ///
- /// The number of bytes decrypted.
- ///
- public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
- {
- return ProcessBytes(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
- }
-
///
/// Encrypts the specified input.
///
@@ -98,18 +66,6 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
return output;
}
- ///
- /// Decrypts the specified input.
- ///
- /// The input.
- ///
- /// The decrypted data.
- ///
- public override byte[] Decrypt(byte[] input)
- {
- return Decrypt(input, 0, input.Length);
- }
-
///
/// Decrypts the specified input.
///
diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs
index 8cb58a93e..acfb3fc9a 100644
--- a/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs
+++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs
@@ -49,20 +49,6 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
return Transform(paddedBlock);
}
- ///
- /// Decrypts the specified data.
- ///
- /// The data.
- ///
- /// The decrypted data.
- ///
- /// Only block type 01 or 02 are supported.
- /// Thrown when decrypted block type is not supported.
- public override byte[] Decrypt(byte[] input)
- {
- return Decrypt(input, 0, input.Length);
- }
-
///
/// Decrypts the specified input.
///
diff --git a/src/Renci.SshNet/Security/Cryptography/SymmetricCipher.cs b/src/Renci.SshNet/Security/Cryptography/SymmetricCipher.cs
index 33140bd9c..ee2c239f0 100644
--- a/src/Renci.SshNet/Security/Cryptography/SymmetricCipher.cs
+++ b/src/Renci.SshNet/Security/Cryptography/SymmetricCipher.cs
@@ -26,31 +26,5 @@ protected SymmetricCipher(byte[] key)
Key = key;
}
-
- ///
- /// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
- ///
- /// The input data to encrypt.
- /// The offset into the input byte array from which to begin using data.
- /// The number of bytes in the input byte array to use as data.
- /// The output to which to write encrypted data.
- /// The offset into the output byte array from which to begin writing data.
- ///
- /// The number of bytes encrypted.
- ///
- public abstract int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
-
- ///
- /// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
- ///
- /// The input data to decrypt.
- /// The offset into the input byte array from which to begin using data.
- /// The number of bytes in the input byte array to use as data.
- /// The output to which to write decrypted data.
- /// The offset into the output byte array from which to begin writing data.
- ///
- /// The number of bytes decrypted.
- ///
- public abstract int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
}
}
diff --git a/src/Renci.SshNet/Security/IKeyExchange.cs b/src/Renci.SshNet/Security/IKeyExchange.cs
index c8f04b219..2de3cf074 100644
--- a/src/Renci.SshNet/Security/IKeyExchange.cs
+++ b/src/Renci.SshNet/Security/IKeyExchange.cs
@@ -50,18 +50,20 @@ public interface IKeyExchange : IDisposable
///
/// Creates the client-side cipher to use.
///
+ /// to indicate the cipher is AEAD, to indicate the cipher is not AEAD.
///
/// The client cipher.
///
- Cipher CreateClientCipher();
+ Cipher CreateClientCipher(out bool isAead);
///
/// Creates the server-side cipher to use.
///
+ /// to indicate the cipher is AEAD, to indicate the cipher is not AEAD.
///
/// The server cipher.
///
- Cipher CreateServerCipher();
+ Cipher CreateServerCipher(out bool isAead);
///
/// Creates the server-side hash algorithm to use.
diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs
index 1dd09ea97..4ce988339 100644
--- a/src/Renci.SshNet/Security/KeyExchange.cs
+++ b/src/Renci.SshNet/Security/KeyExchange.cs
@@ -83,6 +83,7 @@ from a in message.EncryptionAlgorithmsClientToServer
}
session.ConnectionInfo.CurrentClientEncryption = clientEncryptionAlgorithmName;
+ _clientCipherInfo = session.ConnectionInfo.Encryptions[clientEncryptionAlgorithmName];
// Determine encryption algorithm
var serverDecryptionAlgorithmName = (from b in session.ConnectionInfo.Encryptions.Keys
@@ -95,30 +96,39 @@ from a in message.EncryptionAlgorithmsServerToClient
}
session.ConnectionInfo.CurrentServerEncryption = serverDecryptionAlgorithmName;
+ _serverCipherInfo = session.ConnectionInfo.Encryptions[serverDecryptionAlgorithmName];
- // Determine client hmac algorithm
- var clientHmacAlgorithmName = (from b in session.ConnectionInfo.HmacAlgorithms.Keys
- from a in message.MacAlgorithmsClientToServer
- where a == b
- select a).FirstOrDefault();
- if (string.IsNullOrEmpty(clientHmacAlgorithmName))
+ if (!_clientCipherInfo.IsAead)
{
- throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
- }
+ // Determine client hmac algorithm
+ var clientHmacAlgorithmName = (from b in session.ConnectionInfo.HmacAlgorithms.Keys
+ from a in message.MacAlgorithmsClientToServer
+ where a == b
+ select a).FirstOrDefault();
+ if (string.IsNullOrEmpty(clientHmacAlgorithmName))
+ {
+ throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
+ }
- session.ConnectionInfo.CurrentClientHmacAlgorithm = clientHmacAlgorithmName;
+ session.ConnectionInfo.CurrentClientHmacAlgorithm = clientHmacAlgorithmName;
+ _clientHashInfo = session.ConnectionInfo.HmacAlgorithms[clientHmacAlgorithmName];
+ }
- // Determine server hmac algorithm
- var serverHmacAlgorithmName = (from b in session.ConnectionInfo.HmacAlgorithms.Keys
- from a in message.MacAlgorithmsServerToClient
- where a == b
- select a).FirstOrDefault();
- if (string.IsNullOrEmpty(serverHmacAlgorithmName))
+ if (!_serverCipherInfo.IsAead)
{
- throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
- }
+ // Determine server hmac algorithm
+ var serverHmacAlgorithmName = (from b in session.ConnectionInfo.HmacAlgorithms.Keys
+ from a in message.MacAlgorithmsServerToClient
+ where a == b
+ select a).FirstOrDefault();
+ if (string.IsNullOrEmpty(serverHmacAlgorithmName))
+ {
+ throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
+ }
- session.ConnectionInfo.CurrentServerHmacAlgorithm = serverHmacAlgorithmName;
+ session.ConnectionInfo.CurrentServerHmacAlgorithm = serverHmacAlgorithmName;
+ _serverHashInfo = session.ConnectionInfo.HmacAlgorithms[serverHmacAlgorithmName];
+ }
// Determine compression algorithm
var compressionAlgorithmName = (from b in session.ConnectionInfo.CompressionAlgorithms.Keys
@@ -131,6 +141,7 @@ from a in message.CompressionAlgorithmsClientToServer
}
session.ConnectionInfo.CurrentClientCompressionAlgorithm = compressionAlgorithmName;
+ _compressorFactory = session.ConnectionInfo.CompressionAlgorithms[compressionAlgorithmName];
// Determine decompression algorithm
var decompressionAlgorithmName = (from b in session.ConnectionInfo.CompressionAlgorithms.Keys
@@ -143,12 +154,6 @@ from a in message.CompressionAlgorithmsServerToClient
}
session.ConnectionInfo.CurrentServerCompressionAlgorithm = decompressionAlgorithmName;
-
- _clientCipherInfo = session.ConnectionInfo.Encryptions[clientEncryptionAlgorithmName];
- _serverCipherInfo = session.ConnectionInfo.Encryptions[serverDecryptionAlgorithmName];
- _clientHashInfo = session.ConnectionInfo.HmacAlgorithms[clientHmacAlgorithmName];
- _serverHashInfo = session.ConnectionInfo.HmacAlgorithms[serverHmacAlgorithmName];
- _compressorFactory = session.ConnectionInfo.CompressionAlgorithms[compressionAlgorithmName];
_decompressorFactory = session.ConnectionInfo.CompressionAlgorithms[decompressionAlgorithmName];
}
@@ -168,9 +173,12 @@ public virtual void Finish()
///
/// Creates the server side cipher to use.
///
+ /// to indicate the cipher is AEAD, to indicate the cipher is not AEAD.
/// Server cipher.
- public Cipher CreateServerCipher()
+ public Cipher CreateServerCipher(out bool isAead)
{
+ isAead = _serverCipherInfo.IsAead;
+
// Resolve Session ID
var sessionId = Session.SessionId ?? ExchangeHash;
@@ -193,9 +201,12 @@ public Cipher CreateServerCipher()
///
/// Creates the client side cipher to use.
///
+ /// to indicate the cipher is AEAD, to indicate the cipher is not AEAD.
/// Client cipher.
- public Cipher CreateClientCipher()
+ public Cipher CreateClientCipher(out bool isAead)
{
+ isAead = _clientCipherInfo.IsAead;
+
// Resolve Session ID
var sessionId = Session.SessionId ?? ExchangeHash;
@@ -224,6 +235,12 @@ public Cipher CreateClientCipher()
///
public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC)
{
+ if (_serverHashInfo == null)
+ {
+ isEncryptThenMAC = false;
+ return null;
+ }
+
isEncryptThenMAC = _serverHashInfo.IsEncryptThenMAC;
// Resolve Session ID
@@ -250,6 +267,12 @@ public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC)
///
public HashAlgorithm CreateClientHash(out bool isEncryptThenMAC)
{
+ if (_clientHashInfo == null)
+ {
+ isEncryptThenMAC = false;
+ return null;
+ }
+
isEncryptThenMAC = _clientHashInfo.IsEncryptThenMAC;
// Resolve Session ID
diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs
index dd20f34c9..f2fe7f2ae 100644
--- a/src/Renci.SshNet/Session.cs
+++ b/src/Renci.SshNet/Session.cs
@@ -164,9 +164,13 @@ public class Session : ISession
private bool _clientEtm;
+ private Cipher _serverCipher;
+
private Cipher _clientCipher;
- private Cipher _serverCipher;
+ private bool _serverAead;
+
+ private bool _clientAead;
private Compressor _serverDecompression;
@@ -1041,8 +1045,8 @@ internal void SendMessage(Message message)
DiagnosticAbstraction.Log(string.Format("[{0}] Sending message '{1}' to server: '{2}'.", ToHex(SessionId), message.GetType().Name, message));
- var paddingMultiplier = _clientCipher is null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
- var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientMac != null && _clientEtm);
+ var paddingMultiplier = _clientCipher is null ? (byte) 8 : Math.Max((byte) 8, _clientCipher.MinimumSize);
+ var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientEtm || _clientAead);
// take a write lock to ensure the outbound packet sequence number is incremented
// atomically, and only after the packet has actually been sent
@@ -1051,11 +1055,11 @@ internal void SendMessage(Message message)
byte[] hash = null;
var packetDataOffset = 4; // first four bytes are reserved for outbound packet sequence
+ // write outbound packet sequence to start of packet data
+ Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
+
if (_clientMac != null && !_clientEtm)
{
- // write outbound packet sequence to start of packet data
- Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
-
// calculate packet hash
hash = _clientMac.ComputeHash(packetData);
}
@@ -1063,7 +1067,7 @@ internal void SendMessage(Message message)
// Encrypt packet data
if (_clientCipher != null)
{
- if (_clientMac != null && _clientEtm)
+ if (_clientEtm)
{
// The length of the "packet length" field in bytes
const int packetLengthFieldLength = 4;
@@ -1072,9 +1076,6 @@ internal void SendMessage(Message message)
Array.Resize(ref packetData, packetDataOffset + packetLengthFieldLength + encryptedData.Length);
- // write outbound packet sequence to start of packet data
- Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
-
// write encrypted data
Buffer.BlockCopy(encryptedData, 0, packetData, packetDataOffset + packetLengthFieldLength, encryptedData.Length);
@@ -1205,9 +1206,8 @@ private Message ReceiveMessage(Socket socket)
int blockSize;
- // Determine the size of the first block which is 8 or cipher block size (whichever is larger) bytes
- // The "packet length" field is not encrypted in ETM.
- if (_serverMac != null && _serverEtm)
+ // Determine the size of the first block which is 8 or cipher block size (whichever is larger) bytes, or 4 if "packet length" field is handled separately.
+ if (_serverEtm || _serverAead)
{
blockSize = (byte) 4;
}
@@ -1220,7 +1220,16 @@ private Message ReceiveMessage(Socket socket)
blockSize = (byte) 8;
}
- var serverMacLength = _serverMac != null ? _serverMac.HashSize/8 : 0;
+ var serverMacLength = 0;
+
+ if (_serverAead)
+ {
+ serverMacLength = _serverCipher.TagSize;
+ }
+ else if (_serverMac != null)
+ {
+ serverMacLength = _serverMac.HashSize / 8;
+ }
byte[] data;
uint packetLength;
@@ -1238,7 +1247,7 @@ private Message ReceiveMessage(Socket socket)
return null;
}
- if (_serverCipher != null && (_serverMac == null || !_serverEtm))
+ if (_serverCipher != null && !_serverAead && (_serverMac == null || !_serverEtm))
{
firstBlock = _serverCipher.Decrypt(firstBlock);
}
@@ -1507,10 +1516,12 @@ internal void OnNewKeysReceived(NewKeysMessage message)
}
// Update negotiated algorithms
- _serverCipher = _keyExchange.CreateServerCipher();
- _clientCipher = _keyExchange.CreateClientCipher();
+ _serverCipher = _keyExchange.CreateServerCipher(out _serverAead);
+ _clientCipher = _keyExchange.CreateClientCipher(out _clientAead);
+
_serverMac = _keyExchange.CreateServerHash(out _serverEtm);
_clientMac = _keyExchange.CreateClientHash(out _clientEtm);
+
_clientCompression = _keyExchange.CreateCompressor();
_serverDecompression = _keyExchange.CreateDecompressor();
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index 4f458b676..932ed8620 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -121,7 +121,7 @@ public override bool IsConnected
{
get
{
- return base.IsConnected && _sftpSession.IsOpen;
+ return base.IsConnected && _sftpSession?.IsOpen == true;
}
}
diff --git a/test/Renci.SshNet.IntegrationTests/CipherTests.cs b/test/Renci.SshNet.IntegrationTests/CipherTests.cs
index 1a11f9814..b2b3fd4c1 100644
--- a/test/Renci.SshNet.IntegrationTests/CipherTests.cs
+++ b/test/Renci.SshNet.IntegrationTests/CipherTests.cs
@@ -64,6 +64,19 @@ public void Aes256Ctr()
DoTest(Cipher.Aes256Ctr);
}
+#if NET6_0_OR_GREATER
+ [TestMethod]
+ public void Aes128Gcm()
+ {
+ DoTest(Cipher.Aes128Gcm);
+ }
+
+ [TestMethod]
+ public void Aes256Gcm()
+ {
+ DoTest(Cipher.Aes256Gcm);
+ }
+#endif
private void DoTest(Cipher cipher)
{
_remoteSshdConfig.ClearCiphers()
diff --git a/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs b/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs
index 5ce0d4675..42c1f54a6 100644
--- a/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs
+++ b/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs
@@ -211,10 +211,18 @@ private void SetupMocks()
_ = _keyExchangeMock.Setup(p => p.Start(Session, It.IsAny(), false));
_ = _keyExchangeMock.Setup(p => p.ExchangeHash)
.Returns(SessionId);
- _ = _keyExchangeMock.Setup(p => p.CreateServerCipher())
- .Returns((Cipher) null);
- _ = _keyExchangeMock.Setup(p => p.CreateClientCipher())
- .Returns((Cipher) null);
+ _ = _keyExchangeMock.Setup(p => p.CreateServerCipher(out It.Ref.IsAny))
+ .Returns((ref bool serverAead) =>
+ {
+ serverAead = false;
+ return (Cipher) null;
+ });
+ _ = _keyExchangeMock.Setup(p => p.CreateClientCipher(out It.Ref.IsAny))
+ .Returns((ref bool clientAead) =>
+ {
+ clientAead = false;
+ return (Cipher) null;
+ });
_ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref.IsAny))
.Returns((ref bool serverEtm) =>
{
diff --git a/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerAndClientDisconnectRace.cs b/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerAndClientDisconnectRace.cs
index c75fa32a3..b0e714811 100644
--- a/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerAndClientDisconnectRace.cs
+++ b/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerAndClientDisconnectRace.cs
@@ -160,10 +160,18 @@ private void SetupMocks()
_ = _keyExchangeMock.Setup(p => p.Start(Session, It.IsAny(), false));
_ = _keyExchangeMock.Setup(p => p.ExchangeHash)
.Returns(SessionId);
- _ = _keyExchangeMock.Setup(p => p.CreateServerCipher())
- .Returns((Cipher) null);
- _ = _keyExchangeMock.Setup(p => p.CreateClientCipher())
- .Returns((Cipher) null);
+ _ = _keyExchangeMock.Setup(p => p.CreateServerCipher(out It.Ref.IsAny))
+ .Returns((ref bool serverAead) =>
+ {
+ serverAead = false;
+ return (Cipher) null;
+ });
+ _ = _keyExchangeMock.Setup(p => p.CreateClientCipher(out It.Ref.IsAny))
+ .Returns((ref bool clientAead) =>
+ {
+ clientAead = false;
+ return (Cipher) null;
+ });
_ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref.IsAny))
.Returns((ref bool serverEtm) =>
{