ソースを参照

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 https://github.com/sshnet/SSH.NET/pull/1371#discussion_r1704293356

* 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 <wojtpl2@gmail.com>
Scott Xu 1 年間 前
コミット
ebb31bb16e

+ 4 - 0
.editorconfig

@@ -87,6 +87,10 @@ dotnet_diagnostic.S1144.severity = none
 # This is a duplicate of IDE0060.
 dotnet_diagnostic.S1172.severity = none
 
+# S1199: Nested code blocks should not be used
+# https://rules.sonarsource.com/csharp/RSPEC-1199
+dotnet_diagnostic.S1199.severity = none
+
 # S1481: Unused local variables should be removed
 # https://rules.sonarsource.com/csharp/RSPEC-1481
 #

+ 2 - 2
README.md

@@ -70,8 +70,8 @@ The main types provided by this library are:
 * aes128-ctr
 * aes192-ctr
 * aes256-ctr
-* aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
-* aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
+* aes128-gcm<span></span>@openssh.com
+* aes256-gcm<span></span>@openssh.com
 * chacha20-poly1305<span></span>@openssh.com
 * aes128-cbc
 * aes192-cbc

+ 1 - 1
appveyor.yml

@@ -32,7 +32,7 @@ for:
     # some coverage until a proper solution for running the .NET Framework integration tests in CI is found.
     # Running all the tests causes problems which are not worth solving in this rare configuration.
     # See https://github.com/sshnet/SSH.NET/pull/1462 and related links
-    - 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
+    - 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
 
 -
   matrix:

+ 13 - 16
src/Renci.SshNet/ConnectionInfo.cs

@@ -381,22 +381,19 @@ namespace Renci.SshNet
                     { "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() },
                 };
 
-            Encryptions = new Dictionary<string, CipherInfo>();
-            Encryptions.Add("aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
-            Encryptions.Add("aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
-            Encryptions.Add("aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)));
-#if NET6_0_OR_GREATER
-            if (AesGcm.IsSupported)
-            {
-                Encryptions.Add("aes128-gcm@openssh.com", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
-                Encryptions.Add("aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
-            }
-#endif
-            Encryptions.Add("chacha20-poly1305@openssh.com", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true));
-            Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
-            Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
-            Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
-            Encryptions.Add("3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)));
+            Encryptions = new Dictionary<string, CipherInfo>
+                {
+                    { "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
+                    { "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
+                    { "aes256-ctr", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
+                    { "aes128-gcm@openssh.com", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
+                    { "aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
+                    { "chacha20-poly1305@openssh.com", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true) },
+                    { "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
+                    { "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
+                    { "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
+                    { "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)) },
+                };
 
             HmacAlgorithms = new Dictionary<string, HashInfo>
                 {

+ 72 - 0
src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.BclImpl.cs

@@ -0,0 +1,72 @@
+#if NET6_0_OR_GREATER
+using System;
+using System.Security.Cryptography;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Messages.Transport;
+
+namespace Renci.SshNet.Security.Cryptography.Ciphers
+{
+    internal partial class AesGcmCipher
+    {
+        private sealed class BclImpl : Impl
+        {
+            private readonly AesGcm _aesGcm;
+            private readonly byte[] _nonce;
+
+            public BclImpl(byte[] key, byte[] nonce)
+            {
+#if NET8_0_OR_GREATER
+                _aesGcm = new AesGcm(key, TagSizeInBytes);
+#else
+                _aesGcm = new AesGcm(key);
+#endif
+                _nonce = nonce;
+            }
+
+            public override void Encrypt(byte[] input, int plainTextOffset, int plainTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int cipherTextOffset)
+            {
+                var cipherTextLength = plainTextLength;
+                var plainText = new ReadOnlySpan<byte>(input, plainTextOffset, plainTextLength);
+                var cipherText = new Span<byte>(output, cipherTextOffset, cipherTextLength);
+                var tag = new Span<byte>(output, cipherTextOffset + cipherTextLength, TagSizeInBytes);
+                var associatedData = new ReadOnlySpan<byte>(input, associatedDataOffset, associatedDataLength);
+
+                _aesGcm.Encrypt(_nonce, plainText, cipherText, tag, associatedData);
+            }
+
+            public override void Decrypt(byte[] input, int cipherTextOffset, int cipherTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int plainTextOffset)
+            {
+                var plainTextLength = cipherTextLength;
+                var cipherText = new ReadOnlySpan<byte>(input, cipherTextOffset, cipherTextLength);
+                var tag = new ReadOnlySpan<byte>(input, cipherTextOffset + cipherTextLength, TagSizeInBytes);
+                var plainText = new Span<byte>(output, plainTextOffset, plainTextLength);
+                var associatedData = new ReadOnlySpan<byte>(input, associatedDataOffset, associatedDataLength);
+
+                try
+                {
+                    _aesGcm.Decrypt(_nonce, cipherText, tag, output, associatedData);
+                }
+#if NET8_0_OR_GREATER
+                catch (AuthenticationTagMismatchException ex)
+#else
+                catch (CryptographicException ex)
+#endif
+                {
+                    throw new SshConnectionException("MAC error", DisconnectReason.MacError, ex);
+                }
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                base.Dispose(disposing);
+
+                if (disposing)
+                {
+                    _aesGcm.Dispose();
+                }
+            }
+        }
+    }
+}
+#endif

+ 48 - 0
src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.BouncyCastleImpl.cs

@@ -0,0 +1,48 @@
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Modes;
+using Org.BouncyCastle.Crypto.Parameters;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Messages.Transport;
+
+namespace Renci.SshNet.Security.Cryptography.Ciphers
+{
+    internal partial class AesGcmCipher
+    {
+        private sealed class BouncyCastleImpl : Impl
+        {
+            private readonly GcmBlockCipher _cipher;
+            private readonly AeadParameters _parameters;
+
+            public BouncyCastleImpl(byte[] key, byte[] nonce)
+            {
+                _cipher = new GcmBlockCipher(new AesEngine());
+                _parameters = new AeadParameters(new KeyParameter(key), TagSizeInBytes * 8, nonce);
+            }
+
+            public override void Encrypt(byte[] input, int plainTextOffset, int plainTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int cipherTextOffset)
+            {
+                _cipher.Init(forEncryption: true, _parameters);
+                _cipher.ProcessAadBytes(input, associatedDataOffset, associatedDataLength);
+                var cipherTextLength = _cipher.ProcessBytes(input, plainTextOffset, plainTextLength, output, cipherTextOffset);
+                _ = _cipher.DoFinal(output, cipherTextOffset + cipherTextLength);
+            }
+
+            public override void Decrypt(byte[] input, int cipherTextOffset, int cipherTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int plainTextOffset)
+            {
+                _cipher.Init(forEncryption: false, _parameters);
+                _cipher.ProcessAadBytes(input, associatedDataOffset, associatedDataLength);
+                var plainTextLength = _cipher.ProcessBytes(input, cipherTextOffset, cipherTextLength + TagSizeInBytes, output, plainTextOffset);
+                try
+                {
+                    _ = _cipher.DoFinal(output, plainTextLength);
+                }
+                catch (InvalidCipherTextException ex)
+                {
+                    throw new SshConnectionException("MAC error", DisconnectReason.MacError, ex);
+                }
+            }
+        }
+    }
+}

+ 55 - 25
src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs

@@ -1,8 +1,6 @@
-#if NET6_0_OR_GREATER
-using System;
+using System;
 using System.Buffers.Binary;
 using System.Diagnostics;
-using System.Security.Cryptography;
 
 using Renci.SshNet.Common;
 
@@ -12,10 +10,16 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
     /// AES GCM cipher implementation.
     /// <see href="https://datatracker.ietf.org/doc/html/rfc5647"/>.
     /// </summary>
-    internal sealed class AesGcmCipher : SymmetricCipher, IDisposable
+    internal sealed partial class AesGcmCipher : SymmetricCipher, IDisposable
     {
+        private const int PacketLengthFieldLength = 4;
+        private const int TagSizeInBytes = 16;
         private readonly byte[] _iv;
-        private readonly AesGcm _aesGcm;
+#if NET6_0_OR_GREATER
+        private readonly Impl _impl;
+#else
+        private readonly BouncyCastleImpl _impl;
+#endif
 
         /// <summary>
         /// Gets the minimun block size.
@@ -42,7 +46,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         {
             get
             {
-                return 16;
+                return TagSizeInBytes;
             }
         }
 
@@ -56,11 +60,16 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         {
             // SSH AES-GCM requires a 12-octet Initial IV
             _iv = iv.Take(12);
-#if NET8_0_OR_GREATER
-            _aesGcm = new AesGcm(key, TagSize);
-#else
-            _aesGcm = new AesGcm(key);
+#if NET6_0_OR_GREATER
+            if (System.Security.Cryptography.AesGcm.IsSupported)
+            {
+                _impl = new BclImpl(key, _iv);
+            }
+            else
 #endif
+            {
+                _impl = new BouncyCastleImpl(key, _iv);
+            }
         }
 
         /// <summary>
@@ -84,15 +93,17 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// </returns>
         public override byte[] Encrypt(byte[] input, int offset, int length)
         {
-            var packetLengthField = new ReadOnlySpan<byte>(input, offset, 4);
-            var plainText = new ReadOnlySpan<byte>(input, offset + 4, length - 4);
-
             var output = new byte[length + TagSize];
-            packetLengthField.CopyTo(output);
-            var cipherText = new Span<byte>(output, 4, length - 4);
-            var tag = new Span<byte>(output, length, TagSize);
+            Buffer.BlockCopy(input, offset, output, 0, PacketLengthFieldLength);
 
-            _aesGcm.Encrypt(nonce: _iv, plainText, cipherText, tag, associatedData: packetLengthField);
+            _impl.Encrypt(
+                input,
+                plainTextOffset: offset + PacketLengthFieldLength,
+                plainTextLength: length - PacketLengthFieldLength,
+                associatedDataOffset: offset,
+                associatedDataLength: PacketLengthFieldLength,
+                output,
+                cipherTextOffset: PacketLengthFieldLength);
 
             IncrementCounter();
 
@@ -122,14 +133,16 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         {
             Debug.Assert(offset == 8, "The offset must be 8");
 
-            var packetLengthField = new ReadOnlySpan<byte>(input, 4, 4);
-            var cipherText = new ReadOnlySpan<byte>(input, offset, length);
-            var tag = new ReadOnlySpan<byte>(input, offset + length, TagSize);
-
             var output = new byte[length];
-            var plainText = new Span<byte>(output);
 
-            _aesGcm.Decrypt(nonce: _iv, cipherText, tag, plainText, associatedData: packetLengthField);
+            _impl.Decrypt(
+                input,
+                cipherTextOffset: offset,
+                cipherTextLength: length,
+                associatedDataOffset: offset - PacketLengthFieldLength,
+                associatedDataLength: PacketLengthFieldLength,
+                output,
+                plainTextOffset: 0);
 
             IncrementCounter();
 
@@ -158,7 +171,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         {
             if (disposing)
             {
-                _aesGcm.Dispose();
+                _impl.Dispose();
             }
         }
 
@@ -169,6 +182,23 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
             Dispose(disposing: true);
             GC.SuppressFinalize(this);
         }
+
+        private abstract class Impl : IDisposable
+        {
+            public abstract void Encrypt(byte[] input, int plainTextOffset, int plainTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int cipherTextOffset);
+
+            public abstract void Decrypt(byte[] input, int cipherTextOffset, int cipherTextLength, int associatedDataOffset, int associatedDataLength, byte[] output, int plainTextOffset);
+
+            protected virtual void Dispose(bool disposing)
+            {
+            }
+
+            public void Dispose()
+            {
+                // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+                Dispose(disposing: true);
+                GC.SuppressFinalize(this);
+            }
+        }
     }
 }
-#endif

+ 1 - 3
src/Renci.SshNet/Security/KeyExchangeECDH.cs

@@ -46,12 +46,10 @@ namespace Renci.SshNet.Security
                 _impl = new BclImpl(Curve);
             }
             else
+#endif
             {
                 _impl = new BouncyCastleImpl(CurveParameter);
             }
-#else
-            _impl = new BouncyCastleImpl(CurveParameter);
-#endif
 
             _clientExchangeValue = _impl.GenerateClientECPoint();
 

+ 1 - 5
src/Renci.SshNet/Session.cs

@@ -1258,11 +1258,7 @@ namespace Renci.SshNet
                 var plainFirstBlock = firstBlock;
 
                 // First block is not encrypted in AES GCM mode.
-                if (_serverCipher is not null
-#if NET6_0_OR_GREATER
-        and not Security.Cryptography.Ciphers.AesGcmCipher
-#endif
-                    )
+                if (_serverCipher is not null and not Security.Cryptography.Ciphers.AesGcmCipher)
                 {
                     _serverCipher.SetSequenceNumber(_inboundPacketSequence);
 

+ 1 - 2
test/Renci.SshNet.IntegrationTests/CipherTests.cs

@@ -64,7 +64,6 @@ namespace Renci.SshNet.IntegrationTests
             DoTest(Cipher.Aes256Ctr);
         }
 
-#if NET6_0_OR_GREATER
         [TestMethod]
         public void Aes128Gcm()
         {
@@ -76,7 +75,7 @@ namespace Renci.SshNet.IntegrationTests
         {
             DoTest(Cipher.Aes256Gcm);
         }
-#endif
+
         [TestMethod]
         public void ChaCha20Poly1305()
         {