mirror of https://github.com/dahall/Vanara.git
327 lines
15 KiB
C#
327 lines
15 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Drawing;
|
|
using System.Security;
|
|
using System.Windows.Forms;
|
|
using Vanara.PInvoke;
|
|
using Vanara.Security;
|
|
using static Vanara.PInvoke.AdvApi32;
|
|
using static Vanara.PInvoke.CredUI;
|
|
using static Vanara.PInvoke.Gdi32;
|
|
|
|
namespace Vanara.Windows.Forms;
|
|
|
|
/// <summary>Dialog box which prompts for user credentials using the Win32 CREDUI methods.</summary>
|
|
[ToolboxItem(true), ToolboxBitmap(typeof(CredentialsDialog), "Dialog"), ToolboxItemFilter("System.Windows.Forms.Control.TopLevel"),
|
|
DesignTimeVisible(true), Description("Dialog that prompts the user for credentials."),
|
|
Designer("System.ComponentModel.Design.ComponentDesigner, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
|
|
public class CredentialsDialog : CommonDialog
|
|
{
|
|
private static readonly bool PreVista = Environment.OSVersion.Version.Major <= 5;
|
|
private uint authPackage;
|
|
private bool saveChecked;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="CredentialsDialog"/> class.</summary>
|
|
public CredentialsDialog() => Reset();
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="CredentialsDialog"/> class.</summary>
|
|
/// <param name="caption">The caption.</param>
|
|
/// <param name="message">The message.</param>
|
|
/// <param name="userName">Name of the user.</param>
|
|
public CredentialsDialog(string caption, string? message = null, string? userName = null) : this()
|
|
{
|
|
Caption = caption;
|
|
Message = message;
|
|
UserName = userName;
|
|
}
|
|
|
|
/// <summary>Occurs after the OK button has been clicked for validation of the supplied credentials.</summary>
|
|
/// <remarks>
|
|
/// If handled, the <see cref="PasswordValidatorEventArgs.Validated"/> property must be set to <c>true</c> if the password is valid or <c>false</c> if
|
|
/// not. If <c>false</c>, the <see cref="CommonDialog.ShowDialog()"/> method will return <see cref="DialogResult.Cancel"/>.
|
|
/// </remarks>
|
|
[Description("Validates the supplied credentials."), Category("Data")]
|
|
public event EventHandler<PasswordValidatorEventArgs>? ValidatePassword;
|
|
|
|
/// <summary>Gets or sets the Windows Error Code that caused this credential dialog to appear, if applicable.</summary>
|
|
[DefaultValue(0), Category("Data"), Description("Windows Error Code that caused this credential dialog")]
|
|
public int AuthenticationError { get; set; }
|
|
|
|
/// <summary>
|
|
/// <c>Windows Vista and later:</c> On input, the value of this parameter is used to specify the authentication package for which the credentials are serialized.
|
|
/// <para>
|
|
/// To get the appropriate value to use for this parameter on input, call the LsaLookupAuthenticationPackage function and use the value of the
|
|
/// AuthenticationPackage parameter of that function.
|
|
/// </para>
|
|
/// <para>On output, this parameter specifies the authentication package for which the credentials in the ppvOutAuthBuffer buffer are serialized.</para>
|
|
/// </summary>
|
|
/// <value>The authentication package.</value>
|
|
[Browsable(false), DefaultValue(0u)]
|
|
public uint AuthenticationPackage { get => authPackage; set => authPackage = value; }
|
|
|
|
/// <summary><c>Windows XP and earlier:</c> Gets or sets the image to display as the banner for the dialog.</summary>
|
|
[DefaultValue(null), Category("Appearance"), Description("Image to display in dialog banner")]
|
|
public Bitmap? Banner { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the caption for the dialog. If this value is <c>null</c> or an empty string, the name of the application will be shown as the caption.
|
|
/// </summary>
|
|
[DefaultValue(null), Category("Appearance"), Description("Caption to display for dialog")]
|
|
public string? Caption { get; set; }
|
|
|
|
/// <summary>Gets or sets a value indicating whether to encrypt password.</summary>
|
|
/// <value><c>true</c> if password is to be encrypted; otherwise, <c>false</c>.</value>
|
|
[DefaultValue(false), Category("Behavior"), Description("Indicates whether to encrypt password")]
|
|
public bool EncryptPassword { get; set; }
|
|
|
|
/// <summary>Gets or sets a value indicating whether to cause the pre-Windows Vista style dialog to be used regardless of the current Windows version.</summary>
|
|
/// <value><c>true</c> if older UI will always be used; <c>false</c> to display the UI commensurate for the current Windows version.</value>
|
|
[DefaultValue(false), Category("Appearance"), Description("Indicates whether to force the older UI.")]
|
|
public bool ForcePreVistaUI { get; set; }
|
|
|
|
/// <summary>Gets or sets the message to display on the dialog</summary>
|
|
[DefaultValue(null), Category("Appearance"), Description("Message to display in the dialog")]
|
|
public string? Message { get; set; }
|
|
|
|
/// <summary>Gets the password entered by the user</summary>
|
|
[DefaultValue(null), Browsable(false)]
|
|
public string? Password { get; private set; }
|
|
|
|
/// <summary>Gets or sets a boolean indicating if the save check box was checked</summary>
|
|
[DefaultValue(false), Category("Behavior"), Description("Indicates if the save check box is checked.")]
|
|
public bool SaveChecked { get => saveChecked; set => saveChecked = value; }
|
|
|
|
/// <summary>Gets the password entered by the user using an encrypted string</summary>
|
|
[DefaultValue(null), Browsable(false)]
|
|
public SecureString? SecurePassword { get; private set; }
|
|
|
|
/// <summary>Gets or sets a value that determines if the combo box is populated with local administrators only.</summary>
|
|
public bool ShowAdminsOnly { get; set; }
|
|
|
|
/// <summary>Gets or sets a boolean indicating if the save check box should be shown.</summary>
|
|
[DefaultValue(false), Category("Behavior"), Description("Indicates if the save check box is shown.")]
|
|
public bool ShowSaveCheckBox { get; set; }
|
|
|
|
/// <summary>Gets or sets the name of the target for these credentials</summary>
|
|
/// <remarks>This value is used as a key to store the credentials if persisted</remarks>
|
|
[DefaultValue(null), Category("Data"), Description("Target for the credentials")]
|
|
public string? Target { get; set; }
|
|
|
|
/// <summary>Gets or sets the username entered</summary>
|
|
/// <remarks>If non-empty before calling <see cref="RunDialog"/>, this value will be displayed in the dialog</remarks>
|
|
[DefaultValue(null), Category("Data"), Description("User name displayed in the dialog")]
|
|
public string? UserName { get; set; }
|
|
|
|
/// <summary>Gets a default value for the target.</summary>
|
|
/// <value>The default target.</value>
|
|
private static string DefaultTarget => Environment.UserDomainName;
|
|
|
|
/// <summary>Validates a password using the LogonUser API function.</summary>
|
|
/// <param name="userName">Name of the user.</param>
|
|
/// <param name="password">The password.</param>
|
|
/// <returns><c>true</c> if the credentials are validated, otherwise <c>false</c>.</returns>
|
|
public static bool IsValidPassword(string userName, string? password)
|
|
{
|
|
ParseUserName(userName, out var user, out var domain);
|
|
try
|
|
{
|
|
if (LogonUser(user, domain, password, LogonUserType.LOGON32_LOGON_NETWORK, LogonUserProvider.LOGON32_PROVIDER_DEFAULT, out var obj))
|
|
return true;
|
|
}
|
|
catch { }
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Wraps the CredUIParseUserName function which extracts the domain and user account name from a fully qualified user name.</summary>
|
|
/// <param name="userName">Contains the user name to be parsed. The name must be in UPN or down-level format, or a certificate.</param>
|
|
/// <param name="user">Receives the user account name.</param>
|
|
/// <param name="domain">Receives the domain name. If <paramref name="userName"/> specifies a certificate, domain will be NULL.</param>
|
|
public static void ParseUserName(string userName, out string user, out string? domain)
|
|
{
|
|
if (userName == null)
|
|
throw new ArgumentNullException(nameof(userName));
|
|
var sbUser = new StringBuilder(CREDUI_MAX_USERNAME_LENGTH);
|
|
var sbDomain = new StringBuilder(CREDUI_MAX_DOMAIN_TARGET_LENGTH);
|
|
var ret = CredUIParseUserName(userName, sbUser, CREDUI_MAX_USERNAME_LENGTH, sbDomain, CREDUI_MAX_DOMAIN_TARGET_LENGTH);
|
|
ret.ThrowIfFailed();
|
|
user = sbUser.ToString();
|
|
domain = sbDomain.Length != 0 ? sbDomain.ToString() : null;
|
|
}
|
|
|
|
/// <summary>Implements a standard password validator using the LogonUser API function.</summary>
|
|
/// <param name="sender">The sender.</param>
|
|
/// <param name="args">The <see cref="PasswordValidatorEventArgs"/> instance containing the event data.</param>
|
|
public static void StandardPasswordValidator(object? sender, PasswordValidatorEventArgs args)
|
|
{
|
|
if (args.SecurePassword != null) throw new ArgumentException();
|
|
args.Validated = IsValidPassword(args.Username, args.Password);
|
|
}
|
|
|
|
/// <summary>Confirms the credentials.</summary>
|
|
/// <param name="updateCredentials">If set to <c>true</c> the credentials are stored in the credential manager.</param>
|
|
/// <returns><c>true</c> if the credentials were confirmed; otherwise <c>false</c>.</returns>
|
|
public bool ConfirmCredentials(bool updateCredentials) => CredUIConfirmCredentials(Target ?? DefaultTarget, updateCredentials).Succeeded;
|
|
|
|
/// <summary>When overridden in a derived class, resets the properties of a common dialog box to their default values.</summary>
|
|
public override void Reset()
|
|
{
|
|
Target = UserName = Caption = Message = Password = null;
|
|
Banner = null;
|
|
EncryptPassword = ForcePreVistaUI = SaveChecked = ShowSaveCheckBox = false;
|
|
}
|
|
|
|
/// <summary>Gets the flags to use in <see cref="CredUIPromptForWindowsCredentials"/>.</summary>
|
|
/// <returns>The flags based on current properties.</returns>
|
|
protected virtual WindowsCredentialsDialogOptions GetDialogFlags() => WindowsCredentialsDialogOptions.CREDUIWIN_GENERIC |
|
|
(ShowAdminsOnly ? WindowsCredentialsDialogOptions.CREDUIWIN_ENUMERATE_ADMINS : 0) |
|
|
(ShowSaveCheckBox ? WindowsCredentialsDialogOptions.CREDUIWIN_CHECKBOX : 0);
|
|
|
|
/// <summary>Gets the flags to use in <see cref="CredUIPromptForCredentials"/>.</summary>
|
|
/// <returns>The flags based on current properties.</returns>
|
|
protected virtual CredentialsDialogOptions GetPreVistaDialogFlags() => CredentialsDialogOptions.CREDUI_FLAGS_DEFAULT |
|
|
(ShowAdminsOnly ? CredentialsDialogOptions.CREDUI_FLAGS_REQUEST_ADMINISTRATOR : 0) |
|
|
(ShowSaveCheckBox ? CredentialsDialogOptions.CREDUI_FLAGS_SHOW_SAVE_CHECK_BOX : 0);
|
|
|
|
/// <summary>When overridden in a derived class, specifies a common dialog box.</summary>
|
|
/// <param name="parentWindowHandle">A value that represents the window handle of the owner window for the common dialog box.</param>
|
|
/// <returns>true if the dialog box was successfully run; otherwise, false.</returns>
|
|
protected override bool RunDialog(IntPtr parentWindowHandle)
|
|
{
|
|
var info = new CREDUI_INFO(parentWindowHandle, Caption, Message) { hbmBanner = Banner?.GetHbitmap() ?? IntPtr.Zero };
|
|
try
|
|
{
|
|
if (PreVista || ForcePreVistaUI)
|
|
{
|
|
var userName = new StringBuilder(UserName, CREDUI_MAX_USERNAME_LENGTH);
|
|
var password = new StringBuilder(CREDUI_MAX_PASSWORD_LENGTH);
|
|
|
|
if (string.IsNullOrEmpty(Target)) Target = DefaultTarget;
|
|
var ret = CredUIPromptForCredentials(info, Target ?? DefaultTarget, IntPtr.Zero, unchecked((uint)AuthenticationError), userName,
|
|
CREDUI_MAX_USERNAME_LENGTH, password, CREDUI_MAX_PASSWORD_LENGTH, ref saveChecked, GetPreVistaDialogFlags());
|
|
if (ret == Win32Error.ERROR_CANCELLED)
|
|
return false;
|
|
if (ret != Win32Error.ERROR_SUCCESS)
|
|
throw new InvalidOperationException($"Unknown error in {nameof(CredentialsDialog)}. Error: 0x{(uint)ret:X}");
|
|
|
|
if (EncryptPassword)
|
|
{
|
|
// Convert the password to a SecureString
|
|
var newPassword = StringBuilderToSecureString(password);
|
|
|
|
// Clear the old password and set the new one (read-only)
|
|
SecurePassword?.Dispose();
|
|
newPassword.MakeReadOnly();
|
|
SecurePassword = newPassword;
|
|
Password = null;
|
|
}
|
|
else
|
|
Password = password.ToString();
|
|
|
|
if (ValidatePassword != null)
|
|
{
|
|
var pve = new PasswordValidatorEventArgs(userName.ToString(), Password, SecurePassword);
|
|
ValidatePassword.Invoke(this, pve);
|
|
if (!pve.Validated)
|
|
return false;
|
|
}
|
|
/*if (save)
|
|
{
|
|
CredUIReturnCodes cret = CredUIConfirmCredentials(this.Target, false);
|
|
if (cret != CredUIReturnCodes.NO_ERROR && cret != CredUIReturnCodes.ERROR_INVALID_PARAMETER)
|
|
{
|
|
this.Options |= CredentialsDialogOptions.IncorrectPassword;
|
|
return false;
|
|
}
|
|
}*/
|
|
|
|
// Update other properties
|
|
UserName = userName.ToString();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
using (var buf = EncryptPassword && SecurePassword != null ? new AuthenticationBuffer(UserName?.ToSecureString(), SecurePassword)
|
|
: new AuthenticationBuffer(UserName, Password))
|
|
{
|
|
var retVal = CredUIPromptForWindowsCredentials(info, 0, ref authPackage, buf, (uint)buf.Size, out var outAuthBuffer, out var outAuthBufferSize, ref saveChecked, GetDialogFlags());
|
|
if (retVal == Win32Error.ERROR_CANCELLED)
|
|
return false;
|
|
retVal.ThrowIfFailed();
|
|
|
|
using var outAuth = new AuthenticationBuffer(outAuthBuffer, (int)outAuthBufferSize);
|
|
if (EncryptPassword)
|
|
{
|
|
outAuth.UnPack(true, out SecureString u, out var d, out var p);
|
|
Password = null;
|
|
SecurePassword = p;
|
|
UserName = d?.Length > 0 ? $"{d.ToInsecureString()}\\{u.ToInsecureString()}" : u.ToInsecureString();
|
|
}
|
|
else
|
|
{
|
|
outAuth.UnPack(true, out string u, out var d, out var p);
|
|
Password = p;
|
|
SecurePassword = null;
|
|
UserName = string.IsNullOrEmpty(d) ? u : $"{d}\\{u}";
|
|
}
|
|
}
|
|
if (ValidatePassword != null)
|
|
{
|
|
var pve = new PasswordValidatorEventArgs(UserName ?? string.Empty, Password, SecurePassword);
|
|
ValidatePassword.Invoke(this, pve);
|
|
if (!pve.Validated)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!info.hbmBanner.IsNull)
|
|
DeleteObject(info.hbmBanner);
|
|
}
|
|
}
|
|
|
|
private static SecureString StringBuilderToSecureString(StringBuilder password)
|
|
{
|
|
// Copy the password into the secure string, zeroing the original buffer as we go
|
|
var newPassword = new SecureString();
|
|
for (var i = 0; i < password.Length; i++)
|
|
{
|
|
newPassword.AppendChar(password[i]);
|
|
password[i] = '\0';
|
|
}
|
|
return newPassword;
|
|
}
|
|
|
|
/// <summary>Used by the <see cref="ValidatePassword"/> event.</summary>
|
|
public class PasswordValidatorEventArgs : EventArgs
|
|
{
|
|
/// <summary>Initializes a new instance of the <see cref="PasswordValidatorEventArgs"/> class.</summary>
|
|
/// <param name="un">The user name.</param>
|
|
/// <param name="pwd">The password.</param>
|
|
/// <param name="sPwd">The secure password.</param>
|
|
internal PasswordValidatorEventArgs(string un, string? pwd, SecureString? sPwd)
|
|
{
|
|
Username = un;
|
|
Password = pwd;
|
|
SecurePassword = sPwd;
|
|
}
|
|
|
|
/// <summary>Gets the password.</summary>
|
|
/// <value>The password.</value>
|
|
public string? Password { get; }
|
|
|
|
/// <summary>Gets the secure password.</summary>
|
|
/// <value>The secure password.</value>
|
|
public SecureString? SecurePassword { get; }
|
|
|
|
/// <summary>Gets the username.</summary>
|
|
/// <value>The username.</value>
|
|
public string Username { get; }
|
|
|
|
/// <summary>Gets or sets a value indicating whether this <see cref="PasswordValidatorEventArgs"/> is validated.</summary>
|
|
/// <value><c>true</c> if validated; otherwise, <c>false</c>.</value>
|
|
public bool Validated { get; set; } = false;
|
|
}
|
|
} |