Skip to content

Commit 8ed8d38

Browse files
authored
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).
1 parent 45d8631 commit 8ed8d38

12 files changed

Lines changed: 973 additions & 107 deletions

File tree

src/Renci.SshNet/Common/Extensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Net.Sockets;
1212
using System.Numerics;
1313
using System.Runtime.CompilerServices;
14+
using System.Security.Cryptography;
1415
using System.Threading;
1516

1617
using Renci.SshNet.Messages;
@@ -434,5 +435,30 @@ internal bool IsCompletedSuccessfully
434435
}
435436
}
436437
#endif
438+
public static bool TryComputeHash(
439+
this HashAlgorithm hashAlgorithm,
440+
byte[] buffer,
441+
int offset,
442+
int count,
443+
Span<byte> destination,
444+
out int bytesWritten)
445+
{
446+
#if NET
447+
return hashAlgorithm.TryComputeHash(buffer.AsSpan(offset, count), destination, out bytesWritten);
448+
#else
449+
if (destination.Length < hashAlgorithm.HashSize / 8)
450+
{
451+
bytesWritten = 0;
452+
return false;
453+
}
454+
455+
var hash = hashAlgorithm.ComputeHash(buffer, offset, count);
456+
457+
hash.CopyTo(destination);
458+
459+
bytesWritten = hash.Length;
460+
return true;
461+
#endif
462+
}
437463
}
438464
}

src/Renci.SshNet/Messages/Message.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using System.IO;
2-
using System.Security.Cryptography;
1+
#nullable enable
2+
using System.IO;
33

4+
using Renci.SshNet.Abstractions;
45
using Renci.SshNet.Common;
56
using Renci.SshNet.Compression;
67

@@ -37,7 +38,8 @@ protected override void WriteBytes(SshDataStream stream)
3738
base.WriteBytes(stream);
3839
}
3940

40-
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool excludePacketLengthFieldWhenPadding = false)
41+
/// <returns>[4 bytes] || packet_len || padding_len || payload || padding || [macLength bytes].</returns>
42+
internal byte[] GetPacket(byte paddingMultiplier, Compressor? compressor, bool excludePacketLengthFieldWhenPadding = false, int macLength = 0)
4143
{
4244
const int outboundPacketSequenceSize = 4;
4345

@@ -82,10 +84,6 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
8284
// padding length calculation
8385
var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);
8486

85-
// add padding bytes
86-
var paddingBytes = RandomNumberGenerator.GetBytes(paddingLength);
87-
sshDataStream.Write(paddingBytes, 0, paddingLength);
88-
8987
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
9088

9189
// skip bytes for outbound packet sequence
@@ -97,7 +95,16 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
9795
// add packet padding length
9896
sshDataStream.WriteByte(paddingLength);
9997

100-
return sshDataStream.ToArray();
98+
_ = sshDataStream.Seek(0, SeekOrigin.End);
99+
100+
sshDataStream.SetLength(sshDataStream.Length + paddingLength + macLength);
101+
102+
var buffer = sshDataStream.ToArray();
103+
104+
// add padding bytes
105+
CryptoAbstraction.Randomizer.GetBytes(buffer, (int)sshDataStream.Position, paddingLength);
106+
107+
return buffer;
101108
}
102109
}
103110
else
@@ -112,7 +119,7 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
112119
var packetDataLength = GetPacketDataLength(messageLength, paddingLength);
113120

114121
// lets construct an SSH data stream of the exact size required
115-
using (var sshDataStream = new SshDataStream(packetLength + paddingLength + outboundPacketSequenceSize))
122+
using (var sshDataStream = new SshDataStream(packetLength + paddingLength + outboundPacketSequenceSize + macLength))
116123
{
117124
// skip bytes for outbound packet sequenceSize
118125
_ = sshDataStream.Seek(outboundPacketSequenceSize, SeekOrigin.Begin);
@@ -126,11 +133,14 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool ex
126133
// add message payload
127134
WriteBytes(sshDataStream);
128135

136+
sshDataStream.SetLength(sshDataStream.Length + paddingLength + macLength);
137+
138+
var buffer = sshDataStream.ToArray();
139+
129140
// add padding bytes
130-
var paddingBytes = RandomNumberGenerator.GetBytes(paddingLength);
131-
sshDataStream.Write(paddingBytes, 0, paddingLength);
141+
CryptoAbstraction.Randomizer.GetBytes(buffer, (int)sshDataStream.Position, paddingLength);
132142

133-
return sshDataStream.ToArray();
143+
return buffer;
134144
}
135145
}
136146
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ public byte[] Encrypt(byte[] input)
5454
/// </returns>
5555
public abstract byte[] Encrypt(byte[] input, int offset, int length);
5656

57+
/// <summary>
58+
/// Encrypts the specified input into a given buffer.
59+
/// </summary>
60+
/// <param name="input">The input.</param>
61+
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
62+
/// <param name="length">The number of bytes to encrypt from <paramref name="input"/>.</param>
63+
/// <param name="output">The output buffer to write to.</param>
64+
/// <param name="outputOffset">The zero-based offset in <paramref name="output"/> at which to write encrypted output.</param>
65+
/// <returns>
66+
/// The number of bytes written to <paramref name="output"/>.
67+
/// </returns>
68+
public virtual int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
69+
{
70+
var ciphertext = Encrypt(input, offset, length);
71+
72+
ciphertext.AsSpan().CopyTo(output.AsSpan(outputOffset));
73+
74+
return ciphertext.Length;
75+
}
76+
5777
/// <summary>
5878
/// Decrypts the specified input.
5979
/// </summary>

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
22
using System;
3+
using System.Diagnostics;
34
using System.Security.Cryptography;
45

56
using Renci.SshNet.Common;
@@ -44,6 +45,13 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
4445
return Transform(_encryptor, input, offset, length, output: null, 0, out _);
4546
}
4647

48+
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
49+
{
50+
_ = Transform(_encryptor, input, offset, length, output, outputOffset, out var bytesWritten);
51+
52+
return bytesWritten;
53+
}
54+
4755
public override byte[] Decrypt(byte[] input, int offset, int length)
4856
{
4957
return Transform(_decryptor, input, offset, length, output: null, 0, out _);
@@ -80,6 +88,8 @@ private byte[] Transform(ICryptoTransform transform, byte[] input, int offset, i
8088
// encrypted data in all packets are considered a single data
8189
// stream i.e. we do not want to reset the state between calls to Decrypt.
8290

91+
byte[]? tmp = null;
92+
8393
var paddingLength = 0;
8494
if (length % BlockSize > 0)
8595
{
@@ -89,34 +99,29 @@ private byte[] Transform(ICryptoTransform transform, byte[] input, int offset, i
8999
// See https://github.com/dotnet/runtime/blob/e7d837da5b1aacd9325a8b8f2214cfaf4d3f0ff6/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SymmetricPadding.cs#L20-L21
90100
paddingLength = BlockSize - (length % BlockSize);
91101

92-
var tmp = new byte[length + paddingLength];
102+
tmp = new byte[length + paddingLength];
93103

94104
input.AsSpan(offset, length).CopyTo(tmp);
95-
96-
input = tmp;
97-
offset = 0;
98-
length = tmp.Length;
99105
}
100106
}
101107

102-
if (output is null)
103-
{
104-
output = new byte[length];
105-
106-
bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
108+
output ??= new byte[length];
107109

108-
bytesWritten -= paddingLength;
110+
if (tmp is not null)
111+
{
112+
bytesWritten = transform.TransformBlock(tmp, 0, tmp.Length, tmp, 0);
109113

110-
// Manually unpad the output.
111-
Array.Resize(ref output, bytesWritten);
114+
tmp.AsSpan(0, length).CopyTo(output.AsSpan(outputOffset));
112115
}
113116
else
114117
{
115118
bytesWritten = transform.TransformBlock(input, offset, length, output, outputOffset);
116-
117-
bytesWritten -= paddingLength;
118119
}
119120

121+
bytesWritten -= paddingLength;
122+
123+
Debug.Assert(bytesWritten == length);
124+
120125
return output;
121126
}
122127

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

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ public partial class AesCipher
1111
{
1212
private sealed class CtrImpl : BlockCipher, IDisposable
1313
{
14+
private const int KeystreamBufferLength = 4096;
15+
1416
private readonly Aes _aes;
1517

1618
private readonly ICryptoTransform _encryptor;
1719

1820
private ulong _ivUpper; // The upper 64 bits of the IV
1921
private ulong _ivLower; // The lower 64 bits of the IV
2022

23+
private byte[]? _keystreamBuffer;
24+
2125
public CtrImpl(
2226
byte[] key,
2327
byte[] iv)
@@ -39,6 +43,11 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
3943
return Decrypt(input, offset, length);
4044
}
4145

46+
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
47+
{
48+
return Decrypt(input, offset, length, output, outputOffset);
49+
}
50+
4251
public override byte[] Decrypt(byte[] input, int offset, int length)
4352
{
4453
ArgumentNullException.ThrowIfNull(input);
@@ -84,23 +93,61 @@ private byte[] CTREncryptDecrypt(byte[] data, int offset, int length, byte[]? ou
8493

8594
Debug.Assert(blockSizedLength % BlockSize == 0);
8695

87-
if (output is null)
96+
byte[] keystream;
97+
int keystreamOffset;
98+
int chunkSize;
99+
100+
if (data == output && offset == outputOffset)
88101
{
89-
output = new byte[blockSizedLength];
90-
outputOffset = 0;
102+
keystream = _keystreamBuffer ??= new byte[KeystreamBufferLength];
103+
keystreamOffset = 0;
104+
chunkSize = KeystreamBufferLength;
91105
}
92-
else if (data.AsSpan(offset, length).Overlaps(output.AsSpan(outputOffset, blockSizedLength)))
106+
else
93107
{
94-
throw new ArgumentException("Input and output buffers must not overlap");
108+
if (output is null)
109+
{
110+
output = new byte[blockSizedLength];
111+
outputOffset = 0;
112+
}
113+
else if (data.AsSpan(offset, length).Overlaps(output.AsSpan(outputOffset, blockSizedLength)))
114+
{
115+
throw new ArgumentException("Input and output buffers must not overlap (except when identical).");
116+
}
117+
118+
keystream = output;
119+
keystreamOffset = outputOffset;
120+
chunkSize = length;
95121
}
96122

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);
123+
var bytesProcessed = 0;
124+
while (bytesProcessed < length)
125+
{
126+
var bytesThisChunk = Math.Min(chunkSize, length - bytesProcessed);
127+
var blockSizedChunk = (bytesThisChunk + BlockSize - 1) & ~(BlockSize - 1);
128+
129+
CTRCreateCounterArray(keystream.AsSpan(keystreamOffset, blockSizedChunk));
130+
131+
var bytesWritten = _encryptor.TransformBlock(
132+
inputBuffer: keystream,
133+
inputOffset: keystreamOffset,
134+
inputCount: blockSizedChunk,
135+
outputBuffer: keystream,
136+
outputOffset: keystreamOffset);
137+
138+
Debug.Assert(bytesWritten == blockSizedChunk);
139+
140+
ArrayXOR(
141+
dst: output,
142+
dstOffset: outputOffset + bytesProcessed,
143+
a: data,
144+
aOffset: offset + bytesProcessed,
145+
b: keystream,
146+
bOffset: keystreamOffset,
147+
length: bytesThisChunk);
148+
149+
bytesProcessed += bytesThisChunk;
150+
}
104151

105152
return output;
106153
}
@@ -120,21 +167,21 @@ private void CTRCreateCounterArray(Span<byte> buffer)
120167
}
121168
}
122169

123-
// XOR 2 arrays using Vector<byte>
124-
private static void ArrayXOR(byte[] buffer, int bufferOffset, byte[] data, int offset, int length)
170+
// dst[i] = a[i] ^ b[i]
171+
private static void ArrayXOR(byte[] dst, int dstOffset, byte[] a, int aOffset, byte[] b, int bOffset, int length)
125172
{
126173
var i = 0;
127174

128175
var oneVectorFromEnd = length - Vector<byte>.Count;
129176
for (; i <= oneVectorFromEnd; i += Vector<byte>.Count)
130177
{
131-
var v = new Vector<byte>(buffer, bufferOffset + i) ^ new Vector<byte>(data, offset + i);
132-
v.CopyTo(buffer, bufferOffset + i);
178+
var v = new Vector<byte>(a, aOffset + i) ^ new Vector<byte>(b, bOffset + i);
179+
v.CopyTo(dst, dstOffset + i);
133180
}
134181

135182
for (; i < length; i++)
136183
{
137-
buffer[bufferOffset + i] ^= data[offset + i];
184+
dst[dstOffset + i] = (byte)(a[aOffset + i] ^ b[bOffset + i]);
138185
}
139186
}
140187

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#nullable enable
12
using System;
23
using System.Security.Cryptography;
34

@@ -71,6 +72,12 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
7172
return _impl.Encrypt(input, offset, length);
7273
}
7374

75+
/// <inheritdoc/>
76+
public override int Encrypt(byte[] input, int offset, int length, byte[] output, int outputOffset)
77+
{
78+
return _impl.Encrypt(input, offset, length, output, outputOffset);
79+
}
80+
7481
/// <inheritdoc/>
7582
public override byte[] Decrypt(byte[] input, int offset, int length)
7683
{

0 commit comments

Comments
 (0)