diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index f2deaf1d0..e1a6542ab 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -955,6 +955,15 @@ Do you want to delete the entry?
%1 (Passkey)
+
+ KeePassXC: Update Passkey
+
+
+
+ Entry already has a Passkey.
+Do you want to overwrite the Passkey in %1 - %2?
+
+
Converting attributes to custom data…
@@ -6017,10 +6026,6 @@ Do you want to overwrite it?
KeePassXC - Passkey Import
-
- Do you want to import the Passkey?
-
-
URL: %1
@@ -6029,10 +6034,6 @@ Do you want to overwrite it?
Username: %1
-
- Use default group (Imported Passkeys)
-
-
Group
@@ -6041,10 +6042,6 @@ Do you want to overwrite it?
Database
-
- Select Database
-
-
Import Passkey
@@ -6058,11 +6055,23 @@ Do you want to overwrite it?
- Database: %1
+ Import the following Passkey:
- Group:
+ Entry
+
+
+
+ Import the following Passkey to this entry:
+
+
+
+ Create new entry
+
+
+
+ Default Passkeys group (Imported Passkeys)
@@ -6100,6 +6109,12 @@ Do you want to overwrite it?
Cannot import Passkey file "%1". Private key is missing or malformed.
+
+ Cannot import Passkey file "%1".
+The following data is missing:
+%2
+
+
PasswordEditWidget
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
index e70ec699f..f9c333fd7 100644
--- a/src/browser/BrowserService.cpp
+++ b/src/browser/BrowserService.cpp
@@ -64,8 +64,6 @@ const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEnt
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww");
-// Multiple URL's
-const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");
Q_GLOBAL_STATIC(BrowserService, s_browserService);
@@ -727,6 +725,20 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
return;
}
+ // Ask confirmation if entry already contains a Passkey
+ if (entry->hasPasskey()) {
+ if (MessageBox::question(
+ m_currentDatabaseWidget,
+ tr("KeePassXC: Update Passkey"),
+ tr("Entry already has a Passkey.\nDo you want to overwrite the Passkey in %1 - %2?")
+ .arg(entry->title(), entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME)),
+ MessageBox::Overwrite | MessageBox::Cancel,
+ MessageBox::Cancel)
+ != MessageBox::Overwrite) {
+ return;
+ }
+ }
+
entry->beginUpdate();
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
@@ -1110,7 +1122,13 @@ void BrowserService::denyEntry(Entry* entry, const QString& siteHost, const QStr
QJsonObject BrowserService::prepareEntry(const Entry* entry)
{
QJsonObject res;
+#ifdef WITH_XC_BROWSER_PASSKEYS
+ // Use Passkey's username instead if found
+ res["login"] = entry->hasPasskey() ? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME)
+ : entry->resolveMultiplePlaceholders(entry->username());
+#else
res["login"] = entry->resolveMultiplePlaceholders(entry->username());
+#endif
res["password"] = entry->resolveMultiplePlaceholders(entry->password());
res["name"] = entry->resolveMultiplePlaceholders(entry->title());
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
@@ -1317,8 +1335,7 @@ QList BrowserService::getPasskeyEntries(const QString& rpId, const Strin
{
QList entries;
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
- if (entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM)
- && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
+ if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
entries << entry;
}
}
@@ -1445,14 +1462,34 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false;
}
-QSharedPointer BrowserService::getDatabase()
+QSharedPointer BrowserService::getDatabase(const QUuid& rootGroupUuid)
{
+ if (!rootGroupUuid.isNull()) {
+ const auto openDatabases = getOpenDatabases();
+ for (const auto& db : openDatabases) {
+ if (db->rootGroup()->uuid() == rootGroupUuid) {
+ return db;
+ }
+ }
+ }
+
if (m_currentDatabaseWidget) {
return m_currentDatabaseWidget->database();
}
return {};
}
+QList> BrowserService::getOpenDatabases()
+{
+ QList> databaseList;
+ for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
+ if (!dbWidget->isLocked()) {
+ databaseList << dbWidget->database();
+ }
+ }
+ return databaseList;
+}
+
QSharedPointer BrowserService::selectedDatabase()
{
QList databaseWidgets;
diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h
index 5154c8f81..da5133613 100644
--- a/src/browser/BrowserService.h
+++ b/src/browser/BrowserService.h
@@ -83,7 +83,9 @@ public:
QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
bool isPasswordGeneratorRequested() const;
+ QSharedPointer getDatabase(const QUuid& rootGroupUuid = {});
QSharedPointer selectedDatabase();
+ QList> getOpenDatabases();
#ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject
showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
@@ -124,7 +126,6 @@ public:
static const QString OPTION_ONLY_HTTP_AUTH;
static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW;
- static const QString ADDITIONAL_URL;
signals:
void requestUnlock();
@@ -191,7 +192,6 @@ private:
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain = false);
- QSharedPointer getDatabase();
QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid();
bool checkLegacySettings(QSharedPointer db);
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index 718ff4b9e..04120e90b 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -385,7 +385,8 @@ QStringList Entry::getAllUrls() const
}
for (const auto& key : m_attributes->keys()) {
- if (key.startsWith("KP2A_URL")) {
+ if (key.startsWith(EntryAttributes::AdditionalUrlAttribute)
+ || key == QString("%1_RELYING_PARTY").arg(EntryAttributes::PasskeyAttribute)) {
auto additionalUrl = m_attributes->value(key);
if (!additionalUrl.isEmpty()) {
urlList << resolveMultiplePlaceholders(additionalUrl);
@@ -545,6 +546,11 @@ bool Entry::hasTotp() const
return !m_data.totpSettings.isNull();
}
+bool Entry::hasPasskey() const
+{
+ return m_attributes->hasPasskey();
+}
+
QString Entry::totp() const
{
if (hasTotp()) {
diff --git a/src/core/Entry.h b/src/core/Entry.h
index 8d9dd1b23..796170514 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -121,6 +121,7 @@ public:
void setExcludeFromReports(bool state);
bool hasTotp() const;
+ bool hasPasskey() const;
bool isExpired() const;
bool willExpireInDays(int days) const;
bool isRecycled() const;
diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp
index 13207e168..49c243ec1 100644
--- a/src/core/EntryAttributes.cpp
+++ b/src/core/EntryAttributes.cpp
@@ -34,6 +34,7 @@ const QString EntryAttributes::SearchInGroupName = "SearchIn";
const QString EntryAttributes::SearchTextGroupName = "SearchText";
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
+const QString EntryAttributes::AdditionalUrlAttribute = "KP2A_URL";
const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY";
EntryAttributes::EntryAttributes(QObject* parent)
@@ -52,6 +53,18 @@ bool EntryAttributes::hasKey(const QString& key) const
return m_attributes.contains(key);
}
+bool EntryAttributes::hasPasskey() const
+{
+ const auto keyList = keys();
+ for (const auto& key : keyList) {
+ if (isPasskeyAttribute(key)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
QList EntryAttributes::customKeys() const
{
QList customKeys;
diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h
index 2e7f8c05c..fecf6a993 100644
--- a/src/core/EntryAttributes.h
+++ b/src/core/EntryAttributes.h
@@ -1,6 +1,6 @@
/*
+ * Copyright (C) 2023 KeePassXC Team
* Copyright (C) 2012 Felix Geyer
- * Copyright (C) 2017 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
@@ -33,6 +33,7 @@ public:
explicit EntryAttributes(QObject* parent = nullptr);
QList keys() const;
bool hasKey(const QString& key) const;
+ bool hasPasskey() const;
QList customKeys() const;
QString value(const QString& key) const;
QList values(const QList& keys) const;
@@ -61,6 +62,7 @@ public:
static const QString NotesKey;
static const QStringList DefaultAttributes;
static const QString RememberCmdExecAttr;
+ static const QString AdditionalUrlAttribute;
static const QString PasskeyAttribute;
static bool isDefaultAttribute(const QString& key);
static bool isPasskeyAttribute(const QString& key);
diff --git a/src/core/Tools.h b/src/core/Tools.h
index 4316a44e8..85c1b53c0 100644
--- a/src/core/Tools.h
+++ b/src/core/Tools.h
@@ -22,6 +22,7 @@
#include "core/Global.h"
#include
+#include
#include
class QIODevice;
@@ -100,6 +101,19 @@ namespace Tools
return version;
}
+ // Checks if all values are found inside the list. Returns a list of values not found.
+ template QList getMissingValuesFromList(const QList& list, const QList& required)
+ {
+ QList missingValues;
+ for (const auto& r : required) {
+ if (!list.contains(r)) {
+ missingValues << r;
+ }
+ }
+
+ return missingValues;
+ }
+
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties = {"objectName"});
QString substituteBackupFilePath(QString pattern, const QString& databasePath);
diff --git a/src/format/OpVaultReaderBandEntry.cpp b/src/format/OpVaultReaderBandEntry.cpp
index 6f79dd637..3a9774b68 100644
--- a/src/format/OpVaultReaderBandEntry.cpp
+++ b/src/format/OpVaultReaderBandEntry.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 KeePassXC Team
+ * Copyright (C) 2023 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
@@ -243,7 +243,8 @@ bool OpVaultReader::fillAttributes(Entry* entry, const QJsonObject& bandEntry)
auto newUrl = urlObj["u"].toString();
if (newUrl != url) {
// Add this url if it isn't the base one
- entry->attributes()->set(QString("KP2A_URL_%1").arg(i), newUrl);
+ entry->attributes()->set(
+ QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), newUrl);
++i;
}
}
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index 44d2260c6..1b9802699 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -566,7 +566,12 @@ void DatabaseTabWidget::showPasskeys()
void DatabaseTabWidget::importPasskey()
{
- currentDatabaseWidget()->switchToImportPasskey();
+ currentDatabaseWidget()->showImportPasskeyDialog();
+}
+
+void DatabaseTabWidget::importPasskeyToEntry()
+{
+ currentDatabaseWidget()->showImportPasskeyDialog(true);
}
#endif
diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h
index 6b4b121af..8f2703878 100644
--- a/src/gui/DatabaseTabWidget.h
+++ b/src/gui/DatabaseTabWidget.h
@@ -88,6 +88,7 @@ public slots:
#ifdef WITH_XC_BROWSER_PASSKEYS
void showPasskeys();
void importPasskey();
+ void importPasskeyToEntry();
#endif
void performGlobalAutoType(const QString& search);
void performBrowserUnlock();
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index 5f1543ef0..94fa2fbe7 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -1403,10 +1403,20 @@ void DatabaseWidget::switchToPasskeys()
m_reportsDialog->activatePasskeysPage();
}
-void DatabaseWidget::switchToImportPasskey()
+void DatabaseWidget::showImportPasskeyDialog(bool isEntry)
{
PasskeyImporter passkeyImporter;
- passkeyImporter.importPasskey(m_db);
+
+ if (isEntry) {
+ auto currentEntry = currentSelectedEntry();
+ if (!currentEntry) {
+ return;
+ }
+
+ passkeyImporter.importPasskey(m_db, currentEntry);
+ } else {
+ passkeyImporter.importPasskey(m_db);
+ }
}
#endif
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index d92909198..77fa3d7f4 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -214,7 +214,7 @@ public slots:
void switchToDatabaseSettings();
#ifdef WITH_XC_BROWSER_PASSKEYS
void switchToPasskeys();
- void switchToImportPasskey();
+ void showImportPasskeyDialog(bool isEntry = false);
#endif
void switchToOpenDatabase();
void switchToOpenDatabase(const QString& filePath);
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 9e8d35f82..d08c709d6 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -135,6 +135,10 @@ MainWindow::MainWindow()
m_entryContextMenu->addSeparator();
m_entryContextMenu->addAction(m_ui->actionEntryAutoType);
m_entryContextMenu->addSeparator();
+#ifdef WITH_XC_BROWSER_PASSKEYS
+ m_entryContextMenu->addAction(m_ui->actionEntryImportPasskey);
+ m_entryContextMenu->addSeparator();
+#endif
m_entryContextMenu->addAction(m_ui->actionEntryEdit);
m_entryContextMenu->addAction(m_ui->actionEntryClone);
m_entryContextMenu->addAction(m_ui->actionEntryDelete);
@@ -441,6 +445,7 @@ MainWindow::MainWindow()
#ifdef WITH_XC_BROWSER_PASSKEYS
m_ui->actionPasskeys->setIcon(icons()->icon("passkey"));
m_ui->actionImportPasskey->setIcon(icons()->icon("document-import"));
+ m_ui->actionEntryImportPasskey->setIcon(icons()->icon("document-import"));
#endif
m_actionMultiplexer.connect(
@@ -491,6 +496,7 @@ MainWindow::MainWindow()
#ifdef WITH_XC_BROWSER_PASSKEYS
connect(m_ui->actionPasskeys, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showPasskeys()));
connect(m_ui->actionImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskey()));
+ connect(m_ui->actionEntryImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskeyToEntry()));
#endif
connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv()));
connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database()));
@@ -989,6 +995,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
#ifdef WITH_XC_BROWSER_PASSKEYS
m_ui->actionPasskeys->setEnabled(true);
m_ui->actionImportPasskey->setEnabled(true);
+ m_ui->actionEntryImportPasskey->setEnabled(true);
#endif
#ifdef WITH_XC_SSHAGENT
bool singleEntryHasSshKey =
@@ -1060,9 +1067,11 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
#ifdef WITH_XC_BROWSER_PASSKEYS
m_ui->actionPasskeys->setEnabled(false);
m_ui->actionImportPasskey->setEnabled(false);
+ m_ui->actionEntryImportPasskey->setEnabled(false);
#else
m_ui->actionPasskeys->setVisible(false);
m_ui->actionImportPasskey->setVisible(false);
+ m_ui->actionEntryImportPasskey->setVisible(false);
#endif
m_searchWidgetAction->setEnabled(false);
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
index e6fbfc22c..a0b8d6e68 100644
--- a/src/gui/MainWindow.ui
+++ b/src/gui/MainWindow.ui
@@ -342,6 +342,8 @@
+
+
@@ -730,6 +732,14 @@
Perform &Auto-Type
+
+
+ false
+
+
+ Import Passkey
+
+
false
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 1a53ef36c..a1297d4c0 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -1,6 +1,6 @@
/*
+ * Copyright (C) 2023 KeePassXC Team
* Copyright (C) 2010 Felix Geyer
- * Copyright (C) 2021 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
@@ -330,11 +330,11 @@ void EditEntryWidget::insertURL()
{
Q_ASSERT(!m_history);
- QString name(BrowserService::ADDITIONAL_URL);
+ QString name(EntryAttributes::AdditionalUrlAttribute);
int i = 1;
while (m_entryAttributes->keys().contains(name)) {
- name = QString("%1_%2").arg(BrowserService::ADDITIONAL_URL, QString::number(i));
+ name = QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i));
i++;
}
diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp
index 9a4340f5c..046fbee61 100644
--- a/src/gui/entry/EntryURLModel.cpp
+++ b/src/gui/entry/EntryURLModel.cpp
@@ -70,7 +70,7 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const
const auto urlValid = urlTools()->isUrlValid(value);
// Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison.
- auto customAttributeKeys = m_entryAttributes->customKeys().filter(BrowserService::ADDITIONAL_URL);
+ auto customAttributeKeys = m_entryAttributes->customKeys().filter(EntryAttributes::AdditionalUrlAttribute);
customAttributeKeys.removeOne(key);
const auto duplicateUrl =
@@ -148,7 +148,7 @@ void EntryURLModel::updateAttributes()
const auto attributesKeyList = m_entryAttributes->keys();
for (const auto& key : attributesKeyList) {
- if (!EntryAttributes::isDefaultAttribute(key) && key.contains(BrowserService::ADDITIONAL_URL)) {
+ if (!EntryAttributes::isDefaultAttribute(key) && key.contains(EntryAttributes::AdditionalUrlAttribute)) {
const auto value = m_entryAttributes->value(key);
m_urls.append(qMakePair(key, value));
diff --git a/src/gui/passkeys/PasskeyImportDialog.cpp b/src/gui/passkeys/PasskeyImportDialog.cpp
index 2d54ba5ba..0c1c31e6d 100644
--- a/src/gui/passkeys/PasskeyImportDialog.cpp
+++ b/src/gui/passkeys/PasskeyImportDialog.cpp
@@ -27,33 +27,41 @@
PasskeyImportDialog::PasskeyImportDialog(QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::PasskeyImportDialog())
- , m_useDefaultGroup(true)
{
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
m_ui->setupUi(this);
- m_ui->useDefaultGroupCheckbox->setChecked(true);
- m_ui->selectGroupComboBox->setEnabled(false);
+ connect(this, SIGNAL(updateGroups()), this, SLOT(addGroups()));
+ connect(this, SIGNAL(updateEntries()), this, SLOT(addEntries()));
connect(m_ui->importButton, SIGNAL(clicked()), SLOT(accept()));
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
- connect(m_ui->selectDatabaseButton, SIGNAL(clicked()), SLOT(selectDatabase()));
+ connect(m_ui->selectDatabaseCombobBox, SIGNAL(currentIndexChanged(int)), SLOT(changeDatabase(int)));
+ connect(m_ui->selectEntryComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeEntry(int)));
connect(m_ui->selectGroupComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeGroup(int)));
- connect(m_ui->useDefaultGroupCheckbox, SIGNAL(stateChanged(int)), SLOT(useDefaultGroupChanged()));
}
PasskeyImportDialog::~PasskeyImportDialog()
{
}
-void PasskeyImportDialog::setInfo(const QString& url, const QString& username, const QSharedPointer& database)
+void PasskeyImportDialog::setInfo(const QString& url,
+ const QString& username,
+ const QSharedPointer& database,
+ bool isEntry)
{
m_ui->urlLabel->setText(tr("URL: %1").arg(url));
m_ui->usernameLabel->setText(tr("Username: %1").arg(username));
- m_ui->selectDatabaseLabel->setText(tr("Database: %1").arg(getDatabaseName(database)));
- m_ui->selectGroupLabel->setText(tr("Group:"));
- addGroups(database);
+ if (isEntry) {
+ m_ui->verticalLayout->setSizeConstraint(QLayout::SetFixedSize);
+ m_ui->infoLabel->setText(tr("Import the following Passkey to this entry:"));
+ m_ui->groupBox->setVisible(false);
+ }
+
+ m_selectedDatabase = database;
+ addDatabases();
+ addGroups();
auto openDatabaseCount = 0;
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
@@ -61,34 +69,96 @@ void PasskeyImportDialog::setInfo(const QString& url, const QString& username, c
openDatabaseCount++;
}
}
- m_ui->selectDatabaseButton->setEnabled(openDatabaseCount > 1);
+ m_ui->selectDatabaseCombobBox->setEnabled(openDatabaseCount > 1);
}
-QSharedPointer PasskeyImportDialog::getSelectedDatabase()
+QSharedPointer PasskeyImportDialog::getSelectedDatabase() const
{
return m_selectedDatabase;
}
-QUuid PasskeyImportDialog::getSelectedGroupUuid()
+QUuid PasskeyImportDialog::getSelectedEntryUuid() const
+{
+ return m_selectedEntryUuid;
+}
+
+QUuid PasskeyImportDialog::getSelectedGroupUuid() const
{
return m_selectedGroupUuid;
}
-bool PasskeyImportDialog::useDefaultGroup()
+bool PasskeyImportDialog::useDefaultGroup() const
{
- return m_useDefaultGroup;
+ return m_selectedGroupUuid.isNull();
}
-QString PasskeyImportDialog::getDatabaseName(const QSharedPointer& database) const
+bool PasskeyImportDialog::createNewEntry() const
{
- return QFileInfo(database->filePath()).fileName();
+ return m_selectedEntryUuid.isNull();
}
-void PasskeyImportDialog::addGroups(const QSharedPointer& database)
+void PasskeyImportDialog::addDatabases()
{
+ auto currentDatabaseIndex = 0;
+ const auto openDatabases = browserService()->getOpenDatabases();
+ const auto currentDatabase = browserService()->getDatabase();
+
+ m_ui->selectDatabaseCombobBox->clear();
+ for (const auto& db : openDatabases) {
+ m_ui->selectDatabaseCombobBox->addItem(db->metadata()->name(), db->rootGroup()->uuid());
+ if (db->rootGroup()->uuid() == currentDatabase->rootGroup()->uuid()) {
+ currentDatabaseIndex = m_ui->selectDatabaseCombobBox->count() - 1;
+ }
+ }
+
+ m_ui->selectDatabaseCombobBox->setCurrentIndex(currentDatabaseIndex);
+}
+
+void PasskeyImportDialog::addEntries()
+{
+ if (!m_selectedDatabase || !m_selectedDatabase->rootGroup()) {
+ return;
+ }
+
+ m_ui->selectEntryComboBox->clear();
+ m_ui->selectEntryComboBox->addItem(tr("Create new entry"), {});
+
+ const auto group = m_selectedDatabase->rootGroup()->findGroupByUuid(m_selectedGroupUuid);
+ if (!group) {
+ return;
+ }
+
+ // Collect all entries in the group and resolve the title
+ QList> entries;
+ for (const auto entry : group->entries()) {
+ if (!entry || entry->isRecycled()) {
+ continue;
+ }
+ entries.append({entry->resolveMultiplePlaceholders(entry->title()), entry->uuid()});
+ }
+
+ // Sort entries by title
+ std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
+ return a.first.compare(b.first, Qt::CaseInsensitive) < 0;
+ });
+
+ // Add sorted entries to the combobox
+ for (const auto& pair : entries) {
+ m_ui->selectEntryComboBox->addItem(pair.first, pair.second);
+ }
+}
+
+void PasskeyImportDialog::addGroups()
+{
+ if (!m_selectedDatabase) {
+ return;
+ }
+
m_ui->selectGroupComboBox->clear();
- for (const auto& group : database->rootGroup()->groupsRecursive(true)) {
- if (!group || group->isRecycled() || group == database->metadata()->recycleBin()) {
+ m_ui->selectGroupComboBox->addItem(tr("Default Passkeys group (Imported Passkeys)"), {});
+
+ for (const auto& group : m_selectedDatabase->rootGroup()->groupsRecursive(true)) {
+ if (!group || group->isRecycled() || group == m_selectedDatabase->metadata()->recycleBin()) {
continue;
}
@@ -96,26 +166,20 @@ void PasskeyImportDialog::addGroups(const QSharedPointer& database)
}
}
-void PasskeyImportDialog::selectDatabase()
+void PasskeyImportDialog::changeDatabase(int index)
{
- auto selectedDatabase = browserService()->selectedDatabase();
- if (!selectedDatabase) {
- return;
- }
+ m_selectedDatabaseUuid = m_ui->selectDatabaseCombobBox->itemData(index).value();
+ m_selectedDatabase = browserService()->getDatabase(m_selectedDatabaseUuid);
+ emit updateGroups();
+}
- m_selectedDatabase = selectedDatabase;
- m_ui->selectDatabaseLabel->setText(QString("Database: %1").arg(getDatabaseName(m_selectedDatabase)));
-
- addGroups(m_selectedDatabase);
+void PasskeyImportDialog::changeEntry(int index)
+{
+ m_selectedEntryUuid = m_ui->selectEntryComboBox->itemData(index).value();
}
void PasskeyImportDialog::changeGroup(int index)
{
m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value();
-}
-
-void PasskeyImportDialog::useDefaultGroupChanged()
-{
- m_ui->selectGroupComboBox->setEnabled(!m_ui->useDefaultGroupCheckbox->isChecked());
- m_useDefaultGroup = m_ui->useDefaultGroupCheckbox->isChecked();
+ emit updateEntries();
}
diff --git a/src/gui/passkeys/PasskeyImportDialog.h b/src/gui/passkeys/PasskeyImportDialog.h
index 7b316721e..705c6d187 100644
--- a/src/gui/passkeys/PasskeyImportDialog.h
+++ b/src/gui/passkeys/PasskeyImportDialog.h
@@ -36,25 +36,33 @@ public:
explicit PasskeyImportDialog(QWidget* parent = nullptr);
~PasskeyImportDialog() override;
- void setInfo(const QString& url, const QString& username, const QSharedPointer& database);
- QSharedPointer getSelectedDatabase();
- QUuid getSelectedGroupUuid();
- bool useDefaultGroup();
+ void setInfo(const QString& url, const QString& username, const QSharedPointer& database, bool isEntry);
+ QSharedPointer getSelectedDatabase() const;
+ QUuid getSelectedEntryUuid() const;
+ QUuid getSelectedGroupUuid() const;
+ bool useDefaultGroup() const;
+ bool createNewEntry() const;
private:
- QString getDatabaseName(const QSharedPointer& database) const;
- void addGroups(const QSharedPointer& database);
+ void addDatabases();
+
+signals:
+ void updateEntries();
+ void updateGroups();
private slots:
- void selectDatabase();
+ void addEntries();
+ void addGroups();
+ void changeDatabase(int index);
+ void changeEntry(int index);
void changeGroup(int index);
- void useDefaultGroupChanged();
private:
QScopedPointer m_ui;
QSharedPointer m_selectedDatabase;
+ QUuid m_selectedDatabaseUuid;
+ QUuid m_selectedEntryUuid;
QUuid m_selectedGroupUuid;
- bool m_useDefaultGroup;
};
#endif // KEEPASSXC_PASSKEYIMPORTDIALOG_H
diff --git a/src/gui/passkeys/PasskeyImportDialog.ui b/src/gui/passkeys/PasskeyImportDialog.ui
index ffc80d141..ec8de7e06 100755
--- a/src/gui/passkeys/PasskeyImportDialog.ui
+++ b/src/gui/passkeys/PasskeyImportDialog.ui
@@ -6,10 +6,22 @@
0
0
- 405
- 227
+ 500
+ 300
+
+
+ 0
+ 0
+
+
+
+
+ 400
+ 300
+
+
KeePassXC - Passkey Import
@@ -24,7 +36,7 @@
- Do you want to import the Passkey?
+ Import the following Passkey:
Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
@@ -52,80 +64,62 @@
-
-
-
- Use default group (Imported Passkeys)
+
+
+ Qt::Vertical
-
- false
+
+
+ 20
+ 10
+
+
+
+ -
+
+
+
-
+
+
-
+
+
+ Database
+
+
+
+ -
+
+
+ -
+
+
+ Group
+
+
+
+ -
+
+
+ -
+
+
+ Entry
+
+
+
+ -
+
+
+
+
+
- -
-
-
-
-
-
- Group
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
- Database
-
-
- false
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Select Database
-
-
-
-
-
-
-
+
-
diff --git a/src/gui/passkeys/PasskeyImporter.cpp b/src/gui/passkeys/PasskeyImporter.cpp
index 0b48c102d..77e37c689 100644
--- a/src/gui/passkeys/PasskeyImporter.cpp
+++ b/src/gui/passkeys/PasskeyImporter.cpp
@@ -22,6 +22,7 @@
#include "browser/BrowserService.h"
#include "core/Entry.h"
#include "core/Group.h"
+#include "core/Tools.h"
#include "gui/FileDialog.h"
#include "gui/MessageBox.h"
#include
@@ -29,7 +30,7 @@
static const QString IMPORTED_PASSKEYS_GROUP = QStringLiteral("Imported Passkeys");
-void PasskeyImporter::importPasskey(QSharedPointer& database)
+void PasskeyImporter::importPasskey(QSharedPointer& database, Entry* entry)
{
auto filter = QString("%1 (*.passkey);;%2 (*)").arg(tr("Passkey file"), tr("All files"));
auto fileName =
@@ -47,10 +48,10 @@ void PasskeyImporter::importPasskey(QSharedPointer& database)
return;
}
- importSelectedFile(file, database);
+ importSelectedFile(file, database, entry);
}
-void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer& database)
+void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer& database, Entry* entry)
{
const auto fileData = file.readAll();
const auto passkeyObject = browserMessageBuilder()->getJsonObject(fileData);
@@ -61,18 +62,20 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer&
return;
}
- const auto relyingParty = passkeyObject["relyingParty"].toString();
- const auto url = passkeyObject["url"].toString();
- const auto username = passkeyObject["username"].toString();
- const auto credentialId = passkeyObject["credentialId"].toString();
- const auto userHandle = passkeyObject["userHandle"].toString();
const auto privateKey = passkeyObject["privateKey"].toString();
+ const auto missingKeys = Tools::getMissingValuesFromList(passkeyObject.keys(),
+ QStringList() << "relyingParty"
+ << "url"
+ << "username"
+ << "credentialId"
+ << "userHandle"
+ << "privateKey");
- if (relyingParty.isEmpty() || username.isEmpty() || credentialId.isEmpty() || userHandle.isEmpty()
- || privateKey.isEmpty()) {
+ if (!missingKeys.isEmpty()) {
MessageBox::information(nullptr,
tr("Cannot import Passkey"),
- tr("Cannot import Passkey file \"%1\". Data is missing.").arg(file.fileName()));
+ 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-----")) {
MessageBox::information(
@@ -80,7 +83,12 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer&
tr("Cannot import Passkey"),
tr("Cannot import Passkey file \"%1\". Private key is missing or malformed.").arg(file.fileName()));
} else {
- showImportDialog(database, url, relyingParty, username, credentialId, userHandle, privateKey);
+ const auto relyingParty = passkeyObject["relyingParty"].toString();
+ const auto url = passkeyObject["url"].toString();
+ const auto username = passkeyObject["username"].toString();
+ const auto credentialId = passkeyObject["credentialId"].toString();
+ const auto userHandle = passkeyObject["userHandle"].toString();
+ showImportDialog(database, url, relyingParty, username, credentialId, userHandle, privateKey, entry);
}
}
@@ -90,10 +98,11 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database,
const QString& username,
const QString& credentialId,
const QString& userHandle,
- const QString& privateKey)
+ const QString& privateKey,
+ Entry* entry)
{
PasskeyImportDialog passkeyImportDialog;
- passkeyImportDialog.setInfo(relyingParty, username, database);
+ passkeyImportDialog.setInfo(relyingParty, username, database, entry != nullptr);
auto ret = passkeyImportDialog.exec();
if (ret != QDialog::Accepted) {
@@ -105,6 +114,29 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database,
db = database;
}
+ // Store to entry if given directly
+ if (entry) {
+ browserService()->addPasskeyToEntry(
+ entry, relyingParty, relyingParty, username, credentialId, userHandle, privateKey);
+ return;
+ }
+
+ // Import to entry selected instead of creating a new one
+ if (!passkeyImportDialog.createNewEntry()) {
+ auto groupUuid = passkeyImportDialog.getSelectedGroupUuid();
+ auto group = db->rootGroup()->findGroupByUuid(groupUuid);
+
+ if (group) {
+ auto selectedEntry = group->findEntryByUuid(passkeyImportDialog.getSelectedEntryUuid());
+ if (selectedEntry) {
+ browserService()->addPasskeyToEntry(
+ selectedEntry, relyingParty, relyingParty, username, credentialId, userHandle, privateKey);
+ }
+ }
+
+ return;
+ }
+
// Group settings. Use default group "Imported Passkeys" if user did not select a specific one.
Group* group = nullptr;
@@ -123,7 +155,7 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database,
group, url, relyingParty, relyingParty, username, credentialId, userHandle, privateKey);
}
-Group* PasskeyImporter::getDefaultGroup(QSharedPointer& database)
+Group* PasskeyImporter::getDefaultGroup(QSharedPointer& database) const
{
auto defaultGroup = database->rootGroup()->findGroupByPath(IMPORTED_PASSKEYS_GROUP);
diff --git a/src/gui/passkeys/PasskeyImporter.h b/src/gui/passkeys/PasskeyImporter.h
index 093da53e2..9cc7fab48 100644
--- a/src/gui/passkeys/PasskeyImporter.h
+++ b/src/gui/passkeys/PasskeyImporter.h
@@ -21,6 +21,7 @@
#include "core/Database.h"
#include
#include
+#include
class Entry;
@@ -31,18 +32,19 @@ class PasskeyImporter : public QObject
public:
explicit PasskeyImporter() = default;
- void importPasskey(QSharedPointer& database);
+ void importPasskey(QSharedPointer& database, Entry* entry = nullptr);
private:
- void importSelectedFile(QFile& file, QSharedPointer& database);
+ void importSelectedFile(QFile& file, QSharedPointer& database, Entry* entry);
void showImportDialog(QSharedPointer& database,
const QString& url,
const QString& relyingParty,
const QString& username,
const QString& credentialId,
const QString& userHandle,
- const QString& privateKey);
- Group* getDefaultGroup(QSharedPointer& database);
+ const QString& privateKey,
+ Entry* entry);
+ Group* getDefaultGroup(QSharedPointer& database) const;
};
#endif // KEEPASSXC_PASSKEYIMPORTER_H
diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp
index aa084921e..19dbba8f4 100644
--- a/tests/TestBrowser.cpp
+++ b/tests/TestBrowser.cpp
@@ -341,8 +341,8 @@ void TestBrowser::testSearchEntriesByReference()
auto secondEntryUuid = entries[1]->uuidToHex();
auto fullReference = QString("{REF:A@I:%1}").arg(firstEntryUuid);
auto partialReference = QString("https://subdomain.{REF:A@I:%1}").arg(secondEntryUuid);
- entries[2]->attributes()->set(BrowserService::ADDITIONAL_URL, fullReference);
- entries[3]->attributes()->set(BrowserService::ADDITIONAL_URL, partialReference);
+ entries[2]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, fullReference);
+ entries[3]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, partialReference);
entries[4]->setUrl(fullReference);
entries[5]->setUrl(partialReference);
@@ -351,11 +351,13 @@ void TestBrowser::testSearchEntriesByReference()
QCOMPARE(result[0]->url(), urls[0]);
QCOMPARE(result[1]->url(), urls[1]);
QCOMPARE(result[2]->url(), urls[2]);
- QCOMPARE(result[2]->resolveMultiplePlaceholders(result[2]->attributes()->value(BrowserService::ADDITIONAL_URL)),
- urls[0]);
+ QCOMPARE(
+ result[2]->resolveMultiplePlaceholders(result[2]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
+ urls[0]);
QCOMPARE(result[3]->url(), urls[3]);
- QCOMPARE(result[3]->resolveMultiplePlaceholders(result[3]->attributes()->value(BrowserService::ADDITIONAL_URL)),
- urls[0]);
+ QCOMPARE(
+ result[3]->resolveMultiplePlaceholders(result[3]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
+ urls[0]);
QCOMPARE(result[4]->url(), fullReference);
QCOMPARE(result[4]->resolveMultiplePlaceholders(result[4]->url()), urls[0]); // Should be resolved to the main entry
QCOMPARE(result[5]->url(), partialReference);
@@ -386,7 +388,7 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs()
auto entries = createEntries(urls, root);
// Add an additional URL to the first entry
- entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://keepassxc.org");
+ entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://keepassxc.org");
auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
@@ -663,7 +665,7 @@ void TestBrowser::testBestMatchingWithAdditionalURLs()
browserSettings()->setBestMatchOnly(true);
// Add an additional URL to the first entry
- entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://test.github.com/anotherpage");
+ entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://test.github.com/anotherpage");
// The first entry should be triggered
auto result = m_browserService->searchEntries(
diff --git a/tests/TestPasskeys.cpp b/tests/TestPasskeys.cpp
index 556e287d7..4e5db2803 100644
--- a/tests/TestPasskeys.cpp
+++ b/tests/TestPasskeys.cpp
@@ -19,6 +19,9 @@
#include "browser/BrowserCbor.h"
#include "browser/BrowserMessageBuilder.h"
#include "browser/BrowserService.h"
+#include "core/Database.h"
+#include "core/Entry.h"
+#include "core/Group.h"
#include "crypto/Crypto.h"
#include
@@ -469,3 +472,27 @@ void TestPasskeys::testSetFlags()
auto discouragedResult = browserPasskeys()->setFlagsFromJson(discouragedJson);
QCOMPARE(discouragedResult, 0x01);
}
+
+void TestPasskeys::testEntry()
+{
+ Database db;
+ auto* root = db.rootGroup();
+ root->setUuid(QUuid::createUuid());
+
+ auto* group1 = new Group();
+ group1->setUuid(QUuid::createUuid());
+ group1->setParent(root);
+
+ auto* entry = new Entry();
+ entry->setGroup(root);
+
+ browserService()->addPasskeyToEntry(entry,
+ QString("example.com"),
+ QString("example.com"),
+ QString("username"),
+ QString("userId"),
+ QString("userHandle"),
+ QString("privateKey"));
+
+ QVERIFY(entry->hasPasskey());
+}
diff --git a/tests/TestPasskeys.h b/tests/TestPasskeys.h
index ef2b68c24..3d702e84a 100644
--- a/tests/TestPasskeys.h
+++ b/tests/TestPasskeys.h
@@ -43,5 +43,7 @@ private slots:
void testExtensions();
void testParseFlags();
void testSetFlags();
+
+ void testEntry();
};
#endif // KEEPASSXC_TESTPASSKEYS_H
diff --git a/tests/TestTools.cpp b/tests/TestTools.cpp
index f1cba482b..56b3e593b 100644
--- a/tests/TestTools.cpp
+++ b/tests/TestTools.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 KeePassXC Team
+ * Copyright (C) 2023 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
@@ -239,3 +239,30 @@ void TestTools::testConvertToRegex_data()
<< input << static_cast(Tools::RegexConvertOpts::WILDCARD_UNLIMITED_MATCH)
<< QString(R"(te\|st.*t\?\[5\]\^\(test\)\;\'\,\.)");
}
+
+void TestTools::testArrayContainsValues()
+{
+ const auto values = QStringList() << "first"
+ << "second"
+ << "third";
+
+ // One missing
+ const auto result1 = Tools::getMissingValuesFromList(values,
+ QStringList() << "first"
+ << "second"
+ << "none");
+ QCOMPARE(result1.length(), 1);
+ QCOMPARE(result1.first(), QString("none"));
+
+ // All found
+ const auto result2 = Tools::getMissingValuesFromList(values,
+ QStringList() << "first"
+ << "second"
+ << "third");
+ QCOMPARE(result2.length(), 0);
+
+ // None are found
+ const auto numberValues = QList({1, 2, 3, 4, 5});
+ const auto result3 = Tools::getMissingValuesFromList(numberValues, QList({6, 7, 8}));
+ QCOMPARE(result3.length(), 3);
+}
diff --git a/tests/TestTools.h b/tests/TestTools.h
index 2e8cbb8bb..377b00fdb 100644
--- a/tests/TestTools.h
+++ b/tests/TestTools.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 KeePassXC Team
+ * Copyright (C) 2023 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
@@ -35,6 +35,7 @@ private slots:
void testEscapeRegex_data();
void testConvertToRegex();
void testConvertToRegex_data();
+ void testArrayContainsValues();
};
#endif // KEEPASSX_TESTTOOLS_H