diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 0ee054f6e..373358fb8 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -9061,6 +9061,10 @@ This option is deprecated, use --set-key-file instead.
+
+
+
+
QtIOCompressor
diff --git a/src/browser/BrowserPasskeys.cpp b/src/browser/BrowserPasskeys.cpp
index 2b6d95f65..1f9804487 100644
--- a/src/browser/BrowserPasskeys.cpp
+++ b/src/browser/BrowserPasskeys.cpp
@@ -58,17 +58,6 @@ const QString BrowserPasskeys::REQUIREMENT_REQUIRED = QStringLiteral("required")
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("direct");
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_NONE = QStringLiteral("none");
-const QString BrowserPasskeys::KPEX_PASSKEY_USERNAME = QStringLiteral("KPEX_PASSKEY_USERNAME");
-const QString BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID = QStringLiteral("KPEX_PASSKEY_CREDENTIAL_ID");
-
-// For compatibility with StrongBox
-const QString BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
-const QString BrowserPasskeys::KPXC_PASSKEY_USERNAME = QStringLiteral("KPXC_PASSKEY_USERNAME");
-
-const QString BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM = QStringLiteral("KPEX_PASSKEY_PRIVATE_KEY_PEM");
-const QString BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX_PASSKEY_RELYING_PARTY");
-const QString BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
-
BrowserPasskeys* BrowserPasskeys::instance()
{
return s_browserPasskeys;
diff --git a/src/browser/BrowserPasskeys.h b/src/browser/BrowserPasskeys.h
index 21a8f1e51..4955654c9 100644
--- a/src/browser/BrowserPasskeys.h
+++ b/src/browser/BrowserPasskeys.h
@@ -105,14 +105,6 @@ public:
static const QString PASSKEYS_ATTESTATION_DIRECT;
static const QString PASSKEYS_ATTESTATION_NONE;
- static const QString KPXC_PASSKEY_USERNAME;
- static const QString KPEX_PASSKEY_USERNAME;
- static const QString KPEX_PASSKEY_CREDENTIAL_ID;
- static const QString KPEX_PASSKEY_GENERATED_USER_ID;
- static const QString KPEX_PASSKEY_PRIVATE_KEY_PEM;
- static const QString KPEX_PASSKEY_RELYING_PARTY;
- static const QString KPEX_PASSKEY_USER_HANDLE;
-
private:
QByteArray buildAttestationObject(const QJsonObject& credentialCreationOptions,
const QString& extensions,
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
index d60a55109..38c812f94 100644
--- a/src/browser/BrowserService.cpp
+++ b/src/browser/BrowserService.cpp
@@ -24,6 +24,7 @@
#include "BrowserHost.h"
#include "BrowserMessageBuilder.h"
#include "BrowserSettings.h"
+#include "core/EntryAttributes.h"
#include "core/Tools.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
@@ -763,9 +764,9 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
}
- const auto privateKeyPem = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
+ const auto privateKeyPem = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM);
const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry);
- const auto userHandle = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
+ const auto userHandle = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
auto publicKeyCredential =
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
@@ -843,11 +844,11 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
entry->beginUpdate();
- entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
- entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
- entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
- entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
- entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USERNAME, username);
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId);
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
entry->addTag(tr("Passkey"));
entry->endUpdate();
@@ -1042,7 +1043,7 @@ QList BrowserService::searchEntries(const QSharedPointer& db,
#ifdef WITH_XC_BROWSER_PASSKEYS
// With Passkeys, check for the Relying Party instead of URL
- if (passkey && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) != siteUrl) {
+ if (passkey && entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY) != siteUrl) {
continue;
}
#endif
@@ -1384,7 +1385,7 @@ QList BrowserService::getPasskeyEntries(const QString& rpId, const Strin
{
QList entries;
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
- if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
+ if (entry->hasPasskey() && entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
entries << entry;
}
}
@@ -1399,8 +1400,8 @@ QList BrowserService::getPasskeyEntriesWithUserHandle(const QString& rpI
{
QList entries;
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
- if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId
- && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE) == userId) {
+ if (entry->hasPasskey() && entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY) == rpId
+ && entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE) == userId) {
entries << entry;
}
}
@@ -1425,7 +1426,7 @@ QList BrowserService::getPasskeyAllowedEntries(const QJsonObject& assert
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
if (allowedCredentials.contains(passkeyUtils()->getCredentialIdFromEntry(entry))
|| (allowedCredentials.isEmpty()
- && entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
+ && entry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_USER_HANDLE))) {
entries << entry;
}
}
diff --git a/src/browser/PasskeyUtils.cpp b/src/browser/PasskeyUtils.cpp
index 6ac448932..41fc94221 100644
--- a/src/browser/PasskeyUtils.cpp
+++ b/src/browser/PasskeyUtils.cpp
@@ -18,6 +18,7 @@
#include "PasskeyUtils.h"
#include "BrowserMessageBuilder.h"
#include "BrowserPasskeys.h"
+#include "core/EntryAttributes.h"
#include "core/Tools.h"
#include "gui/UrlTools.h"
@@ -389,9 +390,9 @@ QString PasskeyUtils::getCredentialIdFromEntry(const Entry* entry) const
return {};
}
- return entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
- ? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
- : entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID);
+ return entry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID)
+ ? entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID)
+ : entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID);
}
// For compatibility with StrongBox (and other possible clients in the future)
@@ -401,7 +402,7 @@ QString PasskeyUtils::getUsernameFromEntry(const Entry* entry) const
return {};
}
- return entry->attributes()->hasKey(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
- ? entry->attributes()->value(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
- : entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME);
+ return entry->attributes()->hasKey(EntryAttributes::KPXC_PASSKEY_USERNAME)
+ ? entry->attributes()->value(EntryAttributes::KPXC_PASSKEY_USERNAME)
+ : entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USERNAME);
}
diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp
index dc49bf56a..5c1e1c3a0 100644
--- a/src/core/EntryAttributes.cpp
+++ b/src/core/EntryAttributes.cpp
@@ -36,7 +36,20 @@ const QString EntryAttributes::SearchTextGroupName = "SearchText";
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
const QString EntryAttributes::AdditionalUrlAttribute = "KP2A_URL";
+
+// Passkey related attributes
const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY";
+const QString EntryAttributes::KPEX_PASSKEY_USERNAME = QStringLiteral("KPEX_PASSKEY_USERNAME");
+const QString EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID = QStringLiteral("KPEX_PASSKEY_CREDENTIAL_ID");
+const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM = QStringLiteral("KPEX_PASSKEY_PRIVATE_KEY_PEM");
+const QString EntryAttributes::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX_PASSKEY_RELYING_PARTY");
+const QString EntryAttributes::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
+const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START = QStringLiteral("-----BEGIN PRIVATE KEY-----");
+const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END = QStringLiteral("-----END PRIVATE KEY-----");
+
+// For compatibility with StrongBox
+const QString EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
+const QString EntryAttributes::KPXC_PASSKEY_USERNAME = QStringLiteral("KPXC_PASSKEY_USERNAME");
EntryAttributes::EntryAttributes(QObject* parent)
: ModifiableObject(parent)
diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h
index ee814b03d..d0767a4c1 100644
--- a/src/core/EntryAttributes.h
+++ b/src/core/EntryAttributes.h
@@ -64,7 +64,18 @@ public:
static const QStringList DefaultAttributes;
static const QString RememberCmdExecAttr;
static const QString AdditionalUrlAttribute;
+
static const QString PasskeyAttribute;
+ static const QString KPXC_PASSKEY_USERNAME;
+ static const QString KPEX_PASSKEY_USERNAME;
+ static const QString KPEX_PASSKEY_CREDENTIAL_ID;
+ static const QString KPEX_PASSKEY_GENERATED_USER_ID;
+ static const QString KPEX_PASSKEY_PRIVATE_KEY_PEM;
+ static const QString KPEX_PASSKEY_RELYING_PARTY;
+ static const QString KPEX_PASSKEY_USER_HANDLE;
+ static const QString KPEX_PASSKEY_PRIVATE_KEY_START;
+ static const QString KPEX_PASSKEY_PRIVATE_KEY_END;
+
static bool isDefaultAttribute(const QString& key);
static bool isPasskeyAttribute(const QString& key);
diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp
index 861169b7b..5f729aa77 100644
--- a/src/format/BitwardenReader.cpp
+++ b/src/format/BitwardenReader.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 KeePassXC Team
+ * Copyright (C) 2024 KeePassXC Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -81,6 +81,43 @@ namespace
entry->setTotp(Totp::parseSettings(totp));
}
+ // Parse passkey
+ if (loginMap.contains("fido2Credentials")) {
+ const auto fido2CredentialsMap = loginMap.value("fido2Credentials").toList();
+ for (const auto& fido2Credentials : fido2CredentialsMap) {
+ const auto passkey = fido2Credentials.toMap();
+
+ // Change from UUID to base64 byte array
+ const auto credentialIdValue = passkey.value("credentialId").toString();
+ if (!credentialIdValue.isEmpty()) {
+ const auto credentialUuid = Tools::uuidToHex(credentialIdValue);
+ const auto credentialIdArray = QByteArray::fromHex(credentialUuid.toUtf8());
+ const auto credentialId =
+ credentialIdArray.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
+ }
+
+ // Base64 needs to be changed from URL encoding back to normal, and the result as PEM string
+ const auto keyValue = passkey.value("keyValue").toString();
+ if (!keyValue.isEmpty()) {
+ const auto keyValueArray =
+ QByteArray::fromBase64(keyValue.toUtf8(), QByteArray::Base64UrlEncoding);
+ auto privateKey = keyValueArray.toBase64(QByteArray::Base64Encoding);
+ privateKey.insert(0, EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START.toUtf8());
+ privateKey.append(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END.toUtf8());
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
+ }
+
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USERNAME,
+ passkey.value("userName").toString());
+ entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY,
+ passkey.value("rpId").toString());
+ entry->attributes()->set(
+ EntryAttributes::KPEX_PASSKEY_USER_HANDLE, passkey.value("userHandle").toString(), true);
+ entry->addTag(QObject::tr("Passkey"));
+ }
+ }
+
// Set the entry url(s)
int i = 1;
for (const auto& urlObj : loginMap.value("uris").toList()) {
diff --git a/src/gui/passkeys/PasskeyExporter.cpp b/src/gui/passkeys/PasskeyExporter.cpp
index 315825b4b..4ff84e5f8 100644
--- a/src/gui/passkeys/PasskeyExporter.cpp
+++ b/src/gui/passkeys/PasskeyExporter.cpp
@@ -21,6 +21,7 @@
#include "browser/BrowserPasskeys.h"
#include "browser/PasskeyUtils.h"
#include "core/Entry.h"
+#include "core/EntryAttributes.h"
#include "core/Tools.h"
#include "gui/MessageBox.h"
#include
@@ -94,12 +95,12 @@ void PasskeyExporter::exportSelectedEntry(const Entry* entry, const QString& fol
}
QJsonObject passkeyObject;
- passkeyObject["relyingParty"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY);
+ passkeyObject["relyingParty"] = entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY);
passkeyObject["url"] = entry->url();
passkeyObject["username"] = passkeyUtils()->getUsernameFromEntry(entry);
passkeyObject["credentialId"] = passkeyUtils()->getCredentialIdFromEntry(entry);
- passkeyObject["userHandle"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
- passkeyObject["privateKey"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
+ passkeyObject["userHandle"] = entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
+ passkeyObject["privateKey"] = entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM);
QJsonDocument document(passkeyObject);
if (passkeyFile.write(document.toJson()) < 0) {
diff --git a/src/gui/passkeys/PasskeyImporter.cpp b/src/gui/passkeys/PasskeyImporter.cpp
index 12ba9875f..df4042069 100644
--- a/src/gui/passkeys/PasskeyImporter.cpp
+++ b/src/gui/passkeys/PasskeyImporter.cpp
@@ -76,8 +76,8 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer&
tr("Cannot import passkey"),
tr("Cannot import passkey file \"%1\".\nThe following data is missing:\n%2")
.arg(file.fileName(), missingKeys.join(", ")));
- } else if (!privateKey.startsWith("-----BEGIN PRIVATE KEY-----")
- || !privateKey.trimmed().endsWith("-----END PRIVATE KEY-----")) {
+ } else if (!privateKey.startsWith(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START)
+ || !privateKey.trimmed().endsWith(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END)) {
MessageBox::information(
m_parent,
tr("Cannot import passkey"),
diff --git a/src/gui/reports/ReportsWidgetPasskeys.cpp b/src/gui/reports/ReportsWidgetPasskeys.cpp
index adf56332d..831f4c721 100644
--- a/src/gui/reports/ReportsWidgetPasskeys.cpp
+++ b/src/gui/reports/ReportsWidgetPasskeys.cpp
@@ -21,6 +21,7 @@
#include "browser/BrowserPasskeys.h"
#include "browser/PasskeyUtils.h"
#include "core/AsyncTask.h"
+#include "core/EntryAttributes.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "gui/GuiTools.h"
@@ -75,7 +76,7 @@ PasskeyList::PasskeyList(const QSharedPointer& db)
}
for (auto entry : group->entries()) {
- if (entry->isRecycled() || !entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM)) {
+ if (entry->isRecycled() || !entry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM)) {
continue;
}
@@ -134,7 +135,7 @@ void ReportsWidgetPasskeys::addPasskeyRow(Group* group, Entry* entry)
row << new QStandardItem(Icons::entryIconPixmap(entry), title);
row << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/"));
row << new QStandardItem(passkeyUtils()->getUsernameFromEntry(entry));
- row << new QStandardItem(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY));
+ row << new QStandardItem(entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY));
row << new QStandardItem(urlList.join('\n'));
// Set tooltips
diff --git a/tests/TestImports.cpp b/tests/TestImports.cpp
index eb06599df..c34b9190f 100644
--- a/tests/TestImports.cpp
+++ b/tests/TestImports.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 KeePassXC Team
+ * Copyright (C) 2024 KeePassXC Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -282,3 +282,36 @@ void TestImports::testBitwardenEncrypted()
}
QVERIFY(db);
}
+
+void TestImports::testBitwardenPasskey()
+{
+ auto bitwardenPath =
+ QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_passkey_export.json"));
+
+ BitwardenReader reader;
+ auto db = reader.convert(bitwardenPath);
+ QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
+ QVERIFY(db);
+
+ // Confirm Login fields
+ auto entry = db->rootGroup()->findEntryByPath("/webauthn.io");
+ QVERIFY(entry);
+ QCOMPARE(entry->title(), QStringLiteral("webauthn.io"));
+ QCOMPARE(entry->username(), QStringLiteral("KPXC_BITWARDEN"));
+ QCOMPARE(entry->url(), QStringLiteral("https://webauthn.io/"));
+
+ // Confirm passkey attributes
+ auto attr = entry->attributes();
+ QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID), QStringLiteral("o-FfiyfBQq6Qz6YVrYeFTw"));
+ QCOMPARE(
+ attr->value(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM),
+ QStringLiteral(
+ "-----BEGIN PRIVATE "
+ "KEY-----"
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmr4GQQjerojFuf0ZouOuUllMvAwxZSZAfB6gwDYcLiehRANCAAT0WR5zVS"
+ "p6ieusvjkLkzaGc7fjGBmwpiuLPxR/d+ZjqMI9L2DKh+takp6wGt2x0n4jzr1KA352NZg0vjZX9CHh-----END PRIVATE KEY-----"));
+ QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_USERNAME), QStringLiteral("KPXC_BITWARDEN"));
+ QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY), QStringLiteral("webauthn.io"));
+ QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE),
+ QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
+}
diff --git a/tests/TestImports.h b/tests/TestImports.h
index 2e00de9a6..ece40d539 100644
--- a/tests/TestImports.h
+++ b/tests/TestImports.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 KeePassXC Team
+ * Copyright (C) 2024 KeePassXC Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,6 +30,7 @@ private slots:
void testOPVault();
void testBitwarden();
void testBitwardenEncrypted();
+ void testBitwardenPasskey();
};
#endif /* TEST_IMPORTS_H */
diff --git a/tests/data/bitwarden_passkey_export.json b/tests/data/bitwarden_passkey_export.json
new file mode 100644
index 000000000..0044fc334
--- /dev/null
+++ b/tests/data/bitwarden_passkey_export.json
@@ -0,0 +1,49 @@
+{
+ "encrypted": false,
+ "folders": [],
+ "items": [
+ {
+ "passwordHistory": null,
+ "revisionDate": "2024-10-23T16:38:08.870Z",
+ "creationDate": "2024-10-23T16:38:08.606Z",
+ "deletedDate": null,
+ "id": "a8e579f0-98c2-4ac9-a126-b212011225f8",
+ "organizationId": null,
+ "folderId": null,
+ "type": 1,
+ "reprompt": 0,
+ "name": "webauthn.io",
+ "notes": null,
+ "favorite": false,
+ "login": {
+ "fido2Credentials": [
+ {
+ "credentialId": "a3e15f8b-27c1-42ae-90cf-a615ad87854f",
+ "keyType": "public-key",
+ "keyAlgorithm": "ECDSA",
+ "keyCurve": "P-256",
+ "keyValue": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmr4GQQjerojFuf0ZouOuUllMvAwxZSZAfB6gwDYcLiehRANCAAT0WR5zVSp6ieusvjkLkzaGc7fjGBmwpiuLPxR_d-ZjqMI9L2DKh-takp6wGt2x0n4jzr1KA352NZg0vjZX9CHh",
+ "rpId": "webauthn.io",
+ "userHandle": "aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw",
+ "userName": "KPXC_BITWARDEN",
+ "counter": "0",
+ "rpName": "webauthn.io",
+ "userDisplayName": "KPXC_BITWARDEN",
+ "discoverable": "true",
+ "creationDate": "2024-10-23T16:38:08.617Z"
+ }
+ ],
+ "uris": [
+ {
+ "match": null,
+ "uri": "https://webauthn.io/"
+ }
+ ],
+ "username": "KPXC_BITWARDEN",
+ "password": null,
+ "totp": null
+ },
+ "collectionIds": null
+ }
+ ]
+}
\ No newline at end of file