diff --git a/README.md b/README.md index 46c5563..bc962bf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ SshNet.Keygen ============= -[SSH.NET](https://github.com/sshnet/SSH.NET) Extension to generate and export Authentication Keys in OpenSSH and PuTTY Format. +[SSH.NET](https://github.com/sshnet/SSH.NET) Extension to generate and export Authentication Keys in OpenSSH and PuTTY v2 and v3 Format. [![License](https://img.shields.io/github/license/darinkes/SshNet.KeyGen)](https://github.com/darinkes/SshNet.KeyGen/blob/main/LICENSE) [![NuGet](https://img.shields.io/nuget/v/SshNet.Keygen.svg?style=flat)](https://www.nuget.org/packages/SshNet.Keygen) @@ -63,11 +63,11 @@ Console.WriteLine(client.RunCommand("hostname").Result); ```cs var keyInfo = new SshKeyGenerateInfo { - KeyFormat = SshKeyFormat.PuTTY + KeyFormat = SshKeyFormat.PuTTYv3 }; var key = SshKey.Generate("test.ppk", FileMode.Create, keyInfo); -var publicKey = key.ToPublic(); +var publicKey = key.ToPublic(SshKeyFormat.OpenSSH); var fingerprint = key.Fingerprint(); Console.WriteLine("Fingerprint: {0}", fingerprint); @@ -100,17 +100,39 @@ client.Connect(); Console.WriteLine(client.RunCommand("hostname").Result); ``` -### Generate an password protected RSA-2048 Key in Putty File, Show the Public Key and Connect with the Private Key +### Generate an password protected RSA-2048 Key in Putty v2 File, Show the Public Key and Connect with the Private Key ```cs var keyInfo = new SshKeyGenerateInfo { - KeyFormat = SshKeyFormat.PuTTY, + KeyFormat = SshKeyFormat.PuTTYv2, Encryption = new SshKeyEncryptionAes256("12345") }; var key = SshKey.Generate("test.ppk", FileMode.Create, keyInfo); -var publicKey = key.ToPublic(); +var publicKey = key.ToPublic(SshKeyFormat.OpenSSH); +var fingerprint = key.Fingerprint(); + +Console.WriteLine("Fingerprint: {0}", fingerprint); +Console.WriteLine("Add this to your .ssh/authorized_keys on the SSH Server: {0}", publicKey); +Console.ReadLine(); + +using var client = new SshClient("ssh.foo.com", "root", key); +client.Connect(); +Console.WriteLine(client.RunCommand("hostname").Result); +``` + +### Generate an password protected RSA-2048 Key in Putty v3 File with own Argon Options, Show the Public Key and Connect with the Private Key + +```cs +var keyInfo = new SshKeyGenerateInfo +{ + KeyFormat = SshKeyFormat.PuTTYv3, + Encryption = new SshKeyEncryptionAes256("12345", new PuttyV3Encryption { KeyDerivation = ArgonKeyDerivation.Argon2d, Iterations = 64, DegreeOfParallelism = 44 }) +}; +var key = SshKey.Generate("test.ppk", FileMode.Create, keyInfo); + +var publicKey = key.ToPublic(SshKeyFormat.OpenSSH); var fingerprint = key.Fingerprint(); Console.WriteLine("Fingerprint: {0}", fingerprint); @@ -207,11 +229,13 @@ Console.WriteLine("Public Key: {0}", publicKey); ```cs var keyFile = new PrivateKeyFile("test.key"); -var privateKey = keyFile.ToOpenSshFormat(new SshKeyEncryptionAes256("12345")); -var puttyKey = keyFile.ToPuttyFormat(new SshKeyEncryptionAes256("12345")); +var privateKey = keyFile.ToOpenSshFormat("12345"); +var puttyKey = keyFile.ToPuttyFormat("12345"); var publicKey = keyFile.ToPublic(); +var puttyPublicKey = keyFile.ToPuttyPublicFormat(); Console.WriteLine("Private Key: {0}", privateKey); Console.WriteLine("Putty Private Key: {0}", puttyKey); Console.WriteLine("Public Key: {0}", publicKey); +Console.WriteLine("Putty Public Key: {0}", puttyPublicKey); ``` diff --git a/SshNet.Keygen.Tests/TestKey.cs b/SshNet.Keygen.Tests/TestKey.cs index ed09d56..386a3db 100644 --- a/SshNet.Keygen.Tests/TestKey.cs +++ b/SshNet.Keygen.Tests/TestKey.cs @@ -28,6 +28,9 @@ public void TestExceptions() keyInfo.KeyType = SshKeyType.RSA; Assert.Throws(() => SshKey.Generate(keyInfo)); + + var key = SshKey.Generate(); + Assert.Throws(() => key.ToPuttyFormat(SshKeyFormat.OpenSSH)); } [Test] @@ -57,24 +60,14 @@ private static void KeyGenTest(SshKeyType keyType, int keyLength = 0) { foreach (var sshKeyEncryption in sshKeyEncryptions) { - TestContext.WriteLine($"File: {path} - Encryption: {sshKeyEncryption}"); + TestContext.WriteLine($"File: '{path}' - Encryption: '{sshKeyEncryption}' - Comment: '{comment}'"); var keyInfo = new SshKeyGenerateInfo(keyType) { Encryption = sshKeyEncryption, - KeyLength = keyLength - }; - if (!string.IsNullOrEmpty(comment)) - keyInfo.Comment = comment; - - var puttyKeyInfo = new SshKeyGenerateInfo(keyType) - { - KeyFormat = SshKeyFormat.PuTTY, - Encryption = sshKeyEncryption, - KeyLength = keyLength + KeyLength = keyLength, + Comment = comment }; - if (!string.IsNullOrEmpty(comment)) - puttyKeyInfo.Comment = comment; IPrivateKeySource keyFile; if (string.IsNullOrEmpty(path)) @@ -85,20 +78,67 @@ private static void KeyGenTest(SshKeyType keyType, int keyLength = 0) } else { - _ = SshKey.Generate(path, FileMode.Create, keyInfo); - keyFile = new PrivateKeyFile(path, password); + var genKey = SshKey.Generate(path, FileMode.Create, keyInfo); ClassicAssert.IsTrue(File.Exists(path)); + keyFile = new PrivateKeyFile(path, password); + _ = new PrivateKeyFile(genKey.ToOpenSshFormat().ToStream(), password); + + ClassicAssert.AreEqual(genKey.ToOpenSshPublicFormat(), genKey.ToPublic()); + ClassicAssert.AreEqual(genKey.ToOpenSshPublicFormat(), keyFile.ToPublic()); + ClassicAssert.AreEqual(1, genKey.ToPublic().Split('\n').Length - 1); + ClassicAssert.AreEqual(1, keyFile.ToPublic().Split('\n').Length - 1); + + StringAssert.Contains(((KeyHostAlgorithm) genKey.HostKeyAlgorithms.First()).Key.ToString(), genKey.ToPublic()); + StringAssert.Contains(comment ?? SshKeyGenerateInfo.DefaultSshKeyComment, genKey.ToPublic()); + StringAssert.Contains(((KeyHostAlgorithm) genKey.HostKeyAlgorithms.First()).Key.ToString(), genKey.ToOpenSshPublicFormat()); + StringAssert.Contains(comment ?? SshKeyGenerateInfo.DefaultSshKeyComment, genKey.ToOpenSshPublicFormat()); - switch (sshKeyEncryption.CipherName) + foreach (var keyFormat in new List { SshKeyFormat.PuTTYv2 , SshKeyFormat.PuTTYv3 }) { - case "aes256-ctr": - Assert.Throws(() => SshKey.Generate($"{path}.ppk", FileMode.Create, puttyKeyInfo)); - break; - default: - File.Delete($"{path}.ppk"); - _ = SshKey.Generate($"{path}.ppk", FileMode.Create, puttyKeyInfo); - ClassicAssert.IsTrue(File.Exists($"{path}.ppk")); - break; + keyInfo.KeyFormat = keyFormat; + var puttyFile = $"{path}-{keyFormat}.ppk"; + + switch (sshKeyEncryption.CipherName) + { + case "aes256-ctr": + Assert.Throws(() => SshKey.Generate(puttyFile, FileMode.Create, keyInfo)); + break; + default: + File.Delete(puttyFile); + var puttyKey = SshKey.Generate(puttyFile, FileMode.Create, keyInfo); + ClassicAssert.IsTrue(File.Exists(puttyFile)); + + foreach (var puttyContent in new List { File.ReadAllText(puttyFile), puttyKey.ToPuttyFormat() }) + { + StringAssert.Contains($"Comment: {comment ?? SshKeyGenerateInfo.DefaultSshKeyComment}", puttyContent); + StringAssert.Contains($"Encryption: {sshKeyEncryption.CipherName}", puttyContent); + + switch (keyFormat) + { + case SshKeyFormat.PuTTYv2: + StringAssert.Contains("PuTTY-User-Key-File-2: ", puttyContent); + break; + case SshKeyFormat.PuTTYv3: + StringAssert.Contains("PuTTY-User-Key-File-3: ", puttyContent); + if (keyInfo.Encryption is SshKeyEncryptionAes256) + { + StringAssert.Contains("Key-Derivation: Argon2id", puttyContent); + StringAssert.Contains("Argon2-Memory: 8192", puttyContent); + StringAssert.Contains("Argon2-Passes: 22", puttyContent); + StringAssert.Contains("Argon2-Parallelism: 1", puttyContent); + StringAssert.Contains("Argon2-Salt:", puttyContent); + } + break; + } + } + + var puttyPubContent = puttyKey.ToPuttyPublicFormat(); + ClassicAssert.AreEqual(puttyPubContent, puttyKey.ToPublic()); + StringAssert.Contains("---- BEGIN SSH2 PUBLIC KEY ----\n", puttyPubContent); + StringAssert.Contains($"Comment: \"{comment ?? SshKeyGenerateInfo.DefaultSshKeyComment}\"\n", puttyPubContent); + StringAssert.Contains("---- END SSH2 PUBLIC KEY ----\n", puttyPubContent); + break; + } } } @@ -106,10 +146,7 @@ private static void KeyGenTest(SshKeyType keyType, int keyLength = 0) if (keyLength != 0) ClassicAssert.AreEqual(keyLength, (((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.KeyLength)); - ClassicAssert.AreEqual( - string.IsNullOrEmpty(comment) - ? $"{Environment.UserName}@{Environment.MachineName}" - : comment, + ClassicAssert.AreEqual(comment ?? SshKeyGenerateInfo.DefaultSshKeyComment, ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.Comment); } } diff --git a/SshNet.Keygen/Extensions/KeyExtension.cs b/SshNet.Keygen/Extensions/KeyExtension.cs index d07e667..bbfc010 100644 --- a/SshNet.Keygen/Extensions/KeyExtension.cs +++ b/SshNet.Keygen/Extensions/KeyExtension.cs @@ -11,12 +11,12 @@ public static class KeyExtension { #region Fingerprint - public static string Fingerprint(this Key key) + internal static string Fingerprint(this Key key) { return key.Fingerprint(SshKeyGenerateInfo.DefaultHashAlgorithmName); } - public static string Fingerprint(this Key key, SshKeyHashAlgorithmName hashAlgorithm) + internal static string Fingerprint(this Key key, SshKeyHashAlgorithmName hashAlgorithm) { using var pubStream = new MemoryStream(); using var pubWriter = new BinaryWriter(pubStream); @@ -29,14 +29,12 @@ public static string Fingerprint(this Key key, SshKeyHashAlgorithmName hashAlgor ? BitConverter.ToString(pubKeyHash).ToLower().Replace('-', ':') : Convert.ToBase64String(pubKeyHash, 0, pubKeyHash.Length).TrimEnd('='); - return $"{key.KeyLength} {SshKeyHashAlgorithm.HashAlgorithmName(hashAlgorithm)}:{base64} {key.Comment ?? ""} ({key.KeyName()})"; + return $"{key.KeyLength} {SshKeyHashAlgorithm.HashAlgorithmName(hashAlgorithm)}:{base64} {key.Comment} ({key.KeyName()})"; } #endregion - #region Public - - public static string ToPublic(this Key key) + internal static string ToPublic(this Key key) { using var pubStream = new MemoryStream(); using var pubWriter = new BinaryWriter(pubStream); @@ -46,16 +44,14 @@ public static string ToPublic(this Key key) return $"{key} {base64} {key.Comment ?? ""}\n"; } - #endregion - #region OpenSshFormat - public static string ToOpenSshFormat(this Key key) + internal static string ToOpenSshFormat(this Key key) { return key.ToOpenSshFormat(SshKeyGenerateInfo.DefaultSshKeyEncryption); } - public static string ToOpenSshFormat(this Key key, ISshKeyEncryption encryption) + internal static string ToOpenSshFormat(this Key key, ISshKeyEncryption encryption) { var s = new StringWriter(); s.Write("-----BEGIN OPENSSH PRIVATE KEY-----\n"); @@ -139,7 +135,7 @@ private static string OpensshPrivateKeyData(this Key key, ISshKeyEncryption encr throw new NotSupportedException($"Unsupported KeyType: {key}"); } // comment - privWriter.EncodeBinary(key.Comment ?? ""); + privWriter.EncodeBinary(key.Comment); // private key padding (1, 2, 3, ...) var pad = 0; @@ -166,13 +162,30 @@ private static string OpensshPrivateKeyData(this Key key, ISshKeyEncryption encr #region PuttyFormat - public static string ToPuttyFormat(this Key key) + internal static string ToPuttyPublicFormat(this Key key) { - return key.ToPuttyFormat(SshKeyGenerateInfo.DefaultSshKeyEncryption); + using var pubStream = new MemoryStream(); + using var pubWriter = new BinaryWriter(pubStream); + key.PublicKeyData(pubWriter); + + var s = new StringWriter(); + s.Write("---- BEGIN SSH2 PUBLIC KEY ----\n"); + s.Write($"Comment: \"{key.Comment}\"\n"); + s.Write(Convert.ToBase64String(pubStream.ToArray()).FormatNewLines(64) + "\n"); + s.Write("---- END SSH2 PUBLIC KEY ----\n"); + return s.ToString(); + } + + internal static string ToPuttyFormat(this Key key, SshKeyFormat sshKeyFormat) + { + return key.ToPuttyFormat(SshKeyGenerateInfo.DefaultSshKeyEncryption, sshKeyFormat); } - public static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption) + internal static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption, SshKeyFormat sshKeyFormat) { + if (sshKeyFormat is not SshKeyFormat.PuTTYv2 and not SshKeyFormat.PuTTYv3) + throw new NotSupportedException($"Unsupported PuTTY Key Format {sshKeyFormat}"); + // Public Key using var pubStream = new MemoryStream(); using var pubWriter = new BinaryWriter(pubStream); @@ -215,34 +228,70 @@ public static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption) privWriter.Write((byte)++pad); } - var encrypted = encryption.PuttyEncrypt(privStream.ToArray()); - var privateBase64String = Convert.ToBase64String(encrypted).FormatNewLines(64); - // MAC using var macStream = new MemoryStream(); using var macWriter = new BinaryWriter(macStream); macWriter.EncodeBinary(key.ToString()); macWriter.EncodeBinary(encryption.CipherName); - macWriter.EncodeBinary(key.Comment ?? ""); + macWriter.EncodeBinary(key.Comment); macWriter.EncodeBinary(pubStream); macWriter.EncodeBinary(privStream); var hashData = macStream.ToArray(); - using var sha1 = SHA1.Create(); - var macKey = sha1.ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key" + encryption.Passphrase)); - using var hmac = new HMACSHA1(macKey); - var macHash = hmac.ComputeHash(hashData); + string privateBase64String; + PuttyV3Encryption? puttyV3Encryption = null; + byte[] macHash = new byte[0]; + if (sshKeyFormat is SshKeyFormat.PuTTYv2) + { + byte[] encrypted = encryption.PuttyV2Encrypt(privStream.ToArray()); + privateBase64String = Convert.ToBase64String(encrypted).FormatNewLines(64); + + using var sha1 = SHA1.Create(); + var macKey = sha1.ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key" + encryption.Passphrase)); + using var hmac = new HMACSHA1(macKey); + macHash = hmac.ComputeHash(hashData); + } + else + { + puttyV3Encryption = encryption.PuttyV3Encrypt(privStream.ToArray()); + privateBase64String = Convert.ToBase64String(puttyV3Encryption.Result).FormatNewLines(64); + + using var hmac = new HMACSHA256(puttyV3Encryption.MacKey); + macHash = hmac.ComputeHash(hashData); + } var s = new StringWriter(); - s.Write($"PuTTY-User-Key-File-2: {key}\n"); + if (sshKeyFormat is SshKeyFormat.PuTTYv2) + { + s.Write($"PuTTY-User-Key-File-2: {key}\n"); + s.Write($"Encryption: {encryption.CipherName}\n"); + s.Write($"Comment: {key.Comment}\n"); + s.Write($"Public-Lines: {publicBase64String.Split('\n').Length}\n"); + s.Write($"{publicBase64String}\n"); + s.Write($"Private-Lines: {privateBase64String.Split('\n').Length}\n"); + s.Write($"{privateBase64String}\n"); + s.Write($"Private-MAC: {BitConverter.ToString(macHash).Replace("-", "").ToLower()}\n"); + return s.ToString(); + } + + s.Write($"PuTTY-User-Key-File-3: {key}\n"); s.Write($"Encryption: {encryption.CipherName}\n"); - s.Write($"Comment: {key.Comment ?? ""}\n"); + s.Write($"Comment: {key.Comment}\n"); s.Write($"Public-Lines: {publicBase64String.Split('\n').Length}\n"); s.Write($"{publicBase64String}\n"); + if (puttyV3Encryption?.Salt is not null) + { + s.Write($"Key-Derivation: {puttyV3Encryption.KeyDerivation}\n"); + s.Write($"Argon2-Memory: {puttyV3Encryption.MemorySize}\n"); + s.Write($"Argon2-Passes: {puttyV3Encryption.Iterations}\n"); + s.Write($"Argon2-Parallelism: {puttyV3Encryption.DegreeOfParallelism}\n"); + s.Write($"Argon2-Salt: {BitConverter.ToString(puttyV3Encryption.Salt).Replace("-", "").ToLower()}\n"); + } s.Write($"Private-Lines: {privateBase64String.Split('\n').Length}\n"); s.Write($"{privateBase64String}\n"); s.Write($"Private-MAC: {BitConverter.ToString(macHash).Replace("-", "").ToLower()}\n"); + return s.ToString(); } diff --git a/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs b/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs index 41de1fd..af3b359 100644 --- a/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs +++ b/SshNet.Keygen/Extensions/PrivateKeyFileExtension.cs @@ -25,7 +25,28 @@ public static string Fingerprint(this IPrivateKeySource keyFile, SshKeyHashAlgor public static string ToPublic(this IPrivateKeySource keyFile) { - return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPublic(); + var keyFormat = SshKeyGenerateInfo.DefaultSshKeyFormat; + if (keyFile is GeneratedPrivateKey generatedPrivateKey) + keyFormat = generatedPrivateKey.Info.KeyFormat; + + return keyFile.ToPublic(keyFormat); + } + + public static string ToPublic(this IPrivateKeySource keyFile, SshKeyFormat sshKeyFormat) + { + return sshKeyFormat is SshKeyFormat.PuTTYv2 or SshKeyFormat.PuTTYv3 + ? keyFile.ToPuttyPublicFormat() + : ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPublic(); + } + + public static string ToOpenSshPublicFormat(this IPrivateKeySource keyFile) + { + return keyFile.ToPublic(SshKeyFormat.OpenSSH); + } + + public static string ToPuttyPublicFormat(this IPrivateKeySource keyFile) + { + return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPuttyPublicFormat(); } #endregion @@ -38,7 +59,12 @@ public static string ToOpenSshFormat(this IPrivateKeySource keyFile) if (keyFile is GeneratedPrivateKey generatedPrivateKey) encryption = generatedPrivateKey.Info.Encryption; - return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToOpenSshFormat(encryption); + return keyFile.ToOpenSshFormat(encryption); + } + + public static string ToOpenSshFormat(this IPrivateKeySource keyFile, string passphrase) + { + return keyFile.ToOpenSshFormat(new SshKeyEncryptionAes256(passphrase)); } public static string ToOpenSshFormat(this IPrivateKeySource keyFile, ISshKeyEncryption encryption) @@ -51,17 +77,44 @@ public static string ToOpenSshFormat(this IPrivateKeySource keyFile, ISshKeyEncr #region PuttyFormat public static string ToPuttyFormat(this IPrivateKeySource keyFile) + { + var sshKeyFormat = SshKeyFormat.PuTTYv3; + if (keyFile is GeneratedPrivateKey generatedPrivateKey) + { + if (generatedPrivateKey.Info.KeyFormat is SshKeyFormat.PuTTYv2 or SshKeyFormat.PuTTYv3) + sshKeyFormat = generatedPrivateKey.Info.KeyFormat; + } + + return keyFile.ToPuttyFormat(sshKeyFormat); + } + + public static string ToPuttyFormat(this IPrivateKeySource keyFile, string passphrase) + { + return keyFile.ToPuttyFormat(new SshKeyEncryptionAes256(passphrase), SshKeyFormat.PuTTYv3); + } + + public static string ToPuttyFormat(this IPrivateKeySource keyFile, string passphrase, SshKeyFormat sshKeyFormat) + { + return keyFile.ToPuttyFormat(new SshKeyEncryptionAes256(passphrase), sshKeyFormat); + } + + public static string ToPuttyFormat(this IPrivateKeySource keyFile, SshKeyFormat sshKeyFormat) { var encryption = SshKeyGenerateInfo.DefaultSshKeyEncryption; if (keyFile is GeneratedPrivateKey generatedPrivateKey) encryption = generatedPrivateKey.Info.Encryption; - return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPuttyFormat(encryption); + return keyFile.ToPuttyFormat(encryption, sshKeyFormat); } public static string ToPuttyFormat(this IPrivateKeySource keyFile, ISshKeyEncryption encryption) { - return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPuttyFormat(encryption); + return keyFile.ToPuttyFormat(encryption, SshKeyFormat.PuTTYv3); + } + + public static string ToPuttyFormat(this IPrivateKeySource keyFile, ISshKeyEncryption encryption, SshKeyFormat sshKeyFormat) + { + return ((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.ToPuttyFormat(encryption, sshKeyFormat); } #endregion diff --git a/SshNet.Keygen/SshKey.cs b/SshNet.Keygen/SshKey.cs index 9f5f616..e961975 100644 --- a/SshNet.Keygen/SshKey.cs +++ b/SshNet.Keygen/SshKey.cs @@ -30,8 +30,9 @@ public static GeneratedPrivateKey Generate(Stream stream, SshKeyGenerateInfo inf case SshKeyFormat.OpenSSH: writer.Write(key.ToOpenSshFormat(info.Encryption)); break; - case SshKeyFormat.PuTTY: - writer.Write(key.ToPuttyFormat(info.Encryption)); + case SshKeyFormat.PuTTYv2: + case SshKeyFormat.PuTTYv3: + writer.Write(key.ToPuttyFormat(info.Encryption, info.KeyFormat)); break; default: throw new NotSupportedException($"Not supported Key Format {info.KeyFormat}"); diff --git a/SshNet.Keygen/SshKeyEncryption/ISshKeyEncryption.cs b/SshNet.Keygen/SshKeyEncryption/ISshKeyEncryption.cs index d4c31dd..854fdbf 100644 --- a/SshNet.Keygen/SshKeyEncryption/ISshKeyEncryption.cs +++ b/SshNet.Keygen/SshKeyEncryption/ISshKeyEncryption.cs @@ -16,8 +16,12 @@ public interface ISshKeyEncryption public byte[] Encrypt(byte[] data, int offset, int length); - public byte[] PuttyEncrypt(byte[] data); + public byte[] PuttyV2Encrypt(byte[] data); - public byte[] PuttyEncrypt(byte[] data, int offset, int length); + public byte[] PuttyV2Encrypt(byte[] data, int offset, int length); + + public PuttyV3Encryption PuttyV3Encrypt(byte[] data); + + public PuttyV3Encryption PuttyV3Encrypt(byte[] data, int offset, int length); } } \ No newline at end of file diff --git a/SshNet.Keygen/SshKeyEncryption/PuttyV3Encryption.cs b/SshNet.Keygen/SshKeyEncryption/PuttyV3Encryption.cs new file mode 100644 index 0000000..7c48abe --- /dev/null +++ b/SshNet.Keygen/SshKeyEncryption/PuttyV3Encryption.cs @@ -0,0 +1,26 @@ +namespace SshNet.Keygen.SshKeyEncryption +{ + public enum ArgonKeyDerivation + { + Argon2d, + Argon2i, + Argon2id + } + + public class PuttyV3Encryption + { + internal byte[]? Result; + internal byte[] MacKey; + internal byte[]? Salt; + + public ArgonKeyDerivation KeyDerivation = ArgonKeyDerivation.Argon2id; + public int DegreeOfParallelism = 1; + public int MemorySize = 8192; + public int Iterations = 22; + + public PuttyV3Encryption() + { + MacKey = new byte[0]; + } + } +} \ No newline at end of file diff --git a/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionAes256.cs b/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionAes256.cs index bcdcde2..43024cc 100644 --- a/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionAes256.cs +++ b/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionAes256.cs @@ -3,6 +3,7 @@ using System.IO; using System.Security.Cryptography; using System.Text; +using Konscious.Security.Cryptography; using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; using Renci.SshNet.Security.Cryptography.Ciphers.Paddings; @@ -22,6 +23,7 @@ public class SshKeyEncryptionAes256 : ISshKeyEncryption public string KdfName => "bcrypt"; public int BlockSize => 16; public string Passphrase => _passphrase; + public PuttyV3Encryption PuttyV3Encryption => _puttyV3Encryption; private const int SaltLen = 16; private const int Rounds = 16; @@ -29,15 +31,18 @@ public class SshKeyEncryptionAes256 : ISshKeyEncryption private readonly byte[] _passPhraseBytes; private readonly byte[] _salt; private readonly string _passphrase; + private readonly PuttyV3Encryption _puttyV3Encryption; - public SshKeyEncryptionAes256(string passphrase) + public SshKeyEncryptionAes256(string passphrase, PuttyV3Encryption? puttyV3Encryption = null) { _passphrase = passphrase; _passPhraseBytes = Encoding.ASCII.GetBytes(passphrase); _salt = new byte[SaltLen]; + _puttyV3Encryption = puttyV3Encryption ?? new PuttyV3Encryption(); } - public SshKeyEncryptionAes256(string passphrase, Aes256Mode mode) : this(passphrase) + public SshKeyEncryptionAes256(string passphrase, Aes256Mode mode, PuttyV3Encryption? puttyV3Encryption = null) + : this(passphrase, puttyV3Encryption) { _mode = mode; } @@ -84,7 +89,7 @@ public byte[] Encrypt(byte[] data, int offset, int length) return Encrypt(buffer); } - public byte[] PuttyEncrypt(byte[] data) + public byte[] PuttyV2Encrypt(byte[] data) { using var sha1 = SHA1.Create(); @@ -119,11 +124,62 @@ public byte[] PuttyEncrypt(byte[] data) return cipher.Encrypt(data); } - public byte[] PuttyEncrypt(byte[] data, int offset, int length) + public byte[] PuttyV2Encrypt(byte[] data, int offset, int length) { var buffer = new byte[length]; Array.Copy(data, offset, buffer, 0, length); - return PuttyEncrypt(buffer); + return PuttyV2Encrypt(buffer); + } + + public PuttyV3Encryption PuttyV3Encrypt(byte[] data) + { + Argon2 argon2 = _puttyV3Encryption.KeyDerivation switch + { + ArgonKeyDerivation.Argon2d => new Argon2d(_passPhraseBytes), + ArgonKeyDerivation.Argon2i => new Argon2i(_passPhraseBytes), + ArgonKeyDerivation.Argon2id => new Argon2id(_passPhraseBytes), + _ => throw new NotSupportedException($"Encryption Key Derivation {_puttyV3Encryption.KeyDerivation} is not supported.") + }; + + argon2.DegreeOfParallelism = _puttyV3Encryption.DegreeOfParallelism; + argon2.MemorySize = _puttyV3Encryption.MemorySize; + argon2.Iterations = _puttyV3Encryption.Iterations; + + using var rng = new RNGCryptoServiceProvider(); + rng.GetBytes(_salt); + argon2.Salt = _salt; + _puttyV3Encryption.Salt = _salt; + + var cipherKeyComplete = argon2.GetBytes(80); + var cipherKey = new byte[32]; + var crcIv = new byte[16]; + var macKey = new byte[32]; + Buffer.BlockCopy(cipherKeyComplete, 0, cipherKey, 0, cipherKey.Length); + Buffer.BlockCopy(cipherKeyComplete, 32, crcIv, 0, crcIv.Length); + Buffer.BlockCopy(cipherKeyComplete, 48, macKey, 0, macKey.Length); + + AesCipher cipher; + switch(_mode) + { + case Aes256Mode.CTR: + throw new NotSupportedException($"Unsupported AES Mode: {_mode}"); + default: + _mode = Aes256Mode.CBC; + cipher = new AesCipher(cipherKey, crcIv, AesCipherMode.CBC); + break; + } + + _puttyV3Encryption.Result = cipher.Encrypt(data); + _puttyV3Encryption.MacKey = macKey; + + return _puttyV3Encryption; + } + + public PuttyV3Encryption PuttyV3Encrypt(byte[] data, int offset, int length) + { + var buffer = new byte[length]; + Array.Copy(data, offset, buffer, 0, length); + return PuttyV3Encrypt(buffer); } } } \ No newline at end of file diff --git a/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionNone.cs b/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionNone.cs index 39d06ba..0d4d086 100644 --- a/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionNone.cs +++ b/SshNet.Keygen/SshKeyEncryption/SshKeyEncryptionNone.cs @@ -26,14 +26,24 @@ public byte[] Encrypt(byte[] data, int offset, int length) return Encrypt(buffer); } - public byte[] PuttyEncrypt(byte[] data) + public byte[] PuttyV2Encrypt(byte[] data) { return Encrypt(data); } - public byte[] PuttyEncrypt(byte[] data, int offset, int length) + public byte[] PuttyV2Encrypt(byte[] data, int offset, int length) { return Encrypt(data, offset, length); } + + public PuttyV3Encryption PuttyV3Encrypt(byte[] data) + { + return new PuttyV3Encryption { Result = Encrypt(data) }; + } + + public PuttyV3Encryption PuttyV3Encrypt(byte[] data, int offset, int length) + { + return new PuttyV3Encryption { Result = Encrypt(data, offset, length) }; + } } } \ No newline at end of file diff --git a/SshNet.Keygen/SshKeyFormat.cs b/SshNet.Keygen/SshKeyFormat.cs index 036db7e..0dabac3 100644 --- a/SshNet.Keygen/SshKeyFormat.cs +++ b/SshNet.Keygen/SshKeyFormat.cs @@ -3,6 +3,7 @@ public enum SshKeyFormat { OpenSSH, - PuTTY + PuTTYv2, + PuTTYv3, } } \ No newline at end of file diff --git a/SshNet.Keygen/SshNet.Keygen.csproj b/SshNet.Keygen/SshNet.Keygen.csproj index c45ab02..7122b1c 100644 --- a/SshNet.Keygen/SshNet.Keygen.csproj +++ b/SshNet.Keygen/SshNet.Keygen.csproj @@ -5,7 +5,7 @@ 9 enable SshNet.Keygen - 2024.0.0-beta + 2024.0.0.1-beta $(Version) ssh;scp;sftp SSH.NET Extension to generate and export Authentication Keys in OpenSSH and PuTTY Format. @@ -25,6 +25,7 @@ +