Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CredUIPromptForWindowsCredentials crashes in unpackaged app #4340

Open
Balkoth opened this issue Apr 15, 2024 · 28 comments
Open

CredUIPromptForWindowsCredentials crashes in unpackaged app #4340

Balkoth opened this issue Apr 15, 2024 · 28 comments

Comments

@Balkoth
Copy link

Balkoth commented Apr 15, 2024

Describe the bug

Please see the attached solution, there are three projects, one packaged WASDK, one unpackaged WASDK and one WinForms project. All three projects use the same CredentialService file/class to display the credential ui. The unpackaged app crashes here:

image

Not Flagged		29672	1	Main Thread	Main Thread	UnpackagedWASDK.dll!TestCredPicker.CredentialService.NativeMethods.CredUIPromptForWindowsCredentials
Not Flagged		60556	4	Worker Thread	.NET Counter Poller	System.Private.CoreLib.dll!System.Threading.WaitHandle.WaitOneNoCheck
Not Flagged		31620	5	Worker Thread	.NET TP Worker	System.Private.CoreLib.dll!Interop.Kernel32.GetQueuedCompletionStatus
Not Flagged		18440	6	Worker Thread	.NET TP Gate	System.Private.CoreLib.dll!System.Threading.WaitHandle.WaitOneNoCheck
Not Flagged		31444	7	Worker Thread	.NET TP Worker	System.Private.CoreLib.dll!Interop.Kernel32.GetQueuedCompletionStatus
Not Flagged		62060	8	Worker Thread	.NET ThreadPool IO	System.Private.CoreLib.dll!System.Threading.PortableThreadPool.IOCompletionPoller.Poll
Not Flagged		67480	9	Worker Thread	.NET TP Worker	System.Private.CoreLib.dll!Interop.Kernel32.GetQueuedCompletionStatus
Not Flagged		39756	10	Worker Thread	.NET TP Worker	System.Private.CoreLib.dll!Interop.Kernel32.GetQueuedCompletionStatus
Not Flagged	>	57496	0	Worker Thread	<No Name>	UnpackagedWASDK.dll!UnpackagedWASDK.App.InitializeComponent.AnonymousMethod__4_2

UnpackagedWASDK.dll!UnpackagedWASDK.App.InitializeComponent.AnonymousMethod__4_2(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) Line 75	C#
Microsoft.WinUI.dll!WinRT._EventSource_global__Microsoft_UI_Xaml_UnhandledExceptionEventHandler.EventState.GetEventInvoke.AnonymousMethod__1_0(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)	Unknown
Microsoft.WinUI.dll!ABI.Microsoft.UI.Xaml.UnhandledExceptionEventHandler.Do_Abi_Invoke(nint thisPtr, nint sender, nint e)	Unknown

Steps to reproduce the bug

  • See the attached sample app: TestCredPicker.zip
  • Should display the credential dialog for the current logged on user:
    image

Expected behavior

I would expect that either all projects crash, because there may be something wrong with my PInvoke code, or none. But as both packaged and WinForms project run without problems, i suspect there is something wrong with the unpackaged nature of the project.

Screenshots

No response

NuGet package version

Windows App SDK 1.5.2: 1.5.240404000

Packaging type

Unpackaged

Windows version

Windows 10 version 22H2 (19045, 2022 Update)

IDE

Visual Studio 2022

Additional context

I don't know what version of WASDK introduced this problem, because at some point in time this code was developed and working with WASD (1.2.221116.1).

@Balkoth
Copy link
Author

Balkoth commented Apr 15, 2024

There is a working WinForms project included which does not use WinUI or WASDK.

@DarranRowe
Copy link

DarranRowe commented Apr 15, 2024

This seems to work without issue on Win11.

Screenshot 2024-04-15 150946

@Balkoth
Copy link
Author

Balkoth commented Apr 15, 2024

I guess the other projects work too on your system using Win11?
Can anyone confirm that Win10 is the culprit?

@DarranRowe
Copy link

I guess the other projects work too on your system using Win11?

Yes.

@DarranRowe
Copy link

DarranRowe commented Apr 16, 2024

Thinking about it for a little, have you tried referencing the Windows App SDK in a WinForms or WPF application and seeing if you have the same problem?
Also, you have the Windows App SDK set as self contained in the unpackaged project, do you have different results if it is framework dependent? I ask this because that is a difference between the packaged and unpackaged projects.

@Balkoth
Copy link
Author

Balkoth commented Apr 16, 2024

I did check with true and false for WindowsAppSDKSelfContained but that did not make a difference.

@Balkoth
Copy link
Author

Balkoth commented Apr 16, 2024

What do you mean by referencing? Addin a ProjectReference to WASDK and using some random component? Isn't there a whole lot of stuff going on behind the scenes which the WASDK template sets up?

@DarranRowe
Copy link

I mean simply adding the NuGet package to a project and setting WindowsPackageType to None and maybe setting the Windows App SDK self contained setting too.
The stuff that goes on in the templates is irrelevant, this is all about testing whether the presence of the Windows App SDK in the project or process is enough to cause problems.

@Balkoth
Copy link
Author

Balkoth commented Apr 17, 2024

I tried your suggestion, even getting Microsoft.Windows.SDK.NET.dll loaded into the process like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows10.0.20348.0</TargetFramework>
    <TargetPlatformMinVersion>10.0.19044.0</TargetPlatformMinVersion>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <WindowsPackageType>None</WindowsPackageType>
    <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
    <UseRidGraph>true</UseRidGraph>
    <Platforms>x64</Platforms>
    <RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240404000" />
  </ItemGroup>

</Project>
public Form1()
{
  InitializeComponent();

  try
  {
    Microsoft.UI.Xaml.Window window = new Microsoft.UI.Xaml.Window();
  }
  catch (Exception)
  {
  }

  CredentialService credentialService = new CredentialService();
  credentialService.PromptForCurrentCredentials(this.Handle, "Credentials", "Please enter");
}

"Unfortunately" this does not crash on my machine.

@ghost1372
Copy link
Contributor

ghost1372 commented Apr 18, 2024

Hi @Balkoth
your sample its working on Win11.
It looks like you are using p/invoke to open the dialog. You can use P/Invoke for Windows Form and for WinUI 3, you can use Windows.Security.Credentials which is exist in winui.
this is my Class which is using Windows.Security.Credentials and its working in WinUI 3 Packaged/UnPackaged

using Windows.Security.Credentials.UI;
using Windows.Security.Credentials;

namespace WinUICommunity;
public static class CredentialHelper
{
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern bool GetComputerName(StringBuilder lpBuffer, ref uint nSize);

    private static async Task<CredentialPickerResults> PickCredentialBase(string caption, string message, CredentialSaveOption credentialSaveOption, AuthenticationProtocol authenticationProtocol, bool alwaysDisplayDialog)
    {
        uint nSize = 260;
        StringBuilder sbComputerName = new StringBuilder((int)nSize);
        GetComputerName(sbComputerName, ref nSize);
        var options = new CredentialPickerOptions()
        {
            TargetName = sbComputerName.ToString(),
            Caption = caption,
            Message = message,
            CredentialSaveOption = credentialSaveOption,
            AuthenticationProtocol = authenticationProtocol,
            AlwaysDisplayDialog = alwaysDisplayDialog
        };
        return await CredentialPicker.PickAsync(options);
    }
    public static async Task<CredentialPickerResults> PickCredential(string caption, string message, CredentialSaveOption credentialSaveOption, AuthenticationProtocol authenticationProtocol, bool alwaysDisplayDialog)
    {
        return await PickCredentialBase(caption, message, credentialSaveOption, authenticationProtocol, alwaysDisplayDialog);
    }
    public static async Task<CredentialPickerResults> PickCredential(string caption, string message, CredentialSaveOption credentialSaveOption, AuthenticationProtocol authenticationProtocol)
    {
        return await PickCredentialBase(caption, message, credentialSaveOption, authenticationProtocol, true);
    }
    public static async Task<CredentialPickerResults> PickCredential(string caption, string message, CredentialSaveOption credentialSaveOption)
    {
        return await PickCredentialBase(caption, message, credentialSaveOption, AuthenticationProtocol.Basic, true);
    }
    public static async Task<CredentialPickerResults> PickCredential(string caption, string message)
    {
        return await PickCredentialBase(caption, message, CredentialSaveOption.Selected, AuthenticationProtocol.Basic, true);
    }

    public static PasswordCredential GetPasswordCredential(string resource, string username)
    {
        if (string.IsNullOrEmpty(resource) || string.IsNullOrEmpty(username))
        {
            return null;
        }

        try
        {
            var vault = new PasswordVault();
            return vault.Retrieve(resource, username);
        }
        catch (Exception)
        {
        }

        return null;
    }
    public static void AddPasswordCredential(string resource, string username, string password)
    {
        if (string.IsNullOrEmpty(resource) || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
        {
            return;
        }
        var vault = new PasswordVault();
        var credential = new PasswordCredential(resource, username, password);
        vault.Add(credential);
    }
    public static void RemovePasswordCredential(string resource, string username)
    {
        if (string.IsNullOrEmpty(resource) || string.IsNullOrEmpty(username))
        {
            return;
        }
        var vault = new PasswordVault();
        var credential = vault.Retrieve(resource, username);
        vault.Remove(credential);
    }
    public static async Task<bool> RequestWindowsPIN(string message)
    {
        // Check if Windows Hello is available
        if (await UserConsentVerifier.CheckAvailabilityAsync() != UserConsentVerifierAvailability.Available)
        {
            return false;
        }

        var consentResult = await UserConsentVerifier.RequestVerificationAsync(message);

        if (consentResult == UserConsentVerificationResult.Verified)
        {
            // Windows Hello PIN was successfully verified
            return true;
        }
        else
        {
            // Verification failed or was canceled
            return false;
        }
    }
}

@Balkoth
Copy link
Author

Balkoth commented Apr 18, 2024

Thanks but your sample has the one big problem why i have to use PInvoke: Your sample uses string for the password and therefore the password is in cleartext in memory until the app gets shutdown.

So i would prefer if someone from the team can check this on win10.

@DarranRowe
Copy link

"Unfortunately" this does not crash on my machine.

Yes, this is unfortunate. You ruled out it being a platform invoke problem because it should be crashing in the packaged WinUI 3 application and the WPF/WinForms application if it was a platform invoke issue. This unfortunate result actually provides the same logic for the Xaml runtime and the Windows App Runtime. If it was the Xaml runtime, then the packaged application version should be crashing, if it was the bootstrapper or registration free WinRT functionality then the WPF/WinForms application should be crashing.

One easy way to get a definitive answer would be to make an unpackaged C++ WinUI application and directly call this function or, to try to mimic the call portion of platform invoke, use runtime dynamic linking to call it (aka, LoadLibrary/GetProcAddress).

@castorix
Copy link

On my Windows 10 OS (22H2), it works for me with maxversiontested in the Manifest file, otherwise I get "Invalid pointer"

  <maxversiontested Id="10.0.19041.0"/>
  <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />

@Balkoth
Copy link
Author

Balkoth commented Apr 22, 2024

Starts working when i put <maxversiontested Id="10.0.19044.0"/> into the manifest, so who is to blame now?

@Balkoth
Copy link
Author

Balkoth commented Apr 24, 2024

Is anyone from the team looking into this or does the maxversiontested hack confirm this is a non-issue from WASDK?

@Balkoth
Copy link
Author

Balkoth commented Apr 30, 2024

This is incredibly frustrating, because this problem is affecting software in production.

@Balkoth
Copy link
Author

Balkoth commented May 8, 2024

Why does no one on the team care for this issue? What is missing here, that is holding you back, to investigate and acknowledge or deny that this is a WASDK issue?

@jonwis
Copy link
Member

jonwis commented May 8, 2024

Can you share either the output-log dump of CredUIPromptForWindowsCredentials, or enable first-chance exceptions to see what this API itself is returning? This is a Windows Platform API, not directly part of Windows App SDK.

@ghost1372 - the Windows.Security.Credentials API is part of Windows itself, not WinUI (or WinUI3), for clarity. See https://learn.microsoft.com/samples/microsoft/windows-universal-samples/credentialpicker/ as well.

@jonwis
Copy link
Member

jonwis commented May 9, 2024

FWIW, I put together a little WPF app using the CredentialPicker above, like this:

private async void Clickery_Click(object sender, RoutedEventArgs e)
{
    // Show a credential picker to acquire the user's domain credentials.
    var credPicker = new CredentialPickerOptions
    {
        Message = "Sign in",
        Caption = "Windows Credential Picker",
        TargetName = "Windows Authentication",
        AuthenticationProtocol = AuthenticationProtocol.Negotiate,
        CredentialSaveOption = CredentialSaveOption.Hidden,
        CallerSavesCredential = false,
    };

    var credResult = await CredentialPicker.PickAsync(credPicker);
    Clickery.Content = credResult.ErrorCode == 0 ? "Success" : "Failure";
}

... which ran and showed the Windows Hello prompt to get me to sign in. The resulting credResult.Credential contains a valid credential buffer.

I'll try with an unpackaged WASDK WinUI3 app, but my hunch is it'll work there as well. I've asked some folks internally to see if there's known problems with this Win32 API.

@jonwis
Copy link
Member

jonwis commented May 9, 2024

@Balkoth - I'm able to use the Windows credential picker API from WPF, WinUI3, unpackaged apps. See https://github.com/jonwis/jonwis.github.io/tree/user/jonwis/credpicker/code/CredPickerSampleWinUI3/CredPickerSampleWinUI3 for the details. Can you try it out and see? That'd let you avoid all the hand-rolled p/invoke and just use our WinRT API call support.

The one drawback is that while CredUIPrompt... lets you specify an HWND as the parent for the prompt, the CredentialPicker does not; for Win32 apps the UX gets parented to the desktop.

I've filed http://task.ms/50726835 for the team to add a Windows.UI.WindowId ParentWindow; property to the picker options object to fix that.

@Balkoth
Copy link
Author

Balkoth commented May 10, 2024

Thanks but your sample has the one big problem why i have to use PInvoke: Your sample uses string for the password and therefore the password is in cleartext in memory until the app gets shutdown.

This...

@DarranRowe
Copy link

DarranRowe commented May 12, 2024

Thanks but your sample has the one big problem why i have to use PInvoke: Your sample uses string for the password and therefore the password is in cleartext in memory until the app gets shutdown.

This...

Just to ask, in what way is the credential in clear text in memory until the application shuts down?
Why do you believe that the CredentialPickerResults actually stores any passwords in clear text and doesn't use secure means to clear the memory when you release all references to the object?
These are serious questions. If you have major evidence of this, the credential picker has to be reported as a major security flaw.

@Balkoth
Copy link
Author

Balkoth commented May 12, 2024

Because the password is retrieved in a string object and strings live forever.

@DarranRowe
Copy link

I would be extremely surprised if that is true.
Sure, C# strings are immutable, but that only means that the string object is read only while it is alive. This doesn't mean that a C# string will not be reclaimed by the GC.
But I am hardly an expert on C#, so I'll have to leave it to an expert to give a definitive comment on that aspect.

@Balkoth
Copy link
Author

Balkoth commented May 12, 2024

If i remember correctly the runtime itself decides whether to intern strings and therefore keep them forever. Either way, using string for the password is a non deterministic approach on if and when cofidential data gets unloaded from memory.

@jonwis
Copy link
Member

jonwis commented May 12, 2024

FWIW, the .NET folks have this statement on SecureString - https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md - specifically, that it provides minimal actual value, and should not be used in new designs. I've asked the Windows Security team to chime in on this thread for a definitive statement. See also https://devblogs.microsoft.com/oldnewthing/20130529-00/?p=4223 ... note that the point of SecureZeroMemory is to ensure the optimizer won't discard a call to memset on storage that's about to be freed.

However, under the covers, it turns out that CredentialPickerResults stores the user name and password values in an HSTRING. When the -Result object is destroyed (the last reference on it is released, including those from GC objects) it calls SecureZeroMemory on the content of its stored HSTRING instances. Even if the HSTRINGs have been addref'd and given to somebody else to hold on to.

Strictly speaking, however, this violates the HSTRING contract which is that - like .NET String types - they are immutable. Someone with this code (in C++/WinRT) would be very sad:

winrt::hstring pw = (co_await CredentialPicker::PickAsync(...)).CredentialPassword();
UsePasswordToAuth(pw); // pw.data() is now non-null but points to all zeroes.

While in C# the ABI output of get_CredentialPassword is deep-copied into a System.String, in C++ (CX and WinRT and WRL) the output HSTRING is just used directly.

Re the GC and string lifetime and zeroing - correct, the GC allocates memory for the String from whatever store it has. If that's the OS heap, blocks are only zero'd when they're handed out, not on a call to free.

@DarranRowe
Copy link

If i remember correctly the runtime itself decides whether to intern strings and therefore keep them forever.

The supplemental documentation for String.Intern paints a different picture. The runtime only automatically interns strings created from string literals, your code has to explicitly call Intern otherwise.
There is also the CompilationRelaxationsAttribute attribute that can be applied to the assembly to force string interning off.

@Balkoth
Copy link
Author

Balkoth commented May 13, 2024

Can you share either the output-log dump of CredUIPromptForWindowsCredentials, or enable first-chance exceptions to see what this API itself is returning? This is a Windows Platform API, not directly part of Windows App SDK.

The exception is ?caught? on this thread:
image

With this callstack:
image

While the main thread seems to wait:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants