Add support for AEAD AES 128/256 GCM Ciphers (.NET 6.0 onward only) (#1369)

* Init AeadCipher

* Move AeadCipher to parent folder. Move EncryptBlock/DecryptBlock from SymmetricCipher to BlockCipher

* simplify parameter name

* Implement AesGcmCipher

* Update README

* Remove protected IV from AeadCipher; Set offset to outbound sequence just like other ciphers

* Rename associatedData to packetLengthField

* Use Span<byte> to avoid unnecessary allocations

* Use `Span` to improve performance when `IncrementCounter()`

* Add `IsAead` property to `CipherInfo`. Include packet length field and tag field in offset and length when call AesGcm's `Decrypt(...)` method. Do not determine HMAC if cipher is AesGcm during kex.

* Fix build

* Fix UT

* Check `AesGcm.IsSupported` before add to the `Encryptions` collection.
Guard AES-GCM with `NET6_0_OR_GREATER`.
Insert AES-GCM ciphers right after AES-CTR ciphers but before AES-CBC ciphers, which is similar with OpenSSH:
```
debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
```
Although Dictionary's order is not defined, from observation, it is in the same order with add. Anyway that would be another topic.

* Suppress CA1859 "Use concrete types when possible for improved performance" for `ConnectionInfo.Encryptions`.
Test `Aes128Gcm` and `Aes256Gcm` only when `NET6_0_OR_GREATER`

* Update xml doc comments. Do not treat AesGcmCipher separately in Session.cs

* Fix build

* Fix build

* Update the comment as ChaCha20Poly1305 uses a separated key to encypt the packet length and the size it 4.

* Update src/Renci.SshNet/Security/Cryptography/Cipher.cs

Co-authored-by: Rob Hague <rob.hague00@gmail.com>

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

* Fix nullable error in build

* typos

---------

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