#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Cryptography; using System.Text; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Security; using Renci.SshNet.Security.Cryptography.Ciphers; namespace Renci.SshNet { public partial class PrivateKeyFile { private sealed class PuTTY : IPrivateKeyParser { private readonly string _version; private readonly string _algorithmName; private readonly string _encryptionType; private readonly string _comment; private readonly byte[] _publicKey; private readonly string? _argon2Type; private readonly string? _argon2Salt; private readonly string? _argon2Iterations; private readonly string? _argon2Memory; private readonly string? _argon2Parallelism; private readonly byte[] _data; private readonly string _mac; private readonly string? _passPhrase; public PuTTY(string version, string algorithmName, string encryptionType, string comment, byte[] publicKey, string? argon2Type, string? argon2Salt, string? argon2Iterations, string? argon2Memory, string? argon2Parallelism, byte[] data, string mac, string? passPhrase) { _version = version; _algorithmName = algorithmName; _encryptionType = encryptionType; _comment = comment; _publicKey = publicKey; _argon2Type = argon2Type; _argon2Salt = argon2Salt; _argon2Iterations = argon2Iterations; _argon2Memory = argon2Memory; _argon2Parallelism = argon2Parallelism; _data = data; _mac = mac; _passPhrase = passPhrase; } /// /// Parses an PuTTY PPK key file. /// . /// public Key Parse() { byte[] privateKey; HMAC hmac; switch (_encryptionType) { case "aes256-cbc": if (string.IsNullOrEmpty(_passPhrase)) { throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty."); } byte[] cipherKey; byte[] cipherIV; switch (_version) { case "3": ThrowHelper.ThrowIfNullOrEmpty(_argon2Type); ThrowHelper.ThrowIfNullOrEmpty(_argon2Iterations); ThrowHelper.ThrowIfNullOrEmpty(_argon2Memory); ThrowHelper.ThrowIfNullOrEmpty(_argon2Parallelism); ThrowHelper.ThrowIfNullOrEmpty(_argon2Salt); var keyData = Argon2( _argon2Type, Convert.ToInt32(_argon2Iterations), Convert.ToInt32(_argon2Memory), Convert.ToInt32(_argon2Parallelism), #if NET Convert.FromHexString(_argon2Salt), #else Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_argon2Salt), #endif _passPhrase); cipherKey = keyData.Take(32); cipherIV = keyData.Take(32, 16); var macKey = keyData.Take(48, 32); hmac = new HMACSHA256(macKey); break; case "2": keyData = V2KDF(_passPhrase); cipherKey = keyData.Take(32); cipherIV = new byte[16]; macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + _passPhrase)).Take(20); hmac = new HMACSHA1(macKey); break; default: throw new SshException("PuTTY key file version " + _version + " is not supported"); } using (var cipher = new AesCipher(cipherKey, cipherIV, AesCipherMode.CBC, pkcs7Padding: false)) { privateKey = cipher.Decrypt(_data); } break; case "none": switch (_version) { case "3": hmac = new HMACSHA256(Array.Empty()); break; case "2": var macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key")); hmac = new HMACSHA1(macKey); break; default: throw new SshException("PuTTY key file version " + _version + " is not supported"); } privateKey = _data; break; default: throw new SshException("Encryption " + _encryptionType + " is not supported for PuTTY key file"); } byte[] macData; using (var macStream = new SshDataStream(256)) { macStream.Write(_algorithmName, Encoding.UTF8); macStream.Write(_encryptionType, Encoding.UTF8); macStream.Write(_comment, Encoding.UTF8); macStream.WriteBinary(_publicKey); macStream.WriteBinary(privateKey); macData = macStream.ToArray(); } byte[] macValue; using (hmac) { macValue = hmac.ComputeHash(macData); } #if NET var reference = Convert.FromHexString(_mac); #else var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac); #endif if (!macValue.SequenceEqual(reference)) { throw new SshException("MAC verification failed for PuTTY key file"); } var publicKeyReader = new SshDataReader(_publicKey); var keyType = publicKeyReader.ReadString(Encoding.UTF8); Debug.Assert(keyType == _algorithmName, $"{nameof(keyType)} is not the same as {nameof(_algorithmName)}"); var privateKeyReader = new SshDataReader(privateKey); Key parsedKey; switch (keyType) { case "ssh-ed25519": parsedKey = new ED25519Key(privateKeyReader.ReadBignum2()); break; case "ecdsa-sha2-nistp256": case "ecdsa-sha2-nistp384": case "ecdsa-sha2-nistp521": var curve = publicKeyReader.ReadString(Encoding.ASCII); var pub = publicKeyReader.ReadBignum2(); var prv = privateKeyReader.ReadBignum2(); parsedKey = new EcdsaKey(curve, pub, prv); break; case "ssh-rsa": var exponent = publicKeyReader.ReadBignum(); // e var modulus = publicKeyReader.ReadBignum(); // n var d = privateKeyReader.ReadBignum(); // d var p = privateKeyReader.ReadBignum(); // p var q = privateKeyReader.ReadBignum(); // q var inverseQ = privateKeyReader.ReadBignum(); // iqmp parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ); break; default: throw new SshException("Key type " + keyType + " is not supported for PuTTY key file"); } parsedKey.Comment = _comment; return parsedKey; } private static byte[] Argon2(string type, int iterations, int memory, int parallelism, byte[] salt, string passPhrase) { int param; switch (type) { case "Argon2i": param = Argon2Parameters.Argon2i; break; case "Argon2d": param = Argon2Parameters.Argon2d; break; case "Argon2id": param = Argon2Parameters.Argon2id; break; default: throw new SshException("KDF " + type + " is not supported for PuTTY key file"); } var a2p = new Argon2Parameters.Builder(param) .WithVersion(Argon2Parameters.Version13) .WithIterations(iterations) .WithMemoryAsKB(memory) .WithParallelism(parallelism) .WithSalt(salt).Build(); var generator = new Argon2BytesGenerator(); generator.Init(a2p); var output = new byte[80]; var bytes = generator.GenerateBytes(passPhrase.ToCharArray(), output); if (bytes != output.Length) { throw new SshException("Failed to generate key via Argon2"); } return output; } private static byte[] V2KDF(string passPhrase) { var cipherKey = new List(); var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase); for (var sequenceNumber = 0; sequenceNumber < 2; sequenceNumber++) { using (var sha1 = SHA1.Create()) { var sequence = new byte[] { 0, 0, 0, (byte)sequenceNumber }; _ = sha1.TransformBlock(sequence, 0, 4, outputBuffer: null, 0); _ = sha1.TransformFinalBlock(passPhraseBytes, 0, passPhraseBytes.Length); Debug.Assert(sha1.Hash != null, "Hash is null"); cipherKey.AddRange(sha1.Hash); } } return cipherKey.ToArray(); } } } }