microsoft / cppwinrt

C++/WinRT
MIT License
1.66k stars 238 forks source link

Different behavior of KeyCredentialManager when invoked via C++/WinRT vs. C# #999

Closed suzukieng closed 3 years ago

suzukieng commented 3 years ago

I am pretty sure this is not the right place to ask this question, but I don't know where else to go.

I work on an application that uses Windows Hello / KeyCredentialManager in some security-related code. Specifically, I use RequestSignAsync on a previously generated Credential: https://docs.microsoft.com/en-us/uwp/api/windows.security.credentials.keycredential.requestsignasync?view=winrt-20348 The code in question triggers the "Making Sure It's You" popup where the user enters Windows Hello credentials, e.g. presents a fingerprint, enters a PIN, etc:

Screenshot 2021-08-12 at 07 56 11

I first wrote a dummy prototype in C# which worked fine, the popup is displayed correctly. The real application is written actually in Java and uses JavaFX, so I ported the C# KeyCredentialManager code over to C++/WinRT and packaged it in a DLL, which I invoke using JNA from Java. This also works, except for one problem: the Windows Hello popup does not appear in the front, above the application window, instead it appears behind the application window, and only flashes in the task bar. Crucially, fingerprints are not detected, presumably because the window does not have input focus.

Screenshot 2021-08-12 at 07 52 21

What could trigger this kind of behavior? I already tried a couple of things, but nothing seems to work:

I also inspected the Window using Spy++, but since the popup is displayed by a separate process (Credential Manager UI Host.exe) and I have no handle on it anyway, and the KeyCredentialManager code does not expose anything UI-related.

Any pointers or ideas how to debug this would be greatly appreciated. My current assumption is that it's some kind of misuse of C++/WinRT on my side, but logic-wise everything works, and authentication works too, if the popup is manually brought into focus.

kennykerr commented 3 years ago

This usually happens when the parent window isn't set correctly, however I'm not familiar with how you would do this with this particular API. Might I suggest https://stackoverflow.com/.

suzukieng commented 3 years ago

Hi @kennykerr , thanks for taking the time to comment. There already exists a Stackoverflow issue (https://stackoverflow.com/questions/53941400/windows-security-popup-appears-behind-the-main-application-window), which I have now updated to include your hint. Unfortunately the API does not expose any handle on the window being displayed, and it seems to be presented by another process anyway. If you happen to know a Microsoft project where this question would be better posed, please let me know. Thanks!

sylveon commented 3 years ago

If UserConsentVerifier fits your need (only asking the user for authentication instead of actually encrypting/decrypting data with the KeyCredential), IUserConsentVerifierInterop allows to specify an HWND, so the dialog will be owned to your app and take foreground correctly. I couldn't find such an interop interface for KeyCredential, unfortunately.

It probably worked in your C# code because it was UWP code, and here the CoreWindow association is implicitly done for you.

suzukieng commented 3 years ago

Hi @sylveon, thanks for chiming in. UserConsentVerifier is unfortunately not enough, my security model depends on signing a message using the KeyCredential's RSA key pair. If I understand your comment correctly (sorry, as I'm sure you've noticed already I am not a Windows expert), if I could adapt my security model to only use UserConsentVerifier, I could drop down to a lower-level API which would allow me to pass the handle of my Java application window and the popup would (probably) be rendered correctly. Or is there a way to do this CoreWindow association you speak of myself, in the C++/WinRT DLL, before any calls to KeyCredentialManager happen? Or does the app ultimately have to be a real UWP app to have this working correctly? Sorry if that's a dumb question.

Update: interestingly enough, if I call UserConsentVerifier::RequestVerificationAsync(msg) from my app at the moment, no popup appears at all... the app just hangs.

sylveon commented 3 years ago

If I understand your comment correctly (sorry, as I'm sure you've noticed already I am not a Windows expert), if I could adapt my security model to only use UserConsentVerifier, I could drop down to a lower-level API which would allow me to pass the handle of my Java application window and the popup would (probably) be rendered correctly.

Right. It's how the Bitwarden desktop app operates, for example.

Or is there a way to do this CoreWindow association you speak of myself, in the C++/WinRT DLL, before any calls to KeyCredentialManager happen? Or does the app ultimately have to be a real UWP app to have this working correctly?

Unfortunately no, CoreWindow is purely a UWP construct, it's not present in Win32.

Update: interestingly enough, if I call UserConsentVerifier::RequestVerificationAsync(msg) from my app at the moment, no popup appears at all... the app just hangs.

I'm guessing you might be blocking here (e.g. via doing RequestVerificationAsync(msg).get()) when you shouldn't. That hangs the app because .get() blocks the thread while the API needs a message loop running to work.


Another option, would be not using WinRT for this entirely and instead using the native Windows Data Protection APIs (which the WinRT API effectively wraps, AFAIK). It's a bit more involved, as it's a C API, but it does allow you to pass an HWND (via the pPromptStruct parameter of CryptProtectData and CryptUnprotectData)

suzukieng commented 3 years ago

@sylveon Thanks again for taking the time to respond in such detail. I tried not blocking on UserConsentVerifier::RequestVerificationAsync(msg) and indeed the popup appears correctly now, in front of the window. I didn't even have to use the IUserConsentVerifierInterop interface, I was able to just use the C++/WinRT generated UserConsentVerifier class. So I assume something clever happens behind the scenes (like that generated class calling the Interop interface and correctly guessing the HWND).

It seems I missed a crucial paragraph in the documentation: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-supported-api#methods-that-use-the-request-naming-pattern

Quote:

Methods that follow the Request naming pattern, such as AppCapability.RequestAccessAsync and StoreContext.RequestPurchaseAsync, are not supported in desktop apps. Internally, these methods use the Windows.UI.Popups class. This class requires that the thread have a CoreWindow object, which isn't supported in desktop apps.

According to the docs RequestVerificationAsync would fall under that umbrella and shouldn't work either, but somehow it still does? I don't really understand it. Especially since accessing the supposedly unsupported API KeyCredentialManager.RequestSignAsync(...) does not fail completely, but only partially... all the crypto operations work as expected, it's just the display of the popup that is off. I would kind of expect C++/WinRT to not generate the method at all if it is not supported, or throw an error. But I guess that it out of scope.

Thank you for the pointers to Bitwarden and DPAPI. I am aware of the latter, but my goal is to use Windows Hello's biometric authentication (fingerprint or face recognition) features to securely store a user secret. There is a clever way of doing this using the RSA keypair provided by the KeyCredentialManager (see https://stackoverflow.com/a/54305859/1370154 if you're interested in the details), which provides more security than a simple check "is user here?" using UserConsentVerifier. Bitwarden uses UserConsentVerifier, probably to unlock already decrypted secrets: https://github.com/bitwarden/jslib/blob/master/electron/src/biometric.windows.main.ts

What I was really looking for in the beginning is something akin to the macOS keychain, where an item can be marked as "protected by biometric authentication" (reading the item triggers Touch ID, and the item is only release if the biometric check succeeds).

I will probably end up using UserConsentVerifier, followed by a load from the Windows Credential Manager (CredRead), but this method has known security issues: https://clement.notin.org/blog/2019/12/17/When-Windows-Hello-fails-at-securely-authenticating-users-and-protecting-credentials/#example-2

(sorry, that was the background of the question, it is of course unrelated to C++/WinRT)

sylveon commented 3 years ago

So I assume something clever happens behind the scenes

Yeah, in some update to Windows they made this implicitly use the last active window when called from desktop (see https://github.com/bitwarden/desktop/issues/641), but if your app uses multiple windows I still recommend using the interop interfaces to avoid it being mistakenly assigned the wrong window.

I would kind of expect C++/WinRT to not generate the method at all if it is not supported, or throw an error.

Not supported APIs generally throw an exception mentioning that no CoreWindow exists for the current thread, or that no package identity is present. C++/WinRT itself just generates every type because for some APIs, support depends on runtime conditions (e.g. package identity). And it's possible to "get" a CoreWindow in a Win32 app too, when using XAML Islands (although it has a bunch of pitfalls). So this API seems to actually support desktop, it just doesn't have an interop API (maybe something to ask in https://github.com/microsoft/WindowsAppSDK).

I am aware of the latter, but my goal is to use Windows Hello's biometric authentication (fingerprint or face recognition) features to securely store a user secret.

If you tell CryptUnprotectData to use a prompt, it will also ask the user for authentication via their account password or Windows Hello credentials (PIN, fingerprint or face recognition) and won't unlock the secret until the prompt succeeds.

What I was really looking for in the beginning is something akin to the macOS keychain, where an item can be marked as "protected by biometric authentication" (reading the item triggers Touch ID, and the item is only release if the biometric check succeeds).

I'm not sure if there's a way to require biometric authentication besides checking in your app itself if biometric hardware is available, but considering many Windows PCs ship with no biometric hardware, I would personally recommend not to require biometric auth. KeyCredentialManager also doesn't have a way to require biometric info either, it will succeed on PCs without biometric hardware (such as mine) if the user has set up a Windows Hello PIN.

Bitwarden uses UserConsentVerifier, probably to unlock already decrypted secrets

Those secrets are stored in the Windows Credential Store and retrieved upon prompt success, so are still protected by the user's credentials in the end. The prompt is mostly allow you to not have to remember your complex master password.

suzukieng commented 3 years ago

Thanks again @sylveon. You have been very helpful. Let me know if there is some way I can send you a coffee. I will probably go the Bitwarden route, and use the UserConsentVerifier check as a gate for loading the secret from Windows Credential Store.

After further trial & error I ran into problems using the C++/WinRT UserConsentVerifier::RequestVerificationAsync() method, my app has multiple windows and apparently the underlying implementation does not always choose the right one. I will try to use UserConsentVerifier::CheckAvailability() from the C++/WinRT class (that method is not part of the interop interface), and then try to invoke the IUserConsentVerifierInterop COM interface to do the actual check, passing along the HWND that I can hopefully somehow extract from JavaFX using Reflection.

Oh boy. :-)

suzukieng commented 3 years ago

@sylveon Just FYI, i managed to use the UserConsentVerifier from the Win32 app via Interop API, by passing the HWND of the JavaFX window.

I later figured out that storing the secret in Windows Credential Manager (and gating the load with a prior check via UserConsentVerifier::RequestVerificationAsync) has a limitation that is unacceptable for my scenario: any other application running on the same machine under the same user will be able to read it. I was under the impression that Windows Credential Manager performs some isolation of stored credentials between apps, but unfortunately not.

I did investigate DPAPI as an alternative, but there the problem is just moved one layer up, where to store the encrypted blob... I guess this will typically be stored in a file in AppData\Local and the additional entropy provided via hard-coded/derived constants.

PasswordVault, according to its documentation (https://docs.microsoft.com/en-us/uwp/api/windows.security.credentials.passwordvault?view=winrt-20348) would have this property but it's only accessible to UWP apps.

Represents a Credential Locker of credentials. The contents of the locker are specific to the app or service. Apps and services don't have access to credentials associated with other apps or services.

AdamBraden commented 3 years ago

@suzukieng - I suspect the PasswordVault apis work from a win32 app, but they require Identity. Did you try packaging your app with MSIX via the .wapproj?

suzukieng commented 3 years ago

@AdamBraden No, I haven't, but I will definitely explore that. Your comment about identity led me to this blog post (https://blogs.windows.com/windowsdeveloper/2019/10/29/identity-registration-and-activation-of-non-packaged-win32-apps/). I will explore what this would mean for our application, which is currently packaged using a custom InnoSetup installer and also has auto-updating capabilities that rely on that custom installer. Thanks for the pointer.

tim-weis commented 3 years ago

I was under the impression that Windows Credential Manager performs some isolation of stored credentials between apps, but unfortunately not.

The documentation surely suggests that:

The contents of the locker are specific to the app or service. Apps and services don't have access to credentials associated with other apps or services.

I can only speculate that this part of the documentation was authored at a time when this type was exclusive to the UWP, and never got updated as this requirement got dropped. In my experience, the isolation is provided based off of the user account under which the process executes. In the UWP where each package gets its very own account assigned to it, this all worked out as advertised.

This, unfortunately, isn't true when consuming the PasswordVault type from a Desktop application. In this setup, the 'protected' data is out in the open, accessible to anyone else running under that same user account.

I haven't investigated how (or whether at all) package identity factors into this. If you make any discoveries here I'd be interested to hear about those, too.

AdamBraden commented 3 years ago

@suzukieng - Sparse packages have limited tooling, so I recommend building a sample app that is packaged using the WapProj to validate the scenario. See: https://docs.microsoft.com/en-us/windows/msix/package/packaging-uwp-apps for more info on using the Windows Application Packaging Project.

@tim-weis - I'm not an expert in these apis, but deploying an app via MSIX is supported for UWP or Desktop apps. When deployed via MSIX the app will have a trusted and clean install location.

Let me check with the api owners to confirm if there are differences with a packaged Win32/Desktop app, and get back to this.

droidmonkey commented 2 years ago

Sorry to resurrect this, but I am not sure why this was closed. Using the KeyCredentialManager per the examples does not produce the desired behavior of showing the Windows Hello prompt on top of the window. Interesting enough, I find that when I am running the application through Visual Studio, the popup appears properly on top of the application. When the application is run outside of Visual Studio, it does not appear correctly.

Can the devs please reopen this and investigate this behavior?

suzukieng commented 2 years ago

Hey @AdamBraden ,

Let me check with the api owners to confirm if there are differences with a packaged Win32/Desktop app, and get back to this.

Any news regarding this topic? Still very interested in using PasswordVault from a Win32/Desktop app but not as long as the data is out in the open.

kennykerr commented 2 years ago

This repo's issues are specifically for reporting issues with cppwinrt itself. This is an API specific question and as such your best bet is to go through channels that will get more eyeballs on the question. If stack overflow doesn't help, you can open an issue here:

https://docs.microsoft.com/en-us/answers/topics/windows-api.html

@riverar reminded me that this will ensure we have our support team pick up the case.

For what it's worth, I wrote a similar example for Rust developers here:

https://github.com/microsoft/windows-rs/blob/master/crates/samples/consent/src/main.rs

YexuanXiao commented 6 months ago

@suzukieng - Sparse packages have limited tooling, so I recommend building a sample app that is packaged using the WapProj to validate the scenario. See: https://docs.microsoft.com/en-us/windows/msix/package/packaging-uwp-apps for more info on using the Windows Application Packaging Project.

@tim-weis - I'm not an expert in these apis, but deploying an app via MSIX is supported for UWP or Desktop apps. When deployed via MSIX the app will have a trusted and clean install location.

Let me check with the api owners to confirm if there are differences with a packaged Win32/Desktop app, and get back to this.

Any progress on this? Things got worse in Build 26085; in addition to not showing up at the top, it couldn’t even successfully complete the validation in a non-VS debugger environment.

APshenkin commented 3 days ago

I know that this topic might be quite old already, but want to share some findings. They are mostly copy of my Q&A post to microsoft https://learn.microsoft.com/en-us/answers/questions/2100684/undocumented-usage-of-ncrypt-api-for-microsoft-pas

During couple of weeks I try to integrate Windows Hello inside the app. The app is written in golang, so the app is not UWP based.

The Documentation says, that proper way to integrate with Windows Hello is via Key Credential Manager this API is a part of UWP APIs, however this API is available for Windows Runtime API.

However this approach has few disadvantages:

  1. As we are building non-UWP app, there is no CoreWindow association. That actually don't allow window with biometric prompt to be tied to application. This creates additional icon in taskbar, as well as biometric prompt is not focused correctly with the app. I was able to find mention of this error here, but no good solution was provided https://github.com/microsoft/cppwinrt/issues/999
  2. There is no option to set UI Context message in biometric prompt. All the time the message is the same: For security, an application needs to verify your identity. But we would like to be able to set different message depends of application context, e.g. MyAwesomeApp wants to do something. Please confirm operation . But there is lack of this functionality in KeyCredentialManager
  3. You can only sign using created credential, however in my case I wanted encrypt some data as well and secure this with biometric check

During exploring options how to mitigate this, I find out, that actually key credential created via KeyCredentialManager is stored in Microsoft Passport Key Storage Provider. This can easily been checked after creating an identity using certutil

certutil -csp "Microsoft Passport Key Storage Provider" -user -key

This brought me idea, that we can use nCrypt API to create these credentials and do operations with them. Especially, what is useful, is that nCrypt provides solution for both problems by NCRYPT_USE_CONTEXT_PROPERTY and NCRYPT_WINDOW_HANDLE_PROPERTY

After some experiments I found that it's possible to create keys and do operations on them, but created keys don't trigger biometric prompt. Searching the way how to manage this, I found that this interesting plugin for KeePass mitigated this by using undocumented property NgcCacheType. The usage is just simple via NCryptSetProperty here

So after adding it in my code, everything started working as expected. As well as all problems were solved.

I don't know how original author found this. Therefore I asked this question here: https://github.com/sirAndros/KeePassWinHello/issues/112

Hope someone from Microsoft will be able to answer these questions:

  1. Is it true, that KeyCredentialManager is just a wrapper on top of nCrypt API in case of identities?
  2. Why NgcCacheType property is not documented anywhere and information about it is only that this string exists in cryptngc.dll
  3. Is it legit to use nCrypt with Microsoft Passport Key Storage Provider with NgcCacheType option? Will this option continue to exists? If no, how to mitigate issues described above?
  4. And finally, why all of this is not documented :)

Thank in advance, Andrey