Merge pull request #2591 from tecxx/develop-orig

add Clickstudios Passwordstate API connector
This commit is contained in:
Dimitrij
2024-06-07 13:32:56 +01:00
committed by GitHub
16 changed files with 1095 additions and 274 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,241 @@
namespace ExternalConnectors.CPS
{
partial class CPSConnectionForm
{
/// <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()
{
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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,149 @@
<?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>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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=
</value>
</data>
</root>

View File

@@ -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<JsonNode>(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<JsonNode>(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<JsonNode>(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<JsonNode>(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);
}
}

View File

@@ -7,8 +7,8 @@ using SecretServerAuthentication.DSS;
using SecretServerRestClient.DSS;
using System.Security.Cryptography;
namespace ExternalConnectors.DSS
{
namespace ExternalConnectors.DSS;
public class SecretServerInterface
{
private static class SSConnectionData
@@ -339,4 +339,3 @@ namespace ExternalConnectors.DSS
FetchSecret(secretID, out username, out password, out domain, out privatekey);
}
}
}

View File

@@ -15,8 +15,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.Core" Version="3.7.303.25" />
<PackageReference Include="AWSSDK.EC2" Version="3.7.326" />
<PackageReference Include="AWSSDK.Core" Version="3.7.303.26" />
<PackageReference Include="AWSSDK.EC2" Version="3.7.326.1" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
@@ -25,6 +25,9 @@
<Compile Update="AWS\AWSConnectionForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="CPS\CPSConnectionForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="DSS\SSConnectionForm.cs">
<SubType>Form</SubType>
</Compile>

View File

@@ -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
}
}

View File

@@ -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))
{

View File

@@ -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))

View File

@@ -1807,6 +1807,15 @@ namespace mRemoteNG.Resources.Language {
}
}
/// <summary>
/// Looks up a localized string similar to Clickstudios Passwordstate.
/// </summary>
internal static string ECPClickstudiosPasswordstate {
get {
return ResourceManager.GetString("ECPClickstudiosPasswordstate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delinea Secret Server.
/// </summary>

View File

@@ -2288,6 +2288,9 @@ Nightly Channel includes Alphas, Betas &amp; Release Candidates.</value>
</data>
<data name="ECPDelineaSecretServer" xml:space="preserve">
<value>Delinea Secret Server</value>
</data>
<data name="ECPClickstudiosPasswordstate" xml:space="preserve">
<value>Clickstudios Passwordstate</value>
</data>
<data name="ECPNone" xml:space="preserve">
<value>None</value>

View File

@@ -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));

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB