Skip to content

Commit c88348d

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 4e02502 commit c88348d

File tree

8 files changed

+369
-180
lines changed

8 files changed

+369
-180
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;
@@ -10,5 +14,37 @@ internal static class CryptoAbstraction
1014
internal static readonly RandomNumberGenerator Randomizer = RandomNumberGenerator.Create();
1115

1216
internal static readonly SecureRandom SecureRandom = new SecureRandom(new CryptoApiRandomGenerator(Randomizer));
17+
18+
#if !NET
19+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
20+
#endif
21+
public static bool FixedTimeEquals(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right)
22+
{
23+
#if NET
24+
return CryptographicOperations.FixedTimeEquals(left, right);
25+
#else
26+
// https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptographicOperations.cs
27+
28+
// NoOptimization because we want this method to be exactly as non-short-circuiting
29+
// as written.
30+
//
31+
// NoInlining because the NoOptimization would get lost if the method got inlined.
32+
33+
if (left.Length != right.Length)
34+
{
35+
return false;
36+
}
37+
38+
var length = left.Length;
39+
var accum = 0;
40+
41+
for (var i = 0; i < length; i++)
42+
{
43+
accum |= left[i] - right[i];
44+
}
45+
46+
return accum == 0;
47+
#endif
48+
}
1349
}
1450
}

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: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
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

@@ -34,12 +36,32 @@ public CtrImpl(
3436

3537
public override byte[] Encrypt(byte[] input, int offset, int length)
3638
{
37-
return CTREncryptDecrypt(input, offset, length);
39+
return Decrypt(input, offset, length);
3840
}
3941

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

4567
public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
@@ -52,56 +74,67 @@ public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputC
5274
throw new NotImplementedException($"Invalid usage of {nameof(EncryptBlock)}.");
5375
}
5476

55-
private byte[] CTREncryptDecrypt(byte[] data, int offset, int length)
77+
private byte[] CTREncryptDecrypt(byte[] data, int offset, int length, byte[]? output, int outputOffset)
5678
{
57-
var count = length / BlockSize;
58-
if (length % BlockSize != 0)
79+
var blockSizedLength = length;
80+
if (blockSizedLength % BlockSize != 0)
5981
{
60-
count++;
82+
blockSizedLength += BlockSize - (blockSizedLength % BlockSize);
6183
}
6284

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);
85+
Debug.Assert(blockSizedLength % BlockSize == 0);
6786

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

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

77108
// creates the Counter array filled with incrementing copies of IV
78-
private void CTRCreateCounterArray(byte[] buffer)
109+
private void CTRCreateCounterArray(Span<byte> buffer)
79110
{
111+
Debug.Assert(buffer.Length % 16 == 0);
112+
80113
for (var i = 0; i < buffer.Length; i += 16)
81114
{
82-
BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(i + 8), _ivLower);
83-
BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(i), _ivUpper);
115+
BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(i + 8), _ivLower);
116+
BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(i), _ivUpper);
84117

85118
_ivLower += 1;
86119
_ivUpper += (_ivLower == 0) ? 1UL : 0UL;
87120
}
88121
}
89122

90123
// XOR 2 arrays using Vector<byte>
91-
private static void ArrayXOR(byte[] buffer, byte[] data, int offset, int length)
124+
private static void ArrayXOR(byte[] buffer, int bufferOffset, byte[] data, int offset, int length)
92125
{
93126
var i = 0;
94127

95128
var oneVectorFromEnd = length - Vector<byte>.Count;
96129
for (; i <= oneVectorFromEnd; i += Vector<byte>.Count)
97130
{
98-
var v = new Vector<byte>(buffer, i) ^ new Vector<byte>(data, offset + i);
99-
v.CopyTo(buffer, i);
131+
var v = new Vector<byte>(buffer, bufferOffset + i) ^ new Vector<byte>(data, offset + i);
132+
v.CopyTo(buffer, bufferOffset + i);
100133
}
101134

102135
for (; i < length; i++)
103136
{
104-
buffer[i] ^= data[offset + i];
137+
buffer[bufferOffset + i] ^= data[offset + i];
105138
}
106139
}
107140

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)