From a145bf91191f0a4630a7e31654aff8a8dfd09bf0 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 10 May 2020 21:20:00 -0400 Subject: [PATCH] Complete refactor of Browser Integration classes * Removed option to attach KeePassXC to the browser extension. Users must use the proxy application to communicate with KeePassXC. * Significantly streamlined proxy code. Used same implementation of stdin/stdout interface across all platforms. * Moved browser service entry point to BrowserService class instead of NativeMessagingHost. BrowserService now coordinates the communication to/from clients. * Moved settings page definition out of MainWindow * Decoupled BrowserService from DatabaseTabWidget * Reduced complexity of various functions and cleaned the ABI (public vs private). * Eliminated BrowserClients class, moved functionality into the BrowserService * Renamed HostInstaller to NativeMessageInstaller and renamed NativeMessageHost to BrowserHost. * Recognize XDG_CONFIG_HOME when installing native message file on Linux. Fix #4121 and fix #4123. --- src/CMakeLists.txt | 1 - src/browser/BrowserAction.cpp | 123 +++--- src/browser/BrowserAction.h | 44 +-- src/browser/BrowserClients.cpp | 77 ---- src/browser/BrowserClients.h | 61 --- src/browser/BrowserHost.cpp | 107 ++++++ .../BrowserHost.h} | 40 +- src/browser/BrowserService.cpp | 286 ++++++-------- src/browser/BrowserService.h | 105 ++--- src/browser/BrowserSettings.cpp | 103 +---- src/browser/BrowserSettings.h | 28 +- src/browser/BrowserSettingsPage.cpp | 49 +++ src/browser/BrowserSettingsPage.h | 36 ++ ...onDialog.cpp => BrowserSettingsWidget.cpp} | 98 +++-- ...OptionDialog.h => BrowserSettingsWidget.h} | 21 +- ...tionDialog.ui => BrowserSettingsWidget.ui} | 24 +- src/browser/BrowserShared.cpp | 46 +++ src/browser/BrowserShared.h | 42 ++ src/browser/CMakeLists.txt | 10 +- src/browser/HostInstaller.cpp | 359 ------------------ src/browser/HostInstaller.h | 75 ---- src/browser/NativeMessageInstaller.cpp | 313 +++++++++++++++ src/browser/NativeMessageInstaller.h | 46 +++ src/browser/NativeMessagingBase.cpp | 152 -------- src/browser/NativeMessagingBase.h | 68 ---- src/browser/NativeMessagingHost.cpp | 222 ----------- src/browser/NativeMessagingHost.h | 63 --- src/gui/MainWindow.cpp | 84 ++-- src/gui/MainWindow.h | 2 + .../DatabaseSettingsWidgetBrowser.cpp | 3 +- .../DatabaseSettingsWidgetBrowser.h | 1 - src/proxy/CMakeLists.txt | 22 +- src/proxy/NativeMessagingHost.cpp | 133 ------- src/proxy/NativeMessagingProxy.cpp | 110 ++++++ src/proxy/NativeMessagingProxy.h | 53 +++ src/proxy/keepassxc-proxy.cpp | 5 +- src/sshagent/AgentSettingsPage.cpp | 9 - src/sshagent/AgentSettingsPage.h | 4 +- tests/TestBrowser.cpp | 66 +--- tests/TestBrowser.h | 5 +- tests/data/NewDatabaseBrowser.kdbx | Bin 16743 -> 17463 bytes tests/gui/TestGuiBrowser.cpp | 38 +- tests/gui/TestGuiBrowser.h | 4 +- 43 files changed, 1220 insertions(+), 1918 deletions(-) delete mode 100644 src/browser/BrowserClients.cpp delete mode 100644 src/browser/BrowserClients.h create mode 100644 src/browser/BrowserHost.cpp rename src/{proxy/NativeMessagingHost.h => browser/BrowserHost.h} (56%) create mode 100644 src/browser/BrowserSettingsPage.cpp create mode 100644 src/browser/BrowserSettingsPage.h rename src/browser/{BrowserOptionDialog.cpp => BrowserSettingsWidget.cpp} (71%) rename src/browser/{BrowserOptionDialog.h => BrowserSettingsWidget.h} (65%) rename src/browser/{BrowserOptionDialog.ui => BrowserSettingsWidget.ui} (94%) mode change 100755 => 100644 create mode 100644 src/browser/BrowserShared.cpp create mode 100644 src/browser/BrowserShared.h delete mode 100644 src/browser/HostInstaller.cpp delete mode 100644 src/browser/HostInstaller.h create mode 100644 src/browser/NativeMessageInstaller.cpp create mode 100644 src/browser/NativeMessageInstaller.h delete mode 100644 src/browser/NativeMessagingBase.cpp delete mode 100644 src/browser/NativeMessagingBase.h delete mode 100644 src/browser/NativeMessagingHost.cpp delete mode 100644 src/browser/NativeMessagingHost.h delete mode 100644 src/proxy/NativeMessagingHost.cpp create mode 100644 src/proxy/NativeMessagingProxy.cpp create mode 100644 src/proxy/NativeMessagingProxy.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e57973ae82..c5fa3a1aca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -233,7 +233,6 @@ if(APPLE) add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration") endif() -set(BROWSER_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/browser) add_subdirectory(browser) add_subdirectory(proxy) if(WITH_XC_BROWSER) diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index fec5b985a8..361dc2a9c6 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -1,6 +1,5 @@ /* - * Copyright (C) 2017 Sami Vänttinen - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -17,9 +16,11 @@ */ #include "BrowserAction.h" +#include "BrowserService.h" #include "BrowserSettings.h" -#include "NativeMessagingBase.h" +#include "BrowserShared.h" #include "config-keepassx.h" +#include "core/Global.h" #include #include @@ -27,14 +28,31 @@ #include #include -BrowserAction::BrowserAction(BrowserService& browserService) - : m_mutex(QMutex::Recursive) - , m_browserService(browserService) - , m_associated(false) +namespace { + enum + { + ERROR_KEEPASS_DATABASE_NOT_OPENED = 1, + ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED = 2, + ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED = 3, + ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE = 4, + ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED = 5, + ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED = 6, + ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE = 7, + ERROR_KEEPASS_ASSOCIATION_FAILED = 8, + ERROR_KEEPASS_KEY_CHANGE_FAILED = 9, + ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED = 10, + ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND = 11, + ERROR_KEEPASS_INCORRECT_ACTION = 12, + ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED = 13, + ERROR_KEEPASS_NO_URL_PROVIDED = 14, + ERROR_KEEPASS_NO_LOGINS_FOUND = 15, + ERROR_KEEPASS_NO_GROUPS_FOUND = 16, + ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP = 17 + }; } -QJsonObject BrowserAction::readResponse(const QJsonObject& json) +QJsonObject BrowserAction::processClientMessage(const QJsonObject& json) { if (json.isEmpty()) { return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED); @@ -51,11 +69,10 @@ QJsonObject BrowserAction::readResponse(const QJsonObject& json) return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); } - QMutexLocker locker(&m_mutex); - if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !m_browserService.isDatabaseOpened()) { + if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !browserService()->isDatabaseOpened()) { if (m_clientPublicKey.isEmpty()) { return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); - } else if (!m_browserService.openDatabase(triggerUnlock)) { + } else if (!browserService()->openDatabase(triggerUnlock)) { return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); } } @@ -98,7 +115,6 @@ QJsonObject BrowserAction::handleAction(const QJsonObject& json) QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QString& action) { - QMutexLocker locker(&m_mutex); const QString nonce = json.value("nonce").toString(); const QString clientPublicKey = json.value("publicKey").toString(); @@ -130,7 +146,7 @@ QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); const QJsonObject decrypted = decryptMessage(encrypted, nonce); @@ -153,7 +169,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const // Update a legacy database hash if found const QJsonArray hashes = decrypted.value("connectedKeys").toArray(); if (!hashes.isEmpty()) { - const QString legacyHash = getLegacyDatabaseHash(); + const QString legacyHash = browserService()->getDatabaseHash(true); if (hashes.contains(legacyHash)) { message["oldHash"] = legacyHash; } @@ -167,7 +183,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); const QJsonObject decrypted = decryptMessage(encrypted, nonce); @@ -181,12 +197,11 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); } - QMutexLocker locker(&m_mutex); if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) { // Check for identification key. If it's not found, ensure backwards compatibility and use the current public // key const QString idKey = decrypted.value("idKey").toString(); - const QString id = m_browserService.storeKey((idKey.isEmpty() ? key : idKey)); + const QString id = browserService()->storeKey((idKey.isEmpty() ? key : idKey)); if (id.isEmpty()) { return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); } @@ -205,7 +220,7 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); const QJsonObject decrypted = decryptMessage(encrypted, nonce); @@ -220,8 +235,7 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); } - QMutexLocker locker(&m_mutex); - const QString key = m_browserService.getKey(id); + const QString key = browserService()->getKey(id); if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) { return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); } @@ -238,11 +252,10 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); - QMutexLocker locker(&m_mutex); if (!m_associated) { return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); } @@ -269,7 +282,7 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin const QString submit = decrypted.value("submitUrl").toString(); const QString auth = decrypted.value("httpAuth").toString(); const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0 ? true : false; - const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, "", keyList, httpAuth); + const QJsonArray users = browserService()->findMatchingEntries(id, url, submit, "", keyList, httpAuth); if (users.isEmpty()) { return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND); @@ -311,11 +324,10 @@ QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); - QMutexLocker locker(&m_mutex); if (!m_associated) { return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); } @@ -339,11 +351,11 @@ QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString const QString groupUuid = decrypted.value("groupUuid").toString(); const QString realm; - BrowserService::ReturnValue result = BrowserService::ReturnValue::Success; + bool result = true; if (uuid.isEmpty()) { - m_browserService.addEntry(id, login, password, url, submitUrl, realm, group, groupUuid); + browserService()->addEntry(id, login, password, url, submitUrl, realm, group, groupUuid); } else { - result = m_browserService.updateEntry(id, uuid, login, password, url, submitUrl); + result = browserService()->updateEntry(id, uuid, login, password, url, submitUrl); } const QString newNonce = incrementNonce(nonce); @@ -351,7 +363,7 @@ QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString QJsonObject message = buildMessage(newNonce); message["count"] = QJsonValue::Null; message["entries"] = QJsonValue::Null; - message["error"] = getReturnValue(result); + message["error"] = result ? QStringLiteral("success") : QStringLiteral("error"); message["hash"] = hash; return buildResponse(action, message, newNonce); @@ -359,7 +371,7 @@ QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); const QJsonObject decrypted = decryptMessage(encrypted, nonce); @@ -374,8 +386,7 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt QString command = decrypted.value("action").toString(); if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) { - QMutexLocker locker(&m_mutex); - m_browserService.lockDatabase(); + browserService()->lockDatabase(); const QString newNonce = incrementNonce(nonce); QJsonObject message = buildMessage(newNonce); @@ -388,11 +399,10 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); - QMutexLocker locker(&m_mutex); if (!m_associated) { return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); } @@ -407,7 +417,7 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); } - const QJsonObject groups = m_browserService.getDatabaseGroups(); + const QJsonObject groups = browserService()->getDatabaseGroups(); if (groups.isEmpty()) { return getErrorReply(action, ERROR_KEEPASS_NO_GROUPS_FOUND); } @@ -422,11 +432,10 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const QString& action) { - const QString hash = getDatabaseHash(); + const QString hash = browserService()->getDatabaseHash(); const QString nonce = json.value("nonce").toString(); const QString encrypted = json.value("message").toString(); - QMutexLocker locker(&m_mutex); if (!m_associated) { return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); } @@ -442,7 +451,7 @@ QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const Q } QString group = decrypted.value("groupName").toString(); - const QJsonObject newGroup = m_browserService.createNewGroup(group); + const QJsonObject newGroup = browserService()->createNewGroup(group); if (newGroup.isEmpty() || newGroup["name"].toString().isEmpty() || newGroup["uuid"].toString().isEmpty()) { return getErrorReply(action, ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP); } @@ -524,38 +533,6 @@ QString BrowserAction::getErrorMessage(const int errorCode) const } } -QString BrowserAction::getReturnValue(const BrowserService::ReturnValue returnValue) const -{ - switch (returnValue) { - case BrowserService::ReturnValue::Success: - return QString("success"); - case BrowserService::ReturnValue::Error: - return QString("error"); - case BrowserService::ReturnValue::Canceled: - return QString("canceled"); - } - return QString("error"); -} - -QString BrowserAction::getDatabaseHash() -{ - QMutexLocker locker(&m_mutex); - QByteArray hash = - QCryptographicHash::hash(m_browserService.getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex(); - return QString(hash); -} - -QString BrowserAction::getLegacyDatabaseHash() -{ - QMutexLocker locker(&m_mutex); - QByteArray hash = - QCryptographicHash::hash( - (m_browserService.getDatabaseRootUuid() + m_browserService.getDatabaseRecycleBinUuid()).toUtf8(), - QCryptographicHash::Sha256) - .toHex(); - return QString(hash); -} - QString BrowserAction::encryptMessage(const QJsonObject& message, const QString& nonce) { if (message.isEmpty() || nonce.isEmpty()) { @@ -586,7 +563,6 @@ QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce) { - QMutexLocker locker(&m_mutex); const QByteArray ma = plaintext.toUtf8(); const QByteArray na = base64Decode(nonce); const QByteArray ca = base64Decode(m_clientPublicKey); @@ -598,7 +574,7 @@ QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce) std::vector sk(sa.cbegin(), sa.cend()); std::vector e; - e.resize(NATIVE_MSG_MAX_LENGTH); + e.resize(BrowserShared::NATIVEMSG_MAX_LENGTH); if (m.empty() || n.empty() || ck.empty() || sk.empty()) { return QString(); @@ -614,7 +590,6 @@ QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce) QByteArray BrowserAction::decrypt(const QString& encrypted, const QString& nonce) { - QMutexLocker locker(&m_mutex); const QByteArray ma = base64Decode(encrypted); const QByteArray na = base64Decode(nonce); const QByteArray ca = base64Decode(m_clientPublicKey); @@ -626,7 +601,7 @@ QByteArray BrowserAction::decrypt(const QString& encrypted, const QString& nonce std::vector sk(sa.cbegin(), sa.cend()); std::vector d; - d.resize(NATIVE_MSG_MAX_LENGTH); + d.resize(BrowserShared::NATIVEMSG_MAX_LENGTH); if (m.empty() || n.empty() || ck.empty() || sk.empty()) { return QByteArray(); diff --git a/src/browser/BrowserAction.h b/src/browser/BrowserAction.h index a8af0915e2..c65409dd81 100644 --- a/src/browser/BrowserAction.h +++ b/src/browser/BrowserAction.h @@ -1,6 +1,5 @@ /* - * Copyright (C) 2017 Sami Vänttinen - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -19,42 +18,16 @@ #ifndef BROWSERACTION_H #define BROWSERACTION_H -#include "BrowserService.h" #include -#include -#include -#include +#include -class BrowserAction : public QObject +class BrowserAction { - Q_OBJECT - - enum - { - ERROR_KEEPASS_DATABASE_NOT_OPENED = 1, - ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED = 2, - ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED = 3, - ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE = 4, - ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED = 5, - ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED = 6, - ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE = 7, - ERROR_KEEPASS_ASSOCIATION_FAILED = 8, - ERROR_KEEPASS_KEY_CHANGE_FAILED = 9, - ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED = 10, - ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND = 11, - ERROR_KEEPASS_INCORRECT_ACTION = 12, - ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED = 13, - ERROR_KEEPASS_NO_URL_PROVIDED = 14, - ERROR_KEEPASS_NO_LOGINS_FOUND = 15, - ERROR_KEEPASS_NO_GROUPS_FOUND = 16, - ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP = 17 - }; - public: - BrowserAction(BrowserService& browserService); + explicit BrowserAction() = default; ~BrowserAction() = default; - QJsonObject readResponse(const QJsonObject& json); + QJsonObject processClientMessage(const QJsonObject& json); private: QJsonObject handleAction(const QJsonObject& json); @@ -73,9 +46,6 @@ class BrowserAction : public QObject QJsonObject buildResponse(const QString& action, const QJsonObject& message, const QString& nonce); QJsonObject getErrorReply(const QString& action, const int errorCode) const; QString getErrorMessage(const int errorCode) const; - QString getReturnValue(const BrowserService::ReturnValue returnValue) const; - QString getDatabaseHash(); - QString getLegacyDatabaseHash(); QString encryptMessage(const QJsonObject& message, const QString& nonce); QJsonObject decryptMessage(const QString& message, const QString& nonce); @@ -90,12 +60,10 @@ class BrowserAction : public QObject QString incrementNonce(const QString& nonce); private: - QMutex m_mutex; - BrowserService& m_browserService; QString m_clientPublicKey; QString m_publicKey; QString m_secretKey; - bool m_associated; + bool m_associated = false; friend class TestBrowser; }; diff --git a/src/browser/BrowserClients.cpp b/src/browser/BrowserClients.cpp deleted file mode 100644 index 083df39459..0000000000 --- a/src/browser/BrowserClients.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 "BrowserClients.h" -#include -#include - -BrowserClients::BrowserClients(BrowserService& browserService) - : m_mutex(QMutex::Recursive) - , m_browserService(browserService) -{ - m_clients.reserve(1000); -} - -QJsonObject BrowserClients::readResponse(const QByteArray& arr) -{ - QJsonObject json; - const QJsonObject message = byteArrayToJson(arr); - const QString clientID = getClientID(message); - - if (!clientID.isEmpty()) { - const ClientPtr client = getClient(clientID); - if (client->browserAction) { - json = client->browserAction->readResponse(message); - } - } - - return json; -} - -QJsonObject BrowserClients::byteArrayToJson(const QByteArray& arr) const -{ - QJsonObject json; - QJsonParseError err; - QJsonDocument doc(QJsonDocument::fromJson(arr, &err)); - if (doc.isObject()) { - json = doc.object(); - } - - return json; -} - -QString BrowserClients::getClientID(const QJsonObject& json) const -{ - return json["clientID"].toString(); -} - -BrowserClients::ClientPtr BrowserClients::getClient(const QString& clientID) -{ - QMutexLocker locker(&m_mutex); - for (const auto& i : m_clients) { - if (i->clientID.compare(clientID, Qt::CaseSensitive) == 0) { - return i; - } - } - - // clientID not found, create a new client - QSharedPointer ba = QSharedPointer::create(m_browserService); - ClientPtr client = ClientPtr::create(clientID, ba); - m_clients.push_back(client); - return m_clients.back(); -} diff --git a/src/browser/BrowserClients.h b/src/browser/BrowserClients.h deleted file mode 100644 index 1fa3dfe17e..0000000000 --- a/src/browser/BrowserClients.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 . - */ - -#ifndef BROWSERCLIENTS_H -#define BROWSERCLIENTS_H - -#include "BrowserAction.h" -#include -#include -#include -#include -#include - -class BrowserClients -{ - struct Client - { - Client(const QString& id, QSharedPointer ba) - : clientID(id) - , browserAction(ba) - { - } - QString clientID; - QSharedPointer browserAction; - }; - - typedef QSharedPointer ClientPtr; - -public: - BrowserClients(BrowserService& browserService); - ~BrowserClients() = default; - - QJsonObject readResponse(const QByteArray& arr); - -private: - QJsonObject byteArrayToJson(const QByteArray& arr) const; - QString getClientID(const QJsonObject& json) const; - ClientPtr getClient(const QString& clientID); - -private: - QMutex m_mutex; - QVector m_clients; - BrowserService& m_browserService; -}; - -#endif // BROWSERCLIENTS_H diff --git a/src/browser/BrowserHost.cpp b/src/browser/BrowserHost.cpp new file mode 100644 index 0000000000..62c3e9cd83 --- /dev/null +++ b/src/browser/BrowserHost.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2020 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 "BrowserHost.h" +#include "BrowserSettings.h" +#include "BrowserShared.h" + +#include +#include +#include +#include +#include + +#include "sodium.h" +#include + +BrowserHost::BrowserHost(QObject* parent) + : QObject(parent) +{ + m_localServer = new QLocalServer(this); + m_localServer->setSocketOptions(QLocalServer::UserAccessOption); + connect(m_localServer.data(), SIGNAL(newConnection()), this, SLOT(proxyConnected())); +} + +BrowserHost::~BrowserHost() +{ + stop(); +} + +void BrowserHost::start() +{ + if (sodium_init() == -1) { + qWarning() << "Failed to start browser service: libsodium failed to initialize!"; + return; + } + + if (!m_localServer->isListening()) { + m_localServer->listen(BrowserShared::localServerPath()); + } +} + +void BrowserHost::stop() +{ + m_socketList.clear(); + m_localServer->close(); +} + +void BrowserHost::proxyConnected() +{ + auto socket = m_localServer->nextPendingConnection(); + if (socket) { + m_socketList.append(socket); + connect(socket, SIGNAL(readyRead()), this, SLOT(readProxyMessage())); + connect(socket, SIGNAL(disconnected()), this, SLOT(proxyDisconnected())); + } +} + +void BrowserHost::readProxyMessage() +{ + QLocalSocket* socket = qobject_cast(QObject::sender()); + if (!socket || socket->bytesAvailable() <= 0) { + return; + } + + socket->setReadBufferSize(BrowserShared::NATIVEMSG_MAX_LENGTH); + + QJsonParseError error; + auto json = QJsonDocument::fromJson(socket->readAll(), &error); + if (json.isNull()) { + qWarning() << "Failed to read proxy message: " << error.errorString(); + return; + } + + emit clientMessageReceived(json.object()); +} + +void BrowserHost::sendClientMessage(const QJsonObject& json) +{ + QString reply(QJsonDocument(json).toJson(QJsonDocument::Compact)); + for (const auto socket : m_socketList) { + if (socket && socket->isValid() && socket->state() == QLocalSocket::ConnectedState) { + QByteArray arr = reply.toUtf8(); + socket->write(arr.constData(), arr.length()); + socket->flush(); + } + } +} + +void BrowserHost::proxyDisconnected() +{ + auto socket = qobject_cast(QObject::sender()); + m_socketList.removeOne(socket); +} diff --git a/src/proxy/NativeMessagingHost.h b/src/browser/BrowserHost.h similarity index 56% rename from src/proxy/NativeMessagingHost.h rename to src/browser/BrowserHost.h index 5bedd9de56..ea8e074096 100644 --- a/src/proxy/NativeMessagingHost.h +++ b/src/browser/BrowserHost.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -18,29 +18,37 @@ #ifndef NATIVEMESSAGINGHOST_H #define NATIVEMESSAGINGHOST_H -#include "NativeMessagingBase.h" +#include +#include +#include -class NativeMessagingHost : public NativeMessagingBase +class QLocalServer; +class QLocalSocket; + +class BrowserHost : public QObject { Q_OBJECT + public: - NativeMessagingHost(); - ~NativeMessagingHost() override; + explicit BrowserHost(QObject* parent = nullptr); + ~BrowserHost() override; -public slots: - void newLocalMessage(); - void deleteSocket(); - void socketStateChanged(QLocalSocket::LocalSocketState socketState); + void start(); + void stop(); -private: - void readNativeMessages() override; - void readLength() override; - bool readStdIn(const quint32 length) override; + void sendClientMessage(const QJsonObject& json); -private: - QLocalSocket* m_localSocket; +signals: + void clientMessageReceived(const QJsonObject& json); + +private slots: + void proxyConnected(); + void readProxyMessage(); + void proxyDisconnected(); - Q_DISABLE_COPY(NativeMessagingHost) +private: + QPointer m_localServer; + QList m_socketList; }; #endif // NATIVEMESSAGINGHOST_H diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 5aa5e77ed8..b83af627a4 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2013 Francois Ferrand * Copyright (C) 2017 Sami Vänttinen - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -26,8 +26,10 @@ #include #include "BrowserAccessControlDialog.h" +#include "BrowserAction.h" #include "BrowserEntryConfig.h" #include "BrowserEntrySaveDialog.h" +#include "BrowserHost.h" #include "BrowserService.h" #include "BrowserSettings.h" #include "core/Database.h" @@ -58,34 +60,45 @@ const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnl // Multiple URL's const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL"); -BrowserService::BrowserService(DatabaseTabWidget* parent) - : m_dbTabWidget(parent) +Q_GLOBAL_STATIC(BrowserService, s_browserService); + +BrowserService::BrowserService() + : QObject() + , m_browserHost(new BrowserHost) , m_dialogActive(false) , m_bringToFrontRequested(false) , m_prevWindowState(WindowState::Normal) , m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224")) { - // Don't connect the signals when used from DatabaseSettingsWidgetBrowser (parent is nullptr) - if (m_dbTabWidget) { - 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*))); + connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage); + setEnabled(browserSettings()->isEnabled()); +} + +BrowserService* BrowserService::instance() +{ + return s_browserService; +} + +void BrowserService::setEnabled(bool enabled) +{ + if (enabled) { + // Update KeePassXC/keepassxc-proxy binary paths to Native Messaging scripts + if (browserSettings()->updateBinaryPath()) { + browserSettings()->updateBinaryPaths(); + } + + m_browserHost->start(); + } else { + m_browserHost->stop(); } } bool BrowserService::isDatabaseOpened() const { - DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); - if (!dbWidget) { - return false; + if (m_currentDatabaseWidget) { + return !m_currentDatabaseWidget->isLocked(); } - - return dbWidget->currentMode() == DatabaseWidget::Mode::ViewMode - || dbWidget->currentMode() == DatabaseWidget::Mode::EditMode; + return false; } bool BrowserService::openDatabase(bool triggerUnlock) @@ -94,13 +107,7 @@ bool BrowserService::openDatabase(bool triggerUnlock) return false; } - DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); - if (!dbWidget) { - return false; - } - - if (dbWidget->currentMode() == DatabaseWidget::Mode::ViewMode - || dbWidget->currentMode() == DatabaseWidget::Mode::EditMode) { + if (m_currentDatabaseWidget && !m_currentDatabaseWidget->isLocked()) { return true; } @@ -114,19 +121,20 @@ bool BrowserService::openDatabase(bool triggerUnlock) void BrowserService::lockDatabase() { - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "lockDatabase", Qt::BlockingQueuedConnection); - } - - DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); - if (!dbWidget) { - return; + if (m_currentDatabaseWidget) { + m_currentDatabaseWidget->lock(); } +} - if (dbWidget->currentMode() == DatabaseWidget::Mode::ViewMode - || dbWidget->currentMode() == DatabaseWidget::Mode::EditMode) { - dbWidget->lock(); +QString BrowserService::getDatabaseHash(bool legacy) +{ + if (legacy) { + return QCryptographicHash::hash( + (browserService()->getDatabaseRootUuid() + browserService()->getDatabaseRecycleBinUuid()).toUtf8(), + QCryptographicHash::Sha256) + .toHex(); } + return QCryptographicHash::hash(getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex(); } QString BrowserService::getDatabaseRootUuid() @@ -180,9 +188,9 @@ QJsonArray BrowserService::getChildrenFromGroup(Group* group) return groupList; } -QJsonObject BrowserService::getDatabaseGroups(const QSharedPointer& selectedDb) +QJsonObject BrowserService::getDatabaseGroups() { - auto db = selectedDb ? selectedDb : getDatabase(); + auto db = getDatabase(); if (!db) { return {}; } @@ -208,15 +216,6 @@ QJsonObject BrowserService::getDatabaseGroups(const QSharedPointer& se QJsonObject BrowserService::createNewGroup(const QString& groupName) { - QJsonObject result; - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, - "createNewGroup", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QJsonObject, result), - Q_ARG(QString, groupName)); - return result; - } auto db = getDatabase(); if (!db) { @@ -232,6 +231,7 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName) // Group already exists if (group) { + QJsonObject result; result["name"] = group->name(); result["uuid"] = Tools::uuidToHex(group->uuid()); return result; @@ -245,7 +245,7 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName) MessageBox::Yes | MessageBox::No); if (dialogResult != MessageBox::Yes) { - return result; + return {}; } QString name, uuid; @@ -279,6 +279,7 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName) previousGroup = tempGroup; } + QJsonObject result; result["name"] = name; result["uuid"] = uuid; return result; @@ -286,25 +287,18 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName) 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(QString, key)); - return id; - } - auto db = getDatabase(); if (!db) { return {}; } bool contains; - MessageBox::Button dialogResult = MessageBox::Cancel; + auto dialogResult = MessageBox::Cancel; + QString id; do { QInputDialog keyDialog; - connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), &keyDialog, SLOT(reject())); + connect(m_currentDatabaseWidget, SIGNAL(databaseLocked()), &keyDialog, SLOT(reject())); keyDialog.setWindowTitle(tr("KeePassXC: New key association request")); keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n" "Give the connection a unique name or ID, for example:\nchrome-laptop.") @@ -353,28 +347,14 @@ QString BrowserService::getKey(const QString& id) return db->metadata()->customData()->value(ASSOCIATE_KEY_PREFIX + id); } -QJsonArray BrowserService::findMatchingEntries(const QString& id, +QJsonArray BrowserService::findMatchingEntries(const QString& dbid, const QString& url, const QString& submitUrl, const QString& realm, const StringPairList& keyList, const bool httpAuth) { - QJsonArray result; - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, - "findMatchingEntries", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QJsonArray, result), - Q_ARG(QString, id), - Q_ARG(QString, url), - Q_ARG(QString, submitUrl), - Q_ARG(QString, realm), - Q_ARG(StringPairList, keyList), - Q_ARG(bool, httpAuth)); - return result; - } - + Q_UNUSED(dbid); const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess(); const bool ignoreHttpAuth = browserSettings()->httpAuthPermission(); const QString host = QUrl(url).host(); @@ -425,18 +405,19 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id, } if (pwEntries.isEmpty()) { - return QJsonArray(); + return {}; } // Ensure that database is not locked when the popup was visible if (!isDatabaseOpened()) { - return QJsonArray(); + return {}; } // Sort results pwEntries = sortEntries(pwEntries, host, submitUrl); // Fill the list + QJsonArray result; for (auto* entry : pwEntries) { result.append(prepareEntry(entry)); } @@ -444,7 +425,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id, return result; } -void BrowserService::addEntry(const QString& id, +void BrowserService::addEntry(const QString& dbid, const QString& login, const QString& password, const QString& url, @@ -454,21 +435,8 @@ void BrowserService::addEntry(const QString& id, const QString& groupUuid, const QSharedPointer& selectedDb) { - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, - "addEntry", - Qt::BlockingQueuedConnection, - Q_ARG(QString, id), - Q_ARG(QString, login), - Q_ARG(QString, password), - Q_ARG(QString, url), - Q_ARG(QString, submitUrl), - Q_ARG(QString, realm), - Q_ARG(QString, group), - Q_ARG(QString, groupUuid), - Q_ARG(QSharedPointer, selectedDb)); - } - + // TODO: select database based on this key id + Q_UNUSED(dbid); auto db = selectedDb ? selectedDb : selectedDatabase(); if (!db) { return; @@ -510,37 +478,25 @@ void BrowserService::addEntry(const QString& id, config.save(entry); } -BrowserService::ReturnValue BrowserService::updateEntry(const QString& id, - const QString& uuid, - const QString& login, - const QString& password, - const QString& url, - const QString& submitUrl) +bool BrowserService::updateEntry(const QString& dbid, + const QString& uuid, + const QString& login, + const QString& password, + const QString& url, + const QString& submitUrl) { - ReturnValue result = ReturnValue::Error; - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, - "updateEntry", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(ReturnValue, result), - Q_ARG(QString, id), - Q_ARG(QString, uuid), - Q_ARG(QString, login), - Q_ARG(QString, password), - Q_ARG(QString, url), - Q_ARG(QString, submitUrl)); - } - + // TODO: select database based on this key id + Q_UNUSED(dbid); auto db = selectedDatabase(); if (!db) { - return ReturnValue::Error; + return false; } Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid)); if (!entry) { // If entry is not found for update, add a new one to the selected database - addEntry(id, login, password, url, submitUrl, "", "", "", db); - return ReturnValue::Success; + addEntry(dbid, login, password, url, submitUrl, "", "", "", db); + return true; } // Check if the entry password is a reference. If so, update the original entry instead @@ -549,16 +505,17 @@ BrowserService::ReturnValue BrowserService::updateEntry(const QString& id, if (!referenceUuid.isNull()) { entry = db->rootGroup()->findEntryByUuid(referenceUuid); if (!entry) { - return ReturnValue::Error; + return false; } } } QString username = entry->username(); if (username.isEmpty()) { - return ReturnValue::Error; + return false; } + bool result = false; if (username.compare(login, Qt::CaseSensitive) != 0 || entry->password().compare(password, Qt::CaseSensitive) != 0) { MessageBox::Button dialogResult = MessageBox::No; @@ -580,9 +537,7 @@ BrowserService::ReturnValue BrowserService::updateEntry(const QString& id, } entry->setPassword(password); entry->endUpdate(); - result = ReturnValue::Success; - } else { - result = ReturnValue::Canceled; + result = true; } hideWindow(); @@ -646,17 +601,14 @@ QList BrowserService::searchEntries(const QString& url, const QString& s // 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 (auto* dbWidget = qobject_cast(m_dbTabWidget->widget(i))) { - if (const auto& db = dbWidget->database()) { - if (databaseConnected(db)) { - databases << db; - } - } + for (auto dbWidget : getMainWindow()->getOpenDatabases()) { + auto db = dbWidget->database(); + if (db && databaseConnected(dbWidget->database())) { + databases << db; } } - } else if (const auto& db = getDatabase()) { + } else { + const auto& db = getDatabase(); if (databaseConnected(db)) { databases << db; } @@ -674,9 +626,8 @@ QList BrowserService::searchEntries(const QString& url, const QString& s return entries; } -void BrowserService::convertAttributesToCustomData(const QSharedPointer& currentDb) +void BrowserService::convertAttributesToCustomData(QSharedPointer db) { - auto db = currentDb ? currentDb : getDatabase(); if (!db) { return; } @@ -806,7 +757,7 @@ QList BrowserService::confirmEntries(QList& pwEntriesToConfirm, m_dialogActive = true; BrowserAccessControlDialog accessControlDialog; - connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), &accessControlDialog, SLOT(reject())); + connect(m_currentDatabaseWidget, SIGNAL(databaseLocked()), &accessControlDialog, SLOT(reject())); connect(&accessControlDialog, &BrowserAccessControlDialog::disableAccess, [&](QTableWidgetItem* item) { auto entry = pwEntriesToConfirm[item->row()]; BrowserEntryConfig config; @@ -1103,10 +1054,8 @@ QString BrowserService::baseDomain(const QString& hostname) const QSharedPointer BrowserService::getDatabase() { - if (DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget()) { - if (const auto& db = dbWidget->database()) { - return db; - } + if (m_currentDatabaseWidget) { + return m_currentDatabaseWidget->database(); } return {}; } @@ -1114,20 +1063,15 @@ QSharedPointer BrowserService::getDatabase() QSharedPointer BrowserService::selectedDatabase() { QList databaseWidgets; - for (int i = 0;; ++i) { - auto* dbWidget = m_dbTabWidget->databaseWidgetFromIndex(i); + for (auto dbWidget : getMainWindow()->getOpenDatabases()) { // Add only open databases - if (dbWidget && !dbWidget->isLocked()) { - databaseWidgets.push_back(dbWidget); - continue; + if (!dbWidget->isLocked()) { + databaseWidgets << dbWidget; } - - // Break out if dbStruct.dbWidget is nullptr - break; } BrowserEntrySaveDialog browserEntrySaveDialog; - int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_dbTabWidget->currentDatabaseWidget()); + int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget); if (openDatabaseCount > 1) { int res = browserEntrySaveDialog.exec(); if (res == QDialog::Accepted) { @@ -1145,7 +1089,7 @@ QSharedPointer BrowserService::selectedDatabase() return getDatabase(); } -bool BrowserService::moveSettingsToCustomData(Entry* entry, const QString& name) const +bool BrowserService::moveSettingsToCustomData(Entry* entry, const QString& name) { if (entry->attributes()->contains(name)) { QString attr = entry->attributes()->value(name); @@ -1160,7 +1104,7 @@ bool BrowserService::moveSettingsToCustomData(Entry* entry, const QString& name) return false; } -int BrowserService::moveKeysToCustomData(Entry* entry, const QSharedPointer& db) const +int BrowserService::moveKeysToCustomData(Entry* entry, QSharedPointer db) { int keyCounter = 0; for (const auto& key : entry->attributes()->keys()) { @@ -1179,14 +1123,9 @@ int BrowserService::moveKeysToCustomData(Entry* entry, const QSharedPointer db) { - if (!browserSettings()->isEnabled() || browserSettings()->noMigrationPrompt()) { - return false; - } - - auto db = getDatabase(); - if (!db) { + if (!db || !browserSettings()->isEnabled() || browserSettings()->noMigrationPrompt()) { return false; } @@ -1272,7 +1211,9 @@ void BrowserService::raiseWindow(const bool force) void BrowserService::databaseLocked(DatabaseWidget* dbWidget) { if (dbWidget) { - emit databaseLocked(); + QJsonObject msg; + msg["action"] = QString("database-locked"); + m_browserHost->sendClientMessage(msg); } } @@ -1283,22 +1224,43 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) hideWindow(); m_bringToFrontRequested = false; } - emit databaseUnlocked(); - if (checkLegacySettings()) { - convertAttributesToCustomData(); + QJsonObject msg; + msg["action"] = QString("database-unlocked"); + m_browserHost->sendClientMessage(msg); + + auto db = dbWidget->database(); + if (checkLegacySettings(db)) { + convertAttributesToCustomData(db); } } } -void BrowserService::activateDatabaseChanged(DatabaseWidget* dbWidget) +void BrowserService::activeDatabaseChanged(DatabaseWidget* dbWidget) { + m_currentDatabaseWidget = dbWidget; if (dbWidget) { - auto currentMode = dbWidget->currentMode(); - if (currentMode == DatabaseWidget::Mode::ViewMode || currentMode == DatabaseWidget::Mode::EditMode) { - emit databaseUnlocked(); + if (dbWidget->isLocked()) { + databaseLocked(dbWidget); } else { - emit databaseLocked(); + databaseUnlocked(dbWidget); } } } + +void BrowserService::processClientMessage(const QJsonObject& message) +{ + auto clientID = message["clientID"].toString(); + if (clientID.isEmpty()) { + return; + } + + // Create a new client action if we haven't seen this id yet + if (!m_browserClients.contains(clientID)) { + m_browserClients.insert(clientID, QSharedPointer::create()); + } + + auto& action = m_browserClients.value(clientID); + auto response = action->processClientMessage(message); + m_browserHost->sendClientMessage(response); +} diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 3157df61f4..6de5e49bfe 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -1,7 +1,7 @@ /* * Copyright (C) 2013 Francois Ferrand * Copyright (C) 2017 Sami Vänttinen - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -21,8 +21,9 @@ #define BROWSERSERVICE_H #include "core/Entry.h" -#include "gui/DatabaseTabWidget.h" #include +#include +#include #include typedef QPair StringPair; @@ -33,28 +34,33 @@ enum max_length = 16 * 1024 }; +class DatabaseTabWidget; +class DatabaseWidget; +class BrowserHost; +class BrowserAction; + class BrowserService : public QObject { Q_OBJECT public: - enum ReturnValue - { - Success, - Error, - Canceled - }; + explicit BrowserService(); + static BrowserService* instance(); - explicit BrowserService(DatabaseTabWidget* parent); + void setEnabled(bool enabled); + + QString getKey(const QString& id); + QString storeKey(const QString& key); + QString getDatabaseHash(bool legacy = false); bool isDatabaseOpened() const; bool openDatabase(bool triggerUnlock); - QString getDatabaseRootUuid(); - QString getDatabaseRecycleBinUuid(); - QJsonObject getDatabaseGroups(const QSharedPointer& selectedDb = {}); + void lockDatabase(); + + QJsonObject getDatabaseGroups(); QJsonObject createNewGroup(const QString& groupName); - QString getKey(const QString& id); - void addEntry(const QString& id, + + void addEntry(const QString& dbid, const QString& login, const QString& password, const QString& url, @@ -63,11 +69,22 @@ class BrowserService : public QObject const QString& group, const QString& groupUuid, const QSharedPointer& selectedDb = {}); - QList searchEntries(const QSharedPointer& db, const QString& url, const QString& submitUrl); - QList searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList); - void convertAttributesToCustomData(const QSharedPointer& currentDb = {}); + bool updateEntry(const QString& dbid, + const QString& uuid, + const QString& login, + const QString& password, + const QString& url, + const QString& submitUrl); + + QJsonArray findMatchingEntries(const QString& dbid, + const QString& url, + const QString& submitUrl, + const QString& realm, + const StringPairList& keyList, + const bool httpAuth = false); + + static void convertAttributesToCustomData(QSharedPointer db); -public: static const QString KEEPASSXCBROWSER_NAME; static const QString KEEPASSXCBROWSER_OLD_NAME; static const QString ASSOCIATE_KEY_PREFIX; @@ -78,28 +95,12 @@ class BrowserService : public QObject static const QString ADDITIONAL_URL; public slots: - QJsonArray findMatchingEntries(const QString& id, - const QString& url, - const QString& submitUrl, - const QString& realm, - const StringPairList& keyList, - const bool httpAuth = false); - QString storeKey(const QString& key); - ReturnValue updateEntry(const QString& id, - const QString& uuid, - const QString& login, - const QString& password, - const QString& url, - const QString& submitUrl); void databaseLocked(DatabaseWidget* dbWidget); void databaseUnlocked(DatabaseWidget* dbWidget); - void activateDatabaseChanged(DatabaseWidget* dbWidget); - void lockDatabase(); + void activeDatabaseChanged(DatabaseWidget* dbWidget); -signals: - void databaseLocked(); - void databaseUnlocked(); - void databaseChanged(); +private slots: + void processClientMessage(const QJsonObject& message); private: enum Access @@ -116,7 +117,8 @@ public slots: Hidden }; -private: + QList searchEntries(const QSharedPointer& db, const QString& url, const QString& submitUrl); + QList searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList); QList sortEntries(QList& pwEntries, const QString& host, const QString& submitUrl); QList confirmEntries(QList& pwEntriesToConfirm, const QString& url, @@ -125,6 +127,7 @@ public slots: const QString& realm, const bool httpAuth); QJsonObject prepareEntry(const Entry* entry); + QJsonArray getChildrenFromGroup(Group* group); Access checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm); Group* getDefaultEntryGroup(const QSharedPointer& selectedDb = {}); int @@ -135,21 +138,35 @@ public slots: QString baseDomain(const QString& hostname) const; QSharedPointer getDatabase(); QSharedPointer selectedDatabase(); - QJsonArray getChildrenFromGroup(Group* group); - bool moveSettingsToCustomData(Entry* entry, const QString& name) const; - int moveKeysToCustomData(Entry* entry, const QSharedPointer& db) const; - bool checkLegacySettings(); + QString getDatabaseRootUuid(); + QString getDatabaseRecycleBinUuid(); + + bool checkLegacySettings(QSharedPointer db); + void hideWindow() const; void raiseWindow(const bool force = false); -private: - DatabaseTabWidget* const m_dbTabWidget; + static bool moveSettingsToCustomData(Entry* entry, const QString& name); + static int moveKeysToCustomData(Entry* entry, QSharedPointer db); + + QPointer m_browserHost; + QHash> m_browserClients; + bool m_dialogActive; bool m_bringToFrontRequested; WindowState m_prevWindowState; QUuid m_keepassBrowserUUID; + QPointer m_currentDatabaseWidget; + + Q_DISABLE_COPY(BrowserService); + friend class TestBrowser; }; +static inline BrowserService* browserService() +{ + return BrowserService::instance(); +} + #endif // BROWSERSERVICE_H diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index 7fa80ea3bd..5d6514cae9 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -162,16 +162,6 @@ void BrowserSettings::setNoMigrationPrompt(bool prompt) config()->set(Config::Browser_NoMigrationPrompt, prompt); } -bool BrowserSettings::supportBrowserProxy() -{ - return config()->get(Config::Browser_SupportBrowserProxy).toBool(); -} - -void BrowserSettings::setSupportBrowserProxy(bool enabled) -{ - config()->set(Config::Browser_SupportBrowserProxy, enabled); -} - bool BrowserSettings::useCustomProxy() { return config()->get(Config::Browser_UseCustomProxy).toBool(); @@ -184,9 +174,6 @@ void BrowserSettings::setUseCustomProxy(bool enabled) QString BrowserSettings::customProxyLocation() { - if (!useCustomProxy()) { - return QString(); - } return config()->get(Config::Browser_CustomProxyLocation).toString(); } @@ -195,6 +182,11 @@ void BrowserSettings::setCustomProxyLocation(const QString& location) config()->set(Config::Browser_CustomProxyLocation, location); } +QString BrowserSettings::proxyLocation() +{ + return m_nativeMessageInstaller.getProxyPath(); +} + bool BrowserSettings::updateBinaryPath() { return config()->get(Config::Browser_UpdateBinaryPath).toBool(); @@ -215,81 +207,14 @@ void BrowserSettings::setAllowExpiredCredentials(bool enabled) config()->set(Config::Browser_AllowExpiredCredentials, enabled); } -bool BrowserSettings::chromeSupport() -{ - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROME); -} - -void BrowserSettings::setChromeSupport(bool enabled) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::CHROME, enabled, supportBrowserProxy(), customProxyLocation()); -} - -bool BrowserSettings::chromiumSupport() -{ - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROMIUM); -} - -void BrowserSettings::setChromiumSupport(bool enabled) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::CHROMIUM, enabled, supportBrowserProxy(), customProxyLocation()); -} - -bool BrowserSettings::firefoxSupport() -{ - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::FIREFOX); -} - -void BrowserSettings::setFirefoxSupport(bool enabled) +bool BrowserSettings::browserSupport(BrowserShared::SupportedBrowsers browser) { - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::FIREFOX, enabled, supportBrowserProxy(), customProxyLocation()); + return m_nativeMessageInstaller.isBrowserEnabled(browser); } -bool BrowserSettings::vivaldiSupport() +void BrowserSettings::setBrowserSupport(BrowserShared::SupportedBrowsers browser, bool enabled) { - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::VIVALDI); -} - -void BrowserSettings::setVivaldiSupport(bool enabled) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::VIVALDI, enabled, supportBrowserProxy(), customProxyLocation()); -} - -bool BrowserSettings::braveSupport() -{ - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::BRAVE); -} - -void BrowserSettings::setBraveSupport(bool enabled) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::BRAVE, enabled, supportBrowserProxy(), customProxyLocation()); -} - -bool BrowserSettings::torBrowserSupport() -{ - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::TOR_BROWSER); -} - -void BrowserSettings::setTorBrowserSupport(bool enabled) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::TOR_BROWSER, enabled, supportBrowserProxy(), customProxyLocation()); -} - -bool BrowserSettings::edgeSupport() -{ - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::EDGE); -} - -void BrowserSettings::setEdgeSupport(bool enabled) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::EDGE, enabled, supportBrowserProxy(), customProxyLocation()); + m_nativeMessageInstaller.setBrowserEnabled(browser, enabled); } bool BrowserSettings::passwordUseNumbers() @@ -563,13 +488,7 @@ QJsonObject BrowserSettings::generatePassword() return password; } -void BrowserSettings::updateBinaryPaths(const QString& customProxyLocation) -{ - bool isProxy = supportBrowserProxy(); - m_hostInstaller.updateBinaryPaths(isProxy, customProxyLocation); -} - -bool BrowserSettings::checkIfProxyExists(QString& path) +void BrowserSettings::updateBinaryPaths() { - return m_hostInstaller.checkIfProxyExists(supportBrowserProxy(), customProxyLocation(), path); + m_nativeMessageInstaller.updateBinaryPaths(); } diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h index 9340cd0a3a..3f5cceea75 100644 --- a/src/browser/BrowserSettings.h +++ b/src/browser/BrowserSettings.h @@ -20,7 +20,8 @@ #ifndef BROWSERSETTINGS_H #define BROWSERSETTINGS_H -#include "HostInstaller.h" +#include "BrowserShared.h" +#include "NativeMessageInstaller.h" #include "core/PassphraseGenerator.h" #include "core/PasswordGenerator.h" @@ -58,30 +59,18 @@ class BrowserSettings bool noMigrationPrompt(); void setNoMigrationPrompt(bool prompt); - bool supportBrowserProxy(); - void setSupportBrowserProxy(bool enabled); bool useCustomProxy(); void setUseCustomProxy(bool enabled); QString customProxyLocation(); void setCustomProxyLocation(const QString& location); + QString proxyLocation(); bool updateBinaryPath(); void setUpdateBinaryPath(bool enabled); bool allowExpiredCredentials(); void setAllowExpiredCredentials(bool enabled); - bool chromeSupport(); - void setChromeSupport(bool enabled); - bool chromiumSupport(); - void setChromiumSupport(bool enabled); - bool firefoxSupport(); - void setFirefoxSupport(bool enabled); - bool vivaldiSupport(); - void setVivaldiSupport(bool enabled); - bool braveSupport(); - void setBraveSupport(bool enabled); - bool torBrowserSupport(); - void setTorBrowserSupport(bool enabled); - bool edgeSupport(); - void setEdgeSupport(bool enabled); + + bool browserSupport(BrowserShared::SupportedBrowsers browser); + void setBrowserSupport(BrowserShared::SupportedBrowsers browser, bool enabled); bool passwordUseNumbers(); void setPasswordUseNumbers(bool useNumbers); @@ -126,15 +115,14 @@ class BrowserSettings PasswordGenerator::CharClasses passwordCharClasses(); PasswordGenerator::GeneratorFlags passwordGeneratorFlags(); QJsonObject generatePassword(); - void updateBinaryPaths(const QString& customProxyLocation = QString()); - bool checkIfProxyExists(QString& path); + void updateBinaryPaths(); private: static BrowserSettings* m_instance; PasswordGenerator m_passwordGenerator; PassphraseGenerator m_passPhraseGenerator; - HostInstaller m_hostInstaller; + NativeMessageInstaller m_nativeMessageInstaller; }; inline BrowserSettings* browserSettings() diff --git a/src/browser/BrowserSettingsPage.cpp b/src/browser/BrowserSettingsPage.cpp new file mode 100644 index 0000000000..692854bf85 --- /dev/null +++ b/src/browser/BrowserSettingsPage.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 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 "BrowserSettingsPage.h" + +#include "BrowserService.h" +#include "BrowserSettings.h" +#include "BrowserSettingsWidget.h" +#include "core/Resources.h" + +QString BrowserSettingsPage::name() +{ + return QObject::tr("Browser Integration"); +} + +QIcon BrowserSettingsPage::icon() +{ + return Resources::instance()->icon("internet-web-browser"); +} + +QWidget* BrowserSettingsPage::createWidget() +{ + return new BrowserSettingsWidget(); +} + +void BrowserSettingsPage::loadSettings(QWidget* widget) +{ + qobject_cast(widget)->loadSettings(); +} + +void BrowserSettingsPage::saveSettings(QWidget* widget) +{ + qobject_cast(widget)->saveSettings(); + browserService()->setEnabled(browserSettings()->isEnabled()); +} diff --git a/src/browser/BrowserSettingsPage.h b/src/browser/BrowserSettingsPage.h new file mode 100644 index 0000000000..9e669b1944 --- /dev/null +++ b/src/browser/BrowserSettingsPage.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 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 . + */ + +#ifndef KEEPASSXC_BROWSERSETTINGSPAGE_H +#define KEEPASSXC_BROWSERSETTINGSPAGE_H + +#include "gui/ApplicationSettingsWidget.h" + +class BrowserSettingsPage : public ISettingsPage +{ +public: + explicit BrowserSettingsPage() = default; + ~BrowserSettingsPage() override = default; + + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget) override; + void saveSettings(QWidget* widget) override; +}; + +#endif // KEEPASSXC_BROWSERSETTINGSPAGE_H diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserSettingsWidget.cpp similarity index 71% rename from src/browser/BrowserOptionDialog.cpp rename to src/browser/BrowserSettingsWidget.cpp index 8a67d62dad..15b20b19a8 100644 --- a/src/browser/BrowserOptionDialog.cpp +++ b/src/browser/BrowserSettingsWidget.cpp @@ -1,7 +1,5 @@ /* - * Copyright (C) 2013 Francois Ferrand - * Copyright (C) 2017 Sami Vänttinen - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -17,8 +15,8 @@ * along with this program. If not, see . */ -#include "BrowserOptionDialog.h" -#include "ui_BrowserOptionDialog.h" +#include "BrowserSettingsWidget.h" +#include "ui_BrowserSettingsWidget.h" #include "BrowserSettings.h" #include "config-keepassx.h" @@ -26,9 +24,9 @@ #include -BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) +BrowserSettingsWidget::BrowserSettingsWidget(QWidget* parent) : QWidget(parent) - , m_ui(new Ui::BrowserOptionDialog()) + , m_ui(new Ui::BrowserSettingsWidget()) { m_ui->setupUi(this); @@ -52,13 +50,9 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) snapInstructions)); // clang-format on - m_ui->scriptWarningWidget->setVisible(false); - m_ui->scriptWarningWidget->setAutoHideTimeout(-1); - - m_ui->warningWidget->showMessage(tr("Warning: The following options can be dangerous!"), - MessageWidget::Warning); m_ui->warningWidget->setCloseButtonVisible(false); m_ui->warningWidget->setAutoHideTimeout(-1); + m_ui->warningWidget->setAnimate(false); m_ui->tabWidget->setEnabled(m_ui->enableBrowserSupport->isChecked()); connect(m_ui->enableBrowserSupport, SIGNAL(toggled(bool)), m_ui->tabWidget, SLOT(setEnabled(bool))); @@ -67,6 +61,8 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) m_ui->customProxyLocationBrowseButton->setEnabled(m_ui->useCustomProxy->isChecked()); connect(m_ui->useCustomProxy, SIGNAL(toggled(bool)), m_ui->customProxyLocation, SLOT(setEnabled(bool))); connect(m_ui->useCustomProxy, SIGNAL(toggled(bool)), m_ui->customProxyLocationBrowseButton, SLOT(setEnabled(bool))); + connect(m_ui->useCustomProxy, SIGNAL(toggled(bool)), SLOT(validateCustomProxyLocation())); + connect(m_ui->customProxyLocation, SIGNAL(editingFinished()), SLOT(validateCustomProxyLocation())); connect(m_ui->customProxyLocationBrowseButton, SIGNAL(clicked()), this, SLOT(showProxyLocationFileDialog())); #ifndef Q_OS_LINUX @@ -86,11 +82,11 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) m_ui->browserGlobalWarningWidget->setVisible(false); } -BrowserOptionDialog::~BrowserOptionDialog() +BrowserSettingsWidget::~BrowserSettingsWidget() { } -void BrowserOptionDialog::loadSettings() +void BrowserSettingsWidget::loadSettings() { auto settings = browserSettings(); m_ui->enableBrowserSupport->setChecked(settings->isEnabled()); @@ -116,43 +112,39 @@ void BrowserOptionDialog::loadSettings() m_ui->searchInAllDatabases->setChecked(settings->searchInAllDatabases()); m_ui->supportKphFields->setChecked(settings->supportKphFields()); m_ui->noMigrationPrompt->setChecked(settings->noMigrationPrompt()); - m_ui->supportBrowserProxy->setChecked(settings->supportBrowserProxy()); m_ui->useCustomProxy->setChecked(settings->useCustomProxy()); m_ui->customProxyLocation->setText(settings->customProxyLocation()); m_ui->updateBinaryPath->setChecked(settings->updateBinaryPath()); m_ui->allowExpiredCredentials->setChecked(settings->allowExpiredCredentials()); - m_ui->chromeSupport->setChecked(settings->chromeSupport()); - m_ui->chromiumSupport->setChecked(settings->chromiumSupport()); - m_ui->firefoxSupport->setChecked(settings->firefoxSupport()); - m_ui->edgeSupport->setChecked(settings->edgeSupport()); + m_ui->chromeSupport->setChecked(settings->browserSupport(BrowserShared::CHROME)); + m_ui->chromiumSupport->setChecked(settings->browserSupport(BrowserShared::CHROMIUM)); + m_ui->firefoxSupport->setChecked(settings->browserSupport(BrowserShared::FIREFOX)); + m_ui->edgeSupport->setChecked(settings->browserSupport(BrowserShared::EDGE)); #ifndef Q_OS_WIN - m_ui->braveSupport->setChecked(settings->braveSupport()); - m_ui->vivaldiSupport->setChecked(settings->vivaldiSupport()); - m_ui->torBrowserSupport->setChecked(settings->torBrowserSupport()); + m_ui->braveSupport->setChecked(settings->browserSupport(BrowserShared::BRAVE)); + m_ui->vivaldiSupport->setChecked(settings->browserSupport(BrowserShared::VIVALDI)); + m_ui->torBrowserSupport->setChecked(settings->browserSupport(BrowserShared::TOR_BROWSER)); #endif #ifndef Q_OS_LINUX m_ui->snapWarningLabel->setVisible(false); #endif -// TODO: Enable when Linux version is released +// TODO: Enable Edge support when Linux version is released #ifdef Q_OS_LINUX m_ui->edgeSupport->setChecked(false); m_ui->edgeSupport->setEnabled(false); #endif -#if defined(KEEPASSXC_DIST_APPIMAGE) - m_ui->supportBrowserProxy->setChecked(true); - m_ui->supportBrowserProxy->setEnabled(false); -#elif defined(KEEPASSXC_DIST_SNAP) +#ifdef KEEPASSXC_DIST_SNAP // Disable settings that will not work - m_ui->supportBrowserProxy->setChecked(true); - m_ui->supportBrowserProxy->setEnabled(false); m_ui->useCustomProxy->setChecked(false); - m_ui->useCustomProxy->setEnabled(false); + m_ui->useCustomProxy->setVisible(false); + m_ui->customProxyLocation->setVisible(false); + m_ui->customProxyLocationBrowseButton->setVisible(false); m_ui->browsersGroupBox->setVisible(false); m_ui->browsersGroupBox->setEnabled(false); m_ui->updateBinaryPath->setChecked(false); - m_ui->updateBinaryPath->setEnabled(false); + m_ui->updateBinaryPath->setVisible(false); // Show notice to user m_ui->browserGlobalWarningWidget->showMessage(tr("Please see special instructions for browser extension use below"), MessageWidget::Warning); @@ -160,23 +152,23 @@ void BrowserOptionDialog::loadSettings() m_ui->browserGlobalWarningWidget->setAutoHideTimeout(-1); #endif - // Check for native messaging host location errors - QString path; - if (!settings->checkIfProxyExists(path)) { - auto text = - tr("Warning, the keepassxc-proxy application was not found!" - "
Please check the KeePassXC installation directory or confirm the custom path in advanced options." - "
Browser integration WILL NOT WORK without the proxy application." - "
Expected Path: %1") - .arg(path); - m_ui->scriptWarningWidget->showMessage(text, MessageWidget::Warning); - m_ui->scriptWarningWidget->setVisible(true); + validateCustomProxyLocation(); +} + +void BrowserSettingsWidget::validateCustomProxyLocation() +{ + auto path = m_ui->customProxyLocation->text(); + if (m_ui->useCustomProxy->isChecked() && !QFile::exists(path)) { + m_ui->warningWidget->showMessage(tr("Error: The custom proxy location cannot be found!" + "
Browser integration WILL NOT WORK without the proxy application."), + MessageWidget::Error); } else { - m_ui->scriptWarningWidget->setVisible(false); + m_ui->warningWidget->showMessage(tr("Warning: The following options can be dangerous!"), + MessageWidget::Warning); } } -void BrowserOptionDialog::saveSettings() +void BrowserSettingsWidget::saveSettings() { auto settings = browserSettings(); settings->setEnabled(m_ui->enableBrowserSupport->isChecked()); @@ -186,7 +178,6 @@ void BrowserOptionDialog::saveSettings() settings->setMatchUrlScheme(m_ui->matchUrlScheme->isChecked()); settings->setSortByUsername(m_ui->sortByUsername->isChecked()); - settings->setSupportBrowserProxy(m_ui->supportBrowserProxy->isChecked()); settings->setUseCustomProxy(m_ui->useCustomProxy->isChecked()); settings->setCustomProxyLocation(m_ui->customProxyLocation->text()); @@ -199,18 +190,18 @@ void BrowserOptionDialog::saveSettings() settings->setSupportKphFields(m_ui->supportKphFields->isChecked()); settings->setNoMigrationPrompt(m_ui->noMigrationPrompt->isChecked()); - settings->setChromeSupport(m_ui->chromeSupport->isChecked()); - settings->setChromiumSupport(m_ui->chromiumSupport->isChecked()); - settings->setFirefoxSupport(m_ui->firefoxSupport->isChecked()); - settings->setEdgeSupport(m_ui->edgeSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::CHROME, m_ui->chromeSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::CHROMIUM, m_ui->chromiumSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::FIREFOX, m_ui->firefoxSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::EDGE, m_ui->edgeSupport->isChecked()); #ifndef Q_OS_WIN - settings->setBraveSupport(m_ui->braveSupport->isChecked()); - settings->setVivaldiSupport(m_ui->vivaldiSupport->isChecked()); - settings->setTorBrowserSupport(m_ui->torBrowserSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::BRAVE, m_ui->braveSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::VIVALDI, m_ui->vivaldiSupport->isChecked()); + settings->setBrowserSupport(BrowserShared::TOR_BROWSER, m_ui->torBrowserSupport->isChecked()); #endif } -void BrowserOptionDialog::showProxyLocationFileDialog() +void BrowserSettingsWidget::showProxyLocationFileDialog() { #ifdef Q_OS_WIN QString fileTypeFilter(QString("%1 (*.exe);;%2 (*.*)").arg(tr("Executable Files"), tr("All Files"))); @@ -222,4 +213,5 @@ void BrowserOptionDialog::showProxyLocationFileDialog() QFileInfo(QCoreApplication::applicationDirPath()).filePath(), fileTypeFilter); m_ui->customProxyLocation->setText(proxyLocation); + validateCustomProxyLocation(); } diff --git a/src/browser/BrowserOptionDialog.h b/src/browser/BrowserSettingsWidget.h similarity index 65% rename from src/browser/BrowserOptionDialog.h rename to src/browser/BrowserSettingsWidget.h index 5efb808e52..8f9dea62f1 100644 --- a/src/browser/BrowserOptionDialog.h +++ b/src/browser/BrowserSettingsWidget.h @@ -1,7 +1,5 @@ /* - * Copyright (C) 2013 Francois Ferrand - * Copyright (C) 2017 Sami Vänttinen - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 @@ -17,8 +15,8 @@ * along with this program. If not, see . */ -#ifndef BROWSEROPTIONDIALOG_H -#define BROWSEROPTIONDIALOG_H +#ifndef BROWSERSETTINGSWIDGET_H +#define BROWSERSETTINGSWIDGET_H #include #include @@ -26,16 +24,16 @@ namespace Ui { - class BrowserOptionDialog; + class BrowserSettingsWidget; } -class BrowserOptionDialog : public QWidget +class BrowserSettingsWidget : public QWidget { Q_OBJECT public: - explicit BrowserOptionDialog(QWidget* parent = nullptr); - ~BrowserOptionDialog(); + explicit BrowserSettingsWidget(QWidget* parent = nullptr); + ~BrowserSettingsWidget(); public slots: void loadSettings(); @@ -43,9 +41,10 @@ public slots: private slots: void showProxyLocationFileDialog(); + void validateCustomProxyLocation(); private: - QScopedPointer m_ui; + QScopedPointer m_ui; }; -#endif // BROWSEROPTIONDIALOG_H +#endif // BROWSERSETTINGSWIDGET_H diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserSettingsWidget.ui old mode 100755 new mode 100644 similarity index 94% rename from src/browser/BrowserOptionDialog.ui rename to src/browser/BrowserSettingsWidget.ui index 9dabde948a..b3458fdcd7 --- a/src/browser/BrowserOptionDialog.ui +++ b/src/browser/BrowserSettingsWidget.ui @@ -1,7 +1,7 @@ - BrowserOptionDialog - + BrowserSettingsWidget + 0 @@ -49,16 +49,6 @@ General - - - - - 0 - 0 - - - - @@ -351,16 +341,6 @@ - - - - Support a proxy application between KeePassXC and browser extension. - - - Use a proxy application between KeePassXC and browser extension - - - diff --git a/src/browser/BrowserShared.cpp b/src/browser/BrowserShared.cpp new file mode 100644 index 0000000000..654201705f --- /dev/null +++ b/src/browser/BrowserShared.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 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 "BrowserShared.h" +#include "config-keepassx.h" + +#include +#include +#include +#include + +namespace BrowserShared +{ + QString localServerPath() + { + const auto appName = qApp->property("KPXC_QUALIFIED_APPNAME").toString(); + const auto serverName = QStringLiteral("/%1.BrowserServer").arg(appName); +#if defined(KEEPASSXC_DIST_SNAP) + return QProcessEnvironment::systemEnvironment().value("SNAP_USER_COMMON") + serverName; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) + // Use XDG_RUNTIME_DIR instead of /tmp if it's available + QString path = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); + return path.isEmpty() ? QStandardPaths::writableLocation(QStandardPaths::TempLocation) + serverName + : path + serverName; +#elif defined(Q_OS_WIN) + // Windows uses named pipes + return serverName; +#else // Q_OS_MACOS and others + return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + serverName; +#endif + } +} // namespace BrowserShared diff --git a/src/browser/BrowserShared.h b/src/browser/BrowserShared.h new file mode 100644 index 0000000000..02bee9c44c --- /dev/null +++ b/src/browser/BrowserShared.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 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 . + */ + +#ifndef KEEPASSXC_BROWSERSHARED_H +#define KEEPASSXC_BROWSERSHARED_H + +#include + +namespace BrowserShared +{ + constexpr int NATIVEMSG_MAX_LENGTH = 1024 * 1024; + + enum SupportedBrowsers : int + { + CHROME = 0, + CHROMIUM, + FIREFOX, + VIVALDI, + TOR_BROWSER, + BRAVE, + EDGE, + MAX_SUPPORTED + }; + + QString localServerPath(); +} // namespace BrowserShared + +#endif // KEEPASSXC_BROWSERSHARED_H diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt index 7e813eb5bf..bb92511bc5 100755 --- a/src/browser/CMakeLists.txt +++ b/src/browser/CMakeLists.txt @@ -20,15 +20,15 @@ if(WITH_XC_BROWSER) set(keepassxcbrowser_SOURCES BrowserAccessControlDialog.cpp BrowserAction.cpp - BrowserClients.cpp BrowserEntryConfig.cpp BrowserEntrySaveDialog.cpp - BrowserOptionDialog.cpp + BrowserHost.cpp + BrowserSettingsPage.cpp + BrowserSettingsWidget.cpp BrowserService.cpp BrowserSettings.cpp - HostInstaller.cpp - NativeMessagingBase.cpp - NativeMessagingHost.cpp + BrowserShared.cpp + NativeMessageInstaller.cpp Variant.cpp) add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES}) diff --git a/src/browser/HostInstaller.cpp b/src/browser/HostInstaller.cpp deleted file mode 100644 index f4ffae3b7a..0000000000 --- a/src/browser/HostInstaller.cpp +++ /dev/null @@ -1,359 +0,0 @@ -/* - * 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 "HostInstaller.h" -#include "config-keepassx.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -HostInstaller::HostInstaller() - : HOST_NAME("org.keepassxc.keepassxc_browser") - , ALLOWED_EXTENSIONS(QStringList() << "keepassxc-browser@keepassxc.org") - , ALLOWED_ORIGINS(QStringList() << "chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/" - << "chrome-extension://oboonakemofpalcgghocfoadofidjkkk/") -#if defined(Q_OS_MACOS) - , TARGET_DIR_CHROME("/Library/Application Support/Google/Chrome/NativeMessagingHosts") - , TARGET_DIR_CHROMIUM("/Library/Application Support/Chromium/NativeMessagingHosts") - , TARGET_DIR_FIREFOX("/Library/Application Support/Mozilla/NativeMessagingHosts") - , TARGET_DIR_VIVALDI("/Library/Application Support/Vivaldi/NativeMessagingHosts") - , TARGET_DIR_TOR_BROWSER("/Library/Application Support/TorBrowser-Data/Browser/Mozilla/NativeMessagingHosts") - , TARGET_DIR_BRAVE("/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts") - , TARGET_DIR_EDGE("/Library/Application Support/Microsoft Edge/NativeMessagingHosts") -#elif defined(Q_OS_WIN) - // clang-format off - , TARGET_DIR_CHROME("HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser") - , TARGET_DIR_CHROMIUM("HKEY_CURRENT_USER\\Software\\Chromium\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser") - // clang-format on - , TARGET_DIR_FIREFOX("HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser") - , TARGET_DIR_VIVALDI(TARGET_DIR_CHROME) - , TARGET_DIR_TOR_BROWSER(TARGET_DIR_FIREFOX) - , TARGET_DIR_BRAVE(TARGET_DIR_CHROME) - , TARGET_DIR_EDGE( - "HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser") -#else - , TARGET_DIR_CHROME("/.config/google-chrome/NativeMessagingHosts") - , TARGET_DIR_CHROMIUM("/.config/chromium/NativeMessagingHosts") - , TARGET_DIR_FIREFOX("/.mozilla/native-messaging-hosts") - , TARGET_DIR_VIVALDI("/.config/vivaldi/NativeMessagingHosts") - , TARGET_DIR_TOR_BROWSER("/.tor-browser/app/Browser/TorBrowser/Data/Browser/.mozilla/native-messaging-hosts") - , TARGET_DIR_BRAVE("/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts") - , TARGET_DIR_EDGE("/.config/microsoftedge/NativeMessagingHosts") -#endif -{ -} - -/** - * Checks if the selected browser has native messaging host properly installed - * - * @param browser Selected browser - * @return bool Script is installed correctly - */ -bool HostInstaller::checkIfInstalled(SupportedBrowsers browser) -{ - QString fileName = getPath(browser); -#ifdef Q_OS_WIN - QSettings settings(getTargetPath(browser), QSettings::NativeFormat); - return registryEntryFound(settings); -#else - return QFile::exists(fileName); -#endif -} - -/** - * Checks if keepassxc-proxy location is found - * - * @param proxy Is keepassxc-proxy enabled - * @param location Custom proxy location - * @param path The path is set here and returned to the caller - * @return bool - */ -bool HostInstaller::checkIfProxyExists(const bool& proxy, const QString& location, QString& path) const -{ - QString fileName = getProxyPath(proxy, location); - path = fileName; - return QFile::exists(fileName); -} - -/** - * Installs native messaging JSON script for the selected browser - * - * @param browser Selected browser - * @param enabled Is browser integration enabled - * @param proxy Is keepassxc-proxy enabled - * @param location Custom proxy location - */ -void HostInstaller::installBrowser(SupportedBrowsers browser, - const bool& enabled, - const bool& proxy, - const QString& location) -{ - if (enabled) { -#ifdef Q_OS_WIN - // Create a registry key - QSettings settings(getTargetPath(browser), QSettings::NativeFormat); - settings.setValue("Default", getPath(browser)); -#endif - // Always create the script file - QJsonObject script = constructFile(browser, proxy, location); - if (!saveFile(browser, script)) { - QMessageBox::critical(nullptr, - tr("KeePassXC: Cannot save file!"), - tr("Cannot save the native messaging script file."), - QMessageBox::Ok); - } - } else { - // Remove the script file - QString fileName = getPath(browser); - QFile::remove(fileName); -#ifdef Q_OS_WIN - // Remove the registry entry - QSettings settings(getTargetPath(browser), QSettings::NativeFormat); - settings.remove("Default"); -#endif - } -} - -/** - * Updates the paths to native messaging host for each browser that has been enabled - * - * @param proxy Is keepassxc-proxy enabled - * @param location Custom proxy location - */ -void HostInstaller::updateBinaryPaths(const bool& proxy, const QString& location) -{ - for (int i = 0; i <= SupportedBrowsers::EDGE; ++i) { - if (checkIfInstalled(static_cast(i))) { - installBrowser(static_cast(i), true, proxy, location); - } - } -} - -/** - * Returns the target path for each browser. Windows uses a registry path instead of a file path - * - * @param browser Selected browser - * @return QString Current target path for the selected browser - */ -QString HostInstaller::getTargetPath(SupportedBrowsers browser) const -{ - switch (browser) { - case SupportedBrowsers::CHROME: - return TARGET_DIR_CHROME; - case SupportedBrowsers::CHROMIUM: - return TARGET_DIR_CHROMIUM; - case SupportedBrowsers::FIREFOX: - return TARGET_DIR_FIREFOX; - case SupportedBrowsers::VIVALDI: - return TARGET_DIR_VIVALDI; - case SupportedBrowsers::TOR_BROWSER: - return TARGET_DIR_TOR_BROWSER; - case SupportedBrowsers::BRAVE: - return TARGET_DIR_BRAVE; - case SupportedBrowsers::EDGE: - return TARGET_DIR_EDGE; - default: - return QString(); - } -} - -/** - * Returns the browser name - * Needed for Windows to separate Chromium- or Firefox-based scripts - * - * @param browser Selected browser - * @return QString Name of the selected browser - */ -QString HostInstaller::getBrowserName(SupportedBrowsers browser) const -{ - switch (browser) { - case SupportedBrowsers::CHROME: - return "chrome"; - case SupportedBrowsers::CHROMIUM: - return "chromium"; - case SupportedBrowsers::FIREFOX: - return "firefox"; - case SupportedBrowsers::VIVALDI: - return "vivaldi"; - case SupportedBrowsers::TOR_BROWSER: - return "tor-browser"; - case SupportedBrowsers::BRAVE: - return "brave"; - case SupportedBrowsers::EDGE: - return "edge"; - default: - return QString(); - } -} - -/** - * Returns the path of native messaging JSON script for the selected browser - * - * @param browser Selected browser - * @return QString JSON script path for the selected browser - */ -QString HostInstaller::getPath(SupportedBrowsers browser) const -{ -#ifdef Q_OS_WIN - // If portable settings file exists save the JSON scripts to application folder - QString userPath; - QString portablePath = QCoreApplication::applicationDirPath() + "/keepassxc.ini"; - if (QFile::exists(portablePath)) { - userPath = QCoreApplication::applicationDirPath(); - } else { - userPath = QDir::fromNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); - } - - QString winPath = QString("%1/%2_%3.json").arg(userPath, HOST_NAME, getBrowserName(browser)); - winPath.replace("/", "\\"); - return winPath; -#else - QString path = getTargetPath(browser); - return QString("%1%2/%3.json").arg(QDir::homePath(), path, HOST_NAME); -#endif -} - -/** - * Gets the installation directory for JSON script file (application install path) - * - * @param browser Selected browser - * @return QString Install path - */ -QString HostInstaller::getInstallDir(SupportedBrowsers browser) const -{ - QString path = getTargetPath(browser); -#ifdef Q_OS_WIN - return QCoreApplication::applicationDirPath(); -#else - return QString("%1%2").arg(QDir::homePath(), path); -#endif -} - -/** - * Gets the path to keepassxc-proxy binary - * - * @param proxy Is keepassxc-proxy used with KeePassXC - * @param location Custom proxy path - * @return path Path to keepassxc-proxy - */ -QString HostInstaller::getProxyPath(const bool& proxy, const QString& location) const -{ - QString path; -#ifdef KEEPASSXC_DIST_APPIMAGE - if (proxy && !location.isEmpty()) { - path = location; - } else { - path = QProcessEnvironment::systemEnvironment().value("APPIMAGE"); - } -#else - if (proxy) { - if (!location.isEmpty()) { - path = location; - } else { - path = QFileInfo(QCoreApplication::applicationFilePath()).absolutePath(); - path.append("/keepassxc-proxy"); -#ifdef Q_OS_WIN - path.append(".exe"); -#endif - } - } else { - path = QFileInfo(QCoreApplication::applicationFilePath()).absoluteFilePath(); - } -#ifdef Q_OS_WIN - path.replace("/", "\\"); -#endif - -#endif // #ifdef KEEPASSXC_DIST_APPIMAGE - return path; -} - -/** - * Constructs the JSON script file used with native messaging - * - * @param browser Browser (Chromium- and Firefox-based browsers need a different parameters for the script) - * @param proxy Is keepassxc-proxy used with KeePassXC - * @param location Custom proxy location - * @return script The JSON script file - */ -QJsonObject HostInstaller::constructFile(SupportedBrowsers browser, const bool& proxy, const QString& location) -{ - QString path = getProxyPath(proxy, location); - - QJsonObject script; - script["name"] = HOST_NAME; - script["description"] = QString("KeePassXC integration with native messaging support"); - script["path"] = path; - script["type"] = QString("stdio"); - - QJsonArray arr; - if (browser == SupportedBrowsers::FIREFOX || browser == SupportedBrowsers::TOR_BROWSER) { - for (const QString& extension : ALLOWED_EXTENSIONS) { - arr.append(extension); - } - script["allowed_extensions"] = arr; - } else { - for (const QString& origin : ALLOWED_ORIGINS) { - arr.append(origin); - } - script["allowed_origins"] = arr; - } - - return script; -} - -/** - * Checks if a registry setting is found with default value - * - * @param settings Registry path - * @return bool Is the registry value found - */ -bool HostInstaller::registryEntryFound(const QSettings& settings) -{ - return !settings.value("Default").isNull(); -} - -/** - * Saves a JSON script file - * - * @param browser Selected browser - * @param script JSON native messaging script object - * @return bool Write succeeds - */ -bool HostInstaller::saveFile(SupportedBrowsers browser, const QJsonObject& script) -{ - QString path = getPath(browser); - QString installDir = getInstallDir(browser); - QDir dir(installDir); - if (!dir.exists()) { - QDir().mkpath(installDir); - } - - QFile scriptFile(path); - if (!scriptFile.open(QIODevice::WriteOnly)) { - return false; - } - - QJsonDocument doc(script); - return scriptFile.write(doc.toJson()) >= 0; -} diff --git a/src/browser/HostInstaller.h b/src/browser/HostInstaller.h deleted file mode 100644 index 2136d1c340..0000000000 --- a/src/browser/HostInstaller.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 . - */ - -#ifndef HOSTINSTALLER_H -#define HOSTINSTALLER_H - -#include -#include -#include - -class HostInstaller : public QObject -{ - Q_OBJECT - -public: - enum SupportedBrowsers : int - { - CHROME = 0, - CHROMIUM = 1, - FIREFOX = 2, - VIVALDI = 3, - TOR_BROWSER = 4, - BRAVE = 5, - EDGE = 6 - }; - -public: - HostInstaller(); - bool checkIfInstalled(SupportedBrowsers browser); - bool checkIfProxyExists(const bool& proxy, const QString& location, QString& path) const; - void installBrowser(SupportedBrowsers browser, - const bool& enabled, - const bool& proxy = false, - const QString& location = ""); - void updateBinaryPaths(const bool& proxy, const QString& location = ""); - -private: - QString getTargetPath(SupportedBrowsers browser) const; - QString getBrowserName(SupportedBrowsers browser) const; - QString getPath(SupportedBrowsers browser) const; - QString getInstallDir(SupportedBrowsers browser) const; - QString getProxyPath(const bool& proxy, const QString& location) const; - QJsonObject constructFile(SupportedBrowsers browser, const bool& proxy, const QString& location); - bool registryEntryFound(const QSettings& settings); - bool saveFile(SupportedBrowsers browser, const QJsonObject& script); - -private: - const QString HOST_NAME; - const QStringList ALLOWED_EXTENSIONS; - const QStringList ALLOWED_ORIGINS; - const QString TARGET_DIR_CHROME; - const QString TARGET_DIR_CHROMIUM; - const QString TARGET_DIR_FIREFOX; - const QString TARGET_DIR_VIVALDI; - const QString TARGET_DIR_TOR_BROWSER; - const QString TARGET_DIR_BRAVE; - const QString TARGET_DIR_EDGE; -}; - -#endif // HOSTINSTALLER_H diff --git a/src/browser/NativeMessageInstaller.cpp b/src/browser/NativeMessageInstaller.cpp new file mode 100644 index 0000000000..d3b3daf321 --- /dev/null +++ b/src/browser/NativeMessageInstaller.cpp @@ -0,0 +1,313 @@ +/* + * 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 "NativeMessageInstaller.h" +#include "BrowserSettings.h" +#include "config-keepassx.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace BrowserShared; + +namespace +{ + const QString HOST_NAME = QStringLiteral("org.keepassxc.keepassxc_browser"); + const QStringList ALLOWED_EXTENSIONS = QStringList() << QStringLiteral("keepassxc-browser@keepassxc.org"); + const QStringList ALLOWED_ORIGINS = QStringList() + << QStringLiteral("chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/") + << QStringLiteral("chrome-extension://oboonakemofpalcgghocfoadofidjkkk/"); +#if defined(Q_OS_MACOS) + const QString TARGET_DIR_CHROME = QStringLiteral("/Library/Application Support/Google/Chrome/NativeMessagingHosts"); + const QString TARGET_DIR_CHROMIUM = QStringLiteral("/Library/Application Support/Chromium/NativeMessagingHosts"); + const QString TARGET_DIR_FIREFOX = QStringLiteral("/Library/Application Support/Mozilla/NativeMessagingHosts"); + const QString TARGET_DIR_VIVALDI = QStringLiteral("/Library/Application Support/Vivaldi/NativeMessagingHosts"); + const QString TARGET_DIR_TOR_BROWSER = + QStringLiteral("/Library/Application Support/TorBrowser-Data/Browser/Mozilla/NativeMessagingHosts"); + const QString TARGET_DIR_BRAVE = + QStringLiteral("/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"); + const QString TARGET_DIR_EDGE = QStringLiteral("/Library/Application Support/Microsoft Edge/NativeMessagingHosts"); +#elif defined(Q_OS_WIN) + const QString TARGET_DIR_CHROME = QStringLiteral( + "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser"); + const QString TARGET_DIR_CHROMIUM = + QStringLiteral("HKEY_CURRENT_USER\\Software\\Chromium\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser"); + const QString TARGET_DIR_FIREFOX = + QStringLiteral("HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser"); + const QString TARGET_DIR_VIVALDI = TARGET_DIR_CHROME; + const QString TARGET_DIR_TOR_BROWSER = TARGET_DIR_FIREFOX; + const QString TARGET_DIR_BRAVE = TARGET_DIR_CHROME; + const QString TARGET_DIR_EDGE = QStringLiteral( + "HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser"); +#else + const QString TARGET_DIR_CHROME = QStringLiteral("/google-chrome/NativeMessagingHosts"); + const QString TARGET_DIR_CHROMIUM = QStringLiteral("/chromium/NativeMessagingHosts"); + const QString TARGET_DIR_FIREFOX = QStringLiteral("/.mozilla/native-messaging-hosts"); + const QString TARGET_DIR_VIVALDI = QStringLiteral("/vivaldi/NativeMessagingHosts"); + const QString TARGET_DIR_TOR_BROWSER = QStringLiteral( + "/torbrowser/tbb/x86_64/tor-browser_en-US/Browser/TorBrowser/Data/Browser/.mozilla/native-messaging-hosts"); + const QString TARGET_DIR_BRAVE = QStringLiteral("/BraveSoftware/Brave-Browser/NativeMessagingHosts"); + const QString TARGET_DIR_EDGE = QStringLiteral("/microsoftedge/NativeMessagingHosts"); +#endif +} // namespace + +/** + * Checks if the selected browser has native messaging host properly installed + * + * @param browser Selected browser + * @return bool Script is installed correctly + */ +bool NativeMessageInstaller::isBrowserEnabled(SupportedBrowsers browser) +{ +#ifdef Q_OS_WIN + QSettings settings(getTargetPath(browser), QSettings::NativeFormat); + return !settings.value("Default").isNull(); +#else + return QFile::exists(getNativeMessagePath(browser)); +#endif +} + +/** + * Installs native messaging JSON script for the selected browser + * + * @param browser Selected browser + * @param enabled Is browser integration enabled + */ +void NativeMessageInstaller::setBrowserEnabled(SupportedBrowsers browser, bool enabled) +{ + if (enabled) { +#ifdef Q_OS_WIN + // Create a registry key + QSettings settings(getTargetPath(browser), QSettings::NativeFormat); + settings.setValue("Default", getNativeMessagePath(browser)); +#endif + // Always create the script file + if (!createNativeMessageFile(browser)) { + QMessageBox::critical( + nullptr, + QObject::tr("Browser Plugin Failure"), + QObject::tr("Could not save the native messaging script file for %1.").arg(getBrowserName(browser)), + QMessageBox::Ok); + } + } else { + // Remove the script file + QString fileName = getNativeMessagePath(browser); + QFile::remove(fileName); +#ifdef Q_OS_WIN + // Remove the registry entry + QSettings settings(getTargetPath(browser), QSettings::NativeFormat); + settings.remove("Default"); +#endif + } +} + +/** + * Updates the paths to native messaging host for each browser that has been enabled + */ +void NativeMessageInstaller::updateBinaryPaths() +{ + for (int i = 0; i < SupportedBrowsers::MAX_SUPPORTED; ++i) { + if (isBrowserEnabled(static_cast(i))) { + setBrowserEnabled(static_cast(i), true); + } + } +} + +/** + * Returns the target path for each browser. Windows uses a registry path instead of a file path + * + * @param browser Selected browser + * @return QString Current target path for the selected browser + */ +QString NativeMessageInstaller::getTargetPath(SupportedBrowsers browser) const +{ + switch (browser) { + case SupportedBrowsers::CHROME: + return TARGET_DIR_CHROME; + case SupportedBrowsers::CHROMIUM: + return TARGET_DIR_CHROMIUM; + case SupportedBrowsers::FIREFOX: + return TARGET_DIR_FIREFOX; + case SupportedBrowsers::VIVALDI: + return TARGET_DIR_VIVALDI; + case SupportedBrowsers::TOR_BROWSER: + return TARGET_DIR_TOR_BROWSER; + case SupportedBrowsers::BRAVE: + return TARGET_DIR_BRAVE; + case SupportedBrowsers::EDGE: + return TARGET_DIR_EDGE; + default: + return {}; + } +} + +/** + * Returns the browser name + * Needed for Windows to separate Chromium- or Firefox-based scripts + * + * @param browser Selected browser + * @return QString Name of the selected browser + */ +QString NativeMessageInstaller::getBrowserName(SupportedBrowsers browser) const +{ + switch (browser) { + case SupportedBrowsers::CHROME: + return QStringLiteral("chrome"); + case SupportedBrowsers::CHROMIUM: + return QStringLiteral("chromium"); + case SupportedBrowsers::FIREFOX: + return QStringLiteral("firefox"); + case SupportedBrowsers::VIVALDI: + return QStringLiteral("vivaldi"); + case SupportedBrowsers::TOR_BROWSER: + return QStringLiteral("tor-browser"); + case SupportedBrowsers::BRAVE: + return QStringLiteral("brave"); + case SupportedBrowsers::EDGE: + return QStringLiteral("edge"); + default: + return {}; + } +} + +/** + * Returns the path of native messaging JSON script for the selected browser + * + * @param browser Selected browser + * @return QString JSON script path for the selected browser + */ +QString NativeMessageInstaller::getNativeMessagePath(SupportedBrowsers browser) const +{ + QString basePath; +#if defined(Q_OS_WIN) + // If portable settings file exists save the JSON scripts to the application folder + if (QFile::exists(QCoreApplication::applicationDirPath() + QStringLiteral("/keepassxc.ini"))) { + basePath = QCoreApplication::applicationDirPath(); + } else { + basePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + } + return QStringLiteral("%1/%2_%3.json").arg(basePath, HOST_NAME, getBrowserName(browser)); +#elif defined(Q_OS_LINUX) + if (browser == SupportedBrowsers::TOR_BROWSER) { + // Tor Browser launcher stores its config in ~/.local/share/... + basePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + } else if (browser == SupportedBrowsers::FIREFOX) { + // Firefox stores its config in ~/ + basePath = QDir::homePath(); + } else { + basePath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + } +#else + basePath = QDir::homePath(); +#endif + return QStringLiteral("%1%2/%3.json").arg(basePath, getTargetPath(browser), HOST_NAME); +} + +/** + * Gets the path to keepassxc-proxy binary + * + * @param location Custom proxy path + * @return path Path to keepassxc-proxy + */ +QString NativeMessageInstaller::getProxyPath() const +{ + if (browserSettings()->useCustomProxy()) { + return browserSettings()->customProxyLocation(); + } + + QString path; +#ifdef KEEPASSXC_DIST_APPIMAGE + path = QProcessEnvironment::systemEnvironment().value("APPIMAGE"); +#else + path = QCoreApplication::applicationDirPath() + QStringLiteral("/keepassxc-proxy"); +#ifdef Q_OS_WIN + path.append(QStringLiteral(".exe")); +#endif // #ifdef Q_OS_WIN + +#endif // #ifdef KEEPASSXC_DIST_APPIMAGE + return QDir::toNativeSeparators(path); +} + +/** + * Constructs the JSON script file used with native messaging + * + * @param browser Browser (Chromium- and Firefox-based browsers need a different parameters for the script) + * @param location Custom proxy location + * @return script The JSON script file + */ +QJsonObject NativeMessageInstaller::constructFile(SupportedBrowsers browser) +{ + QJsonObject script; + script["name"] = HOST_NAME; + script["description"] = QStringLiteral("KeePassXC integration with native messaging support"); + script["path"] = getProxyPath(); + script["type"] = QStringLiteral("stdio"); + + QJsonArray arr; + if (browser == SupportedBrowsers::FIREFOX || browser == SupportedBrowsers::TOR_BROWSER) { + for (const QString& extension : ALLOWED_EXTENSIONS) { + arr.append(extension); + } + script["allowed_extensions"] = arr; + } else { + for (const QString& origin : ALLOWED_ORIGINS) { + arr.append(origin); + } + script["allowed_origins"] = arr; + } + + return script; +} + +/** + * Saves a JSON script file + * + * @param browser Selected browser + * @param script JSON native messaging script object + * @return bool Write succeeds + */ +bool NativeMessageInstaller::createNativeMessageFile(SupportedBrowsers browser) +{ + auto path = getNativeMessagePath(browser); + + // Make the parent directory path if necessary + QDir().mkpath(QFileInfo(path).absolutePath()); + + QFile scriptFile(path); + if (!scriptFile.open(QIODevice::WriteOnly)) { + qWarning() << "Browser Plugin: Failed to open native message file for writing at " << scriptFile.fileName(); + qWarning() << scriptFile.errorString(); + return false; + } + + QJsonDocument doc(constructFile(browser)); + if (scriptFile.write(doc.toJson()) < 0) { + qWarning() << "Browser Plugin: Failed to write native message file at " << scriptFile.fileName(); + qWarning() << scriptFile.errorString(); + return false; + } + return true; +} diff --git a/src/browser/NativeMessageInstaller.h b/src/browser/NativeMessageInstaller.h new file mode 100644 index 0000000000..4c0e339eea --- /dev/null +++ b/src/browser/NativeMessageInstaller.h @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +#ifndef NATIVEMESSAGEINSTALLER_H +#define NATIVEMESSAGEINSTALLER_H + +#include "BrowserShared.h" +#include + +class NativeMessageInstaller +{ +public: + NativeMessageInstaller() = default; + + void setBrowserEnabled(BrowserShared::SupportedBrowsers browser, bool enabled); + bool isBrowserEnabled(BrowserShared::SupportedBrowsers browser); + + QString getProxyPath() const; + void updateBinaryPaths(); + +private: + QString getTargetPath(BrowserShared::SupportedBrowsers browser) const; + QString getBrowserName(BrowserShared::SupportedBrowsers browser) const; + QString getNativeMessagePath(BrowserShared::SupportedBrowsers browser) const; + QJsonObject constructFile(BrowserShared::SupportedBrowsers browser); + bool createNativeMessageFile(BrowserShared::SupportedBrowsers browser); + + Q_DISABLE_COPY(NativeMessageInstaller); +}; + +#endif // NATIVEMESSAGEINSTALLER_H diff --git a/src/browser/NativeMessagingBase.cpp b/src/browser/NativeMessagingBase.cpp deleted file mode 100644 index 208d28a1ee..0000000000 --- a/src/browser/NativeMessagingBase.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 "NativeMessagingBase.h" -#include - -#include "config-keepassx.h" - -#if defined(Q_OS_UNIX) && !defined(Q_OS_LINUX) -#include -#include -#include -#include -#endif - -#ifdef Q_OS_LINUX -#include -#include -#endif - -#ifdef Q_OS_WIN -#include -#include -#endif - -NativeMessagingBase::NativeMessagingBase(const bool enabled) -{ -#ifdef Q_OS_WIN - Q_UNUSED(enabled); - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); -#else - if (enabled) { - m_notifier.reset(new QSocketNotifier(fileno(stdin), QSocketNotifier::Read, this)); - connect(m_notifier.data(), SIGNAL(activated(int)), this, SLOT(newNativeMessage())); - } -#endif -} - -void NativeMessagingBase::newNativeMessage() -{ -#if defined(Q_OS_UNIX) && !defined(Q_OS_LINUX) - struct kevent ev[1]; - struct timespec ts = {5, 0}; - - int fd = kqueue(); - if (fd == -1) { - m_notifier->setEnabled(false); - return; - } - - EV_SET(ev, fileno(stdin), EVFILT_READ, EV_ADD, 0, 0, nullptr); - if (kevent(fd, ev, 1, nullptr, 0, &ts) == -1) { - m_notifier->setEnabled(false); - ::close(fd); - return; - } - - int ret = kevent(fd, NULL, 0, ev, 1, &ts); - if (ret < 1) { - m_notifier->setEnabled(false); - ::close(fd); - return; - } -#elif defined(Q_OS_LINUX) - int fd = epoll_create(5); - struct epoll_event event; - event.events = EPOLLIN; - event.data.fd = 0; - if (epoll_ctl(fd, EPOLL_CTL_ADD, 0, &event) != 0) { - m_notifier->setEnabled(false); - ::close(fd); - return; - } - - if (epoll_wait(fd, &event, 1, 5000) < 1) { - m_notifier->setEnabled(false); - ::close(fd); - return; - } -#endif - readLength(); -#ifndef Q_OS_WIN - ::close(fd); -#endif -} - -void NativeMessagingBase::readNativeMessages() -{ -#ifdef Q_OS_WIN - quint32 length = 0; - while (m_running.load() != 0 && !std::cin.eof()) { - length = 0; - std::cin.readsome(reinterpret_cast(&length), 4); - readStdIn(length); - QThread::msleep(100); - } -#endif -} - -QString NativeMessagingBase::jsonToString(const QJsonObject& json) const -{ - return QString(QJsonDocument(json).toJson(QJsonDocument::Compact)); -} - -void NativeMessagingBase::sendReply(const QJsonObject& json) -{ - if (!json.isEmpty()) { - sendReply(jsonToString(json)); - } -} - -void NativeMessagingBase::sendReply(const QString& reply) -{ - if (!reply.isEmpty()) { - QByteArray bytes = reply.toUtf8(); - uint len = bytes.size(); - std::cout << char(((len >> 0) & 0xFF)) << char(((len >> 8) & 0xFF)) << char(((len >> 16) & 0xFF)) - << char(((len >> 24) & 0xFF)); - std::cout << reply.toStdString() << std::flush; - } -} - -QString NativeMessagingBase::getLocalServerPath() const -{ - const QString serverPath = "/kpxc_server"; -#if defined(KEEPASSXC_DIST_SNAP) - return QProcessEnvironment::systemEnvironment().value("SNAP_USER_COMMON") + serverPath; -#elif defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - // Use XDG_RUNTIME_DIR instead of /tmp if it's available - QString path = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); - return path.isEmpty() ? QStandardPaths::writableLocation(QStandardPaths::TempLocation) + serverPath - : path + serverPath; -#else // Q_OS_MACOS, Q_OS_WIN and others - return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + serverPath; -#endif -} diff --git a/src/browser/NativeMessagingBase.h b/src/browser/NativeMessagingBase.h deleted file mode 100644 index b68208c681..0000000000 --- a/src/browser/NativeMessagingBase.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 . - */ - -#ifndef NATIVEMESSAGINGBASE_H -#define NATIVEMESSAGINGBASE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef Q_OS_WIN -#include -#include -#endif - -static const int NATIVE_MSG_MAX_LENGTH = 1024 * 1024; - -class NativeMessagingBase : public QObject -{ - Q_OBJECT - -public: - explicit NativeMessagingBase(const bool enabled); - ~NativeMessagingBase() = default; - -protected slots: - void newNativeMessage(); - -protected: - virtual void readLength() = 0; - virtual bool readStdIn(const quint32 length) = 0; - virtual void readNativeMessages(); - QString jsonToString(const QJsonObject& json) const; - void sendReply(const QJsonObject& json); - void sendReply(const QString& reply); - QString getLocalServerPath() const; - -protected: - QAtomicInt m_running; - QSharedPointer m_notifier; - QFuture m_future; -}; - -#endif // NATIVEMESSAGINGBASE_H diff --git a/src/browser/NativeMessagingHost.cpp b/src/browser/NativeMessagingHost.cpp deleted file mode 100644 index a6c3212156..0000000000 --- a/src/browser/NativeMessagingHost.cpp +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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 "NativeMessagingHost.h" -#include "BrowserSettings.h" -#include "sodium.h" -#include -#include -#include - -#ifdef Q_OS_WIN -#include -#endif - -NativeMessagingHost::NativeMessagingHost(DatabaseTabWidget* parent, const bool enabled) - : NativeMessagingBase(enabled) - , m_mutex(QMutex::Recursive) - , m_browserService(parent) - , m_browserClients(m_browserService) -{ - m_localServer.reset(new QLocalServer(this)); - m_localServer->setSocketOptions(QLocalServer::UserAccessOption); - m_running.store(0); - - if (browserSettings()->isEnabled() && m_running.load() == 0) { - run(); - } - - connect(&m_browserService, SIGNAL(databaseLocked()), this, SLOT(databaseLocked())); - connect(&m_browserService, SIGNAL(databaseUnlocked()), this, SLOT(databaseUnlocked())); -} - -NativeMessagingHost::~NativeMessagingHost() -{ - stop(); -} - -int NativeMessagingHost::init() -{ - QMutexLocker locker(&m_mutex); - return sodium_init(); -} - -void NativeMessagingHost::run() -{ - QMutexLocker locker(&m_mutex); - if (m_running.load() == 0 && init() == -1) { - return; - } - - // Update KeePassXC/keepassxc-proxy binary paths to Native Messaging scripts - if (browserSettings()->updateBinaryPath()) { - browserSettings()->updateBinaryPaths( - browserSettings()->useCustomProxy() ? browserSettings()->customProxyLocation() : ""); - } - - m_running.store(1); -#ifdef Q_OS_WIN - m_future = - QtConcurrent::run(this, static_cast(&NativeMessagingHost::readNativeMessages)); -#endif - - if (browserSettings()->supportBrowserProxy()) { - QString serverPath = getLocalServerPath(); - QFile::remove(serverPath); - - // Ensure that STDIN is not being listened when proxy is used - if (m_notifier && m_notifier->isEnabled()) { - m_notifier->setEnabled(false); - } - - if (m_localServer->isListening()) { - m_localServer->close(); - } - - m_localServer->listen(serverPath); - connect(m_localServer.data(), SIGNAL(newConnection()), this, SLOT(newLocalConnection())); - } else { - m_localServer->close(); - } -} - -void NativeMessagingHost::stop() -{ - databaseLocked(); - QMutexLocker locker(&m_mutex); - m_socketList.clear(); - m_running.testAndSetOrdered(1, 0); - m_future.waitForFinished(); - m_localServer->close(); -} - -void NativeMessagingHost::readLength() -{ - quint32 length = 0; - std::cin.read(reinterpret_cast(&length), 4); - if (!std::cin.eof() && length > 0) { - readStdIn(length); - } else { - m_notifier->setEnabled(false); - } -} - -bool NativeMessagingHost::readStdIn(const quint32 length) -{ - if (length <= 0) { - return false; - } - - QByteArray arr; - arr.reserve(length); - - QMutexLocker locker(&m_mutex); - - for (quint32 i = 0; i < length; ++i) { - int c = std::getchar(); - if (c == EOF) { - // message ended prematurely, ignore it and return - return false; - } - arr.append(static_cast(c)); - } - - if (arr.length() > 0) { - sendReply(m_browserClients.readResponse(arr)); - } - return true; -} - -void NativeMessagingHost::newLocalConnection() -{ - QLocalSocket* socket = m_localServer->nextPendingConnection(); - if (socket) { - connect(socket, SIGNAL(readyRead()), this, SLOT(newLocalMessage())); - connect(socket, SIGNAL(disconnected()), this, SLOT(disconnectSocket())); - } -} - -void NativeMessagingHost::newLocalMessage() -{ - QLocalSocket* socket = qobject_cast(QObject::sender()); - if (!socket || socket->bytesAvailable() <= 0) { - return; - } - - socket->setReadBufferSize(NATIVE_MSG_MAX_LENGTH); - int socketDesc = socket->socketDescriptor(); - if (socketDesc) { - int max = NATIVE_MSG_MAX_LENGTH; - setsockopt(socketDesc, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&max), sizeof(max)); - } - - QByteArray arr = socket->readAll(); - if (arr.isEmpty()) { - return; - } - - QMutexLocker locker(&m_mutex); - if (!m_socketList.contains(socket)) { - m_socketList.push_back(socket); - } - - QString reply = jsonToString(m_browserClients.readResponse(arr)); - if (socket && socket->isValid() && socket->state() == QLocalSocket::ConnectedState) { - QByteArray arr = reply.toUtf8(); - socket->write(arr.constData(), arr.length()); - socket->flush(); - } -} - -void NativeMessagingHost::sendReplyToAllClients(const QJsonObject& json) -{ - QString reply = jsonToString(json); - QMutexLocker locker(&m_mutex); - for (const auto socket : m_socketList) { - if (socket && socket->isValid() && socket->state() == QLocalSocket::ConnectedState) { - QByteArray arr = reply.toUtf8(); - socket->write(arr.constData(), arr.length()); - socket->flush(); - } - } -} - -void NativeMessagingHost::disconnectSocket() -{ - QLocalSocket* socket(qobject_cast(QObject::sender())); - QMutexLocker locker(&m_mutex); - for (auto s : m_socketList) { - if (s == socket) { - m_socketList.removeOne(s); - } - } -} - -void NativeMessagingHost::databaseLocked() -{ - QJsonObject response; - response["action"] = QString("database-locked"); - sendReplyToAllClients(response); -} - -void NativeMessagingHost::databaseUnlocked() -{ - QJsonObject response; - response["action"] = QString("database-unlocked"); - sendReplyToAllClients(response); -} diff --git a/src/browser/NativeMessagingHost.h b/src/browser/NativeMessagingHost.h deleted file mode 100644 index 9ce1dab60b..0000000000 --- a/src/browser/NativeMessagingHost.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 . - */ - -#ifndef NATIVEMESSAGINGHOST_H -#define NATIVEMESSAGINGHOST_H - -#include "BrowserClients.h" -#include "BrowserService.h" -#include "NativeMessagingBase.h" -#include "gui/DatabaseTabWidget.h" - -class NativeMessagingHost : public NativeMessagingBase -{ - Q_OBJECT - - typedef QList SocketList; - -public: - explicit NativeMessagingHost(DatabaseTabWidget* parent = nullptr, const bool enabled = false); - ~NativeMessagingHost() override; - int init(); - void run(); - void stop(); - -signals: - void quit(); - -private: - void readLength() override; - bool readStdIn(const quint32 length) override; - void sendReplyToAllClients(const QJsonObject& json); - -private slots: - void databaseLocked(); - void databaseUnlocked(); - void newLocalConnection(); - void newLocalMessage(); - void disconnectSocket(); - -private: - QMutex m_mutex; - BrowserService m_browserService; - BrowserClients m_browserClients; - QSharedPointer m_localServer; - SocketList m_socketList; -}; - -#endif // NATIVEMESSAGINGHOST_H diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f20f3b9d11..a94229376b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -44,6 +44,9 @@ #ifdef Q_OS_MACOS #include "gui/osutils/macutils/MacUtils.h" +#ifdef WITH_XC_TOUCHID +#include "touchid/TouchID.h" +#endif #endif #ifdef WITH_XC_UPDATECHECK @@ -56,7 +59,7 @@ #include "sshagent/AgentSettingsPage.h" #include "sshagent/SSHAgent.h" #endif -#if defined(WITH_XC_KEESHARE) +#ifdef WITH_XC_KEESHARE #include "keeshare/KeeShare.h" #include "keeshare/SettingsPageKeeShare.h" #endif @@ -66,9 +69,8 @@ #endif #ifdef WITH_XC_BROWSER -#include "browser/BrowserOptionDialog.h" -#include "browser/BrowserSettings.h" -#include "browser/NativeMessagingHost.h" +#include "browser/BrowserService.h" +#include "browser/BrowserSettingsPage.h" #endif #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) @@ -77,61 +79,6 @@ #include #endif -#include "gui/ApplicationSettingsWidget.h" -#include "gui/PasswordGeneratorWidget.h" - -#include "touchid/TouchID.h" - -#ifdef WITH_XC_BROWSER -class BrowserPlugin : public ISettingsPage -{ -public: - explicit BrowserPlugin(DatabaseTabWidget* tabWidget) - { - m_nativeMessagingHost = - QSharedPointer(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); - } - - ~BrowserPlugin() - { - } - - QString name() override - { - return QObject::tr("Browser Integration"); - } - - QIcon icon() override - { - return Resources::instance()->icon("internet-web-browser"); - } - - QWidget* createWidget() override - { - BrowserOptionDialog* dlg = new BrowserOptionDialog(); - return dlg; - } - - void loadSettings(QWidget* widget) override - { - qobject_cast(widget)->loadSettings(); - } - - void saveSettings(QWidget* widget) override - { - qobject_cast(widget)->saveSettings(); - if (browserSettings()->isEnabled()) { - m_nativeMessagingHost->run(); - } else { - m_nativeMessagingHost->stop(); - } - } - -private: - QSharedPointer m_nativeMessagingHost; -}; -#endif - const QString MainWindow::BaseWindowTitle = "KeePassXC"; MainWindow* g_MainWindow = nullptr; @@ -186,13 +133,19 @@ MainWindow::MainWindow() restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray()); restoreState(config()->get(Config::GUI_MainWindowState).toByteArray()); #ifdef WITH_XC_BROWSER - m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); + m_ui->settingsWidget->addSettingsPage(new BrowserSettingsPage()); + connect(m_ui->tabWidget, &DatabaseTabWidget::databaseLocked, browserService(), &BrowserService::databaseLocked); + connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, browserService(), &BrowserService::databaseUnlocked); + connect(m_ui->tabWidget, + &DatabaseTabWidget::activateDatabaseChanged, + browserService(), + &BrowserService::activeDatabaseChanged); #endif #ifdef WITH_XC_SSHAGENT connect(sshAgent(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); connect(sshAgent(), SIGNAL(enabledChanged(bool)), this, SLOT(agentEnabled(bool))); - m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); + m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage()); m_entryContextMenu->addSeparator(); m_entryContextMenu->addAction(m_ui->actionEntryAddToAgent); @@ -565,6 +518,15 @@ MainWindow::~MainWindow() { } +QList MainWindow::getOpenDatabases() +{ + QList dbWidgets; + for (int i = 0; i < m_ui->tabWidget->count(); ++i) { + dbWidgets << m_ui->tabWidget->databaseWidgetFromIndex(i); + } + return dbWidgets; +} + void MainWindow::showErrorMessage(const QString& message) { m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index a038b7c296..81d8212af6 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -48,6 +48,8 @@ class MainWindow : public QMainWindow MainWindow(); ~MainWindow(); + QList getOpenDatabases(); + enum StackedWidgetIndex { DatabaseTabScreen = 0, diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp index 31fe34eef2..a37f1f7428 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp @@ -34,7 +34,6 @@ DatabaseSettingsWidgetBrowser::DatabaseSettingsWidgetBrowser(QWidget* parent) , m_ui(new Ui::DatabaseSettingsWidgetBrowser()) , m_customData(new CustomData(this)) , m_customDataModel(new QStandardItemModel(this)) - , m_browserService(nullptr) { m_ui->setupUi(this); m_ui->removeCustomDataButton->setEnabled(false); @@ -254,7 +253,7 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData() return; } - m_browserService.convertAttributesToCustomData(m_db); + BrowserService::convertAttributesToCustomData(m_db); } void DatabaseSettingsWidgetBrowser::refreshDatabaseID() diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h index c3cc0b1223..51abf7f395 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h @@ -77,7 +77,6 @@ private slots: private: QPointer m_customData; QPointer m_customDataModel; - BrowserService m_browserService; }; #endif // KEEPASSXC_DATABASESETTINGSWIDGETBROWSER_H diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index 61dfd1b25d..b7bec6deb3 100755 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -1,5 +1,4 @@ -# Copyright (C) 2017 Sami Vänttinen -# Copyright (C) 2017 KeePassXC Team +# Copyright (C) 2020 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 @@ -15,19 +14,17 @@ # along with this program. If not, see . if(WITH_XC_BROWSER) - include_directories(${BROWSER_SOURCE_DIR}) - set(proxy_SOURCES - ../core/Alloc.cpp + ../browser/BrowserShared.cpp keepassxc-proxy.cpp - ${BROWSER_SOURCE_DIR}/NativeMessagingBase.cpp - NativeMessagingHost.cpp) + NativeMessagingProxy.cpp) - add_library(proxy STATIC ${proxy_SOURCES}) - target_link_libraries(proxy Qt5::Core Qt5::Network ${sodium_LIBRARY_RELEASE}) - add_executable(keepassxc-proxy keepassxc-proxy.cpp) - target_link_libraries(keepassxc-proxy proxy) + # Alloc must be defined in a static library to prevent clashing with clang ASAN definitions + add_library(proxy_alloc STATIC ../core/Alloc.cpp) + target_link_libraries(proxy_alloc PRIVATE Qt5::Core ${sodium_LIBRARY_RELEASE}) + add_executable(keepassxc-proxy ${proxy_SOURCES}) + target_link_libraries(keepassxc-proxy proxy_alloc Qt5::Core Qt5::Network) install(TARGETS keepassxc-proxy BUNDLE DESTINATION . COMPONENT Runtime RUNTIME DESTINATION ${PROXY_INSTALL_DIR} COMPONENT Runtime) @@ -56,7 +53,4 @@ if(WITH_XC_BROWSER) COMMAND ${CMAKE_COMMAND} -E copy keepassxc-proxy ${PROXY_APP_DIR}/keepassxc-proxy COMMENT "Copying keepassxc-proxy inside the application") endif() - if(MINGW) - target_link_libraries(keepassxc-proxy Wtsapi32.lib Ws2_32.lib) - endif() endif() diff --git a/src/proxy/NativeMessagingHost.cpp b/src/proxy/NativeMessagingHost.cpp deleted file mode 100644 index 44b3ab7efa..0000000000 --- a/src/proxy/NativeMessagingHost.cpp +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 "NativeMessagingHost.h" -#include - -#ifdef Q_OS_WIN -#include -#endif - -NativeMessagingHost::NativeMessagingHost() - : NativeMessagingBase(true) -{ - m_localSocket = new QLocalSocket(); - m_localSocket->connectToServer(getLocalServerPath()); - m_localSocket->setReadBufferSize(NATIVE_MSG_MAX_LENGTH); - - int socketDesc = m_localSocket->socketDescriptor(); - if (socketDesc) { - int max = NATIVE_MSG_MAX_LENGTH; - setsockopt(socketDesc, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&max), sizeof(max)); - } -#ifdef Q_OS_WIN - m_running.store(1); - m_future = QtConcurrent::run(this, &NativeMessagingHost::readNativeMessages); -#endif - connect(m_localSocket, SIGNAL(readyRead()), this, SLOT(newLocalMessage())); - connect(m_localSocket, SIGNAL(disconnected()), this, SLOT(deleteSocket())); - connect(m_localSocket, - SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), - SLOT(socketStateChanged(QLocalSocket::LocalSocketState))); -} - -NativeMessagingHost::~NativeMessagingHost() -{ -#ifdef Q_OS_WIN - m_future.waitForFinished(); -#endif -} - -void NativeMessagingHost::readNativeMessages() -{ -#ifdef Q_OS_WIN - quint32 length = 0; - while (m_running.load() == 1 && !std::cin.eof()) { - length = 0; - std::cin.read(reinterpret_cast(&length), 4); - if (!readStdIn(length)) { - QCoreApplication::quit(); - } - QThread::msleep(1); - } -#endif -} - -void NativeMessagingHost::readLength() -{ - quint32 length = 0; - std::cin.read(reinterpret_cast(&length), 4); - if (!std::cin.eof() && length > 0) { - readStdIn(length); - } else { - QCoreApplication::quit(); - } -} - -bool NativeMessagingHost::readStdIn(const quint32 length) -{ - if (length <= 0) { - return false; - } - - QByteArray arr; - arr.reserve(length); - - for (quint32 i = 0; i < length; ++i) { - int c = std::getchar(); - if (c == EOF) { - // message ended prematurely, ignore it and return - return false; - } - arr.append(static_cast(c)); - } - - if (arr.length() > 0 && m_localSocket && m_localSocket->state() == QLocalSocket::ConnectedState) { - m_localSocket->write(arr.constData(), arr.length()); - m_localSocket->flush(); - } - - return true; -} - -void NativeMessagingHost::newLocalMessage() -{ - if (!m_localSocket || m_localSocket->bytesAvailable() <= 0) { - return; - } - - QByteArray arr = m_localSocket->readAll(); - if (!arr.isEmpty()) { - sendReply(arr); - } -} - -void NativeMessagingHost::deleteSocket() -{ - if (m_notifier) { - m_notifier->setEnabled(false); - } - m_localSocket->deleteLater(); - QCoreApplication::quit(); -} - -void NativeMessagingHost::socketStateChanged(QLocalSocket::LocalSocketState socketState) -{ - if (socketState == QLocalSocket::UnconnectedState || socketState == QLocalSocket::ClosingState) { - m_running.testAndSetOrdered(1, 0); - } -} diff --git a/src/proxy/NativeMessagingProxy.cpp b/src/proxy/NativeMessagingProxy.cpp new file mode 100644 index 0000000000..a7bd1c0f38 --- /dev/null +++ b/src/proxy/NativeMessagingProxy.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 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 "NativeMessagingProxy.h" +#include "browser/BrowserShared.h" + +#include +#include + +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + +NativeMessagingProxy::NativeMessagingProxy() + : QObject() +{ + connect(this, + &NativeMessagingProxy::stdinMessage, + this, + &NativeMessagingProxy::transferStdinMessage, + Qt::QueuedConnection); + + setupStandardInput(); + setupLocalSocket(); +} + +void NativeMessagingProxy::setupStandardInput() +{ +#ifdef Q_OS_WIN + setmode(fileno(stdin), _O_BINARY); + setmode(fileno(stdout), _O_BINARY); +#endif + + QtConcurrent::run([this] { + while (std::cin.good()) { + if (std::cin.peek() != EOF) { + uint length = 0; + for (uint i = 0; i < sizeof(uint); ++i) { + length |= getchar() << (i * 8); + } + + QString msg; + msg.reserve(length); + for (uint i = 0; i < length; ++i) { + msg.append(getchar()); + } + + if (msg.length() > 0) { + emit stdinMessage(msg); + } + } + QThread::msleep(100); + } + QCoreApplication::quit(); + }); +} + +void NativeMessagingProxy::transferStdinMessage(const QString& msg) +{ + if (m_localSocket && m_localSocket->state() == QLocalSocket::ConnectedState) { + m_localSocket->write(msg.toUtf8(), msg.length()); + m_localSocket->flush(); + } +} + +void NativeMessagingProxy::setupLocalSocket() +{ + m_localSocket.reset(new QLocalSocket()); + m_localSocket->connectToServer(BrowserShared::localServerPath()); + m_localSocket->setReadBufferSize(BrowserShared::NATIVEMSG_MAX_LENGTH); + + connect(m_localSocket.data(), SIGNAL(readyRead()), this, SLOT(transferSocketMessage())); + connect(m_localSocket.data(), SIGNAL(disconnected()), this, SLOT(socketDisconnected())); +} + +void NativeMessagingProxy::transferSocketMessage() +{ + auto msg = m_localSocket->readAll(); + if (!msg.isEmpty()) { + // Explicitly write the message length as 1 byte chunks + uint len = msg.size(); + std::cout.write(reinterpret_cast(&len), sizeof(len)); + + // Write the message and flush the stream + std::cout << msg.toStdString() << std::flush; + } +} + +void NativeMessagingProxy::socketDisconnected() +{ + // Shutdown the proxy when disconnected from the application + QCoreApplication::quit(); +} diff --git a/src/proxy/NativeMessagingProxy.h b/src/proxy/NativeMessagingProxy.h new file mode 100644 index 0000000000..75e6f03ac6 --- /dev/null +++ b/src/proxy/NativeMessagingProxy.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 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 . + */ + +#ifndef NATIVEMESSAGINGPROXY_H +#define NATIVEMESSAGINGPROXY_H + +#include +#include +#include + +class QWinEventNotifier; +class QSocketNotifier; + +class NativeMessagingProxy : public QObject +{ + Q_OBJECT +public: + NativeMessagingProxy(); + ~NativeMessagingProxy() override = default; + +signals: + void stdinMessage(QString msg); + +public slots: + void transferSocketMessage(); + void transferStdinMessage(const QString& msg); + void socketDisconnected(); + +private: + void setupStandardInput(); + void setupLocalSocket(); + +private: + QScopedPointer m_localSocket; + + Q_DISABLE_COPY(NativeMessagingProxy) +}; + +#endif // NATIVEMESSAGINGPROXY_H diff --git a/src/proxy/keepassxc-proxy.cpp b/src/proxy/keepassxc-proxy.cpp index ea472b2c3e..b2a2b14587 100644 --- a/src/proxy/keepassxc-proxy.cpp +++ b/src/proxy/keepassxc-proxy.cpp @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -#include "NativeMessagingHost.h" +#include "NativeMessagingProxy.h" #include + #include #ifndef Q_OS_WIN @@ -79,6 +80,6 @@ int main(int argc, char* argv[]) #else SetConsoleCtrlHandler(static_cast(ConsoleHandler), TRUE); #endif - NativeMessagingHost host; + NativeMessagingProxy proxy; return a.exec(); } diff --git a/src/sshagent/AgentSettingsPage.cpp b/src/sshagent/AgentSettingsPage.cpp index eb86f3fce9..efadfbab8a 100644 --- a/src/sshagent/AgentSettingsPage.cpp +++ b/src/sshagent/AgentSettingsPage.cpp @@ -20,15 +20,6 @@ #include "AgentSettingsWidget.h" #include "core/Resources.h" -AgentSettingsPage::AgentSettingsPage(DatabaseTabWidget* tabWidget) -{ - Q_UNUSED(tabWidget); -} - -AgentSettingsPage::~AgentSettingsPage() -{ -} - QString AgentSettingsPage::name() { return QObject::tr("SSH Agent"); diff --git a/src/sshagent/AgentSettingsPage.h b/src/sshagent/AgentSettingsPage.h index 015dfb9ac0..33f29b0557 100644 --- a/src/sshagent/AgentSettingsPage.h +++ b/src/sshagent/AgentSettingsPage.h @@ -25,8 +25,8 @@ class AgentSettingsPage : public ISettingsPage { public: - AgentSettingsPage(DatabaseTabWidget* tabWidget); - ~AgentSettingsPage() override; + AgentSettingsPage() = default; + ~AgentSettingsPage() override = default; QString name() override; QIcon icon() override; diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index 5ddb5e8982..5b2f611788 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -16,11 +16,13 @@ */ #include "TestBrowser.h" + #include "TestGlobal.h" #include "browser/BrowserSettings.h" #include "core/Tools.h" #include "crypto/Crypto.h" #include "sodium/crypto_box.h" + #include QTEST_GUILESS_MAIN(TestBrowser) @@ -35,12 +37,12 @@ const QString CLIENTID = "testClient"; void TestBrowser::initTestCase() { QVERIFY(Crypto::init()); - m_browserService.reset(new BrowserService(nullptr)); - m_browserAction.reset(new BrowserAction(*m_browserService.data())); + m_browserService = browserService(); } -void TestBrowser::cleanupTestCase() +void TestBrowser::init() { + m_browserAction.reset(new BrowserAction()); } /** @@ -54,7 +56,7 @@ void TestBrowser::testChangePublicKeys() json["publicKey"] = PUBLICKEY; json["nonce"] = NONCE; - auto response = m_browserAction->handleAction(json); + auto response = m_browserAction->processClientMessage(json); QCOMPARE(response["action"].toString(), QString("change-public-keys")); QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false); QCOMPARE(response["success"].toString(), TRUE_STR); @@ -393,62 +395,6 @@ void TestBrowser::testSortEntries() QCOMPARE(result[3]->url(), QString("github.com/login")); } -void TestBrowser::testGetDatabaseGroups() -{ - auto db = QSharedPointer::create(); - auto* root = db->rootGroup(); - - QScopedPointer group1(new Group()); - group1->setParent(root); - group1->setName("group1"); - - QScopedPointer group2(new Group()); - group2->setParent(root); - group2->setName("group2"); - - QScopedPointer group3(new Group()); - group3->setParent(root); - group3->setName("group3"); - - QScopedPointer group2_1(new Group()); - group2_1->setParent(group2.data()); - group2_1->setName("group2_1"); - - QScopedPointer group2_2(new Group()); - group2_2->setParent(group2.data()); - group2_2->setName("group2_2"); - - QScopedPointer group2_1_1(new Group()); - group2_1_1->setParent(group2_1.data()); - group2_1_1->setName("group2_1_1"); - - auto result = m_browserService->getDatabaseGroups(db); - QCOMPARE(result.length(), 1); - - auto groups = result["groups"].toArray(); - auto first = groups.at(0); - auto children = first.toObject()["children"].toArray(); - QCOMPARE(first.toObject()["name"].toString(), QString("Root")); - QCOMPARE(children.size(), 3); - - auto firstChild = children.at(0); - auto secondChild = children.at(1); - auto thirdChild = children.at(2); - QCOMPARE(firstChild.toObject()["name"].toString(), QString("group1")); - QCOMPARE(secondChild.toObject()["name"].toString(), QString("group2")); - QCOMPARE(thirdChild.toObject()["name"].toString(), QString("group3")); - - auto childrenOfSecond = secondChild.toObject()["children"].toArray(); - auto firstOfCOS = childrenOfSecond.at(0); - auto secondOfCOS = childrenOfSecond.at(1); - QCOMPARE(firstOfCOS.toObject()["name"].toString(), QString("group2_1")); - QCOMPARE(secondOfCOS.toObject()["name"].toString(), QString("group2_2")); - - auto lastChildren = firstOfCOS.toObject()["children"].toArray(); - auto lastChild = lastChildren.at(0); - QCOMPARE(lastChild.toObject()["name"].toString(), QString("group2_1_1")); -} - QList TestBrowser::createEntries(QStringList& urls, Group* root) const { QList entries; diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h index 69ba693095..00f9d7528c 100644 --- a/tests/TestBrowser.h +++ b/tests/TestBrowser.h @@ -30,7 +30,7 @@ class TestBrowser : public QObject private slots: void initTestCase(); - void cleanupTestCase(); + void init(); void testChangePublicKeys(); void testEncryptMessage(); @@ -46,14 +46,13 @@ private slots: void testInvalidEntries(); void testSubdomainsAndPaths(); void testSortEntries(); - void testGetDatabaseGroups(); void testValidURLs(); private: QList createEntries(QStringList& urls, Group* root) const; QScopedPointer m_browserAction; - QScopedPointer m_browserService; + QPointer m_browserService; }; #endif // KEEPASSXC_TESTBROWSER_H diff --git a/tests/data/NewDatabaseBrowser.kdbx b/tests/data/NewDatabaseBrowser.kdbx index 97599fccf2d6b49d19cbf357e931ced5c8c3515f..fb327943cbf1121fbb893bc90e05763ac9999ede 100644 GIT binary patch literal 17463 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 G00016n%}qp literal 16743 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¥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 z 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 #include +#include "browser/BrowserService.h" #include "config-keepassx-tests.h" #include "core/Bootstrap.h" #include "core/Config.h" @@ -82,30 +83,19 @@ void TestGuiBrowser::initTestCase() Bootstrap::restoreMainWindowState(*m_mainWindow); m_tabWidget = m_mainWindow->findChild("tabWidget"); m_mainWindow->show(); - - // Load the NewDatabase.kdbx file into temporary storage - QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabaseBrowser.kdbx")); - QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); - QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); - sourceDbFile.close(); } // Every test starts with opening the temp database void TestGuiBrowser::init() { m_dbFile.reset(new TemporaryFile()); - // Write the temp storage to a temp database file for use in our tests - QVERIFY(m_dbFile->open()); - QCOMPARE(m_dbFile->write(m_dbData), static_cast((m_dbData.size()))); - m_dbFileName = QFileInfo(m_dbFile->fileName()).fileName(); - m_dbFilePath = m_dbFile->fileName(); - m_dbFile->close(); + m_dbFile->copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabaseBrowser.kdbx")); // make sure window is activated or focus tests may fail m_mainWindow->activateWindow(); QApplication::processEvents(); - fileDialog()->setNextFileName(m_dbFilePath); + fileDialog()->setNextFileName(m_dbFile->fileName()); triggerAction("actionDatabaseOpen"); auto* databaseOpenWidget = m_tabWidget->currentDatabaseWidget()->findChild("databaseOpenWidget"); @@ -241,6 +231,28 @@ void TestGuiBrowser::testAdditionalURLs() } } +void TestGuiBrowser::testGetDatabaseGroups() +{ + auto result = browserService()->getDatabaseGroups(); + QCOMPARE(result.length(), 1); + + auto groups = result["groups"].toArray(); + auto first = groups.at(0); + auto children = first.toObject()["children"].toArray(); + QCOMPARE(first.toObject()["name"].toString(), QString("NewDatabase")); + QCOMPARE(children.size(), 6); + + auto firstChild = children.at(0).toObject(); + auto secondChild = children.at(1).toObject(); + QCOMPARE(firstChild["name"].toString(), QString("General")); + QCOMPARE(secondChild["name"].toString(), QString("Windows")); + + auto subGroups = firstChild["children"].toArray(); + QCOMPARE(subGroups.count(), 1); + auto subGroupObj = subGroups.at(0).toObject(); + QCOMPARE(subGroupObj["name"].toString(), QString("SubGroup")); +} + void TestGuiBrowser::triggerAction(const QString& name) { auto* action = m_mainWindow->findChild(name); diff --git a/tests/gui/TestGuiBrowser.h b/tests/gui/TestGuiBrowser.h index 53a9c73c42..818a36952c 100644 --- a/tests/gui/TestGuiBrowser.h +++ b/tests/gui/TestGuiBrowser.h @@ -45,6 +45,7 @@ private slots: void testEntrySettings(); void testAdditionalURLs(); + void testGetDatabaseGroups(); private: void triggerAction(const QString& name); @@ -57,10 +58,7 @@ private slots: QPointer m_tabWidget; QPointer m_dbWidget; QSharedPointer m_db; - QByteArray m_dbData; QScopedPointer m_dbFile; - QString m_dbFileName; - QString m_dbFilePath; }; #endif // KEEPASSXC_TESTGUIBROWSER_H