changeset 5:b6fe203af9d5

Private key passwords and validation
author Brad Greco <brad@bgreco.net>
date Thu, 28 Feb 2019 21:19:32 -0500
parents 3142e52cbe69
children c1dffaac66fa
files ServerMonitor/Forms/InputDialog.Designer.cs ServerMonitor/Forms/InputDialog.cs ServerMonitor/Forms/InputDialog.resx ServerMonitor/Forms/ServerForm.Designer.cs ServerMonitor/Forms/ServerForm.cs ServerMonitor/Forms/ServerSummaryForm.cs ServerMonitor/Helpers.cs ServerMonitor/Objects/Checks/SshCheck.cs ServerMonitor/Objects/Server.cs ServerMonitor/Objects/ServerMonitor.cs ServerMonitor/ServerMonitor.csproj
diffstat 11 files changed, 508 insertions(+), 23 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Forms/InputDialog.Designer.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -0,0 +1,137 @@
+namespace ServerMonitorApp
+{
+    partial class InputDialog
+    {
+        /// <summary>
+        /// Required designer variable.
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary>
+        /// Clean up any resources being used.
+        /// </summary>
+        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+            {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Windows Form Designer generated code
+
+        /// <summary>
+        /// Required method for Designer support - do not modify
+        /// the contents of this method with the code editor.
+        /// </summary>
+        private void InitializeComponent()
+        {
+            this.OkButton = new System.Windows.Forms.Button();
+            this.InputCancelButton = new System.Windows.Forms.Button();
+            this.MessageLabel = new System.Windows.Forms.Label();
+            this.panel1 = new System.Windows.Forms.Panel();
+            this.MessageIconPictureBox = new System.Windows.Forms.PictureBox();
+            this.InputTextBox = new System.Windows.Forms.TextBox();
+            this.panel1.SuspendLayout();
+            ((System.ComponentModel.ISupportInitialize)(this.MessageIconPictureBox)).BeginInit();
+            this.SuspendLayout();
+            // 
+            // OkButton
+            // 
+            this.OkButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+            this.OkButton.DialogResult = System.Windows.Forms.DialogResult.OK;
+            this.OkButton.Location = new System.Drawing.Point(211, 13);
+            this.OkButton.Name = "OkButton";
+            this.OkButton.Size = new System.Drawing.Size(75, 23);
+            this.OkButton.TabIndex = 1;
+            this.OkButton.Text = "&OK";
+            this.OkButton.UseVisualStyleBackColor = true;
+            // 
+            // InputCancelButton
+            // 
+            this.InputCancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+            this.InputCancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+            this.InputCancelButton.Location = new System.Drawing.Point(292, 13);
+            this.InputCancelButton.Name = "InputCancelButton";
+            this.InputCancelButton.Size = new System.Drawing.Size(75, 23);
+            this.InputCancelButton.TabIndex = 2;
+            this.InputCancelButton.Text = "&Cancel";
+            this.InputCancelButton.UseVisualStyleBackColor = true;
+            // 
+            // MessageLabel
+            // 
+            this.MessageLabel.AutoSize = true;
+            this.MessageLabel.Location = new System.Drawing.Point(75, 31);
+            this.MessageLabel.Name = "MessageLabel";
+            this.MessageLabel.Size = new System.Drawing.Size(34, 13);
+            this.MessageLabel.TabIndex = 3;
+            this.MessageLabel.Text = "Input:";
+            // 
+            // panel1
+            // 
+            this.panel1.BackColor = System.Drawing.SystemColors.Control;
+            this.panel1.Controls.Add(this.OkButton);
+            this.panel1.Controls.Add(this.InputCancelButton);
+            this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
+            this.panel1.Location = new System.Drawing.Point(0, 84);
+            this.panel1.Name = "panel1";
+            this.panel1.Size = new System.Drawing.Size(379, 49);
+            this.panel1.TabIndex = 5;
+            // 
+            // MessageIconPictureBox
+            // 
+            this.MessageIconPictureBox.Location = new System.Drawing.Point(25, 41);
+            this.MessageIconPictureBox.Name = "MessageIconPictureBox";
+            this.MessageIconPictureBox.Size = new System.Drawing.Size(32, 32);
+            this.MessageIconPictureBox.TabIndex = 4;
+            this.MessageIconPictureBox.TabStop = false;
+            // 
+            // InputTextBox
+            // 
+            this.InputTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.InputTextBox.Location = new System.Drawing.Point(78, 55);
+            this.InputTextBox.Name = "InputTextBox";
+            this.InputTextBox.PasswordChar = '●';
+            this.InputTextBox.Size = new System.Drawing.Size(289, 20);
+            this.InputTextBox.TabIndex = 1;
+            this.InputTextBox.TextChanged += new System.EventHandler(this.InputTextBox_TextChanged);
+            // 
+            // InputDialog
+            // 
+            this.AcceptButton = this.OkButton;
+            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+            this.BackColor = System.Drawing.SystemColors.Control;
+            this.CancelButton = this.InputCancelButton;
+            this.ClientSize = new System.Drawing.Size(379, 133);
+            this.Controls.Add(this.InputTextBox);
+            this.Controls.Add(this.panel1);
+            this.Controls.Add(this.MessageIconPictureBox);
+            this.Controls.Add(this.MessageLabel);
+            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
+            this.MaximizeBox = false;
+            this.MinimizeBox = false;
+            this.Name = "InputDialog";
+            this.ShowInTaskbar = false;
+            this.Text = "Unlock private key";
+            this.Load += new System.EventHandler(this.InputDialog_Load);
+            this.panel1.ResumeLayout(false);
+            ((System.ComponentModel.ISupportInitialize)(this.MessageIconPictureBox)).EndInit();
+            this.ResumeLayout(false);
+            this.PerformLayout();
+
+        }
+
+        #endregion
+
+        private System.Windows.Forms.Button OkButton;
+        private System.Windows.Forms.Button InputCancelButton;
+        private System.Windows.Forms.Label MessageLabel;
+        private System.Windows.Forms.PictureBox MessageIconPictureBox;
+        private System.Windows.Forms.Panel panel1;
+        private System.Windows.Forms.TextBox InputTextBox;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Forms/InputDialog.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace ServerMonitorApp
+{
+    public partial class InputDialog : Form
+    {
+        public string Message { get; set; }
+
+        public Icon MessageIcon { get; set; }
+
+        public string Input { get; private set; }
+
+        public InputDialog()
+        {
+            InitializeComponent();
+        }
+
+        private void InputDialog_Load(object sender, EventArgs e)
+        {
+            MessageIconPictureBox.Image = (MessageIcon ?? SystemIcons.Question).ToBitmap();
+            MessageLabel.Text = Message;
+        }
+
+        public static string ShowDialog(string message, Icon icon = null, IWin32Window owner = null)
+        {
+            using (InputDialog dialog = new InputDialog() { Message = message, MessageIcon = icon })
+            {
+                return dialog.ShowDialog(owner) == DialogResult.OK ? dialog.Input : null;
+            }
+        }
+
+        private void InputTextBox_TextChanged(object sender, EventArgs e)
+        {
+            Input = InputTextBox.Text;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Forms/InputDialog.resx	Thu Feb 28 21:19:32 2019 -0500
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>
\ No newline at end of file
--- a/ServerMonitor/Forms/ServerForm.Designer.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Forms/ServerForm.Designer.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -192,6 +192,7 @@
             this.KeyTextBox.Name = "KeyTextBox";
             this.KeyTextBox.Size = new System.Drawing.Size(202, 20);
             this.KeyTextBox.TabIndex = 13;
+            this.KeyTextBox.Leave += new System.EventHandler(this.KeyTextBox_Leave);
             // 
             // LoginLabel
             // 
--- a/ServerMonitor/Forms/ServerForm.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Forms/ServerForm.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -1,9 +1,12 @@
-using ServerMonitorApp.Properties;
+using Renci.SshNet;
+using Renci.SshNet.Common;
+using ServerMonitorApp.Properties;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
 using System.Drawing;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -54,8 +57,7 @@
             }
             else
             {
-                Text = Server.Name;
-                TitleLabel.Text = Server.Name;
+                SetTitle();
                 NameTextBox.Text = Server.Name;
                 HostTextBox.Text = Server.Host;
                 PortTextBox.Text = Server.Port.ToString();
@@ -114,10 +116,16 @@
             }
         }
 
+        private void SetTitle(string title = null)
+        {
+            title = (title ?? Server.Name) + (Server.Enabled ? "" : " (disabled)");
+            Text = title;
+            TitleLabel.Text = title;
+        }
+
         private void NameTextBox_TextChanged(object sender, EventArgs e)
         {
-            Text = NameTextBox.Text;
-            TitleLabel.Text = NameTextBox.Text;
+            SetTitle(NameTextBox.Text);
         }
 
         private void LoginComboBox_SelectedIndexChanged(object sender, EventArgs e)
@@ -231,6 +239,7 @@
 
         private void BindChangeListeners()
         {
+            Server.EnabledChanged += Server_EnabledChanged;
             foreach (Control control in ServerInfoPanel.Controls.OfType<Control>().Where(c => c is TextBox))
                 control.TextChanged += (sender, e) => UpdateServer(false);
             foreach (Control control in ServerInfoPanel.Controls.OfType<Control>().Where(c => c is ComboBox))
@@ -383,5 +392,45 @@
         {
             return filteredStatuses.Contains(result.CheckStatus) && (LogCheckComboBox.SelectedIndex == 0 || LogCheckComboBox.SelectedItem == result.Check);
         }
+
+        private void KeyTextBox_Leave(object sender, EventArgs e)
+        {
+            OpenPrivateKey(monitor, Server, this);
+        }
+
+        public static void OpenPrivateKey(ServerMonitor monitor, Server server, IWin32Window owner)
+        {
+            if (server.LoginType != LoginType.PrivateKey)
+                return;
+
+            KeyStatus keyStatus = monitor.OpenPrivateKey(server.KeyFile);
+            if (keyStatus == KeyStatus.NeedPassword)
+            {
+                string message = "Private key password for " + server.Name + ":";
+                Icon icon = SystemIcons.Question;
+                while (keyStatus != KeyStatus.Open)
+                {
+                    string password = InputDialog.ShowDialog(message, icon, owner);
+                    if (password == null)
+                        return;
+                    keyStatus = monitor.OpenPrivateKey(server.KeyFile, password);
+                    if (keyStatus == KeyStatus.NeedPassword)
+                    {
+                        message = "Incorrect private key password for " + server.Name + ", please try again:";
+                        icon = SystemIcons.Error;
+                    }
+                }
+            }
+            else if (keyStatus == KeyStatus.NotAccessible)
+            {
+                MessageBox.Show("The private key file " + server.KeyFile + " is not accessible or does not exist.", "Cannot open private key", MessageBoxButtons.OK, MessageBoxIcon.Error);
+                return;
+            }
+        }
+
+        private void Server_EnabledChanged(object sender, EventArgs e)
+        {
+            SetTitle();
+        }
     }
 }
--- a/ServerMonitor/Forms/ServerSummaryForm.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Forms/ServerSummaryForm.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -64,6 +64,7 @@
             NotifyIcon.Icon = new Icon(Icon, 16, 16);
             monitor.CheckStatusChanged += Monitor_CheckStatusChanged;
             RefreshDisplay();
+            CollectPrivateKeyPasswords();
         }
 
         private ServerForm ShowServerForm(Server server, bool activate = true)
@@ -95,6 +96,8 @@
             ServerPanel.Controls.Clear();
             foreach (Server server in monitor.Servers)
             {
+                server.EnabledChanged -= Server_EnabledChanged;
+                server.EnabledChanged += Server_EnabledChanged;
                 ServerSummaryControl control = new ServerSummaryControl(server);
                 control.ContextMenuStrip = ServerContextMenu;
                 control.Click += ServerSummaryControl_Click;
@@ -102,10 +105,28 @@
             }
         }
 
+        private void RefreshServer(Server server)
+        {
+            ServerPanel.Controls.Cast<ServerSummaryControl>().FirstOrDefault(c => c.Server == server).Refresh();
+        }
+
+        private void CollectPrivateKeyPasswords()
+        {
+            foreach (Server server in monitor.Servers)
+            {
+                ServerForm.OpenPrivateKey(monitor, server, this);
+            }
+        }
+
+        private void Server_EnabledChanged(object sender, EventArgs e)
+        {
+            RefreshServer((Server)sender);
+        }
+
         private void Monitor_CheckStatusChanged(object sender, CheckStatusChangedEventArgs e)
         {
             if (e.CheckResult != null)
-                ServerPanel.Controls.Cast<ServerSummaryControl>().FirstOrDefault(c => c.Server == e.Check.Server).Refresh();
+                RefreshServer(e.Check.Server);
         }
 
         private ToolTipIcon GetToolTipIcon(CheckStatus status)
@@ -166,7 +187,7 @@
 
         private void ServerContextMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
         {
-            Server server = getClickedServer((ContextMenuStrip)e.ClickedItem.Owner);
+            Server server = GetClickedServer((ContextMenuStrip)e.ClickedItem.Owner);
             if (e.ClickedItem == DeleteServerMenuItem)
             {
                 ServerContextMenu.Close();
@@ -184,6 +205,11 @@
             else if (e.ClickedItem == ToggleEnableServerMenuItem)
             {
                 bool enable = ToggleEnableServerMenuItem.Text == "Enable";
+                if (enable)
+                {
+                    ServerContextMenu.Close();
+                    ServerForm.OpenPrivateKey(monitor, server, this);
+                }
                 server.Enabled = enable;
                 RefreshDisplay();
             }
@@ -191,11 +217,11 @@
 
         private void ServerContextMenu_Opening(object sender, CancelEventArgs e)
         {
-            Server server = getClickedServer((ContextMenuStrip)sender);
+            Server server = GetClickedServer((ContextMenuStrip)sender);
             ToggleEnableServerMenuItem.Text = server.Enabled ? "Disable" : "Enable";
         }
 
-        private Server getClickedServer(ContextMenuStrip menu)
+        private Server GetClickedServer(ContextMenuStrip menu)
         {
             return ((ServerSummaryControl)menu.SourceControl).Server;
         }
--- a/ServerMonitor/Helpers.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Helpers.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -1,4 +1,6 @@
-using ServerMonitorApp.Properties;
+using Renci.SshNet;
+using Renci.SshNet.Common;
+using ServerMonitorApp.Properties;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
--- a/ServerMonitor/Objects/Checks/SshCheck.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Objects/Checks/SshCheck.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -29,12 +29,19 @@
 
         protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
         {
+            try
+            {
+                if (!Server.SshClient.IsConnected)
+                    Server.SshClient.Connect();
+            }
+            catch (Exception e)
+            {
+                return Task.FromResult(Fail(e));
+            }
             return Task.Run(() =>
             {
                 try
                 {
-                    if (!Server.SshClient.IsConnected)
-                        Server.SshClient.Connect();
                     token.ThrowIfCancellationRequested();
                     using (SshCommand command = Server.SshClient.CreateCommand(Command))
                     {
--- a/ServerMonitor/Objects/Server.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Objects/Server.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -6,6 +6,7 @@
 using System.ComponentModel;
 using Renci.SshNet;
 using System.Runtime.Serialization;
+using System.Xml.Serialization;
 
 namespace ServerMonitorApp
 {
@@ -21,6 +22,7 @@
         private SshClient _sshClient;
         private bool _enabled = true;
         private byte[] passwordHash;
+        private PrivateKeyFile _privateKeyFile;
 
         public event EventHandler CheckModified;
         public event EventHandler EnabledChanged;
@@ -70,17 +72,51 @@
             set
             {
                 passwordHash = ProtectedData.Protect(Encoding.UTF8.GetBytes(value),
-                                 Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Minor obfuscation of additional entropy
-                                 DataProtectionScope.CurrentUser);
+                                    Encoding.UTF8.GetBytes("Server".Reverse().ToString()), // Minor obfuscation of additional entropy
+                                    DataProtectionScope.CurrentUser);
             }
         }
 
+        [XmlIgnore]
+        public PrivateKeyFile PrivateKeyFile
+        {
+            get { return _privateKeyFile; }
+            set
+            {
+                _privateKeyFile = value;
+                if (LoginType == LoginType.PrivateKey)
+                {
+                    if (_privateKeyFile == null)
+                    {
+                        KeyStatus = KeyStatus.Closed;
+                        Enabled = false;
+                    }
+                    else
+                    {
+                        if (!KeyStatus.In(KeyStatus.Open, KeyStatus.Closed))
+                            Enabled = true;
+                        KeyStatus = KeyStatus.Open;
+                    }
+                }
+            }
+        }
+
+        public KeyStatus KeyStatus { get; set; }
+
         public bool Enabled
         {
             get { return _enabled; }
-            set { _enabled = value; EnabledChanged?.Invoke(this, new EventArgs()); }
+            set
+            {
+                if (LoginType == LoginType.PrivateKey && PrivateKeyFile == null && value == true)
+                    return;
+                _enabled = value;
+                EnabledChanged?.Invoke(this, new EventArgs());
+            }
         }
 
+        //public bool WaitingForUser { get; set; }
+
         public CheckStatus Status => !Enabled ? CheckStatus.Disabled : Checks
             .Where(c => c.Enabled)
             .Select(c => c.LastRunStatus)
@@ -93,12 +129,7 @@
             {
                 if (_sshClient == null)
                 {
-                    AuthenticationMethod auth = null;
-                    if (LoginType == LoginType.Password)
-                        auth = new PasswordAuthenticationMethod(Username, Password);
-                    else
-                        auth = new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile));
-                    ConnectionInfo info = new ConnectionInfo(Host, Port, Username, auth);
+                    ConnectionInfo info = new ConnectionInfo(Host, Port, Username, GetAuthentication());
                     _sshClient = new SshClient(info);
                 }
                 return _sshClient;
@@ -156,10 +187,28 @@
                 && Checks.Count == 0;
         }
 
+        private AuthenticationMethod GetAuthentication()
+        {
+            if (LoginType == LoginType.Password)
+                return new PasswordAuthenticationMethod(Username, Password);
+            else
+                return new PrivateKeyAuthenticationMethod(Username, PrivateKeyFile);
+        }
+
         private void InvalidateSshConnection()
         {
             _sshClient?.Dispose();
             _sshClient = null;
         }
     }
+
+    public enum KeyStatus
+    {
+        Closed,
+        Open,
+        NotAccessible,
+        NeedPassword,
+    }
+
+
 }
--- a/ServerMonitor/Objects/ServerMonitor.cs	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/Objects/ServerMonitor.cs	Thu Feb 28 21:19:32 2019 -0500
@@ -1,4 +1,6 @@
-using ServerMonitorApp.Properties;
+using Renci.SshNet;
+using Renci.SshNet.Common;
+using ServerMonitorApp.Properties;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
@@ -18,6 +20,7 @@
         private readonly string configFileDir;
         private readonly Logger logger;
         private readonly Dictionary<int, CancellationTokenSource> tokens = new Dictionary<int, CancellationTokenSource>();
+        private readonly Dictionary<string, PrivateKeyFile> privateKeys = new Dictionary<string, PrivateKeyFile>();
         private readonly List<int> pausedChecks = new List<int>();
         private bool running, networkAvailable;
         private Dictionary<Task<CheckResult>, int> tasks;
@@ -33,6 +36,8 @@
 
         public string ConfigFile { get; private set; }
 
+        public IEnumerable<string> LockedKeys { get { return privateKeys.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key); } }
+
         public ServerMonitor(ServerSummaryForm mainForm)
         {
             this.mainForm = mainForm;
@@ -67,6 +72,8 @@
                 // that doesn't work when using the XML serializer for some reason.
                 foreach (Server server in Servers)
                 {
+                    if (server.LoginType == LoginType.PrivateKey)
+                        OpenPrivateKey(server.KeyFile);
                     foreach (Check check in server.Checks)
                     {
                         check.Server = server;
@@ -236,7 +243,8 @@
         private void CancelCheck(Check check)
         {
             Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key;
-            tasks.Remove(task);
+            if (task != null)
+                tasks.Remove(task);
             pausedChecks.RemoveAll(id => id == check.Id);
             if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts))
                 cts.Cancel();
@@ -269,6 +277,38 @@
                 mainForm.Invoke((MethodInvoker)(() => Run()));
         }
 
+        public KeyStatus OpenPrivateKey(string path, string password = null)
+        {
+            KeyStatus keyStatus;
+            if (path == null)
+                return KeyStatus.NotAccessible;
+            if (privateKeys.TryGetValue(path, out PrivateKeyFile key) && key != null)
+                return KeyStatus.Open;
+            try
+            {
+               key = new PrivateKeyFile(path, password);
+                keyStatus = KeyStatus.Open;
+            }
+            catch (Exception e) when (e is SshPassPhraseNullOrEmptyException || e is InvalidOperationException)
+            {
+                keyStatus = KeyStatus.NeedPassword;
+            }
+            catch (Exception)
+            {
+                keyStatus = KeyStatus.NotAccessible;
+            }
+            foreach (Server server in Servers)
+            {
+                if (server.KeyFile == path)
+                {
+                    server.PrivateKeyFile = key;
+                    server.KeyStatus = keyStatus;
+                }
+            }
+            privateKeys[path] = key;
+            return keyStatus;
+        }
+
         private void GenerateIds()
         {
             if (Servers.Any())
--- a/ServerMonitor/ServerMonitor.csproj	Sun Feb 10 20:51:26 2019 -0500
+++ b/ServerMonitor/ServerMonitor.csproj	Thu Feb 28 21:19:32 2019 -0500
@@ -95,6 +95,12 @@
     <Compile Include="Controls\MatchComboBox.cs">
       <SubType>Component</SubType>
     </Compile>
+    <Compile Include="Forms\InputDialog.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="Forms\InputDialog.Designer.cs">
+      <DependentUpon>InputDialog.cs</DependentUpon>
+    </Compile>
     <Compile Include="Forms\SettingsForm.cs">
       <SubType>Form</SubType>
     </Compile>
@@ -179,6 +185,9 @@
     <EmbeddedResource Include="Forms\CheckForm.resx">
       <DependentUpon>CheckForm.cs</DependentUpon>
     </EmbeddedResource>
+    <EmbeddedResource Include="Forms\InputDialog.resx">
+      <DependentUpon>InputDialog.cs</DependentUpon>
+    </EmbeddedResource>
     <EmbeddedResource Include="Forms\QuickHelpForm.resx">
       <DependentUpon>QuickHelpForm.cs</DependentUpon>
     </EmbeddedResource>