< Summary

Information
Class: Renci.SshNet.PrivateKeyFile
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\PrivateKeyFile.cs
Line coverage
85%
Covered lines: 316
Uncovered lines: 52
Coverable lines: 368
Total lines: 732
Line coverage: 85.8%
Branch coverage
78%
Covered branches: 83
Total branches: 106
Branch coverage: 78.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
.cctor()100%1100%
.ctor(...)100%116.66%
get_HostKeyAlgorithms()100%1100%
get_Key()100%1100%
.ctor(...)100%1100%
.ctor(...)100%1100%
.ctor(...)100%2100%
.ctor(...)100%1100%
Open(...)84.61%5288.63%
GetCipherKey(...)100%2100%
DecryptKey(...)62.5%877.77%
ParseOpenSshV1Key(...)69.44%3677.55%
Dispose()100%1100%
Dispose(...)83.33%686.66%
Finalize()100%1100%
.ctor(...)100%1100%
ReadUInt32()100%1100%
ReadString(...)100%1100%
ReadBytes(...)100%1100%
ReadBytes()100%1100%
ReadBigIntWithBits()100%1100%
ReadBignum()100%1100%
ReadBignum2()100%1100%
LoadData()100%1100%
SaveData()100%10%

File(s)

\home\appveyor\projects\ssh-net\src\Renci.SshNet\PrivateKeyFile.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics;
 4using System.Globalization;
 5using System.IO;
 6using System.Security.Cryptography;
 7using System.Text;
 8using System.Text.RegularExpressions;
 9
 10using Renci.SshNet.Abstractions;
 11using Renci.SshNet.Common;
 12using Renci.SshNet.Security;
 13using Renci.SshNet.Security.Cryptography;
 14using Renci.SshNet.Security.Cryptography.Ciphers;
 15using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
 16using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
 17
 18namespace Renci.SshNet
 19{
 20    /// <summary>
 21    /// Represents private key information.
 22    /// </summary>
 23    /// <example>
 24    ///     <code source="..\..\src\Renci.SshNet.Tests\Data\Key.RSA.txt" language="Text" title="Private RSA key example"
 25    /// </example>
 26    /// <remarks>
 27    /// <para>
 28    /// The following private keys are supported:
 29    /// <list type="bullet">
 30    ///     <item>
 31    ///         <description>RSA in OpenSSL PEM, ssh.com and OpenSSH key format</description>
 32    ///     </item>
 33    ///     <item>
 34    ///         <description>DSA in OpenSSL PEM and ssh.com format</description>
 35    ///     </item>
 36    ///     <item>
 37    ///         <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
 38    ///     </item>
 39    ///     <item>
 40    ///         <description>ED25519 in OpenSSH key format</description>
 41    ///     </item>
 42    /// </list>
 43    /// </para>
 44    /// <para>
 45    /// The following encryption algorithms are supported:
 46    /// <list type="bullet">
 47    ///     <item>
 48    ///         <description>DES-EDE3-CBC</description>
 49    ///     </item>
 50    ///     <item>
 51    ///         <description>DES-EDE3-CFB</description>
 52    ///     </item>
 53    ///     <item>
 54    ///         <description>DES-CBC</description>
 55    ///     </item>
 56    ///     <item>
 57    ///         <description>AES-128-CBC</description>
 58    ///     </item>
 59    ///     <item>
 60    ///         <description>AES-192-CBC</description>
 61    ///     </item>
 62    ///     <item>
 63    ///         <description>AES-256-CBC</description>
 64    ///     </item>
 65    /// </list>
 66    /// </para>
 67    /// </remarks>
 68    public class PrivateKeyFile : IPrivateKeySource, IDisposable
 69    {
 470        private static readonly Regex PrivateKeyRegex = new Regex(@"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?
 471                                                                  RegexOptions.Compiled | RegexOptions.Multiline | Regex
 72
 60473        private readonly List<HostAlgorithm> _hostAlgorithms = new List<HostAlgorithm>();
 74        private Key _key;
 75        private bool _isDisposed;
 76
 77        /// <summary>
 78        /// Gets the supported host algorithms for this key file.
 79        /// </summary>
 80        public IReadOnlyCollection<HostAlgorithm> HostKeyAlgorithms
 81        {
 82            get
 84883            {
 84884                return _hostAlgorithms;
 84885            }
 86        }
 87
 88        /// <summary>
 89        /// Gets the key.
 90        /// </summary>
 91        public Key Key
 92        {
 93            get
 3094            {
 3095                return _key;
 3096            }
 97        }
 98
 99        /// <summary>
 100        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
 101        /// </summary>
 102        /// <param name="key">The key.</param>
 0103        public PrivateKeyFile(Key key)
 0104        {
 0105            _key = key;
 0106            _hostAlgorithms.Add(new KeyHostAlgorithm(key.ToString(), key));
 0107        }
 108
 109        /// <summary>
 110        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
 111        /// </summary>
 112        /// <param name="privateKey">The private key.</param>
 108113        public PrivateKeyFile(Stream privateKey)
 108114        {
 108115            Open(privateKey, passPhrase: null);
 105116            Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
 105117        }
 118
 119        /// <summary>
 120        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
 121        /// </summary>
 122        /// <param name="fileName">Name of the file.</param>
 123        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/> or empty.</exc
 124        /// <remarks>
 125        /// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exception
 126        /// </remarks>
 127        public PrivateKeyFile(string fileName)
 9128            : this(fileName, passPhrase: null)
 3129        {
 3130        }
 131
 132        /// <summary>
 133        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
 134        /// </summary>
 135        /// <param name="fileName">Name of the file.</param>
 136        /// <param name="passPhrase">The pass phrase.</param>
 137        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/> or empty, or <
 138        /// <remarks>
 139        /// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exception
 140        /// </remarks>
 30141        public PrivateKeyFile(string fileName, string passPhrase)
 30142        {
 30143            if (string.IsNullOrEmpty(fileName))
 12144            {
 12145                throw new ArgumentNullException(nameof(fileName));
 146            }
 147
 18148            using (var keyFile = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
 18149            {
 18150                Open(keyFile, passPhrase);
 12151            }
 152
 12153            Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
 12154        }
 155
 156        /// <summary>
 157        /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
 158        /// </summary>
 159        /// <param name="privateKey">The private key.</param>
 160        /// <param name="passPhrase">The pass phrase.</param>
 161        /// <exception cref="ArgumentNullException"><paramref name="privateKey"/> or <paramref name="passPhrase"/> is <s
 466162        public PrivateKeyFile(Stream privateKey, string passPhrase)
 466163        {
 466164            Open(privateKey, passPhrase);
 165
 453166            Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
 453167        }
 168
 169        /// <summary>
 170        /// Opens the specified private key.
 171        /// </summary>
 172        /// <param name="privateKey">The private key.</param>
 173        /// <param name="passPhrase">The pass phrase.</param>
 174        private void Open(Stream privateKey, string passPhrase)
 592175        {
 592176            if (privateKey is null)
 6177            {
 6178                throw new ArgumentNullException(nameof(privateKey));
 179            }
 180
 181            Match privateKeyMatch;
 182
 586183            using (var sr = new StreamReader(privateKey))
 586184            {
 586185                var text = sr.ReadToEnd();
 586186                privateKeyMatch = PrivateKeyRegex.Match(text);
 586187            }
 188
 586189            if (!privateKeyMatch.Success)
 0190            {
 0191                throw new SshException("Invalid private key file.");
 192            }
 193
 586194            var keyName = privateKeyMatch.Result("${keyName}");
 586195            var cipherName = privateKeyMatch.Result("${cipherName}");
 586196            var salt = privateKeyMatch.Result("${salt}");
 586197            var data = privateKeyMatch.Result("${data}");
 198
 586199            var binaryData = Convert.FromBase64String(data);
 200
 201            byte[] decryptedData;
 202
 586203            if (!string.IsNullOrEmpty(cipherName) && !string.IsNullOrEmpty(salt))
 48204            {
 48205                if (string.IsNullOrEmpty(passPhrase))
 7206                {
 7207                    throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
 208                }
 209
 41210                var binarySalt = new byte[salt.Length / 2];
 1250211                for (var i = 0; i < binarySalt.Length; i++)
 584212                {
 584213                    binarySalt[i] = Convert.ToByte(salt.Substring(i * 2, 2), 16);
 584214                }
 215
 216                CipherInfo cipher;
 41217                switch (cipherName)
 218                {
 219                    case "DES-EDE3-CBC":
 6220                        cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), new PK
 3221                        break;
 222                    case "DES-EDE3-CFB":
 6223                        cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CfbCipherMode(iv), new PK
 3224                        break;
 225                    case "DES-CBC":
 6226                        cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padd
 3227                        break;
 228                    case "AES-128-CBC":
 50229                        cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding
 25230                        break;
 231                    case "AES-192-CBC":
 6232                        cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding
 3233                        break;
 234                    case "AES-256-CBC":
 8235                        cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding
 4236                        break;
 237                    default:
 0238                        throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" i
 239                }
 240
 41241                decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
 41242            }
 243            else
 538244            {
 538245                decryptedData = binaryData;
 538246            }
 247
 579248            switch (keyName)
 249            {
 250                case "RSA":
 499251                    var rsaKey = new RsaKey(decryptedData);
 499252                    _key = rsaKey;
 253#pragma warning disable CA2000 // Dispose objects before losing scope
 499254                    _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashA
 499255                    _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashA
 256#pragma warning restore CA2000 // Dispose objects before losing scope
 499257                    _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
 499258                    break;
 259                case "DSA":
 0260                    _key = new DsaKey(decryptedData);
 0261                    _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
 0262                    break;
 263                case "EC":
 19264                    _key = new EcdsaKey(decryptedData);
 19265                    _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
 19266                    break;
 267                case "OPENSSH":
 33268                    _key = ParseOpenSshV1Key(decryptedData, passPhrase);
 33269                    if (_key is RsaKey parsedRsaKey)
 6270                    {
 271#pragma warning disable CA2000 // Dispose objects before losing scope
 6272                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsa
 6273                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsa
 274#pragma warning restore CA2000 // Dispose objects before losing scope
 6275                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
 6276                    }
 277                    else
 27278                    {
 27279                        _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
 27280                    }
 281
 33282                    break;
 283                case "SSH2 ENCRYPTED":
 28284                    var reader = new SshDataReader(decryptedData);
 28285                    var magicNumber = reader.ReadUInt32();
 28286                    if (magicNumber != 0x3f6ff9eb)
 0287                    {
 0288                        throw new SshException("Invalid SSH2 private key.");
 289                    }
 290
 28291                    _ = reader.ReadUInt32(); // Read total bytes length including magic number
 28292                    var keyType = reader.ReadString(SshData.Ascii);
 28293                    var ssh2CipherName = reader.ReadString(SshData.Ascii);
 28294                    var blobSize = (int)reader.ReadUInt32();
 295
 296                    byte[] keyData;
 28297                    if (ssh2CipherName == "none")
 12298                    {
 12299                        keyData = reader.ReadBytes(blobSize);
 12300                    }
 16301                    else if (ssh2CipherName == "3des-cbc")
 16302                    {
 16303                        if (string.IsNullOrEmpty(passPhrase))
 6304                        {
 6305                            throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empt
 306                        }
 307
 10308                        var key = GetCipherKey(passPhrase, 192 / 8);
 10309                        var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
 10310                        keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
 10311                    }
 312                    else
 0313                    {
 0314                        throw new SshException(string.Format("Cipher method '{0}' is not supported.", cipherName));
 315                    }
 316
 317                    /*
 318                     * TODO: Create two specific data types to avoid using SshDataReader class.
 319                     */
 320
 22321                    reader = new SshDataReader(keyData);
 322
 22323                    var decryptedLength = reader.ReadUInt32();
 324
 22325                    if (decryptedLength > blobSize - 4)
 3326                    {
 3327                        throw new SshException("Invalid passphrase.");
 328                    }
 329
 19330                    if (keyType == "if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}")
 6331                    {
 6332                        var exponent = reader.ReadBigIntWithBits(); // e
 6333                        var d = reader.ReadBigIntWithBits(); // d
 6334                        var modulus = reader.ReadBigIntWithBits(); // n
 6335                        var inverseQ = reader.ReadBigIntWithBits(); // u
 6336                        var q = reader.ReadBigIntWithBits(); // p
 6337                        var p = reader.ReadBigIntWithBits(); // q
 6338                        var decryptedRsaKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
 6339                        _key = decryptedRsaKey;
 340#pragma warning disable CA2000 // Dispose objects before losing scope
 6341                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(decrypted
 6342                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(decrypted
 343#pragma warning restore CA2000 // Dispose objects before losing scope
 6344                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
 6345                    }
 13346                    else if (keyType == "dl-modp{sign{dsa-nist-sha1},dh{plain}}")
 13347                    {
 13348                        var zero = reader.ReadUInt32();
 13349                        if (zero != 0)
 0350                        {
 0351                            throw new SshException("Invalid private key");
 352                        }
 353
 13354                        var p = reader.ReadBigIntWithBits();
 13355                        var g = reader.ReadBigIntWithBits();
 13356                        var q = reader.ReadBigIntWithBits();
 13357                        var y = reader.ReadBigIntWithBits();
 13358                        var x = reader.ReadBigIntWithBits();
 13359                        _key = new DsaKey(p, q, g, y, x);
 13360                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
 13361                    }
 362                    else
 0363                    {
 0364                        throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType));
 365                    }
 366
 19367                    break;
 368                default:
 0369                    throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supporte
 370            }
 570371        }
 372
 373        private static byte[] GetCipherKey(string passphrase, int length)
 10374        {
 10375            var cipherKey = new List<byte>();
 376
 10377            using (var md5 = CryptoAbstraction.CreateMD5())
 10378            {
 10379                var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
 380
 10381                var hash = md5.ComputeHash(passwordBytes);
 10382                cipherKey.AddRange(hash);
 383
 20384                while (cipherKey.Count < length)
 10385                {
 10386                    hash = passwordBytes.Concat(hash);
 10387                    hash = md5.ComputeHash(hash);
 10388                    cipherKey.AddRange(hash);
 10389                }
 10390            }
 391
 10392            return cipherKey.ToArray().Take(length);
 10393        }
 394
 395        /// <summary>
 396        /// Decrypts encrypted private key file data.
 397        /// </summary>
 398        /// <param name="cipherInfo">The cipher info.</param>
 399        /// <param name="cipherData">Encrypted data.</param>
 400        /// <param name="passPhrase">Decryption pass phrase.</param>
 401        /// <param name="binarySalt">Decryption binary salt.</param>
 402        /// <returns>Decrypted byte array.</returns>
 403        /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <par
 404        private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
 41405        {
 41406            if (cipherInfo is null)
 0407            {
 0408                throw new ArgumentNullException(nameof(cipherInfo));
 409            }
 410
 41411            if (cipherData is null)
 0412            {
 0413                throw new ArgumentNullException(nameof(cipherData));
 414            }
 415
 41416            if (binarySalt is null)
 0417            {
 0418                throw new ArgumentNullException(nameof(binarySalt));
 419            }
 420
 41421            var cipherKey = new List<byte>();
 422
 41423            using (var md5 = CryptoAbstraction.CreateMD5())
 41424            {
 41425                var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
 426
 427                // Use 8 bytes binary salt
 41428                var initVector = passwordBytes.Concat(binarySalt.Take(8));
 429
 41430                var hash = md5.ComputeHash(initVector);
 41431                cipherKey.AddRange(hash);
 432
 54433                while (cipherKey.Count < cipherInfo.KeySize / 8)
 13434                {
 13435                    hash = hash.Concat(initVector);
 13436                    hash = md5.ComputeHash(hash);
 13437                    cipherKey.AddRange(hash);
 13438                }
 41439            }
 440
 41441            var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
 442
 41443            return cipher.Decrypt(cipherData);
 41444        }
 445
 446        /// <summary>
 447        /// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec:
 448        /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
 449        /// </summary>
 450        /// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
 451        /// <param name="passPhrase">Passphrase or <see langword="null"/> if there isn't one.</param>
 452        /// <returns>
 453        /// The OpenSSH V1 key.
 454        /// </returns>
 455        private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
 33456        {
 33457            var keyReader = new SshDataReader(keyFileData);
 458
 459            // check magic header
 33460            var authMagic = Encoding.UTF8.GetBytes("openssh-key-v1\0");
 33461            var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length);
 33462            if (!authMagic.IsEqualTo(keyHeaderBytes))
 0463            {
 0464                throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
 465            }
 466
 467            // cipher will be "aes256-cbc" if using a passphrase, "none" otherwise
 33468            var cipherName = keyReader.ReadString(Encoding.UTF8);
 469
 470            // key derivation function (kdf): bcrypt or nothing
 33471            var kdfName = keyReader.ReadString(Encoding.UTF8);
 472
 473            // kdf options length: 24 if passphrase, 0 if no passphrase
 33474            var kdfOptionsLen = (int)keyReader.ReadUInt32();
 33475            byte[] salt = null;
 33476            var rounds = 0;
 33477            if (kdfOptionsLen > 0)
 18478            {
 18479                var saltLength = (int) keyReader.ReadUInt32();
 18480                salt = keyReader.ReadBytes(saltLength);
 18481                rounds = (int) keyReader.ReadUInt32();
 18482            }
 483
 484            // number of public keys, only supporting 1 for now
 33485            var numberOfPublicKeys = (int)keyReader.ReadUInt32();
 33486            if (numberOfPublicKeys != 1)
 0487            {
 0488                throw new SshException("At this time only one public key in the openssh key is supported.");
 489            }
 490
 491            // read public key in ssh-format, but we dont need it
 33492            _ = keyReader.ReadString(Encoding.UTF8);
 493
 494            // possibly encrypted private key
 33495            var privateKeyLength = (int) keyReader.ReadUInt32();
 33496            var privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
 497
 498            // decrypt private key if necessary
 33499            if (cipherName != "none")
 18500            {
 18501                if (string.IsNullOrEmpty(passPhrase))
 0502                {
 0503                    throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
 504                }
 505
 18506                if (string.IsNullOrEmpty(kdfName) || kdfName != "bcrypt")
 0507                {
 0508                    throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
 509                }
 510
 511                // inspired by the SSHj library (https://github.com/hierynomus/sshj)
 512                // apply the kdf to derive a key and iv from the passphrase
 18513                var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
 18514                var keyiv = new byte[48];
 18515                new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
 18516                var key = new byte[32];
 18517                Array.Copy(keyiv, 0, key, 0, 32);
 18518                var iv = new byte[16];
 18519                Array.Copy(keyiv, 32, iv, 0, 16);
 520
 521                AesCipher cipher;
 18522                switch (cipherName)
 523                {
 524                    case "aes256-cbc":
 0525                        cipher = new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false);
 0526                        break;
 527                    case "aes256-ctr":
 18528                        cipher = new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false);
 18529                        break;
 530                    default:
 0531                        throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
 532                }
 533
 534                try
 18535                {
 18536                    privateKeyBytes = cipher.Decrypt(privateKeyBytes);
 18537                }
 538                finally
 18539                {
 18540                    cipher.Dispose();
 18541                }
 18542            }
 543
 544            // validate private key length
 33545            privateKeyLength = privateKeyBytes.Length;
 33546            if (privateKeyLength % 8 != 0)
 0547            {
 0548                throw new SshException("The private key section must be a multiple of the block size (8)");
 549            }
 550
 551            // now parse the data we called the private key, it actually contains the public key again
 552            // so we need to parse through it to get the private key bytes, plus there's some
 553            // validation we need to do.
 33554            var privateKeyReader = new SshDataReader(privateKeyBytes);
 555
 556            // check ints should match, they wouldn't match for example if the wrong passphrase was supplied
 33557            var checkInt1 = (int) privateKeyReader.ReadUInt32();
 33558            var checkInt2 = (int) privateKeyReader.ReadUInt32();
 33559            if (checkInt1 != checkInt2)
 0560            {
 0561                throw new SshException(string.Format(CultureInfo.InvariantCulture,
 0562                                                     "The random check bytes of the OpenSSH key do not match ({0} <-> {1
 0563                                                     checkInt1.ToString(CultureInfo.InvariantCulture),
 0564                                                     checkInt2.ToString(CultureInfo.InvariantCulture)));
 565            }
 566
 567            // key type
 33568            var keyType = privateKeyReader.ReadString(Encoding.UTF8);
 569
 570            Key parsedKey;
 571            byte[] publicKey;
 572            byte[] unencryptedPrivateKey;
 33573            switch (keyType)
 574            {
 575                case "ssh-ed25519":
 576                    // public key
 7577                    publicKey = privateKeyReader.ReadBignum2();
 578
 579                    // private key
 7580                    unencryptedPrivateKey = privateKeyReader.ReadBignum2();
 7581                    parsedKey = new ED25519Key(publicKey.Reverse(), unencryptedPrivateKey);
 7582                    break;
 583                case "ecdsa-sha2-nistp256":
 584                case "ecdsa-sha2-nistp384":
 585                case "ecdsa-sha2-nistp521":
 586                    // curve
 20587                    var len = (int) privateKeyReader.ReadUInt32();
 20588                    var curve = Encoding.ASCII.GetString(privateKeyReader.ReadBytes(len));
 589
 590                    // public key
 20591                    publicKey = privateKeyReader.ReadBignum2();
 592
 593                    // private key
 20594                    unencryptedPrivateKey = privateKeyReader.ReadBignum2();
 20595                    parsedKey = new EcdsaKey(curve, publicKey, unencryptedPrivateKey.TrimLeadingZeros());
 20596                    break;
 597                case "ssh-rsa":
 6598                    var modulus = privateKeyReader.ReadBignum(); // n
 6599                    var exponent = privateKeyReader.ReadBignum(); // e
 6600                    var d = privateKeyReader.ReadBignum(); // d
 6601                    var inverseQ = privateKeyReader.ReadBignum(); // iqmp
 6602                    var p = privateKeyReader.ReadBignum(); // p
 6603                    var q = privateKeyReader.ReadBignum(); // q
 6604                    parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
 6605                    break;
 606                default:
 0607                    throw new SshException("OpenSSH key type '" + keyType + "' is not supported.");
 608            }
 609
 33610            parsedKey.Comment = privateKeyReader.ReadString(Encoding.UTF8);
 611
 612            // The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
 613            // until the total length is a multiple of the cipher block size.
 33614            var padding = privateKeyReader.ReadBytes();
 666615            for (var i = 0; i < padding.Length; i++)
 300616            {
 300617                if ((int) padding[i] != i + 1)
 0618                {
 0619                    throw new SshException("Padding of openssh key format contained wrong byte at position: " +
 0620                                           i.ToString(CultureInfo.InvariantCulture));
 621                }
 300622            }
 623
 33624            return parsedKey;
 33625        }
 626
 627        /// <summary>
 628        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 629        /// </summary>
 630        public void Dispose()
 3631        {
 3632            Dispose(disposing: true);
 3633            GC.SuppressFinalize(this);
 3634        }
 635
 636        /// <summary>
 637        /// Releases unmanaged and - optionally - managed resources.
 638        /// </summary>
 639        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor
 640        protected virtual void Dispose(bool disposing)
 604641        {
 604642            if (_isDisposed)
 0643            {
 0644                return;
 645            }
 646
 604647            if (disposing)
 3648            {
 3649                var key = _key;
 3650                if (key != null)
 3651                {
 3652                    ((IDisposable) key).Dispose();
 3653                    _key = null;
 3654                }
 655
 3656                _isDisposed = true;
 3657            }
 604658        }
 659
 660        /// <summary>
 661        /// Finalizes an instance of the <see cref="PrivateKeyFile"/> class.
 662        /// </summary>
 663        ~PrivateKeyFile()
 1202664        {
 601665            Dispose(disposing: false);
 1202666        }
 667
 668        private sealed class SshDataReader : SshData
 669        {
 116670            public SshDataReader(byte[] data)
 116671            {
 116672                Load(data);
 116673            }
 674
 675            public new uint ReadUInt32()
 340676            {
 340677                return base.ReadUInt32();
 340678            }
 679
 680            public new string ReadString(Encoding encoding)
 221681            {
 221682                return base.ReadString(encoding);
 221683            }
 684
 685            public new byte[] ReadBytes(int length)
 126686            {
 126687                return base.ReadBytes(length);
 126688            }
 689
 690            public new byte[] ReadBytes()
 33691            {
 33692                return base.ReadBytes();
 33693            }
 694
 695            /// <summary>
 696            /// Reads next mpint data type from internal buffer where length specified in bits.
 697            /// </summary>
 698            /// <returns>mpint read.</returns>
 699            public BigInteger ReadBigIntWithBits()
 101700            {
 101701                var length = (int) base.ReadUInt32();
 702
 101703                length = (length + 7) / 8;
 704
 101705                var data = base.ReadBytes(length);
 101706                var bytesArray = new byte[data.Length + 1];
 101707                Buffer.BlockCopy(data, 0, bytesArray, 1, data.Length);
 708
 101709                return new BigInteger(bytesArray.Reverse());
 101710            }
 711
 712            public BigInteger ReadBignum()
 36713            {
 36714                return new BigInteger(ReadBignum2().Reverse());
 36715            }
 716
 717            public byte[] ReadBignum2()
 90718            {
 90719                var length = (int)base.ReadUInt32();
 90720                return base.ReadBytes(length);
 90721            }
 722
 723            protected override void LoadData()
 116724            {
 116725            }
 726
 727            protected override void SaveData()
 0728            {
 0729            }
 730        }
 731    }
 732}