|
@@ -3,7 +3,6 @@ using System;
|
|
|
using System.Collections.Generic;
|
|
using System.Collections.Generic;
|
|
|
using System.Diagnostics;
|
|
using System.Diagnostics;
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
-using System.Formats.Asn1;
|
|
|
|
|
using System.Globalization;
|
|
using System.Globalization;
|
|
|
using System.IO;
|
|
using System.IO;
|
|
|
using System.Linq;
|
|
using System.Linq;
|
|
@@ -12,17 +11,9 @@ using System.Security.Cryptography;
|
|
|
using System.Text;
|
|
using System.Text;
|
|
|
using System.Text.RegularExpressions;
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
|
|
-using Org.BouncyCastle.Asn1.EdEC;
|
|
|
|
|
-using Org.BouncyCastle.Asn1.Pkcs;
|
|
|
|
|
-using Org.BouncyCastle.Asn1.X9;
|
|
|
|
|
-using Org.BouncyCastle.Pkcs;
|
|
|
|
|
-
|
|
|
|
|
using Renci.SshNet.Common;
|
|
using Renci.SshNet.Common;
|
|
|
using Renci.SshNet.Security;
|
|
using Renci.SshNet.Security;
|
|
|
using Renci.SshNet.Security.Cryptography;
|
|
using Renci.SshNet.Security.Cryptography;
|
|
|
-using Renci.SshNet.Security.Cryptography.Ciphers;
|
|
|
|
|
-using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
|
|
|
|
|
-using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
|
|
|
|
|
|
|
|
|
|
namespace Renci.SshNet
|
|
namespace Renci.SshNet
|
|
|
{
|
|
{
|
|
@@ -305,606 +296,53 @@ namespace Renci.SshNet
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var keyName = privateKeyMatch.Result("${keyName}");
|
|
var keyName = privateKeyMatch.Result("${keyName}");
|
|
|
- if (!keyName.EndsWith("PRIVATE KEY", StringComparison.Ordinal))
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("Invalid private key file.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var cipherName = privateKeyMatch.Result("${cipherName}");
|
|
|
|
|
- var salt = privateKeyMatch.Result("${salt}");
|
|
|
|
|
var data = privateKeyMatch.Result("${data}");
|
|
var data = privateKeyMatch.Result("${data}");
|
|
|
-
|
|
|
|
|
var binaryData = Convert.FromBase64String(data);
|
|
var binaryData = Convert.FromBase64String(data);
|
|
|
|
|
|
|
|
- byte[] decryptedData;
|
|
|
|
|
-
|
|
|
|
|
- if (!string.IsNullOrEmpty(cipherName) && !string.IsNullOrEmpty(salt))
|
|
|
|
|
- {
|
|
|
|
|
- if (string.IsNullOrEmpty(passPhrase))
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var binarySalt = new byte[salt.Length / 2];
|
|
|
|
|
- for (var i = 0; i < binarySalt.Length; i++)
|
|
|
|
|
- {
|
|
|
|
|
- binarySalt[i] = Convert.ToByte(salt.Substring(i * 2, 2), 16);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- CipherInfo cipher;
|
|
|
|
|
- switch (cipherName)
|
|
|
|
|
- {
|
|
|
|
|
- case "DES-EDE3-CBC":
|
|
|
|
|
- cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
|
|
|
|
|
- break;
|
|
|
|
|
- case "DES-EDE3-CFB":
|
|
|
|
|
- cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CfbCipherMode(iv), new PKCS7Padding()));
|
|
|
|
|
- break;
|
|
|
|
|
- case "DES-CBC":
|
|
|
|
|
- cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
|
|
|
|
|
- break;
|
|
|
|
|
- case "AES-128-CBC":
|
|
|
|
|
- cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
|
|
|
|
|
- break;
|
|
|
|
|
- case "AES-192-CBC":
|
|
|
|
|
- cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
|
|
|
|
|
- break;
|
|
|
|
|
- case "AES-256-CBC":
|
|
|
|
|
- cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
|
|
|
|
|
- break;
|
|
|
|
|
- default:
|
|
|
|
|
- throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- decryptedData = binaryData;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ IPrivateKeyParser parser;
|
|
|
switch (keyName)
|
|
switch (keyName)
|
|
|
{
|
|
{
|
|
|
case "RSA PRIVATE KEY":
|
|
case "RSA PRIVATE KEY":
|
|
|
- var rsaKey = new RsaKey(decryptedData);
|
|
|
|
|
- _key = rsaKey;
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
|
|
|
|
|
-#pragma warning disable CA2000 // Dispose objects before losing scope
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
|
|
|
|
|
-#pragma warning restore CA2000 // Dispose objects before losing scope
|
|
|
|
|
- break;
|
|
|
|
|
case "DSA PRIVATE KEY":
|
|
case "DSA PRIVATE KEY":
|
|
|
- _key = new DsaKey(decryptedData);
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
|
|
|
|
|
- break;
|
|
|
|
|
case "EC PRIVATE KEY":
|
|
case "EC PRIVATE KEY":
|
|
|
- _key = new EcdsaKey(decryptedData);
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
|
|
|
|
|
|
|
+ var cipherName = privateKeyMatch.Result("${cipherName}");
|
|
|
|
|
+ var salt = privateKeyMatch.Result("${salt}");
|
|
|
|
|
+ parser = new PKCS1(cipherName, salt, keyName, binaryData, passPhrase);
|
|
|
break;
|
|
break;
|
|
|
case "PRIVATE KEY":
|
|
case "PRIVATE KEY":
|
|
|
- var privateKeyInfo = PrivateKeyInfo.GetInstance(binaryData);
|
|
|
|
|
- _key = ParseOpenSslPkcs8PrivateKey(privateKeyInfo);
|
|
|
|
|
- if (_key is RsaKey parsedRsaKey)
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
|
|
|
|
|
-#pragma warning disable CA2000 // Dispose objects before losing scope
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA512)));
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA256)));
|
|
|
|
|
-#pragma warning restore CA2000 // Dispose objects before losing scope
|
|
|
|
|
- }
|
|
|
|
|
- else if (_key is DsaKey parsedDsaKey)
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ parser = new PKCS8(encrypted: false, binaryData, passPhrase);
|
|
|
break;
|
|
break;
|
|
|
case "ENCRYPTED PRIVATE KEY":
|
|
case "ENCRYPTED PRIVATE KEY":
|
|
|
- var encryptedPrivateKeyInfo = EncryptedPrivateKeyInfo.GetInstance(binaryData);
|
|
|
|
|
- privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(passPhrase?.ToCharArray(), encryptedPrivateKeyInfo);
|
|
|
|
|
- _key = ParseOpenSslPkcs8PrivateKey(privateKeyInfo);
|
|
|
|
|
- if (_key is RsaKey parsedRsaKey2)
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
|
|
|
|
|
-#pragma warning disable CA2000 // Dispose objects before losing scope
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey2, HashAlgorithmName.SHA512)));
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey2, HashAlgorithmName.SHA256)));
|
|
|
|
|
-#pragma warning restore CA2000 // Dispose objects before losing scope
|
|
|
|
|
- }
|
|
|
|
|
- else if (_key is DsaKey parsedDsaKey)
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ parser = new PKCS8(encrypted: true, binaryData, passPhrase);
|
|
|
break;
|
|
break;
|
|
|
case "OPENSSH PRIVATE KEY":
|
|
case "OPENSSH PRIVATE KEY":
|
|
|
- _key = ParseOpenSshV1Key(decryptedData, passPhrase);
|
|
|
|
|
- if (_key is RsaKey parsedRsaKey3)
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
|
|
|
|
|
-#pragma warning disable CA2000 // Dispose objects before losing scope
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey3, HashAlgorithmName.SHA512)));
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey3, HashAlgorithmName.SHA256)));
|
|
|
|
|
-#pragma warning restore CA2000 // Dispose objects before losing scope
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ parser = new OpenSSH(binaryData, passPhrase);
|
|
|
break;
|
|
break;
|
|
|
case "SSH2 ENCRYPTED PRIVATE KEY":
|
|
case "SSH2 ENCRYPTED PRIVATE KEY":
|
|
|
- var reader = new SshDataReader(decryptedData);
|
|
|
|
|
- var magicNumber = reader.ReadUInt32();
|
|
|
|
|
- if (magicNumber != 0x3f6ff9eb)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("Invalid SSH2 private key.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- _ = reader.ReadUInt32(); // Read total bytes length including magic number
|
|
|
|
|
- var keyType = reader.ReadString(SshData.Ascii);
|
|
|
|
|
- var ssh2CipherName = reader.ReadString(SshData.Ascii);
|
|
|
|
|
- var blobSize = (int)reader.ReadUInt32();
|
|
|
|
|
-
|
|
|
|
|
- byte[] keyData;
|
|
|
|
|
- if (ssh2CipherName == "none")
|
|
|
|
|
- {
|
|
|
|
|
- keyData = reader.ReadBytes(blobSize);
|
|
|
|
|
- }
|
|
|
|
|
- else if (ssh2CipherName == "3des-cbc")
|
|
|
|
|
- {
|
|
|
|
|
- if (string.IsNullOrEmpty(passPhrase))
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var key = GetCipherKey(passPhrase, 192 / 8);
|
|
|
|
|
- var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
|
|
|
|
|
- keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException(string.Format("Cipher method '{0}' is not supported.", ssh2CipherName));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /*
|
|
|
|
|
- * TODO: Create two specific data types to avoid using SshDataReader class.
|
|
|
|
|
- */
|
|
|
|
|
-
|
|
|
|
|
- reader = new SshDataReader(keyData);
|
|
|
|
|
-
|
|
|
|
|
- var decryptedLength = reader.ReadUInt32();
|
|
|
|
|
-
|
|
|
|
|
- if (decryptedLength > blobSize - 4)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("Invalid passphrase.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (keyType.Contains("rsa"))
|
|
|
|
|
- {
|
|
|
|
|
- var exponent = reader.ReadBigIntWithBits(); // e
|
|
|
|
|
- var d = reader.ReadBigIntWithBits(); // d
|
|
|
|
|
- var modulus = reader.ReadBigIntWithBits(); // n
|
|
|
|
|
- var inverseQ = reader.ReadBigIntWithBits(); // u
|
|
|
|
|
- var q = reader.ReadBigIntWithBits(); // p
|
|
|
|
|
- var p = reader.ReadBigIntWithBits(); // q
|
|
|
|
|
- var decryptedRsaKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
|
|
|
|
|
- _key = decryptedRsaKey;
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
|
|
|
|
|
-#pragma warning disable CA2000 // Dispose objects before losing scope
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA512)));
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA256)));
|
|
|
|
|
-#pragma warning restore CA2000 // Dispose objects before losing scope
|
|
|
|
|
- }
|
|
|
|
|
- else if (keyType.Contains("dsa"))
|
|
|
|
|
- {
|
|
|
|
|
- var zero = reader.ReadUInt32();
|
|
|
|
|
- if (zero != 0)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("Invalid private key");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var p = reader.ReadBigIntWithBits();
|
|
|
|
|
- var g = reader.ReadBigIntWithBits();
|
|
|
|
|
- var q = reader.ReadBigIntWithBits();
|
|
|
|
|
- var y = reader.ReadBigIntWithBits();
|
|
|
|
|
- var x = reader.ReadBigIntWithBits();
|
|
|
|
|
- _key = new DsaKey(p, q, g, y, x);
|
|
|
|
|
- _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
- {
|
|
|
|
|
- throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ parser = new SSHCOM(binaryData, passPhrase);
|
|
|
break;
|
|
break;
|
|
|
default:
|
|
default:
|
|
|
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
|
|
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private static byte[] GetCipherKey(string passphrase, int length)
|
|
|
|
|
- {
|
|
|
|
|
- var cipherKey = new List<byte>();
|
|
|
|
|
-
|
|
|
|
|
-#pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
|
|
|
|
|
- using (var md5 = MD5.Create())
|
|
|
|
|
- {
|
|
|
|
|
- var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
|
|
|
|
|
-
|
|
|
|
|
- var hash = md5.ComputeHash(passwordBytes);
|
|
|
|
|
- cipherKey.AddRange(hash);
|
|
|
|
|
-
|
|
|
|
|
- while (cipherKey.Count < length)
|
|
|
|
|
- {
|
|
|
|
|
- hash = passwordBytes.Concat(hash);
|
|
|
|
|
- hash = md5.ComputeHash(hash);
|
|
|
|
|
- cipherKey.AddRange(hash);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-#pragma warning restore CA1850 // Prefer static HashData method
|
|
|
|
|
-
|
|
|
|
|
- return cipherKey.ToArray().Take(length);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// Decrypts encrypted private key file data.
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- /// <param name="cipherInfo">The cipher info.</param>
|
|
|
|
|
- /// <param name="cipherData">Encrypted data.</param>
|
|
|
|
|
- /// <param name="passPhrase">Decryption pass phrase.</param>
|
|
|
|
|
- /// <param name="binarySalt">Decryption binary salt.</param>
|
|
|
|
|
- /// <returns>Decrypted byte array.</returns>
|
|
|
|
|
- /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is <see langword="null"/>.</exception>
|
|
|
|
|
- private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
|
|
|
|
|
- {
|
|
|
|
|
- Debug.Assert(cipherInfo != null);
|
|
|
|
|
- Debug.Assert(cipherData != null);
|
|
|
|
|
- Debug.Assert(binarySalt != null);
|
|
|
|
|
-
|
|
|
|
|
- var cipherKey = new List<byte>();
|
|
|
|
|
-
|
|
|
|
|
-#pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
|
|
|
|
|
- using (var md5 = MD5.Create())
|
|
|
|
|
- {
|
|
|
|
|
- var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
|
|
|
|
|
-
|
|
|
|
|
- // Use 8 bytes binary salt
|
|
|
|
|
- var initVector = passwordBytes.Concat(binarySalt.Take(8));
|
|
|
|
|
-
|
|
|
|
|
- var hash = md5.ComputeHash(initVector);
|
|
|
|
|
- cipherKey.AddRange(hash);
|
|
|
|
|
-
|
|
|
|
|
- while (cipherKey.Count < cipherInfo.KeySize / 8)
|
|
|
|
|
- {
|
|
|
|
|
- hash = hash.Concat(initVector);
|
|
|
|
|
- hash = md5.ComputeHash(hash);
|
|
|
|
|
- cipherKey.AddRange(hash);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-#pragma warning restore CA1850 // Prefer static HashData method
|
|
|
|
|
-
|
|
|
|
|
- var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
|
|
|
|
|
-
|
|
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- return cipher.Decrypt(cipherData);
|
|
|
|
|
- }
|
|
|
|
|
- finally
|
|
|
|
|
- {
|
|
|
|
|
- if (cipher is IDisposable disposable)
|
|
|
|
|
- {
|
|
|
|
|
- disposable.Dispose();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// Parses an OpenSSH V1 key file according to the key spec:
|
|
|
|
|
- /// <see href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key"/>.
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- /// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
|
|
|
|
|
- /// <param name="passPhrase">Passphrase or <see langword="null"/> if there isn't one.</param>
|
|
|
|
|
- /// <returns>
|
|
|
|
|
- /// The OpenSSH V1 key.
|
|
|
|
|
- /// </returns>
|
|
|
|
|
- private static Key ParseOpenSshV1Key(byte[] keyFileData, string? passPhrase)
|
|
|
|
|
- {
|
|
|
|
|
- var keyReader = new SshDataReader(keyFileData);
|
|
|
|
|
-
|
|
|
|
|
- // check magic header
|
|
|
|
|
- var authMagic = "openssh-key-v1\0"u8;
|
|
|
|
|
- var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length);
|
|
|
|
|
- if (!authMagic.SequenceEqual(keyHeaderBytes))
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 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
|
|
|
|
|
- var kdfName = keyReader.ReadString(Encoding.UTF8);
|
|
|
|
|
|
|
+ _key = parser.Parse();
|
|
|
|
|
|
|
|
- // kdf options length: 24 if passphrase, 0 if no passphrase
|
|
|
|
|
- var kdfOptionsLen = (int)keyReader.ReadUInt32();
|
|
|
|
|
- byte[]? salt = null;
|
|
|
|
|
- var rounds = 0;
|
|
|
|
|
- if (kdfOptionsLen > 0)
|
|
|
|
|
|
|
+ if (_key is RsaKey rsaKey)
|
|
|
{
|
|
{
|
|
|
- var saltLength = (int)keyReader.ReadUInt32();
|
|
|
|
|
- salt = keyReader.ReadBytes(saltLength);
|
|
|
|
|
- rounds = (int)keyReader.ReadUInt32();
|
|
|
|
|
|
|
+ _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
|
|
|
|
|
+#pragma warning disable CA2000 // Dispose objects before losing scope
|
|
|
|
|
+ _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
|
|
|
|
|
+ _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
|
|
|
|
|
+#pragma warning restore CA2000 // Dispose objects before losing scope
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // number of public keys, only supporting 1 for now
|
|
|
|
|
- var numberOfPublicKeys = (int)keyReader.ReadUInt32();
|
|
|
|
|
- if (numberOfPublicKeys != 1)
|
|
|
|
|
|
|
+ else if (_key is DsaKey)
|
|
|
{
|
|
{
|
|
|
- throw new SshException("At this time only one public key in the openssh key is supported.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // read public key in ssh-format, but we dont need it
|
|
|
|
|
- _ = keyReader.ReadString(Encoding.UTF8);
|
|
|
|
|
-
|
|
|
|
|
- // possibly encrypted private key
|
|
|
|
|
- var privateKeyLength = (int)keyReader.ReadUInt32();
|
|
|
|
|
- byte[] privateKeyBytes;
|
|
|
|
|
-
|
|
|
|
|
- // decrypt private key if necessary
|
|
|
|
|
- if (cipherName != "none")
|
|
|
|
|
- {
|
|
|
|
|
- if (string.IsNullOrEmpty(passPhrase))
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (string.IsNullOrEmpty(kdfName) || kdfName != "bcrypt")
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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":
|
|
|
|
|
- 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":
|
|
|
|
|
- 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);
|
|
|
|
|
-
|
|
|
|
|
- // The authentication tag data (if any) is concatenated to the end of the encrypted private key string.
|
|
|
|
|
- // See https://github.com/openssh/openssh-portable/blob/509b757c052ea969b3a41fc36818b44801caf1cf/sshkey.c#L2951
|
|
|
|
|
- // and https://github.com/openssh/openssh-portable/blob/509b757c052ea969b3a41fc36818b44801caf1cf/cipher.c#L340
|
|
|
|
|
- var cipherData = keyReader.ReadBytes(privateKeyLength + cipher.TagSize);
|
|
|
|
|
-
|
|
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- privateKeyBytes = cipher.Decrypt(cipherData, 0, privateKeyLength);
|
|
|
|
|
- }
|
|
|
|
|
- finally
|
|
|
|
|
- {
|
|
|
|
|
- if (cipher is IDisposable disposable)
|
|
|
|
|
- {
|
|
|
|
|
- disposable.Dispose();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
|
{
|
|
{
|
|
|
- privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // validate private key length
|
|
|
|
|
- privateKeyLength = privateKeyBytes.Length;
|
|
|
|
|
- if (privateKeyLength % 8 != 0)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("The private key section must be a multiple of the block size (8)");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // now parse the data we called the private key, it actually contains the public key again
|
|
|
|
|
- // so we need to parse through it to get the private key bytes, plus there's some
|
|
|
|
|
- // validation we need to do.
|
|
|
|
|
- var privateKeyReader = new SshDataReader(privateKeyBytes);
|
|
|
|
|
-
|
|
|
|
|
- // check ints should match, they wouldn't match for example if the wrong passphrase was supplied
|
|
|
|
|
- var checkInt1 = (int)privateKeyReader.ReadUInt32();
|
|
|
|
|
- var checkInt2 = (int)privateKeyReader.ReadUInt32();
|
|
|
|
|
- if (checkInt1 != checkInt2)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException(string.Format(CultureInfo.InvariantCulture,
|
|
|
|
|
- "The random check bytes of the OpenSSH key do not match ({0} <-> {1}).",
|
|
|
|
|
- checkInt1.ToString(CultureInfo.InvariantCulture),
|
|
|
|
|
- checkInt2.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // key type
|
|
|
|
|
- var keyType = privateKeyReader.ReadString(Encoding.UTF8);
|
|
|
|
|
-
|
|
|
|
|
- Key parsedKey;
|
|
|
|
|
- byte[] publicKey;
|
|
|
|
|
- byte[] unencryptedPrivateKey;
|
|
|
|
|
- switch (keyType)
|
|
|
|
|
- {
|
|
|
|
|
- case "ssh-ed25519":
|
|
|
|
|
- // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-11#section-3.2.3
|
|
|
|
|
-
|
|
|
|
|
- // ENC(A)
|
|
|
|
|
- _ = privateKeyReader.ReadBignum2();
|
|
|
|
|
-
|
|
|
|
|
- // k || ENC(A)
|
|
|
|
|
- unencryptedPrivateKey = privateKeyReader.ReadBignum2();
|
|
|
|
|
- parsedKey = new ED25519Key(unencryptedPrivateKey);
|
|
|
|
|
- break;
|
|
|
|
|
- case "ecdsa-sha2-nistp256":
|
|
|
|
|
- case "ecdsa-sha2-nistp384":
|
|
|
|
|
- case "ecdsa-sha2-nistp521":
|
|
|
|
|
- // curve
|
|
|
|
|
- var len = (int)privateKeyReader.ReadUInt32();
|
|
|
|
|
- var curve = Encoding.ASCII.GetString(privateKeyReader.ReadBytes(len));
|
|
|
|
|
-
|
|
|
|
|
- // public key
|
|
|
|
|
- publicKey = privateKeyReader.ReadBignum2();
|
|
|
|
|
-
|
|
|
|
|
- // private key
|
|
|
|
|
- unencryptedPrivateKey = privateKeyReader.ReadBignum2();
|
|
|
|
|
- parsedKey = new EcdsaKey(curve, publicKey, unencryptedPrivateKey.TrimLeadingZeros());
|
|
|
|
|
- break;
|
|
|
|
|
- case "ssh-rsa":
|
|
|
|
|
- var modulus = privateKeyReader.ReadBignum(); // n
|
|
|
|
|
- var exponent = privateKeyReader.ReadBignum(); // e
|
|
|
|
|
- var d = privateKeyReader.ReadBignum(); // d
|
|
|
|
|
- var inverseQ = privateKeyReader.ReadBignum(); // iqmp
|
|
|
|
|
- var p = privateKeyReader.ReadBignum(); // p
|
|
|
|
|
- var q = privateKeyReader.ReadBignum(); // q
|
|
|
|
|
- parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
|
|
|
|
|
- break;
|
|
|
|
|
- default:
|
|
|
|
|
- throw new SshException("OpenSSH key type '" + keyType + "' is not supported.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- parsedKey.Comment = privateKeyReader.ReadString(Encoding.UTF8);
|
|
|
|
|
-
|
|
|
|
|
- // The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
|
|
|
|
|
- // until the total length is a multiple of the cipher block size.
|
|
|
|
|
- var padding = privateKeyReader.ReadBytes();
|
|
|
|
|
- for (var i = 0; i < padding.Length; i++)
|
|
|
|
|
- {
|
|
|
|
|
- if ((int)padding[i] != i + 1)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("Padding of openssh key format contained wrong byte at position: " +
|
|
|
|
|
- i.ToString(CultureInfo.InvariantCulture));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return parsedKey;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// Parses an OpenSSL PKCS#8 key file according to RFC5208:
|
|
|
|
|
- /// <see href="https://www.rfc-editor.org/rfc/rfc5208#section-5"/>.
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- /// <param name="privateKeyInfo">The <see cref="PrivateKeyInfo"/>.</param>
|
|
|
|
|
- /// <returns>
|
|
|
|
|
- /// The <see cref="Key"/>.
|
|
|
|
|
- /// </returns>
|
|
|
|
|
- /// <exception cref="SshException">Algorithm not supported.</exception>
|
|
|
|
|
- private static Key ParseOpenSslPkcs8PrivateKey(PrivateKeyInfo privateKeyInfo)
|
|
|
|
|
- {
|
|
|
|
|
- var algorithmOid = privateKeyInfo.PrivateKeyAlgorithm.Algorithm;
|
|
|
|
|
- var key = privateKeyInfo.PrivateKey.GetOctets();
|
|
|
|
|
- if (algorithmOid.Equals(PkcsObjectIdentifiers.RsaEncryption))
|
|
|
|
|
- {
|
|
|
|
|
- return new RsaKey(key);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (algorithmOid.Equals(X9ObjectIdentifiers.IdDsa))
|
|
|
|
|
- {
|
|
|
|
|
- var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
|
|
|
|
|
- var parametersReader = new AsnReader(parameters, AsnEncodingRules.BER);
|
|
|
|
|
- var sequenceReader = parametersReader.ReadSequence();
|
|
|
|
|
- parametersReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- var p = sequenceReader.ReadInteger();
|
|
|
|
|
- var q = sequenceReader.ReadInteger();
|
|
|
|
|
- var g = sequenceReader.ReadInteger();
|
|
|
|
|
- sequenceReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- var keyReader = new AsnReader(key, AsnEncodingRules.BER);
|
|
|
|
|
- var x = keyReader.ReadInteger();
|
|
|
|
|
- keyReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- var y = BigInteger.ModPow(g, x, p);
|
|
|
|
|
-
|
|
|
|
|
- return new DsaKey(p, q, g, y, x);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (algorithmOid.Equals(X9ObjectIdentifiers.IdECPublicKey))
|
|
|
|
|
- {
|
|
|
|
|
- var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
|
|
|
|
|
- var parametersReader = new AsnReader(parameters, AsnEncodingRules.DER);
|
|
|
|
|
- var curve = parametersReader.ReadObjectIdentifier();
|
|
|
|
|
- parametersReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- var privateKeyReader = new AsnReader(key, AsnEncodingRules.DER);
|
|
|
|
|
- var sequenceReader = privateKeyReader.ReadSequence();
|
|
|
|
|
- privateKeyReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- var version = sequenceReader.ReadInteger();
|
|
|
|
|
- if (version != BigInteger.One)
|
|
|
|
|
- {
|
|
|
|
|
- throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "EC version '{0}' is not supported.", version));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var privatekey = sequenceReader.ReadOctetString();
|
|
|
|
|
-
|
|
|
|
|
- var publicKeyReader = sequenceReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true));
|
|
|
|
|
- var publickey = publicKeyReader.ReadBitString(out _);
|
|
|
|
|
- publicKeyReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- sequenceReader.ThrowIfNotEmpty();
|
|
|
|
|
-
|
|
|
|
|
- return new EcdsaKey(curve, publickey, privatekey.TrimLeadingZeros());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (algorithmOid.Equals(EdECObjectIdentifiers.id_Ed25519))
|
|
|
|
|
- {
|
|
|
|
|
- return new ED25519Key(key);
|
|
|
|
|
|
|
+ _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -1052,5 +490,17 @@ namespace Renci.SshNet
|
|
|
{
|
|
{
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Represents private key parser.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private interface IPrivateKeyParser
|
|
|
|
|
+ {
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Parses the private key.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <returns>The <see cref="Key"/>.</returns>
|
|
|
|
|
+ Key Parse();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|