Initial implementation of gatewayaccesstoken support

This commit is contained in:
Jure Purgar
2023-07-18 16:07:06 +02:00
parent 9bc5b62828
commit f1fa40f6fd
9 changed files with 341 additions and 8 deletions

View File

@@ -58,6 +58,7 @@ namespace mRemoteNG.Connection
private string _rdGatewayUsername;
private string _rdGatewayPassword;
private string _rdGatewayDomain;
private string _rdGatewayAccessToken;
private ExternalCredentialProvider _rdGatewayExternalCredentialProvider;
private string _rdGatewayUserViaAPI = "";
@@ -530,6 +531,17 @@ namespace mRemoteNG.Connection
set => SetField(ref _rdGatewayPassword, value, "RDGatewayPassword");
}
[LocalizedAttributes.LocalizedCategory(nameof(Language.RDPGateway), 4),
LocalizedAttributes.LocalizedDisplayName(nameof(Language.RdpGatewayAccessToken)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionRdpGatewayAccessToken)),
PasswordPropertyText(true),
AttributeUsedInProtocol(ProtocolType.RDP)]
public string RDGatewayAccessToken
{
get => GetPropertyValue("RDGatewayAccessToken", _rdGatewayAccessToken);
set => SetField(ref _rdGatewayAccessToken, value, "RDGatewayAccessToken");
}
[LocalizedAttributes.LocalizedCategory(nameof(Language.RDPGateway), 4),
LocalizedAttributes.LocalizedDisplayName(nameof(Language.RdpGatewayDomain)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionRDGatewayDomain)),

View File

@@ -15,6 +15,9 @@ namespace mRemoteNG.Connection.Protocol.RDP
SmartCard = 2,
[LocalizedAttributes.LocalizedDescription(nameof(Language.UseExternalCredentialProvider))]
ExternalCredentialProvider = 3
ExternalCredentialProvider = 3,
[LocalizedAttributes.LocalizedDescription(nameof(Language.UseAccessToken))]
AccessToken = 4
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace mRemoteNG.Connection.Protocol.RDP
{
internal static class RdGatewayAccessTokenHelper
{
public static string EncryptAuthCookieString(string cookieString)
{
byte[] cookieBytes = TsCryptEncryptString(cookieString);
if (cookieBytes != null)
{
return Convert.ToBase64String(cookieBytes);
}
return null;
}
public static string DecryptAuthCookieString(string cookieString) //TODO: decrypt is newer use, should we remove it?
{
return TsCryptDecryptString(Convert.FromBase64String(cookieString));
}
[StructLayout(LayoutKind.Sequential)]
struct CryptProtectPromptStruct
{
public int Size;
public int Flags;
public IntPtr Window;
public string Message;
}
[StructLayout(LayoutKind.Sequential)]
struct DataBlob
{
public int Size;
public IntPtr Data;
}
private const int CRYPTPROTECT_LOCAL_MACHINE = 0x00000004;
private const int CRYPTPROTECT_UI_FORBIDDEN = 0x00000001;
private const int CRYPTPROTECT_AUDIT = 0x00000010;
[DllImport("crypt32.dll", CharSet = CharSet.Unicode)]
private static extern bool CryptProtectData(
ref DataBlob dataIn,
IntPtr description,
IntPtr optionalEntropy,
IntPtr reserved,
IntPtr promptStruct,
int flags,
out DataBlob dataOut);
[DllImport("crypt32.dll", CharSet = CharSet.Unicode)] //TODO: decrypt is newer use, should we remove it?
private static extern bool CryptUnprotectData(
ref DataBlob dataIn,
IntPtr description,
IntPtr optionalEntropy,
IntPtr reserved,
IntPtr promptStruct,
int flags,
out DataBlob dataOut);
private static byte[] TsCryptEncryptString(string inputString)
{
DataBlob inputBlob;
DataBlob outputBlob;
byte[] outputData = null;
byte[] stringBytes = Encoding.Unicode.GetBytes(inputString);
byte[] inputData = new byte[stringBytes.Length + 2];
Buffer.BlockCopy(stringBytes, 0, inputData, 0, stringBytes.Length);
inputBlob.Size = inputData.Length;
inputBlob.Data = Marshal.AllocHGlobal(inputData.Length);
Marshal.Copy(inputData, 0, inputBlob.Data, inputBlob.Size);
if (CryptProtectData(ref inputBlob, IntPtr.Zero, IntPtr.Zero,
IntPtr.Zero, IntPtr.Zero, CRYPTPROTECT_UI_FORBIDDEN, out outputBlob))
{
outputData = new byte[outputBlob.Size];
Marshal.Copy(outputBlob.Data, outputData, 0, outputBlob.Size);
}
Marshal.FreeHGlobal(inputBlob.Data);
Marshal.FreeHGlobal(outputBlob.Data);
return outputData;
}
private static string TsCryptDecryptString(byte[] inputBytes) //TODO: decrypt is newer use, should we remove it?
{
DataBlob inputBlob;
DataBlob outputBlob;
byte[] outputData = null;
inputBlob.Size = inputBytes.Length;
inputBlob.Data = Marshal.AllocHGlobal(inputBytes.Length);
Marshal.Copy(inputBytes, 0, inputBlob.Data, inputBlob.Size);
if (CryptUnprotectData(ref inputBlob, IntPtr.Zero, IntPtr.Zero,
IntPtr.Zero, IntPtr.Zero, CRYPTPROTECT_UI_FORBIDDEN, out outputBlob))
{
outputData = new byte[outputBlob.Size];
Marshal.Copy(outputBlob.Data, outputData, 0, outputBlob.Size);
}
Marshal.FreeHGlobal(inputBlob.Data);
Marshal.FreeHGlobal(outputBlob.Data);
if (outputData != null)
{
return Encoding.Unicode.GetString(outputData).TrimEnd((Char)0);
}
return null;
}
}
}

View File

@@ -428,9 +428,18 @@ namespace mRemoteNG.Connection.Protocol.RDP
}
}
_rdpClient.TransportSettings2.GatewayUsername = gwu;
_rdpClient.TransportSettings2.GatewayPassword = gwp;
_rdpClient.TransportSettings2.GatewayDomain = gwd;
if (connectionInfo.RDGatewayUseConnectionCredentials != RDGatewayUseConnectionCredentials.AccessToken)
{
_rdpClient.TransportSettings2.GatewayUsername = gwu;
_rdpClient.TransportSettings2.GatewayPassword = gwp;
_rdpClient.TransportSettings2.GatewayDomain = gwd;
}
else
{
//TODO: should we check client version and throw if it is less than 7
}
break;
}
}

View File

@@ -37,6 +37,15 @@ namespace mRemoteNG.Connection.Protocol.RDP
if (connectionInfo.UseEnhancedMode)
RdpClient7.AdvancedSettings7.PCB += ";EnhancedMode=1";
}
if (connectionInfo.RDGatewayUseConnectionCredentials == RDGatewayUseConnectionCredentials.AccessToken)
{
var authToken = connectionInfo.RDGatewayAccessToken;
var encryptedAuthToken = RdGatewayAccessTokenHelper.EncryptAuthCookieString(authToken);
RdpClient7.TransportSettings3.GatewayEncryptedAuthCookie = encryptedAuthToken;
RdpClient7.TransportSettings3.GatewayEncryptedAuthCookieSize = (uint)encryptedAuthToken.Length;
RdpClient7.TransportSettings3.GatewayCredsSource = 5;
}
}
catch (Exception ex)
{

View File

@@ -646,7 +646,7 @@ namespace mRemoteNG.Resources.Language {
}
/// <summary>
/// Looks up a localized string similar to Automatically try to reconnect when disconnected from server (RDP &amp;&amp; ICA only).
/// Looks up a localized string similar to Display reconnection dialog when disconnected from server (RDP &amp;&amp; ICA only).
/// </summary>
internal static string CheckboxAutomaticReconnect {
get {
@@ -663,6 +663,15 @@ namespace mRemoteNG.Resources.Language {
}
}
/// <summary>
/// Looks up a localized string similar to Automatically try to reconnect when disconnected from server (RDP &amp;&amp; ICA only).
/// </summary>
internal static string CheckboxNoReconnect {
get {
return ResourceManager.GetString("CheckboxNoReconnect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This proxy server requires authentication.
/// </summary>
@@ -1547,7 +1556,7 @@ namespace mRemoteNG.Resources.Language {
}
/// <summary>
/// Looks up a localized string similar to Disable Cursor blinking.
/// Looks up a localized string similar to Disable Cursor Blinking.
/// </summary>
internal static string DisableCursorBlinking {
get {
@@ -4053,6 +4062,15 @@ namespace mRemoteNG.Resources.Language {
}
}
/// <summary>
/// Looks up a localized string similar to Specifies the access token for Remote Desktop Gateway server..
/// </summary>
internal static string PropertyDescriptionRdpGatewayAccessToken {
get {
return ResourceManager.GetString("PropertyDescriptionRdpGatewayAccessToken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Specifies the password of the Remote Desktop Gateway server..
/// </summary>
@@ -4133,8 +4151,8 @@ namespace mRemoteNG.Resources.Language {
return ResourceManager.GetString("PropertyDescriptionRedirectDiskDrivesCustom", resourceCulture);
}
}
/// <summary>
/// <summary>
/// Looks up a localized string similar to Select whether local disk drives should be shown on the remote host..
/// </summary>
internal static string PropertyDescriptionRedirectDrives {
@@ -4909,6 +4927,15 @@ namespace mRemoteNG.Resources.Language {
}
}
/// <summary>
/// Looks up a localized string similar to RDP-Gateway Access Token.
/// </summary>
internal static string RdpGatewayAccessToken {
get {
return ResourceManager.GetString("RdpGatewayAccessToken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to RDP-Gateway Domain.
/// </summary>
@@ -6334,6 +6361,15 @@ namespace mRemoteNG.Resources.Language {
}
}
/// <summary>
/// Looks up a localized string similar to Use RD Gateway access token.
/// </summary>
internal static string UseAccessToken {
get {
return ResourceManager.GetString("UseAccessToken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use Console Session.
/// </summary>

View File

@@ -2334,4 +2334,13 @@ Nightly Channel includes Alphas, Betas &amp; Release Candidates.</value>
<data name="RedirectDiskDrivesCustom" xml:space="preserve">
<value>Custom Drives</value>
</data>
<data name="PropertyDescriptionRdpGatewayAccessToken" xml:space="preserve">
<value>Specifies the access token for Remote Desktop Gateway server.</value>
</data>
<data name="RdpGatewayAccessToken" xml:space="preserve">
<value>RDP-Gateway Access Token</value>
</data>
<data name="UseAccessToken" xml:space="preserve">
<value>Use RD Gateway access token</value>
</data>
</root>

View File

@@ -267,6 +267,7 @@ namespace mRemoteNG.UI.Controls.ConnectionInfoPropertyGrid
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayPassword));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUseConnectionCredentials));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUsername));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayAccessToken));
}
else if (SelectedConnectionInfo.RDGatewayUseConnectionCredentials == RDGatewayUseConnectionCredentials.Yes ||
SelectedConnectionInfo.RDGatewayUseConnectionCredentials == RDGatewayUseConnectionCredentials.SmartCard)
@@ -276,12 +277,22 @@ namespace mRemoteNG.UI.Controls.ConnectionInfoPropertyGrid
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUsername));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayExternalCredentialProvider));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUserViaAPI));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayAccessToken));
}
else if (SelectedConnectionInfo.RDGatewayUseConnectionCredentials == RDGatewayUseConnectionCredentials.ExternalCredentialProvider)
{
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayDomain));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayPassword));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUsername));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayAccessToken));
}
else if (SelectedConnectionInfo.RDGatewayUseConnectionCredentials == RDGatewayUseConnectionCredentials.AccessToken)
{
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayDomain));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayPassword));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUsername));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayExternalCredentialProvider));
strHide.Add(nameof(AbstractConnectionRecord.RDGatewayUserViaAPI));
}
if (!(SelectedConnectionInfo.Resolution == RDPResolutions.FitToWindow ||

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>