sources/Google.Solutions.IapDesktop.Application/Windows/Dialog/CredentialDialog.cs (364 lines of code) (raw):

// // Copyright 2022 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Util; using Google.Solutions.IapDesktop.Application.Theme; using Google.Solutions.IapDesktop.Core.ObjectModel; using Google.Solutions.Platform.Interop; using Microsoft.Win32.SafeHandles; using System; using System.ComponentModel; using System.Net; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; namespace Google.Solutions.IapDesktop.Application.Windows.Dialog { public interface ICredentialDialog { /// <summary> /// Prompt for Windows credential using the CredUI API. /// </summary> DialogResult PromptForWindowsCredentials( IWin32Window? owner, CredentialDialogParameters parameters, out bool save, out NetworkCredential? credential); /// <summary> /// Prompt for a username. There's no CredUI counterpart to this. /// </summary> DialogResult PromptForUsername( IWin32Window? owner, string caption, string message, out string? username); } public struct CredentialDialogParameters { /// <summary> /// Caption to show in dialog. /// </summary> public string Caption { get; set; } /// <summary> /// Message to show in dialog. /// </summary> public string Message { get; set; } /// <summary> /// Authentication package. /// </summary> public AuthenticationPackage Package { get; set; } /// <summary> /// Display checkbox to save credentials. /// </summary> public bool ShowSaveCheckbox { get; set; } /// <summary> /// Credential to pre-populate the dialog with. /// </summary> public NetworkCredential? InputCredential { get; set; } } public enum AuthenticationPackage { Ntlm, Kerberos, Negoriate, Any } public class CredentialDialog : ICredentialDialog { private readonly Service<ISystemDialogTheme> theme; public CredentialDialog(Service<ISystemDialogTheme> theme) { this.theme = theme.ExpectNotNull(nameof(theme)); } public DialogResult PromptForWindowsCredentials( IWin32Window? owner, CredentialDialogParameters parameters, out bool save, out NetworkCredential? credential) { var uiInfo = new NativeMethods.CREDUI_INFO() { cbSize = Marshal.SizeOf<NativeMethods.CREDUI_INFO>(), hwndParent = owner?.Handle ?? IntPtr.Zero, pszCaptionText = parameters.Caption, pszMessageText = parameters.Message }; using (var packedInCredential = new PackedCredential( parameters.InputCredential ?? new NetworkCredential())) { var packageId = LookupAuthenticationPackageId(parameters.Package); var saveCheckboxState = false; var flags = NativeMethods.CREDUIWIN_FLAGS.AUTHPACKAGE_ONLY; if (parameters.ShowSaveCheckbox) { flags |= NativeMethods.CREDUIWIN_FLAGS.CHECKBOX; } var error = NativeMethods.CredUIPromptForWindowsCredentials( ref uiInfo, 0, ref packageId, packedInCredential.Handle, packedInCredential.Size, out var outAuthBuffer, out var outAuthBufferSize, ref saveCheckboxState, flags); if (error == NativeMethods.ERROR_CANCELLED) { credential = null; save = false; return DialogResult.Cancel; } else if (error != NativeMethods.ERROR_NOERROR) { throw new Win32Exception(error); } using (var packedOutCredential = new PackedCredential( outAuthBuffer, outAuthBufferSize)) { credential = packedOutCredential.Unpack(); save = saveCheckboxState; return DialogResult.OK; } } } internal static uint LookupAuthenticationPackageId(AuthenticationPackage package) { if (package == AuthenticationPackage.Any) { return 0; } using (var lsa = Lsa.ConnectUntrusted()) { var packageName = package switch { AuthenticationPackage.Ntlm => Lsa.MSV1_0_PACKAGE_NAME, AuthenticationPackage.Kerberos => Lsa.MICROSOFT_KERBEROS_NAME_A, AuthenticationPackage.Negoriate => Lsa.NEGOSSP_NAME_A, _ => throw new ArgumentException(nameof(package)), }; return lsa.LookupAuthenticationPackage(packageName); } } public DialogResult PromptForUsername( IWin32Window? owner, string caption, string message, out string? username) { using (var dialog = new SystemInputDialog( new InputDialogParameters() { Title = "Security", Caption = caption, Message = message, Cue = "User name" })) { try { this.theme.Activate().ApplyTo(dialog); } catch (UnknownServiceException) { } var result = dialog.ShowDialog(owner); username = dialog.Value; return result; } } //--------------------------------------------------------------------- // P/Invoke. //--------------------------------------------------------------------- internal class PackedCredential : IDisposable { private readonly CoTaskMemAllocSafeHandle buffer; public IntPtr Handle => this.buffer.DangerousGetHandle(); public uint Size { get; } public PackedCredential( CoTaskMemAllocSafeHandle buffer, uint bufferSize) { this.buffer = buffer; this.Size = bufferSize; } public PackedCredential(NetworkCredential inputCredential) { var username = inputCredential.UserName; var password = inputCredential.Password; if (!string.IsNullOrEmpty(inputCredential.Domain) && !string.IsNullOrEmpty(username) && !username.Contains("\\") && !username.Contains("@")) { // // The input credential specifies a domain. // CredPackAuthenticationBuffer doesn't let us // pass that domain, so we need to // prepend it to the username in NetBIOS format. // // NB. If the username also contains a domain // (domain\user or user@domain), then we let // that take precedence. // username = $"{inputCredential.Domain}\\{username}"; } uint bufferSize = 0; if (!NativeMethods.CredPackAuthenticationBuffer( NativeMethods.CRED_PACK_PROTECTED_CREDENTIALS, username, password, IntPtr.Zero, ref bufferSize) && bufferSize == 0) { throw new Win32Exception(); } this.Size = bufferSize; this.buffer = CoTaskMemAllocSafeHandle.Alloc((int)this.Size); if (!NativeMethods.CredPackAuthenticationBuffer( NativeMethods.CRED_PACK_PROTECTED_CREDENTIALS, username, password, this.buffer.DangerousGetHandle(), ref bufferSize)) { this.buffer.Dispose(); throw new Win32Exception(); } } public NetworkCredential Unpack() { var usernameBuffer = new StringBuilder(256); var passwordBuffer = new StringBuilder(256); var domainBuffer = new StringBuilder(256); var usernameLength = usernameBuffer.Capacity; var passwordLength = passwordBuffer.Capacity; var domainLength = domainBuffer.Capacity; if (!NativeMethods.CredUnPackAuthenticationBuffer( NativeMethods.CRED_PACK_PROTECTED_CREDENTIALS, this.buffer, this.Size, usernameBuffer, ref usernameLength, domainBuffer, ref domainLength, passwordBuffer, ref passwordLength)) { throw new Win32Exception(); } return new NetworkCredential( usernameBuffer.ToString(), passwordBuffer.ToString(), domainBuffer.ToString()); } public void Dispose() { this.buffer.Dispose(); } } private static class NativeMethods { public const int ERROR_NOERROR = 0; public const int ERROR_CANCELLED = 1223; public const uint CRED_PACK_PROTECTED_CREDENTIALS = 0x1; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct CREDUI_INFO { public int cbSize; public IntPtr hwndParent; public string pszMessageText; public string pszCaptionText; public IntPtr hbmBanner; } [Flags] public enum CREDUIWIN_FLAGS { GENERIC = 0x1, CHECKBOX = 0x2, AUTHPACKAGE_ONLY = 0x10, IN_CRED_ONLY = 0x20, ENUMERATE_ADMINS = 0x100, ENUMERATE_CURRENT_USER = 0x200, SECURE_PROMPT = 0x1000, PACK_32_WOW = 0x10000000, } [DllImport("credui.dll", CharSet = CharSet.Unicode)] public static extern int CredUIPromptForWindowsCredentials( ref CREDUI_INFO uiInfo, int authError, ref uint authPackage, IntPtr inAuthBuffer, uint inAuthBufferSize, out CoTaskMemAllocSafeHandle outAuthBuffer, out uint outAuthBufferSize, ref bool save, CREDUIWIN_FLAGS flags); [DllImport("credui.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool CredPackAuthenticationBuffer( uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszUserName, [MarshalAs(UnmanagedType.LPWStr)] string pszPassword, IntPtr pPackedCredentials, ref uint pcbPackedCredentials); [DllImport("credui.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool CredUnPackAuthenticationBuffer( uint dwFlags, CoTaskMemAllocSafeHandle pAuthBuffer, uint cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword); } //--------------------------------------------------------------------- // Helper class for LSA API. //--------------------------------------------------------------------- internal sealed class Lsa : IDisposable { public const string MSV1_0_PACKAGE_NAME = "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0"; public const string MICROSOFT_KERBEROS_NAME_A = "Kerberos"; public const string NEGOSSP_NAME_A = "Negotiate"; private readonly LsaSafeHandle handle; private Lsa(LsaSafeHandle handle) { this.handle = handle; } public static Lsa ConnectUntrusted() { var status = NativeMethods.LsaConnectUntrusted(out var handle); if (status == 0 && handle != null) { return new Lsa(handle); } else { throw new Win32Exception( NativeMethods.LsaNtStatusToWinError(status)); } } public void Dispose() { this.handle.Dispose(); } public uint LookupAuthenticationPackage(string packageName) { using (var packageNameHandle = CoTaskMemAllocSafeHandle.Alloc(packageName)) { var nativePackageName = new NativeMethods.LSA_STRING { Buffer = packageNameHandle.DangerousGetHandle(), Length = (ushort)packageName.Length, MaximumLength = (ushort)packageName.Length }; var status = NativeMethods.LsaLookupAuthenticationPackage( this.handle, ref nativePackageName, out var package); if (status == 0) { return package; } else { throw new Win32Exception( NativeMethods.LsaNtStatusToWinError(status)); } } } private class NativeMethods { public struct LSA_STRING { public ushort Length; public ushort MaximumLength; public /*PCHAR*/ IntPtr Buffer; } [DllImport("secur32.dll", SetLastError = false)] public static extern uint LsaConnectUntrusted([Out] out LsaSafeHandle LsaHandle); [DllImport("secur32.dll", SetLastError = false)] public static extern uint LsaDeregisterLogonProcess([In] IntPtr LsaHandle); [DllImport("advapi32.dll", SetLastError = false)] public static extern int LsaNtStatusToWinError(uint status); [DllImport("secur32.dll", SetLastError = false)] public static extern uint LsaLookupAuthenticationPackage( [In] LsaSafeHandle LsaHandle, [In] ref LSA_STRING PackageName, [Out] out uint AuthenticationPackage); } private class LsaSafeHandle : SafeHandleZeroOrMinusOneIsInvalid { public LsaSafeHandle() : base(true) { } protected override bool ReleaseHandle() { return NativeMethods.LsaDeregisterLogonProcess(this.handle) == 0; } } } } }