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} | 42 +- 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, 1221 insertions(+), 1919 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 e57973ae8..c5fa3a1ac 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 fec5b985a..361dc2a9c 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 a8af0915e..c65409dd8 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 @@ private: 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 @@ private: 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 083df3945..000000000 --- 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 1fa3dfe17..000000000 --- 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 000000000..62c3e9cd8 --- /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 5bedd9de5..ea8e07409 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(); + + void sendClientMessage(const QJsonObject& json); + +signals: + void clientMessageReceived(const QJsonObject& json); + +private slots: + void proxyConnected(); + void readProxyMessage(); + void proxyDisconnected(); private: - void readNativeMessages() override; - void readLength() override; - bool readStdIn(const quint32 length) override; - -private: - QLocalSocket* m_localSocket; - - Q_DISABLE_COPY(NativeMessagingHost) + QPointer m_localServer; + QList m_socketList; }; #endif // NATIVEMESSAGINGHOST_H diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 5aa5e77ed..b83af627a 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); + if (m_currentDatabaseWidget) { + m_currentDatabaseWidget->lock(); } +} - DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); - if (!dbWidget) { - return; - } - - 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 3157df61f..6de5e49bf 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 @@ public: 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 @@ public: 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 @@ private: 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 @@ private: 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 @@ private: 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 7fa80ea3b..5d6514cae 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() +bool BrowserSettings::browserSupport(BrowserShared::SupportedBrowsers browser) { - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROME); + return m_nativeMessageInstaller.isBrowserEnabled(browser); } -void BrowserSettings::setChromeSupport(bool enabled) +void BrowserSettings::setBrowserSupport(BrowserShared::SupportedBrowsers browser, 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) -{ - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::FIREFOX, enabled, supportBrowserProxy(), customProxyLocation()); -} - -bool BrowserSettings::vivaldiSupport() -{ - 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) +void BrowserSettings::updateBinaryPaths() { - bool isProxy = supportBrowserProxy(); - m_hostInstaller.updateBinaryPaths(isProxy, customProxyLocation); -} - -bool BrowserSettings::checkIfProxyExists(QString& path) -{ - return m_hostInstaller.checkIfProxyExists(supportBrowserProxy(), customProxyLocation(), path); + m_nativeMessageInstaller.updateBinaryPaths(); } diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h index 9340cd0a3..3f5cceea7 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 @@ public: 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 @@ public: 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 000000000..692854bf8 --- /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 000000000..9e669b194 --- /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 8a67d62da..15b20b19a 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 5efb808e5..8f9dea62f 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 9dabde948..b3458fdcd --- 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 000000000..654201705 --- /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 000000000..02bee9c44 --- /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 7e813eb5b..bb92511bc 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 f4ffae3b7..000000000 --- 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 2136d1c34..000000000 --- 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 000000000..d3b3daf32 --- /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 000000000..4c0e339ee --- /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 208d28a1e..000000000 --- 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 b68208c68..000000000 --- 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 a6c321215..000000000 --- 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 9ce1dab60..000000000 --- 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 f20f3b9d1..a94229376 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 a038b7c29..81d8212af 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -48,6 +48,8 @@ public: MainWindow(); ~MainWindow(); + QList getOpenDatabases(); + enum StackedWidgetIndex { DatabaseTabScreen = 0, diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp index 31fe34eef..a37f1f742 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 c3cc0b122..51abf7f39 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h @@ -77,7 +77,6 @@ protected: 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 61dfd1b25..b7bec6deb 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 44b3ab7ef..000000000 --- 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 000000000..a7bd1c0f3 --- /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 000000000..75e6f03ac --- /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 ea472b2c3..b2a2b1458 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 eb86f3fce..efadfbab8 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 015dfb9ac..33f29b055 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 5ddb5e898..5b2f61178 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 69ba69309..00f9d7528 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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 #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 53a9c73c4..818a36952 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: 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