Skip to content

Fix WINBIO_E_INVALID_TICKET (0x80098044) on face unlock: enforce foreground on the Hello prompt#159

Open
MrSco wants to merge 1 commit into
sirAndros:masterfrom
MrSco:fix/winbio-invalid-ticket-foreground
Open

Fix WINBIO_E_INVALID_TICKET (0x80098044) on face unlock: enforce foreground on the Hello prompt#159
MrSco wants to merge 1 commit into
sirAndros:masterfrom
MrSco:fix/winbio-invalid-ticket-foreground

Conversation

@MrSco

@MrSco MrSco commented Jul 11, 2026

Copy link
Copy Markdown

The bug

Face unlock fails with NCryptDecrypt0x80098044 (WINBIO_E_INVALID_TICKET) on the first prompt of a fresh KeePass launch, while PIN always works — reported in #113, #118, #120, #128, #130, #132, #137, #138, #139.

Root cause

It's a foreground race, not a stale ticket, key corruption, or Windows-session state. The Windows Hello credential dialog ("Windows Security", class Credential Dialog Xaml Host) is hosted in another process. During KeePass startup the dialog can lose the foreground to KeePass's own appearing windows, and Windows rejects a biometric gesture ticket that completes against a non-foreground dialog. This is the same failure mode Bitwarden confirmed in their desktop app ("Windows Hello fails if the prompt is not in the foreground").

The evidence, gathered on a machine reproducing this 100% of the time (Windows 11 Pro 25H2, no Enhanced Sign-in Security):

  • Fresh KeePass open → face fails, PIN works. Same process, lock and reopen the database → face works. Reboots and sign-out/in change nothing — it is per-first-prompt-of-launch, not per-session.
  • A standalone console harness (tools/HelloTicketProbe, included in this PR) replaying the plugin's exact NGC/NCrypt call sequence against the same persistent key never fails — a console app the user just interacted with is always foreground.
  • PIN "working" is the tell: typing a PIN takes seconds, by which time window activation has settled. Face auto-scans instantly, inside the race window.

This also explains why plain retry loops (the approach in #158) don't fix it: each retried prompt hits the same unsettled foreground state. The missing ingredient is foreground enforcement, which this repo already had — WinHelloProviderForegroundDecorator — but it only activates when KeePass runs elevated, and only once per unlock rather than per prompt attempt.

The fix

  • CredentialDialogForegroundEnforcer (in WinHelloProvider): around every NCryptDecrypt prompt, grant foreground rights (AllowSetForegroundWindow(ASFW_ANY)) and keep pushing the credential dialog to the foreground (~2 s, 100 ms cadence, stops as soon as the dialog is foreground) — the same technique the elevated-only decorator used, now applied to every prompt regardless of elevation.
  • On WINBIO_E_INVALID_TICKET: bring the parent window to the front and immediately re-prompt (no artificial delay — the prompt is user-paced), with the retry also running under the enforcer.
  • If all retries fail, throw a presentable AuthProviderInvalidTicketException suggesting the PIN option instead of surfacing a raw 0x80098044.
  • Repairs KeyManager.cs (net4.0-compatible Interlocked.CompareExchange instead of Volatile.Read) and adds the HelloTicketProbe diagnostic harness used during the investigation.

Testing

On hardware reproducing the bug on every fresh launch: with this change, face unlock succeeds on the first prompt of a fresh KeePass start. Subsequent unlocks unaffected. PIN path unaffected.

Closes #113, closes #118, closes #120, closes #128, closes #130, closes #132, closes #137, closes #138, closes #139.

🤖 Generated with Claude Code

…NVALID_TICKET

The long-standing 0x80098044 failure on face unlock (sirAndros#113, sirAndros#118, sirAndros#120,
sirAndros#128, sirAndros#130, sirAndros#132, sirAndros#137-sirAndros#139) turns out to be a foreground race on the
first Hello prompt of a fresh KeePass launch: the credential dialog
("Windows Security", hosted in another process) can lose the foreground
while KeePass is still starting up, and a biometric gesture that
completes against a non-foreground dialog yields a rejected ticket.
PIN was never affected only because typing it takes long enough for the
window state to settle - which also explains why every unlock after the
first one works with face.

The repo already had the cure (WinHelloProviderForegroundDecorator) but
only applied it when KeePass ran elevated, and only once per unlock, so
plain retries kept failing. Now every decrypt prompt runs under a
CredentialDialogForegroundEnforcer that grants foreground rights and
keeps pushing the credential dialog to the foreground while it settles;
on WINBIO_E_INVALID_TICKET the parent window is brought to the front
and the prompt is retried immediately under the same enforcement. If
all retries fail, a presentable AuthProviderInvalidTicketException
suggests the PIN fallback instead of surfacing a raw error code.

Also repairs a truncated KeyManager.cs, keeps net4.0 compatibility
(Interlocked.CompareExchange instead of Volatile.Read), and adds
tools/HelloTicketProbe, a standalone console harness that replays the
plugin's exact NGC/NCrypt calls for diagnosing Hello ticket issues
(note: a console app is always foreground, so it cannot reproduce this
particular race).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@MrSco
MrSco marked this pull request as ready for review July 11, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment