From 6e0baf9f2c08f52cd5125b683ad8ae4ef55ef2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Fri, 25 Oct 2024 03:12:47 +0300 Subject: [PATCH] Support passkeys with Bitwarden import (#11401) --- share/translations/keepassxc_en.ts | 4 ++ src/browser/BrowserPasskeys.cpp | 11 ----- src/browser/BrowserPasskeys.h | 8 ---- src/browser/BrowserService.cpp | 25 ++++++------ src/browser/PasskeyUtils.cpp | 13 +++--- src/core/EntryAttributes.cpp | 13 ++++++ src/core/EntryAttributes.h | 11 +++++ src/format/BitwardenReader.cpp | 39 +++++++++++++++++- src/gui/passkeys/PasskeyExporter.cpp | 7 ++-- src/gui/passkeys/PasskeyImporter.cpp | 4 +- src/gui/reports/ReportsWidgetPasskeys.cpp | 5 ++- tests/TestImports.cpp | 35 +++++++++++++++- tests/TestImports.h | 3 +- tests/data/bitwarden_passkey_export.json | 49 +++++++++++++++++++++++ 14 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 tests/data/bitwarden_passkey_export.json 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. + + Passkey + + 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