Repository navigation
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.
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
The default group for new passkey entries is KeePassXC-Browser Passkeys.
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.
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.
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,txtPasskeyImporter / 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.
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.