Kaynağa Gözat

Support more ciphers for OpenSSH private key decryption. (#1487)

Scott Xu 1 yıl önce
ebeveyn
işleme
b067e75f18
26 değiştirilmiş dosya ile 375 ekleme ve 75 silme
  1. 13 1
      README.md
  2. 3 3
      src/Renci.SshNet/ConnectionInfo.cs
  3. 100 19
      src/Renci.SshNet/PrivateKeyFile.cs
  4. 16 18
      src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs
  5. 51 31
      src/Renci.SshNet/Security/Cryptography/Ciphers/ChaCha20Poly1305Cipher.cs
  6. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.3Des.CBC.pub
  7. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.3Des.CBC.txt
  8. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CBC.pub
  9. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CBC.txt
  10. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CTR.pub
  11. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CTR.txt
  12. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.GCM.pub
  13. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.GCM.txt
  14. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CBC.pub
  15. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CBC.txt
  16. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CTR.pub
  17. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CTR.txt
  18. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CBC.pub
  19. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CBC.txt
  20. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CTR.pub
  21. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CTR.txt
  22. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.GCM.pub
  23. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.GCM.txt
  24. 1 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.pub
  25. 8 0
      test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.txt
  26. 102 3
      test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs

+ 13 - 1
README.md

@@ -101,7 +101,7 @@ The main types provided by this library are:
 * ECDSA 256/384/521 in OpenSSL PEM format ("BEGIN EC PRIVATE KEY")
 * ECDSA 256/384/521, ED25519 and RSA in OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
 
-Private keys can be encrypted using one of the following cipher methods:
+Private keys in OpenSSL PEM and ssh.com format can be encrypted using one of the following cipher methods:
 * DES-EDE3-CBC
 * DES-EDE3-CFB
 * DES-CBC
@@ -109,6 +109,18 @@ Private keys can be encrypted using one of the following cipher methods:
 * AES-192-CBC
 * AES-256-CBC
 
+Private keys in OpenSSH key format can be encrypted using one of the following cipher methods:
+* 3des-cbc
+* aes128-cbc
+* aes192-cbc
+* aes256-cbc
+* aes128-ctr
+* aes192-ctr
+* aes256-ctr
+* aes128-gcm<span></span>@openssh.com
+* aes256-gcm<span></span>@openssh.com
+* chacha20-poly1305<span></span>@openssh.com
+
 ## Host Key Algorithms
 
 **SSH.NET** supports the following host key algorithms:

+ 3 - 3
src/Renci.SshNet/ConnectionInfo.cs

@@ -386,9 +386,9 @@ namespace Renci.SshNet
                     { "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-gcm@openssh.com", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv, aadLength: 4), isAead: true) },
+                    { "aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv, aadLength: 4), isAead: true) },
+                    { "chacha20-poly1305@openssh.com", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key, aadLength: 4), 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)) },

+ 100 - 19
src/Renci.SshNet/PrivateKeyFile.cs

@@ -39,7 +39,7 @@ namespace Renci.SshNet
     /// </list>
     /// </para>
     /// <para>
-    /// The following encryption algorithms are supported:
+    /// The following encryption algorithms are supported for OpenSSL PEM and ssh.com format:
     /// <list type="bullet">
     ///     <item>
     ///         <description>DES-EDE3-CBC</description>
@@ -60,6 +60,39 @@ namespace Renci.SshNet
     ///         <description>AES-256-CBC</description>
     ///     </item>
     /// </list>
+    /// The following encryption algorithms are supported for OpenSSH format:
+    /// <list type="bullet">
+    ///     <item>
+    ///         <description>3des-cbc</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes128-cbc</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes192-cbc</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes256-cbc</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes128-ctr</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes192-ctr</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes256-ctr</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes128-gcm@openssh.com</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>aes256-gcm@openssh.com</description>
+    ///     </item>
+    ///     <item>
+    ///         <description>chacha20-poly1305@openssh.com</description>
+    ///     </item>
+    /// </list>
     /// </para>
     /// </remarks>
     public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
@@ -450,7 +483,17 @@ namespace Renci.SshNet
 
             var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
 
-            return cipher.Decrypt(cipherData);
+            try
+            {
+                return cipher.Decrypt(cipherData);
+            }
+            finally
+            {
+                if (cipher is IDisposable disposable)
+                {
+                    disposable.Dispose();
+                }
+            }
         }
 
         /// <summary>
@@ -474,7 +517,7 @@ namespace Renci.SshNet
                 throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
             }
 
-            // cipher will be "aes256-cbc" if using a passphrase, "none" otherwise
+            // cipher will be "aes256-cbc" or other cipher if using a passphrase, "none" otherwise
             var cipherName = keyReader.ReadString(Encoding.UTF8);
 
             // key derivation function (kdf): bcrypt or nothing
@@ -503,7 +546,7 @@ namespace Renci.SshNet
 
             // possibly encrypted private key
             var privateKeyLength = (int)keyReader.ReadUInt32();
-            var privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
+            byte[] privateKeyBytes;
 
             // decrypt private key if necessary
             if (cipherName != "none")
@@ -518,38 +561,76 @@ namespace Renci.SshNet
                     throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
                 }
 
-                // inspired by the SSHj library (https://github.com/hierynomus/sshj)
-                // apply the kdf to derive a key and iv from the passphrase
-                var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
-                var keyiv = new byte[48];
-                new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
-                var key = new byte[32];
-                Array.Copy(keyiv, 0, key, 0, 32);
-                var iv = new byte[16];
-                Array.Copy(keyiv, 32, iv, 0, 16);
-
-                AesCipher cipher;
+                var ivLength = 16;
+                CipherInfo cipherInfo;
                 switch (cipherName)
                 {
+                    case "3des-cbc":
+                        ivLength = 8;
+                        cipherInfo = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null));
+                        break;
+                    case "aes128-cbc":
+                        cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
+                        break;
+                    case "aes192-cbc":
+                        cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
+                        break;
                     case "aes256-cbc":
-                        cipher = new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false);
+                        cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
+                        break;
+                    case "aes128-ctr":
+                        cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
+                        break;
+                    case "aes192-ctr":
+                        cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
                         break;
                     case "aes256-ctr":
-                        cipher = new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false);
+                        cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
+                        break;
+                    case "aes128-gcm@openssh.com":
+                        cipherInfo = new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
+                        break;
+                    case "aes256-gcm@openssh.com":
+                        cipherInfo = new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
+                        break;
+                    case "chacha20-poly1305@openssh.com":
+                        ivLength = 12;
+                        cipherInfo = new CipherInfo(256, (key, iv) => new ChaCha20Poly1305Cipher(key, aadLength: 0), isAead: true);
                         break;
                     default:
                         throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
                 }
 
+                var keyLength = cipherInfo.KeySize / 8;
+
+                // inspired by the SSHj library (https://github.com/hierynomus/sshj)
+                // apply the kdf to derive a key and iv from the passphrase
+                var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
+                var keyiv = new byte[keyLength + ivLength];
+                new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
+
+                var key = keyiv.Take(keyLength);
+                var iv = keyiv.Take(keyLength, ivLength);
+
+                var cipher = cipherInfo.Cipher(key, iv);
+                var cipherData = keyReader.ReadBytes(privateKeyLength + cipher.TagSize);
+
                 try
                 {
-                    privateKeyBytes = cipher.Decrypt(privateKeyBytes);
+                    privateKeyBytes = cipher.Decrypt(cipherData, 0, privateKeyLength);
                 }
                 finally
                 {
-                    cipher.Dispose();
+                    if (cipher is IDisposable disposable)
+                    {
+                        disposable.Dispose();
+                    }
                 }
             }
+            else
+            {
+                privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
+            }
 
             // validate private key length
             privateKeyLength = privateKeyBytes.Length;

+ 16 - 18
src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs

@@ -12,9 +12,9 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
     /// </summary>
     internal sealed partial class AesGcmCipher : SymmetricCipher, IDisposable
     {
-        private const int PacketLengthFieldLength = 4;
         private const int TagSizeInBytes = 16;
         private readonly byte[] _iv;
+        private readonly int _aadLength;
 #if NET6_0_OR_GREATER
         private readonly Impl _impl;
 #else
@@ -55,11 +55,13 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// </summary>
         /// <param name="key">The key.</param>
         /// <param name="iv">The IV.</param>
-        public AesGcmCipher(byte[] key, byte[] iv)
+        /// <param name="aadLength">The length of additional associated data.</param>
+        public AesGcmCipher(byte[] key, byte[] iv, int aadLength)
             : base(key)
         {
             // SSH AES-GCM requires a 12-octet Initial IV
             _iv = iv.Take(12);
+            _aadLength = aadLength;
 #if NET6_0_OR_GREATER
             if (System.Security.Cryptography.AesGcm.IsSupported)
             {
@@ -78,8 +80,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <param name="input">
         /// The input data with below format:
         ///   <code>
-        ///   [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
-        ///   [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
+        ///   [----(offset)][----AAD----][----Plain Text----(length)]
         ///   </code>
         /// </param>
         /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
@@ -87,23 +88,22 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <returns>
         /// The encrypted data with below format:
         ///   <code>
-        ///   [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
-        ///   [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
+        ///   [----AAD----][----Cipher Text----][----TAG----]
         ///   </code>
         /// </returns>
         public override byte[] Encrypt(byte[] input, int offset, int length)
         {
             var output = new byte[length + TagSize];
-            Buffer.BlockCopy(input, offset, output, 0, PacketLengthFieldLength);
+            Buffer.BlockCopy(input, offset, output, 0, _aadLength);
 
             _impl.Encrypt(
                 input,
-                plainTextOffset: offset + PacketLengthFieldLength,
-                plainTextLength: length - PacketLengthFieldLength,
+                plainTextOffset: offset + _aadLength,
+                plainTextLength: length - _aadLength,
                 associatedDataOffset: offset,
-                associatedDataLength: PacketLengthFieldLength,
+                associatedDataLength: _aadLength,
                 output,
-                cipherTextOffset: PacketLengthFieldLength);
+                cipherTextOffset: _aadLength);
 
             IncrementCounter();
 
@@ -116,8 +116,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <param name="input">
         /// The input data with below format:
         ///   <code>
-        ///   [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
-        ///   [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
+        ///   [----][----AAD----(offset)][----Cipher Text----(length)][----TAG----]
         ///   </code>
         /// </param>
         /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
@@ -125,13 +124,12 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <returns>
         /// The decrypted data with below format:
         /// <code>
-        ///   [padding length field sz][payload][random paddings]
-        ///   [--------------------Plain Text-------------------]
+        ///   [----Plain Text----]
         /// </code>
         /// </returns>
         public override byte[] Decrypt(byte[] input, int offset, int length)
         {
-            Debug.Assert(offset == 8, "The offset must be 8");
+            Debug.Assert(offset >= _aadLength, "The offset must be greater than or equals to aad length");
 
             var output = new byte[length];
 
@@ -139,8 +137,8 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
                 input,
                 cipherTextOffset: offset,
                 cipherTextLength: length,
-                associatedDataOffset: offset - PacketLengthFieldLength,
-                associatedDataLength: PacketLengthFieldLength,
+                associatedDataOffset: offset - _aadLength,
+                associatedDataLength: _aadLength,
                 output,
                 plainTextOffset: 0);
 

+ 51 - 31
src/Renci.SshNet/Security/Cryptography/Ciphers/ChaCha20Poly1305Cipher.cs

@@ -18,9 +18,13 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
     /// </summary>
     internal sealed class ChaCha20Poly1305Cipher : SymmetricCipher
     {
-        private readonly ChaCha7539Engine _aadCipher = new ChaCha7539Engine();
-        private readonly ChaCha7539Engine _cipher = new ChaCha7539Engine();
-        private readonly Poly1305 _mac = new Poly1305();
+        private readonly byte[] _iv;
+        private readonly int _aadLength;
+        private readonly KeyParameter _aadKeyParameter;
+        private readonly KeyParameter _keyParameter;
+        private readonly ChaCha7539Engine _aadCipher;
+        private readonly ChaCha7539Engine _cipher;
+        private readonly Poly1305 _mac;
 
         /// <summary>
         /// Gets the minimun block size.
@@ -51,9 +55,23 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// Initializes a new instance of the <see cref="ChaCha20Poly1305Cipher"/> class.
         /// </summary>
         /// <param name="key">The key.</param>
-        public ChaCha20Poly1305Cipher(byte[] key)
+        /// <param name="aadLength">The length of additional associated data.</param>
+        public ChaCha20Poly1305Cipher(byte[] key, int aadLength)
             : base(key)
         {
+            _iv = new byte[12];
+            _aadLength = aadLength;
+
+            _keyParameter = new KeyParameter(key, 0, 32);
+            _cipher = new ChaCha7539Engine();
+
+            if (aadLength > 0)
+            {
+                _aadKeyParameter = new KeyParameter(key, 32, 32);
+                _aadCipher = new ChaCha7539Engine();
+            }
+
+            _mac = new Poly1305();
         }
 
         /// <summary>
@@ -62,8 +80,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <param name="input">
         /// The input data with below format:
         ///   <code>
-        ///   [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
-        ///   [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
+        ///   [----(offset)][----AAD----][----Plain Text----(length)]
         ///   </code>
         /// </param>
         /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
@@ -71,16 +88,22 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <returns>
         /// The encrypted data with below format:
         ///   <code>
-        ///   [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
-        ///   [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
+        ///   [----Cipher AAD----][----Cipher Text----][----TAG----]
         ///   </code>
         /// </returns>
         public override byte[] Encrypt(byte[] input, int offset, int length)
         {
+            _aadCipher?.Init(forEncryption: true, new ParametersWithIV(_aadKeyParameter, _iv));
+            _cipher.Init(forEncryption: true, new ParametersWithIV(_keyParameter, _iv));
+
+            var keyStream = new byte[64];
+            _cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
+            _mac.Init(new KeyParameter(keyStream, 0, 32));
+
             var output = new byte[length + TagSize];
 
-            _aadCipher.ProcessBytes(input, offset, 4, output, 0);
-            _cipher.ProcessBytes(input, offset + 4, length - 4, output, 4);
+            _aadCipher?.ProcessBytes(input, offset, _aadLength, output, 0);
+            _cipher.ProcessBytes(input, offset + _aadLength, length - _aadLength, output, _aadLength);
 
             _mac.BlockUpdate(output, 0, length);
             _ = _mac.DoFinal(output, length);
@@ -89,12 +112,16 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         }
 
         /// <summary>
-        /// Decrypts the first block which is packet length field.
+        /// Decrypts the AAD.
         /// </summary>
-        /// <param name="input">The encrypted packet length field.</param>
-        /// <returns>The decrypted packet length field.</returns>
+        /// <param name="input">The encrypted AAD.</param>
+        /// <returns>The decrypted AAD.</returns>
         public override byte[] Decrypt(byte[] input)
         {
+            Debug.Assert(_aadCipher != null, "The aadCipher must not be null");
+
+            _aadCipher.Init(forEncryption: false, new ParametersWithIV(_aadKeyParameter, _iv));
+
             var output = new byte[input.Length];
             _aadCipher.ProcessBytes(input, 0, input.Length, output, 0);
 
@@ -107,8 +134,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <param name="input">
         /// The input data with below format:
         ///   <code>
-        ///   [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
-        ///   [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
+        ///   [----][----Cipher AAD----(offset)][----Cipher Text----(length)][----TAG----]
         ///   </code>
         /// </param>
         /// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
@@ -116,16 +142,21 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
         /// <returns>
         /// The decrypted data with below format:
         /// <code>
-        ///   [padding length field sz][payload][random paddings]
-        ///   [--------------------Plain Text-------------------]
+        ///   [----Plain Text----]
         /// </code>
         /// </returns>
         public override byte[] Decrypt(byte[] input, int offset, int length)
         {
-            Debug.Assert(offset == 8, "The offset must be 8");
+            Debug.Assert(offset >= _aadLength, "The offset must be greater than or equals to aad length");
+
+            _cipher.Init(forEncryption: false, new ParametersWithIV(_keyParameter, _iv));
+
+            var keyStream = new byte[64];
+            _cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
+            _mac.Init(new KeyParameter(keyStream, 0, 32));
 
             var tag = new byte[TagSize];
-            _mac.BlockUpdate(input, offset - 4, length + 4);
+            _mac.BlockUpdate(input, offset - _aadLength, length + _aadLength);
             _ = _mac.DoFinal(tag, 0);
             if (!Arrays.FixedTimeEquals(TagSize, tag, 0, input, offset + length))
             {
@@ -140,18 +171,7 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers
 
         internal override void SetSequenceNumber(uint sequenceNumber)
         {
-            var iv = new byte[12];
-            BinaryPrimitives.WriteUInt64BigEndian(iv.AsSpan(4), sequenceNumber);
-
-            // ChaCha20 encryption and decryption is completely
-            // symmetrical, so the 'forEncryption' is
-            // irrelevant. (Like 90% of stream ciphers)
-            _aadCipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 32, 32), iv));
-            _cipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 0, 32), iv));
-
-            var keyStream = new byte[64];
-            _cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
-            _mac.Init(new KeyParameter(keyStream, 0, 32));
+            BinaryPrimitives.WriteUInt64BigEndian(_iv.AsSpan(4), sequenceNumber);
         }
     }
 }

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.3Des.CBC.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC5fr8EY9Gb65zsvrCsGDF7+AXJgDJrAkvBAFDiYBBiX SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.3Des.CBC.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACDNkZXMtY2JjAAAABmJjcnlwdAAAABgAAAAQEMaDlDt8i7
+NL7keWLs6LGAAAABAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAuX6/BGPRm+uc7L6wr
+Bgxe/gFyYAyawJLwQBQ4mAQYlwAAAJC/OWi6TtpChMbEYxOK4TuUencG4ULol0k3hJ4905
+LI0+etT6s5fqr8W3D93A6ElxGLtxtkhgLfhTC4DIo4fPxD7mHNKtjbbyJn2oqzxqUlvETS
+d9gg7Ph+zw4a+/GMhhIlPc79D6QxISSNrdtFccvLwTVukhkm04OGIyNJzE5qOz4g78lejZ
+pKjFzVLY+WjSg=
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CBC.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMYdAxNcki2uVihPLcIxl6Zk27MBZdI2ef7NBMIYFHtW SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CBC.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jYmMAAAAGYmNyeXB0AAAAGAAAABApmviQDZ
+dmF4eaa1glB0maAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMYdAxNcki2uVihP
+LcIxl6Zk27MBZdI2ef7NBMIYFHtWAAAAkHLroaoIoLkV3nmNck418+ndWFFEsvbk3AURyG
+3fZboIGmjTCFGWDigRmB/w75jKwurLdfXvORWMxqaxZ0sjnHFwt0i/pumOv61nFC0timVV
+VSoCWcAgyBsl2+lnRhnORlY5THVvgU6zF6p8Mf8ONMT59HXRxc9VMRc+eJhY3wb3q0BIN+
+vSQ7PXvnKrq7/mbA==
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CTR.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP44SYIMQiq6RfzllvHztr7ATNkXbAFqFZtukvHa0VaU SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.CTR.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jdHIAAAAGYmNyeXB0AAAAGAAAABAGS7ibrm
+I3evObqzYMOyFsAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIP44SYIMQiq6Rfzl
+lvHztr7ATNkXbAFqFZtukvHa0VaUAAAAkHezd7R3B/U5jmRG9ayxcB5v9eIjjM2eZ/7O6z
+7waNtAIO36Ve+BY5qIduP4t8qdZ1JDFHaPxx/WLqezfV0hqRLzR4Pm/bcAHN60610x0BSF
+xHDC7nFej/X4Sr0SEplqsCfqfk5B4wsmdLUGwvpxIqoUCsCLx4YVaAlG/OezZUJ6b6lViZ
+zcaaG2MGRlm28f9Q==
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.GCM.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJrCQwF23e9NNyfNKyYFhmbAme9DahSl8S7IVdjHFUZk SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.128.GCM.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAAFmFlczEyOC1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA
+AAGAAAABAVgfqUe6lBEiQrnz6KqwwkAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA
+IJrCQwF23e9NNyfNKyYFhmbAme9DahSl8S7IVdjHFUZkAAAAkJn29lzPneR2zVKGlR1+mw
+qDiwgKDEHdGVC/LgNQwxsaOryYEwA8fK0pBx4Ai+oRJZIAL430ZvwXSNrbkRo6oLkOouxH
+0Cx9Uq4ETgumAAMAuyPecU8POBTUQUJsbA35EJZofxceVOa8iKleGSEKzVlQeSESY78AFH
+U3bsmAJ21q3daVGXKrH5RIi3vSDE5IdRWMqonvhXSSLk8HADgR4Kw=
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CBC.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPjSxcsIj5ycJxIhhcMQuJAYTgANIXUa4Y1WEiOrblXA SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CBC.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczE5Mi1jYmMAAAAGYmNyeXB0AAAAGAAAABCO+DfG2m
+YhNIGKmiDRH4cTAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIPjSxcsIj5ycJxIh
+hcMQuJAYTgANIXUa4Y1WEiOrblXAAAAAkC9fsItDX1HaNpw9PBxBX/Eedlu72MGGO7osjJ
+QtuvNb5VhcR07iYhxyw97F7MeraRYNvLrWQyxURB1BkZaSCemBrQJ/ljgEOhU5IgqmlooI
+T4x5BuChnet8HfPJ5Ws9fd5WMOtfMpdO8ZHkJM5VPiSUUhfgWSV0YGKY4Q7luDcpLHzBZk
+ZpossNHwNKsBMC9g==
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CTR.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIERKx068MaW5RqzljZBz1nZw6OIzp4zyUjTKlJQ45wAg SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.192.CTR.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczE5Mi1jdHIAAAAGYmNyeXB0AAAAGAAAABCV/tolDk
+gwraQcGNlFOu+GAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIERKx068MaW5Rqzl
+jZBz1nZw6OIzp4zyUjTKlJQ45wAgAAAAkGnQfFCyY4tOQO+dNs+1Zzt6l5UbRHMjECnvuF
+M3P5oFi0FXRSNSODXvZzZWCn9EVtaICV0bP+UKx9SfVAAkS64ZHl8n0IlRI9PmmthP6yZr
+N7RzXaejppY/ZqQm+yH7S1cNb9KsAEIGMUlws34KlPCitc1HKJu8r9UmQGXaXXur/l47f5
+AVP+RmKjdDZy7FvA==
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CBC.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC+D1UJTH4n4ipIrHFeBBVjDJkhnjFzhZfewDxNAg5hb SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CBC.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABAwPJUhck
+LArH2ovPv8EjcMAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIC+D1UJTH4n4ipIr
+HFeBBVjDJkhnjFzhZfewDxNAg5hbAAAAkIrxGvN1uUkltnk7X6fktv3KWQVL1oSuJ10kK8
+HY854Dp8qbGi0sFPcBrX/0y7kTyN58L72UWzjWwUZXrG/n6mb+PHXQbfr4MO5R/+BMnIA0
+0NmEj4nzY/9WCtU7En30zv5IF1MiLU2x7qPBqwdkOIKkTz+cTOXo0PCqT1s/Y2cbMUarS/
+cPdZ1FL5JwOcbqvQ==
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CTR.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJiK1qYA1G7CJwZrDhleCnSXM0YgZtVA4lEqDBlrV6LE SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.CTR.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCLdsObaY
+r3GiQxDA6fA1AlAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIJiK1qYA1G7CJwZr
+DhleCnSXM0YgZtVA4lEqDBlrV6LEAAAAkClmUCCE7AqpnGJCaI44L/mCRBVMCEXi7O9FNG
+VLklY6VJV//S77A696OKqeEZxv17l9VnkL+KIbn5dgf05JzMAXFGgaUO9xsz++21A/3bfF
+lQ1oghMQPN6iXVg2eVTTzLrlxW8+9wQRr+RZ+38rL55fnU187f/evLGajjJdVhtzw/PvKg
+sCJ4xZHaGOUR6OxQ==
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.GCM.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKs32WTgxZkPlc1Xg4q6m9H2MRsMYDOuRXXXKNa8sBIM scott@SCOTTLAPTOP

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.Aes.256.GCM.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAAFmFlczI1Ni1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA
+AAGAAAABDlF34NcdHSHJFFHrK8PK/FAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA
+IGgS0NrTKMQcchUDkewJOtOC5J+nmrdny2rxCN0koPHdAAAAkHCy69z0kHw/kCs4a+LCBn
+2R37rprUEqHSiykVEPKxRDqpjqitaVNGzvPo6uhUclW9xxsAufMYv+Mn/Rz5ZLqHSV1Jio
+zdBZrAkM13DJpW6xKVjbjGTr7zXpnjr1dgMs1tmq/T9F503Dky84u9qA+jUVczFitWuwvn
+JMHIJ7zgAI2fp5z+aq51lH9rLAp6Vmjoal5MRsZhIHBrrwheBEWPE=
+-----END OPENSSH PRIVATE KEY-----

+ 1 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.pub

@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBquS79wnun0ksZv6JJMgHzoZhzqH6Lkft+1sHTHFOjY SSH.NET

+ 8 - 0
test/Data/Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.txt

@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm
+JjcnlwdAAAABgAAAAQoCDdnsgOWkWp01akN/vJjwAAABAAAAABAAAAMwAAAAtzc2gtZWQy
+NTUxOQAAACAarku/cJ7p9JLGb+iSTIB86GYc6h+i5H7ftbB0xxTo2AAAAJDUnN2iwK9ZgA
+fpExo5jBaxI90HMxcUmgUCc4H7fTOqKousn4oyK9SfXH66IS9S1id7D+6KfVHBnmF+aH5q
+g75pAGaHUGncm5PY3Lfun4KD5mDbN5cnloFDKb+z4pJq6FhpCeg9GyiYbFeIz8HrDeEugK
+RKeq5tKWibhDLX6Ywp0+gQH1Rb14atw2Yy1zV9KX900we18Jjhl7R+FvkrFkUC
+-----END OPENSSH PRIVATE KEY-----

+ 102 - 3
test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs

@@ -562,11 +562,110 @@ namespace Renci.SshNet.Tests.Classes
         }
 
         [TestMethod()]
-        [Owner("bhalbright")]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_3DES_CBC()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.3Des.CBC.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_128_CBC()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.128.CBC.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_192_CBC()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.192.CBC.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_256_CBC()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.256.CBC.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_128_CTR()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.128.CTR.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_192_CTR()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.192.CTR.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_256_CTR()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.256.CTR.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_128_GCM()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.128.GCM.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
+        [TestCategory("PrivateKey")]
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_AES_256_GCM()
+        {
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.Aes.256.GCM.txt"))
+            {
+                _ = new PrivateKeyFile(stream, "12345");
+            }
+        }
+
+        [TestMethod()]
+        [Owner("scott-xu")]
         [TestCategory("PrivateKey")]
-        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED()
+        public void Test_PrivateKey_OPENSSH_ED25519_ENCRYPTED_ChaCha20_Poly1305()
         {
-            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.txt"))
+            using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.ChaCha20.Poly1305.txt"))
             {
                 _ = new PrivateKeyFile(stream, "12345");
             }