Get-WinCredential/CredentialsDialog.cs

using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Windows.Forms;
using GetWinCredential.Pinvoke;
 
namespace GetWinCredential
{
    /// <summary>
    /// Encapsulates dialog functionality from the Credential Management API.
    /// </summary>
    public sealed class CredentialsDialog
    {
        /// <summary>
        /// Gets or sets if the dialog will be shown even if the credentials can be returned
        /// from an existing credential in the credential manager.
        /// </summary>
        private readonly bool _alwaysDisplay;
 
        /// <summary>
        /// Gets or sets if the dialog is populated with username/password only.
        /// </summary>
        private readonly bool _excludeCertificates;
 
        /// <summary>
        /// Gets or sets if the username is read-only.
        /// </summary>
        private readonly bool _keepName;
 
        /// <summary>
        /// Gets or sets if the credentials are to be persisted in the credential manager.
        /// </summary>
        private readonly bool _persist;
 
        /// <summary>
        /// Gets or sets if the save checkbox is displayed.
        /// </summary>
        /// <remarks> This value only has effect if _persist is true. </remarks>
        private readonly bool _saveDisplayed;
 
        /// <summary>
        /// Gets or sets the username of the target for the credentials, typically a server username.
        /// </summary>
        private readonly string _target;
 
        /// <summary>
        /// Gets or sets if modern dialog is used or not.
        /// </summary>
        private readonly bool _useModernUI;
 
        private string _captionValue;
 
        private string _messageValue;
 
        private string _name = string.Empty;
 
        private SecureString _password = null;
 
        /// <summary>
        /// Gets or sets if the save checkbox status.
        /// </summary>
        private bool _saveChecked;
 
        /// <summary>
        /// Gets or sets the password for the credentials.
        /// </summary>
        public SecureString Password
        {
            get
            {
                return _password;
            }
            set
            {
                if (value != null)
                {
                    if (value.Length > CREDUI.MAX_PASSWORD_LENGTH)
                    {
                        var message = string.Format(
                            CultureInfo.InvariantCulture,
                            "The password has a maximum length of {0} characters.",
                            CREDUI.MAX_PASSWORD_LENGTH);
                        throw new ArgumentException(message, "Password");
                    }
                }
                // Convert to secure string here
                _password = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the username for the credentials.
        /// </summary>
        public string UserName
        {
            get
            {
                return _name;
            }
            set
            {
                if (value != null)
                {
                    if (value.Length > CREDUI.MAX_USERNAME_LENGTH)
                    {
                        var message = string.Format(
                            CultureInfo.InvariantCulture,
                            "The username has a maximum length of {0} characters.",
                            CREDUI.MAX_USERNAME_LENGTH);
                        throw new ArgumentException(message, "UserName");
                    }
                }
                _name = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the caption of the dialog.
        /// </summary>
        /// <remarks> A null value will cause a system default caption to be used. </remarks>
        private string Message
        {
            get
            {
                return _messageValue;
            }
            set
            {
                if (value != null)
                {
                    if (value.Length > CREDUI.MAX_MESSAGE_LENGTH)
                    {
                        var message = string.Format(
                            CultureInfo.InvariantCulture,
                            "The caption has a maximum length of {0} characters.",
                            CREDUI.MAX_MESSAGE_LENGTH);
                        throw new ArgumentException(message, "Message");
                    }
                }
                _messageValue = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the caption of the dialog.
        /// </summary>
        /// <remarks> A null value will cause a default caption to be used. </remarks>
        private string Caption
        {
            get
            {
                return _captionValue;
            }
            set
            {
                if (value != null)
                {
                    if (value.Length > CREDUI.MAX_CAPTION_LENGTH)
                    {
                        var caption = string.Format(
                            CultureInfo.InvariantCulture,
                            "The caption has a maximum length of {0} characters.",
                            CREDUI.MAX_CAPTION_LENGTH);
                        throw new ArgumentException(caption, "Caption");
                    }
                }
                _captionValue = value;
            }
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="T:GetWinCredential.CredentialsDialog"
        /// /> class with the specified caption.
        /// </summary>
        /// <param name="message">
        /// The caption of the dialog (null will cause a system default caption to be used).
        /// </param>
        /// <param name="useModernUI"> Use Vista+ dialog </param>
        public CredentialsDialog(string caption="", string message = "", bool useModernUI = false)
        {
            _target = "PowerShell";
 
            if (string.IsNullOrEmpty(caption))
            {
                Caption = "Credentials";
            }
            else
            {
                Caption = caption;
            }
 
            if (string.IsNullOrEmpty(message))
            {
                Message = "Enter your credentials.";
            }
            else
            {
                Message = message;
            }
            _useModernUI = useModernUI;
            // Keep the default values
            _alwaysDisplay = true;
            _excludeCertificates = false;
            _persist = false;
            _keepName = false;
            _saveChecked = false;
            _saveDisplayed = false;
        }
        /// <summary>
        /// Shows the credentials dialog with the specified owner, username, password and save
        /// checkbox status.
        /// </summary>
        /// <param name="username"> The username for the credentials. </param>
        /// <returns> Returns a DialogResult indicating the user action. </returns>
        public DialogResult Show(string username = "")
        {
            if (string.IsNullOrEmpty(username))
            {
                username = "";
            }
            UserName = username;
            _saveChecked = false;
 
            // Get the owner
            var owner = new NativeWindow();
            owner.AssignHandle(Process.GetCurrentProcess().MainWindowHandle);
 
            return ShowDialog(owner);
        }
 
        private static SecureString ConvertToSecureString(string value)
        {
            var secureString = new SecureString();
            foreach (var c in value)
            {
                secureString.AppendChar(c);
            }
            return secureString;
        }
        /// <summary>
        /// Returns a DialogResult from the specified code.
        /// </summary>
        /// <param name="code"> The credential return code. </param>
        private static DialogResult GetDialogResult(CREDUI.ReturnCodes code)
        {
            switch (code)
            {
                case CREDUI.ReturnCodes.NO_ERROR:
                    return DialogResult.OK;
 
                case CREDUI.ReturnCodes.ERROR_CANCELLED:
                    return DialogResult.Cancel;
 
                case CREDUI.ReturnCodes.ERROR_NO_SUCH_LOGON_SESSION:
                    throw new ApplicationException("No such logon session.");
                case CREDUI.ReturnCodes.ERROR_NOT_FOUND:
                    throw new ApplicationException("Not found.");
                case CREDUI.ReturnCodes.ERROR_INVALID_ACCOUNT_NAME:
                    throw new ApplicationException("Invalid account username.");
                case CREDUI.ReturnCodes.ERROR_INSUFFICIENT_BUFFER:
                    throw new ApplicationException("Insufficient buffer.");
                case CREDUI.ReturnCodes.ERROR_INVALID_PARAMETER:
                    throw new ApplicationException("Invalid parameter.");
                case CREDUI.ReturnCodes.ERROR_INVALID_FLAGS:
                    throw new ApplicationException("Invalid flags.");
                default:
                    throw new ApplicationException("Unknown credential result encountered.");
            }
        }
 
        /// <summary>
        /// Returns a DialogResult from the specified code.
        /// </summary>
        /// <param name="code"> The credential return code. </param>
        private static DialogResult GetDialogResultModernUI(CREDUI.ReturnCodesModernUI code)
        {
            switch (code)
            {
                case CREDUI.ReturnCodesModernUI.NO_ERROR:
                    return DialogResult.OK;
 
                case CREDUI.ReturnCodesModernUI.ERROR_CANCELLED:
                    return DialogResult.Cancel;
 
                case CREDUI.ReturnCodesModernUI.ERROR_NO_SUCH_LOGON_SESSION:
                    throw new ApplicationException("No such logon session.");
                case CREDUI.ReturnCodesModernUI.ERROR_NOT_FOUND:
                    throw new ApplicationException("Not found.");
                case CREDUI.ReturnCodesModernUI.ERROR_INVALID_ACCOUNT_NAME:
                    throw new ApplicationException("Invalid account username.");
                case CREDUI.ReturnCodesModernUI.ERROR_INSUFFICIENT_BUFFER:
                    throw new ApplicationException("Insufficient buffer.");
                case CREDUI.ReturnCodesModernUI.ERROR_INVALID_PARAMETER:
                    throw new ApplicationException("Invalid parameter.");
                case CREDUI.ReturnCodesModernUI.ERROR_INVALID_FLAGS:
                    throw new ApplicationException("Invalid flags.");
                default:
                    throw new ApplicationException("Unknown credential result encountered.");
            }
        }
 
        /// <summary>
        /// Returns the flags for dialog display options.
        /// </summary>
        private CREDUI.FLAGS GetFlags()
        {
            var flags = CREDUI.FLAGS.GENERIC_CREDENTIALS;
            // grrrr... can't seem to get this to work... if (incorrectPassword) flags = flags | CredUI.CREDUI_FLAGS.INCORRECT_PASSWORD;
            if (_alwaysDisplay) flags |= CREDUI.FLAGS.ALWAYS_SHOW_UI;
            if (_excludeCertificates) flags |= CREDUI.FLAGS.EXCLUDE_CERTIFICATES;
            if (_persist)
            {
                flags |= CREDUI.FLAGS.EXPECT_CONFIRMATION;
                if (_saveDisplayed) flags |= CREDUI.FLAGS.SHOW_SAVE_CHECK_BOX;
            }
            else
            {
                flags |= CREDUI.FLAGS.DO_NOT_PERSIST;
            }
            if (_keepName) flags |= CREDUI.FLAGS.KEEP_USERNAME;
            return flags;
        }
 
        /// <summary>
        /// Returns the flags for modern dialog display options.
        /// </summary>
        private CREDUI.FLAGS_MODERN_UI GetFlagsModernUI()
        {
            // It is possible to improve using the flags but for most use cases, using
            // GENERIC is more than enough. But we need to enumerate the domain, etc.
            return CREDUI.FLAGS_MODERN_UI.CREDUIWIN_AUTHPACKAGE_ONLY;
        }
 
        /// <summary>
        /// Returns the info structure for dialog display settings.
        /// </summary>
        /// <param name="owner">
        /// The System.Windows.Forms.IWin32Window the dialog will display in front of.
        /// </param>
        private CREDUI.INFO GetInfo(IWin32Window owner)
        {
            var info = new CREDUI.INFO();
            if (owner != null) info.hwndParent = owner.Handle;
            info.pszCaptionText = Caption;
            info.pszMessageText = Message;
            info.cbSize = Marshal.SizeOf(info);
            return info;
        }
 
        private void SetCredentials(StringBuilder n, StringBuilder pw, int save)
        {
            UserName = n.ToString();
            Password = ConvertToSecureString(pw.ToString());
            _saveChecked = Convert.ToBoolean(save);
        }
 
        private void SetCredentialsModern(StringBuilder n, StringBuilder pw)
        {
            UserName = n.ToString();
            Password = ConvertToSecureString(pw.ToString());
        }
 
        /// <summary>
        /// Returns a DialogResult indicating the user action.
        /// </summary>
        /// <param name="owner">
        /// The System.Windows.Forms.IWin32Window the dialog will display in front of.
        /// </param>
        /// <remarks>
        /// Sets the username, password and SaveChecked accessors to the state of the dialog as
        /// it was dismissed by the user.
        /// </remarks>
        private DialogResult ShowDialog(IWin32Window owner)
        {
            // set the API call parameters
            var name = new StringBuilder(CREDUI.MAX_USERNAME_LENGTH);
            name.Append(UserName);
            var password = new StringBuilder(CREDUI.MAX_PASSWORD_LENGTH);
            var info = GetInfo(owner);
            // make the API call
            if (_useModernUI)
            {
                uint authPackage = 0;
                var flags = GetFlagsModernUI();
                var code = CREDUI.CredUIPromptForWindowsCredentials(ref info,
                    0,
                    ref authPackage,
                    IntPtr.Zero,
                    0,
                    out var outCredBuffer,
                    out var outCredSize,
                    ref _saveChecked,
                    flags);
 
                if (code == CREDUI.ReturnCodesModernUI.NO_ERROR)
                {
                    var domainBuf = new StringBuilder(100);
                    var maxUserName = CREDUI.MAX_USERNAME_LENGTH;
                    var maxDomain = CREDUI.MAX_DOMAIN_TARGET_LENGTH;
                    var maxPassword = CREDUI.MAX_PASSWORD_LENGTH;
                    if (CREDUI.CredUnPackAuthenticationBuffer(0, outCredBuffer, outCredSize, name, ref maxUserName,
                            domainBuf, ref maxDomain, password, ref maxPassword))
                    {
                        //clear the memory allocated by CredUIPromptForWindowsCredentials
                        CREDUI.CoTaskMemFree(outCredBuffer);
                        SetCredentialsModern(name, password);
                    }
                }
                return GetDialogResultModernUI(code);
            }
            else
            {
                var flags = GetFlags();
                var saveChecked = Convert.ToInt32(_saveChecked);
 
                var code = CREDUI.PromptForCredentials(
                    ref info,
                    _target,
                    IntPtr.Zero,
                    0,
                    name,
                    CREDUI.MAX_USERNAME_LENGTH,
                    password,
                    CREDUI.MAX_PASSWORD_LENGTH,
                    ref saveChecked,
                    flags
                );
                // set the accessors from the API call parameters
                SetCredentials(name, password, saveChecked);
                return GetDialogResult(code);
            }
        }
    }
}