瀏覽代碼

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: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
```
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 <rob.hague00@gmail.com>

* Make `AesGcmCipher` internal.
Assert offset when decrypt.

* Fix nullable error in build

* typos

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Scott Xu 1 年之前
父節點
當前提交
3dc3fc8b1c

+ 2 - 0
README.md

@@ -50,6 +50,8 @@ the missing test once you figure things out.  🤓
 * aes128-cbc
 * aes192-cbc
 * aes256-cbc
+* aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
+* aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
 * blowfish-cbc
 * twofish-cbc
 * twofish192-cbc

+ 11 - 1
src/Renci.SshNet/CipherInfo.cs

@@ -17,6 +17,14 @@ namespace Renci.SshNet
         /// </value>
         public int KeySize { get; private set; }
 
+        /// <summary>
+        /// Gets a value indicating whether the cipher is AEAD (Authenticated Encryption with Associated data).
+        /// </summary>
+        /// <value>
+        /// <see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.
+        /// </value>
+        public bool IsAead { get; private set; }
+
         /// <summary>
         /// Gets the cipher.
         /// </summary>
@@ -27,10 +35,12 @@ namespace Renci.SshNet
         /// </summary>
         /// <param name="keySize">Size of the key.</param>
         /// <param name="cipher">The cipher.</param>
-        public CipherInfo(int keySize, Func<byte[], byte[], Cipher> cipher)
+        /// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.</param>
+        public CipherInfo(int keySize, Func<byte[], byte[], Cipher> cipher, bool isAead = false)
         {
             KeySize = keySize;
             Cipher = (key, iv) => cipher(key.Take(KeySize / 8), iv);
+            IsAead = isAead;
         }
     }
 }

+ 26 - 19
src/Renci.SshNet/ConnectionInfo.cs

@@ -55,7 +55,9 @@ namespace Renci.SshNet
         /// <summary>
         /// Gets supported encryptions for this connection.
         /// </summary>
+#pragma warning disable CA1859 // Use concrete types when possible for improved performance
         public IDictionary<string, CipherInfo> Encryptions { get; private set; }
+#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 
         /// <summary>
         /// Gets supported hash algorithms for this connection.
@@ -380,25 +382,30 @@ namespace Renci.SshNet
                     { "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() },
                 };
 
-            Encryptions = new Dictionary<string, CipherInfo>
-                {
-                    { "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<string, CipherInfo>();
+            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<string, HashInfo>

+ 5 - 5
src/Renci.SshNet/Messages/Message.cs

@@ -37,7 +37,7 @@ namespace Renci.SshNet.Messages
             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 @@ namespace Renci.SshNet.Messages
                     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 @@ namespace Renci.SshNet.Messages
                 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);
 

+ 26 - 12
src/Renci.SshNet/Security/Cryptography/BlockCipher.cs

@@ -110,18 +110,6 @@ namespace Renci.SshNet.Security.Cryptography
             return output;
         }
 
-        /// <summary>
-        /// Decrypts the specified data.
-        /// </summary>
-        /// <param name="input">The data.</param>
-        /// <returns>
-        /// The decrypted data.
-        /// </returns>
-        public override byte[] Decrypt(byte[] input)
-        {
-            return Decrypt(input, 0, input.Length);
-        }
-
         /// <summary>
         /// Decrypts the specified input.
         /// </summary>
@@ -167,5 +155,31 @@ namespace Renci.SshNet.Security.Cryptography
 
             return output;
         }
+
+        /// <summary>
+        /// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
+        /// </summary>
+        /// <param name="inputBuffer">The input data to encrypt.</param>
+        /// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
+        /// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
+        /// <param name="outputBuffer">The output to which to write encrypted data.</param>
+        /// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
+        /// <returns>
+        /// The number of bytes encrypted.
+        /// </returns>
+        public abstract int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
+
+        /// <summary>
+        /// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
+        /// </summary>
+        /// <param name="inputBuffer">The input data to decrypt.</param>
+        /// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
+        /// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
+        /// <param name="outputBuffer">The output to which to write decrypted data.</param>
+        /// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
+        /// <returns>
+        /// The number of bytes decrypted.
+        /// </returns>
+        public abstract int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
     }
 }

+ 13 - 1
src/Renci.SshNet/Security/Cryptography/Cipher.cs

@@ -13,6 +13,15 @@
         /// </value>
         public abstract byte MinimumSize { get; }
 
+        /// <summary>
+        /// Gets the size of the authentication tag for ciphers which implement Authenticated Encryption (AE).
+        /// </summary>
+        /// <returns>
+        /// When this <see cref="Cipher"/> implements Authenticated Encryption, the size, in bytes,
+        /// of the authentication tag included in the encrypted message.
+        /// </returns>
+        public virtual int TagSize { get; }
+
         /// <summary>
         /// Encrypts the specified input.
         /// </summary>
@@ -41,7 +50,10 @@
         /// <returns>
         /// The decrypted data.
         /// </returns>
-        public abstract byte[] Decrypt(byte[] input);
+        public byte[] Decrypt(byte[] input)
+        {
+            return Decrypt(input, 0, input.Length);
+        }
 
         /// <summary>
         /// Decrypts the specified input.

+ 6 - 6
src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipherMode.cs

@@ -5,22 +5,22 @@
     /// </summary>
     public enum AesCipherMode
     {
-        /// <summary>CBC Mode.</summary>
+        /// <summary>Cipher Block Chain Mode.</summary>
         CBC = 1,
 
-        /// <summary>ECB Mode.</summary>
+        /// <summary>Electronic Codebook Mode.</summary>
         ECB = 2,
 
-        /// <summary>OFB Mode.</summary>
+        /// <summary>Output Feedback Mode.</summary>
         OFB = 3,
 
-        /// <summary>CFB Mode.</summary>
+        /// <summary>Cipher Feedback Mode.</summary>
         CFB = 4,
 
-        /// <summary>CTS Mode.</summary>
+        /// <summary>Cipher Text Stealing Mode.</summary>
         CTS = 5,
 
-        /// <summary>CTR Mode.</summary>
+        /// <summary>Counter Mode.</summary>
         CTR = 6
     }
 }

+ 174 - 0
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
+{
+    /// <summary>
+    /// AES GCM cipher implementation.
+    /// <see href="https://datatracker.ietf.org/doc/html/rfc5647"/>.
+    /// </summary>
+    internal sealed class AesGcmCipher : SymmetricCipher, IDisposable
+    {
+        private readonly byte[] _iv;
+        private readonly AesGcm _aesGcm;
+
+        /// <summary>
+        /// 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).
+        /// <see href="https://datatracker.ietf.org/doc/html/rfc5647#section-7.1"/>.
+        /// </summary>
+        public override byte MinimumSize
+        {
+            get
+            {
+                return 16;
+            }
+        }
+
+        /// <summary>
+        /// Gets the tag size in bytes.
+        /// Both AEAD_AES_128_GCM and AEAD_AES_256_GCM produce a 16-octet
+        /// Authentication Tag
+        /// <see href="https://datatracker.ietf.org/doc/html/rfc5647#section-6.3"/>.
+        /// </summary>
+        public override int TagSize
+        {
+            get
+            {
+                return 16;
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AesGcmCipher"/> class.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <param name="iv">The IV.</param>
+        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
+        }
+
+        /// <summary>
+        /// Encrypts the specified input.
+        /// </summary>
+        /// <param name="input">
+        /// The input data with below format:
+        ///   <code>
+        ///   [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
+        ///   [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
+        ///   </code>
+        /// </param>
+        /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
+        /// <param name="length">The number of bytes to encrypt from <paramref name="input"/>.</param>
+        /// <returns>
+        /// The encrypted data with below format:
+        ///   <code>
+        ///   [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
+        ///   [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
+        ///   </code>
+        /// </returns>
+        public override byte[] Encrypt(byte[] input, int offset, int length)
+        {
+            var packetLengthField = new ReadOnlySpan<byte>(input, offset, 4);
+            var plainText = new ReadOnlySpan<byte>(input, offset + 4, length - 4);
+
+            var output = new byte[length + TagSize];
+            packetLengthField.CopyTo(output);
+            var cipherText = new Span<byte>(output, 4, length - 4);
+            var tag = new Span<byte>(output, length, TagSize);
+
+            _aesGcm.Encrypt(nonce: _iv, plainText, cipherText, tag, associatedData: packetLengthField);
+
+            IncrementCounter();
+
+            return output;
+        }
+
+        /// <summary>
+        /// Decrypts the specified input.
+        /// </summary>
+        /// <param name="input">
+        /// The input data with below format:
+        ///   <code>
+        ///   [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
+        ///   [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
+        ///   </code>
+        /// </param>
+        /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
+        /// <param name="length">The number of bytes to decrypt and authenticate from <paramref name="input"/>.</param>
+        /// <returns>
+        /// The decrypted data with below format:
+        /// <code>
+        ///   [padding length field sz][payload][random paddings]
+        ///   [--------------------Plain Text-------------------]
+        /// </code>
+        /// </returns>
+        public override byte[] Decrypt(byte[] input, int offset, int length)
+        {
+            Debug.Assert(offset == 8, "The offset must be 8");
+
+            var packetLengthField = new ReadOnlySpan<byte>(input, 4, 4);
+            var cipherText = new ReadOnlySpan<byte>(input, offset, length);
+            var tag = new ReadOnlySpan<byte>(input, offset + length, TagSize);
+
+            var output = new byte[length];
+            var plainText = new Span<byte>(output);
+
+            _aesGcm.Decrypt(nonce: _iv, cipherText, tag, plainText, associatedData: packetLengthField);
+
+            IncrementCounter();
+
+            return output;
+        }
+
+        /// <summary>
+        /// 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.
+        /// <see href="https://datatracker.ietf.org/doc/html/rfc5647#section-7.1"/>.
+        /// </summary>
+        private void IncrementCounter()
+        {
+            var invocationCounter = new Span<byte>(_iv, 4, 8);
+            var count = BinaryPrimitives.ReadUInt64BigEndian(invocationCounter);
+            BinaryPrimitives.WriteUInt64BigEndian(invocationCounter, count + 1);
+        }
+
+        /// <summary>
+        /// Dispose the instance.
+        /// </summary>
+        /// <param name="disposing">Set to True to dispose of resouces.</param>
+        public void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _aesGcm.Dispose();
+            }
+        }
+
+        /// <inheritdoc/>
+        public void Dispose()
+        {
+            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}
+#endif

+ 0 - 44
src/Renci.SshNet/Security/Cryptography/Ciphers/Arc4Cipher.cs

@@ -50,38 +50,6 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
             }
         }
 
-        /// <summary>
-        /// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
-        /// </summary>
-        /// <param name="inputBuffer">The input data to encrypt.</param>
-        /// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
-        /// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
-        /// <param name="outputBuffer">The output to which to write encrypted data.</param>
-        /// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
-        /// <returns>
-        /// The number of bytes encrypted.
-        /// </returns>
-        public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
-        {
-            return ProcessBytes(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
-        }
-
-        /// <summary>
-        /// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
-        /// </summary>
-        /// <param name="inputBuffer">The input data to decrypt.</param>
-        /// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
-        /// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
-        /// <param name="outputBuffer">The output to which to write decrypted data.</param>
-        /// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
-        /// <returns>
-        /// The number of bytes decrypted.
-        /// </returns>
-        public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
-        {
-            return ProcessBytes(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
-        }
-
         /// <summary>
         /// Encrypts the specified input.
         /// </summary>
@@ -98,18 +66,6 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
             return output;
         }
 
-        /// <summary>
-        /// Decrypts the specified input.
-        /// </summary>
-        /// <param name="input">The input.</param>
-        /// <returns>
-        /// The decrypted data.
-        /// </returns>
-        public override byte[] Decrypt(byte[] input)
-        {
-            return Decrypt(input, 0, input.Length);
-        }
-
         /// <summary>
         /// Decrypts the specified input.
         /// </summary>

+ 0 - 14
src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs

@@ -49,20 +49,6 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
             return Transform(paddedBlock);
         }
 
-        /// <summary>
-        /// Decrypts the specified data.
-        /// </summary>
-        /// <param name="input">The data.</param>
-        /// <returns>
-        /// The decrypted data.
-        /// </returns>
-        /// <exception cref="NotSupportedException">Only block type 01 or 02 are supported.</exception>
-        /// <exception cref="NotSupportedException">Thrown when decrypted block type is not supported.</exception>
-        public override byte[] Decrypt(byte[] input)
-        {
-            return Decrypt(input, 0, input.Length);
-        }
-
         /// <summary>
         /// Decrypts the specified input.
         /// </summary>

+ 0 - 26
src/Renci.SshNet/Security/Cryptography/SymmetricCipher.cs

@@ -26,31 +26,5 @@ namespace Renci.SshNet.Security.Cryptography
 
             Key = key;
         }
-
-        /// <summary>
-        /// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
-        /// </summary>
-        /// <param name="inputBuffer">The input data to encrypt.</param>
-        /// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
-        /// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
-        /// <param name="outputBuffer">The output to which to write encrypted data.</param>
-        /// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
-        /// <returns>
-        /// The number of bytes encrypted.
-        /// </returns>
-        public abstract int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
-
-        /// <summary>
-        /// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
-        /// </summary>
-        /// <param name="inputBuffer">The input data to decrypt.</param>
-        /// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
-        /// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
-        /// <param name="outputBuffer">The output to which to write decrypted data.</param>
-        /// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
-        /// <returns>
-        /// The number of bytes decrypted.
-        /// </returns>
-        public abstract int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
     }
 }

+ 4 - 2
src/Renci.SshNet/Security/IKeyExchange.cs

@@ -50,18 +50,20 @@ namespace Renci.SshNet.Security
         /// <summary>
         /// Creates the client-side cipher to use.
         /// </summary>
+        /// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.</param>
         /// <returns>
         /// The client cipher.
         /// </returns>
-        Cipher CreateClientCipher();
+        Cipher CreateClientCipher(out bool isAead);
 
         /// <summary>
         /// Creates the server-side cipher to use.
         /// </summary>
+        /// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.</param>
         /// <returns>
         /// The server cipher.
         /// </returns>
-        Cipher CreateServerCipher();
+        Cipher CreateServerCipher(out bool isAead);
 
         /// <summary>
         /// Creates the server-side hash algorithm to use.

+ 49 - 26
src/Renci.SshNet/Security/KeyExchange.cs

@@ -83,6 +83,7 @@ namespace Renci.SshNet.Security
             }
 
             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 @@ namespace Renci.SshNet.Security
             }
 
             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 @@ namespace Renci.SshNet.Security
             }
 
             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 @@ namespace Renci.SshNet.Security
             }
 
             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 @@ namespace Renci.SshNet.Security
         /// <summary>
         /// Creates the server side cipher to use.
         /// </summary>
+        /// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.</param>
         /// <returns>Server cipher.</returns>
-        public Cipher CreateServerCipher()
+        public Cipher CreateServerCipher(out bool isAead)
         {
+            isAead = _serverCipherInfo.IsAead;
+
             // Resolve Session ID
             var sessionId = Session.SessionId ?? ExchangeHash;
 
@@ -193,9 +201,12 @@ namespace Renci.SshNet.Security
         /// <summary>
         /// Creates the client side cipher to use.
         /// </summary>
+        /// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to indicate the cipher is not AEAD.</param>
         /// <returns>Client cipher.</returns>
-        public Cipher CreateClientCipher()
+        public Cipher CreateClientCipher(out bool isAead)
         {
+            isAead = _clientCipherInfo.IsAead;
+
             // Resolve Session ID
             var sessionId = Session.SessionId ?? ExchangeHash;
 
@@ -224,6 +235,12 @@ namespace Renci.SshNet.Security
         /// </returns>
         public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC)
         {
+            if (_serverHashInfo == null)
+            {
+                isEncryptThenMAC = false;
+                return null;
+            }
+
             isEncryptThenMAC = _serverHashInfo.IsEncryptThenMAC;
 
             // Resolve Session ID
@@ -250,6 +267,12 @@ namespace Renci.SshNet.Security
         /// </returns>
         public HashAlgorithm CreateClientHash(out bool isEncryptThenMAC)
         {
+            if (_clientHashInfo == null)
+            {
+                isEncryptThenMAC = false;
+                return null;
+            }
+
             isEncryptThenMAC = _clientHashInfo.IsEncryptThenMAC;
 
             // Resolve Session ID

+ 28 - 17
src/Renci.SshNet/Session.cs

@@ -164,9 +164,13 @@ namespace Renci.SshNet
 
         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 @@ namespace Renci.SshNet
 
             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 @@ namespace Renci.SshNet
                 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 @@ namespace Renci.SshNet
                 // 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 @@ namespace Renci.SshNet
 
                         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 @@ namespace Renci.SshNet
 
             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 @@ namespace Renci.SshNet
                 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 @@ namespace Renci.SshNet
                     return null;
                 }
 
-                if (_serverCipher != null && (_serverMac == null || !_serverEtm))
+                if (_serverCipher != null && !_serverAead && (_serverMac == null || !_serverEtm))
                 {
                     firstBlock = _serverCipher.Decrypt(firstBlock);
                 }
@@ -1507,10 +1516,12 @@ namespace Renci.SshNet
             }
 
             // 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();
 

+ 13 - 0
test/Renci.SshNet.IntegrationTests/CipherTests.cs

@@ -64,6 +64,19 @@ namespace Renci.SshNet.IntegrationTests
             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()

+ 12 - 4
test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs

@@ -211,10 +211,18 @@ namespace Renci.SshNet.Tests.Classes
             _ = _keyExchangeMock.Setup(p => p.Start(Session, It.IsAny<KeyExchangeInitMessage>(), 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<bool>.IsAny))
+                                .Returns((ref bool serverAead) =>
+                                {
+                                    serverAead = false;
+                                    return (Cipher) null;
+                                });
+            _ = _keyExchangeMock.Setup(p => p.CreateClientCipher(out It.Ref<bool>.IsAny))
+                                .Returns((ref bool clientAead) =>
+                                {
+                                    clientAead = false;
+                                    return (Cipher) null;
+                                });
             _ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref<bool>.IsAny))
                                 .Returns((ref bool serverEtm) =>
                                 {

+ 12 - 4
test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerAndClientDisconnectRace.cs

@@ -160,10 +160,18 @@ namespace Renci.SshNet.Tests.Classes
             _ = _keyExchangeMock.Setup(p => p.Start(Session, It.IsAny<KeyExchangeInitMessage>(), 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<bool>.IsAny))
+                                .Returns((ref bool serverAead) =>
+                                {
+                                    serverAead = false;
+                                    return (Cipher) null;
+                                });
+            _ = _keyExchangeMock.Setup(p => p.CreateClientCipher(out It.Ref<bool>.IsAny))
+                                .Returns((ref bool clientAead) =>
+                                {
+                                    clientAead = false;
+                                    return (Cipher) null;
+                                });
             _ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref<bool>.IsAny))
                                 .Returns((ref bool serverEtm) =>
                                 {