Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Renci.SshNet/Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Net.Sockets;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;

using Renci.SshNet.Messages;
Expand Down Expand Up @@ -434,5 +435,30 @@ internal bool IsCompletedSuccessfully
}
}
#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
}
}
}
34 changes: 22 additions & 12 deletions src/Renci.SshNet/Messages/Message.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -37,7 +38,8 @@ protected override void WriteBytes(SshDataStream stream)
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;

Expand Down Expand Up @@ -82,10 +84,6 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
// 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
Expand All @@ -97,7 +95,16 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
// 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
Expand All @@ -112,7 +119,7 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
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);
Expand All @@ -126,11 +133,14 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
// add message payload
WriteBytes(sshDataStream);

sshDataStream.SetLength(sshDataStream.Length + paddingLength + macLength);

var buffer = sshDataStream.ToArray();

// add padding bytes
var paddingBytes = RandomNumberGenerator.GetBytes(paddingLength);
sshDataStream.Write(paddingBytes, 0, paddingLength);
CryptoAbstraction.Randomizer.GetBytes(buffer, (int)sshDataStream.Position, paddingLength);

return sshDataStream.ToArray();
return buffer;
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/Renci.SshNet/Security/Cryptography/Cipher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ public byte[] Encrypt(byte[] input)
/// </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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Security.Cryptography;

using Renci.SshNet.Common;
Expand Down Expand Up @@ -44,6 +45,13 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
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 _);
Expand Down Expand Up @@ -80,6 +88,8 @@ private byte[] Transform(ICryptoTransform transform, byte[] input, int offset, i
// 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)
{
Expand All @@ -89,34 +99,29 @@ private byte[] Transform(ICryptoTransform transform, byte[] input, int offset, i
// 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];

bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
output ??= new byte[length];

bytesWritten -= paddingLength;
if (tmp is not null)
{
bytesWritten = transform.TransformBlock(tmp, 0, tmp.Length, tmp, 0);

// 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ public partial class AesCipher
{
private sealed class CtrImpl : BlockCipher, IDisposable
{
private const int KeystreamBufferLength = 4096;

private readonly Aes _aes;

private readonly ICryptoTransform _encryptor;

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)
Expand All @@ -39,6 +43,11 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
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);
Expand Down Expand Up @@ -84,23 +93,61 @@ private byte[] CTREncryptDecrypt(byte[] data, int offset, int length, byte[]? ou

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 bytesWritten = _encryptor.TransformBlock(output, outputOffset, blockSizedLength, output, outputOffset);

Debug.Assert(bytesWritten == blockSizedLength);

ArrayXOR(output, outputOffset, data, offset, length);
var bytesProcessed = 0;
while (bytesProcessed < length)
{
var bytesThisChunk = Math.Min(chunkSize, length - bytesProcessed);
var blockSizedChunk = (bytesThisChunk + BlockSize - 1) & ~(BlockSize - 1);

CTRCreateCounterArray(keystream.AsSpan(keystreamOffset, blockSizedChunk));

var bytesWritten = _encryptor.TransformBlock(
inputBuffer: keystream,
inputOffset: keystreamOffset,
inputCount: blockSizedChunk,
outputBuffer: keystream,
outputOffset: keystreamOffset);

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;
}
Expand All @@ -120,21 +167,21 @@ private void CTRCreateCounterArray(Span<byte> buffer)
}
}

// 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]);
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Security.Cryptography;

Expand Down Expand Up @@ -71,6 +72,12 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
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)
{
Expand Down
Loading