mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-25 23:39:45 -05:00
Support passkeys with Bitwarden import (#11401)
This commit is contained in:
parent
95bae8377c
commit
6e0baf9f2c
@ -9061,6 +9061,10 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||||||
<numerusform></numerusform>
|
<numerusform></numerusform>
|
||||||
</translation>
|
</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Passkey</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>QtIOCompressor</name>
|
<name>QtIOCompressor</name>
|
||||||
|
@ -58,17 +58,6 @@ const QString BrowserPasskeys::REQUIREMENT_REQUIRED = QStringLiteral("required")
|
|||||||
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("direct");
|
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("direct");
|
||||||
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_NONE = QStringLiteral("none");
|
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()
|
BrowserPasskeys* BrowserPasskeys::instance()
|
||||||
{
|
{
|
||||||
return s_browserPasskeys;
|
return s_browserPasskeys;
|
||||||
|
@ -105,14 +105,6 @@ public:
|
|||||||
static const QString PASSKEYS_ATTESTATION_DIRECT;
|
static const QString PASSKEYS_ATTESTATION_DIRECT;
|
||||||
static const QString PASSKEYS_ATTESTATION_NONE;
|
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:
|
private:
|
||||||
QByteArray buildAttestationObject(const QJsonObject& credentialCreationOptions,
|
QByteArray buildAttestationObject(const QJsonObject& credentialCreationOptions,
|
||||||
const QString& extensions,
|
const QString& extensions,
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
#include "BrowserHost.h"
|
#include "BrowserHost.h"
|
||||||
#include "BrowserMessageBuilder.h"
|
#include "BrowserMessageBuilder.h"
|
||||||
#include "BrowserSettings.h"
|
#include "BrowserSettings.h"
|
||||||
|
#include "core/EntryAttributes.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
#include "gui/MainWindow.h"
|
#include "gui/MainWindow.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
@ -763,9 +764,9 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
|
|||||||
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
|
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 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 =
|
auto publicKeyCredential =
|
||||||
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
|
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
|
||||||
@ -843,11 +844,11 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
|
|||||||
|
|
||||||
entry->beginUpdate();
|
entry->beginUpdate();
|
||||||
|
|
||||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USERNAME, username);
|
||||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
|
||||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
|
||||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId);
|
||||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
|
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
|
||||||
entry->addTag(tr("Passkey"));
|
entry->addTag(tr("Passkey"));
|
||||||
|
|
||||||
entry->endUpdate();
|
entry->endUpdate();
|
||||||
@ -1042,7 +1043,7 @@ QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
|
|||||||
|
|
||||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||||
// With Passkeys, check for the Relying Party instead of URL
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -1384,7 +1385,7 @@ QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const Strin
|
|||||||
{
|
{
|
||||||
QList<Entry*> entries;
|
QList<Entry*> entries;
|
||||||
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
|
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;
|
entries << entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1399,8 +1400,8 @@ QList<Entry*> BrowserService::getPasskeyEntriesWithUserHandle(const QString& rpI
|
|||||||
{
|
{
|
||||||
QList<Entry*> entries;
|
QList<Entry*> entries;
|
||||||
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
|
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
|
||||||
&& entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE) == userId) {
|
&& entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE) == userId) {
|
||||||
entries << entry;
|
entries << entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1425,7 +1426,7 @@ QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& assert
|
|||||||
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
|
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
|
||||||
if (allowedCredentials.contains(passkeyUtils()->getCredentialIdFromEntry(entry))
|
if (allowedCredentials.contains(passkeyUtils()->getCredentialIdFromEntry(entry))
|
||||||
|| (allowedCredentials.isEmpty()
|
|| (allowedCredentials.isEmpty()
|
||||||
&& entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
|
&& entry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_USER_HANDLE))) {
|
||||||
entries << entry;
|
entries << entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
#include "PasskeyUtils.h"
|
#include "PasskeyUtils.h"
|
||||||
#include "BrowserMessageBuilder.h"
|
#include "BrowserMessageBuilder.h"
|
||||||
#include "BrowserPasskeys.h"
|
#include "BrowserPasskeys.h"
|
||||||
|
#include "core/EntryAttributes.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
#include "gui/UrlTools.h"
|
#include "gui/UrlTools.h"
|
||||||
|
|
||||||
@ -389,9 +390,9 @@ QString PasskeyUtils::getCredentialIdFromEntry(const Entry* entry) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
|
return entry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID)
|
||||||
? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
|
? entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID)
|
||||||
: entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID);
|
: entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For compatibility with StrongBox (and other possible clients in the future)
|
// For compatibility with StrongBox (and other possible clients in the future)
|
||||||
@ -401,7 +402,7 @@ QString PasskeyUtils::getUsernameFromEntry(const Entry* entry) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry->attributes()->hasKey(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
|
return entry->attributes()->hasKey(EntryAttributes::KPXC_PASSKEY_USERNAME)
|
||||||
? entry->attributes()->value(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
|
? entry->attributes()->value(EntryAttributes::KPXC_PASSKEY_USERNAME)
|
||||||
: entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME);
|
: entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USERNAME);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,20 @@ const QString EntryAttributes::SearchTextGroupName = "SearchText";
|
|||||||
|
|
||||||
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
|
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
|
||||||
const QString EntryAttributes::AdditionalUrlAttribute = "KP2A_URL";
|
const QString EntryAttributes::AdditionalUrlAttribute = "KP2A_URL";
|
||||||
|
|
||||||
|
// Passkey related attributes
|
||||||
const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY";
|
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)
|
EntryAttributes::EntryAttributes(QObject* parent)
|
||||||
: ModifiableObject(parent)
|
: ModifiableObject(parent)
|
||||||
|
@ -64,7 +64,18 @@ public:
|
|||||||
static const QStringList DefaultAttributes;
|
static const QStringList DefaultAttributes;
|
||||||
static const QString RememberCmdExecAttr;
|
static const QString RememberCmdExecAttr;
|
||||||
static const QString AdditionalUrlAttribute;
|
static const QString AdditionalUrlAttribute;
|
||||||
|
|
||||||
static const QString PasskeyAttribute;
|
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 isDefaultAttribute(const QString& key);
|
||||||
static bool isPasskeyAttribute(const QString& key);
|
static bool isPasskeyAttribute(const QString& key);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -81,6 +81,43 @@ namespace
|
|||||||
entry->setTotp(Totp::parseSettings(totp));
|
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)
|
// Set the entry url(s)
|
||||||
int i = 1;
|
int i = 1;
|
||||||
for (const auto& urlObj : loginMap.value("uris").toList()) {
|
for (const auto& urlObj : loginMap.value("uris").toList()) {
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#include "browser/BrowserPasskeys.h"
|
#include "browser/BrowserPasskeys.h"
|
||||||
#include "browser/PasskeyUtils.h"
|
#include "browser/PasskeyUtils.h"
|
||||||
#include "core/Entry.h"
|
#include "core/Entry.h"
|
||||||
|
#include "core/EntryAttributes.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
@ -94,12 +95,12 @@ void PasskeyExporter::exportSelectedEntry(const Entry* entry, const QString& fol
|
|||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject passkeyObject;
|
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["url"] = entry->url();
|
||||||
passkeyObject["username"] = passkeyUtils()->getUsernameFromEntry(entry);
|
passkeyObject["username"] = passkeyUtils()->getUsernameFromEntry(entry);
|
||||||
passkeyObject["credentialId"] = passkeyUtils()->getCredentialIdFromEntry(entry);
|
passkeyObject["credentialId"] = passkeyUtils()->getCredentialIdFromEntry(entry);
|
||||||
passkeyObject["userHandle"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
|
passkeyObject["userHandle"] = entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
|
||||||
passkeyObject["privateKey"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
|
passkeyObject["privateKey"] = entry->attributes()->value(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM);
|
||||||
|
|
||||||
QJsonDocument document(passkeyObject);
|
QJsonDocument document(passkeyObject);
|
||||||
if (passkeyFile.write(document.toJson()) < 0) {
|
if (passkeyFile.write(document.toJson()) < 0) {
|
||||||
|
@ -76,8 +76,8 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer<Database>&
|
|||||||
tr("Cannot import passkey"),
|
tr("Cannot import passkey"),
|
||||||
tr("Cannot import passkey file \"%1\".\nThe following data is missing:\n%2")
|
tr("Cannot import passkey file \"%1\".\nThe following data is missing:\n%2")
|
||||||
.arg(file.fileName(), missingKeys.join(", ")));
|
.arg(file.fileName(), missingKeys.join(", ")));
|
||||||
} else if (!privateKey.startsWith("-----BEGIN PRIVATE KEY-----")
|
} else if (!privateKey.startsWith(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START)
|
||||||
|| !privateKey.trimmed().endsWith("-----END PRIVATE KEY-----")) {
|
|| !privateKey.trimmed().endsWith(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END)) {
|
||||||
MessageBox::information(
|
MessageBox::information(
|
||||||
m_parent,
|
m_parent,
|
||||||
tr("Cannot import passkey"),
|
tr("Cannot import passkey"),
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#include "browser/BrowserPasskeys.h"
|
#include "browser/BrowserPasskeys.h"
|
||||||
#include "browser/PasskeyUtils.h"
|
#include "browser/PasskeyUtils.h"
|
||||||
#include "core/AsyncTask.h"
|
#include "core/AsyncTask.h"
|
||||||
|
#include "core/EntryAttributes.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
#include "gui/GuiTools.h"
|
#include "gui/GuiTools.h"
|
||||||
@ -75,7 +76,7 @@ PasskeyList::PasskeyList(const QSharedPointer<Database>& db)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto entry : group->entries()) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +135,7 @@ void ReportsWidgetPasskeys::addPasskeyRow(Group* group, Entry* entry)
|
|||||||
row << new QStandardItem(Icons::entryIconPixmap(entry), title);
|
row << new QStandardItem(Icons::entryIconPixmap(entry), title);
|
||||||
row << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/"));
|
row << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/"));
|
||||||
row << new QStandardItem(passkeyUtils()->getUsernameFromEntry(entry));
|
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'));
|
row << new QStandardItem(urlList.join('\n'));
|
||||||
|
|
||||||
// Set tooltips
|
// Set tooltips
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -282,3 +282,36 @@ void TestImports::testBitwardenEncrypted()
|
|||||||
}
|
}
|
||||||
QVERIFY(db);
|
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"));
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
|
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -30,6 +30,7 @@ private slots:
|
|||||||
void testOPVault();
|
void testOPVault();
|
||||||
void testBitwarden();
|
void testBitwarden();
|
||||||
void testBitwardenEncrypted();
|
void testBitwardenEncrypted();
|
||||||
|
void testBitwardenPasskey();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* TEST_IMPORTS_H */
|
#endif /* TEST_IMPORTS_H */
|
||||||
|
49
tests/data/bitwarden_passkey_export.json
Normal file
49
tests/data/bitwarden_passkey_export.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user