mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-09-10 09:15:47 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) =>
|
||||
{
|
||||
|
||||
+12
-4
@@ -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) =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user