diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 52237dd2a1..0b0239b16d 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -959,6 +959,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? + + BrowserSettingsWidget @@ -6040,6 +6049,14 @@ Do you want to overwrite it? Group: + + Import to entry + + + + Do you want to import the Passkey to this entry? + + PasskeyImporter diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 0a2ec6eaf3..43a46e2e06 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -773,6 +773,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); @@ -1289,8 +1303,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; } } @@ -1417,14 +1430,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 01daaee852..137cb05da8 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -85,7 +85,9 @@ class BrowserService : public QObject void showPasswordGenerator(const KeyPairMessage& keyPairMessage); bool isPasswordGeneratorRequested() const; bool isUrlIdentical(const QString& first, const QString& second) 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); @@ -192,7 +194,6 @@ private slots: 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 7e238f0494..1bfa3cb36e 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -545,6 +545,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 88f878e98a..3a03892d0a 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -121,6 +121,7 @@ class Entry : public ModifiableObject 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 13207e1688..4693323988 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -52,6 +52,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 2e7f8c05c0..0b029ec965 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 @@ class EntryAttributes : public ModifiableObject 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; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 4454266339..73a61fac39 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -564,7 +564,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 6b4b121af7..8f27038786 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 d42292d348..b20f5a240b 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1407,10 +1407,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 4f79ebd2f8..1102245bde 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 27dada98d6..a41f631e4a 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 e6fbfc22ca..a0b8d6e687 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/passkeys/PasskeyImportDialog.cpp b/src/gui/passkeys/PasskeyImportDialog.cpp index 2d54ba5bad..c7237711a7 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,61 +69,106 @@ 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() { - m_ui->selectGroupComboBox->clear(); - for (const auto& group : database->rootGroup()->groupsRecursive(true)) { - if (!group || group->isRecycled() || group == database->metadata()->recycleBin()) { + 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; + } + + for (const auto& entry : group->entries()) { + if (!entry || entry->isRecycled()) { continue; } - m_ui->selectGroupComboBox->addItem(group->fullPath(), group->uuid()); + m_ui->selectEntryComboBox->addItem(entry->title(), entry->uuid()); } } -void PasskeyImportDialog::selectDatabase() +void PasskeyImportDialog::addGroups() { - auto selectedDatabase = browserService()->selectedDatabase(); - if (!selectedDatabase) { + if (!m_selectedDatabase) { return; } - m_selectedDatabase = selectedDatabase; - m_ui->selectDatabaseLabel->setText(QString("Database: %1").arg(getDatabaseName(m_selectedDatabase))); + m_ui->selectGroupComboBox->clear(); + 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; + } + + m_ui->selectGroupComboBox->addItem(group->fullPath(), group->uuid()); + } +} - addGroups(m_selectedDatabase); +void PasskeyImportDialog::changeDatabase(int index) +{ + m_selectedDatabaseUuid = m_ui->selectDatabaseCombobBox->itemData(index).value(); + m_selectedDatabase = browserService()->getDatabase(m_selectedDatabaseUuid); + emit updateGroups(); } -void PasskeyImportDialog::changeGroup(int index) +void PasskeyImportDialog::changeEntry(int index) { - m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value(); + m_selectedEntryUuid = m_ui->selectEntryComboBox->itemData(index).value(); } -void PasskeyImportDialog::useDefaultGroupChanged() +void PasskeyImportDialog::changeGroup(int index) { - m_ui->selectGroupComboBox->setEnabled(!m_ui->useDefaultGroupCheckbox->isChecked()); - m_useDefaultGroup = m_ui->useDefaultGroupCheckbox->isChecked(); + m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value(); + emit updateEntries(); } diff --git a/src/gui/passkeys/PasskeyImportDialog.h b/src/gui/passkeys/PasskeyImportDialog.h index 7b316721e7..705c6d187f 100644 --- a/src/gui/passkeys/PasskeyImportDialog.h +++ b/src/gui/passkeys/PasskeyImportDialog.h @@ -36,25 +36,33 @@ class PasskeyImportDialog : public QDialog 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 ffc80d1419..ec8de7e062 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 + - - - - - - - - Group - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - + - - - - - Database - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Select Database - - - - + + + + + + + + Database + + + + + + + + + + Group + + + + + + + + + + Entry + + + + + + + + + + - + diff --git a/src/gui/passkeys/PasskeyImporter.cpp b/src/gui/passkeys/PasskeyImporter.cpp index 103b1df4e7..a8bbe12325 100644 --- a/src/gui/passkeys/PasskeyImporter.cpp +++ b/src/gui/passkeys/PasskeyImporter.cpp @@ -29,7 +29,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 +47,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); @@ -80,7 +80,7 @@ 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, password, userHandle, privateKey); + showImportDialog(database, url, relyingParty, username, password, userHandle, privateKey, entry); } } @@ -90,10 +90,11 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database, const QString& username, const QString& userId, 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 +106,29 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database, db = database; } + // Store to entry if given directly + if (entry) { + browserService()->addPasskeyToEntry( + entry, relyingParty, relyingParty, username, userId, 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, userId, userHandle, privateKey); + } + } + + return; + } + // Group settings. Use default group "Imported Passkeys" if user did not select a specific one. Group* group = nullptr; @@ -123,7 +147,7 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database, group, url, relyingParty, relyingParty, username, userId, 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 c1523cbc18..5babee226f 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& userId, 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/TestPasskeys.cpp b/tests/TestPasskeys.cpp index 556e287d70..4e5db2803b 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 ef2b68c24c..3d702e84a5 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