Skip to content

Commit 46e9dff

Browse files
committed
Add a socket receive buffer
Currently an array is allocated to read each packet from the socket, followed by decryption which allocates another array for the plaintext payload. We can save one of these two allocations by adding a persistent buffer for socket receives, and allowing the cipher implementations to decrypt into the given payload array. We can save the other allocation similarly, but in a separate change.
1 parent e5ad82c commit 46e9dff

File tree

8 files changed

+365
-172
lines changed

8 files changed

+365
-172
lines changed

src/Renci.SshNet/Abstractions/CryptoAbstraction.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using System;
2+
#if !NET
3+
using System.Runtime.CompilerServices;
4+
#endif
15
using System.Security.Cryptography;
26

37
using Org.BouncyCastle.Crypto.Prng;
@@ -80,6 +84,38 @@ public static byte[] HashSHA512(byte[] source)
8084
{
8185
return sha512.ComputeHash(source);
8286
}
87+
#endif
88+
}
89+
90+
#if !NET
91+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
92+
#endif
93+
public static bool FixedTimeEquals(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right)
94+
{
95+
#if NET
96+
return CryptographicOperations.FixedTimeEquals(left, right);
97+
#else
98+
// https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptographicOperations.cs
99+
100+
// NoOptimization because we want this method to be exactly as non-short-circuiting
101+
// as written.
102+
//
103+
// NoInlining because the NoOptimization would get lost if the method got inlined.
104+
105+
if (left.Length != right.Length)
106+
{
107+
return false;
108+
}
109+
110+
var length = left.Length;
111+
var accum = 0;
112+
113+
for (var i = 0; i < length; i++)
114+
{
115+
accum |= left[i] - right[i];
116+
}
117+
118+
return accum == 0;
83119
#endif
84120
}
85121
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#nullable enable
2+
using System;
3+
14
namespace Renci.SshNet.Security.Cryptography
25
{
36
/// <summary>
@@ -73,5 +76,25 @@ public virtual byte[] Decrypt(byte[] input)
7376
/// The decrypted data.
7477
/// </returns>
7578
public abstract byte[] Decrypt(byte[] input, int offset, int length);
79+
80+
/// <summary>
81+
/// Decrypts the specified input into a given buffer.
82+
/// </summary>
83+
/// <param name="input">The input.</param>
84+
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting.</param>
85+
/// <param name="length">The number of bytes to decrypt from <paramref name="input"/>.</param>
86+
/// <param name="output">The output buffer to write to.</param>
87+
/// <param name="outputOffset">The zero-based offset in <paramref name="output"/> at which to write decrypted output.</param>
88+
/// <returns>
89+
/// The number of bytes written to <paramref name="output"/>.
90+
/// </returns>
91+
public virtual int Decrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
92+
{
93+
var plaintext = Decrypt(input, offset, length);
94+
95+
plaintext.AsSpan().CopyTo(output.AsSpan(outputOffset));
96+
97+
return plaintext.Length;
98+
}
7699
}
77100
}

src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.BclImpl.cs

Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
#nullable enable
2+
using System;
23
using System.Security.Cryptography;
34

45
using Renci.SshNet.Common;
@@ -39,53 +40,45 @@ public BclImpl(
3940
}
4041

4142
public override byte[] Encrypt(byte[] input, int offset, int length)
43+
{
44+
return Transform(_encryptor, input, offset, length, output: null, 0, out _);
45+
}
46+
47+
public override byte[] Decrypt(byte[] input, int offset, int length)
48+
{
49+
return Transform(_decryptor, input, offset, length, output: null, 0, out _);
50+
}
51+
52+
public override int Decrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
53+
{
54+
_ = Transform(_decryptor, input, offset, length, output, outputOffset, out var bytesWritten);
55+
56+
return bytesWritten;
57+
}
58+
59+
private byte[] Transform(ICryptoTransform transform, byte[] input, int offset, int length, byte[]? output, int outputOffset, out int bytesWritten)
4260
{
4361
if (_aes.Padding != PaddingMode.None)
4462
{
4563
// If padding has been specified, call TransformFinalBlock to apply
4664
// the padding and reset the state.
47-
return _encryptor.TransformFinalBlock(input, offset, length);
48-
}
4965

50-
var paddingLength = 0;
51-
if (length % BlockSize > 0)
52-
{
53-
if (_aes.Mode is System.Security.Cryptography.CipherMode.CFB or System.Security.Cryptography.CipherMode.OFB)
66+
var finalBlock = transform.TransformFinalBlock(input, offset, length);
67+
68+
if (output is not null)
5469
{
55-
// Manually pad the input for cfb and ofb cipher mode as BCL doesn't support partial block.
56-
// See https://github.com/dotnet/runtime/blob/e7d837da5b1aacd9325a8b8f2214cfaf4d3f0ff6/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SymmetricPadding.cs#L20-L21
57-
paddingLength = BlockSize - (length % BlockSize);
58-
input = input.Take(offset, length);
59-
length += paddingLength;
60-
Array.Resize(ref input, length);
61-
offset = 0;
70+
finalBlock.AsSpan().CopyTo(output.AsSpan(outputOffset));
6271
}
72+
73+
bytesWritten = finalBlock.Length;
74+
75+
return finalBlock;
6376
}
6477

6578
// Otherwise, (the most important case) assume this instance is
6679
// used for one direction of an SSH connection, whereby the
6780
// encrypted data in all packets are considered a single data
68-
// stream i.e. we do not want to reset the state between calls to Encrypt.
69-
var output = new byte[length];
70-
_ = _encryptor.TransformBlock(input, offset, length, output, 0);
71-
72-
if (paddingLength > 0)
73-
{
74-
// Manually unpad the output.
75-
Array.Resize(ref output, output.Length - paddingLength);
76-
}
77-
78-
return output;
79-
}
80-
81-
public override byte[] Decrypt(byte[] input, int offset, int length)
82-
{
83-
if (_aes.Padding != PaddingMode.None)
84-
{
85-
// If padding has been specified, call TransformFinalBlock to apply
86-
// the padding and reset the state.
87-
return _decryptor.TransformFinalBlock(input, offset, length);
88-
}
81+
// stream i.e. we do not want to reset the state between calls to Decrypt.
8982

9083
var paddingLength = 0;
9184
if (length % BlockSize > 0)
@@ -95,24 +88,33 @@ public override byte[] Decrypt(byte[] input, int offset, int length)
9588
// Manually pad the input for cfb and ofb cipher mode as BCL doesn't support partial block.
9689
// See https://github.com/dotnet/runtime/blob/e7d837da5b1aacd9325a8b8f2214cfaf4d3f0ff6/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SymmetricPadding.cs#L20-L21
9790
paddingLength = BlockSize - (length % BlockSize);
98-
input = input.Take(offset, length);
99-
length += paddingLength;
100-
Array.Resize(ref input, length);
91+
92+
var tmp = new byte[length + paddingLength];
93+
94+
input.AsSpan(offset, length).CopyTo(tmp);
95+
96+
input = tmp;
10197
offset = 0;
98+
length = tmp.Length;
10299
}
103100
}
104101

105-
// Otherwise, (the most important case) assume this instance is
106-
// used for one direction of an SSH connection, whereby the
107-
// encrypted data in all packets are considered a single data
108-
// stream i.e. we do not want to reset the state between calls to Decrypt.
109-
var output = new byte[length];
110-
_ = _decryptor.TransformBlock(input, offset, length, output, 0);
111-
112-
if (paddingLength > 0)
102+
if (output is null)
113103
{
104+
output = new byte[length];
105+
106+
bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
107+
108+
bytesWritten -= paddingLength;
109+
114110
// Manually unpad the output.
115-
Array.Resize(ref output, output.Length - paddingLength);
111+
Array.Resize(ref output, bytesWritten);
112+
}
113+
else
114+
{
115+
bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
116+
117+
bytesWritten -= paddingLength;
116118
}
117119

118120
return output;

src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.CtrImpl.cs

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
using System;
1+
#nullable enable
2+
using System;
23
using System.Buffers.Binary;
4+
using System.Diagnostics;
35
using System.Numerics;
46
using System.Security.Cryptography;
57

8+
using Renci.SshNet.Common;
9+
610
namespace Renci.SshNet.Security.Cryptography.Ciphers
711
{
812
public partial class AesCipher
@@ -34,12 +38,32 @@ public CtrImpl(
3438

3539
public override byte[] Encrypt(byte[] input, int offset, int length)
3640
{
37-
return CTREncryptDecrypt(input, offset, length);
41+
return Decrypt(input, offset, length);
3842
}
3943

4044
public override byte[] Decrypt(byte[] input, int offset, int length)
4145
{
42-
return CTREncryptDecrypt(input, offset, length);
46+
ThrowHelper.ThrowIfNull(input);
47+
48+
var buffer = CTREncryptDecrypt(input, offset, length, output: null, 0);
49+
50+
// adjust output for non-blocksized lengths
51+
if (buffer.Length > length)
52+
{
53+
Array.Resize(ref buffer, length);
54+
}
55+
56+
return buffer;
57+
}
58+
59+
public override int Decrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
60+
{
61+
ThrowHelper.ThrowIfNull(input);
62+
ThrowHelper.ThrowIfNull(output);
63+
64+
_ = CTREncryptDecrypt(input, offset, length, output, outputOffset);
65+
66+
return length;
4367
}
4468

4569
public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
@@ -52,56 +76,67 @@ public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputC
5276
throw new NotImplementedException($"Invalid usage of {nameof(EncryptBlock)}.");
5377
}
5478

55-
private byte[] CTREncryptDecrypt(byte[] data, int offset, int length)
79+
private byte[] CTREncryptDecrypt(byte[] data, int offset, int length, byte[]? output, int outputOffset)
5680
{
57-
var count = length / BlockSize;
58-
if (length % BlockSize != 0)
81+
var blockSizedLength = length;
82+
if (blockSizedLength % BlockSize != 0)
5983
{
60-
count++;
84+
blockSizedLength += BlockSize - (blockSizedLength % BlockSize);
6185
}
6286

63-
var buffer = new byte[count * BlockSize];
64-
CTRCreateCounterArray(buffer);
65-
_ = _encryptor.TransformBlock(buffer, 0, buffer.Length, buffer, 0);
66-
ArrayXOR(buffer, data, offset, length);
87+
Debug.Assert(blockSizedLength % BlockSize == 0);
6788

68-
// adjust output for non-blocksized lengths
69-
if (buffer.Length > length)
89+
if (output is null)
7090
{
71-
Array.Resize(ref buffer, length);
91+
output = new byte[blockSizedLength];
92+
outputOffset = 0;
93+
}
94+
else if (data.AsSpan(offset, length).Overlaps(output.AsSpan(outputOffset, blockSizedLength)))
95+
{
96+
throw new ArgumentException("Input and output buffers must not overlap");
7297
}
7398

74-
return buffer;
99+
CTRCreateCounterArray(output.AsSpan(outputOffset, blockSizedLength));
100+
101+
var bytesWritten = _encryptor.TransformBlock(output, outputOffset, blockSizedLength, output, outputOffset);
102+
103+
Debug.Assert(bytesWritten == blockSizedLength);
104+
105+
ArrayXOR(output, outputOffset, data, offset, length);
106+
107+
return output;
75108
}
76109

77110
// creates the Counter array filled with incrementing copies of IV
78-
private void CTRCreateCounterArray(byte[] buffer)
111+
private void CTRCreateCounterArray(Span<byte> buffer)
79112
{
113+
Debug.Assert(buffer.Length % 16 == 0);
114+
80115
for (var i = 0; i < buffer.Length; i += 16)
81116
{
82-
BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(i + 8), _ivLower);
83-
BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(i), _ivUpper);
117+
BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(i + 8), _ivLower);
118+
BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(i), _ivUpper);
84119

85120
_ivLower += 1;
86121
_ivUpper += (_ivLower == 0) ? 1UL : 0UL;
87122
}
88123
}
89124

90125
// XOR 2 arrays using Vector<byte>
91-
private static void ArrayXOR(byte[] buffer, byte[] data, int offset, int length)
126+
private static void ArrayXOR(byte[] buffer, int bufferOffset, byte[] data, int offset, int length)
92127
{
93128
var i = 0;
94129

95130
var oneVectorFromEnd = length - Vector<byte>.Count;
96131
for (; i <= oneVectorFromEnd; i += Vector<byte>.Count)
97132
{
98-
var v = new Vector<byte>(buffer, i) ^ new Vector<byte>(data, offset + i);
99-
v.CopyTo(buffer, i);
133+
var v = new Vector<byte>(buffer, bufferOffset + i) ^ new Vector<byte>(data, offset + i);
134+
v.CopyTo(buffer, bufferOffset + i);
100135
}
101136

102137
for (; i < length; i++)
103138
{
104-
buffer[i] ^= data[offset + i];
139+
buffer[bufferOffset + i] ^= data[offset + i];
105140
}
106141
}
107142

src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ public override byte[] Decrypt(byte[] input, int offset, int length)
7777
return _impl.Decrypt(input, offset, length);
7878
}
7979

80+
/// <inheritdoc/>
81+
public override int Decrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
82+
{
83+
return _impl.Decrypt(input, offset, length, output, outputOffset);
84+
}
85+
8086
/// <inheritdoc/>
8187
public void Dispose()
8288
{

0 commit comments

Comments
 (0)