From 8b5c81984c4997f42c95fc85375f8ddd66f493da Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Wed, 27 Mar 2024 21:35:37 +0800 Subject: [PATCH 01/12] Init AeadCipher --- .../Cryptography/Ciphers/AeadCipher.cs | 34 ++++++++++++++ src/Renci.SshNet/Session.cs | 44 ++++++++++++++----- 2 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs new file mode 100644 index 000000000..8cec6477d --- /dev/null +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs @@ -0,0 +1,34 @@ +using Renci.SshNet.Common; + +namespace Renci.SshNet.Security.Cryptography.Ciphers +{ + /// + /// Represents algorithm for Authenticated Encryption with Associated data. + /// + public abstract class AeadCipher : BlockCipher + { + /// + /// Gets the initial vector (nonce) for AEAD Encrypt and Decrypt. + /// + protected byte[] IV { get; } + + /// + /// Gets the tag size in bytes. + /// + public int TagSize { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The key. + /// The initial vector (nonce). + /// The nonce size in bytes. + /// The tag size in bytes. + protected AeadCipher(byte[] key, byte[] iv, int nonceSize, int tagSize) + : base(key, blockSize: 16, mode: null, padding: null) + { + IV = iv.Take(nonceSize); + TagSize = tagSize; + } + } +} diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index 0c067ec2d..f44c91bae 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -19,6 +19,7 @@ using Renci.SshNet.Messages.Transport; using Renci.SshNet.Security; using Renci.SshNet.Security.Cryptography; +using Renci.SshNet.Security.Cryptography.Ciphers; namespace Renci.SshNet { @@ -1051,11 +1052,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); } @@ -1072,9 +1073,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); @@ -1194,6 +1192,8 @@ private bool TrySendMessage(Message message) /// private Message ReceiveMessage(Socket socket) { + var aeadCipher = _serverCipher as AeadCipher; + // the length of the packet sequence field in bytes const int inboundPacketSequenceLength = 4; @@ -1207,7 +1207,11 @@ private Message ReceiveMessage(Socket socket) // 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) + if (aeadCipher != null) + { + blockSize = (byte) 4; + } + else if (_serverMac != null && _serverEtm) { blockSize = (byte) 4; } @@ -1220,7 +1224,16 @@ private Message ReceiveMessage(Socket socket) blockSize = (byte) 8; } - var serverMacLength = _serverMac != null ? _serverMac.HashSize/8 : 0; + var serverMacLength = 0; + + if (aeadCipher != null) + { + serverMacLength = aeadCipher.TagSize; + } + else if (_serverMac != null) + { + serverMacLength = _serverMac.HashSize / 8; + } byte[] data; uint packetLength; @@ -1238,7 +1251,7 @@ private Message ReceiveMessage(Socket socket) return null; } - if (_serverCipher != null && (_serverMac == null || !_serverEtm)) + if (_serverCipher != null && aeadCipher == null && (_serverMac == null || !_serverEtm)) { firstBlock = _serverCipher.Decrypt(firstBlock); } @@ -1509,8 +1522,17 @@ internal void OnNewKeysReceived(NewKeysMessage message) // Update negotiated algorithms _serverCipher = _keyExchange.CreateServerCipher(); _clientCipher = _keyExchange.CreateClientCipher(); - _serverMac = _keyExchange.CreateServerHash(out _serverEtm); - _clientMac = _keyExchange.CreateClientHash(out _clientEtm); + + if (_serverCipher is not AeadCipher) + { + _serverMac = _keyExchange.CreateServerHash(out _serverEtm); + } + + if (_clientCipher is not AeadCipher) + { + _clientMac = _keyExchange.CreateClientHash(out _clientEtm); + } + _clientCompression = _keyExchange.CreateCompressor(); _serverDecompression = _keyExchange.CreateDecompressor(); From 4d160ca277ec5b258bdeb9343b9140a0cce39098 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Wed, 27 Mar 2024 23:23:25 +0800 Subject: [PATCH 02/12] Move AeadCipher to parent folder. Move EncryptBlock/DecryptBlock from SymmetricCipher to BlockCipher --- src/Renci.SshNet/Messages/Message.cs | 10 +++---- .../Cryptography/{Ciphers => }/AeadCipher.cs | 21 ++++++++++++--- .../Security/Cryptography/BlockCipher.cs | 26 +++++++++++++++++++ .../Security/Cryptography/SymmetricCipher.cs | 26 ------------------- src/Renci.SshNet/Session.cs | 14 +++------- 5 files changed, 53 insertions(+), 44 deletions(-) rename src/Renci.SshNet/Security/Cryptography/{Ciphers => }/AeadCipher.cs (66%) diff --git a/src/Renci.SshNet/Messages/Message.cs b/src/Renci.SshNet/Messages/Message.cs index fa3ba5f72..42ececa51 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 excludePacketDataLengthFieldWhenCalculatePaddingLength = 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, excludePacketDataLengthFieldWhenCalculatePaddingLength ? 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, excludePacketDataLengthFieldWhenCalculatePaddingLength ? packetLength - 4 : packetLength); var packetDataLength = GetPacketDataLength(messageLength, paddingLength); diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs b/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs similarity index 66% rename from src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs rename to src/Renci.SshNet/Security/Cryptography/AeadCipher.cs index 8cec6477d..3339ec9c3 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AeadCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs @@ -1,12 +1,20 @@ using Renci.SshNet.Common; -namespace Renci.SshNet.Security.Cryptography.Ciphers +namespace Renci.SshNet.Security.Cryptography { /// /// Represents algorithm for Authenticated Encryption with Associated data. /// - public abstract class AeadCipher : BlockCipher + public abstract class AeadCipher : SymmetricCipher { + /// + /// Gets the size of the block in bytes. + /// + /// + /// The size of the block in bytes. + /// + private readonly byte _blockSize; + /// /// Gets the initial vector (nonce) for AEAD Encrypt and Decrypt. /// @@ -17,6 +25,12 @@ public abstract class AeadCipher : BlockCipher /// public int TagSize { get; } + /// + public override byte MinimumSize + { + get { return _blockSize; } + } + /// /// Initializes a new instance of the class. /// @@ -25,8 +39,9 @@ public abstract class AeadCipher : BlockCipher /// The nonce size in bytes. /// The tag size in bytes. protected AeadCipher(byte[] key, byte[] iv, int nonceSize, int tagSize) - : base(key, blockSize: 16, mode: null, padding: null) + : base(key) { + _blockSize = 16; IV = iv.Take(nonceSize); TagSize = tagSize; } diff --git a/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs b/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs index b9f7dde58..dd022eea4 100644 --- a/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs @@ -167,5 +167,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/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/Session.cs b/src/Renci.SshNet/Session.cs index f44c91bae..68513d9eb 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -19,7 +19,6 @@ using Renci.SshNet.Messages.Transport; using Renci.SshNet.Security; using Renci.SshNet.Security.Cryptography; -using Renci.SshNet.Security.Cryptography.Ciphers; namespace Renci.SshNet { @@ -1042,8 +1041,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 || _clientCipher is AeadCipher); // take a write lock to ensure the outbound packet sequence number is incremented // atomically, and only after the packet has actually been sent @@ -1205,13 +1204,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 (aeadCipher != null) - { - blockSize = (byte) 4; - } - else 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 || aeadCipher != null) { blockSize = (byte) 4; } From 7546a5a26497984a93a3829f561d5e5a3fc0f6e5 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Wed, 27 Mar 2024 23:25:43 +0800 Subject: [PATCH 03/12] simplify parameter name --- src/Renci.SshNet/Messages/Message.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Renci.SshNet/Messages/Message.cs b/src/Renci.SshNet/Messages/Message.cs index 42ececa51..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 excludePacketDataLengthFieldWhenCalculatePaddingLength = false) + internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool excludePacketLengthFieldWhenPadding = false) { const int outboundPacketSequenceSize = 4; @@ -80,7 +80,7 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex // determine the padding length // 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, excludePacketDataLengthFieldWhenCalculatePaddingLength ? packetLength - 4 : packetLength); + var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength); // add padding bytes var paddingBytes = new byte[paddingLength]; @@ -108,7 +108,7 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex // determine the padding length // 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, excludePacketDataLengthFieldWhenCalculatePaddingLength ? packetLength - 4 : packetLength); + var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength); var packetDataLength = GetPacketDataLength(messageLength, paddingLength); From 270fb96c4475a67c84a430ee3f73335054f5efe3 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Thu, 28 Mar 2024 01:17:01 +0800 Subject: [PATCH 04/12] Implement AesGcmCipher --- src/Renci.SshNet/ConnectionInfo.cs | 4 + .../Security/Cryptography/BlockCipher.cs | 12 --- .../Security/Cryptography/Cipher.cs | 5 +- .../Cryptography/Ciphers/AesCipherMode.cs | 12 +-- .../Cryptography/Ciphers/AesGcmCipher.cs | 100 ++++++++++++++++++ .../Cryptography/Ciphers/Arc4Cipher.cs | 44 -------- .../Cryptography/Ciphers/RsaCipher.cs | 14 --- src/Renci.SshNet/Session.cs | 4 +- .../CipherTests.cs | 12 +++ 9 files changed, 128 insertions(+), 79 deletions(-) create mode 100644 src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs index 7584816ff..176960cc6 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)) }, + { "aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv)) }, +#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/Security/Cryptography/BlockCipher.cs b/src/Renci.SshNet/Security/Cryptography/BlockCipher.cs index dd022eea4..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. /// diff --git a/src/Renci.SshNet/Security/Cryptography/Cipher.cs b/src/Renci.SshNet/Security/Cryptography/Cipher.cs index e624bbba3..7422b994b 100644 --- a/src/Renci.SshNet/Security/Cryptography/Cipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Cipher.cs @@ -41,7 +41,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..e314d4dc0 --- /dev/null +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -0,0 +1,100 @@ +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System; +using System.Security.Cryptography; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.Security.Cryptography.Ciphers +{ + /// + /// AES GCM cipher implementation. + /// + public sealed class AesGcmCipher : AeadCipher, IDisposable + { + private readonly AesGcm _aesGcm; + + /// + /// Initializes a new instance of the class. + /// + /// The key. + /// The IV. + public AesGcmCipher(byte[] key, byte[] iv) + : base(key, iv, nonceSize: 12, tagSize: 16) + { +#if NET8_0_OR_GREATER + _aesGcm = new AesGcm(key, TagSize); +#else + _aesGcm = new AesGcm(key); +#endif + } + + /// + public override byte[] Encrypt(byte[] input, int offset, int length) + { + //// [outbound sequence][packet length field][padding length field sz][payload][random paddings] + //// [-----4 bytes-----]|----4 bytes(offset)||-------------------Plain Text--------------------| + var associateData = new ReadOnlySpan(input, offset - 4, 4); + var plainText = new ReadOnlySpan(input, offset, length); + + var cipherText = new byte[length]; + var tag = new byte[TagSize]; + + _aesGcm.Encrypt(IV, plainText, cipherText, tag, associateData); + + var result = new byte[length + TagSize]; + Buffer.BlockCopy(cipherText, 0, result, 0, length); + Buffer.BlockCopy(tag, 0, result, length, TagSize); + + IncrementCounter(); + + return result; + } + + /// + public override byte[] Decrypt(byte[] input, int offset, int length) + { + //// [inbound sequence][packet length field][padding length field sz][payload][random paddings][Authenticated TAG] + //// |-----4 bytes----||----4 bytes(offset)||------------------Cipher Text--------------------||-------TAG-------| + var associateData = new ReadOnlySpan(input, offset - 4, 4); + var cipherText = new ReadOnlySpan(input, offset, length); + var tag = new ReadOnlySpan(input, offset + length, TagSize); + + var plainText = new byte[length]; + + _aesGcm.Decrypt(IV, cipherText, tag, plainText, associateData); + + IncrementCounter(); + + return plainText; + } + + private void IncrementCounter() + { + var invocationCounter = IV.Take(4, 8); + var count = Pack.BigEndianToUInt64(invocationCounter) + 1; + invocationCounter = Pack.UInt64ToBigEndian(count); + Buffer.BlockCopy(invocationCounter, 0, IV, 4, 8); + } + + /// + /// 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/Session.cs b/src/Renci.SshNet/Session.cs index 68513d9eb..7b461a9f2 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -1063,7 +1063,7 @@ internal void SendMessage(Message message) // Encrypt packet data if (_clientCipher != null) { - if (_clientMac != null && _clientEtm) + if (_clientEtm || _clientCipher is AeadCipher) { // The length of the "packet length" field in bytes const int packetLengthFieldLength = 4; @@ -1076,7 +1076,7 @@ internal void SendMessage(Message message) Buffer.BlockCopy(encryptedData, 0, packetData, packetDataOffset + packetLengthFieldLength, encryptedData.Length); // calculate packet hash - hash = _clientMac.ComputeHash(packetData); + hash = _clientMac?.ComputeHash(packetData); } else { 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() From 1059b68372e9611dec752e9d3ef6d7d7c205d398 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Thu, 28 Mar 2024 08:58:08 +0800 Subject: [PATCH 05/12] Update README --- README.md | 2 ++ .../Cryptography/Ciphers/AesGcmCipher.cs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) 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/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs index e314d4dc0..3798dbeb5 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -31,15 +31,15 @@ public AesGcmCipher(byte[] key, byte[] iv) /// public override byte[] Encrypt(byte[] input, int offset, int length) { - //// [outbound sequence][packet length field][padding length field sz][payload][random paddings] - //// [-----4 bytes-----]|----4 bytes(offset)||-------------------Plain Text--------------------| - var associateData = new ReadOnlySpan(input, offset - 4, 4); + // [outbound sequence][packet length field][padding length field sz][payload][random paddings] + // [-----4 bytes-----][----4 bytes(offset)][-------------------Plain Text--------------------] + var associatedData = new ReadOnlySpan(input, offset - 4, 4); var plainText = new ReadOnlySpan(input, offset, length); var cipherText = new byte[length]; var tag = new byte[TagSize]; - _aesGcm.Encrypt(IV, plainText, cipherText, tag, associateData); + _aesGcm.Encrypt(IV, plainText, cipherText, tag, associatedData); var result = new byte[length + TagSize]; Buffer.BlockCopy(cipherText, 0, result, 0, length); @@ -53,15 +53,15 @@ public override byte[] Encrypt(byte[] input, int offset, int length) /// public override byte[] Decrypt(byte[] input, int offset, int length) { - //// [inbound sequence][packet length field][padding length field sz][payload][random paddings][Authenticated TAG] - //// |-----4 bytes----||----4 bytes(offset)||------------------Cipher Text--------------------||-------TAG-------| - var associateData = new ReadOnlySpan(input, offset - 4, 4); + // [inbound sequence][packet length field][padding length field sz][payload][random paddings][Authenticated TAG] + // [-----4 bytes----][----4 bytes(offset)][------------------Cipher Text--------------------][-------TAG-------] + var associatedData = new ReadOnlySpan(input, offset - 4, 4); var cipherText = new ReadOnlySpan(input, offset, length); var tag = new ReadOnlySpan(input, offset + length, TagSize); var plainText = new byte[length]; - _aesGcm.Decrypt(IV, cipherText, tag, plainText, associateData); + _aesGcm.Decrypt(IV, cipherText, tag, plainText, associatedData); IncrementCounter(); From fe26b655029016ac8a6c0a4358398d8418a7b723 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sun, 31 Mar 2024 12:14:02 +0800 Subject: [PATCH 06/12] Remove protected IV from AeadCipher; Set offset to outbound sequence just like other ciphers --- .../Security/Cryptography/AeadCipher.cs | 14 ++--------- .../Cryptography/Ciphers/AesGcmCipher.cs | 25 +++++++++++-------- src/Renci.SshNet/Session.cs | 4 +-- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs b/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs index 3339ec9c3..b9538fdc6 100644 --- a/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs @@ -1,6 +1,4 @@ -using Renci.SshNet.Common; - -namespace Renci.SshNet.Security.Cryptography +namespace Renci.SshNet.Security.Cryptography { /// /// Represents algorithm for Authenticated Encryption with Associated data. @@ -15,11 +13,6 @@ public abstract class AeadCipher : SymmetricCipher /// private readonly byte _blockSize; - /// - /// Gets the initial vector (nonce) for AEAD Encrypt and Decrypt. - /// - protected byte[] IV { get; } - /// /// Gets the tag size in bytes. /// @@ -35,14 +28,11 @@ public override byte MinimumSize /// Initializes a new instance of the class. /// /// The key. - /// The initial vector (nonce). - /// The nonce size in bytes. /// The tag size in bytes. - protected AeadCipher(byte[] key, byte[] iv, int nonceSize, int tagSize) + protected AeadCipher(byte[] key, int tagSize) : base(key) { _blockSize = 16; - IV = iv.Take(nonceSize); TagSize = tagSize; } } diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs index 3798dbeb5..a9c7b0ad1 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -8,9 +8,11 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers { /// /// AES GCM cipher implementation. + /// . /// public sealed class AesGcmCipher : AeadCipher, IDisposable { + private readonly byte[] _nonce; private readonly AesGcm _aesGcm; /// @@ -19,8 +21,9 @@ public sealed class AesGcmCipher : AeadCipher, IDisposable /// The key. /// The IV. public AesGcmCipher(byte[] key, byte[] iv) - : base(key, iv, nonceSize: 12, tagSize: 16) + : base(key, tagSize: 16) { + _nonce = iv.Take(12); #if NET8_0_OR_GREATER _aesGcm = new AesGcm(key, TagSize); #else @@ -32,17 +35,19 @@ public AesGcmCipher(byte[] key, byte[] iv) public override byte[] Encrypt(byte[] input, int offset, int length) { // [outbound sequence][packet length field][padding length field sz][payload][random paddings] - // [-----4 bytes-----][----4 bytes(offset)][-------------------Plain Text--------------------] - var associatedData = new ReadOnlySpan(input, offset - 4, 4); - var plainText = new ReadOnlySpan(input, offset, length); + // [--4 bytes(offset)][------4 bytes------][-------------------Plain Text--------------------] + var packetLengthField = new ReadOnlySpan(input, offset, 4); + var plainText = new ReadOnlySpan(input, offset + 4, length - 4); - var cipherText = new byte[length]; + var cipherText = new byte[length - 4]; var tag = new byte[TagSize]; - _aesGcm.Encrypt(IV, plainText, cipherText, tag, associatedData); + _aesGcm.Encrypt(_nonce, plainText, cipherText, tag, packetLengthField); var result = new byte[length + TagSize]; - Buffer.BlockCopy(cipherText, 0, result, 0, length); + + packetLengthField.CopyTo(result); + Buffer.BlockCopy(cipherText, 0, result, 4, length - 4); Buffer.BlockCopy(tag, 0, result, length, TagSize); IncrementCounter(); @@ -61,7 +66,7 @@ public override byte[] Decrypt(byte[] input, int offset, int length) var plainText = new byte[length]; - _aesGcm.Decrypt(IV, cipherText, tag, plainText, associatedData); + _aesGcm.Decrypt(_nonce, cipherText, tag, plainText, associatedData); IncrementCounter(); @@ -70,10 +75,10 @@ public override byte[] Decrypt(byte[] input, int offset, int length) private void IncrementCounter() { - var invocationCounter = IV.Take(4, 8); + var invocationCounter = _nonce.Take(4, 8); var count = Pack.BigEndianToUInt64(invocationCounter) + 1; invocationCounter = Pack.UInt64ToBigEndian(count); - Buffer.BlockCopy(invocationCounter, 0, IV, 4, 8); + Buffer.BlockCopy(invocationCounter, 0, _nonce, 4, 8); } /// diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index 7b461a9f2..68513d9eb 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -1063,7 +1063,7 @@ internal void SendMessage(Message message) // Encrypt packet data if (_clientCipher != null) { - if (_clientEtm || _clientCipher is AeadCipher) + if (_clientMac != null && _clientEtm) { // The length of the "packet length" field in bytes const int packetLengthFieldLength = 4; @@ -1076,7 +1076,7 @@ internal void SendMessage(Message message) Buffer.BlockCopy(encryptedData, 0, packetData, packetDataOffset + packetLengthFieldLength, encryptedData.Length); // calculate packet hash - hash = _clientMac?.ComputeHash(packetData); + hash = _clientMac.ComputeHash(packetData); } else { From 1b92a6a33d98713a258c7c8b7192108d108dc456 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sun, 31 Mar 2024 12:55:24 +0800 Subject: [PATCH 07/12] Rename associatedData to packetLengthField --- .../Security/Cryptography/Ciphers/AesGcmCipher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs index a9c7b0ad1..addc2196e 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -60,13 +60,13 @@ public override byte[] Decrypt(byte[] input, int offset, int length) { // [inbound sequence][packet length field][padding length field sz][payload][random paddings][Authenticated TAG] // [-----4 bytes----][----4 bytes(offset)][------------------Cipher Text--------------------][-------TAG-------] - var associatedData = new ReadOnlySpan(input, offset - 4, 4); + var packetLengthField = new ReadOnlySpan(input, offset - 4, 4); var cipherText = new ReadOnlySpan(input, offset, length); var tag = new ReadOnlySpan(input, offset + length, TagSize); var plainText = new byte[length]; - _aesGcm.Decrypt(_nonce, cipherText, tag, plainText, associatedData); + _aesGcm.Decrypt(_nonce, cipherText, tag, plainText, packetLengthField); IncrementCounter(); From 7377cbea5c0115accac27101daa67d3694aab784 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sun, 31 Mar 2024 21:03:14 +0800 Subject: [PATCH 08/12] Use Span to avoid unnecessary allocations --- .../Security/Cryptography/Ciphers/AesGcmCipher.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs index addc2196e..9e62050a0 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -39,16 +39,12 @@ 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 cipherText = new byte[length - 4]; - var tag = new byte[TagSize]; - - _aesGcm.Encrypt(_nonce, plainText, cipherText, tag, packetLengthField); - var result = new byte[length + TagSize]; - packetLengthField.CopyTo(result); - Buffer.BlockCopy(cipherText, 0, result, 4, length - 4); - Buffer.BlockCopy(tag, 0, result, length, TagSize); + var cipherText = new Span(result, 4, length - 4); + var tag = new Span(result, length, TagSize); + + _aesGcm.Encrypt(_nonce, plainText, cipherText, tag, packetLengthField); IncrementCounter(); From cdbe822543a2e8bbb97b41b517cf10356c6027eb Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sun, 31 Mar 2024 21:35:32 +0800 Subject: [PATCH 09/12] Use `Span` to improve performance when `IncrementCounter()` --- .../Security/Cryptography/Ciphers/AesGcmCipher.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs index 9e62050a0..af977e15d 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -1,5 +1,6 @@ #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER using System; +using System.Buffers.Binary; using System.Security.Cryptography; using Renci.SshNet.Common; @@ -71,10 +72,9 @@ public override byte[] Decrypt(byte[] input, int offset, int length) private void IncrementCounter() { - var invocationCounter = _nonce.Take(4, 8); - var count = Pack.BigEndianToUInt64(invocationCounter) + 1; - invocationCounter = Pack.UInt64ToBigEndian(count); - Buffer.BlockCopy(invocationCounter, 0, _nonce, 4, 8); + var invocationCounter = new Span(_nonce, 4, 8); + var count = BinaryPrimitives.ReadUInt64BigEndian(invocationCounter); + BinaryPrimitives.WriteUInt64BigEndian(invocationCounter, count + 1); } /// From 65ad16a87be51595c8a2a0866118d0a2c405c471 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 2 Apr 2024 09:53:07 +0800 Subject: [PATCH 10/12] 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. --- src/Renci.SshNet/CipherInfo.cs | 12 +++- src/Renci.SshNet/ConnectionInfo.cs | 4 +- .../Security/Cryptography/AeadCipher.cs | 39 ---------- .../Security/Cryptography/Cipher.cs | 8 +++ .../Cryptography/Ciphers/AesGcmCipher.cs | 72 ++++++++++++++----- src/Renci.SshNet/Security/IKeyExchange.cs | 6 +- src/Renci.SshNet/Security/KeyExchange.cs | 65 ++++++++++------- src/Renci.SshNet/Session.cs | 55 +++++++------- 8 files changed, 151 insertions(+), 110 deletions(-) delete mode 100644 src/Renci.SshNet/Security/Cryptography/AeadCipher.cs 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 176960cc6..358f8b699 100644 --- a/src/Renci.SshNet/ConnectionInfo.cs +++ b/src/Renci.SshNet/ConnectionInfo.cs @@ -389,8 +389,8 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy { "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)) }, - { "aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv)) }, + { "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)) }, diff --git a/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs b/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs deleted file mode 100644 index b9538fdc6..000000000 --- a/src/Renci.SshNet/Security/Cryptography/AeadCipher.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Renci.SshNet.Security.Cryptography -{ - /// - /// Represents algorithm for Authenticated Encryption with Associated data. - /// - public abstract class AeadCipher : SymmetricCipher - { - /// - /// Gets the size of the block in bytes. - /// - /// - /// The size of the block in bytes. - /// - private readonly byte _blockSize; - - /// - /// Gets the tag size in bytes. - /// - public int TagSize { get; } - - /// - public override byte MinimumSize - { - get { return _blockSize; } - } - - /// - /// Initializes a new instance of the class. - /// - /// The key. - /// The tag size in bytes. - protected AeadCipher(byte[] key, int tagSize) - : base(key) - { - _blockSize = 16; - TagSize = tagSize; - } - } -} diff --git a/src/Renci.SshNet/Security/Cryptography/Cipher.cs b/src/Renci.SshNet/Security/Cryptography/Cipher.cs index 7422b994b..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. /// diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs index af977e15d..ea64efcba 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs @@ -11,18 +11,36 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers /// AES GCM cipher implementation. /// . /// - public sealed class AesGcmCipher : AeadCipher, IDisposable + 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, tagSize: 16) + : base(key) { _nonce = iv.Take(12); #if NET8_0_OR_GREATER @@ -32,42 +50,60 @@ public AesGcmCipher(byte[] key, byte[] iv) #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][packet length field][padding length field sz][payload][random paddings] - // [--4 bytes(offset)][------4 bytes------][-------------------Plain Text--------------------] + // [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 result = new byte[length + TagSize]; - packetLengthField.CopyTo(result); - var cipherText = new Span(result, 4, length - 4); - var tag = new Span(result, length, TagSize); + 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 result; + 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][packet length field][padding length field sz][payload][random paddings][Authenticated TAG] - // [-----4 bytes----][----4 bytes(offset)][------------------Cipher Text--------------------][-------TAG-------] - var packetLengthField = new ReadOnlySpan(input, offset - 4, 4); - var cipherText = new ReadOnlySpan(input, offset, length); - var tag = new ReadOnlySpan(input, offset + length, TagSize); + // [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 plainText = new byte[length]; + 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 plainText; + return output; } private void IncrementCounter() 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..04167859b 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]; } @@ -224,6 +229,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 +261,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 68513d9eb..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; @@ -1042,7 +1046,7 @@ 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, _clientCipher.MinimumSize); - var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientEtm || _clientCipher is AeadCipher); + 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 @@ -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; @@ -1191,8 +1195,6 @@ private bool TrySendMessage(Message message) /// private Message ReceiveMessage(Socket socket) { - var aeadCipher = _serverCipher as AeadCipher; - // the length of the packet sequence field in bytes const int inboundPacketSequenceLength = 4; @@ -1205,7 +1207,7 @@ 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, or 4 if "packet length" field is not encrypted - if (_serverEtm || aeadCipher != null) + if (_serverEtm || _serverAead) { blockSize = (byte) 4; } @@ -1220,9 +1222,9 @@ private Message ReceiveMessage(Socket socket) var serverMacLength = 0; - if (aeadCipher != null) + if (_serverAead) { - serverMacLength = aeadCipher.TagSize; + serverMacLength = _serverCipher.TagSize; } else if (_serverMac != null) { @@ -1245,7 +1247,7 @@ private Message ReceiveMessage(Socket socket) return null; } - if (_serverCipher != null && aeadCipher == null && (_serverMac == null || !_serverEtm)) + if (_serverCipher != null && !_serverAead && (_serverMac == null || !_serverEtm)) { firstBlock = _serverCipher.Decrypt(firstBlock); } @@ -1303,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); + } } } @@ -1514,18 +1528,11 @@ internal void OnNewKeysReceived(NewKeysMessage message) } // Update negotiated algorithms - _serverCipher = _keyExchange.CreateServerCipher(); - _clientCipher = _keyExchange.CreateClientCipher(); - - if (_serverCipher is not AeadCipher) - { - _serverMac = _keyExchange.CreateServerHash(out _serverEtm); - } + _serverCipher = _keyExchange.CreateServerCipher(out _serverAead); + _clientCipher = _keyExchange.CreateClientCipher(out _clientAead); - if (_clientCipher is not AeadCipher) - { - _clientMac = _keyExchange.CreateClientHash(out _clientEtm); - } + _serverMac = _keyExchange.CreateServerHash(out _serverEtm); + _clientMac = _keyExchange.CreateClientHash(out _clientEtm); _clientCompression = _keyExchange.CreateCompressor(); _serverDecompression = _keyExchange.CreateDecompressor(); From 3336d3da8831ccd4ae6794b7dc6d4a9039eb11b2 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 2 Apr 2024 13:14:08 +0800 Subject: [PATCH 11/12] Fix build --- src/Renci.SshNet/Security/KeyExchange.cs | 10 ++++++++-- .../Classes/SessionTest_ConnectedBase.cs | 16 ++++++++++++---- ...st_Connected_ServerAndClientDisconnectRace.cs | 16 ++++++++++++---- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs index 04167859b..e444059e7 100644 --- a/src/Renci.SshNet/Security/KeyExchange.cs +++ b/src/Renci.SshNet/Security/KeyExchange.cs @@ -173,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; @@ -198,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; diff --git a/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs b/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs index 5ce0d4675..9d8b2df42 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.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) => + { + serverEtm = false; + return (HashAlgorithm) 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) => { From 37a16bf1adadf441832a867225a4b014f87faacb Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 2 Apr 2024 14:45:16 +0800 Subject: [PATCH 12/12] Fix UT --- .../Classes/SessionTest_ConnectedBase.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs b/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs index 9d8b2df42..42c1f54a6 100644 --- a/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs +++ b/test/Renci.SshNet.Tests/Classes/SessionTest_ConnectedBase.cs @@ -211,6 +211,12 @@ private void SetupMocks() _ = _keyExchangeMock.Setup(p => p.Start(Session, It.IsAny(), false)); _ = _keyExchangeMock.Setup(p => p.ExchangeHash) .Returns(SessionId); + _ = _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) => { @@ -223,12 +229,6 @@ private void SetupMocks() serverEtm = false; return (HashAlgorithm) null; }); - _ = _keyExchangeMock.Setup(p => p.CreateServerHash(out It.Ref.IsAny)) - .Returns((ref bool serverEtm) => - { - serverEtm = false; - return (HashAlgorithm) null; - }); _ = _keyExchangeMock.Setup(p => p.CreateClientHash(out It.Ref.IsAny)) .Returns((ref bool clientEtm) => {