Skip to content

Passkeys

Claude Fable 5.1 edited this page Oct 11, 2026 · 2 revisions

Passkeys

Passkey support is upstream KeePassXC functionality reached through browser integration. This page documents the save path, because that is the part that must not break.

Registration flow

sequenceDiagram
  participant B as Browser extension
  participant S as BrowserService
  participant D as Confirmation dialog
  participant E as Entry / KDBX

  B->>S: showPasskeysRegisterPrompt(publicKeyOptions, origin)
  S->>S: getCredentialCreationOptions
  S->>S: reject if credential excluded
  S->>D: BrowserPasskeysConfirmationDialog
  D-->>S: confirm (new entry or update existing)
  alt update an existing entry
    S->>E: addPasskeyToEntry
  else create a new entry
    S->>E: addPasskeyToGroup → addPasskeyToEntry
  end
  E-->>B: publicKeyCredential
Loading

The default group for new passkey entries is KeePassXC-Browser Passkeys.

What gets stored

BrowserService::addPasskeyToEntry() writes these entry attributes:

Attribute Protected Contents
KPEX_PASSKEY_USERNAME no Relying-party username
KPEX_PASSKEY_RELYING_PARTY no RP id, e.g. example.com
KPEX_PASSKEY_CREDENTIAL_ID yes Credential identifier
KPEX_PASSKEY_PRIVATE_KEY_PEM yes The private key
KPEX_PASSKEY_USER_HANDLE yes User handle
KPEX_PASSKEY_FLAG_BE no Backup eligible
KPEX_PASSKEY_FLAG_BS no Backup state

The entry also gains a Passkey tag and the browser passkey icon.

Important

The credential id, private key, and user handle must retain their protected attribute flags. Attribute protection applies the inner protected stream to those serialized XML values; it is distinct from the outer encryption of the KDBX payload. An unprotected attribute lacks that additional inner protection, but it is still inside the encrypted KDBX payload, not plaintext on disk. Plaintext XML exports have a separate exposure boundary and must be handled as sensitive data.

Overwrite protection

If the target entry already has a passkey, the save is gated behind a confirmation prompt and aborts unless the user picks Overwrite. A new entry never hits this path.

Test coverage

tests/TestPasskeys.cpp — 25 passed, 0 failed.

Beyond the WebAuthn primitives (attestation objects, EC and RSA key loading, flags, RP id validation), three tests cover saving specifically:

Test Asserts
testEntry hasPasskey() after registration
testPasskeyAttributesAreStoredAndProtected Every attribute value, and that the three secrets are protected
testPasskeySurvivesDatabaseRoundTrip A registered passkey written through KeePass2Writer and read back with KeePass2Reader keeps its values and its protection flags

The round-trip test is the one that matters: registration is only useful if the credential is still there after the database is closed and reopened.

cmake --build build --target testpasskeys
./build/tests/testpasskeys -o results.txt,txt

Import and export

PasskeyImporter / PasskeyExporter handle .passkey files, with PasskeyImportDialog letting the user pick the target entry and group. The importer is also reused during registration when the user chooses to attach a passkey to an existing entry.

Interface rewrite

The Material rewrite touches how these dialogs look, never what they store. addPasskeyToEntry and addPasskeyToGroup are untouched, and the tests above are the guard rail: if a restyle breaks the save path, testPasskeySurvivesDatabaseRoundTrip fails.

Clone this wiki locally