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,28 +7,28 @@ using SecretServerAuthentication.DSS;
using SecretServerRestClient.DSS; using SecretServerRestClient.DSS;
using System.Security.Cryptography; 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 = ""; if (initdone == true)
public static string ssPassword = ""; return;
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;
RegistryKey key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\mRemoteSSInterface"); RegistryKey key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\mRemoteSSInterface");
try try
@@ -39,174 +39,174 @@ namespace ExternalConnectors.DSS
f.tbUsername.Text = un ?? ""; f.tbUsername.Text = un ?? "";
f.tbPassword.Text = SSConnectionData.ssPassword; // in OTP refresh cases, this value might already be filled f.tbPassword.Text = SSConnectionData.ssPassword; // in OTP refresh cases, this value might already be filled
string? url = key.GetValue("URL") as string; string? url = key.GetValue("URL") as string;
if (url == null || !url.Contains("://")) if (url == null || !url.Contains("://"))
url = "https://cred.domain.local/SecretServer"; url = "https://cred.domain.local/SecretServer";
f.tbSSURL.Text = url; f.tbSSURL.Text = url;
var b = key.GetValue("SSO"); var b = key.GetValue("SSO");
if (b == null || (string)b != "True") if (b == null || (string)b != "True")
ssSSO = false; ssSSO = false;
else else
ssSSO = true; ssSSO = true;
f.cbUseSSO.Checked = ssSSO; f.cbUseSSO.Checked = ssSSO;
// show dialog // show dialog
while (true) 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 (TestCredentials() == true)
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) initdone = true;
{ break;
initdone = true;
break;
}
}
catch (Exception)
{
MessageBox.Show("Test Credentials failed - please check your credentials");
} }
} }
catch (Exception)
{
// write values to registry MessageBox.Show("Test Credentials failed - please check your credentials");
key.SetValue("Username", ssUsername); }
key.SetValue("URL", ssUrl);
key.SetValue("SSO", ssSSO);
}
catch (Exception)
{
throw;
}
finally
{
key.Close();
} }
// write values to registry
key.SetValue("Username", ssUsername);
key.SetValue("URL", ssUrl);
key.SetValue("SSO", ssSSO);
} }
} catch (Exception)
{
private static bool TestCredentials() throw;
{ }
if (SSConnectionData.ssSSO) 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; return true;
} }
else else
{ {
return false;
if (!String.IsNullOrEmpty(GetToken()))
{
return true;
}
else
{
return false;
}
} }
} }
}
private static SecretsServiceClient ConstructSecretsServiceClient() private static SecretsServiceClient ConstructSecretsServiceClient()
{
string baseURL = SSConnectionData.ssUrl;
if (SSConnectionData.ssSSO)
{ {
string baseURL = SSConnectionData.ssUrl; // REQUIRES IIS CONFIG! https://docs.thycotic.com/ss/11.0.0/api-scripting/webservice-iwa-powershell
if (SSConnectionData.ssSSO) 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 // Call REST API:
var handler = new HttpClientHandler() { UseDefaultCredentials = true }; return new SecretsServiceClient($"{baseURL}/winauthwebservices/api", httpClient);
var httpClient = new HttpClient(handler);
{
// 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(); var httpClient = new HttpClient();
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? var token = GetToken();
if (!string.IsNullOrEmpty(privatekeypassphrase)) // Set credentials (token):
{ httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
try
{
var key = DecodePrivateKey(privatekey, privatekeypassphrase);
privatekey = key;
}
catch(Exception)
{
} // Call REST API:
} return new SecretsServiceClient($"{baseURL}/api", httpClient);
// 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)
{
}
} }
} }
}
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 #region PUTTY KEY HANDLING
// decode rsa private key with encryption password // decode rsa private key with encryption password
private static string DecodePrivateKey(string encryptedPrivateKey, string password) private static string DecodePrivateKey(string encryptedPrivateKey, string password)
@@ -214,31 +214,31 @@ namespace ExternalConnectors.DSS
TextReader textReader = new StringReader(encryptedPrivateKey); TextReader textReader = new StringReader(encryptedPrivateKey);
PemReader pemReader = new(textReader, new PasswordFinder(password)); PemReader pemReader = new(textReader, new PasswordFinder(password));
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
TextWriter textWriter = new StringWriter(); TextWriter textWriter = new StringWriter();
var pemWriter = new PemWriter(textWriter); var pemWriter = new PemWriter(textWriter);
pemWriter.WriteObject(keyPair.Private); pemWriter.WriteObject(keyPair.Private);
pemWriter.Writer.Flush(); pemWriter.Writer.Flush();
return ""+textWriter.ToString(); return ""+textWriter.ToString();
} }
private class PasswordFinder : IPasswordFinder private class PasswordFinder : IPasswordFinder
{
private string password;
public PasswordFinder(string password)
{ {
private string password; this.password = password;
public PasswordFinder(string password)
{
this.password = password;
}
public char[] GetPassword()
{
return password.ToCharArray();
}
} }
public char[] GetPassword()
{
return password.ToCharArray();
}
}
// read private key pem string to rsacryptoserviceprovider // read private key pem string to rsacryptoserviceprovider
public static RSACryptoServiceProvider ImportPrivateKey(string pem) public static RSACryptoServiceProvider ImportPrivateKey(string pem)
{ {
@@ -252,91 +252,90 @@ namespace ExternalConnectors.DSS
#endregion #endregion
#region TOKEN #region TOKEN
private static string GetToken() 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 return GetTokenFresh();
if (String.IsNullOrEmpty(SSConnectionData.ssTokenBearer)) }
// 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(); var tokenClient = new OAuth2ServiceClient(SSConnectionData.ssUrl, httpClient);
} TokenResponse token = new();
// if there is a token, check if it is valid try
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); token = tokenClient.AuthorizeAsync(Grant_type.Refresh_token, null, null, SSConnectionData.ssTokenRefresh, null).Result;
TokenResponse token = new(); var tokenResult = token.Access_token;
try
{
token = tokenClient.AuthorizeAsync(Grant_type.Refresh_token, null, null, SSConnectionData.ssTokenRefresh, null).Result;
var tokenResult = token.Access_token;
SSConnectionData.ssTokenBearer = tokenResult; SSConnectionData.ssTokenBearer = tokenResult;
SSConnectionData.ssTokenRefresh = token.Refresh_token; SSConnectionData.ssTokenRefresh = token.Refresh_token;
SSConnectionData.ssTokenExpiresOn = token.Expires_on; SSConnectionData.ssTokenExpiresOn = token.Expires_on;
return tokenResult; return tokenResult;
} }
catch (Exception) 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.initdone = false;
SSConnectionData.ssTokenBearer = ""; // the call below executes a connection test, which fetches a valid token
SSConnectionData.ssTokenRefresh = ""; SSConnectionData.Init();
SSConnectionData.ssTokenExpiresOn = DateTime.Now; // we now have a fresh token in memory. return it to caller
// if OTP is required we need to ask user for a new OTP return SSConnectionData.ssTokenBearer;
if (!String.IsNullOrEmpty(SSConnectionData.ssOTP)) }
{ else
SSConnectionData.initdone = false; {
// the call below executes a connection test, which fetches a valid token // no user interaction required. get a fresh token and return it to caller
SSConnectionData.Init(); return GetTokenFresh();
// 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);
// Authenticate: // call below will throw an exception if the creds are invalid
var tokenClient = new OAuth2ServiceClient(SSConnectionData.ssUrl, httpClient); var token = tokenClient.AuthorizeAsync(Grant_type.Password, SSConnectionData.ssUsername, SSConnectionData.ssPassword, null, SSConnectionData.ssOTP).Result;
// call below will throw an exception if the creds are invalid // here we can be sure the creds are ok - return success state
var token = tokenClient.AuthorizeAsync(Grant_type.Password, SSConnectionData.ssUsername, SSConnectionData.ssPassword, null, SSConnectionData.ssOTP).Result; var tokenResult = token.Access_token;
// here we can be sure the creds are ok - return success state
var tokenResult = token.Access_token;
SSConnectionData.ssTokenBearer = tokenResult; SSConnectionData.ssTokenBearer = tokenResult;
SSConnectionData.ssTokenRefresh = token.Refresh_token; SSConnectionData.ssTokenRefresh = token.Refresh_token;
SSConnectionData.ssTokenExpiresOn = token.Expires_on; SSConnectionData.ssTokenExpiresOn = token.Expires_on;
return tokenResult; 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);
} }
} }
#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);
}
} }

View File

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

View File

@@ -9,6 +9,10 @@ namespace mRemoteNG.Connection
None = 0, None = 0,
[LocalizedAttributes.LocalizedDescription(nameof(Language.ECPDelineaSecretServer))] [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.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
// ReSharper disable ArrangeAccessorOwnerBody // ReSharper disable ArrangeAccessorOwnerBody
@@ -127,6 +128,28 @@ namespace mRemoteNG.Connection.Protocol
Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0); 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)) if (string.IsNullOrEmpty(username))
{ {

View File

@@ -18,6 +18,8 @@ using MSTSCLib;
using mRemoteNG.Resources.Language; using mRemoteNG.Resources.Language;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using FileDialog = Microsoft.Win32.FileDialog; using FileDialog = Microsoft.Win32.FileDialog;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
using System.DirectoryServices.ActiveDirectory;
namespace mRemoteNG.Connection.Protocol.RDP namespace mRemoteNG.Connection.Protocol.RDP
{ {
@@ -450,8 +452,20 @@ namespace mRemoteNG.Connection.Protocol.RDP
{ {
Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0); 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) if (connectionInfo.RDGatewayUseConnectionCredentials != RDGatewayUseConnectionCredentials.AccessToken)
{ {
@@ -538,7 +552,17 @@ namespace mRemoteNG.Connection.Protocol.RDP
{ {
Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0); 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)) 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> /// <summary>
/// Looks up a localized string similar to Delinea Secret Server. /// Looks up a localized string similar to Delinea Secret Server.
/// </summary> /// </summary>

View File

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

View File

@@ -238,7 +238,8 @@ namespace mRemoteNG.UI.Controls.ConnectionInfoPropertyGrid
{ {
strHide.Add(nameof(AbstractConnectionRecord.UserViaAPI)); 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.Username));
strHide.Add(nameof(AbstractConnectionRecord.Password)); strHide.Add(nameof(AbstractConnectionRecord.Password));

View File

@@ -1,21 +1,44 @@
************* **************************
Credential Vault Connector 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: 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/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.
.. figure:: /images/credvault02.png .. 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