Skip to content

Commit 7fc88b5

Browse files
AesGcmCipher uses BouncyCastle as a fallback if BCL does not support. (#1450)
* [AesGcmCipher] Use BouncyCastle as a fallback if BCL does not support. * Switch back to collection initializer * Remove conditional compilation * Throw SshConnectionException with Reason MacError when authentication tag mismatch * Separate BCL and BouncyCastle implementation * Update AesGcmCipher.BclImpl.cs * Naming enhancement * Remove empty line * Disable S1199. See sshnet/SSH.NET#1371 (comment) * Set InnerException when MAC error. Remove Message check. * Store KeyParameter as private field * Use GcmCipher.ProcessAadBytes to avoid the copy of associated data * Move nonce to constructor to avoid creating AeadParameters each packet * Use const int for tag size --------- Co-authored-by: Wojciech Nagórski <[email protected]>
1 parent 3080e05 commit 7fc88b5

File tree

10 files changed

+198
-54
lines changed

10 files changed

+198
-54
lines changed

.editorconfig

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ dotnet_diagnostic.S1144.severity = none
8787
# This is a duplicate of IDE0060.
8888
dotnet_diagnostic.S1172.severity = none
8989

90+
# S1199: Nested code blocks should not be used
91+
# https://rules.sonarsource.com/csharp/RSPEC-1199
92+
dotnet_diagnostic.S1199.severity = none
93+
9094
# S1481: Unused local variables should be removed
9195
# https://rules.sonarsource.com/csharp/RSPEC-1481
9296
#

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ The main types provided by this library are:
7070
* aes128-ctr
7171
* aes192-ctr
7272
* aes256-ctr
73-
* aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
74-
* aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
73+
* aes128-gcm<span></span>@openssh.com
74+
* aes256-gcm<span></span>@openssh.com
7575
* chacha20-poly1305<span></span>@openssh.com
7676
* aes128-cbc
7777
* aes192-cbc

appveyor.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ for:
3232
# some coverage until a proper solution for running the .NET Framework integration tests in CI is found.
3333
# Running all the tests causes problems which are not worth solving in this rare configuration.
3434
# See https://github.com/sshnet/SSH.NET/pull/1462 and related links
35-
- sh: dotnet test -f net48 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_48_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_48_coverage.xml --filter "Name~Ecdh|Name~Zlib" test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
35+
- sh: dotnet test -f net48 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_48_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_48_coverage.xml --filter "Name~Ecdh|Name~Zlib|Name~Gcm" test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
3636

3737
-
3838
matrix:

src/Renci.SshNet/ConnectionInfo.cs

+13-16
Original file line numberDiff line numberDiff line change
@@ -381,22 +381,19 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
381381
{ "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() },
382382
};
383383

384-
Encryptions = new Dictionary<string, CipherInfo>();
385-
Encryptions.Add("aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
386-
Encryptions.Add("aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
387-
Encryptions.Add("aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
388-
#if NET6_0_OR_GREATER
389-
if (AesGcm.IsSupported)
390-
{
391-
Encryptions.Add("[email protected]", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
392-
Encryptions.Add("[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
393-
}
394-
#endif
395-
Encryptions.Add("[email protected]", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true));
396-
Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
397-
Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
398-
Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
399-
Encryptions.Add("3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)));
384+
Encryptions = new Dictionary<string, CipherInfo>
385+
{
386+
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
387+
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
388+
{ "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
389+
{ "[email protected]", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
390+
{ "[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
391+
{ "[email protected]", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true) },
392+
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
393+
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
394+
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
395+
{ "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)) },
396+
};
400397

401398
HmacAlgorithms = new Dictionary<string, HashInfo>
402399
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.Security.Cryptography;
4+
5+
using Renci.SshNet.Common;
6+
using Renci.SshNet.Messages.Transport;
7+
8+
namespace Renci.SshNet.Security.Cryptography.Ciphers
9+
{
10+
internal partial class AesGcmCipher
11+
{
12+
private sealed class BclImpl : Impl
13+
{
14+
private readonly AesGcm _aesGcm;
15+
private readonly byte[] _nonce;
16+
17+
public BclImpl(byte[] key, byte[] nonce)
18+
{
19+
#if NET8_0_OR_GREATER
20+
_aesGcm = new AesGcm(key, TagSizeInBytes);
21+
#else
22+
_aesGcm = new AesGcm(key);
23+
#endif
24+
_nonce = nonce;
25+
}
26+
27+
public override void Encrypt(byte[] input, int plainTextOffset, int plainTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int cipherTextOffset)
28+
{
29+
var cipherTextLength = plainTextLength;
30+
var plainText = new ReadOnlySpan<byte>(input, plainTextOffset, plainTextLength);
31+
var cipherText = new Span<byte>(output, cipherTextOffset, cipherTextLength);
32+
var tag = new Span<byte>(output, cipherTextOffset + cipherTextLength, TagSizeInBytes);
33+
var associatedData = new ReadOnlySpan<byte>(input, associatedDataOffset, associatedDataLength);
34+
35+
_aesGcm.Encrypt(_nonce, plainText, cipherText, tag, associatedData);
36+
}
37+
38+
public override void Decrypt(byte[] input, int cipherTextOffset, int cipherTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int plainTextOffset)
39+
{
40+
var plainTextLength = cipherTextLength;
41+
var cipherText = new ReadOnlySpan<byte>(input, cipherTextOffset, cipherTextLength);
42+
var tag = new ReadOnlySpan<byte>(input, cipherTextOffset + cipherTextLength, TagSizeInBytes);
43+
var plainText = new Span<byte>(output, plainTextOffset, plainTextLength);
44+
var associatedData = new ReadOnlySpan<byte>(input, associatedDataOffset, associatedDataLength);
45+
46+
try
47+
{
48+
_aesGcm.Decrypt(_nonce, cipherText, tag, output, associatedData);
49+
}
50+
#if NET8_0_OR_GREATER
51+
catch (AuthenticationTagMismatchException ex)
52+
#else
53+
catch (CryptographicException ex)
54+
#endif
55+
{
56+
throw new SshConnectionException("MAC error", DisconnectReason.MacError, ex);
57+
}
58+
}
59+
60+
protected override void Dispose(bool disposing)
61+
{
62+
base.Dispose(disposing);
63+
64+
if (disposing)
65+
{
66+
_aesGcm.Dispose();
67+
}
68+
}
69+
}
70+
}
71+
}
72+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Org.BouncyCastle.Crypto;
2+
using Org.BouncyCastle.Crypto.Engines;
3+
using Org.BouncyCastle.Crypto.Modes;
4+
using Org.BouncyCastle.Crypto.Parameters;
5+
6+
using Renci.SshNet.Common;
7+
using Renci.SshNet.Messages.Transport;
8+
9+
namespace Renci.SshNet.Security.Cryptography.Ciphers
10+
{
11+
internal partial class AesGcmCipher
12+
{
13+
private sealed class BouncyCastleImpl : Impl
14+
{
15+
private readonly GcmBlockCipher _cipher;
16+
private readonly AeadParameters _parameters;
17+
18+
public BouncyCastleImpl(byte[] key, byte[] nonce)
19+
{
20+
_cipher = new GcmBlockCipher(new AesEngine());
21+
_parameters = new AeadParameters(new KeyParameter(key), TagSizeInBytes * 8, nonce);
22+
}
23+
24+
public override void Encrypt(byte[] input, int plainTextOffset, int plainTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int cipherTextOffset)
25+
{
26+
_cipher.Init(forEncryption: true, _parameters);
27+
_cipher.ProcessAadBytes(input, associatedDataOffset, associatedDataLength);
28+
var cipherTextLength = _cipher.ProcessBytes(input, plainTextOffset, plainTextLength, output, cipherTextOffset);
29+
_ = _cipher.DoFinal(output, cipherTextOffset + cipherTextLength);
30+
}
31+
32+
public override void Decrypt(byte[] input, int cipherTextOffset, int cipherTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int plainTextOffset)
33+
{
34+
_cipher.Init(forEncryption: false, _parameters);
35+
_cipher.ProcessAadBytes(input, associatedDataOffset, associatedDataLength);
36+
var plainTextLength = _cipher.ProcessBytes(input, cipherTextOffset, cipherTextLength + TagSizeInBytes, output, plainTextOffset);
37+
try
38+
{
39+
_ = _cipher.DoFinal(output, plainTextLength);
40+
}
41+
catch (InvalidCipherTextException ex)
42+
{
43+
throw new SshConnectionException("MAC error", DisconnectReason.MacError, ex);
44+
}
45+
}
46+
}
47+
}
48+
}

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

+55-25
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
#if NET6_0_OR_GREATER
2-
using System;
1+
using System;
32
using System.Buffers.Binary;
43
using System.Diagnostics;
5-
using System.Security.Cryptography;
64

75
using Renci.SshNet.Common;
86

@@ -12,10 +10,16 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
1210
/// AES GCM cipher implementation.
1311
/// <see href="https://datatracker.ietf.org/doc/html/rfc5647"/>.
1412
/// </summary>
15-
internal sealed class AesGcmCipher : SymmetricCipher, IDisposable
13+
internal sealed partial class AesGcmCipher : SymmetricCipher, IDisposable
1614
{
15+
private const int PacketLengthFieldLength = 4;
16+
private const int TagSizeInBytes = 16;
1717
private readonly byte[] _iv;
18-
private readonly AesGcm _aesGcm;
18+
#if NET6_0_OR_GREATER
19+
private readonly Impl _impl;
20+
#else
21+
private readonly BouncyCastleImpl _impl;
22+
#endif
1923

2024
/// <summary>
2125
/// Gets the minimun block size.
@@ -42,7 +46,7 @@ public override int TagSize
4246
{
4347
get
4448
{
45-
return 16;
49+
return TagSizeInBytes;
4650
}
4751
}
4852

@@ -56,11 +60,16 @@ public AesGcmCipher(byte[] key, byte[] iv)
5660
{
5761
// SSH AES-GCM requires a 12-octet Initial IV
5862
_iv = iv.Take(12);
59-
#if NET8_0_OR_GREATER
60-
_aesGcm = new AesGcm(key, TagSize);
61-
#else
62-
_aesGcm = new AesGcm(key);
63+
#if NET6_0_OR_GREATER
64+
if (System.Security.Cryptography.AesGcm.IsSupported)
65+
{
66+
_impl = new BclImpl(key, _iv);
67+
}
68+
else
6369
#endif
70+
{
71+
_impl = new BouncyCastleImpl(key, _iv);
72+
}
6473
}
6574

6675
/// <summary>
@@ -84,15 +93,17 @@ public AesGcmCipher(byte[] key, byte[] iv)
8493
/// </returns>
8594
public override byte[] Encrypt(byte[] input, int offset, int length)
8695
{
87-
var packetLengthField = new ReadOnlySpan<byte>(input, offset, 4);
88-
var plainText = new ReadOnlySpan<byte>(input, offset + 4, length - 4);
89-
9096
var output = new byte[length + TagSize];
91-
packetLengthField.CopyTo(output);
92-
var cipherText = new Span<byte>(output, 4, length - 4);
93-
var tag = new Span<byte>(output, length, TagSize);
97+
Buffer.BlockCopy(input, offset, output, 0, PacketLengthFieldLength);
9498

95-
_aesGcm.Encrypt(nonce: _iv, plainText, cipherText, tag, associatedData: packetLengthField);
99+
_impl.Encrypt(
100+
input,
101+
plainTextOffset: offset + PacketLengthFieldLength,
102+
plainTextLength: length - PacketLengthFieldLength,
103+
associatedDataOffset: offset,
104+
associatedDataLength: PacketLengthFieldLength,
105+
output,
106+
cipherTextOffset: PacketLengthFieldLength);
96107

97108
IncrementCounter();
98109

@@ -122,14 +133,16 @@ public override byte[] Decrypt(byte[] input, int offset, int length)
122133
{
123134
Debug.Assert(offset == 8, "The offset must be 8");
124135

125-
var packetLengthField = new ReadOnlySpan<byte>(input, 4, 4);
126-
var cipherText = new ReadOnlySpan<byte>(input, offset, length);
127-
var tag = new ReadOnlySpan<byte>(input, offset + length, TagSize);
128-
129136
var output = new byte[length];
130-
var plainText = new Span<byte>(output);
131137

132-
_aesGcm.Decrypt(nonce: _iv, cipherText, tag, plainText, associatedData: packetLengthField);
138+
_impl.Decrypt(
139+
input,
140+
cipherTextOffset: offset,
141+
cipherTextLength: length,
142+
associatedDataOffset: offset - PacketLengthFieldLength,
143+
associatedDataLength: PacketLengthFieldLength,
144+
output,
145+
plainTextOffset: 0);
133146

134147
IncrementCounter();
135148

@@ -158,7 +171,7 @@ public void Dispose(bool disposing)
158171
{
159172
if (disposing)
160173
{
161-
_aesGcm.Dispose();
174+
_impl.Dispose();
162175
}
163176
}
164177

@@ -169,6 +182,23 @@ public void Dispose()
169182
Dispose(disposing: true);
170183
GC.SuppressFinalize(this);
171184
}
185+
186+
private abstract class Impl : IDisposable
187+
{
188+
public abstract void Encrypt(byte[] input, int plainTextOffset, int plainTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int cipherTextOffset);
189+
190+
public abstract void Decrypt(byte[] input, int cipherTextOffset, int cipherTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int plainTextOffset);
191+
192+
protected virtual void Dispose(bool disposing)
193+
{
194+
}
195+
196+
public void Dispose()
197+
{
198+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
199+
Dispose(disposing: true);
200+
GC.SuppressFinalize(this);
201+
}
202+
}
172203
}
173204
}
174-
#endif

src/Renci.SshNet/Security/KeyExchangeECDH.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,10 @@ public override void Start(Session session, KeyExchangeInitMessage message, bool
4646
_impl = new BclImpl(Curve);
4747
}
4848
else
49+
#endif
4950
{
5051
_impl = new BouncyCastleImpl(CurveParameter);
5152
}
52-
#else
53-
_impl = new BouncyCastleImpl(CurveParameter);
54-
#endif
5553

5654
_clientExchangeValue = _impl.GenerateClientECPoint();
5755

src/Renci.SshNet/Session.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -1258,11 +1258,7 @@ private Message ReceiveMessage(Socket socket)
12581258
var plainFirstBlock = firstBlock;
12591259

12601260
// First block is not encrypted in AES GCM mode.
1261-
if (_serverCipher is not null
1262-
#if NET6_0_OR_GREATER
1263-
and not Security.Cryptography.Ciphers.AesGcmCipher
1264-
#endif
1265-
)
1261+
if (_serverCipher is not null and not Security.Cryptography.Ciphers.AesGcmCipher)
12661262
{
12671263
_serverCipher.SetSequenceNumber(_inboundPacketSequence);
12681264

test/Renci.SshNet.IntegrationTests/CipherTests.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ public void Aes256Ctr()
6464
DoTest(Cipher.Aes256Ctr);
6565
}
6666

67-
#if NET6_0_OR_GREATER
6867
[TestMethod]
6968
public void Aes128Gcm()
7069
{
@@ -76,7 +75,7 @@ public void Aes256Gcm()
7675
{
7776
DoTest(Cipher.Aes256Gcm);
7877
}
79-
#endif
78+
8079
[TestMethod]
8180
public void ChaCha20Poly1305()
8281
{

0 commit comments

Comments
 (0)