diff --git a/src/browser/BrowserEntryConfig.cpp b/src/browser/BrowserEntryConfig.cpp index 90d9f107c4..47e8762a8b 100644 --- a/src/browser/BrowserEntryConfig.cpp +++ b/src/browser/BrowserEntryConfig.cpp @@ -21,7 +21,7 @@ #include "core/EntryAttributes.h" #include -static const char KEEPASSBROWSER_NAME[] = "KeePassXC-Browser Settings"; +static const char KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings"; BrowserEntryConfig::BrowserEntryConfig(QObject* parent) : QObject(parent) @@ -82,7 +82,7 @@ void BrowserEntryConfig::setRealm(const QString& realm) bool BrowserEntryConfig::load(const Entry* entry) { - QString s = entry->attributes()->value(KEEPASSBROWSER_NAME); + QString s = entry->customData()->value(KEEPASSXCBROWSER_NAME); if (s.isEmpty()) { return false; } @@ -104,5 +104,5 @@ void BrowserEntryConfig::save(Entry* entry) QVariantMap v = qo2qv(this); QJsonObject o = QJsonObject::fromVariantMap(v); QByteArray json = QJsonDocument(o).toJson(QJsonDocument::Compact); - entry->attributes()->set(KEEPASSBROWSER_NAME, json); + entry->customData()->set(KEEPASSXCBROWSER_NAME, json); } diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp index e2ed8420be..4a1201ffa1 100644 --- a/src/browser/BrowserOptionDialog.cpp +++ b/src/browser/BrowserOptionDialog.cpp @@ -24,14 +24,24 @@ #include "ui_BrowserOptionDialog.h" #include -#include +#include "gui/MessageBox.h" -BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) +BrowserOptionDialog::BrowserOptionDialog(DatabaseTabWidget* parent) : QWidget(parent) , m_ui(new Ui::BrowserOptionDialog()) + , m_customData(new CustomData(this)) + , m_customDataModel(new QStandardItemModel(this)) + , m_dbTabWidget(parent) { m_ui->setupUi(this); + m_ui->removeCustomDataButton->setEnabled(false); + m_ui->customDataTable->setModel(m_customDataModel); + connect(m_ui->customDataTable->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + SLOT(toggleRemoveButton(QItemSelection))); + connect(m_ui->removeCustomDataButton, SIGNAL(clicked()), SLOT(removeSelectedKey())); + connect(m_ui->convertToCustomData, SIGNAL(clicked()), this, SIGNAL(convertAttributesToCustomData())); connect(m_ui->removeSharedEncryptionKeys, SIGNAL(clicked()), this, SIGNAL(removeSharedEncryptionKeys())); + connect(m_ui->removeSharedEncryptionKeys, SIGNAL(clicked()), this, SLOT(updateSharedKeyList())); connect(m_ui->removeStoredPermissions, SIGNAL(clicked()), this, SIGNAL(removeStoredPermissions())); m_ui->extensionLabel->setOpenExternalLinks(true); @@ -64,6 +74,17 @@ BrowserOptionDialog::~BrowserOptionDialog() { } +CustomData* BrowserOptionDialog::customData() const +{ + // Returns the current database customData from metadata. Otherwise return an empty customData member. + if (DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget()) { + if (Database* db = dbWidget->database()) { + return db->metadata()->customData(); + } + } + return m_customData; +} + void BrowserOptionDialog::loadSettings() { auto settings = browserSettings(); @@ -97,6 +118,9 @@ void BrowserOptionDialog::loadSettings() m_ui->firefoxSupport->setChecked(settings->firefoxSupport()); m_ui->vivaldiSupport->setChecked(settings->vivaldiSupport()); + // Update the stored key list every time settings are loaded + updateModel(); + #if defined(KEEPASSXC_DIST_APPIMAGE) m_ui->supportBrowserProxy->setChecked(true); m_ui->supportBrowserProxy->setEnabled(false); @@ -150,3 +174,48 @@ void BrowserOptionDialog::showProxyLocationFileDialog() fileTypeFilter); m_ui->customProxyLocation->setText(proxyLocation); } + +void BrowserOptionDialog::removeSelectedKey() +{ + if (QMessageBox::Yes != MessageBox::question(this, + tr("Delete the selected key?"), + tr("Do you really want to delete the selected key?\n" + "This may prevent connection to the browser plugin."), + QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel)) { + return; + } + + const QItemSelectionModel* itemSelectionModel = m_ui->customDataTable->selectionModel(); + if (itemSelectionModel) { + for (const QModelIndex& index : itemSelectionModel->selectedRows(0)) { + const QString key = index.data().toString(); + customData()->remove(key); + } + updateModel(); + } +} + +void BrowserOptionDialog::toggleRemoveButton(const QItemSelection& selected) +{ + m_ui->removeCustomDataButton->setEnabled(!selected.isEmpty()); +} + +void BrowserOptionDialog::updateModel() +{ + m_customDataModel->clear(); + m_customDataModel->setHorizontalHeaderLabels({tr("Key"), tr("Value")}); + + for (const QString& key : customData()->keys()) { + m_customDataModel->appendRow(QList() + << new QStandardItem(key) + << new QStandardItem(customData()->value(key))); + } + + m_ui->removeCustomDataButton->setEnabled(false); +} + +// Updates the shared key list after the list is cleared +void BrowserOptionDialog::updateSharedKeyList() +{ + updateModel(); +} diff --git a/src/browser/BrowserOptionDialog.h b/src/browser/BrowserOptionDialog.h index a48504dfd9..d3668269eb 100644 --- a/src/browser/BrowserOptionDialog.h +++ b/src/browser/BrowserOptionDialog.h @@ -21,7 +21,11 @@ #define BROWSEROPTIONDIALOG_H #include -#include +#include +#include +#include +#include "core/CustomData.h" +#include "gui/DatabaseTabWidget.h" namespace Ui { @@ -33,9 +37,11 @@ class BrowserOptionDialog : public QWidget Q_OBJECT public: - explicit BrowserOptionDialog(QWidget* parent = nullptr); + explicit BrowserOptionDialog(DatabaseTabWidget* parent = nullptr); ~BrowserOptionDialog(); + CustomData* customData() const; + public slots: void loadSettings(); void saveSettings(); @@ -43,12 +49,21 @@ public slots: signals: void removeSharedEncryptionKeys(); void removeStoredPermissions(); + void convertAttributesToCustomData(); private slots: void showProxyLocationFileDialog(); + void removeSelectedKey(); + void toggleRemoveButton(const QItemSelection& selected); + void updateSharedKeyList(); private: + void updateModel(); QScopedPointer m_ui; + + QPointer m_customData; + QPointer m_customDataModel; + DatabaseTabWidget* const m_dbTabWidget; }; #endif // BROWSEROPTIONDIALOG_H diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserOptionDialog.ui index 24589c147f..938f7cf187 100755 --- a/src/browser/BrowserOptionDialog.ui +++ b/src/browser/BrowserOptionDialog.ui @@ -135,7 +135,7 @@ - + Qt::Vertical @@ -205,7 +205,7 @@ - + Qt::Vertical @@ -251,7 +251,76 @@ - + + + + + + 0 + 0 + + + + Move KeePassHTTP attributes to KeePassXC-Browser &custom data + + + + + + + + + Stored Keys + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + 200 + + + true + + + false + + + + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Qt::Vertical @@ -366,7 +435,7 @@ - + Qt::Vertical diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 086c150628..96caf2dd88 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -38,6 +38,9 @@ static const char KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings"; static const char ASSOCIATE_KEY_PREFIX[] = "Public Key: "; static const char KEEPASSXCBROWSER_GROUP_NAME[] = "KeePassXC-Browser Passwords"; static int KEEPASSXCBROWSER_DEFAULT_ICON = 1; +// These are for the settings and password conversion +static const char KEEPASSHTTP_NAME[] = "KeePassHttp Settings"; +static const char KEEPASSHTTP_GROUP_NAME[] = "KeePassHttp Passwords"; BrowserService::BrowserService(DatabaseTabWidget* parent) : m_dbTabWidget(parent) @@ -106,12 +109,12 @@ QString BrowserService::getDatabaseRootUuid() { Database* db = getDatabase(); if (!db) { - return QString(); + return {}; } Group* rootGroup = db->rootGroup(); if (!rootGroup) { - return QString(); + return {}; } return rootGroup->uuidToHex(); @@ -121,46 +124,16 @@ QString BrowserService::getDatabaseRecycleBinUuid() { Database* db = getDatabase(); if (!db) { - return QString(); + return {}; } Group* recycleBin = db->metadata()->recycleBin(); if (!recycleBin) { - return QString(); + return {}; } return recycleBin->uuidToHex(); } -Entry* BrowserService::getConfigEntry(bool create) -{ - Entry* entry = nullptr; - Database* db = getDatabase(); - if (!db) { - return nullptr; - } - - entry = db->resolveEntry(m_keepassBrowserUUID); - if (!entry && create) { - entry = new Entry(); - entry->setTitle(QLatin1String(KEEPASSXCBROWSER_NAME)); - entry->setUuid(m_keepassBrowserUUID); - entry->setAutoTypeEnabled(false); - entry->setGroup(db->rootGroup()); - return entry; - } - - if (entry && entry->group() == db->metadata()->recycleBin()) { - if (!create) { - return nullptr; - } else { - entry->setGroup(db->rootGroup()); - return entry; - } - } - - return entry; -} - QString BrowserService::storeKey(const QString& key) { QString id; @@ -171,8 +144,8 @@ QString BrowserService::storeKey(const QString& key) return id; } - Entry* config = getConfigEntry(true); - if (!config) { + Database* db = getDatabase(); + if (!db) { return {}; } @@ -198,7 +171,7 @@ QString BrowserService::storeKey(const QString& key) return {}; } - contains = config->attributes()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); + contains = db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); if (contains) { dialogResult = QMessageBox::warning(nullptr, tr("KeePassXC: Overwrite existing key?"), @@ -209,18 +182,18 @@ QString BrowserService::storeKey(const QString& key) } } while (contains && dialogResult == QMessageBox::No); - config->attributes()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key, true); + db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key); return id; } QString BrowserService::getKey(const QString& id) { - Entry* config = getConfigEntry(); - if (!config) { - return QString(); + Database* db = getDatabase(); + if (!db) { + return {}; } - return config->attributes()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); + return db->metadata()->customData()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); } QJsonArray BrowserService::findMatchingEntries(const QString& id, @@ -421,12 +394,9 @@ QList BrowserService::searchEntries(const QString& url, const StringPair if (Database* db = dbWidget->database()) { // Check if database is connected with KeePassXC-Browser for (const StringPair keyPair : keyList) { - Entry* entry = db->resolveEntry(m_keepassBrowserUUID); - if (entry) { - QString key = entry->attributes()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + keyPair.first); - if (!key.isEmpty() && keyPair.second == key) { - databases << db; - } + QString key = db->metadata()->customData()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + keyPair.first); + if (!key.isEmpty() && keyPair.second == key) { + databases << db; } } } @@ -459,17 +429,13 @@ void BrowserService::removeSharedEncryptionKeys() return; } - Entry* entry = getConfigEntry(); - if (!entry) { - QMessageBox::information(0, - tr("KeePassXC: Settings not available!"), - tr("The active database does not contain a settings entry."), - QMessageBox::Ok); + Database* db = getDatabase(); + if (!db) { return; } QStringList keysToRemove; - for (const QString& key : entry->attributes()->keys()) { + for (const QString& key : db->metadata()->customData()->keys()) { if (key.startsWith(ASSOCIATE_KEY_PREFIX)) { keysToRemove << key; } @@ -483,11 +449,9 @@ void BrowserService::removeSharedEncryptionKeys() return; } - entry->beginUpdate(); for (const QString& key : keysToRemove) { - entry->attributes()->remove(key); + db->metadata()->customData()->remove(key); } - entry->endUpdate(); const int count = keysToRemove.count(); QMessageBox::information(0, @@ -507,7 +471,7 @@ void BrowserService::removeStoredPermissions() return; } - Database* db = m_dbTabWidget->currentDatabaseWidget()->database(); + Database* db = getDatabase(); if (!db) { return; } @@ -523,9 +487,9 @@ void BrowserService::removeStoredPermissions() return; } - if (entry->attributes()->contains(KEEPASSXCBROWSER_NAME)) { + if (entry->customData()->contains(KEEPASSXCBROWSER_NAME)) { entry->beginUpdate(); - entry->attributes()->remove(KEEPASSXCBROWSER_NAME); + entry->customData()->remove(KEEPASSXCBROWSER_NAME); entry->endUpdate(); ++counter; } @@ -546,6 +510,80 @@ void BrowserService::removeStoredPermissions() } } +void BrowserService::convertAttributesToCustomData() +{ + if (!isDatabaseOpened()) { + QMessageBox::critical(0, tr("KeePassXC: Database locked!"), + tr("The active database is locked!\n" + "Please unlock the selected database or choose another one which is unlocked."), + QMessageBox::Ok); + return; + } + + Database* db = getDatabase(); + if (!db) { + return; + } + + QList entries = db->rootGroup()->entriesRecursive(); + QProgressDialog progress(tr("Converting attributes to custom data…"), tr("Abort"), 0, entries.count()); + progress.setWindowModality(Qt::WindowModal); + + int counter = 0; + int keyCounter = 0; + for (Entry* entry : entries) { + if (progress.wasCanceled()) { + return; + } + + if (moveSettingsToCustomData(entry, KEEPASSHTTP_NAME)) { + ++counter; + } + if (moveSettingsToCustomData(entry, KEEPASSXCBROWSER_NAME)) { + ++counter; + } + + if (entry->title() == KEEPASSHTTP_NAME || entry->title() == KEEPASSXCBROWSER_NAME) { + keyCounter += moveKeysToCustomData(entry, db); + delete entry; + } + + progress.setValue(progress.value() + 1); + } + progress.reset(); + + if (counter > 0) { + QMessageBox::information(0, tr("KeePassXC: Converted KeePassHTTP attributes"), + tr("Successfully converted attributes from %1 entry(s).\n" + "Moved %2 keys to custom data.", "").arg(counter).arg(keyCounter), + QMessageBox::Ok); + } else if (counter == 0 && keyCounter > 0) { + QMessageBox::information(0, tr("KeePassXC: Converted KeePassHTTP attributes"), + tr("Successfully moved %n keys to custom data.", "", keyCounter), + QMessageBox::Ok); + } else { + QMessageBox::information(0, tr("KeePassXC: No entry with KeePassHTTP attributes found!"), + tr("The active database does not contain an entry with KeePassHTTP attributes."), + QMessageBox::Ok); + } + + // Rename password groupName + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return; + } + + const QString keePassBrowserGroupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME); + const QString keePassHttpGroupName = QLatin1String(KEEPASSHTTP_GROUP_NAME); + + for (Group* g : rootGroup->groupsRecursive(true)) { + if (g->name() == keePassHttpGroupName) { + g->setName(keePassBrowserGroupName); + break; + } + } +} + QList BrowserService::sortEntries(QList& pwEntries, const QString& host, const QString& entryUrl) { QUrl url(entryUrl); @@ -790,6 +828,75 @@ Database* BrowserService::getDatabase() return nullptr; } +bool BrowserService::moveSettingsToCustomData(Entry* entry, const QString& name) const +{ + if (entry->attributes()->contains(name)) { + QString attr = entry->attributes()->value(name); + entry->beginUpdate(); + if (!attr.isEmpty()) { + entry->customData()->set(KEEPASSXCBROWSER_NAME, attr); + } + entry->attributes()->remove(name); + entry->endUpdate(); + return true; + } + return false; +} + +int BrowserService::moveKeysToCustomData(Entry* entry, Database* db) const +{ + int keyCounter = 0; + for (const auto& key : entry->attributes()->keys()) { + if (key.contains(ASSOCIATE_KEY_PREFIX)) { + QString publicKey = key; + publicKey.remove(ASSOCIATE_KEY_PREFIX); + + // Add key to database custom data + if (db && !db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + publicKey)) { + db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + publicKey, entry->attributes()->value(key)); + ++keyCounter; + } + } + } + + return keyCounter; +} + +bool BrowserService::checkLegacySettings() +{ + Database* db = getDatabase(); + if (!db) { + return false; + } + + bool legacySettingsFound = false; + QList entries = db->rootGroup()->entriesRecursive(); + for (const auto& e : entries) { + if ((e->attributes()->contains(KEEPASSHTTP_NAME) || e->attributes()->contains(KEEPASSXCBROWSER_NAME)) || + (e->title() == KEEPASSHTTP_NAME || e->title() == KEEPASSXCBROWSER_NAME)) { + legacySettingsFound = true; + break; + } + } + + if (!legacySettingsFound) { + return false; + } + + auto dialogResult = QMessageBox::warning(nullptr, + tr("KeePassXC: Legacy browser integration settings detected"), + tr("Legacy browser integration settings have been detected.\n" + "Do you want to transfer the settings and stored keys to custom data?\n" + "This ensures the compatibility with KeePassXC-Browser."), + QMessageBox::Yes | QMessageBox::No); + + if (dialogResult == QMessageBox::No) { + return false; + } + + return true; +} + void BrowserService::databaseLocked(DatabaseWidget* dbWidget) { if (dbWidget) { @@ -805,6 +912,10 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) m_bringToFrontRequested = false; } emit databaseUnlocked(); + + if (checkLegacySettings()) { + convertAttributesToCustomData(); + } } } diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 10c3a786e3..e82fb64792 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -56,6 +56,7 @@ class BrowserService : public QObject QList searchEntries(const QString& url, const StringPairList& keyList); void removeSharedEncryptionKeys(); void removeStoredPermissions(); + void convertAttributesToCustomData(); public slots: QJsonArray findMatchingEntries(const QString& id, @@ -102,6 +103,9 @@ public slots: bool matchUrlScheme(const QString& url); bool removeFirstDomain(QString& hostname); Database* getDatabase(); + bool moveSettingsToCustomData(Entry* entry, const QString& name) const; + int moveKeysToCustomData(Entry* entry, Database* db) const; + bool checkLegacySettings(); private: DatabaseTabWidget* const m_dbTabWidget; diff --git a/src/browser/NativeMessagingHost.cpp b/src/browser/NativeMessagingHost.cpp index fc35bbec51..d9dbb395d5 100644 --- a/src/browser/NativeMessagingHost.cpp +++ b/src/browser/NativeMessagingHost.cpp @@ -219,6 +219,12 @@ void NativeMessagingHost::removeStoredPermissions() m_browserService.removeStoredPermissions(); } +void NativeMessagingHost::convertAttributesToCustomData() +{ + QMutexLocker locker(&m_mutex); + m_browserService.convertAttributesToCustomData(); +} + void NativeMessagingHost::databaseLocked() { QJsonObject response; diff --git a/src/browser/NativeMessagingHost.h b/src/browser/NativeMessagingHost.h index fde8f051c1..1e8441a91a 100644 --- a/src/browser/NativeMessagingHost.h +++ b/src/browser/NativeMessagingHost.h @@ -40,6 +40,7 @@ class NativeMessagingHost : public NativeMessagingBase public slots: void removeSharedEncryptionKeys(); void removeStoredPermissions(); + void convertAttributesToCustomData(); signals: void quit(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 733ac01633..90c06a8734 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -62,7 +62,8 @@ class BrowserPlugin : public ISettingsPage { public: - BrowserPlugin(DatabaseTabWidget* tabWidget) + BrowserPlugin(DatabaseTabWidget* tabWidget) : + m_dbTabWidget(tabWidget) { m_nativeMessagingHost = QSharedPointer(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); } @@ -83,13 +84,17 @@ class BrowserPlugin : public ISettingsPage QWidget* createWidget() override { - BrowserOptionDialog* dlg = new BrowserOptionDialog(); + BrowserOptionDialog* dlg = new BrowserOptionDialog(m_dbTabWidget); QObject::connect(dlg, SIGNAL(removeSharedEncryptionKeys()), m_nativeMessagingHost.data(), SLOT(removeSharedEncryptionKeys())); QObject::connect( dlg, SIGNAL(removeStoredPermissions()), m_nativeMessagingHost.data(), SLOT(removeStoredPermissions())); + QObject::connect(dlg, + SIGNAL(convertAttributesToCustomData()), + m_nativeMessagingHost.data(), + SLOT(convertAttributesToCustomData())); return dlg; } @@ -108,8 +113,9 @@ class BrowserPlugin : public ISettingsPage } } -private: - QSharedPointer m_nativeMessagingHost; + private: + QSharedPointer m_nativeMessagingHost; + DatabaseTabWidget* const m_dbTabWidget; }; #endif