diff --git a/README.md b/README.md index 84e665bd9..e8b6c68f3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ the missing test once you figure things out. 🤓 * aes128-cbc * aes192-cbc * aes256-cbc +* aes128-gcm@openssh.com (.NET Standard 2.1, .NET 6 and higher) +* aes256-gcm@openssh.com (.NET Standard 2.1, .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..f8d94c5f4 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 incidicate 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 incidicate 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 7584816ff..358f8b699 100644 --- a/src/Renci.SshNet/ConnectionInfo.cs +++ b/src/Renci.SshNet/ConnectionInfo.cs @@ -388,6 +388,10 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy { "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)) }, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + { "aes128-gcm@openssh.com", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true) }, + { "aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true) }, +#endif { "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)) }, 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..7fb5ee111 100644 --- a/src/Renci.SshNet/Security/Cryptography/Cipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Cipher.cs @@ -13,6 +13,14 @@ public abstract class Cipher /// public abstract byte MinimumSize { get; } + /// + /// Gets the tag (MAC) size. + /// + /// + /// The tag (MAC) size. + /// + public virtual int TagSize { get; } + /// /// Encrypts the specified input. /// @@ -41,7 +49,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..ea64efcba --- /dev/null +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -0,0 +1,137 @@ +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System; +using System.Buffers.Binary; +using System.Security.Cryptography; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.Security.Cryptography.Ciphers +{ + /// + /// AES GCM cipher implementation. + /// . + /// + public sealed class AesGcmCipher : SymmetricCipher, IDisposable + { + private readonly byte[] _nonce; + private readonly AesGcm _aesGcm; + + /// + public override byte MinimumSize + { + get + { + return 16; + } + } + + /// + 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) + { + _nonce = 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. + /// The zero-based offset in at which to begin encrypting. + /// The number of bytes to encrypt from . + /// + /// The packet length field + cipher text + tag. + /// + public override byte[] Encrypt(byte[] input, int offset, int length) + { + // [outbound sequence field][packet length field][padding length field sz][payload][random paddings] + // [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(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, plainText, cipherText, tag, packetLengthField); + + IncrementCounter(); + + return output; + } + + /// + /// Decrypts the specified input. + /// + /// The input. + /// The zero-based offset in at which to begin decrypting and authenticating. + /// The number of bytes to decrypt and authenticate from . + /// + /// The packet length field + plain text. + /// + public override byte[] Decrypt(byte[] input, int offset, int length) + { + // [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG] + // [----4 bytes---(offset)][------4 bytes------][------------------Cipher Text--------------------][---TAG---(length)] + var packetLengthField = new ReadOnlySpan(input, offset, 4); + var cipherText = new ReadOnlySpan(input, offset + 4, length - 4 - TagSize); + var tag = new ReadOnlySpan(input, offset + length - TagSize, TagSize); + + var output = new byte[length - TagSize]; + packetLengthField.CopyTo(output); + var plainText = new Span(output, 4, length - 4 - TagSize); + + _aesGcm.Decrypt(_nonce, cipherText, tag, plainText, packetLengthField); + + IncrementCounter(); + + return output; + } + + private void IncrementCounter() + { + var invocationCounter = new Span(_nonce, 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..7f2e349d5 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 incidicate 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 incidicate 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 10f7e0f8a..e444059e7 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 incidicate 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 incidicate 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 0c067ec2d..e033ec646 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 not encrypted + 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); } @@ -1296,11 +1305,23 @@ private Message ReceiveMessage(Socket socket) if (_serverCipher != null) { - var numberOfBytesToDecrypt = data.Length - (blockSize + inboundPacketSequenceLength + serverMacLength); - if (numberOfBytesToDecrypt > 0) + if (_serverAead) { - var decryptedData = _serverCipher.Decrypt(data, blockSize + inboundPacketSequenceLength, numberOfBytesToDecrypt); - Buffer.BlockCopy(decryptedData, 0, data, blockSize + inboundPacketSequenceLength, decryptedData.Length); + var numberOfBytesToDecryptAndAuthenticate = data.Length - inboundPacketSequenceLength; + if (numberOfBytesToDecryptAndAuthenticate > 0) + { + var decryptedData = _serverCipher.Decrypt(data, inboundPacketSequenceLength, numberOfBytesToDecryptAndAuthenticate); + Buffer.BlockCopy(decryptedData, 0, data, inboundPacketSequenceLength, decryptedData.Length); + } + } + else + { + var numberOfBytesToDecrypt = data.Length - (blockSize + inboundPacketSequenceLength + serverMacLength); + if (numberOfBytesToDecrypt > 0) + { + var decryptedData = _serverCipher.Decrypt(data, blockSize + inboundPacketSequenceLength, numberOfBytesToDecrypt); + Buffer.BlockCopy(decryptedData, 0, data, blockSize + inboundPacketSequenceLength, decryptedData.Length); + } } } @@ -1507,10 +1528,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/test/Renci.SshNet.IntegrationTests/CipherTests.cs b/test/Renci.SshNet.IntegrationTests/CipherTests.cs index 1a11f9814..d60d6e579 100644 --- a/test/Renci.SshNet.IntegrationTests/CipherTests.cs +++ b/test/Renci.SshNet.IntegrationTests/CipherTests.cs @@ -64,6 +64,18 @@ public void Aes256Ctr() DoTest(Cipher.Aes256Ctr); } + [TestMethod] + public void Aes128Gcm() + { + DoTest(Cipher.Aes128Gcm); + } + + [TestMethod] + public void Aes256Gcm() + { + DoTest(Cipher.Aes256Gcm); + } + 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) => {