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)); diff --git a/mRemoteNGDocumentation/howtos/credvault.rst b/mRemoteNGDocumentation/howtos/credvault.rst index 8824c56e..d6a1a3eb 100644 --- a/mRemoteNGDocumentation/howtos/credvault.rst +++ b/mRemoteNGDocumentation/howtos/credvault.rst @@ -1,21 +1,44 @@ -************* +************************** Credential Vault Connector -************* +************************** -.. warning:: +mRemote supports fetching credentials from external credential vaults. This allows providing credentials to the connection without storing sensitive information in the config file, which has numerous benefits (security, auditing, rotating passwords, etc). +Two password vaults are currently supported: -This feature is currently only developed for Thycotic Secret Server (on-premise installations). It is implemented for RDP and SSH connections. +- Delinea Secret Server +- Clickstudios Passwordstate -mRemote supports fetching credentials from external credential vaults. This allows providing credentials to the connection without storing these to disk, which has numerous benefits (security, auditing, rotating passwords, etc). +The feature is implemented for RDP, RDP Gateway and SSH connections. -Instead of specifying username/password/domain directly in mRemote, leave these fields empty and just set the secret id: - -.. figure:: /images/credvault01.png - -The secret id is the unique identifier of your secret, you can find it in the URL in your thycotic interface. -e.g. https://cred.domain.local/SecretServer/app/#/secret/3318/general -> the secret id is 3318 - -Before initiating the connection mRemote will access your Secret Server API URL and fetch the data. For this to work the API endpoint URL and access credentials need to be specified. A popup will show up if this information has not yet been set. +Before initiating a connection mRemote will access your Password Vault API and fetch the secret. For this to work the API endpoint URL and access credentials need to be specified. A popup will show up if this information has not yet been set. .. figure:: /images/credvault02.png + +Instead of setting username/password/domain directly in mRemote, leave these fields empty and specify the secret id instead: + +.. figure:: /images/credvault01.png + +The secret id is the unique identifier of your secret. + + +Delinea Secret Server +--------------------- + +The secret ID can be found in the url of your secret: https://cred.domain.local/SecretServer/app/#/secret/3318/general -> the secret id is 3318 + +Authentication works with WinAuth/SSO (OnPremise) and Username/Password (OnPremise, Cloud). MFA via OTP is supported. + + +Clickstudios PasswordState +-------------------------- + +The secred ID can be found in the UI after enabling "toggle visibility of web API IDs" in the "List Administrator Actions" dropdown + +.. figure:: /images/credvault03.png + +Authentication works with WinAuth/SSO and list-based API-Keys. MFA via OTP is supported. + +- There is currently no support for token authentication, so if your API has MFA enabled, you need to specify a fresh OTP code quite frequently +- If you are using list-based API keys to access the vault, only one API key can currently be specified in the connector configuration + diff --git a/mRemoteNGDocumentation/images/credvault01.png b/mRemoteNGDocumentation/images/credvault01.png index 14d2cad8..e50f3a2d 100644 Binary files a/mRemoteNGDocumentation/images/credvault01.png and b/mRemoteNGDocumentation/images/credvault01.png differ diff --git a/mRemoteNGDocumentation/images/credvault03.png b/mRemoteNGDocumentation/images/credvault03.png new file mode 100644 index 00000000..81c5cf20 Binary files /dev/null and b/mRemoteNGDocumentation/images/credvault03.png differ