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) => {