mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-10-17 05:21:15 +08:00
Encrypt packets in-place (#1787)
Support in-place encryption in the cipher types, then use it on the plaintext packets instead of allocating a new array each time. Removes 2 of 4 bytes allocated for each byte uploaded over SFTP. For AES-CTR, supporting in-place encryption in this case means adding a persistent buffer for the keystream and encrypting in chunks. The performance difference is ~1-2% i.e. marginal versus one-shotting it. The variance is similar also for different choices of buffer size (here 4096 is used).
This commit is contained in:
@@ -11,6 +11,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
|
||||
using Renci.SshNet.Messages;
|
||||
@@ -434,5 +435,30 @@ namespace Renci.SshNet.Common
|
||||
}
|
||||
}
|
||||
#endif
|
||||
public static bool TryComputeHash(
|
||||
this HashAlgorithm hashAlgorithm,
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
Span<byte> destination,
|
||||
out int bytesWritten)
|
||||
{
|
||||
#if NET
|
||||
return hashAlgorithm.TryComputeHash(buffer.AsSpan(offset, count), destination, out bytesWritten);
|
||||
#else
|
||||
if (destination.Length < hashAlgorithm.HashSize / 8)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var hash = hashAlgorithm.ComputeHash(buffer, offset, count);
|
||||
|
||||
hash.CopyTo(destination);
|
||||
|
||||
bytesWritten = hash.Length;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
#nullable enable
|
||||
using System.IO;
|
||||
|
||||
using Renci.SshNet.Abstractions;
|
||||
using Renci.SshNet.Common;
|
||||
using Renci.SshNet.Compression;
|
||||
|
||||
@@ -37,7 +38,8 @@ namespace Renci.SshNet.Messages
|
||||
base.WriteBytes(stream);
|
||||
}
|
||||
|
||||
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool excludePacketLengthFieldWhenPadding = false)
|
||||
/// <returns>[4 bytes] || packet_len || padding_len || payload || padding || [macLength bytes].</returns>
|
||||
internal byte[] GetPacket(byte paddingMultiplier, Compressor? compressor, bool excludePacketLengthFieldWhenPadding = false, int macLength = 0)
|
||||
{
|
||||
const int outboundPacketSequenceSize = 4;
|
||||
|
||||
@@ -82,10 +84,6 @@ namespace Renci.SshNet.Messages
|
||||
// padding length calculation
|
||||
var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);
|
||||
|
||||
// add padding bytes
|
||||
var paddingBytes = RandomNumberGenerator.GetBytes(paddingLength);
|
||||
sshDataStream.Write(paddingBytes, 0, paddingLength);
|
||||
|
||||
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
|
||||
|
||||
// skip bytes for outbound packet sequence
|
||||
@@ -97,7 +95,16 @@ namespace Renci.SshNet.Messages
|
||||
// add packet padding length
|
||||
sshDataStream.WriteByte(paddingLength);
|
||||
|
||||
return sshDataStream.ToArray();
|
||||
_ = sshDataStream.Seek(0, SeekOrigin.End);
|
||||
|
||||
sshDataStream.SetLength(sshDataStream.Length + paddingLength + macLength);
|
||||
|
||||
var buffer = sshDataStream.ToArray();
|
||||
|
||||
// add padding bytes
|
||||
CryptoAbstraction.Randomizer.GetBytes(buffer, (int)sshDataStream.Position, paddingLength);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -112,7 +119,7 @@ namespace Renci.SshNet.Messages
|
||||
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
|
||||
|
||||
// lets construct an SSH data stream of the exact size required
|
||||
using (var sshDataStream = new SshDataStream(packetLength + paddingLength + outboundPacketSequenceSize))
|
||||
using (var sshDataStream = new SshDataStream(packetLength + paddingLength + outboundPacketSequenceSize + macLength))
|
||||
{
|
||||
// skip bytes for outbound packet sequenceSize
|
||||
_ = sshDataStream.Seek(outboundPacketSequenceSize, SeekOrigin.Begin);
|
||||
@@ -126,11 +133,14 @@ namespace Renci.SshNet.Messages
|
||||
// add message payload
|
||||
WriteBytes(sshDataStream);
|
||||
|
||||
// add padding bytes
|
||||
var paddingBytes = RandomNumberGenerator.GetBytes(paddingLength);
|
||||
sshDataStream.Write(paddingBytes, 0, paddingLength);
|
||||
sshDataStream.SetLength(sshDataStream.Length + paddingLength + macLength);
|
||||
|
||||
return sshDataStream.ToArray();
|
||||
var buffer = sshDataStream.ToArray();
|
||||
|
||||
// add padding bytes
|
||||
CryptoAbstraction.Randomizer.GetBytes(buffer, (int)sshDataStream.Position, paddingLength);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,26 @@ namespace Renci.SshNet.Security.Cryptography
|
||||
/// </returns>
|
||||
public abstract byte[] Encrypt(byte[] input, int offset, int length);
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts the specified input into a given buffer.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</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>
|
||||
/// <param name="output">The output buffer to write to.</param>
|
||||
/// <param name="outputOffset">The zero-based offset in <paramref name="output"/> at which to write encrypted output.</param>
|
||||
/// <returns>
|
||||
/// The number of bytes written to <paramref name="output"/>.
|
||||
/// </returns>
|
||||
public virtual int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
|
||||
{
|
||||
var ciphertext = Encrypt(input, offset, length);
|
||||
|
||||
ciphertext.AsSpan().CopyTo(output.AsSpan(outputOffset));
|
||||
|
||||
return ciphertext.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts the specified input.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using Renci.SshNet.Common;
|
||||
@@ -44,6 +45,13 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
return Transform(_encryptor, input, offset, length, output: null, 0, out _);
|
||||
}
|
||||
|
||||
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
|
||||
{
|
||||
_ = Transform(_encryptor, input, offset, length, output, outputOffset, out var bytesWritten);
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
public override byte[] Decrypt(byte[] input, int offset, int length)
|
||||
{
|
||||
return Transform(_decryptor, input, offset, length, output: null, 0, out _);
|
||||
@@ -80,6 +88,8 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
// encrypted data in all packets are considered a single data
|
||||
// stream i.e. we do not want to reset the state between calls to Decrypt.
|
||||
|
||||
byte[]? tmp = null;
|
||||
|
||||
var paddingLength = 0;
|
||||
if (length % BlockSize > 0)
|
||||
{
|
||||
@@ -89,34 +99,29 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
// See https://github.com/dotnet/runtime/blob/e7d837da5b1aacd9325a8b8f2214cfaf4d3f0ff6/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SymmetricPadding.cs#L20-L21
|
||||
paddingLength = BlockSize - (length % BlockSize);
|
||||
|
||||
var tmp = new byte[length + paddingLength];
|
||||
tmp = new byte[length + paddingLength];
|
||||
|
||||
input.AsSpan(offset, length).CopyTo(tmp);
|
||||
|
||||
input = tmp;
|
||||
offset = 0;
|
||||
length = tmp.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (output is null)
|
||||
output ??= new byte[length];
|
||||
|
||||
if (tmp is not null)
|
||||
{
|
||||
output = new byte[length];
|
||||
bytesWritten = transform.TransformBlock(tmp, 0, tmp.Length, tmp, 0);
|
||||
|
||||
bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
|
||||
|
||||
bytesWritten -= paddingLength;
|
||||
|
||||
// Manually unpad the output.
|
||||
Array.Resize(ref output, bytesWritten);
|
||||
tmp.AsSpan(0, length).CopyTo(output.AsSpan(outputOffset));
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
|
||||
|
||||
bytesWritten -= paddingLength;
|
||||
}
|
||||
|
||||
bytesWritten -= paddingLength;
|
||||
|
||||
Debug.Assert(bytesWritten == length);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
{
|
||||
private sealed class CtrImpl : BlockCipher, IDisposable
|
||||
{
|
||||
private const int KeystreamBufferLength = 4096;
|
||||
|
||||
private readonly Aes _aes;
|
||||
|
||||
private readonly ICryptoTransform _encryptor;
|
||||
@@ -18,6 +20,8 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
private ulong _ivUpper; // The upper 64 bits of the IV
|
||||
private ulong _ivLower; // The lower 64 bits of the IV
|
||||
|
||||
private byte[]? _keystreamBuffer;
|
||||
|
||||
public CtrImpl(
|
||||
byte[] key,
|
||||
byte[] iv)
|
||||
@@ -39,6 +43,11 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
return Decrypt(input, offset, length);
|
||||
}
|
||||
|
||||
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
|
||||
{
|
||||
return Decrypt(input, offset, length, output, outputOffset);
|
||||
}
|
||||
|
||||
public override byte[] Decrypt(byte[] input, int offset, int length)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
@@ -84,23 +93,61 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
|
||||
Debug.Assert(blockSizedLength % BlockSize == 0);
|
||||
|
||||
if (output is null)
|
||||
byte[] keystream;
|
||||
int keystreamOffset;
|
||||
int chunkSize;
|
||||
|
||||
if (data == output && offset == outputOffset)
|
||||
{
|
||||
output = new byte[blockSizedLength];
|
||||
outputOffset = 0;
|
||||
keystream = _keystreamBuffer ??= new byte[KeystreamBufferLength];
|
||||
keystreamOffset = 0;
|
||||
chunkSize = KeystreamBufferLength;
|
||||
}
|
||||
else if (data.AsSpan(offset, length).Overlaps(output.AsSpan(outputOffset, blockSizedLength)))
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Input and output buffers must not overlap");
|
||||
if (output is null)
|
||||
{
|
||||
output = new byte[blockSizedLength];
|
||||
outputOffset = 0;
|
||||
}
|
||||
else if (data.AsSpan(offset, length).Overlaps(output.AsSpan(outputOffset, blockSizedLength)))
|
||||
{
|
||||
throw new ArgumentException("Input and output buffers must not overlap (except when identical).");
|
||||
}
|
||||
|
||||
keystream = output;
|
||||
keystreamOffset = outputOffset;
|
||||
chunkSize = length;
|
||||
}
|
||||
|
||||
CTRCreateCounterArray(output.AsSpan(outputOffset, blockSizedLength));
|
||||
var bytesProcessed = 0;
|
||||
while (bytesProcessed < length)
|
||||
{
|
||||
var bytesThisChunk = Math.Min(chunkSize, length - bytesProcessed);
|
||||
var blockSizedChunk = (bytesThisChunk + BlockSize - 1) & ~(BlockSize - 1);
|
||||
|
||||
var bytesWritten = _encryptor.TransformBlock(output, outputOffset, blockSizedLength, output, outputOffset);
|
||||
CTRCreateCounterArray(keystream.AsSpan(keystreamOffset, blockSizedChunk));
|
||||
|
||||
Debug.Assert(bytesWritten == blockSizedLength);
|
||||
var bytesWritten = _encryptor.TransformBlock(
|
||||
inputBuffer: keystream,
|
||||
inputOffset: keystreamOffset,
|
||||
inputCount: blockSizedChunk,
|
||||
outputBuffer: keystream,
|
||||
outputOffset: keystreamOffset);
|
||||
|
||||
ArrayXOR(output, outputOffset, data, offset, length);
|
||||
Debug.Assert(bytesWritten == blockSizedChunk);
|
||||
|
||||
ArrayXOR(
|
||||
dst: output,
|
||||
dstOffset: outputOffset + bytesProcessed,
|
||||
a: data,
|
||||
aOffset: offset + bytesProcessed,
|
||||
b: keystream,
|
||||
bOffset: keystreamOffset,
|
||||
length: bytesThisChunk);
|
||||
|
||||
bytesProcessed += bytesThisChunk;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -120,21 +167,21 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
}
|
||||
}
|
||||
|
||||
// XOR 2 arrays using Vector<byte>
|
||||
private static void ArrayXOR(byte[] buffer, int bufferOffset, byte[] data, int offset, int length)
|
||||
// dst[i] = a[i] ^ b[i]
|
||||
private static void ArrayXOR(byte[] dst, int dstOffset, byte[] a, int aOffset, byte[] b, int bOffset, int length)
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
var oneVectorFromEnd = length - Vector<byte>.Count;
|
||||
for (; i <= oneVectorFromEnd; i += Vector<byte>.Count)
|
||||
{
|
||||
var v = new Vector<byte>(buffer, bufferOffset + i) ^ new Vector<byte>(data, offset + i);
|
||||
v.CopyTo(buffer, bufferOffset + i);
|
||||
var v = new Vector<byte>(a, aOffset + i) ^ new Vector<byte>(b, bOffset + i);
|
||||
v.CopyTo(dst, dstOffset + i);
|
||||
}
|
||||
|
||||
for (; i < length; i++)
|
||||
{
|
||||
buffer[bufferOffset + i] ^= data[offset + i];
|
||||
dst[dstOffset + i] = (byte)(a[aOffset + i] ^ b[bOffset + i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
@@ -71,6 +72,12 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
return _impl.Encrypt(input, offset, length);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
|
||||
{
|
||||
return _impl.Encrypt(input, offset, length, output, outputOffset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override byte[] Decrypt(byte[] input, int offset, int length)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -74,6 +75,17 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
}
|
||||
}
|
||||
|
||||
public override byte[] Encrypt(byte[] input, int offset, int length)
|
||||
{
|
||||
var output = new byte[length + TagSizeInBytes];
|
||||
|
||||
var bytesWritten = Encrypt(input, offset, length, output, 0);
|
||||
|
||||
Debug.Assert(bytesWritten == output.Length);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts the specified input.
|
||||
/// </summary>
|
||||
@@ -85,16 +97,17 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
/// </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:
|
||||
/// <param name="output">The output buffer to write to.</param>
|
||||
/// <param name="outputOffset">The zero-based offset in <paramref name="output"/> at which to write encrypted output.</param>
|
||||
/// <remarks>
|
||||
/// The output data is written with the below format:
|
||||
/// <code>
|
||||
/// [----AAD----][----Cipher Text----][----TAG----]
|
||||
/// </code>
|
||||
/// </returns>
|
||||
public override byte[] Encrypt(byte[] input, int offset, int length)
|
||||
/// </remarks>
|
||||
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
|
||||
{
|
||||
var output = new byte[length + TagSize];
|
||||
Buffer.BlockCopy(input, offset, output, 0, _aadLength);
|
||||
input.AsSpan(offset, _aadLength).CopyTo(output.AsSpan(outputOffset));
|
||||
|
||||
_impl.Encrypt(
|
||||
input,
|
||||
@@ -103,11 +116,11 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
associatedDataOffset: offset,
|
||||
associatedDataLength: _aadLength,
|
||||
output,
|
||||
cipherTextOffset: _aadLength);
|
||||
outputOffset + _aadLength);
|
||||
|
||||
IncrementCounter();
|
||||
|
||||
return output;
|
||||
return length + TagSizeInBytes;
|
||||
}
|
||||
|
||||
public override byte[] Decrypt(byte[] input, int offset, int length)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -20,9 +21,9 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
{
|
||||
private readonly byte[] _iv;
|
||||
private readonly int _aadLength;
|
||||
private readonly KeyParameter _aadKeyParameter;
|
||||
private readonly KeyParameter? _aadKeyParameter;
|
||||
private readonly KeyParameter _keyParameter;
|
||||
private readonly ChaCha7539Engine _aadCipher;
|
||||
private readonly ChaCha7539Engine? _aadCipher;
|
||||
private readonly ChaCha7539Engine _cipher;
|
||||
private readonly Poly1305 _mac;
|
||||
|
||||
@@ -74,6 +75,17 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
_mac = new Poly1305();
|
||||
}
|
||||
|
||||
public override byte[] Encrypt(byte[] input, int offset, int length)
|
||||
{
|
||||
var output = new byte[length + TagSize];
|
||||
|
||||
var bytesWritten = Encrypt(input, offset, length, output, 0);
|
||||
|
||||
Debug.Assert(bytesWritten == length);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts the specified input.
|
||||
/// </summary>
|
||||
@@ -85,13 +97,15 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
/// </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:
|
||||
/// <param name="output">The output buffer to write to.</param>
|
||||
/// <param name="outputOffset">The zero-based offset in <paramref name="output"/> at which to write encrypted output.</param>
|
||||
/// <remarks>
|
||||
/// The output data is written with the below format:
|
||||
/// <code>
|
||||
/// [----Cipher AAD----][----Cipher Text----][----TAG----]
|
||||
/// </code>
|
||||
/// </returns>
|
||||
public override byte[] Encrypt(byte[] input, int offset, int length)
|
||||
/// </remarks>
|
||||
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
|
||||
{
|
||||
_aadCipher?.Init(forEncryption: true, new ParametersWithIV(_aadKeyParameter, _iv));
|
||||
_cipher.Init(forEncryption: true, new ParametersWithIV(_keyParameter, _iv));
|
||||
@@ -100,15 +114,15 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
|
||||
_cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
|
||||
_mac.Init(new KeyParameter(keyStream, 0, 32));
|
||||
|
||||
var output = new byte[length + TagSize];
|
||||
_aadCipher?.ProcessBytes(input, offset, _aadLength, output, outputOffset);
|
||||
_cipher.ProcessBytes(input, offset + _aadLength, length - _aadLength, output, outputOffset + _aadLength);
|
||||
|
||||
_aadCipher?.ProcessBytes(input, offset, _aadLength, output, 0);
|
||||
_cipher.ProcessBytes(input, offset + _aadLength, length - _aadLength, output, _aadLength);
|
||||
_mac.BlockUpdate(output, outputOffset, length);
|
||||
var macBytesWritten = _mac.DoFinal(output, outputOffset + length);
|
||||
|
||||
_mac.BlockUpdate(output, 0, length);
|
||||
_ = _mac.DoFinal(output, length);
|
||||
Debug.Assert(macBytesWritten == TagSize);
|
||||
|
||||
return output;
|
||||
return length + macBytesWritten;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+50
-38
@@ -1050,67 +1050,79 @@ namespace Renci.SshNet
|
||||
}
|
||||
|
||||
var paddingMultiplier = _clientCipher is null ? (byte)8 : Math.Max((byte)8, _clientCipher.MinimumSize);
|
||||
var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientEtm || _clientAead);
|
||||
|
||||
var macLength = 0;
|
||||
|
||||
if (_clientAead)
|
||||
{
|
||||
macLength = _clientCipher.TagSize;
|
||||
}
|
||||
else if (_clientMac != null)
|
||||
{
|
||||
macLength = _clientMac.HashSize / 8;
|
||||
}
|
||||
|
||||
var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientEtm || _clientAead, macLength);
|
||||
|
||||
if (packetData.Length > MaximumSshPacketSize)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Packet is too big. Maximum packet size is {0} bytes.", MaximumSshPacketSize));
|
||||
}
|
||||
|
||||
// take a write lock to ensure the outbound packet sequence number is incremented
|
||||
// atomically, and only after the packet has actually been sent
|
||||
lock (_socketWriteLock)
|
||||
{
|
||||
byte[] hash = null;
|
||||
var packetDataOffset = 4; // first four bytes are reserved for outbound packet sequence
|
||||
|
||||
// write outbound packet sequence to start of packet data
|
||||
BinaryPrimitives.WriteUInt32BigEndian(packetData, _outboundPacketSequence);
|
||||
|
||||
if (_clientMac != null && !_clientEtm)
|
||||
{
|
||||
// calculate packet hash
|
||||
hash = _clientMac.ComputeHash(packetData);
|
||||
// non-ETM mac = MAC(key, sequence_number || unencrypted_packet)
|
||||
|
||||
var hashSuccess = _clientMac.TryComputeHash(
|
||||
buffer: packetData,
|
||||
offset: 0,
|
||||
count: packetData.Length - macLength,
|
||||
destination: packetData.AsSpan(packetData.Length - macLength),
|
||||
bytesWritten: out var bytesWritten);
|
||||
|
||||
Debug.Assert(hashSuccess && bytesWritten == macLength);
|
||||
}
|
||||
|
||||
// Encrypt packet data
|
||||
if (_clientCipher != null)
|
||||
{
|
||||
_clientCipher.SetSequenceNumber(_outboundPacketSequence);
|
||||
if (_clientEtm)
|
||||
{
|
||||
// The length of the "packet length" field in bytes
|
||||
const int packetLengthFieldLength = 4;
|
||||
|
||||
var encryptedData = _clientCipher.Encrypt(packetData, packetDataOffset + packetLengthFieldLength, packetData.Length - packetDataOffset - packetLengthFieldLength);
|
||||
// Not encrypting the sequence number (it is not part of the packet),
|
||||
// nor the packet length for ETM.
|
||||
var offset = _clientEtm ? 8 : 4;
|
||||
|
||||
Array.Resize(ref packetData, packetDataOffset + packetLengthFieldLength + encryptedData.Length);
|
||||
var numberOfBytesEncrypted = _clientCipher.Encrypt(
|
||||
input: packetData,
|
||||
offset,
|
||||
length: packetData.Length - offset - macLength,
|
||||
output: packetData,
|
||||
outputOffset: offset);
|
||||
|
||||
// write encrypted data
|
||||
Buffer.BlockCopy(encryptedData, 0, packetData, packetDataOffset + packetLengthFieldLength, encryptedData.Length);
|
||||
|
||||
// calculate packet hash
|
||||
hash = _clientMac.ComputeHash(packetData);
|
||||
}
|
||||
else
|
||||
{
|
||||
packetData = _clientCipher.Encrypt(packetData, packetDataOffset, packetData.Length - packetDataOffset);
|
||||
packetDataOffset = 0;
|
||||
}
|
||||
Debug.Assert(numberOfBytesEncrypted == packetData.Length - offset - macLength + (_clientAead ? macLength : 0));
|
||||
}
|
||||
|
||||
if (packetData.Length > MaximumSshPacketSize)
|
||||
if (_clientMac != null && _clientEtm)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Packet is too big. Maximum packet size is {0} bytes.", MaximumSshPacketSize));
|
||||
// ETM mac = MAC(key, sequence_number || packet_length || encrypted_packet)
|
||||
|
||||
var hashSuccess = _clientMac.TryComputeHash(
|
||||
buffer: packetData,
|
||||
offset: 0,
|
||||
count: packetData.Length - macLength,
|
||||
destination: packetData.AsSpan(packetData.Length - macLength),
|
||||
bytesWritten: out var bytesWritten);
|
||||
|
||||
Debug.Assert(hashSuccess && bytesWritten == macLength);
|
||||
}
|
||||
|
||||
var packetLength = packetData.Length - packetDataOffset;
|
||||
if (hash is null)
|
||||
{
|
||||
SendPacket(packetData, packetDataOffset, packetLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
var data = new byte[packetLength + hash.Length];
|
||||
Buffer.BlockCopy(packetData, packetDataOffset, data, 0, packetLength);
|
||||
Buffer.BlockCopy(hash, 0, data, packetLength, hash.Length);
|
||||
SendPacket(data, 0, data.Length);
|
||||
}
|
||||
SendPacket(packetData, 4, packetData.Length - 4);
|
||||
|
||||
if (_isStrictKex && message is NewKeysMessage)
|
||||
{
|
||||
|
||||
@@ -62,13 +62,13 @@ namespace Renci.SshNet.Benchmarks.Security.Cryptography.Ciphers
|
||||
[Benchmark]
|
||||
public byte[] Encrypt_ECB()
|
||||
{
|
||||
return new AesCipher(_key, null, AesCipherMode.ECB, false).Encrypt(_data);
|
||||
return new AesCipher(_key, [], AesCipherMode.ECB, false).Encrypt(_data);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public byte[] Decrypt_ECB()
|
||||
{
|
||||
return new AesCipher(_key, null, AesCipherMode.ECB, false).Decrypt(_data);
|
||||
return new AesCipher(_key, [], AesCipherMode.ECB, false).Decrypt(_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -141,6 +141,20 @@ foreach ((string mode, (string modeCode, CipherMode? bclMode)) in modes)
|
||||
tw.WriteLine();
|
||||
tw.WriteLine($"CollectionAssert.AreEqual(input, decrypted);");
|
||||
|
||||
tw.WriteLine();
|
||||
tw.WriteLine($"// Decrypt in-place");
|
||||
tw.WriteLine($"var decryptCount = new AesCipher(key, {modeCode}, pkcs7Padding: {(pad ? "true" : "false")}).Decrypt(actual, 0, actual.Length, actual, 0);");
|
||||
tw.WriteLine();
|
||||
tw.WriteLine($"Assert.AreEqual(input.Length, decryptCount);");
|
||||
tw.WriteLine($"CollectionAssert.AreEqual(input, {(pad ? "actual.Take(decryptCount)" : "actual")});");
|
||||
|
||||
tw.WriteLine();
|
||||
tw.WriteLine($"// Re-encrypt in-place");
|
||||
tw.WriteLine($"var encryptCount = new AesCipher(key, {modeCode}, pkcs7Padding: {(pad ? "true" : "false")}).Encrypt(actual, 0, input.Length, actual, 0);");
|
||||
tw.WriteLine();
|
||||
tw.WriteLine($"Assert.AreEqual(expected.Length, encryptCount);");
|
||||
tw.WriteLine($"CollectionAssert.AreEqual(expected, actual);");
|
||||
|
||||
tw.Indent--;
|
||||
tw.WriteLine("}");
|
||||
tw.WriteLine();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user