diff options
Diffstat (limited to 'src/browser/BrowserService.cpp')
-rw-r--r-- | src/browser/BrowserService.cpp | 744 |
1 files changed, 744 insertions, 0 deletions
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp new file mode 100644 index 000000000..fb89e8bcb --- /dev/null +++ b/src/browser/BrowserService.cpp @@ -0,0 +1,744 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <QJsonArray> +#include <QInputDialog> +#include <QProgressDialog> +#include <QMessageBox> +#include "BrowserService.h" +#include "BrowserSettings.h" +#include "BrowserEntryConfig.h" +#include "BrowserAccessControlDialog.h" +#include "core/Database.h" +#include "core/Group.h" +#include "core/EntrySearcher.h" +#include "core/Metadata.h" +#include "core/Uuid.h" +#include "core/PasswordGenerator.h" +#include "gui/MainWindow.h" + + +// de887cc3-0363-43b8-974b-5911b8816224 +static const unsigned char KEEPASSXCBROWSER_UUID_DATA[] = { + 0xde, 0x88, 0x7c, 0xc3, 0x03, 0x63, 0x43, 0xb8, + 0x97, 0x4b, 0x59, 0x11, 0xb8, 0x81, 0x62, 0x24 +}; +static const Uuid KEEPASSXCBROWSER_UUID = Uuid(QByteArray::fromRawData(reinterpret_cast<const char *>(KEEPASSXCBROWSER_UUID_DATA), sizeof(KEEPASSXCBROWSER_UUID_DATA))); +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; + +BrowserService::BrowserService(DatabaseTabWidget* parent) : + m_dbTabWidget(parent), + m_dialogActive(false) +{ + connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), this, SLOT(databaseLocked(DatabaseWidget*))); + connect(m_dbTabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), this, SLOT(databaseUnlocked(DatabaseWidget*))); + connect(m_dbTabWidget, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), this, SLOT(activateDatabaseChanged(DatabaseWidget*))); +} + +bool BrowserService::isDatabaseOpened() const +{ + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return false; + } + + return dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode; + +} + +bool BrowserService::openDatabase(bool triggerUnlock) +{ + if (!BrowserSettings::unlockDatabase()) { + return false; + } + + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return false; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + return true; + } + + if (triggerUnlock) { + KEEPASSXC_MAIN_WINDOW->bringToFront(); + } + + return false; +} + +void BrowserService::lockDatabase() +{ + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "lockDatabase", Qt::BlockingQueuedConnection); + } + + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + dbWidget->lock(); + } +} + +QString BrowserService::getDatabaseRootUuid() +{ + Database* db = getDatabase(); + if (!db) { + return QString(); + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return QString(); + } + + return rootGroup->uuid().toHex(); +} + +QString BrowserService::getDatabaseRecycleBinUuid() +{ + Database* db = getDatabase(); + if (!db) { + return QString(); + } + + Group* recycleBin = db->metadata()->recycleBin(); + if (!recycleBin) { + return QString(); + } + return recycleBin->uuid().toHex(); +} + +Entry* BrowserService::getConfigEntry(bool create) +{ + Entry* entry = nullptr; + Database* db = getDatabase(); + if (!db) { + return nullptr; + } + + entry = db->resolveEntry(KEEPASSXCBROWSER_UUID); + if (!entry && create) { + entry = new Entry(); + entry->setTitle(QLatin1String(KEEPASSXCBROWSER_NAME)); + entry->setUuid(KEEPASSXCBROWSER_UUID); + 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; + + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "storeKey", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, id), + Q_ARG(const QString&, key)); + return id; + } + + Entry* config = getConfigEntry(true); + if (!config) { + return {}; + } + + bool contains; + QMessageBox::StandardButton dialogResult = QMessageBox::No; + + do { + QInputDialog keyDialog; + keyDialog.setWindowTitle(tr("KeePassXC: New key association request")); + keyDialog.setLabelText(tr("You have received an association request for the above key.\n\n" + "If you would like to allow it access to your KeePassXC database,\n" + "give it a unique name to identify and accept it.")); + keyDialog.setOkButtonText(tr("Save and allow access")); + keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint); + keyDialog.show(); + keyDialog.activateWindow(); + keyDialog.raise(); + auto ok = keyDialog.exec(); + + id = keyDialog.textValue(); + + if (ok != QDialog::Accepted || id.isEmpty()) { + return {}; + } + + contains = config->attributes()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); + if (contains) { + dialogResult = QMessageBox::warning(nullptr, tr("KeePassXC: Overwrite existing key?"), + tr("A shared encryption key with the name \"%1\" " + "already exists.\nDo you want to overwrite it?") + .arg(id), + QMessageBox::Yes | QMessageBox::No); + } + } while (contains && dialogResult == QMessageBox::No); + + config->attributes()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key, true); + return id; +} + +QString BrowserService::getKey(const QString& id) +{ + Entry* config = getConfigEntry(); + if (!config) { + return QString(); + } + + return config->attributes()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); +} + +QJsonArray BrowserService::findMatchingEntries(const QString& id, const QString& url, const QString& submitUrl, const QString& realm) +{ + QJsonArray result; + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "findMatchingEntries", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QJsonArray, result), + Q_ARG(const QString&, id), + Q_ARG(const QString&, url), + Q_ARG(const QString&, submitUrl), + Q_ARG(const QString&, realm)); + return result; + } + + const bool alwaysAllowAccess = BrowserSettings::alwaysAllowAccess(); + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + + // Check entries for authorization + QList<Entry*> pwEntriesToConfirm; + QList<Entry*> pwEntries; + for (Entry* entry : searchEntries(url)) { + switch (checkAccess(entry, host, submitHost, realm)) { + case Denied: + continue; + + case Unknown: + if (alwaysAllowAccess) { + pwEntries.append(entry); + } else { + pwEntriesToConfirm.append(entry); + } + break; + + case Allowed: + pwEntries.append(entry); + break; + } + } + + // Confirm entries + if (confirmEntries(pwEntriesToConfirm, url, host, submitHost, realm)) { + pwEntries.append(pwEntriesToConfirm); + } + + if (pwEntries.isEmpty()) { + return QJsonArray(); + } + + // Sort results + pwEntries = sortEntries(pwEntries, host, submitUrl); + + // Fill the list + for (Entry* entry : pwEntries) { + result << prepareEntry(entry); + } + + return result; +} + +void BrowserService::addEntry(const QString&, const QString& login, const QString& password, const QString& url, const QString& submitUrl, const QString& realm) +{ + Group* group = findCreateAddEntryGroup(); + if (!group) { + return; + } + + Entry* entry = new Entry(); + entry->setUuid(Uuid::random()); + entry->setTitle(QUrl(url).host()); + entry->setUrl(url); + entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); + entry->setUsername(login); + entry->setPassword(password); + entry->setGroup(group); + + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + BrowserEntryConfig config; + config.allow(host); + + if (!submitHost.isEmpty()) { + config.allow(submitHost); + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); +} + +void BrowserService::updateEntry(const QString& id, const QString& uuid, const QString& login, const QString& password, const QString& url) +{ + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "updateEntry", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, id), + Q_ARG(const QString&, uuid), + Q_ARG(const QString&, login), + Q_ARG(const QString&, password), + Q_ARG(const QString&, url)); + } + + Database* db = getDatabase(); + if (!db) { + return; + } + + Entry* entry = db->resolveEntry(Uuid::fromHex(uuid)); + if (!entry) { + return; + } + + QString username = entry->username(); + if (username.isEmpty()) { + return; + } + + if (username.compare(login, Qt::CaseSensitive) != 0 || entry->password().compare(password, Qt::CaseSensitive) != 0) { + QMessageBox::StandardButton dialogResult = QMessageBox::No; + if (!BrowserSettings::alwaysAllowUpdate()) { + dialogResult = QMessageBox::warning(0, tr("KeePassXC: Update Entry"), + tr("Do you want to update the information in %1 - %2?") + .arg(QUrl(url).host()).arg(username), + QMessageBox::Yes|QMessageBox::No); + } + + if (BrowserSettings::alwaysAllowUpdate() || dialogResult == QMessageBox::Yes) { + entry->beginUpdate(); + entry->setUsername(login); + entry->setPassword(password); + entry->endUpdate(); + } + } +} + +QList<Entry*> BrowserService::searchEntries(Database* db, const QString& hostname) +{ + QList<Entry*> entries; + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return entries; + } + + for (Entry* entry : EntrySearcher().search(hostname, rootGroup, Qt::CaseInsensitive)) { + QString title = entry->title(); + QString url = entry->url(); + + // Filter to match hostname in Title and Url fields + if ((!title.isEmpty() && hostname.contains(title)) + || (!url.isEmpty() && hostname.contains(url)) + || (matchUrlScheme(title) && hostname.endsWith(QUrl(title).host())) + || (matchUrlScheme(url) && hostname.endsWith(QUrl(url).host())) ) { + entries.append(entry); + } + } + + return entries; +} + +QList<Entry*> BrowserService::searchEntries(const QString& text) +{ + // Get the list of databases to search + QList<Database*> databases; + if (BrowserSettings::searchInAllDatabases()) { + const int count = m_dbTabWidget->count(); + for (int i = 0; i < count; ++i) { + if (DatabaseWidget* dbWidget = qobject_cast<DatabaseWidget*>(m_dbTabWidget->widget(i))) { + if (Database* db = dbWidget->database()) { + databases << db; + } + } + } + } else if (Database* db = getDatabase()) { + databases << db; + } + + // Search entries matching the hostname + QString hostname = QUrl(text).host(); + QList<Entry*> entries; + do { + for (Database* db : databases) { + entries << searchEntries(db, hostname); + } + } while (entries.isEmpty() && removeFirstDomain(hostname)); + + return entries; +} + +void BrowserService::removeSharedEncryptionKeys() +{ + 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; + } + + 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); + return; + } + + QStringList keysToRemove; + for (const QString& key : entry->attributes()->keys()) { + if (key.startsWith(ASSOCIATE_KEY_PREFIX)) { + keysToRemove << key; + } + } + + if (keysToRemove.isEmpty()) { + QMessageBox::information(0, tr("KeePassXC: No keys found"), + tr("No shared encryption keys found in KeePassXC Settings."), + QMessageBox::Ok); + return; + } + + entry->beginUpdate(); + for (const QString& key : keysToRemove) { + entry->attributes()->remove(key); + } + entry->endUpdate(); + + const int count = keysToRemove.count(); + QMessageBox::information(0, tr("KeePassXC: Removed keys from database"), + tr("Successfully removed %n encryption key(s) from KeePassXC settings.", "", count), + QMessageBox::Ok); + +} + +void BrowserService::removeStoredPermissions() +{ + 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 = m_dbTabWidget->currentDatabaseWidget()->database(); + if (!db) { + return; + } + + QList<Entry*> entries = db->rootGroup()->entriesRecursive(); + + QProgressDialog progress(tr("Removing stored permissions…"), tr("Abort"), 0, entries.count()); + progress.setWindowModality(Qt::WindowModal); + + uint counter = 0; + for (Entry* entry : entries) { + if (progress.wasCanceled()) { + return; + } + + if (entry->attributes()->contains(KEEPASSXCBROWSER_NAME)) { + entry->beginUpdate(); + entry->attributes()->remove(KEEPASSXCBROWSER_NAME); + entry->endUpdate(); + ++counter; + } + progress.setValue(progress.value() + 1); + } + progress.reset(); + + if (counter > 0) { + QMessageBox::information(0, tr("KeePassXC: Removed permissions"), + tr("Successfully removed permissions from %n entry(s).", "", counter), + QMessageBox::Ok); + } else { + QMessageBox::information(0, tr("KeePassXC: No entry with permissions found!"), + tr("The active database does not contain an entry with permissions."), + QMessageBox::Ok); + } +} + +QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& entryUrl) +{ + QUrl url(entryUrl); + if (url.scheme().isEmpty()) { + url.setScheme("http"); + } + + const QString submitUrl = url.toString(QUrl::StripTrailingSlash); + const QString baseSubmitUrl = url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + + QMultiMap<int, const Entry*> priorities; + for (const Entry* entry : pwEntries) { + priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry); + } + + QString field = BrowserSettings::sortByTitle() ? "Title" : "UserName"; + std::sort(pwEntries.begin(), pwEntries.end(), [&priorities, &field](const Entry* left, const Entry* right) { + int res = priorities.key(left) - priorities.key(right); + if (res == 0) { + return QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field)) < 0; + } + return res < 0; + }); + + return pwEntries; +} + +bool BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, const QString& url, const QString& host, const QString& submitHost, const QString& realm) +{ + if (pwEntriesToConfirm.isEmpty() || m_dialogActive) { + return false; + } + + m_dialogActive = true; + BrowserAccessControlDialog accessControlDialog; + accessControlDialog.setUrl(url); + accessControlDialog.setItems(pwEntriesToConfirm); + + int res = accessControlDialog.exec(); + if (accessControlDialog.remember()) { + for (Entry* entry : pwEntriesToConfirm) { + BrowserEntryConfig config; + config.load(entry); + if (res == QDialog::Accepted) { + config.allow(host); + if (!submitHost.isEmpty() && host != submitHost) + config.allow(submitHost); + } else if (res == QDialog::Rejected) { + config.deny(host); + if (!submitHost.isEmpty() && host != submitHost) { + config.deny(submitHost); + } + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); + } + } + + m_dialogActive = false; + if (res == QDialog::Accepted) { + return true; + } + + return false; +} + +QJsonObject BrowserService::prepareEntry(const Entry* entry) +{ + QJsonObject res; + res["login"] = entry->resolveMultiplePlaceholders(entry->username()); + res["password"] = entry->resolveMultiplePlaceholders(entry->password()); + res["name"] = entry->resolveMultiplePlaceholders(entry->title()); + res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuid().toHex()); + + if (BrowserSettings::supportKphFields()) { + const EntryAttributes* attr = entry->attributes(); + QJsonArray stringFields; + for (const QString& key : attr->keys()) { + if (key.startsWith(QLatin1String("KPH: "))) { + QJsonObject sField; + sField[key] = entry->resolveMultiplePlaceholders(attr->value(key)); + stringFields << sField; + } + } + res["stringFields"] = stringFields; + } + return res; +} + +BrowserService::Access BrowserService::checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm) +{ + BrowserEntryConfig config; + if (!config.load(entry)) { + return Unknown; + } + if ((config.isAllowed(host)) && (submitHost.isEmpty() || config.isAllowed(submitHost))) { + return Allowed; + } + if ((config.isDenied(host)) || (!submitHost.isEmpty() && config.isDenied(submitHost))) { + return Denied; + } + if (!realm.isEmpty() && config.realm() != realm) { + return Denied; + } + return Unknown; +} + +Group* BrowserService::findCreateAddEntryGroup() +{ + Database* db = getDatabase(); + if (!db) { + return nullptr; + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return nullptr; + } + + const QString groupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME); //TODO: setting to decide where new keys are created + + for (const Group* g : rootGroup->groupsRecursive(true)) { + if (g->name() == groupName) { + return db->resolveGroup(g->uuid()); + } + } + + Group* group = new Group(); + group->setUuid(Uuid::random()); + group->setName(groupName); + group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); + group->setParent(rootGroup); + return group; +} + +int BrowserService::sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const +{ + QUrl url(entry->url()); + if (url.scheme().isEmpty()) { + url.setScheme("http"); + } + const QString entryURL = url.toString(QUrl::StripTrailingSlash); + const QString baseEntryURL = url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (submitUrl == entryURL) { + return 100; + } + if (submitUrl.startsWith(entryURL) && entryURL != host && baseSubmitUrl != entryURL) { + return 90; + } + if (submitUrl.startsWith(baseEntryURL) && entryURL != host && baseSubmitUrl != baseEntryURL) { + return 80; + } + if (entryURL == host) { + return 70; + } + if (entryURL == baseSubmitUrl) { + return 60; + } + if (entryURL.startsWith(submitUrl)) { + return 50; + } + if (entryURL.startsWith(baseSubmitUrl) && baseSubmitUrl != host) { + return 40; + } + if (submitUrl.startsWith(entryURL)) { + return 30; + } + if (submitUrl.startsWith(baseEntryURL)) { + return 20; + } + if (entryURL.startsWith(host)) { + return 10; + } + if (host.startsWith(entryURL)) { + return 5; + } + return 0; +} + +bool BrowserService::matchUrlScheme(const QString& url) +{ + QUrl address(url); + return !address.scheme().isEmpty(); +} + +bool BrowserService::removeFirstDomain(QString& hostname) +{ + int pos = hostname.indexOf("."); + if (pos < 0) { + return false; + } + + // Don't remove the second-level domain if it's the only one + if (hostname.count(".") > 1) { + hostname = hostname.mid(pos + 1); + return !hostname.isEmpty(); + } + + // Nothing removed + return false; +} + +Database* BrowserService::getDatabase() +{ + if (DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget()) { + if (Database* db = dbWidget->database()) { + return db; + } + } + return nullptr; +} + +void BrowserService::databaseLocked(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + emit databaseLocked(); + } +} + +void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + emit databaseUnlocked(); + } +} + +void BrowserService::activateDatabaseChanged(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + auto currentMode = dbWidget->currentMode(); + if (currentMode == DatabaseWidget::ViewMode || currentMode == DatabaseWidget::EditMode) { + emit databaseUnlocked(); + } else { + emit databaseLocked(); + } + } +} |