/* * Copyright (C) 2013 Francois Ferrand * Copyright (C) 2017 Sami Vänttinen * 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 * 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 . */ #include #include #include #include #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" // 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(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; } if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { return true; } return false; } bool BrowserService::openDatabase() { 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; } m_dbTabWidget->activateWindow(); 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 QString(); } bool contains = false; QMessageBox::StandardButton dialogResult = QMessageBox::No; do { bool ok = false; id = QInputDialog::getText(0, tr("KeePassXC: New key association request"), tr("You have received an association " "request for the above key.\n" "If you would like to allow it access " "to your KeePassXC database,\n" "give it a unique name to identify and accept it."), QLineEdit::Normal, QString(), &ok); if (!ok || id.isEmpty()) { return QString(); } contains = config->attributes()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); dialogResult = QMessageBox::warning(0, 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); } // No need to use KeepassHttpProtocol. Just return a JSON array. 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 pwEntriesToConfirm; QList 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 BrowserService::searchEntries(Database* db, const QString& hostname) { QList 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 BrowserService::searchEntries(const QString& text) { // Get the list of databases to search QList databases; if (BrowserSettings::searchInAllDatabases()) { const int count = m_dbTabWidget->count(); for (int i = 0; i < count; ++i) { if (DatabaseWidget* dbWidget = qobject_cast(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 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 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 BrowserService::sortEntries(QList& 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 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& 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(); } } }