diff --git a/ExternalConnectors/CPS/CPS.ico b/ExternalConnectors/CPS/CPS.ico new file mode 100644 index 00000000..5624c387 Binary files /dev/null and b/ExternalConnectors/CPS/CPS.ico differ diff --git a/ExternalConnectors/CPS/CPSConnectionForm.Designer.cs b/ExternalConnectors/CPS/CPSConnectionForm.Designer.cs new file mode 100644 index 00000000..aeb7432e --- /dev/null +++ b/ExternalConnectors/CPS/CPSConnectionForm.Designer.cs @@ -0,0 +1,241 @@ +namespace ExternalConnectors.CPS +{ + partial class CPSConnectionForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(CPSConnectionForm)); + tbServerURL = new TextBox(); + label3 = new Label(); + tbAPIKey = new TextBox(); + btnOK = new Button(); + btnCancel = new Button(); + tableLayoutPanel1 = new TableLayoutPanel(); + label1 = new Label(); + label6 = new Label(); + tbOTP = new TextBox(); + cbUseSSO = new CheckBox(); + tableLayoutPanel2 = new TableLayoutPanel(); + label4 = new Label(); + tableLayoutPanel1.SuspendLayout(); + tableLayoutPanel2.SuspendLayout(); + SuspendLayout(); + // + // tbServerURL + // + tbServerURL.Dock = DockStyle.Fill; + tbServerURL.Location = new Point(298, 5); + tbServerURL.Margin = new Padding(5); + tbServerURL.Name = "tbServerURL"; + tbServerURL.Size = new Size(611, 27); + tbServerURL.TabIndex = 0; + // + // label3 + // + label3.AutoSize = true; + label3.Dock = DockStyle.Fill; + label3.Location = new Point(5, 84); + label3.Margin = new Padding(5, 0, 5, 0); + label3.Name = "label3"; + label3.Size = new Size(283, 42); + label3.TabIndex = 5; + label3.Text = "API Key"; + label3.TextAlign = ContentAlignment.MiddleLeft; + // + // tbAPIKey + // + tbAPIKey.Dock = DockStyle.Fill; + tbAPIKey.Location = new Point(298, 89); + tbAPIKey.Margin = new Padding(5); + tbAPIKey.Name = "tbAPIKey"; + tbAPIKey.Size = new Size(611, 27); + tbAPIKey.TabIndex = 4; + tbAPIKey.UseSystemPasswordChar = true; + // + // btnOK + // + btnOK.Anchor = AnchorStyles.Right; + btnOK.DialogResult = DialogResult.OK; + btnOK.Location = new Point(337, 16); + btnOK.Margin = new Padding(5); + btnOK.Name = "btnOK"; + btnOK.Size = new Size(101, 35); + btnOK.TabIndex = 6; + btnOK.Text = "OK"; + btnOK.UseVisualStyleBackColor = true; + // + // btnCancel + // + btnCancel.Anchor = AnchorStyles.Left; + btnCancel.DialogResult = DialogResult.Cancel; + btnCancel.Location = new Point(474, 16); + btnCancel.Margin = new Padding(5); + btnCancel.Name = "btnCancel"; + btnCancel.Size = new Size(101, 35); + btnCancel.TabIndex = 11; + btnCancel.Text = "Cancel"; + btnCancel.UseVisualStyleBackColor = true; + // + // tableLayoutPanel1 + // + tableLayoutPanel1.ColumnCount = 2; + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 32.06997F)); + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 67.93003F)); + tableLayoutPanel1.Controls.Add(label1, 0, 0); + tableLayoutPanel1.Controls.Add(label3, 0, 2); + tableLayoutPanel1.Controls.Add(tbServerURL, 1, 0); + tableLayoutPanel1.Controls.Add(tbAPIKey, 1, 2); + tableLayoutPanel1.Controls.Add(label6, 0, 3); + tableLayoutPanel1.Controls.Add(tbOTP, 1, 3); + tableLayoutPanel1.Controls.Add(cbUseSSO, 0, 1); + tableLayoutPanel1.Dock = DockStyle.Top; + tableLayoutPanel1.Location = new Point(0, 0); + tableLayoutPanel1.Margin = new Padding(5); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 5; + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 20F)); + tableLayoutPanel1.Size = new Size(914, 212); + tableLayoutPanel1.TabIndex = 12; + // + // label1 + // + label1.AutoSize = true; + label1.Dock = DockStyle.Fill; + label1.Location = new Point(5, 0); + label1.Margin = new Padding(5, 0, 5, 0); + label1.Name = "label1"; + label1.Size = new Size(283, 42); + label1.TabIndex = 2; + label1.Text = "Passwordstate URL"; + label1.TextAlign = ContentAlignment.MiddleLeft; + // + // label6 + // + label6.AutoSize = true; + label6.Dock = DockStyle.Fill; + label6.Location = new Point(3, 126); + label6.Name = "label6"; + label6.Size = new Size(287, 42); + label6.TabIndex = 15; + label6.Text = "2FA OTP (Optional)"; + // + // tbOTP + // + tbOTP.Dock = DockStyle.Fill; + tbOTP.Location = new Point(298, 131); + tbOTP.Margin = new Padding(5); + tbOTP.Name = "tbOTP"; + tbOTP.Size = new Size(611, 27); + tbOTP.TabIndex = 5; + // + // cbUseSSO + // + cbUseSSO.Anchor = AnchorStyles.Left; + cbUseSSO.AutoSize = true; + cbUseSSO.Location = new Point(5, 53); + cbUseSSO.Margin = new Padding(5, 5, 5, 0); + cbUseSSO.Name = "cbUseSSO"; + cbUseSSO.Size = new Size(157, 24); + cbUseSSO.TabIndex = 14; + cbUseSSO.Text = "Use SSO / WinAuth"; + cbUseSSO.UseVisualStyleBackColor = true; + cbUseSSO.CheckedChanged += cbUseSSO_CheckedChanged; + // + // tableLayoutPanel2 + // + tableLayoutPanel2.ColumnCount = 5; + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 106F)); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 26F)); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 107F)); + tableLayoutPanel2.Controls.Add(btnOK, 1, 0); + tableLayoutPanel2.Controls.Add(btnCancel, 3, 0); + tableLayoutPanel2.Dock = DockStyle.Bottom; + tableLayoutPanel2.Location = new Point(0, 300); + tableLayoutPanel2.Margin = new Padding(5); + tableLayoutPanel2.Name = "tableLayoutPanel2"; + tableLayoutPanel2.RowCount = 1; + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel2.Size = new Size(914, 67); + tableLayoutPanel2.TabIndex = 13; + // + // label4 + // + label4.AutoSize = true; + label4.Dock = DockStyle.Fill; + label4.Location = new Point(0, 212); + label4.Margin = new Padding(5, 0, 5, 0); + label4.Name = "label4"; + label4.Size = new Size(345, 20); + label4.TabIndex = 14; + label4.Text = "URL is the base URL, like https://pass.domain.local/"; + label4.TextAlign = ContentAlignment.MiddleLeft; + // + // CPSConnectionForm + // + AcceptButton = btnOK; + AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(914, 367); + Controls.Add(label4); + Controls.Add(tableLayoutPanel2); + Controls.Add(tableLayoutPanel1); + Icon = (Icon)resources.GetObject("$this.Icon"); + Margin = new Padding(5); + Name = "CPSConnectionForm"; + Text = "Passwordstate API Login Data"; + Activated += CPSConnectionForm_Activated; + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + tableLayoutPanel2.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + private System.Windows.Forms.Label label3; + + public System.Windows.Forms.TextBox tbServerURL; + //public System.Windows.Forms.TextBox tbUsername; + public System.Windows.Forms.TextBox tbAPIKey; + private System.Windows.Forms.Button btnOK; + private System.Windows.Forms.Button btnCancel; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2; + public System.Windows.Forms.CheckBox cbUseSSO; + private System.Windows.Forms.Label label4; + private Label label6; + public TextBox tbOTP; + } +} \ No newline at end of file diff --git a/ExternalConnectors/CPS/CPSConnectionForm.cs b/ExternalConnectors/CPS/CPSConnectionForm.cs new file mode 100644 index 00000000..8e17e836 --- /dev/null +++ b/ExternalConnectors/CPS/CPSConnectionForm.cs @@ -0,0 +1,41 @@ +namespace ExternalConnectors.CPS +{ + public partial class CPSConnectionForm : Form + { + public CPSConnectionForm() + { + InitializeComponent(); + } + + private void CPSConnectionForm_Activated(object sender, EventArgs e) + { + SetVisibility(); + if (cbUseSSO.Checked) + btnOK.Focus(); + else + { + if (tbAPIKey.Text.Length == 0) + tbAPIKey.Focus(); + else + tbOTP.Focus(); + } + + tbAPIKey.Focus(); + if (!string.IsNullOrEmpty(tbAPIKey.Text) || cbUseSSO.Checked == true) + tbOTP.Focus(); + + + } + + private void cbUseSSO_CheckedChanged(object sender, EventArgs e) + { + SetVisibility(); + } + private void SetVisibility() + { + bool ch = cbUseSSO.Checked; + tbAPIKey.Enabled = !ch; + //tbUsername.Enabled = !ch; + } + } +} diff --git a/ExternalConnectors/CPS/CPSConnectionForm.resx b/ExternalConnectors/CPS/CPSConnectionForm.resx new file mode 100644 index 00000000..261283b3 --- /dev/null +++ b/ExternalConnectors/CPS/CPSConnectionForm.resx @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAA + AAAtLDAA+PDaAP///wD79ugA/PrzAPf39wBGRUkA7NacAM2WAAA3z6kA+Pz/AIKBgwD+//4A4sNtAHXe + xAD8+vIAjuTOANOjHgDV6/4AJZf3APn5+QDw37IAIB8jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAgICAgICCQ4CAgIWAgIUAgICAgICAgkJAgICFhYWAAIIDwICAgICCQwCAhYWFgYCCAgIBwIC + AgkJAgsWFhYWBQICCAgIAwIJCQIWFhYWAgICAgINCAgCAgICFhYCAgICAgICAgIRAgICAgICAgICAgIC + AgICEwICAgICEhMTExMCAgITExMCAgICAgITExMTAhMTExMCAgICAgICAgICAhMTEwICAgIICAIJCQIC + AgIKAgICAgIICAQCAgkJAgICAgICAgICCAgCAgICCQkCAgICAgICFQgBAgICAgwJAgICAgICAggIAgIC + AgICEAkCAgICAgIIAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + \ No newline at end of file diff --git a/ExternalConnectors/CPS/PasswordstateInterface.cs b/ExternalConnectors/CPS/PasswordstateInterface.cs new file mode 100644 index 00000000..5208a2e3 --- /dev/null +++ b/ExternalConnectors/CPS/PasswordstateInterface.cs @@ -0,0 +1,301 @@ +using Microsoft.Win32; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace ExternalConnectors.CPS; + +public class PasswordstateInterface +{ + private static class CPSConnectionData + { + public static string ssUsername = ""; + public static string ssPassword = ""; + public static string ssUrl = ""; + public static string ssOTP = ""; + public static DateTime ssOTPTimeStampExpiration; + + public static bool ssSSO = false; + public static bool initdone = false; + + //token + //public static string ssTokenBearer = ""; + //public static DateTime ssTokenExpiresOn = DateTime.UtcNow; + //public static string ssTokenRefresh = ""; + + public static void Init() + { + // 2024-05-04 passwordstate currently does not support auth tokens, so we need to re-enter otp codes frequently + if (!string.IsNullOrEmpty(ssOTP) && DateTime.Now > ssOTPTimeStampExpiration) + { + ssOTP = ""; + initdone = false; + } + + if (initdone == true) + return; + + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\mRemoteCPSInterface"); + try + { + // display gui and ask for data + CPSConnectionForm f = new CPSConnectionForm(); + //string? un = key.GetValue("Username") as string; + //f.tbUsername.Text = un ?? ""; + f.tbAPIKey.Text = CPSConnectionData.ssPassword; // in OTP refresh cases, this value might already be filled + + string? url = key.GetValue("URL") as string; + if (url == null || !url.Contains("://")) + url = "https://cred.domain.local/SecretServer"; + f.tbServerURL.Text = url; + + var b = key.GetValue("SSO"); + if (b == null || (string)b != "True") + ssSSO = false; + else + ssSSO = true; + f.cbUseSSO.Checked = ssSSO; + + // show dialog + while (true) + { + _ = f.ShowDialog(); + + if (f.DialogResult != DialogResult.OK) + return; + + // store values to memory + //ssUsername = f.tbUsername.Text; + ssPassword = f.tbAPIKey.Text; + ssUrl = f.tbServerURL.Text; + ssSSO = f.cbUseSSO.Checked; + ssOTP = f.tbOTP.Text; + ssOTPTimeStampExpiration = DateTime.Now.AddSeconds(30); + // check connection first + try + { + if (TestCredentials() == true) + { + initdone = true; + break; + } + } + catch (Exception) + { + MessageBox.Show("Test Credentials failed - please check your credentials"); + } + } + + + // write values to registry + //key.SetValue("Username", ssUsername); + key.SetValue("URL", ssUrl); + key.SetValue("SSO", ssSSO); + } + catch (Exception) + { + throw; + } + finally + { + key.Close(); + } + } + } + + private static bool TestCredentials() + { + return ConnectionTest(); + } + private static bool ConnectionTest() + { + if (CPSConnectionData.ssSSO) + { + string url = $"{CPSConnectionData.ssUrl}/winapi/passwordlists/"; + + using HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("User-Agent", "mRemote"); + client.DefaultRequestHeaders.Add("OTP", CPSConnectionData.ssOTP); + + var json = client.GetStringAsync(url).Result; + JsonNode? data = JsonSerializer.Deserialize(json); + if (data == null) + return false; + return true; + } + else + { + string url = $"{CPSConnectionData.ssUrl}/api/passwordlists/"; + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("User-Agent", "mRemote"); + client.DefaultRequestHeaders.Add("APIKey", CPSConnectionData.ssPassword); + client.DefaultRequestHeaders.Add("OTP", CPSConnectionData.ssOTP); + + var json = client.GetStringAsync(url).Result; + JsonNode? data = JsonSerializer.Deserialize(json); + if (data == null) + return false; + return true; + } + } + + private static JsonNode? FetchDataWinAuth(int secretID) + { + string url = $"{CPSConnectionData.ssUrl}/winapi/passwords/{secretID}"; + + using HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("User-Agent", "mRemote"); + client.DefaultRequestHeaders.Add("OTP", CPSConnectionData.ssOTP); + + var json = client.GetStringAsync(url).Result; + JsonNode? data = JsonSerializer.Deserialize(json); + if (data == null) + return null; + JsonNode? element = data[0]; + return element; + } + private static JsonNode? FetchDataAPIKeyAuth(int secretID) + { + string url = $"{CPSConnectionData.ssUrl}/api/passwords/{secretID}"; + + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("User-Agent", "mRemote"); + client.DefaultRequestHeaders.Add("APIKey", CPSConnectionData.ssPassword); + client.DefaultRequestHeaders.Add("OTP", CPSConnectionData.ssOTP); + + var json = client.GetStringAsync(url).Result; + JsonNode? data = JsonSerializer.Deserialize(json); + if (data == null) + return null; + JsonNode? element = data[0]; + return element; + } + + private static void FetchSecret(int secretID, out string secretUsername, out string secretPassword, out string secretDomain, out string privatekey) + { + // clear return variables + secretDomain = ""; + secretUsername = ""; + secretPassword = ""; + privatekey = ""; + string privatekeypassphrase = ""; + JsonNode? element = null; + + if (CPSConnectionData.ssSSO) + element = FetchDataWinAuth(secretID); + else + element = FetchDataAPIKeyAuth(secretID); + + if (element == null) + return; + + var dom = element["Domain"]; + if (dom != null) secretDomain = dom.ToString(); + + var user = element["UserName"]; + if (user != null) secretUsername = user.ToString(); + + var pw = element["Password"]; + if (pw != null) secretPassword = pw.ToString(); + + var privkey = element["GenericField1"]; + if (privkey != null) privatekey = privkey.ToString(); + + var phrase = element["GenericField3"]; + if (phrase != null) privatekeypassphrase = phrase.ToString(); + + // need to decode the private key? + if (!string.IsNullOrEmpty(privatekeypassphrase)) + { + try + { + var key = DecodePrivateKey(privatekey, privatekeypassphrase); + privatekey = key; + } + catch(Exception) + { + + } + } + + // conversion to putty format necessary? + if (!string.IsNullOrEmpty(privatekey) && !privatekey.StartsWith("PuTTY-User-Key-File-2")) + { + try + { + RSACryptoServiceProvider key = ImportPrivateKey(privatekey); + privatekey = PuttyKeyFileGenerator.ToPuttyPrivateKey(key); + } + catch (Exception) + { + + } + } + } + + #region PUTTY KEY HANDLING + // decode rsa private key with encryption password + private static string DecodePrivateKey(string encryptedPrivateKey, string password) + { + TextReader textReader = new StringReader(encryptedPrivateKey); + PemReader pemReader = new PemReader(textReader, new PasswordFinder(password)); + + AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); + + TextWriter textWriter = new StringWriter(); + var pemWriter = new PemWriter(textWriter); + pemWriter.WriteObject(keyPair.Private); + pemWriter.Writer.Flush(); + + return ""+textWriter.ToString(); + } + private class PasswordFinder : IPasswordFinder + { + private string password; + + public PasswordFinder(string password) + { + this.password = password; + } + + + public char[] GetPassword() + { + return password.ToCharArray(); + } + } + + // read private key pem string to rsacryptoserviceprovider + public static RSACryptoServiceProvider ImportPrivateKey(string pem) + { + PemReader pr = new PemReader(new StringReader(pem)); + AsymmetricCipherKeyPair KeyPair = (AsymmetricCipherKeyPair)pr.ReadObject(); + RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)KeyPair.Private); + RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); + rsa.ImportParameters(rsaParams); + return rsa; + } + #endregion + + + // input: must be the secret id to fetch + public static void FetchSecretFromServer(string secretID, out string username, out string password, out string domain, out string privatekey) + { + // get secret id + int sid = Int32.Parse(secretID); + + // init connection credentials, display popup if necessary + CPSConnectionData.Init(); + + // get the secret + FetchSecret(sid, out username, out password, out domain, out privatekey); + } +} diff --git a/ExternalConnectors/DSS/SecretServerInterface.cs b/ExternalConnectors/DSS/SecretServerInterface.cs index 4d9f0493..b0ae930d 100644 --- a/ExternalConnectors/DSS/SecretServerInterface.cs +++ b/ExternalConnectors/DSS/SecretServerInterface.cs @@ -7,28 +7,28 @@ using SecretServerAuthentication.DSS; using SecretServerRestClient.DSS; using System.Security.Cryptography; -namespace ExternalConnectors.DSS +namespace ExternalConnectors.DSS; + +public class SecretServerInterface { - public class SecretServerInterface + private static class SSConnectionData { - private static class SSConnectionData + public static string ssUsername = ""; + public static string ssPassword = ""; + public static string ssUrl = ""; + public static string ssOTP = ""; + public static bool ssSSO = false; + public static bool initdone = false; + + //token + public static string ssTokenBearer = ""; + public static DateTime ssTokenExpiresOn = DateTime.UtcNow; + public static string ssTokenRefresh = ""; + + public static void Init() { - public static string ssUsername = ""; - public static string ssPassword = ""; - public static string ssUrl = ""; - public static string ssOTP = ""; - public static bool ssSSO = false; - public static bool initdone = false; - - //token - public static string ssTokenBearer = ""; - public static DateTime ssTokenExpiresOn = DateTime.UtcNow; - public static string ssTokenRefresh = ""; - - public static void Init() - { - if (initdone == true) - return; + if (initdone == true) + return; RegistryKey key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\mRemoteSSInterface"); try @@ -39,174 +39,174 @@ namespace ExternalConnectors.DSS f.tbUsername.Text = un ?? ""; f.tbPassword.Text = SSConnectionData.ssPassword; // in OTP refresh cases, this value might already be filled - string? url = key.GetValue("URL") as string; - if (url == null || !url.Contains("://")) - url = "https://cred.domain.local/SecretServer"; - f.tbSSURL.Text = url; + string? url = key.GetValue("URL") as string; + if (url == null || !url.Contains("://")) + url = "https://cred.domain.local/SecretServer"; + f.tbSSURL.Text = url; - var b = key.GetValue("SSO"); - if (b == null || (string)b != "True") - ssSSO = false; - else - ssSSO = true; - f.cbUseSSO.Checked = ssSSO; - - // show dialog - while (true) + var b = key.GetValue("SSO"); + if (b == null || (string)b != "True") + ssSSO = false; + else + ssSSO = true; + f.cbUseSSO.Checked = ssSSO; + + // show dialog + while (true) + { + _ = f.ShowDialog(); + + if (f.DialogResult != DialogResult.OK) + return; + + // store values to memory + ssUsername = f.tbUsername.Text; + ssPassword = f.tbPassword.Text; + ssUrl = f.tbSSURL.Text; + ssSSO = f.cbUseSSO.Checked; + ssOTP = f.tbOTP.Text; + // check connection first + try { - _ = f.ShowDialog(); - - if (f.DialogResult != DialogResult.OK) - return; - - // store values to memory - ssUsername = f.tbUsername.Text; - ssPassword = f.tbPassword.Text; - ssUrl = f.tbSSURL.Text; - ssSSO = f.cbUseSSO.Checked; - ssOTP = f.tbOTP.Text; - // check connection first - try + if (TestCredentials() == true) { - if (TestCredentials() == true) - { - initdone = true; - break; - } - } - catch (Exception) - { - MessageBox.Show("Test Credentials failed - please check your credentials"); + initdone = true; + break; } } - - - // write values to registry - key.SetValue("Username", ssUsername); - key.SetValue("URL", ssUrl); - key.SetValue("SSO", ssSSO); - } - catch (Exception) - { - throw; - } - finally - { - key.Close(); + catch (Exception) + { + MessageBox.Show("Test Credentials failed - please check your credentials"); + } } + + // write values to registry + key.SetValue("Username", ssUsername); + key.SetValue("URL", ssUrl); + key.SetValue("SSO", ssSSO); } - } - - private static bool TestCredentials() - { - if (SSConnectionData.ssSSO) + catch (Exception) + { + throw; + } + finally + { + key.Close(); + } + + } + } + + private static bool TestCredentials() + { + if (SSConnectionData.ssSSO) + { + // checking creds doesn't really make sense here, as we can't modify them anyway if something is wrong + return true; + } + else + { + + if (!String.IsNullOrEmpty(GetToken())) { - // checking creds doesn't really make sense here, as we can't modify them anyway if something is wrong return true; } else { - - if (!String.IsNullOrEmpty(GetToken())) - { - return true; - } - else - { - return false; - } + return false; } } + } - private static SecretsServiceClient ConstructSecretsServiceClient() + private static SecretsServiceClient ConstructSecretsServiceClient() + { + string baseURL = SSConnectionData.ssUrl; + if (SSConnectionData.ssSSO) { - string baseURL = SSConnectionData.ssUrl; - if (SSConnectionData.ssSSO) + // REQUIRES IIS CONFIG! https://docs.thycotic.com/ss/11.0.0/api-scripting/webservice-iwa-powershell + var handler = new HttpClientHandler() { UseDefaultCredentials = true }; + var httpClient = new HttpClient(handler); { - // REQUIRES IIS CONFIG! https://docs.thycotic.com/ss/11.0.0/api-scripting/webservice-iwa-powershell - var handler = new HttpClientHandler() { UseDefaultCredentials = true }; - var httpClient = new HttpClient(handler); - { - // Call REST API: - return new SecretsServiceClient($"{baseURL}/winauthwebservices/api", httpClient); - } + // Call REST API: + return new SecretsServiceClient($"{baseURL}/winauthwebservices/api", httpClient); } - else - { - var httpClient = new HttpClient(); - { - - var token = GetToken(); - // Set credentials (token): - httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - - // Call REST API: - return new SecretsServiceClient($"{baseURL}/api", httpClient); - } - } - } - private static void FetchSecret(int secretID, out string secretUsername, out string secretPassword, out string secretDomain, out string privatekey) + else { - var client = ConstructSecretsServiceClient(); - SecretModel secret = client.GetSecretAsync(false, true, secretID, null).Result; - - // clear return variables - secretDomain = ""; - secretUsername = ""; - secretPassword = ""; - privatekey = ""; - string privatekeypassphrase = ""; - - // parse data and extract what we need - foreach (var item in secret.Items) + var httpClient = new HttpClient(); { - if (item.FieldName.ToLower().Equals("domain")) - secretDomain = item.ItemValue; - else if (item.FieldName.ToLower().Equals("username")) - secretUsername = item.ItemValue; - else if (item.FieldName.ToLower().Equals("password")) - secretPassword = item.ItemValue; - else if (item.FieldName.ToLower().Equals("private key")) - { - client.ReadResponseNoJSONConvert = true; - privatekey = client.GetFieldAsync(false, false, secretID, "private-key").Result; - client.ReadResponseNoJSONConvert = false; - } - else if (item.FieldName.ToLower().Equals("private key passphrase")) - privatekeypassphrase = item.ItemValue; - } - // need to decode the private key? - if (!string.IsNullOrEmpty(privatekeypassphrase)) - { - try - { - var key = DecodePrivateKey(privatekey, privatekeypassphrase); - privatekey = key; - } - catch(Exception) - { + var token = GetToken(); + // Set credentials (token): + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - } - } - - // conversion to putty format necessary? - if (!string.IsNullOrEmpty(privatekey) && !privatekey.StartsWith("PuTTY-User-Key-File-2")) - { - try - { - RSACryptoServiceProvider key = ImportPrivateKey(privatekey); - privatekey = PuttyKeyFileGenerator.ToPuttyPrivateKey(key); - } - catch (Exception) - { - - } + // Call REST API: + return new SecretsServiceClient($"{baseURL}/api", httpClient); } } + } + private static void FetchSecret(int secretID, out string secretUsername, out string secretPassword, out string secretDomain, out string privatekey) + { + var client = ConstructSecretsServiceClient(); + SecretModel secret = client.GetSecretAsync(false, true, secretID, null).Result; + + // clear return variables + secretDomain = ""; + secretUsername = ""; + secretPassword = ""; + privatekey = ""; + string privatekeypassphrase = ""; + + // parse data and extract what we need + foreach (var item in secret.Items) + { + if (item.FieldName.ToLower().Equals("domain")) + secretDomain = item.ItemValue; + else if (item.FieldName.ToLower().Equals("username")) + secretUsername = item.ItemValue; + else if (item.FieldName.ToLower().Equals("password")) + secretPassword = item.ItemValue; + else if (item.FieldName.ToLower().Equals("private key")) + { + client.ReadResponseNoJSONConvert = true; + privatekey = client.GetFieldAsync(false, false, secretID, "private-key").Result; + client.ReadResponseNoJSONConvert = false; + } + else if (item.FieldName.ToLower().Equals("private key passphrase")) + privatekeypassphrase = item.ItemValue; + } + + // need to decode the private key? + if (!string.IsNullOrEmpty(privatekeypassphrase)) + { + try + { + var key = DecodePrivateKey(privatekey, privatekeypassphrase); + privatekey = key; + } + catch(Exception) + { + + } + } + + // conversion to putty format necessary? + if (!string.IsNullOrEmpty(privatekey) && !privatekey.StartsWith("PuTTY-User-Key-File-2")) + { + try + { + RSACryptoServiceProvider key = ImportPrivateKey(privatekey); + privatekey = PuttyKeyFileGenerator.ToPuttyPrivateKey(key); + } + catch (Exception) + { + + } + } + } + #region PUTTY KEY HANDLING // decode rsa private key with encryption password private static string DecodePrivateKey(string encryptedPrivateKey, string password) @@ -214,31 +214,31 @@ namespace ExternalConnectors.DSS TextReader textReader = new StringReader(encryptedPrivateKey); PemReader pemReader = new(textReader, new PasswordFinder(password)); - AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); + AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); - TextWriter textWriter = new StringWriter(); - var pemWriter = new PemWriter(textWriter); - pemWriter.WriteObject(keyPair.Private); - pemWriter.Writer.Flush(); + TextWriter textWriter = new StringWriter(); + var pemWriter = new PemWriter(textWriter); + pemWriter.WriteObject(keyPair.Private); + pemWriter.Writer.Flush(); - return ""+textWriter.ToString(); - } - private class PasswordFinder : IPasswordFinder + return ""+textWriter.ToString(); + } + private class PasswordFinder : IPasswordFinder + { + private string password; + + public PasswordFinder(string password) { - private string password; - - public PasswordFinder(string password) - { - this.password = password; - } - - - public char[] GetPassword() - { - return password.ToCharArray(); - } + this.password = password; } + + public char[] GetPassword() + { + return password.ToCharArray(); + } + } + // read private key pem string to rsacryptoserviceprovider public static RSACryptoServiceProvider ImportPrivateKey(string pem) { @@ -252,91 +252,90 @@ namespace ExternalConnectors.DSS #endregion - #region TOKEN - private static string GetToken() + #region TOKEN + private static string GetToken() + { + // if there is no token, fetch a fresh one + if (String.IsNullOrEmpty(SSConnectionData.ssTokenBearer)) { - // if there is no token, fetch a fresh one - if (String.IsNullOrEmpty(SSConnectionData.ssTokenBearer)) + return GetTokenFresh(); + } + // if there is a token, check if it is valid + if (SSConnectionData.ssTokenExpiresOn >= DateTime.UtcNow) + { + return SSConnectionData.ssTokenBearer; + } + else + { + // try using refresh token + using (var httpClient = new HttpClient()) { - return GetTokenFresh(); - } - // if there is a token, check if it is valid - if (SSConnectionData.ssTokenExpiresOn >= DateTime.UtcNow) - { - return SSConnectionData.ssTokenBearer; - } - else - { - // try using refresh token - using (var httpClient = new HttpClient()) + var tokenClient = new OAuth2ServiceClient(SSConnectionData.ssUrl, httpClient); + TokenResponse token = new(); + try { - var tokenClient = new OAuth2ServiceClient(SSConnectionData.ssUrl, httpClient); - TokenResponse token = new(); - try - { - token = tokenClient.AuthorizeAsync(Grant_type.Refresh_token, null, null, SSConnectionData.ssTokenRefresh, null).Result; - var tokenResult = token.Access_token; + token = tokenClient.AuthorizeAsync(Grant_type.Refresh_token, null, null, SSConnectionData.ssTokenRefresh, null).Result; + var tokenResult = token.Access_token; - SSConnectionData.ssTokenBearer = tokenResult; - SSConnectionData.ssTokenRefresh = token.Refresh_token; - SSConnectionData.ssTokenExpiresOn = token.Expires_on; - return tokenResult; - } - catch (Exception) + SSConnectionData.ssTokenBearer = tokenResult; + SSConnectionData.ssTokenRefresh = token.Refresh_token; + SSConnectionData.ssTokenExpiresOn = token.Expires_on; + return tokenResult; + } + catch (Exception) + { + // refresh token failed. clean memory and start fresh + SSConnectionData.ssTokenBearer = ""; + SSConnectionData.ssTokenRefresh = ""; + SSConnectionData.ssTokenExpiresOn = DateTime.Now; + // if OTP is required we need to ask user for a new OTP + if (!String.IsNullOrEmpty(SSConnectionData.ssOTP)) { - // refresh token failed. clean memory and start fresh - SSConnectionData.ssTokenBearer = ""; - SSConnectionData.ssTokenRefresh = ""; - SSConnectionData.ssTokenExpiresOn = DateTime.Now; - // if OTP is required we need to ask user for a new OTP - if (!String.IsNullOrEmpty(SSConnectionData.ssOTP)) - { - SSConnectionData.initdone = false; - // the call below executes a connection test, which fetches a valid token - SSConnectionData.Init(); - // we now have a fresh token in memory. return it to caller - return SSConnectionData.ssTokenBearer; - } - else - { - // no user interaction required. get a fresh token and return it to caller - return GetTokenFresh(); - } + SSConnectionData.initdone = false; + // the call below executes a connection test, which fetches a valid token + SSConnectionData.Init(); + // we now have a fresh token in memory. return it to caller + return SSConnectionData.ssTokenBearer; + } + else + { + // no user interaction required. get a fresh token and return it to caller + return GetTokenFresh(); } } } } - static string GetTokenFresh() + } + static string GetTokenFresh() + { + using (var httpClient = new HttpClient()) { - using (var httpClient = new HttpClient()) - { - // Authenticate: - var tokenClient = new OAuth2ServiceClient(SSConnectionData.ssUrl, httpClient); - // call below will throw an exception if the creds are invalid - var token = tokenClient.AuthorizeAsync(Grant_type.Password, SSConnectionData.ssUsername, SSConnectionData.ssPassword, null, SSConnectionData.ssOTP).Result; - // here we can be sure the creds are ok - return success state - var tokenResult = token.Access_token; + // Authenticate: + var tokenClient = new OAuth2ServiceClient(SSConnectionData.ssUrl, httpClient); + // call below will throw an exception if the creds are invalid + var token = tokenClient.AuthorizeAsync(Grant_type.Password, SSConnectionData.ssUsername, SSConnectionData.ssPassword, null, SSConnectionData.ssOTP).Result; + // here we can be sure the creds are ok - return success state + var tokenResult = token.Access_token; - SSConnectionData.ssTokenBearer = tokenResult; - SSConnectionData.ssTokenRefresh = token.Refresh_token; - SSConnectionData.ssTokenExpiresOn = token.Expires_on; - return tokenResult; - } - } - #endregion - - - // input must be the secret id to fetch - public static void FetchSecretFromServer(string input, out string username, out string password, out string domain, out string privatekey) - { - // get secret id - int secretID = Int32.Parse(input); - - // init connection credentials, display popup if necessary - SSConnectionData.Init(); - - // get the secret - FetchSecret(secretID, out username, out password, out domain, out privatekey); + SSConnectionData.ssTokenBearer = tokenResult; + SSConnectionData.ssTokenRefresh = token.Refresh_token; + SSConnectionData.ssTokenExpiresOn = token.Expires_on; + return tokenResult; } } + #endregion + + + // input must be the secret id to fetch + public static void FetchSecretFromServer(string input, out string username, out string password, out string domain, out string privatekey) + { + // get secret id + int secretID = Int32.Parse(input); + + // init connection credentials, display popup if necessary + SSConnectionData.Init(); + + // get the secret + FetchSecret(secretID, out username, out password, out domain, out privatekey); + } } diff --git a/ExternalConnectors/ExternalConnectors.csproj b/ExternalConnectors/ExternalConnectors.csproj index 8d806aee..d921a579 100644 --- a/ExternalConnectors/ExternalConnectors.csproj +++ b/ExternalConnectors/ExternalConnectors.csproj @@ -15,8 +15,8 @@ - - + + @@ -25,6 +25,9 @@ Form + + Form + Form diff --git a/mRemoteNG/Connection/ExternalCredentialProviderSelector.cs b/mRemoteNG/Connection/ExternalCredentialProviderSelector.cs index 6eb2504a..29acec54 100644 --- a/mRemoteNG/Connection/ExternalCredentialProviderSelector.cs +++ b/mRemoteNG/Connection/ExternalCredentialProviderSelector.cs @@ -9,6 +9,10 @@ namespace mRemoteNG.Connection None = 0, [LocalizedAttributes.LocalizedDescription(nameof(Language.ECPDelineaSecretServer))] - DelineaSecretServer = 1 + DelineaSecretServer = 1, + + [LocalizedAttributes.LocalizedDescription(nameof(Language.ECPClickstudiosPasswordstate))] + ClickstudiosPasswordState = 2 + } } diff --git a/mRemoteNG/Connection/Protocol/PuttyBase.cs b/mRemoteNG/Connection/Protocol/PuttyBase.cs index 2de96413..2cd21c75 100644 --- a/mRemoteNG/Connection/Protocol/PuttyBase.cs +++ b/mRemoteNG/Connection/Protocol/PuttyBase.cs @@ -14,6 +14,7 @@ using System.Linq; using System.Runtime.Versioning; using System.Threading; using System.Windows.Forms; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel; // ReSharper disable ArrangeAccessorOwnerBody @@ -127,6 +128,28 @@ namespace mRemoteNG.Connection.Protocol Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0); } } + else if (InterfaceControl.Info.ExternalCredentialProvider == ExternalCredentialProvider.ClickstudiosPasswordState) + { + try + { + ExternalConnectors.CPS.PasswordstateInterface.FetchSecretFromServer($"{UserViaAPI}", out username, out password, out domain, out privatekey); + + if (!string.IsNullOrEmpty(privatekey)) + { + optionalTemporaryPrivateKeyPath = Path.GetTempFileName(); + File.WriteAllText(optionalTemporaryPrivateKeyPath, privatekey); + FileInfo fileInfo = new(optionalTemporaryPrivateKeyPath) + { + Attributes = FileAttributes.Temporary + }; + } + } + catch (Exception ex) + { + Event_ErrorOccured(this, "Passwordstate Interface Error: " + ex.Message, 0); + } + } + if (string.IsNullOrEmpty(username)) { diff --git a/mRemoteNG/Connection/Protocol/RDP/RdpProtocol.cs b/mRemoteNG/Connection/Protocol/RDP/RdpProtocol.cs index ffcea237..d83c2141 100644 --- a/mRemoteNG/Connection/Protocol/RDP/RdpProtocol.cs +++ b/mRemoteNG/Connection/Protocol/RDP/RdpProtocol.cs @@ -18,6 +18,8 @@ using MSTSCLib; using mRemoteNG.Resources.Language; using System.Runtime.Versioning; using FileDialog = Microsoft.Win32.FileDialog; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel; +using System.DirectoryServices.ActiveDirectory; namespace mRemoteNG.Connection.Protocol.RDP { @@ -450,8 +452,20 @@ namespace mRemoteNG.Connection.Protocol.RDP { Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0); } - } + else if (InterfaceControl.Info.ExternalCredentialProvider == ExternalCredentialProvider.ClickstudiosPasswordState) + { + try + { + string RDGUserViaAPI = InterfaceControl.Info.RDGatewayUserViaAPI; + ExternalConnectors.CPS.PasswordstateInterface.FetchSecretFromServer($"{RDGUserViaAPI}", out gwu, out gwp, out gwd, out pkey); + } + catch (Exception ex) + { + Event_ErrorOccured(this, "Passwordstate Interface Error: " + ex.Message, 0); + } + } + if (connectionInfo.RDGatewayUseConnectionCredentials != RDGatewayUseConnectionCredentials.AccessToken) { @@ -538,7 +552,17 @@ namespace mRemoteNG.Connection.Protocol.RDP { Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0); } - + } + else if (InterfaceControl.Info.ExternalCredentialProvider == ExternalCredentialProvider.ClickstudiosPasswordState) + { + try + { + ExternalConnectors.CPS.PasswordstateInterface.FetchSecretFromServer($"{userViaApi}", out userName, out password, out domain, out pkey); + } + catch (Exception ex) + { + Event_ErrorOccured(this, "Passwordstate Interface Error: " + ex.Message, 0); + } } if (string.IsNullOrEmpty(userName)) diff --git a/mRemoteNG/Language/Language.Designer.cs b/mRemoteNG/Language/Language.Designer.cs index cedf95c7..79947615 100644 --- a/mRemoteNG/Language/Language.Designer.cs +++ b/mRemoteNG/Language/Language.Designer.cs @@ -1807,6 +1807,15 @@ namespace mRemoteNG.Resources.Language { } } + /// + /// Looks up a localized string similar to Clickstudios Passwordstate. + /// + internal static string ECPClickstudiosPasswordstate { + get { + return ResourceManager.GetString("ECPClickstudiosPasswordstate", resourceCulture); + } + } + /// /// Looks up a localized string similar to Delinea Secret Server. /// diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx index eba84d39..5420b864 100644 --- a/mRemoteNG/Language/Language.resx +++ b/mRemoteNG/Language/Language.resx @@ -2289,7 +2289,10 @@ Nightly Channel includes Alphas, Betas & Release Candidates. Delinea Secret Server - + + Clickstudios Passwordstate + + None diff --git a/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs index 7a354509..fb53dd2c 100644 --- a/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs +++ b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs @@ -238,7 +238,8 @@ namespace mRemoteNG.UI.Controls.ConnectionInfoPropertyGrid { strHide.Add(nameof(AbstractConnectionRecord.UserViaAPI)); } - else if (SelectedConnectionInfo.ExternalCredentialProvider == ExternalCredentialProvider.DelineaSecretServer) + else if (SelectedConnectionInfo.ExternalCredentialProvider == ExternalCredentialProvider.DelineaSecretServer + || SelectedConnectionInfo.ExternalCredentialProvider == ExternalCredentialProvider.ClickstudiosPasswordState) { strHide.Add(nameof(AbstractConnectionRecord.Username)); strHide.Add(nameof(AbstractConnectionRecord.Password));