diff --git a/DearFTP/Configurations/Configuration.cs b/DearFTP/Configurations/Configuration.cs index f74fa1f..8d84d7b 100644 --- a/DearFTP/Configurations/Configuration.cs +++ b/DearFTP/Configurations/Configuration.cs @@ -1,8 +1,9 @@ -using System; -using System.Collections.Generic; +using DearFTP.Utils; +using System; using System.IO; using System.Linq; -using System.Text; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using YamlDotNet.Serialization; namespace DearFTP.Configurations @@ -31,6 +32,7 @@ namespace DearFTP.Configurations } public ServerConfiguration Server { get; set; } = new ServerConfiguration(); + public TlsConfiguration Tls { get; set; } = new TlsConfiguration(); public Share[] Shares { get; set; } = Array.Empty(); public User[] Users { get; set; } = Array.Empty(); @@ -58,6 +60,49 @@ namespace DearFTP.Configurations return false; } + if (Tls.ForceTls && !Tls.AllowTls) + { + Console.WriteLine("Tls is forced but not allowed."); + return false; + } + + if (Tls.AllowTls) + { + if (string.IsNullOrWhiteSpace(Tls.CertificatePath)) + { + Console.WriteLine("Tls is activated, but no certificate is specified."); + return false; + } + else + { + try + { + var certificate = new X509Certificate2(Tls.CertificatePath); + + if (!certificate.HasPrivateKey) + { + if (string.IsNullOrWhiteSpace(Tls.PrivateKeyPath)) + { + Console.WriteLine("No private key loaded and no path is specified."); + return false; + } + + var privateKeyBytes = OpenSslKey.DecodePkcs8PrivateKey(File.ReadAllText(Tls.PrivateKeyPath)); + var privateKey = OpenSslKey.DecodePrivateKeyInfo(privateKeyBytes); + + certificate = certificate.CopyWithPrivateKey(privateKey); + } + + Tls.X509Certificate = certificate; + } + catch (Exception e) + { + Console.WriteLine($"Can't load certificate: {e.Message}"); + return false; + } + } + } + return true; } diff --git a/DearFTP/Configurations/TlsConfiguration.cs b/DearFTP/Configurations/TlsConfiguration.cs new file mode 100644 index 0000000..b9306ca --- /dev/null +++ b/DearFTP/Configurations/TlsConfiguration.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using YamlDotNet.Serialization; + +namespace DearFTP.Configurations +{ + class TlsConfiguration + { + public bool AllowTls { get; set; } = false; + public bool ForceTls { get; set; } = false; + public string CertificatePath { get; set; } = ""; + public string PrivateKeyPath { get; set; } = ""; + + [YamlIgnore()] + public X509Certificate2 X509Certificate { get; set; } + } +} diff --git a/DearFTP/Connection/Commands/AuthCommand.cs b/DearFTP/Connection/Commands/AuthCommand.cs new file mode 100644 index 0000000..720ae2c --- /dev/null +++ b/DearFTP/Connection/Commands/AuthCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace DearFTP.Connection.Commands +{ + class AuthCommand : ICommand + { + public string[] Aliases { get; } = new string[] + { + "AUTH" + }; + + public void Execute(Session session, FtpStream stream, string alias, string argument) + { + string protocol = argument.ToUpper(); + + if (protocol != "TLS" && protocol != "TLS-C" && protocol != "SSL") + { + stream.Send(ResponseCode.ArgumentNotImplemented, "Invalid argument, expected 'TLS'"); + return; + } + + var tlsConfiguration = session.Configuration.Tls; + + if (!tlsConfiguration.AllowTls) + { + session.LogError("Tls", "Client tried to use Tls but Tls is desactivated"); + stream.Send(ResponseCode.NotImplemented, "Tls is not enabled on this server"); + return; + } + + stream.Send(ResponseCode.AcceptAuthenticationMechanism, "Tls activated."); + + session.ActivateTls(); + } + } +} diff --git a/DearFTP/Connection/Commands/CommandsDispatcher.cs b/DearFTP/Connection/Commands/CommandsDispatcher.cs index fc130b5..ab7c324 100644 --- a/DearFTP/Connection/Commands/CommandsDispatcher.cs +++ b/DearFTP/Connection/Commands/CommandsDispatcher.cs @@ -9,6 +9,7 @@ namespace DearFTP.Connection.Commands { public ICommand[] Commands { get; } = new ICommand[] { + new AuthCommand(), new ClntCommand(), new CwdCommand(), new DeleteCommand(), @@ -21,6 +22,8 @@ namespace DearFTP.Connection.Commands new OptionsCommand(), new ParentDirectoryCommand(), new PassiveCommand(), + new ProtectionBufferSizeCommand(), + new ProtectionCommand(), new PwdCommand(), new QuitCommand(), new RenameCommand(), @@ -43,15 +46,21 @@ namespace DearFTP.Connection.Commands } var commandExecutor = Commands.FirstOrDefault(x => x.Aliases.Contains(command, StringComparer.OrdinalIgnoreCase)); + var stream = session.FtpStream; if (commandExecutor == null) { - session.FtpStream.Send(ResponseCode.NotImplemented, $"Command '{command}' not implemented or invalid"); - + stream.Send(ResponseCode.NotImplemented, $"Command '{command}' not implemented or invalid"); return; } - commandExecutor.Execute(session, session.FtpStream, command, argument); + if (session.Configuration.Tls.ForceTls && !session.IsTlsProtected && command.ToUpper() != "AUTH") + { + stream.Send(ResponseCode.InsufficientProtection, "Not protected connection is not allowed on this server."); + return; + } + + commandExecutor.Execute(session, stream, command, argument); } } } diff --git a/DearFTP/Connection/Commands/FeaturesCommand.cs b/DearFTP/Connection/Commands/FeaturesCommand.cs index 82c6fd6..efdb4e7 100644 --- a/DearFTP/Connection/Commands/FeaturesCommand.cs +++ b/DearFTP/Connection/Commands/FeaturesCommand.cs @@ -18,9 +18,12 @@ namespace DearFTP.Connection.Commands ( ResponseCode.SystemStatusOrHelpReply, "Features:", + "AUTH TLS", "MDTM", "MLST", "PASV", + "PBSZ", + "PROT", "REST STREAM", "SIZE", "TVFS", diff --git a/DearFTP/Connection/Commands/HelpCommand.cs b/DearFTP/Connection/Commands/HelpCommand.cs index 7549f93..dc6eabd 100644 --- a/DearFTP/Connection/Commands/HelpCommand.cs +++ b/DearFTP/Connection/Commands/HelpCommand.cs @@ -22,6 +22,7 @@ namespace DearFTP.Connection.Commands "ABOR", "ALLO", "APPE", + "AUTH", "CDUP", "CWD", "DELE", @@ -38,7 +39,9 @@ namespace DearFTP.Connection.Commands "OPTS", "PASS", "PASV", + "PBSZ", "PORT", + "PROT", "PWD", "QUIT", "REIN", diff --git a/DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs b/DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs new file mode 100644 index 0000000..be8bc79 --- /dev/null +++ b/DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DearFTP.Connection.Commands +{ + class ProtectionBufferSizeCommand : ICommand + { + public string[] Aliases { get; } = new string[] + { + "PBSZ" + }; + + public void Execute(Session session, FtpStream stream, string alias, string argument) + { + if (argument != "0") + { + stream.Send(ResponseCode.ArgumentsError, "Invalid argument, expected '0'"); + return; + } + + stream.Send(ResponseCode.OK, "Ok."); + } + } +} diff --git a/DearFTP/Connection/Commands/ProtectionCommand.cs b/DearFTP/Connection/Commands/ProtectionCommand.cs new file mode 100644 index 0000000..6cddd44 --- /dev/null +++ b/DearFTP/Connection/Commands/ProtectionCommand.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DearFTP.Connection.Commands +{ + class ProtectionCommand : ICommand + { + public string[] Aliases { get; } = new string[] + { + "PROT" + }; + + public void Execute(Session session, FtpStream stream, string alias, string argument) + { + switch (argument.ToUpper()) + { + case "C": + session.DataConnection.DesactivateTsl(); + stream.Send(ResponseCode.OK, "Data protection cleared."); + break; + case "P": + session.DataConnection.ActivateTsl(); + stream.Send(ResponseCode.OK, "Data protection set."); + break; + default: + stream.Send(ResponseCode.ArgumentsError, "Invalid argument."); + break; + } + } + } +} diff --git a/DearFTP/Connection/Commands/RetrieveCommand.cs b/DearFTP/Connection/Commands/RetrieveCommand.cs index 955129b..8f7b13c 100644 --- a/DearFTP/Connection/Commands/RetrieveCommand.cs +++ b/DearFTP/Connection/Commands/RetrieveCommand.cs @@ -18,7 +18,7 @@ namespace DearFTP.Connection.Commands { var dataConnection = session.DataConnection; - if (!dataConnection.IsAvailable) + if (!dataConnection.IsTslProtected && !dataConnection.IsAvailable) { stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated."); return; @@ -40,6 +40,12 @@ namespace DearFTP.Connection.Commands stream.Send(ResponseCode.FileStatusOK, "File coming."); + if (dataConnection.IsTslProtected && !dataConnection.IsAvailable) + { + stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated."); + return; + } + int restartPosition = session.RestartPosition; session.RestartPosition = 0; diff --git a/DearFTP/Connection/Commands/StoreCommand.cs b/DearFTP/Connection/Commands/StoreCommand.cs index e7a7a9f..459f0af 100644 --- a/DearFTP/Connection/Commands/StoreCommand.cs +++ b/DearFTP/Connection/Commands/StoreCommand.cs @@ -22,7 +22,7 @@ namespace DearFTP.Connection.Commands { var dataConnection = session.DataConnection; - if (!dataConnection.IsAvailable) + if (!dataConnection.IsTslProtected && !dataConnection.IsAvailable) { stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated."); return; @@ -52,6 +52,12 @@ namespace DearFTP.Connection.Commands stream.Send(ResponseCode.FileStatusOK, "Waiting file."); + if (dataConnection.IsTslProtected && !dataConnection.IsAvailable) + { + stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated."); + return; + } + ReceiveFile(dataConnection.Stream, realPath, alias.ToUpper() == "APPE"); dataConnection.Close(); @@ -59,7 +65,7 @@ namespace DearFTP.Connection.Commands stream.Send(ResponseCode.CloseDataConnection, "File received."); } - private void ReceiveFile(NetworkStream stream, string path, bool append) + private void ReceiveFile(Stream stream, string path, bool append) { using (var file = File.Open(path, append ? FileMode.Append : FileMode.Create)) { diff --git a/DearFTP/Connection/DataConnection.cs b/DearFTP/Connection/DataConnection.cs index 36b6f01..ab96873 100644 --- a/DearFTP/Connection/DataConnection.cs +++ b/DearFTP/Connection/DataConnection.cs @@ -1,6 +1,9 @@ -using System; +using DearFTP.Configurations; +using System; using System.Collections.Generic; +using System.IO; using System.Net; +using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Threading; @@ -10,11 +13,12 @@ namespace DearFTP.Connection { class DataConnection { - public const int Timeout = 10_000; + public const int Timeout = 100_000; public TcpListener Listener { get; private set; } public TcpClient Client { get; private set; } - public NetworkStream Stream { get; private set; } + public Stream Stream { get; private set; } + public bool IsTslProtected { get; private set; } public int Port => ((IPEndPoint)Listener.LocalEndpoint).Port; public bool IsAvailable { @@ -41,7 +45,7 @@ namespace DearFTP.Connection public DataConnection() { - + IsTslProtected = false; } public void Create() @@ -56,15 +60,36 @@ namespace DearFTP.Connection Listener.Start(); } - public void AcceptClient() + public void AcceptClient(bool authenticateAfter = false) { _acceptTask = Listener.AcceptTcpClientAsync().ContinueWith(t => { Client = t.Result; - Stream = Client.GetStream(); + + if (IsTslProtected) + { + var sslStream = new SslStream(Client.GetStream(), false); + sslStream.AuthenticateAsServer(FtpServer.Instance.Configuration.Tls.X509Certificate, false, true); + + Stream = sslStream; + } + else + { + Stream = Client.GetStream(); + } }); } + public void ActivateTsl() + { + IsTslProtected = true; + } + + public void DesactivateTsl() + { + IsTslProtected = false; + } + public void Close() { Stream.Close(); diff --git a/DearFTP/Connection/FtpStream.cs b/DearFTP/Connection/FtpStream.cs index b5e0653..400a4c8 100644 --- a/DearFTP/Connection/FtpStream.cs +++ b/DearFTP/Connection/FtpStream.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Sockets; using System.Text; @@ -10,18 +11,18 @@ namespace DearFTP.Connection { public const int BUFFER_SIZE = 4096; - public NetworkStream NetworkStream { get; } + public Stream Stream { get; } - public FtpStream(NetworkStream networkStream) + public FtpStream(Stream stream) { - NetworkStream = networkStream; + Stream = stream; } public (string command, string argument) Receive() { var buffer = new byte[BUFFER_SIZE]; - int readBytes = NetworkStream.Read(buffer, 0, buffer.Length); + int readBytes = Stream.Read(buffer, 0, buffer.Length); if (readBytes == 0) { @@ -49,14 +50,14 @@ namespace DearFTP.Connection { var bytes = Encoding.UTF8.GetBytes($"{message}{(end ? "\r\n" : "")}"); - NetworkStream.Write(bytes, 0, bytes.Length); + Stream.Write(bytes, 0, bytes.Length); } public void Send(ResponseCode code, string argument) { var bytes = Encoding.UTF8.GetBytes($"{(uint)code} {argument}\r\n"); - NetworkStream.Write(bytes, 0, bytes.Length); + Stream.Write(bytes, 0, bytes.Length); } public void Send(ResponseCode code, string message, params string[] arguments) @@ -74,7 +75,7 @@ namespace DearFTP.Connection var bytes = Encoding.UTF8.GetBytes(builder.ToString()); - NetworkStream.Write(bytes, 0, bytes.Length); + Stream.Write(bytes, 0, bytes.Length); } } } diff --git a/DearFTP/Connection/ResponseCode.cs b/DearFTP/Connection/ResponseCode.cs index 77393c7..709e6a1 100644 --- a/DearFTP/Connection/ResponseCode.cs +++ b/DearFTP/Connection/ResponseCode.cs @@ -175,6 +175,10 @@ namespace DearFTP.Connection /// ArgumentNotImplemented = 504, /// + /// Current level of protection is insufficient, need TLS/SSL + /// + InsufficientProtection = 522, + /// /// Not logged in. /// NotLoggedIn = 530, diff --git a/DearFTP/Connection/Session.cs b/DearFTP/Connection/Session.cs index ee42d77..88c9435 100644 --- a/DearFTP/Connection/Session.cs +++ b/DearFTP/Connection/Session.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Security; using System.Net.Sockets; using System.Text; @@ -20,14 +21,17 @@ namespace DearFTP.Connection public Share[] Shares { get; set; } public Share[] WritablesShares { get; set; } public NavigablePath NavigablePath { get; set; } - public FtpStream FtpStream { get; } + public FtpStream FtpStream { get; private set; } public DataConnection DataConnection { get; set; } public int RestartPosition { get; set; } + public bool IsTlsProtected { get; private set; } public string CurrentWorkingDirectory => NavigablePath.CurrentDirectory; + public string IP => ((IPEndPoint)_client.Client.LocalEndPoint).Address.ToString(); private TcpClient _client; private NetworkStream _networkStream; + private SslStream _sslStream; private bool _isRunning = true; public Session(TcpClient client) @@ -41,6 +45,7 @@ namespace DearFTP.Connection Logger = FtpServer.Instance.Logger; DataConnection = new DataConnection(); RestartPosition = 0; + IsTlsProtected = false; } public void Start() @@ -50,7 +55,7 @@ namespace DearFTP.Connection while (_isRunning) { (string command, string argument) = FtpStream.Receive(); - Logger.Log($"[{_client.Client.RemoteEndPoint}]: {command} {argument}"); + Log($"{command} {argument}"); CommandsDispatcher.Dispatch(this, command, argument); } @@ -63,29 +68,30 @@ namespace DearFTP.Connection Dispose(); } - public string GetRealPath(string path) + public void Log(string message) { - string completePath = Path.Combine(CurrentWorkingDirectory, path); + Logger.Log($"[{_client.Client.RemoteEndPoint}]: {message}"); + } - string[] split = CurrentWorkingDirectory.Substring(1).Split('/'); + public void LogError(string error, string description) + { + Logger.LogError(error, $"[{_client.Client.RemoteEndPoint}]: {description}"); + } - if (split.Length == 0) - { - return null; - } + public void ActivateTls() + { + _sslStream = new SslStream(_networkStream, true); - var share = Shares.FirstOrDefault(x => x.Name == split[0]); + _sslStream.AuthenticateAsServer(Configuration.Tls.X509Certificate, false, true); - if (share == null) - { - return null; - } + FtpStream = new FtpStream(_sslStream); - return Path.Combine(split.Skip(1).Prepend(share.Path).ToArray()); + IsTlsProtected = true; } public void Dispose() { + _sslStream?.Close(); _networkStream.Close(); _client.Close(); } diff --git a/DearFTP/Utils/OpenSslKey.cs b/DearFTP/Utils/OpenSslKey.cs new file mode 100644 index 0000000..40d108c --- /dev/null +++ b/DearFTP/Utils/OpenSslKey.cs @@ -0,0 +1,1070 @@ +//********************************************************************************** +// +//OpenSSLKey +// .NET 2.0 OpenSSL Public & Private Key Parser +// +// Copyright (C) 2008 JavaScience Consulting +// +//*********************************************************************************** +// +// opensslkey.cs +// +// Reads and parses: +// (1) OpenSSL PEM or DER public keys +// (2) OpenSSL PEM or DER traditional SSLeay private keys (encrypted and unencrypted) +// (3) PKCS #8 PEM or DER encoded private keys (encrypted and unencrypted) +// Keys in PEM format must have headers/footers . +// Encrypted Private Key in SSLEay format not supported in DER +// Removes header/footer lines. +// For traditional SSLEAY PEM private keys, checks for encrypted format and +// uses PBE to extract 3DES key. +// For SSLEAY format, only supports encryption format: DES-EDE3-CBC +// For PKCS #8, only supports PKCS#5 v2.0 3des. +// Parses private and public key components and returns .NET RSA object. +// Creates dummy unsigned certificate linked to private keypair and +// optionally exports to pkcs #12 +// +// See also: +// http://www.openssl.org/docs/crypto/pem.html#PEM_ENCRYPTION_FORMAT +//************************************************************************************** + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; + + +namespace DearFTP.Utils +{ + [StructLayout(LayoutKind.Sequential)] + public struct CRYPT_KEY_PROV_INFO + { + [MarshalAs(UnmanagedType.LPWStr)] public string pwszContainerName; + [MarshalAs(UnmanagedType.LPWStr)] public string pwszProvName; + public uint dwProvType; + public uint dwFlags; + public uint cProvParam; + public IntPtr rgProvParam; + public uint dwKeySpec; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CERT_NAME_BLOB + { + public int cbData; + public IntPtr pbData; + } + + public class OpenSslKey + { + + public const string Pemprivheader = "-----BEGIN RSA PRIVATE KEY-----"; + public const string Pemprivfooter = "-----END RSA PRIVATE KEY-----"; + public const string Pempubheader = "-----BEGIN PUBLIC KEY-----"; + public const string Pempubfooter = "-----END PUBLIC KEY-----"; + public const string Pemp8header = "-----BEGIN PRIVATE KEY-----\n"; + public const string Pemp8footer = "-----END PRIVATE KEY-----\n"; + public const string Pemp8encheader = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + public const string Pemp8encfooter = "-----END ENCRYPTED PRIVATE KEY-----"; + + private static bool _verbose = false; + + // ------- Decode PEM pubic, private or pkcs8 key ---------------- + public static void DecodePEMKey(string pemstr) + { + byte[] pempublickey; + byte[] pemprivatekey; + byte[] pkcs8privatekey; + byte[] pkcs8encprivatekey; + + if (pemstr.StartsWith(Pempubheader) && pemstr.EndsWith(Pempubfooter)) + { + Console.WriteLine("Trying to decode and parse a PEM public key .."); + pempublickey = DecodeOpenSSLPublicKey(pemstr); + if (pempublickey != null) + { + if (_verbose) + ShowBytes("\nRSA public key", pempublickey); + //PutFileBytes("rsapubkey.pem", pempublickey, pempublickey.Length) ; + RSACryptoServiceProvider rsa = DecodeX509PublicKey(pempublickey); + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlpublickey = rsa.ToXmlString(false); + Console.WriteLine("\nXML RSA public key: {0} bits\n{1}\n", rsa.KeySize, xmlpublickey); + } + } + else if (pemstr.StartsWith(Pemprivheader) && pemstr.EndsWith(Pemprivfooter)) + { + Console.WriteLine("Trying to decrypt and parse a PEM private key .."); + pemprivatekey = DecodeOpenSSLPrivateKey(pemstr); + if (pemprivatekey != null) + { + if (_verbose) + ShowBytes("\nRSA private key", pemprivatekey); + //PutFileBytes("rsaprivkey.pem", pemprivatekey, pemprivatekey.Length) ; + RSACryptoServiceProvider rsa = DecodeRSAPrivateKey(pemprivatekey); + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlprivatekey = rsa.ToXmlString(true); + Console.WriteLine("\nXML RSA private key: {0} bits\n{1}\n", rsa.KeySize, xmlprivatekey); + ProcessRSA(rsa); + } + } + else if (pemstr.StartsWith(Pemp8header) && pemstr.EndsWith(Pemp8footer)) + { + Console.WriteLine("Trying to decode and parse as PEM PKCS #8 PrivateKeyInfo .."); + pkcs8privatekey = DecodePkcs8PrivateKey(pemstr); + if (pkcs8privatekey != null) + { + if (_verbose) + ShowBytes("\nPKCS #8 PrivateKeyInfo", pkcs8privatekey); + //PutFileBytes("PrivateKeyInfo", pkcs8privatekey, pkcs8privatekey.Length) ; + RSACryptoServiceProvider rsa = DecodePrivateKeyInfo(pkcs8privatekey); + if (rsa != null) + { + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlprivatekey = rsa.ToXmlString(true); + Console.WriteLine("\nXML RSA private key: {0} bits\n{1}\n", rsa.KeySize, xmlprivatekey); + ProcessRSA(rsa); + } + else + Console.WriteLine("\nFailed to create an RSACryptoServiceProvider"); + } + } + else if (pemstr.StartsWith(Pemp8encheader) && pemstr.EndsWith(Pemp8encfooter)) + { + Console.WriteLine("Trying to decode and parse as PEM PKCS #8 EncryptedPrivateKeyInfo .."); + pkcs8encprivatekey = DecodePkcs8EncPrivateKey(pemstr); + if (pkcs8encprivatekey != null) + { + if (_verbose) + ShowBytes("\nPKCS #8 EncryptedPrivateKeyInfo", pkcs8encprivatekey); + //PutFileBytes("EncryptedPrivateKeyInfo", pkcs8encprivatekey, pkcs8encprivatekey.Length) ; + RSACryptoServiceProvider rsa = DecodeEncryptedPrivateKeyInfo(pkcs8encprivatekey); + if (rsa != null) + { + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlprivatekey = rsa.ToXmlString(true); + Console.WriteLine("\nXML RSA private key: {0} bits\n{1}\n", rsa.KeySize, xmlprivatekey); + ProcessRSA(rsa); + } + else + Console.WriteLine("\nFailed to create an RSACryptoServiceProvider"); + } + } + else + { + Console.WriteLine("Not a PEM public, private key or a PKCS #8"); + return; + } + } + + // ------- Decode PEM pubic, private or pkcs8 key ---------------- + public static void DecodeDERKey(string filename) + { + RSACryptoServiceProvider rsa; + byte[] keyblob = GetFileBytes(filename); + + if (keyblob == null) + return; + + rsa = DecodeX509PublicKey(keyblob); + if (rsa != null) + { + Console.WriteLine("\nA valid SubjectPublicKeyInfo\n"); + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlpublickey = rsa.ToXmlString(false); + Console.WriteLine("\nXML RSA public key: {0} bits\n{1}\n", rsa.KeySize, xmlpublickey); + return; + } + + rsa = DecodeRSAPrivateKey(keyblob); + if (rsa != null) + { + Console.WriteLine("\nA valid RSAPrivateKey\n"); + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlprivatekey = rsa.ToXmlString(true); + Console.WriteLine("\nXML RSA private key: {0} bits\n{1}\n", rsa.KeySize, xmlprivatekey); + ProcessRSA(rsa); + return; + } + + rsa = DecodePrivateKeyInfo(keyblob); //PKCS #8 unencrypted + if (rsa != null) + { + Console.WriteLine("\nA valid PKCS #8 PrivateKeyInfo\n"); + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlprivatekey = rsa.ToXmlString(true); + Console.WriteLine("\nXML RSA private key: {0} bits\n{1}\n", rsa.KeySize, xmlprivatekey); + ProcessRSA(rsa); + return; + } + + rsa = DecodeEncryptedPrivateKeyInfo(keyblob); //PKCS #8 encrypted + if (rsa != null) + { + Console.WriteLine("\nA valid PKCS #8 EncryptedPrivateKeyInfo\n"); + Console.WriteLine("\nCreated an RSACryptoServiceProvider instance\n"); + string xmlprivatekey = rsa.ToXmlString(true); + Console.WriteLine("\nXML RSA private key: {0} bits\n{1}\n", rsa.KeySize, xmlprivatekey); + ProcessRSA(rsa); + return; + } + Console.WriteLine("Not a binary DER public, private or PKCS #8 key"); + return; + } + + public static void ProcessRSA(RSACryptoServiceProvider rsa) + { + if (_verbose) + ShowRSAProps(rsa); + } + + //-------- Get the binary PKCS #8 PRIVATE key -------- + public static byte[] DecodePkcs8PrivateKey(string instr) + { + const string pemp8header = "-----BEGIN PRIVATE KEY-----"; + const string pemp8footer = "-----END PRIVATE KEY-----"; + string pemstr = instr.Trim(); + byte[] binkey; + if (!pemstr.StartsWith(pemp8header) || !pemstr.EndsWith(pemp8footer)) + return null; + StringBuilder sb = new StringBuilder(pemstr); + sb.Replace(pemp8header, ""); //remove headers/footers, if present + sb.Replace(pemp8footer, ""); + + string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace + + try + { + binkey = Convert.FromBase64String(pubstr); + } + catch (FormatException) + { //if can't b64 decode, data is not valid + return null; + } + return binkey; + } + + //------- Parses binary asn.1 PKCS #8 PrivateKeyInfo; returns RSACryptoServiceProvider --- + public static RSACryptoServiceProvider DecodePrivateKeyInfo(byte[] pkcs8) + { + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" + // this byte[] includes the sequence byte and terminal encoded null + byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; + byte[] seq = new byte[15]; + // --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------ + MemoryStream mem = new MemoryStream(pkcs8); + int lenstream = (int)mem.Length; + BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading + byte bt = 0; + ushort twobytes = 0; + + try + { + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + + bt = binr.ReadByte(); + if (bt != 0x02) + return null; + + twobytes = binr.ReadUInt16(); + + if (twobytes != 0x0001) + return null; + + seq = binr.ReadBytes(15); //read the Sequence OID + if (!CompareBytearrays(seq, SeqOID)) //make sure Sequence for OID is correct + return null; + + bt = binr.ReadByte(); + if (bt != 0x04) //expect an Octet string + return null; + + bt = binr.ReadByte(); //read next byte, or next 2 bytes is 0x81 or 0x82; otherwise bt is the byte count + if (bt == 0x81) + binr.ReadByte(); + else + if (bt == 0x82) + binr.ReadUInt16(); + //------ at this stage, the remaining sequence should be the RSA private key + + byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position)); + RSACryptoServiceProvider rsacsp = DecodeRSAPrivateKey(rsaprivkey); + return rsacsp; + } + + catch (Exception) + { + return null; + } + + finally { binr.Close(); } + + } + + //-------- Get the binary PKCS #8 Encrypted PRIVATE key -------- + public static byte[] DecodePkcs8EncPrivateKey(string instr) + { + const string pemp8encheader = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + const string pemp8encfooter = "-----END ENCRYPTED PRIVATE KEY-----"; + string pemstr = instr.Trim(); + byte[] binkey; + if (!pemstr.StartsWith(pemp8encheader) || !pemstr.EndsWith(pemp8encfooter)) + return null; + StringBuilder sb = new StringBuilder(pemstr); + sb.Replace(pemp8encheader, ""); //remove headers/footers, if present + sb.Replace(pemp8encfooter, ""); + + string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace + + try + { + binkey = Convert.FromBase64String(pubstr); + } + catch (System.FormatException) + { //if can't b64 decode, data is not valid + return null; + } + return binkey; + } + + //------- Parses binary asn.1 EncryptedPrivateKeyInfo; returns RSACryptoServiceProvider --- + public static RSACryptoServiceProvider DecodeEncryptedPrivateKeyInfo(byte[] encpkcs8) + { + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" + // this byte[] includes the sequence byte and terminal encoded null + byte[] OIDpkcs5PBES2 = { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0D }; + byte[] OIDpkcs5PBKDF2 = { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0C }; + byte[] OIDdesEDE3CBC = { 0x06, 0x08, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x07 }; + byte[] seqdes = new byte[10]; + byte[] seq = new byte[11]; + byte[] salt; + byte[] IV; + byte[] encryptedpkcs8; + byte[] pkcs8; + + int saltsize, ivsize, encblobsize; + int iterations; + + // --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------ + MemoryStream mem = new MemoryStream(encpkcs8); + int lenstream = (int)mem.Length; + BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading + byte bt = 0; + ushort twobytes = 0; + + try + { + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); //inner sequence + if (twobytes == 0x8130) + binr.ReadByte(); + else if (twobytes == 0x8230) + binr.ReadInt16(); + + + seq = binr.ReadBytes(11); //read the Sequence OID + if (!CompareBytearrays(seq, OIDpkcs5PBES2)) //is it a OIDpkcs5PBES2 ? + return null; + + twobytes = binr.ReadUInt16(); //inner sequence for pswd salt + if (twobytes == 0x8130) + binr.ReadByte(); + else if (twobytes == 0x8230) + binr.ReadInt16(); + + twobytes = binr.ReadUInt16(); //inner sequence for pswd salt + if (twobytes == 0x8130) + binr.ReadByte(); + else if (twobytes == 0x8230) + binr.ReadInt16(); + + seq = binr.ReadBytes(11); //read the Sequence OID + if (!CompareBytearrays(seq, OIDpkcs5PBKDF2)) //is it a OIDpkcs5PBKDF2 ? + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) + binr.ReadByte(); + else if (twobytes == 0x8230) + binr.ReadInt16(); + + bt = binr.ReadByte(); + if (bt != 0x04) //expect octet string for salt + return null; + saltsize = binr.ReadByte(); + salt = binr.ReadBytes(saltsize); + + if (_verbose) + ShowBytes("Salt for pbkd", salt); + bt = binr.ReadByte(); + if (bt != 0x02) //expect an integer for PBKF2 interation count + return null; + + int itbytes = binr.ReadByte(); //PBKD2 iterations should fit in 2 bytes. + if (itbytes == 1) + iterations = binr.ReadByte(); + else if (itbytes == 2) + iterations = 256 * binr.ReadByte() + binr.ReadByte(); + else + return null; + if (_verbose) + Console.WriteLine("PBKD2 iterations {0}", iterations); + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) + binr.ReadByte(); + else if (twobytes == 0x8230) + binr.ReadInt16(); + + + seqdes = binr.ReadBytes(10); //read the Sequence OID + if (!CompareBytearrays(seqdes, OIDdesEDE3CBC)) //is it a OIDdes-EDE3-CBC ? + return null; + + bt = binr.ReadByte(); + if (bt != 0x04) //expect octet string for IV + return null; + ivsize = binr.ReadByte(); // IV byte size should fit in one byte (24 expected for 3DES) + IV = binr.ReadBytes(ivsize); + if (_verbose) + ShowBytes("IV for des-EDE3-CBC", IV); + + bt = binr.ReadByte(); + if (bt != 0x04) // expect octet string for encrypted PKCS8 data + return null; + + + bt = binr.ReadByte(); + + if (bt == 0x81) + encblobsize = binr.ReadByte(); // data size in next byte + else if (bt == 0x82) + encblobsize = 256 * binr.ReadByte() + binr.ReadByte(); + else + encblobsize = bt; // we already have the data size + + + encryptedpkcs8 = binr.ReadBytes(encblobsize); + //if(verbose) + // showBytes("Encrypted PKCS8 blob", encryptedpkcs8) ; + + + SecureString secpswd = GetSecPswd("Enter password for Encrypted PKCS #8 ==>"); + pkcs8 = DecryptPBDK2(encryptedpkcs8, salt, IV, secpswd, iterations); + if (pkcs8 == null) // probably a bad pswd entered. + return null; + + //if(verbose) + // showBytes("Decrypted PKCS #8", pkcs8) ; + //----- With a decrypted pkcs #8 PrivateKeyInfo blob, decode it to an RSA --- + RSACryptoServiceProvider rsa = DecodePrivateKeyInfo(pkcs8); + return rsa; + } + catch (Exception) + { + return null; + } + finally + { + binr.Close(); + } + } + + // ------ Uses PBKD2 to derive a 3DES key and decrypts data -------- + public static byte[] DecryptPBDK2(byte[] edata, byte[] salt, byte[] IV, SecureString secpswd, int iterations) + { + CryptoStream decrypt = null; + + IntPtr unmanagedPswd = IntPtr.Zero; + byte[] psbytes = new byte[secpswd.Length]; + unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd); + Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length); + Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd); + + try + { + Rfc2898DeriveBytes kd = new Rfc2898DeriveBytes(psbytes, salt, iterations); + TripleDES decAlg = TripleDES.Create(); + decAlg.Key = kd.GetBytes(24); + decAlg.IV = IV; + MemoryStream memstr = new MemoryStream(); + decrypt = new CryptoStream(memstr, decAlg.CreateDecryptor(), CryptoStreamMode.Write); + decrypt.Write(edata, 0, edata.Length); + decrypt.Flush(); + decrypt.Close(); // this is REQUIRED. + byte[] cleartext = memstr.ToArray(); + return cleartext; + } + catch (Exception e) + { + Console.WriteLine("Problem decrypting: {0}", e.Message); + return null; + } + } + + //-------- Get the binary RSA PUBLIC key -------- + public static byte[] DecodeOpenSSLPublicKey(string instr) + { + const string pempubheader = "-----BEGIN PUBLIC KEY-----"; + const string pempubfooter = "-----END PUBLIC KEY-----"; + string pemstr = instr.Trim(); + byte[] binkey; + if (!pemstr.StartsWith(pempubheader) || !pemstr.EndsWith(pempubfooter)) + return null; + StringBuilder sb = new StringBuilder(pemstr); + sb.Replace(pempubheader, ""); //remove headers/footers, if present + sb.Replace(pempubfooter, ""); + + string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace + + try + { + binkey = Convert.FromBase64String(pubstr); + } + catch (System.FormatException) + { //if can't b64 decode, data is not valid + return null; + } + return binkey; + } + + //------- Parses binary asn.1 X509 SubjectPublicKeyInfo; returns RSACryptoServiceProvider --- + public static RSACryptoServiceProvider DecodeX509PublicKey(byte[] x509key) + { + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" + byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; + byte[] seq = new byte[15]; + // --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------ + MemoryStream mem = new MemoryStream(x509key); + BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading + byte bt = 0; + ushort twobytes = 0; + + try + { + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + seq = binr.ReadBytes(15); //read the Sequence OID + if (!CompareBytearrays(seq, SeqOID)) //make sure Sequence for OID is correct + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8203) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + bt = binr.ReadByte(); + if (bt != 0x00) //expect null byte next + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); + byte lowbyte = 0x00; + byte highbyte = 0x00; + + if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81) + lowbyte = binr.ReadByte(); // read next bytes which is bytes in modulus + else if (twobytes == 0x8202) + { + highbyte = binr.ReadByte(); //advance 2 bytes + lowbyte = binr.ReadByte(); + } + else + return null; + byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; //reverse byte order since asn.1 key uses big endian order + int modsize = BitConverter.ToInt32(modint, 0); + + byte firstbyte = binr.ReadByte(); + binr.BaseStream.Seek(-1, SeekOrigin.Current); + + if (firstbyte == 0x00) + { //if first byte (highest order) of modulus is zero, don't include it + binr.ReadByte(); //skip this null byte + modsize -= 1; //reduce modulus buffer size by 1 + } + + byte[] modulus = binr.ReadBytes(modsize); //read the modulus bytes + + if (binr.ReadByte() != 0x02) //expect an Integer for the exponent data + return null; + int expbytes = (int)binr.ReadByte(); // should only need one byte for actual exponent data (for all useful values) + byte[] exponent = binr.ReadBytes(expbytes); + + + ShowBytes("\nExponent", exponent); + ShowBytes("\nModulus", modulus); + + // ------- create RSACryptoServiceProvider instance and initialize with public key ----- + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSAParameters RSAKeyInfo = new RSAParameters(); + RSAKeyInfo.Modulus = modulus; + RSAKeyInfo.Exponent = exponent; + RSA.ImportParameters(RSAKeyInfo); + return RSA; + } + catch (Exception) + { + return null; + } + finally + { + binr.Close(); + } + } + + //------- Parses binary ans.1 RSA private key; returns RSACryptoServiceProvider --- + public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey) + { + byte[] MODULUS, E, D, P, Q, DP, DQ, IQ; + + // --------- Set up stream to decode the asn.1 encoded RSA private key ------ + MemoryStream mem = new MemoryStream(privkey); + BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading + byte bt = 0; + ushort twobytes = 0; + int elems = 0; + try + { + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes != 0x0102) //version number + return null; + bt = binr.ReadByte(); + if (bt != 0x00) + return null; + + + //------ all private key components are Integer sequences ---- + elems = GetIntegerSize(binr); + MODULUS = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + E = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + D = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + P = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + Q = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + DP = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + DQ = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + IQ = binr.ReadBytes(elems); + + if (_verbose) + { + Console.WriteLine("showing components .."); + + ShowBytes("\nModulus", MODULUS); + ShowBytes("\nExponent", E); + ShowBytes("\nD", D); + ShowBytes("\nP", P); + ShowBytes("\nQ", Q); + ShowBytes("\nDP", DP); + ShowBytes("\nDQ", DQ); + ShowBytes("\nIQ", IQ); + } + + // ------- create RSACryptoServiceProvider instance and initialize with public key ----- + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSAParameters RSAparams = new RSAParameters + { + Modulus = MODULUS, + Exponent = E, + D = D, + P = P, + Q = Q, + DP = DP, + DQ = DQ, + InverseQ = IQ + }; + + RSA.ImportParameters(RSAparams); + + return RSA; + } + catch (Exception) + { + return null; + } + finally { binr.Close(); } + } + + private static int GetIntegerSize(BinaryReader binr) + { + byte bt = 0; + byte lowbyte = 0x00; + byte highbyte = 0x00; + int count = 0; + + bt = binr.ReadByte(); + + if (bt != 0x02) //expect integer + + return 0; + bt = binr.ReadByte(); + + if (bt == 0x81) + count = binr.ReadByte(); // data size in next byte + + else if (bt == 0x82) + { + highbyte = binr.ReadByte(); // data size in next 2 bytes + lowbyte = binr.ReadByte(); + byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; + count = BitConverter.ToInt32(modint, 0); + } + else + { + count = bt; // we already have the data size + } + + while (binr.ReadByte() == 0x00) + { //remove high order zeros in data + count -= 1; + } + + binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte + return count; + } + + //----- Get the binary RSA PRIVATE key, decrypting if necessary ---- + public static byte[] DecodeOpenSSLPrivateKey(string instr) + { + const string pemprivheader = "-----BEGIN RSA PRIVATE KEY-----"; + const string pemprivfooter = "-----END RSA PRIVATE KEY-----"; + string pemstr = instr.Trim(); + byte[] binkey; + + if (!pemstr.StartsWith(pemprivheader) || !pemstr.EndsWith(pemprivfooter)) + return null; + + StringBuilder sb = new StringBuilder(pemstr); + sb.Replace(pemprivheader, ""); //remove headers/footers, if present + sb.Replace(pemprivfooter, ""); + + string pvkstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace + + try + { // if there are no PEM encryption info lines, this is an UNencrypted PEM private key + binkey = Convert.FromBase64String(pvkstr); + return binkey; + } + catch (System.FormatException) + { //if can't b64 decode, it must be an encrypted private key + //Console.WriteLine("Not an unencrypted OpenSSL PEM private key"); + } + + StringReader str = new StringReader(pvkstr); + + //-------- read PEM encryption info. lines and extract salt ----- + if (!str.ReadLine().StartsWith("Proc-Type: 4,ENCRYPTED")) + return null; + string saltline = str.ReadLine(); + if (!saltline.StartsWith("DEK-Info: DES-EDE3-CBC,")) + return null; + string saltstr = saltline.Substring(saltline.IndexOf(",") + 1).Trim(); + byte[] salt = new byte[saltstr.Length / 2]; + for (int i = 0; i < salt.Length; i++) + salt[i] = Convert.ToByte(saltstr.Substring(i * 2, 2), 16); + if (!(str.ReadLine() == "")) + return null; + + //------ remaining b64 data is encrypted RSA key ---- + string encryptedstr = str.ReadToEnd(); + + try + { //should have b64 encrypted RSA key now + binkey = Convert.FromBase64String(encryptedstr); + } + catch (System.FormatException) + { // bad b64 data. + return null; + } + + //------ Get the 3DES 24 byte key using PDK used by OpenSSL ---- + + SecureString despswd = GetSecPswd("Enter password to derive 3DES key==>"); + //Console.Write("\nEnter password to derive 3DES key: "); + //String pswd = Console.ReadLine(); + byte[] deskey = GetOpenSSL3deskey(salt, despswd, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes + if (deskey == null) + return null; + //showBytes("3DES key", deskey) ; + + //------ Decrypt the encrypted 3des-encrypted RSA private key ------ + byte[] rsakey = DecryptKey(binkey, deskey, salt); //OpenSSL uses salt value in PEM header also as 3DES IV + if (rsakey != null) + return rsakey; //we have a decrypted RSA private key + else + { + Console.WriteLine("Failed to decrypt RSA private key; probably wrong password."); + return null; + } + } + + // ----- Decrypt the 3DES encrypted RSA private key ---------- + public static byte[] DecryptKey(byte[] cipherData, byte[] desKey, byte[] IV) + { + MemoryStream memst = new MemoryStream(); + TripleDES alg = TripleDES.Create(); + alg.Key = desKey; + alg.IV = IV; + try + { + CryptoStream cs = new CryptoStream(memst, alg.CreateDecryptor(), CryptoStreamMode.Write); + cs.Write(cipherData, 0, cipherData.Length); + cs.Close(); + } + catch (Exception exc) + { + Console.WriteLine(exc.Message); + return null; + } + byte[] decryptedData = memst.ToArray(); + return decryptedData; + } + + //----- OpenSSL PBKD uses only one hash cycle (count); miter is number of iterations required to build sufficient bytes --- + private static byte[] GetOpenSSL3deskey(byte[] salt, SecureString secpswd, int count, int miter) + { + IntPtr unmanagedPswd = IntPtr.Zero; + int HASHLENGTH = 16; //MD5 bytes + byte[] keymaterial = new byte[HASHLENGTH * miter]; //to store contatenated Mi hashed results + + + byte[] psbytes = new byte[secpswd.Length]; + unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd); + Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length); + Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd); + + //UTF8Encoding utf8 = new UTF8Encoding(); + //byte[] psbytes = utf8.GetBytes(pswd); + + // --- contatenate salt and pswd bytes into fixed data array --- + byte[] data00 = new byte[psbytes.Length + salt.Length]; + Array.Copy(psbytes, data00, psbytes.Length); //copy the pswd bytes + Array.Copy(salt, 0, data00, psbytes.Length, salt.Length); //concatenate the salt bytes + + // ---- do multi-hashing and contatenate results D1, D2 ... into keymaterial bytes ---- + MD5 md5 = new MD5CryptoServiceProvider(); + byte[] result = null; + byte[] hashtarget = new byte[HASHLENGTH + data00.Length]; //fixed length initial hashtarget + + for (int j = 0; j < miter; j++) + { + // ---- Now hash consecutively for count times ------ + if (j == 0) + result = data00; //initialize + else + { + Array.Copy(result, hashtarget, result.Length); + Array.Copy(data00, 0, hashtarget, result.Length, data00.Length); + result = hashtarget; + //Console.WriteLine("Updated new initial hash target:") ; + //showBytes(result) ; + } + + for (int i = 0; i < count; i++) + result = md5.ComputeHash(result); + Array.Copy(result, 0, keymaterial, j * HASHLENGTH, result.Length); //contatenate to keymaterial + } + //showBytes("Final key material", keymaterial); + byte[] deskey = new byte[24]; + Array.Copy(keymaterial, deskey, deskey.Length); + + Array.Clear(psbytes, 0, psbytes.Length); + Array.Clear(data00, 0, data00.Length); + Array.Clear(result, 0, result.Length); + Array.Clear(hashtarget, 0, hashtarget.Length); + Array.Clear(keymaterial, 0, keymaterial.Length); + + return deskey; + } + + private static SecureString GetSecPswd(string prompt) + { + SecureString password = new SecureString(); + + Console.ForegroundColor = ConsoleColor.Gray; + Console.Write(prompt); + Console.ForegroundColor = ConsoleColor.Magenta; + + while (true) + { + ConsoleKeyInfo cki = Console.ReadKey(true); + if (cki.Key == ConsoleKey.Enter) + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(); + return password; + } + else if (cki.Key == ConsoleKey.Backspace) + { + // remove the last asterisk from the screen... + if (password.Length > 0) + { + Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); + Console.Write(" "); + Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); + password.RemoveAt(password.Length - 1); + } + } + else if (cki.Key == ConsoleKey.Escape) + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(); + return password; + } + else if (char.IsLetterOrDigit(cki.KeyChar) || char.IsSymbol(cki.KeyChar)) + { + if (password.Length < 20) + { + password.AppendChar(cki.KeyChar); + Console.Write("*"); + } + else + { + Console.Beep(); + } + } + else + { + Console.Beep(); + } + } + } + + private static bool CompareBytearrays(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + int i = 0; + foreach (byte c in a) + { + if (c != b[i]) + return false; + i++; + } + return true; + } + + private static void ShowRSAProps(RSACryptoServiceProvider rsa) + { + Console.WriteLine("RSA CSP key information:"); + CspKeyContainerInfo keyInfo = rsa.CspKeyContainerInfo; + Console.WriteLine("Accessible property: " + keyInfo.Accessible); + Console.WriteLine("Exportable property: " + keyInfo.Exportable); + Console.WriteLine("HardwareDevice property: " + keyInfo.HardwareDevice); + Console.WriteLine("KeyContainerName property: " + keyInfo.KeyContainerName); + Console.WriteLine("KeyNumber property: " + keyInfo.KeyNumber.ToString()); + Console.WriteLine("MachineKeyStore property: " + keyInfo.MachineKeyStore); + Console.WriteLine("Protected property: " + keyInfo.Protected); + Console.WriteLine("ProviderName property: " + keyInfo.ProviderName); + Console.WriteLine("ProviderType property: " + keyInfo.ProviderType); + Console.WriteLine("RandomlyGenerated property: " + keyInfo.RandomlyGenerated); + Console.WriteLine("Removable property: " + keyInfo.Removable); + Console.WriteLine("UniqueKeyContainerName property: " + keyInfo.UniqueKeyContainerName); + } + + private static void ShowBytes(string info, byte[] data) + { + Console.WriteLine("{0} [{1} bytes]", info, data.Length); + for (int i = 1; i <= data.Length; i++) + { + Console.Write("{0:X2} ", data[i - 1]); + if (i % 16 == 0) + Console.WriteLine(); + } + Console.WriteLine("\n\n"); + } + + private static byte[] GetFileBytes(string filename) + { + if (!File.Exists(filename)) + return null; + + Stream stream = new FileStream(filename, FileMode.Open); + int datalen = (int)stream.Length; + byte[] filebytes = new byte[datalen]; + stream.Seek(0, SeekOrigin.Begin); + stream.Read(filebytes, 0, datalen); + stream.Close(); + return filebytes; + } + + private static void PutFileBytes(string outfile, byte[] data, int bytes) + { + FileStream fs = null; + if (bytes > data.Length) + { + Console.WriteLine("Too many bytes"); + return; + } + try + { + fs = new FileStream(outfile, FileMode.Create); + fs.Write(data, 0, bytes); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + finally + { + fs.Close(); + } + } + } +} \ No newline at end of file