diff options
Diffstat (limited to 'src/browser')
25 files changed, 3891 insertions, 0 deletions
diff --git a/src/browser/BrowserAccessControlDialog.cpp b/src/browser/BrowserAccessControlDialog.cpp new file mode 100755 index 000000000..7090a4d16 --- /dev/null +++ b/src/browser/BrowserAccessControlDialog.cpp @@ -0,0 +1,59 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "BrowserAccessControlDialog.h" +#include "ui_BrowserAccessControlDialog.h" +#include "core/Entry.h" + +BrowserAccessControlDialog::BrowserAccessControlDialog(QWidget* parent) : + QDialog(parent), + ui(new Ui::BrowserAccessControlDialog()) +{ + this->setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + + ui->setupUi(this); + connect(ui->allowButton, SIGNAL(clicked()), this, SLOT(accept())); + connect(ui->denyButton, SIGNAL(clicked()), this, SLOT(reject())); +} + +BrowserAccessControlDialog::~BrowserAccessControlDialog() +{ +} + +void BrowserAccessControlDialog::setUrl(const QString& url) +{ + ui->label->setText(QString(tr("%1 has requested access to passwords for the following item(s).\n" + "Please select whether you want to allow access.")).arg(QUrl(url).host())); +} + +void BrowserAccessControlDialog::setItems(const QList<Entry*>& items) +{ + for (Entry* entry : items) { + ui->itemsList->addItem(entry->title() + " - " + entry->username()); + } +} + +bool BrowserAccessControlDialog::remember() const +{ + return ui->rememberDecisionCheckBox->isChecked(); +} + +void BrowserAccessControlDialog::setRemember(bool r) +{ + ui->rememberDecisionCheckBox->setChecked(r); +} diff --git a/src/browser/BrowserAccessControlDialog.h b/src/browser/BrowserAccessControlDialog.h new file mode 100755 index 000000000..d2a66ce0d --- /dev/null +++ b/src/browser/BrowserAccessControlDialog.h @@ -0,0 +1,48 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERACCESSCONTROLDIALOG_H +#define BROWSERACCESSCONTROLDIALOG_H + +#include <QDialog> +#include <QScopedPointer> + +class Entry; + +namespace Ui { +class BrowserAccessControlDialog; +} + +class BrowserAccessControlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit BrowserAccessControlDialog(QWidget* parent = nullptr); + ~BrowserAccessControlDialog(); + + void setUrl(const QString& url); + void setItems(const QList<Entry*>& items); + bool remember() const; + void setRemember(bool r); + +private: + QScopedPointer<Ui::BrowserAccessControlDialog> ui; +}; + +#endif // BROWSERACCESSCONTROLDIALOG_H diff --git a/src/browser/BrowserAccessControlDialog.ui b/src/browser/BrowserAccessControlDialog.ui new file mode 100755 index 000000000..29715314d --- /dev/null +++ b/src/browser/BrowserAccessControlDialog.ui @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BrowserAccessControlDialog</class> + <widget class="QDialog" name="BrowserAccessControlDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>221</height> + </rect> + </property> + <property name="windowTitle"> + <string>KeePassXC-Browser Confirm Access</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="itemsList"/> + </item> + <item> + <widget class="QCheckBox" name="rememberDecisionCheckBox"> + <property name="text"> + <string>Remember this decision</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="allowButton"> + <property name="text"> + <string>Allow</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="denyButton"> + <property name="text"> + <string>Deny</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp new file mode 100755 index 000000000..b15d8ed59 --- /dev/null +++ b/src/browser/BrowserAction.cpp @@ -0,0 +1,554 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <QJsonDocument> +#include <QJsonParseError> +#include "BrowserAction.h" +#include "BrowserSettings.h" +#include "sodium.h" +#include "sodium/crypto_box.h" +#include "sodium/randombytes.h" +#include "config-keepassx.h" + +BrowserAction::BrowserAction(BrowserService& browserService) : + m_mutex(QMutex::Recursive), + m_browserService(browserService), + m_associated(false) +{ + +} + +QJsonObject BrowserAction::readResponse(const QJsonObject& json) +{ + if (json.isEmpty()) { + return QJsonObject(); + } + + bool triggerUnlock = false; + const QString trigger = json.value("triggerUnlock").toString(); + if (!trigger.isEmpty() && trigger.compare("true", Qt::CaseSensitive) == 0) { + triggerUnlock = true; + } + + const QString action = json.value("action").toString(); + if (action.isEmpty()) { + return QJsonObject(); + } + + QMutexLocker locker(&m_mutex); + if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !m_browserService.isDatabaseOpened()) { + if (m_clientPublicKey.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); + } else if (!m_browserService.openDatabase(triggerUnlock)) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); + } + } + + return handleAction(json); +} + + +// Private functions +/////////////////////// + +QJsonObject BrowserAction::handleAction(const QJsonObject& json) +{ + QString action = json.value("action").toString(); + if (action.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); + } + + if (action.compare("change-public-keys", Qt::CaseSensitive) == 0) { + return handleChangePublicKeys(json, action); + } else if (action.compare("get-databasehash", Qt::CaseSensitive) == 0) { + return handleGetDatabaseHash(json, action); + } else if (action.compare("associate", Qt::CaseSensitive) == 0) { + return handleAssociate(json, action); + } else if (action.compare("test-associate", Qt::CaseSensitive) == 0) { + return handleTestAssociate(json, action); + } else if (action.compare("get-logins", Qt::CaseSensitive) == 0) { + return handleGetLogins(json, action); + } else if (action.compare("generate-password", Qt::CaseSensitive) == 0) { + return handleGeneratePassword(json, action); + } else if (action.compare("set-login", Qt::CaseSensitive) == 0) { + return handleSetLogin(json, action); + } else if (action.compare("lock-database", Qt::CaseSensitive) == 0) { + return handleLockDatabase(json, action); + } + + // Action was not recognized + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); +} + +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(); + + if (clientPublicKey.isEmpty() || nonce.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); + } + + m_associated = false; + unsigned char pk[crypto_box_PUBLICKEYBYTES]; + unsigned char sk[crypto_box_SECRETKEYBYTES]; + crypto_box_keypair(pk, sk); + + const QString publicKey = getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES); + const QString secretKey = getBase64FromKey(sk, crypto_box_SECRETKEYBYTES); + m_clientPublicKey = clientPublicKey; + m_publicKey = publicKey; + m_secretKey = secretKey; + + QJsonObject response = buildMessage(incrementNonce(nonce)); + response["action"] = action; + response["publicKey"] = publicKey; + + return response; +} + +QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + if (hash.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); + } + + QString command = decrypted.value("action").toString(); + if (!command.isEmpty() && command.compare("get-databasehash", Qt::CaseSensitive) == 0) { + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); +} + +QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString key = decrypted.value("key").toString(); + if (key.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + QMutexLocker locker(&m_mutex); + if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) { + const QString id = m_browserService.storeKey(key); + if (id.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); + } + + m_associated = true; + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + message["id"] = id; + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); +} + +QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString responseKey = decrypted.value("key").toString(); + const QString id = decrypted.value("id").toString(); + if (responseKey.isEmpty() || id.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); + } + + QMutexLocker locker(&m_mutex); + const QString key = m_browserService.getKey(id); + if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + m_associated = true; + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + message["id"] = id; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QString& action) +{ + const QString hash = 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); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString url = decrypted.value("url").toString(); + if (url.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED); + } + + const QString id = decrypted.value("id").toString(); + const QString submit = decrypted.value("submitUrl").toString(); + const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, ""); + + if (users.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND); + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["count"] = users.count(); + message["entries"] = users; + message["hash"] = hash; + message["id"] = id; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const QString& action) +{ + const QString nonce = json.value("nonce").toString(); + const QString password = BrowserSettings::generatePassword(); + const QString bits = QString::number(BrowserSettings::getbits()); // For some reason this always returns 1140 bits? + + if (nonce.isEmpty() || password.isEmpty()) { + return QJsonObject(); + } + + QJsonArray arr; + QJsonObject passwd; + passwd["login"] = QString::number(password.length() * 8); //bits; + passwd["password"] = password; + arr.append(passwd); + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["entries"] = arr; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString& action) +{ + const QString hash = 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); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString url = decrypted.value("url").toString(); + if (url.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED); + } + + const QString id = decrypted.value("id").toString(); + const QString login = decrypted.value("login").toString(); + const QString password = decrypted.value("password").toString(); + const QString submitUrl = decrypted.value("submitUrl").toString(); + const QString uuid = decrypted.value("uuid").toString(); + const QString realm; + + if (uuid.isEmpty()) { + m_browserService.addEntry(id, login, password, url, submitUrl, realm); + } else { + m_browserService.updateEntry(id, uuid, login, password, url); + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["count"] = QJsonValue::Null; + message["entries"] = QJsonValue::Null; + message["error"] = ""; + message["hash"] = hash; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + if (hash.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); + } + + QString command = decrypted.value("action").toString(); + if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) { + QMutexLocker locker(&m_mutex); + m_browserService.lockDatabase(); + + const QString newNonce = incrementNonce(nonce); + QJsonObject message = buildMessage(newNonce); + + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); +} + +QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const +{ + QJsonObject response; + response["action"] = action; + response["errorCode"] = QString::number(errorCode); + response["error"] = getErrorMessage(errorCode); + return response; +} + +QJsonObject BrowserAction::buildMessage(const QString& nonce) const +{ + QJsonObject message; + message["version"] = KEEPASSX_VERSION; + message["success"] = "true"; + message["nonce"] = nonce; + return message; +} + +QJsonObject BrowserAction::buildResponse(const QString& action, const QJsonObject& message, const QString& nonce) +{ + QJsonObject response; + response["action"] = action; + response["message"] = encryptMessage(message, nonce); + response["nonce"] = nonce; + return response; +} + +QString BrowserAction::getErrorMessage(const int errorCode) const +{ + switch (errorCode) { + case ERROR_KEEPASS_DATABASE_NOT_OPENED: return QObject::tr("Database not opened"); + case ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED: return QObject::tr("Database hash not available"); + case ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED: return QObject::tr("Client public key not received"); + case ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE: return QObject::tr("Cannot decrypt message"); + case ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED: return QObject::tr("Timeout or cannot connect to KeePassXC"); + case ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED: return QObject::tr("Action cancelled or denied"); + case ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE: return QObject::tr("Cannot encrypt message or public key not found. Is Native Messaging enabled in KeePassXC?"); + case ERROR_KEEPASS_ASSOCIATION_FAILED: return QObject::tr("KeePassXC association failed, try again"); + case ERROR_KEEPASS_KEY_CHANGE_FAILED: return QObject::tr("Key change was not successful"); + case ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED: return QObject::tr("Encryption key is not recognized"); + case ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND: return QObject::tr("No saved databases found"); + case ERROR_KEEPASS_INCORRECT_ACTION: return QObject::tr("Incorrect action"); + case ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED: return QObject::tr("Empty message received"); + case ERROR_KEEPASS_NO_URL_PROVIDED: return QObject::tr("No URL provided"); + case ERROR_KEEPASS_NO_LOGINS_FOUND: return QObject::tr("No logins found"); + default: return QObject::tr("Unknown error"); + } +} + +QString BrowserAction::getDatabaseHash() +{ + 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()) { + return QString(); + } + + const QString reply(QJsonDocument(message).toJson()); + if (!reply.isEmpty()) { + return encrypt(reply, nonce); + } + + return QString(); +} + +QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& nonce, const QString& action) +{ + if (message.isEmpty() || nonce.isEmpty()) { + return QJsonObject(); + } + + QByteArray ba = decrypt(message, nonce); + if (!ba.isEmpty()) { + return getJsonObject(ba); + } + + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); +} + +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); + const QByteArray sa = base64Decode(m_secretKey); + + std::vector<unsigned char> m(ma.cbegin(), ma.cend()); + std::vector<unsigned char> n(na.cbegin(), na.cend()); + std::vector<unsigned char> ck(ca.cbegin(), ca.cend()); + std::vector<unsigned char> sk(sa.cbegin(), sa.cend()); + + std::vector<unsigned char> e; + e.resize(max_length); + + if (m.empty() || n.empty() || ck.empty() || sk.empty()) { + return QString(); + } + + if (crypto_box_easy(e.data(), m.data(), m.size(), n.data(), ck.data(), sk.data()) == 0) { + QByteArray res = getQByteArray(e.data(), (crypto_box_MACBYTES + ma.length())); + return res.toBase64(); + } + + return QString(); +} + +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); + const QByteArray sa = base64Decode(m_secretKey); + + std::vector<unsigned char> m(ma.cbegin(), ma.cend()); + std::vector<unsigned char> n(na.cbegin(), na.cend()); + std::vector<unsigned char> ck(ca.cbegin(), ca.cend()); + std::vector<unsigned char> sk(sa.cbegin(), sa.cend()); + + std::vector<unsigned char> d; + d.resize(max_length); + + if (m.empty() || n.empty() || ck.empty() || sk.empty()) { + return QByteArray(); + } + + if (crypto_box_open_easy(d.data(), m.data(), ma.length(), n.data(), ck.data(), sk.data()) == 0) { + return getQByteArray(d.data(), std::char_traits<char>::length(reinterpret_cast<const char *>(d.data()))); + } + + return QByteArray(); +} + +QString BrowserAction::getBase64FromKey(const uchar* array, const uint len) +{ + return getQByteArray(array, len).toBase64(); +} + +QByteArray BrowserAction::getQByteArray(const uchar* array, const uint len) const +{ + QByteArray qba; + qba.reserve(len); + for (uint i = 0; i < len; ++i) { + qba.append(static_cast<char>(array[i])); + } + return qba; +} + +QJsonObject BrowserAction::getJsonObject(const uchar* pArray, const uint len) const +{ + QByteArray arr = getQByteArray(pArray, len); + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(arr, &err)); + return doc.object(); +} + +QJsonObject BrowserAction::getJsonObject(const QByteArray ba) const +{ + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(ba, &err)); + return doc.object(); +} + +QByteArray BrowserAction::base64Decode(const QString str) +{ + return QByteArray::fromBase64(str.toUtf8()); +} + +QString BrowserAction::incrementNonce(const QString& nonce) +{ + const QByteArray nonceArray = base64Decode(nonce); + std::vector<unsigned char> n(nonceArray.cbegin(), nonceArray.cend()); + + sodium_increment(n.data(), n.size()); + return getQByteArray(n.data(), n.size()).toBase64(); +} + +void BrowserAction::removeSharedEncryptionKeys() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeSharedEncryptionKeys(); +} + +void BrowserAction::removeStoredPermissions() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeStoredPermissions(); +} diff --git a/src/browser/BrowserAction.h b/src/browser/BrowserAction.h new file mode 100755 index 000000000..c4d59d3c9 --- /dev/null +++ b/src/browser/BrowserAction.h @@ -0,0 +1,98 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERACTION_H +#define BROWSERACTION_H + +#include <QtCore> +#include <QObject> +#include <QJsonObject> +#include <QMutex> +#include "BrowserService.h" + +class BrowserAction : public QObject +{ + 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 + }; + +public: + BrowserAction(BrowserService& browserService); + ~BrowserAction() = default; + + QJsonObject readResponse(const QJsonObject& json); + +public slots: + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +private: + QJsonObject handleAction(const QJsonObject& json); + QJsonObject handleChangePublicKeys(const QJsonObject& json, const QString& action); + QJsonObject handleGetDatabaseHash(const QJsonObject& json, const QString& action); + QJsonObject handleAssociate(const QJsonObject& json, const QString& action); + QJsonObject handleTestAssociate(const QJsonObject& json, const QString& action); + QJsonObject handleGetLogins(const QJsonObject& json, const QString& action); + QJsonObject handleGeneratePassword(const QJsonObject& json, const QString& action); + QJsonObject handleSetLogin(const QJsonObject& json, const QString& action); + QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action); + + QJsonObject buildMessage(const QString& nonce) const; + 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 getDatabaseHash(); + + QString encryptMessage(const QJsonObject& message, const QString& nonce); + QJsonObject decryptMessage(const QString& message, const QString& nonce, const QString& action = QString()); + QString encrypt(const QString plaintext, const QString nonce); + QByteArray decrypt(const QString encrypted, const QString nonce); + + QString getBase64FromKey(const uchar* array, const uint len); + QByteArray getQByteArray(const uchar* array, const uint len) const; + QJsonObject getJsonObject(const uchar* pArray, const uint len) const; + QJsonObject getJsonObject(const QByteArray ba) const; + QByteArray base64Decode(const QString str); + QString incrementNonce(const QString& nonce); + +private: + QMutex m_mutex; + BrowserService& m_browserService; + QString m_clientPublicKey; + QString m_publicKey; + QString m_secretKey; + bool m_associated; +}; + +#endif // BROWSERACTION_H diff --git a/src/browser/BrowserClients.cpp b/src/browser/BrowserClients.cpp new file mode 100755 index 000000000..9c47fdd46 --- /dev/null +++ b/src/browser/BrowserClients.cpp @@ -0,0 +1,77 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <QJsonValue> +#include <QJsonParseError> +#include "BrowserClients.h" + +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<BrowserAction> ba = QSharedPointer<BrowserAction>::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 new file mode 100755 index 000000000..f9052e7de --- /dev/null +++ b/src/browser/BrowserClients.h @@ -0,0 +1,56 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERCLIENTS_H +#define BROWSERCLIENTS_H + +#include <QJsonObject> +#include <QMutex> +#include <QVector> +#include <QSharedPointer> +#include <QLocalSocket> +#include "BrowserAction.h" + +class BrowserClients +{ + struct Client { + Client(const QString& id, QSharedPointer<BrowserAction> ba) : clientID(id), browserAction(ba) {} + QString clientID; + QSharedPointer<BrowserAction> browserAction; + }; + + typedef QSharedPointer<Client> 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<ClientPtr> m_clients; + BrowserService& m_browserService; +}; + +#endif // BROWSERCLIENTS_H diff --git a/src/browser/BrowserEntryConfig.cpp b/src/browser/BrowserEntryConfig.cpp new file mode 100644 index 000000000..36d0c7339 --- /dev/null +++ b/src/browser/BrowserEntryConfig.cpp @@ -0,0 +1,109 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "BrowserEntryConfig.h" +#include <QtCore> +#include "core/Entry.h" +#include "core/EntryAttributes.h" + +static const char KEEPASSBROWSER_NAME[] = "KeePassXC-Browser Settings"; + + +BrowserEntryConfig::BrowserEntryConfig(QObject* parent) : + QObject(parent) +{ +} + +QStringList BrowserEntryConfig::allowedHosts() const +{ + return m_allowedHosts.toList(); +} + +void BrowserEntryConfig::setAllowedHosts(const QStringList& allowedHosts) +{ + m_allowedHosts = allowedHosts.toSet(); +} + +QStringList BrowserEntryConfig::deniedHosts() const +{ + return m_deniedHosts.toList(); +} + +void BrowserEntryConfig::setDeniedHosts(const QStringList& deniedHosts) +{ + m_deniedHosts = deniedHosts.toSet(); +} + +bool BrowserEntryConfig::isAllowed(const QString& host) const +{ + return m_allowedHosts.contains(host); +} + +void BrowserEntryConfig::allow(const QString& host) +{ + m_allowedHosts.insert(host); + m_deniedHosts.remove(host); +} + +bool BrowserEntryConfig::isDenied(const QString& host) const +{ + return m_deniedHosts.contains(host); +} + +void BrowserEntryConfig::deny(const QString& host) +{ + m_deniedHosts.insert(host); + m_allowedHosts.remove(host); +} + +QString BrowserEntryConfig::realm() const +{ + return m_realm; +} + +void BrowserEntryConfig::setRealm(const QString& realm) +{ + m_realm = realm; +} + +bool BrowserEntryConfig::load(const Entry* entry) +{ + QString s = entry->attributes()->value(KEEPASSBROWSER_NAME); + if (s.isEmpty()) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8()); + if (doc.isNull()) { + return false; + } + + QVariantMap map = doc.object().toVariantMap(); + for (QVariantMap::const_iterator iter = map.cbegin(); iter != map.cend(); ++iter) { + setProperty(iter.key().toLatin1(), iter.value()); + } + return true; +} + +void BrowserEntryConfig::save(Entry* entry) +{ + QVariantMap v = qo2qv(this); + QJsonObject o = QJsonObject::fromVariantMap(v); + QByteArray json = QJsonDocument(o).toJson(QJsonDocument::Compact); + entry->attributes()->set(KEEPASSBROWSER_NAME, json); +} diff --git a/src/browser/BrowserEntryConfig.h b/src/browser/BrowserEntryConfig.h new file mode 100644 index 000000000..0ffaf985b --- /dev/null +++ b/src/browser/BrowserEntryConfig.h @@ -0,0 +1,60 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERENTRYCONFIG_H +#define BROWSERENTRYCONFIG_H + +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QSet> +#include "Variant.h" + +class Entry; + +class BrowserEntryConfig : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList Allow READ allowedHosts WRITE setAllowedHosts) + Q_PROPERTY(QStringList Deny READ deniedHosts WRITE setDeniedHosts ) + Q_PROPERTY(QString Realm READ realm WRITE setRealm ) + +public: + BrowserEntryConfig(QObject* object = 0); + + bool load(const Entry* entry); + void save(Entry* entry); + bool isAllowed(const QString& host) const; + void allow(const QString& host); + bool isDenied(const QString& host) const; + void deny(const QString& host); + QString realm() const; + void setRealm(const QString& realm); + +private: + QStringList allowedHosts() const; + void setAllowedHosts(const QStringList& allowedHosts); + QStringList deniedHosts() const; + void setDeniedHosts(const QStringList& deniedHosts); + + QSet<QString> m_allowedHosts; + QSet<QString> m_deniedHosts; + QString m_realm; +}; + +#endif // BROWSERENTRYCONFIG_H diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp new file mode 100755 index 000000000..693e62d26 --- /dev/null +++ b/src/browser/BrowserOptionDialog.cpp @@ -0,0 +1,141 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "BrowserOptionDialog.h" +#include "ui_BrowserOptionDialog.h" +#include "config-keepassx.h" +#include "BrowserSettings.h" +#include "core/FilePath.h" + +#include <QMessageBox> +#include <QFileDialog> + +BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) : + QWidget(parent), + m_ui(new Ui::BrowserOptionDialog()) +{ + m_ui->setupUi(this); + connect(m_ui->removeSharedEncryptionKeys, SIGNAL(clicked()), this, SIGNAL(removeSharedEncryptionKeys())); + connect(m_ui->removeStoredPermissions, SIGNAL(clicked()), this, SIGNAL(removeStoredPermissions())); + + m_ui->warningWidget->showMessage(tr("<b>Warning:</b> The following options can be dangerous!"), MessageWidget::Warning); + m_ui->warningWidget->setCloseButtonVisible(false); + m_ui->warningWidget->setAutoHideTimeout(-1); + + m_ui->tabWidget->setEnabled(m_ui->enableBrowserSupport->isChecked()); + connect(m_ui->enableBrowserSupport, SIGNAL(toggled(bool)), m_ui->tabWidget, SLOT(setEnabled(bool))); + + m_ui->customProxyLocation->setEnabled(m_ui->useCustomProxy->isChecked()); + 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->customProxyLocationBrowseButton, SIGNAL(clicked()), this, SLOT(showProxyLocationFileDialog())); + + m_ui->browserGlobalWarningWidget->setVisible(false); +} + +BrowserOptionDialog::~BrowserOptionDialog() +{ +} + +void BrowserOptionDialog::loadSettings() +{ + BrowserSettings settings; + m_ui->enableBrowserSupport->setChecked(settings.isEnabled()); + + m_ui->showNotification->setChecked(settings.showNotification()); + m_ui->bestMatchOnly->setChecked(settings.bestMatchOnly()); + m_ui->unlockDatabase->setChecked(settings.unlockDatabase()); + m_ui->matchUrlScheme->setChecked(settings.matchUrlScheme()); + + // hide unimplemented options + // TODO: fix this + m_ui->showNotification->hide(); + m_ui->bestMatchOnly->hide(); + + if (settings.sortByUsername()) { + m_ui->sortByUsername->setChecked(true); + } else { + m_ui->sortByTitle->setChecked(true); + } + + m_ui->alwaysAllowAccess->setChecked(settings.alwaysAllowAccess()); + m_ui->alwaysAllowUpdate->setChecked(settings.alwaysAllowUpdate()); + m_ui->searchInAllDatabases->setChecked(settings.searchInAllDatabases()); + m_ui->supportKphFields->setChecked(settings.supportKphFields()); + 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->chromeSupport->setChecked(settings.chromeSupport()); + m_ui->chromiumSupport->setChecked(settings.chromiumSupport()); + m_ui->firefoxSupport->setChecked(settings.firefoxSupport()); + m_ui->vivaldiSupport->setChecked(settings.vivaldiSupport()); + +#if defined(KEEPASSXC_DIST_APPIMAGE) + m_ui->supportBrowserProxy->setChecked(true); + m_ui->supportBrowserProxy->setEnabled(false); +#elif defined(KEEPASSXC_DIST_SNAP) + m_ui->enableBrowserSupport->setChecked(false); + m_ui->enableBrowserSupport->setEnabled(false); + m_ui->browserGlobalWarningWidget->showMessage( + tr("We're sorry, but KeePassXC-Browser is not supported for Snap releases at the moment."), MessageWidget::Warning); + m_ui->browserGlobalWarningWidget->setCloseButtonVisible(false); + m_ui->browserGlobalWarningWidget->setAutoHideTimeout(-1); +#endif +} + +void BrowserOptionDialog::saveSettings() +{ + BrowserSettings settings; + settings.setEnabled(m_ui->enableBrowserSupport->isChecked()); + settings.setShowNotification(m_ui->showNotification->isChecked()); + settings.setBestMatchOnly(m_ui->bestMatchOnly->isChecked()); + settings.setUnlockDatabase(m_ui->unlockDatabase->isChecked()); + 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()); + + settings.setUpdateBinaryPath(m_ui->updateBinaryPath->isChecked()); + settings.setAlwaysAllowAccess(m_ui->alwaysAllowAccess->isChecked()); + settings.setAlwaysAllowUpdate(m_ui->alwaysAllowUpdate->isChecked()); + settings.setSearchInAllDatabases(m_ui->searchInAllDatabases->isChecked()); + settings.setSupportKphFields(m_ui->supportKphFields->isChecked()); + + settings.setChromeSupport(m_ui->chromeSupport->isChecked()); + settings.setChromiumSupport(m_ui->chromiumSupport->isChecked()); + settings.setFirefoxSupport(m_ui->firefoxSupport->isChecked()); + settings.setVivaldiSupport(m_ui->vivaldiSupport->isChecked()); +} + +void BrowserOptionDialog::showProxyLocationFileDialog() +{ +#ifdef Q_OS_WIN + QString fileTypeFilter(tr("Executable Files (*.exe);;All Files (*.*)")); +#else + QString fileTypeFilter(tr("Executable Files (*)")); +#endif + auto proxyLocation = QFileDialog::getOpenFileName(this, tr("Select custom proxy location"), + QFileInfo(QCoreApplication::applicationDirPath()).filePath(), + fileTypeFilter); + m_ui->customProxyLocation->setText(proxyLocation); +} diff --git a/src/browser/BrowserOptionDialog.h b/src/browser/BrowserOptionDialog.h new file mode 100755 index 000000000..b562f6c18 --- /dev/null +++ b/src/browser/BrowserOptionDialog.h @@ -0,0 +1,53 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSEROPTIONDIALOG_H +#define BROWSEROPTIONDIALOG_H + +#include <QWidget> +#include <QScopedPointer> + +namespace Ui { +class BrowserOptionDialog; +} + +class BrowserOptionDialog : public QWidget +{ + Q_OBJECT + +public: + explicit BrowserOptionDialog(QWidget* parent = nullptr); + ~BrowserOptionDialog(); + +public slots: + void loadSettings(); + void saveSettings(); + +signals: + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +private slots: + void showProxyLocationFileDialog(); + +private: + QScopedPointer<Ui::BrowserOptionDialog> m_ui; +}; + +#endif // BROWSEROPTIONDIALOG_H diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserOptionDialog.ui new file mode 100755 index 000000000..e82379452 --- /dev/null +++ b/src/browser/BrowserOptionDialog.ui @@ -0,0 +1,377 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BrowserOptionDialog</class> + <widget class="QWidget" name="BrowserOptionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>523</width> + <height>456</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="MessageWidget" name="browserGlobalWarningWidget" native="true"/> + </item> + <item> + <widget class="QCheckBox" name="enableBrowserSupport"> + <property name="toolTip"> + <string>This is required for accessing your databases with KeePassXC-Browser</string> + </property> + <property name="text"> + <string>Enable KeepassXC browser integration</string> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>General</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="browsersGroupBox"> + <property name="title"> + <string>Enable integration for these browsers:</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="horizontalSpacing"> + <number>40</number> + </property> + <item row="0" column="0"> + <widget class="QCheckBox" name="chromeSupport"> + <property name="text"> + <string>&Google Chrome</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="firefoxSupport"> + <property name="text"> + <string>&Firefox</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>179</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="chromiumSupport"> + <property name="text"> + <string>&Chromium</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="vivaldiSupport"> + <property name="text"> + <string>&Vivaldi</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>4</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QCheckBox" name="showNotification"> + <property name="text"> + <string extracomment="Credentials mean login data requested via browser extension">Show a &notification when credentials are requested</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="unlockDatabase"> + <property name="text"> + <string>Re&quest to unlock the database if it is locked</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="matchUrlScheme"> + <property name="toolTip"> + <string>Only entries with the same scheme (http://, https://, ...) are returned.</string> + </property> + <property name="text"> + <string>&Match URL scheme (e.g., https://...)</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="bestMatchOnly"> + <property name="toolTip"> + <string>Only returns the best matches for a specific URL instead of all entries for the whole domain.</string> + </property> + <property name="text"> + <string>&Return only best-matching credentials</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="sortByTitle"> + <property name="text"> + <string extracomment="Credentials mean login data requested via browser extension">Sort &matching credentials by title</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="sortByUsername"> + <property name="text"> + <string extracomment="Credentials mean login data requested via browser extension">Sort matching credentials by &username</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>10</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeSharedEncryptionKeys"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Disconnect all browsers</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeStoredPermissions"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Forget all remembered &permissions</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>Advanced</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="MessageWidget" name="warningWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="alwaysAllowAccess"> + <property name="text"> + <string extracomment="Credentials mean login data requested via browser extension">Never &ask before accessing credentials</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="alwaysAllowUpdate"> + <property name="text"> + <string extracomment="Credentials mean login data requested via browser extension">Never ask before &updating credentials</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="searchInAllDatabases"> + <property name="toolTip"> + <string>Only the selected database has to be connected with a client.</string> + </property> + <property name="text"> + <string extracomment="Credentials mean login data requested via browser extension">Searc&h in all opened databases for matching credentials</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="supportKphFields"> + <property name="toolTip"> + <string>Automatically creating or updating string fields is not supported.</string> + </property> + <property name="text"> + <string>&Return advanced string fields which start with "KPH: "</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="updateBinaryPath"> + <property name="toolTip"> + <string>Updates KeePassXC or keepassxc-proxy binary path automatically to native messaging scripts on startup.</string> + </property> + <property name="text"> + <string>Update &native messaging manifest files at startup</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="supportBrowserProxy"> + <property name="toolTip"> + <string>Support a proxy application between KeePassXC and browser extension.</string> + </property> + <property name="text"> + <string>Use a &proxy application between KeePassXC and browser extension</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useCustomProxy"> + <property name="toolTip"> + <string>Use a custom proxy location if you installed a proxy manually.</string> + </property> + <property name="text"> + <string comment="Meant is the proxy for KeePassXC-Browser">Use a &custom proxy location</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLineEdit" name="customProxyLocation"> + <property name="maxLength"> + <number>999</number> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="customProxyLocationBrowseButton"> + <property name="text"> + <string extracomment="Button for opening file dialog">Browse...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>MessageWidget</class> + <extends>QWidget</extends> + <header>gui/MessageWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp new file mode 100644 index 000000000..fb89e8bcb --- /dev/null +++ b/src/browser/BrowserService.cpp @@ -0,0 +1,744 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <QJsonArray> +#include <QInputDialog> +#include <QProgressDialog> +#include <QMessageBox> +#include "BrowserService.h" +#include "BrowserSettings.h" +#include "BrowserEntryConfig.h" +#include "BrowserAccessControlDialog.h" +#include "core/Database.h" +#include "core/Group.h" +#include "core/EntrySearcher.h" +#include "core/Metadata.h" +#include "core/Uuid.h" +#include "core/PasswordGenerator.h" +#include "gui/MainWindow.h" + + +// de887cc3-0363-43b8-974b-5911b8816224 +static const unsigned char KEEPASSXCBROWSER_UUID_DATA[] = { + 0xde, 0x88, 0x7c, 0xc3, 0x03, 0x63, 0x43, 0xb8, + 0x97, 0x4b, 0x59, 0x11, 0xb8, 0x81, 0x62, 0x24 +}; +static const Uuid KEEPASSXCBROWSER_UUID = Uuid(QByteArray::fromRawData(reinterpret_cast<const char *>(KEEPASSXCBROWSER_UUID_DATA), sizeof(KEEPASSXCBROWSER_UUID_DATA))); +static const char KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings"; +static const char ASSOCIATE_KEY_PREFIX[] = "Public Key: "; +static const char KEEPASSXCBROWSER_GROUP_NAME[] = "KeePassXC-Browser Passwords"; +static int KEEPASSXCBROWSER_DEFAULT_ICON = 1; + +BrowserService::BrowserService(DatabaseTabWidget* parent) : + m_dbTabWidget(parent), + m_dialogActive(false) +{ + connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), this, SLOT(databaseLocked(DatabaseWidget*))); + connect(m_dbTabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), this, SLOT(databaseUnlocked(DatabaseWidget*))); + connect(m_dbTabWidget, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), this, SLOT(activateDatabaseChanged(DatabaseWidget*))); +} + +bool BrowserService::isDatabaseOpened() const +{ + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return false; + } + + return dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode; + +} + +bool BrowserService::openDatabase(bool triggerUnlock) +{ + if (!BrowserSettings::unlockDatabase()) { + return false; + } + + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return false; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + return true; + } + + if (triggerUnlock) { + KEEPASSXC_MAIN_WINDOW->bringToFront(); + } + + return false; +} + +void BrowserService::lockDatabase() +{ + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "lockDatabase", Qt::BlockingQueuedConnection); + } + + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + dbWidget->lock(); + } +} + +QString BrowserService::getDatabaseRootUuid() +{ + Database* db = getDatabase(); + if (!db) { + return QString(); + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return QString(); + } + + return rootGroup->uuid().toHex(); +} + +QString BrowserService::getDatabaseRecycleBinUuid() +{ + Database* db = getDatabase(); + if (!db) { + return QString(); + } + + Group* recycleBin = db->metadata()->recycleBin(); + if (!recycleBin) { + return QString(); + } + return recycleBin->uuid().toHex(); +} + +Entry* BrowserService::getConfigEntry(bool create) +{ + Entry* entry = nullptr; + Database* db = getDatabase(); + if (!db) { + return nullptr; + } + + entry = db->resolveEntry(KEEPASSXCBROWSER_UUID); + if (!entry && create) { + entry = new Entry(); + entry->setTitle(QLatin1String(KEEPASSXCBROWSER_NAME)); + entry->setUuid(KEEPASSXCBROWSER_UUID); + entry->setAutoTypeEnabled(false); + entry->setGroup(db->rootGroup()); + return entry; + } + + if (entry && entry->group() == db->metadata()->recycleBin()) { + if (!create) { + return nullptr; + } else { + entry->setGroup(db->rootGroup()); + return entry; + } + } + + return entry; +} + +QString BrowserService::storeKey(const QString& key) +{ + QString id; + + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "storeKey", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, id), + Q_ARG(const QString&, key)); + return id; + } + + Entry* config = getConfigEntry(true); + if (!config) { + return {}; + } + + bool contains; + QMessageBox::StandardButton dialogResult = QMessageBox::No; + + do { + QInputDialog keyDialog; + keyDialog.setWindowTitle(tr("KeePassXC: New key association request")); + keyDialog.setLabelText(tr("You have received an association request for the above key.\n\n" + "If you would like to allow it access to your KeePassXC database,\n" + "give it a unique name to identify and accept it.")); + keyDialog.setOkButtonText(tr("Save and allow access")); + keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint); + keyDialog.show(); + keyDialog.activateWindow(); + keyDialog.raise(); + auto ok = keyDialog.exec(); + + id = keyDialog.textValue(); + + if (ok != QDialog::Accepted || id.isEmpty()) { + return {}; + } + + contains = config->attributes()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); + if (contains) { + dialogResult = QMessageBox::warning(nullptr, tr("KeePassXC: Overwrite existing key?"), + tr("A shared encryption key with the name \"%1\" " + "already exists.\nDo you want to overwrite it?") + .arg(id), + QMessageBox::Yes | QMessageBox::No); + } + } while (contains && dialogResult == QMessageBox::No); + + config->attributes()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key, true); + return id; +} + +QString BrowserService::getKey(const QString& id) +{ + Entry* config = getConfigEntry(); + if (!config) { + return QString(); + } + + return config->attributes()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); +} + +QJsonArray BrowserService::findMatchingEntries(const QString& id, const QString& url, const QString& submitUrl, const QString& realm) +{ + QJsonArray result; + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "findMatchingEntries", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QJsonArray, result), + Q_ARG(const QString&, id), + Q_ARG(const QString&, url), + Q_ARG(const QString&, submitUrl), + Q_ARG(const QString&, realm)); + return result; + } + + const bool alwaysAllowAccess = BrowserSettings::alwaysAllowAccess(); + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + + // Check entries for authorization + QList<Entry*> pwEntriesToConfirm; + QList<Entry*> pwEntries; + for (Entry* entry : searchEntries(url)) { + switch (checkAccess(entry, host, submitHost, realm)) { + case Denied: + continue; + + case Unknown: + if (alwaysAllowAccess) { + pwEntries.append(entry); + } else { + pwEntriesToConfirm.append(entry); + } + break; + + case Allowed: + pwEntries.append(entry); + break; + } + } + + // Confirm entries + if (confirmEntries(pwEntriesToConfirm, url, host, submitHost, realm)) { + pwEntries.append(pwEntriesToConfirm); + } + + if (pwEntries.isEmpty()) { + return QJsonArray(); + } + + // Sort results + pwEntries = sortEntries(pwEntries, host, submitUrl); + + // Fill the list + for (Entry* entry : pwEntries) { + result << prepareEntry(entry); + } + + return result; +} + +void BrowserService::addEntry(const QString&, const QString& login, const QString& password, const QString& url, const QString& submitUrl, const QString& realm) +{ + Group* group = findCreateAddEntryGroup(); + if (!group) { + return; + } + + Entry* entry = new Entry(); + entry->setUuid(Uuid::random()); + entry->setTitle(QUrl(url).host()); + entry->setUrl(url); + entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); + entry->setUsername(login); + entry->setPassword(password); + entry->setGroup(group); + + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + BrowserEntryConfig config; + config.allow(host); + + if (!submitHost.isEmpty()) { + config.allow(submitHost); + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); +} + +void BrowserService::updateEntry(const QString& id, const QString& uuid, const QString& login, const QString& password, const QString& url) +{ + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "updateEntry", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, id), + Q_ARG(const QString&, uuid), + Q_ARG(const QString&, login), + Q_ARG(const QString&, password), + Q_ARG(const QString&, url)); + } + + Database* db = getDatabase(); + if (!db) { + return; + } + + Entry* entry = db->resolveEntry(Uuid::fromHex(uuid)); + if (!entry) { + return; + } + + QString username = entry->username(); + if (username.isEmpty()) { + return; + } + + if (username.compare(login, Qt::CaseSensitive) != 0 || entry->password().compare(password, Qt::CaseSensitive) != 0) { + QMessageBox::StandardButton dialogResult = QMessageBox::No; + if (!BrowserSettings::alwaysAllowUpdate()) { + dialogResult = QMessageBox::warning(0, tr("KeePassXC: Update Entry"), + tr("Do you want to update the information in %1 - %2?") + .arg(QUrl(url).host()).arg(username), + QMessageBox::Yes|QMessageBox::No); + } + + if (BrowserSettings::alwaysAllowUpdate() || dialogResult == QMessageBox::Yes) { + entry->beginUpdate(); + entry->setUsername(login); + entry->setPassword(password); + entry->endUpdate(); + } + } +} + +QList<Entry*> BrowserService::searchEntries(Database* db, const QString& hostname) +{ + QList<Entry*> entries; + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return entries; + } + + for (Entry* entry : EntrySearcher().search(hostname, rootGroup, Qt::CaseInsensitive)) { + QString title = entry->title(); + QString url = entry->url(); + + // Filter to match hostname in Title and Url fields + if ((!title.isEmpty() && hostname.contains(title)) + || (!url.isEmpty() && hostname.contains(url)) + || (matchUrlScheme(title) && hostname.endsWith(QUrl(title).host())) + || (matchUrlScheme(url) && hostname.endsWith(QUrl(url).host())) ) { + entries.append(entry); + } + } + + return entries; +} + +QList<Entry*> BrowserService::searchEntries(const QString& text) +{ + // Get the list of databases to search + QList<Database*> databases; + if (BrowserSettings::searchInAllDatabases()) { + const int count = m_dbTabWidget->count(); + for (int i = 0; i < count; ++i) { + if (DatabaseWidget* dbWidget = qobject_cast<DatabaseWidget*>(m_dbTabWidget->widget(i))) { + if (Database* db = dbWidget->database()) { + databases << db; + } + } + } + } else if (Database* db = getDatabase()) { + databases << db; + } + + // Search entries matching the hostname + QString hostname = QUrl(text).host(); + QList<Entry*> entries; + do { + for (Database* db : databases) { + entries << searchEntries(db, hostname); + } + } while (entries.isEmpty() && removeFirstDomain(hostname)); + + return entries; +} + +void BrowserService::removeSharedEncryptionKeys() +{ + if (!isDatabaseOpened()) { + QMessageBox::critical(0, tr("KeePassXC: Database locked!"), + tr("The active database is locked!\n" + "Please unlock the selected database or choose another one which is unlocked."), + QMessageBox::Ok); + return; + } + + Entry* entry = getConfigEntry(); + if (!entry) { + QMessageBox::information(0, tr("KeePassXC: Settings not available!"), + tr("The active database does not contain a settings entry."), + QMessageBox::Ok); + return; + } + + QStringList keysToRemove; + for (const QString& key : entry->attributes()->keys()) { + if (key.startsWith(ASSOCIATE_KEY_PREFIX)) { + keysToRemove << key; + } + } + + if (keysToRemove.isEmpty()) { + QMessageBox::information(0, tr("KeePassXC: No keys found"), + tr("No shared encryption keys found in KeePassXC Settings."), + QMessageBox::Ok); + return; + } + + entry->beginUpdate(); + for (const QString& key : keysToRemove) { + entry->attributes()->remove(key); + } + entry->endUpdate(); + + const int count = keysToRemove.count(); + QMessageBox::information(0, tr("KeePassXC: Removed keys from database"), + tr("Successfully removed %n encryption key(s) from KeePassXC settings.", "", count), + QMessageBox::Ok); + +} + +void BrowserService::removeStoredPermissions() +{ + if (!isDatabaseOpened()) { + QMessageBox::critical(0, tr("KeePassXC: Database locked!"), + tr("The active database is locked!\n" + "Please unlock the selected database or choose another one which is unlocked."), + QMessageBox::Ok); + return; + } + + Database* db = m_dbTabWidget->currentDatabaseWidget()->database(); + if (!db) { + return; + } + + QList<Entry*> entries = db->rootGroup()->entriesRecursive(); + + QProgressDialog progress(tr("Removing stored permissions…"), tr("Abort"), 0, entries.count()); + progress.setWindowModality(Qt::WindowModal); + + uint counter = 0; + for (Entry* entry : entries) { + if (progress.wasCanceled()) { + return; + } + + if (entry->attributes()->contains(KEEPASSXCBROWSER_NAME)) { + entry->beginUpdate(); + entry->attributes()->remove(KEEPASSXCBROWSER_NAME); + entry->endUpdate(); + ++counter; + } + progress.setValue(progress.value() + 1); + } + progress.reset(); + + if (counter > 0) { + QMessageBox::information(0, tr("KeePassXC: Removed permissions"), + tr("Successfully removed permissions from %n entry(s).", "", counter), + QMessageBox::Ok); + } else { + QMessageBox::information(0, tr("KeePassXC: No entry with permissions found!"), + tr("The active database does not contain an entry with permissions."), + QMessageBox::Ok); + } +} + +QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& entryUrl) +{ + QUrl url(entryUrl); + if (url.scheme().isEmpty()) { + url.setScheme("http"); + } + + const QString submitUrl = url.toString(QUrl::StripTrailingSlash); + const QString baseSubmitUrl = url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + + QMultiMap<int, const Entry*> priorities; + for (const Entry* entry : pwEntries) { + priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry); + } + + QString field = BrowserSettings::sortByTitle() ? "Title" : "UserName"; + std::sort(pwEntries.begin(), pwEntries.end(), [&priorities, &field](const Entry* left, const Entry* right) { + int res = priorities.key(left) - priorities.key(right); + if (res == 0) { + return QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field)) < 0; + } + return res < 0; + }); + + return pwEntries; +} + +bool BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, const QString& url, const QString& host, const QString& submitHost, const QString& realm) +{ + if (pwEntriesToConfirm.isEmpty() || m_dialogActive) { + return false; + } + + m_dialogActive = true; + BrowserAccessControlDialog accessControlDialog; + accessControlDialog.setUrl(url); + accessControlDialog.setItems(pwEntriesToConfirm); + + int res = accessControlDialog.exec(); + if (accessControlDialog.remember()) { + for (Entry* entry : pwEntriesToConfirm) { + BrowserEntryConfig config; + config.load(entry); + if (res == QDialog::Accepted) { + config.allow(host); + if (!submitHost.isEmpty() && host != submitHost) + config.allow(submitHost); + } else if (res == QDialog::Rejected) { + config.deny(host); + if (!submitHost.isEmpty() && host != submitHost) { + config.deny(submitHost); + } + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); + } + } + + m_dialogActive = false; + if (res == QDialog::Accepted) { + return true; + } + + return false; +} + +QJsonObject BrowserService::prepareEntry(const Entry* entry) +{ + QJsonObject res; + res["login"] = entry->resolveMultiplePlaceholders(entry->username()); + res["password"] = entry->resolveMultiplePlaceholders(entry->password()); + res["name"] = entry->resolveMultiplePlaceholders(entry->title()); + res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuid().toHex()); + + if (BrowserSettings::supportKphFields()) { + const EntryAttributes* attr = entry->attributes(); + QJsonArray stringFields; + for (const QString& key : attr->keys()) { + if (key.startsWith(QLatin1String("KPH: "))) { + QJsonObject sField; + sField[key] = entry->resolveMultiplePlaceholders(attr->value(key)); + stringFields << sField; + } + } + res["stringFields"] = stringFields; + } + return res; +} + +BrowserService::Access BrowserService::checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm) +{ + BrowserEntryConfig config; + if (!config.load(entry)) { + return Unknown; + } + if ((config.isAllowed(host)) && (submitHost.isEmpty() || config.isAllowed(submitHost))) { + return Allowed; + } + if ((config.isDenied(host)) || (!submitHost.isEmpty() && config.isDenied(submitHost))) { + return Denied; + } + if (!realm.isEmpty() && config.realm() != realm) { + return Denied; + } + return Unknown; +} + +Group* BrowserService::findCreateAddEntryGroup() +{ + Database* db = getDatabase(); + if (!db) { + return nullptr; + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return nullptr; + } + + const QString groupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME); //TODO: setting to decide where new keys are created + + for (const Group* g : rootGroup->groupsRecursive(true)) { + if (g->name() == groupName) { + return db->resolveGroup(g->uuid()); + } + } + + Group* group = new Group(); + group->setUuid(Uuid::random()); + group->setName(groupName); + group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); + group->setParent(rootGroup); + return group; +} + +int BrowserService::sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const +{ + QUrl url(entry->url()); + if (url.scheme().isEmpty()) { + url.setScheme("http"); + } + const QString entryURL = url.toString(QUrl::StripTrailingSlash); + const QString baseEntryURL = url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (submitUrl == entryURL) { + return 100; + } + if (submitUrl.startsWith(entryURL) && entryURL != host && baseSubmitUrl != entryURL) { + return 90; + } + if (submitUrl.startsWith(baseEntryURL) && entryURL != host && baseSubmitUrl != baseEntryURL) { + return 80; + } + if (entryURL == host) { + return 70; + } + if (entryURL == baseSubmitUrl) { + return 60; + } + if (entryURL.startsWith(submitUrl)) { + return 50; + } + if (entryURL.startsWith(baseSubmitUrl) && baseSubmitUrl != host) { + return 40; + } + if (submitUrl.startsWith(entryURL)) { + return 30; + } + if (submitUrl.startsWith(baseEntryURL)) { + return 20; + } + if (entryURL.startsWith(host)) { + return 10; + } + if (host.startsWith(entryURL)) { + return 5; + } + return 0; +} + +bool BrowserService::matchUrlScheme(const QString& url) +{ + QUrl address(url); + return !address.scheme().isEmpty(); +} + +bool BrowserService::removeFirstDomain(QString& hostname) +{ + int pos = hostname.indexOf("."); + if (pos < 0) { + return false; + } + + // Don't remove the second-level domain if it's the only one + if (hostname.count(".") > 1) { + hostname = hostname.mid(pos + 1); + return !hostname.isEmpty(); + } + + // Nothing removed + return false; +} + +Database* BrowserService::getDatabase() +{ + if (DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget()) { + if (Database* db = dbWidget->database()) { + return db; + } + } + return nullptr; +} + +void BrowserService::databaseLocked(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + emit databaseLocked(); + } +} + +void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + emit databaseUnlocked(); + } +} + +void BrowserService::activateDatabaseChanged(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + auto currentMode = dbWidget->currentMode(); + if (currentMode == DatabaseWidget::ViewMode || currentMode == DatabaseWidget::EditMode) { + emit databaseUnlocked(); + } else { + emit databaseLocked(); + } + } +} diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h new file mode 100644 index 000000000..5a96e1ecd --- /dev/null +++ b/src/browser/BrowserService.h @@ -0,0 +1,82 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 or (at your option) +* version 3 of the License. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERSERVICE_H +#define BROWSERSERVICE_H + +#include <QtCore> +#include <QObject> +#include "gui/DatabaseTabWidget.h" +#include "core/Entry.h" + +enum { max_length = 16*1024 }; + +class BrowserService : public QObject +{ + Q_OBJECT + +public: + explicit BrowserService(DatabaseTabWidget* parent); + + bool isDatabaseOpened() const; + bool openDatabase(bool triggerUnlock); + QString getDatabaseRootUuid(); + QString getDatabaseRecycleBinUuid(); + Entry* getConfigEntry(bool create = false); + QString getKey(const QString& id); + void addEntry(const QString& id, const QString& login, const QString& password, const QString& url, const QString& submitUrl, const QString& realm); + QList<Entry*> searchEntries(Database* db, const QString& hostname); + QList<Entry*> searchEntries(const QString& text); + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +public slots: + QJsonArray findMatchingEntries(const QString& id, const QString& url, const QString& submitUrl, const QString& realm); + QString storeKey(const QString& key); + void updateEntry(const QString& id, const QString& uuid, const QString& login, const QString& password, const QString& url); + void databaseLocked(DatabaseWidget* dbWidget); + void databaseUnlocked(DatabaseWidget* dbWidget); + void activateDatabaseChanged(DatabaseWidget* dbWidget); + void lockDatabase(); + +signals: + void databaseLocked(); + void databaseUnlocked(); + void databaseChanged(); + +private: + enum Access { Denied, Unknown, Allowed}; + +private: + QList<Entry*> sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& submitUrl); + bool confirmEntries(QList<Entry*>& pwEntriesToConfirm, const QString& url, const QString& host, const QString& submitHost, const QString& realm); + QJsonObject prepareEntry(const Entry* entry); + Access checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm); + Group* findCreateAddEntryGroup(); + int sortPriority(const Entry* entry, const QString &host, const QString& submitUrl, const QString& baseSubmitUrl) const; + bool matchUrlScheme(const QString& url); + bool removeFirstDomain(QString& hostname); + Database* getDatabase(); + +private: + DatabaseTabWidget* const m_dbTabWidget; + bool m_dialogActive; +}; + +#endif // BROWSERSERVICE_H diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp new file mode 100755 index 000000000..ecaf8e23e --- /dev/null +++ b/src/browser/BrowserSettings.cpp @@ -0,0 +1,380 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "BrowserSettings.h" +#include "core/Config.h" + +PasswordGenerator BrowserSettings::m_passwordGenerator; +PassphraseGenerator BrowserSettings::m_passPhraseGenerator; +HostInstaller BrowserSettings::m_hostInstaller; + +bool BrowserSettings::isEnabled() +{ + return config()->get("Browser/Enabled", false).toBool(); +} + +void BrowserSettings::setEnabled(bool enabled) +{ + config()->set("Browser/Enabled", enabled); +} + +bool BrowserSettings::showNotification() +{ + return config()->get("Browser/ShowNotification", true).toBool(); +} + +void BrowserSettings::setShowNotification(bool showNotification) +{ + config()->set("Browser/ShowNotification", showNotification); +} + +bool BrowserSettings::bestMatchOnly() +{ + return config()->get("Browser/BestMatchOnly", false).toBool(); +} + +void BrowserSettings::setBestMatchOnly(bool bestMatchOnly) +{ + config()->set("Browser/BestMatchOnly", bestMatchOnly); +} + +bool BrowserSettings::unlockDatabase() +{ + return config()->get("Browser/UnlockDatabase", true).toBool(); +} + +void BrowserSettings::setUnlockDatabase(bool unlockDatabase) +{ + config()->set("Browser/UnlockDatabase", unlockDatabase); +} + +bool BrowserSettings::matchUrlScheme() +{ + return config()->get("Browser/MatchUrlScheme", true).toBool(); +} + +void BrowserSettings::setMatchUrlScheme(bool matchUrlScheme) +{ + config()->set("Browser/MatchUrlScheme", matchUrlScheme); +} + +bool BrowserSettings::sortByUsername() +{ + return config()->get("Browser/SortByUsername", false).toBool(); +} + +void BrowserSettings::setSortByUsername(bool sortByUsername) +{ + config()->set("Browser/SortByUsername", sortByUsername); +} + +bool BrowserSettings::sortByTitle() +{ + return !sortByUsername(); +} + +void BrowserSettings::setSortByTitle(bool sortByUsertitle) +{ + setSortByUsername(!sortByUsertitle); +} + +bool BrowserSettings::alwaysAllowAccess() +{ + return config()->get("Browser/AlwaysAllowAccess", false).toBool(); +} + +void BrowserSettings::setAlwaysAllowAccess(bool alwaysAllowAccess) +{ + config()->set("Browser/AlwaysAllowAccess", alwaysAllowAccess); +} + +bool BrowserSettings::alwaysAllowUpdate() +{ + return config()->get("Browser/AlwaysAllowUpdate", false).toBool(); +} + +void BrowserSettings::setAlwaysAllowUpdate(bool alwaysAllowUpdate) +{ + config()->set("Browser/AlwaysAllowUpdate", alwaysAllowUpdate); +} + +bool BrowserSettings::searchInAllDatabases() +{ + return config()->get("Browser/SearchInAllDatabases", false).toBool(); +} + +void BrowserSettings::setSearchInAllDatabases(bool searchInAllDatabases) +{ + config()->set("Browser/SearchInAllDatabases", searchInAllDatabases); +} + +bool BrowserSettings::supportKphFields() +{ + return config()->get("Browser/SupportKphFields", true).toBool(); +} + +void BrowserSettings::setSupportKphFields(bool supportKphFields) +{ + config()->set("Browser/SupportKphFields", supportKphFields); +} + +bool BrowserSettings::supportBrowserProxy() +{ + return config()->get("Browser/SupportBrowserProxy", true).toBool(); +} + +void BrowserSettings::setSupportBrowserProxy(bool enabled) +{ + config()->set("Browser/SupportBrowserProxy", enabled); +} + +bool BrowserSettings::useCustomProxy() +{ + return config()->get("Browser/UseCustomProxy", false).toBool(); +} + +void BrowserSettings::setUseCustomProxy(bool enabled) +{ + config()->set("Browser/UseCustomProxy", enabled); +} + +QString BrowserSettings::customProxyLocation() +{ + if (!useCustomProxy()) { + return QString(); + } + return config()->get("Browser/CustomProxyLocation", "").toString(); +} + +void BrowserSettings::setCustomProxyLocation(QString location) +{ + config()->set("Browser/CustomProxyLocation", location); +} + +bool BrowserSettings::updateBinaryPath() +{ + return config()->get("Browser/UpdateBinaryPath", false).toBool(); +} + +void BrowserSettings::setUpdateBinaryPath(bool enabled) +{ + config()->set("Browser/UpdateBinaryPath", enabled); +} + +bool BrowserSettings::chromeSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROME); +} + +void BrowserSettings::setChromeSupport(bool enabled) { + m_hostInstaller.installBrowser(HostInstaller::SupportedBrowsers::CHROME, enabled, supportBrowserProxy(), customProxyLocation()); +} + +bool BrowserSettings::chromiumSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROMIUM); +} + +void BrowserSettings::setChromiumSupport(bool enabled) { + m_hostInstaller.installBrowser(HostInstaller::SupportedBrowsers::CHROMIUM, enabled, supportBrowserProxy(), customProxyLocation()); +} + +bool BrowserSettings::firefoxSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::FIREFOX); +} + +void BrowserSettings::setFirefoxSupport(bool enabled) { + 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::passwordUseNumbers() +{ + return config()->get("generator/Numbers", PasswordGenerator::DefaultNumbers).toBool(); +} + +void BrowserSettings::setPasswordUseNumbers(bool useNumbers) +{ + config()->set("generator/Numbers", useNumbers); +} + +bool BrowserSettings::passwordUseLowercase() +{ + return config()->get("generator/LowerCase", PasswordGenerator::DefaultLower).toBool(); +} + +void BrowserSettings::setPasswordUseLowercase(bool useLowercase) +{ + config()->set("generator/LowerCase", useLowercase); +} + +bool BrowserSettings::passwordUseUppercase() +{ + return config()->get("generator/UpperCase", PasswordGenerator::DefaultUpper).toBool(); +} + +void BrowserSettings::setPasswordUseUppercase(bool useUppercase) +{ + config()->set("generator/UpperCase", useUppercase); +} + +bool BrowserSettings::passwordUseSpecial() +{ + return config()->get("generator/SpecialChars", PasswordGenerator::DefaultSpecial).toBool(); +} + +void BrowserSettings::setPasswordUseSpecial(bool useSpecial) +{ + config()->set("generator/SpecialChars", useSpecial); +} + +bool BrowserSettings::passwordUseEASCII() +{ + return config()->get("generator/EASCII", PasswordGenerator::DefaultEASCII).toBool(); +} + +void BrowserSettings::setPasswordUseEASCII(bool useEASCII) +{ + config()->set("generator/EASCII", useEASCII); +} + +int BrowserSettings::passPhraseWordCount() +{ + return config()->get("generator/WordCount", PassphraseGenerator::DefaultWordCount).toInt(); +} + +void BrowserSettings::setPassPhraseWordCount(int wordCount) +{ + config()->set("generator/WordCount", wordCount); +} + +QString BrowserSettings::passPhraseWordSeparator() +{ + return config()->get("generator/WordSeparator", PassphraseGenerator::DefaultSeparator).toString(); +} + +void BrowserSettings::setPassPhraseWordSeparator(QString separator) +{ + config()->set("generator/WordSeparator", separator); +} + +int BrowserSettings::generatorType() +{ + return config()->get("generator/Type", 0).toInt(); +} + +void BrowserSettings::setGeneratorType(int type) +{ + config()->set("generator/Type", type); +} + +bool BrowserSettings::passwordEveryGroup() +{ + return config()->get("generator/EnsureEvery", PasswordGenerator::DefaultFromEveryGroup).toBool(); +} + +void BrowserSettings::setPasswordEveryGroup(bool everyGroup) +{ + config()->get("generator/EnsureEvery", everyGroup); +} + +bool BrowserSettings::passwordExcludeAlike() +{ + return config()->get("generator/ExcludeAlike", PasswordGenerator::DefaultLookAlike).toBool(); +} + +void BrowserSettings::setPasswordExcludeAlike(bool excludeAlike) +{ + config()->set("generator/ExcludeAlike", excludeAlike); +} + +int BrowserSettings::passwordLength() +{ + return config()->get("generator/Length", PasswordGenerator::DefaultLength).toInt(); +} + +void BrowserSettings::setPasswordLength(int length) +{ + config()->set("generator/Length", length); + m_passwordGenerator.setLength(length); +} + +PasswordGenerator::CharClasses BrowserSettings::passwordCharClasses() +{ + PasswordGenerator::CharClasses classes; + if (passwordUseLowercase()) { + classes |= PasswordGenerator::LowerLetters; + } + if (passwordUseUppercase()) { + classes |= PasswordGenerator::UpperLetters; + } + if (passwordUseNumbers()) { + classes |= PasswordGenerator::Numbers; + } + if (passwordUseSpecial()) { + classes |= PasswordGenerator::SpecialCharacters; + } + if (passwordUseEASCII()) { + classes |= PasswordGenerator::EASCII; + } + return classes; +} + +PasswordGenerator::GeneratorFlags BrowserSettings::passwordGeneratorFlags() +{ + PasswordGenerator::GeneratorFlags flags; + if (passwordExcludeAlike()) { + flags |= PasswordGenerator::ExcludeLookAlike; + } + if (passwordEveryGroup()) { + flags |= PasswordGenerator::CharFromEveryGroup; + } + return flags; +} + +QString BrowserSettings::generatePassword() +{ + if (generatorType() == 0) { + m_passwordGenerator.setLength(passwordLength()); + m_passwordGenerator.setCharClasses(passwordCharClasses()); + m_passwordGenerator.setFlags(passwordGeneratorFlags()); + return m_passwordGenerator.generatePassword(); + } else { + m_passPhraseGenerator.setDefaultWordList(); + m_passPhraseGenerator.setWordCount(passPhraseWordCount()); + m_passPhraseGenerator.setWordSeparator(passPhraseWordSeparator()); + return m_passPhraseGenerator.generatePassphrase(); + } +} + +int BrowserSettings::getbits() +{ + return m_passwordGenerator.getbits(); +} + +void BrowserSettings::updateBinaryPaths(QString customProxyLocation) +{ + bool isProxy = supportBrowserProxy(); + m_hostInstaller.updateBinaryPaths(isProxy, customProxyLocation); +} diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h new file mode 100755 index 000000000..eb59fa5ac --- /dev/null +++ b/src/browser/BrowserSettings.h @@ -0,0 +1,105 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERSETTINGS_H +#define BROWSERSETTINGS_H + +#include "core/PasswordGenerator.h" +#include "core/PassphraseGenerator.h" +#include "HostInstaller.h" + +class BrowserSettings +{ +public: + static bool isEnabled(); + static void setEnabled(bool enabled); + + static bool showNotification(); //TODO!! + static void setShowNotification(bool showNotification); + static bool bestMatchOnly(); //TODO!! + static void setBestMatchOnly(bool bestMatchOnly); + static bool unlockDatabase(); + static void setUnlockDatabase(bool unlockDatabase); + static bool matchUrlScheme(); + static void setMatchUrlScheme(bool matchUrlScheme); + static bool sortByUsername(); + static void setSortByUsername(bool sortByUsername = true); + static bool sortByTitle(); + static void setSortByTitle(bool sortByUsertitle = true); + static bool alwaysAllowAccess(); + static void setAlwaysAllowAccess(bool alwaysAllowAccess); + static bool alwaysAllowUpdate(); + static void setAlwaysAllowUpdate(bool alwaysAllowUpdate); + static bool searchInAllDatabases();//TODO!! + static void setSearchInAllDatabases(bool searchInAllDatabases); + static bool supportKphFields(); + static void setSupportKphFields(bool supportKphFields); + + static bool supportBrowserProxy(); + static void setSupportBrowserProxy(bool enabled); + static bool useCustomProxy(); + static void setUseCustomProxy(bool enabled); + static QString customProxyLocation(); + static void setCustomProxyLocation(QString location); + static bool updateBinaryPath(); + static void setUpdateBinaryPath(bool enabled); + static bool chromeSupport(); + static void setChromeSupport(bool enabled); + static bool chromiumSupport(); + static void setChromiumSupport(bool enabled); + static bool firefoxSupport(); + static void setFirefoxSupport(bool enabled); + static bool vivaldiSupport(); + static void setVivaldiSupport(bool enabled); + + static bool passwordUseNumbers(); + static void setPasswordUseNumbers(bool useNumbers); + static bool passwordUseLowercase(); + static void setPasswordUseLowercase(bool useLowercase); + static bool passwordUseUppercase(); + static void setPasswordUseUppercase(bool useUppercase); + static bool passwordUseSpecial(); + static void setPasswordUseSpecial(bool useSpecial); + static bool passwordUseEASCII(); + static void setPasswordUseEASCII(bool useEASCII); + static int passPhraseWordCount(); + static void setPassPhraseWordCount(int wordCount); + static QString passPhraseWordSeparator(); + static void setPassPhraseWordSeparator(QString separator); + static int generatorType(); + static void setGeneratorType(int type); + static bool passwordEveryGroup(); + static void setPasswordEveryGroup(bool everyGroup); + static bool passwordExcludeAlike(); + static void setPasswordExcludeAlike(bool excludeAlike); + static int passwordLength(); + static void setPasswordLength(int length); + static PasswordGenerator::CharClasses passwordCharClasses(); + static PasswordGenerator::GeneratorFlags passwordGeneratorFlags(); + static QString generatePassword(); + static int getbits(); + static void updateBinaryPaths(QString customProxyLocation = QString()); + +private: + static PasswordGenerator m_passwordGenerator; + static PassphraseGenerator m_passPhraseGenerator; + static HostInstaller m_hostInstaller; +}; + +#endif // BROWSERSETTINGS_H diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt new file mode 100755 index 000000000..61215c181 --- /dev/null +++ b/src/browser/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +# Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +if(WITH_XC_BROWSER) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + find_package(sodium 1.0.12 REQUIRED) + + set(keepassxcbrowser_SOURCES + BrowserAccessControlDialog.cpp + BrowserAction.cpp + BrowserClients.cpp + BrowserEntryConfig.cpp + BrowserOptionDialog.cpp + BrowserService.cpp + BrowserSettings.cpp + HostInstaller.cpp + NativeMessagingBase.cpp + NativeMessagingHost.cpp + Variant.cpp + ) + + add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES}) + target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network sodium) +endif() diff --git a/src/browser/HostInstaller.cpp b/src/browser/HostInstaller.cpp new file mode 100644 index 000000000..9b27ab1cf --- /dev/null +++ b/src/browser/HostInstaller.cpp @@ -0,0 +1,240 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "HostInstaller.h" +#include "config-keepassx.h" +#include <QDir> +#include <QFile> +#include <QStandardPaths> +#include <QJsonArray> +#include <QJsonDocument> +#include <QCoreApplication> +#include <QProcessEnvironment> +#include <QMessageBox> + +const QString HostInstaller::HOST_NAME = "org.keepassxc.keepassxc_browser"; +const QStringList HostInstaller::ALLOWED_ORIGINS = QStringList() + << "chrome-extension://iopaggbpplllidnfmcghoonnokmjoicf/" + << "chrome-extension://oboonakemofpalcgghocfoadofidjkkk/"; + +const QStringList HostInstaller::ALLOWED_EXTENSIONS = QStringList() + << "keepassxc-browser@keepassxc.org"; + +#if defined(Q_OS_OSX) + const QString HostInstaller::TARGET_DIR_CHROME = "/Library/Application Support/Google/Chrome/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_CHROMIUM = "/Library/Application Support/Chromium/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_FIREFOX = "/Library/Application Support/Mozilla/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_VIVALDI = "/Library/Application Support/Vivaldi/NativeMessagingHosts"; +#elif defined(Q_OS_LINUX) + const QString HostInstaller::TARGET_DIR_CHROME = "/.config/google-chrome/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_CHROMIUM = "/.config/chromium/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_FIREFOX = "/.mozilla/native-messaging-hosts"; + const QString HostInstaller::TARGET_DIR_VIVALDI = "/.config/vivaldi/NativeMessagingHosts"; +#elif defined(Q_OS_WIN) + const QString HostInstaller::TARGET_DIR_CHROME = "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; + const QString HostInstaller::TARGET_DIR_CHROMIUM = "HKEY_CURRENT_USER\\Software\\Chromium\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; + const QString HostInstaller::TARGET_DIR_FIREFOX = "HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; + const QString HostInstaller::TARGET_DIR_VIVALDI = "HKEY_CURRENT_USER\\Software\\Vivaldi\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; +#endif + +HostInstaller::HostInstaller() +{ + +} + +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 +} + +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); + if (!registryEntryFound(settings)) { + settings.setValue("Default", getPath(browser)); + } + #endif + // Always create the script file + QJsonObject script = constructFile(browser, proxy, location); + if (!saveFile(browser, script)) { + QMessageBox::critical(0, 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); + if (registryEntryFound(settings)) { + settings.remove("Default"); + } + #endif + } +} + +void HostInstaller::updateBinaryPaths(const bool& proxy, const QString& location) +{ + for (int i = 0; i < 4; ++i) { + if (checkIfInstalled(static_cast<SupportedBrowsers>(i))) { + installBrowser(static_cast<SupportedBrowsers>(i), true, proxy, location); + } + } +} + +QString HostInstaller::getTargetPath(SupportedBrowsers browser) const +{ + switch (browser) { + case SupportedBrowsers::CHROME: return HostInstaller::TARGET_DIR_CHROME; + case SupportedBrowsers::CHROMIUM: return HostInstaller::TARGET_DIR_CHROMIUM; + case SupportedBrowsers::FIREFOX: return HostInstaller::TARGET_DIR_FIREFOX; + case SupportedBrowsers::VIVALDI: return HostInstaller::TARGET_DIR_VIVALDI; + default: return QString(); + } +} + +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"; + default: return QString(); + } +} + +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, HostInstaller::HOST_NAME, getBrowserName(browser)); + winPath.replace("/","\\"); + return winPath; +#else + QString path = getTargetPath(browser); + return QString("%1%2/%3.json").arg(QDir::homePath(), path, HostInstaller::HOST_NAME); +#endif +} + +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 +} + +QJsonObject HostInstaller::constructFile(SupportedBrowsers browser, const bool& proxy, const QString& location) +{ + 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 + + QJsonObject script; + script["name"] = HostInstaller::HOST_NAME; + script["description"] = "KeePassXC integration with native messaging support"; + script["path"] = path; + script["type"] = "stdio"; + + QJsonArray arr; + if (browser == SupportedBrowsers::FIREFOX) { + for (const QString extension : HostInstaller::ALLOWED_EXTENSIONS) { + arr.append(extension); + } + script["allowed_extensions"] = arr; + } else { + for (const QString origin : HostInstaller::ALLOWED_ORIGINS) { + arr.append(origin); + } + script["allowed_origins"] = arr; + } + + return script; +} + +bool HostInstaller::registryEntryFound(const QSettings& settings) +{ + return !settings.value("Default").isNull(); +} + +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); + qint64 bytesWritten = scriptFile.write(doc.toJson()); + if (bytesWritten < 0) { + return false; + } + + return true; +} diff --git a/src/browser/HostInstaller.h b/src/browser/HostInstaller.h new file mode 100644 index 000000000..c3fc85620 --- /dev/null +++ b/src/browser/HostInstaller.h @@ -0,0 +1,63 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef HOSTINSTALLER_H +#define HOSTINSTALLER_H + +#include <QObject> +#include <QJsonObject> +#include <QSettings> + +class HostInstaller : public QObject +{ + Q_OBJECT + +public: + enum SupportedBrowsers : int { + CHROME = 0, + CHROMIUM = 1, + FIREFOX = 2, + VIVALDI = 3 + }; + +public: + HostInstaller(); + bool checkIfInstalled(SupportedBrowsers browser); + 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; + QJsonObject constructFile(SupportedBrowsers browser, const bool& proxy, const QString& location); + bool registryEntryFound(const QSettings& settings); + bool saveFile(SupportedBrowsers browser, const QJsonObject& script); + +private: + static const QString HOST_NAME; + static const QStringList ALLOWED_EXTENSIONS; + static const QStringList ALLOWED_ORIGINS; + static const QString TARGET_DIR_CHROME; + static const QString TARGET_DIR_CHROMIUM; + static const QString TARGET_DIR_FIREFOX; + static const QString TARGET_DIR_VIVALDI; +}; + +#endif // HOSTINSTALLER_H diff --git a/src/browser/NativeMessagingBase.cpp b/src/browser/NativeMessagingBase.cpp new file mode 100644 index 000000000..743953e95 --- /dev/null +++ b/src/browser/NativeMessagingBase.cpp @@ -0,0 +1,141 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "NativeMessagingBase.h" +#include <QStandardPaths> + +#if defined(Q_OS_UNIX) && !defined(Q_OS_LINUX) +#include <sys/types.h> +#include <sys/event.h> +#include <sys/time.h> +#include <unistd.h> +#endif + +#ifdef Q_OS_LINUX +#include <sys/epoll.h> +#include <unistd.h> +#endif + +#ifdef Q_OS_WIN +#include <fcntl.h> +#include <io.h> +#endif + +NativeMessagingBase::NativeMessagingBase() +{ +#ifdef Q_OS_WIN + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#else + 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); + 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); + 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() && !std::cin.eof()) { + length = 0; + std::cin.read(reinterpret_cast<char*>(&length), 4); + readStdIn(length); + QThread::msleep(1); + } +#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()) { + uint len = reply.length(); + 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 +{ +#if defined(Q_OS_WIN) + return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/kpxc_server"; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_MAC) + // Use XDG_RUNTIME_DIR instead of /tmp/ if it's available + QString path = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + "/kpxc_server"; + return path.isEmpty() ? "/tmp/kpxc_server" : path; +#else // Q_OS_MAC and others + return "/tmp/kpxc_server"; +#endif +} diff --git a/src/browser/NativeMessagingBase.h b/src/browser/NativeMessagingBase.h new file mode 100644 index 000000000..4253b7585 --- /dev/null +++ b/src/browser/NativeMessagingBase.h @@ -0,0 +1,61 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NATIVEMESSAGINGBASE_H +#define NATIVEMESSAGINGBASE_H + +#include <QObject> +#include <QJsonObject> +#include <QJsonDocument> +#include <QFuture> +#include <QtConcurrent/QtConcurrent> +#include <QMutex> +#include <QSocketNotifier> +#include <QLocalServer> +#include <QLocalSocket> +#include <QAtomicInteger> +#include <iostream> +#include <unistd.h> + +class NativeMessagingBase : public QObject +{ + Q_OBJECT + +public: + explicit NativeMessagingBase(); + ~NativeMessagingBase() = default; + +protected slots: + void newNativeMessage(); + +protected: + virtual void readLength() = 0; + virtual void readStdIn(const quint32 length) = 0; + void readNativeMessages(); + QString jsonToString(const QJsonObject& json) const; + void sendReply(const QJsonObject& json); + void sendReply(const QString& reply); + QString getLocalServerPath() const; + +protected: + QAtomicInteger<quint8> m_running; + QSharedPointer<QSocketNotifier> m_notifier; + QFuture<void> m_future; +}; + +#endif // NATIVEMESSAGINGBASE_H diff --git a/src/browser/NativeMessagingHost.cpp b/src/browser/NativeMessagingHost.cpp new file mode 100755 index 000000000..4dfa87d51 --- /dev/null +++ b/src/browser/NativeMessagingHost.cpp @@ -0,0 +1,208 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <QMutexLocker> +#include <QtNetwork> +#include <iostream> +#include "sodium.h" +#include "NativeMessagingHost.h" +#include "BrowserSettings.h" + +NativeMessagingHost::NativeMessagingHost(DatabaseTabWidget* parent) : + NativeMessagingBase(), + m_mutex(QMutex::Recursive), + m_browserClients(m_browserService), + m_browserService(parent) +{ + m_localServer.reset(new QLocalServer(this)); + m_localServer->setSocketOptions(QLocalServer::UserAccessOption); + m_running.store(false); + + if (BrowserSettings::isEnabled() && !m_running) { + 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() && 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(true); +#ifdef Q_OS_WIN + m_future = QtConcurrent::run(this, static_cast<void(NativeMessagingHost::*)()>(&NativeMessagingHost::readNativeMessages)); +#endif + + if (BrowserSettings::supportBrowserProxy()) { + QString serverPath = getLocalServerPath(); + QFile::remove(serverPath); + + 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(true, false); + m_future.waitForFinished(); + m_localServer->close(); +} + +void NativeMessagingHost::readLength() +{ + quint32 length = 0; + std::cin.read(reinterpret_cast<char*>(&length), 4); + if (!std::cin.eof() && length > 0) { + readStdIn(length); + } else { + m_notifier->setEnabled(false); + } +} + +void NativeMessagingHost::readStdIn(const quint32 length) +{ + if (length > 0) { + QByteArray arr; + arr.reserve(length); + + for (quint32 i = 0; i < length; ++i) { + arr.append(getchar()); + } + + if (arr.length() > 0) { + QMutexLocker locker(&m_mutex); + sendReply(m_browserClients.readResponse(arr)); + } + } +} + +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<QLocalSocket*>(QObject::sender()); + + if (!socket || socket->bytesAvailable() <= 0) { + return; + } + + 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<QLocalSocket*>(QObject::sender())); + QMutexLocker locker(&m_mutex); + for (auto s : m_socketList) { + if (s == socket) { + m_socketList.removeOne(s); + } + } +} + +void NativeMessagingHost::removeSharedEncryptionKeys() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeSharedEncryptionKeys(); +} + +void NativeMessagingHost::removeStoredPermissions() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeStoredPermissions(); +} + +void NativeMessagingHost::databaseLocked() +{ + QJsonObject response; + response["action"] = "database-locked"; + sendReplyToAllClients(response); +} + +void NativeMessagingHost::databaseUnlocked() +{ + QJsonObject response; + response["action"] = "database-unlocked"; + sendReplyToAllClients(response); +} diff --git a/src/browser/NativeMessagingHost.h b/src/browser/NativeMessagingHost.h new file mode 100755 index 000000000..80825237b --- /dev/null +++ b/src/browser/NativeMessagingHost.h @@ -0,0 +1,67 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NATIVEMESSAGINGHOST_H +#define NATIVEMESSAGINGHOST_H + +#include "NativeMessagingBase.h" +#include "BrowserClients.h" +#include "BrowserService.h" +#include "gui/DatabaseTabWidget.h" + +class NativeMessagingHost : public NativeMessagingBase +{ + Q_OBJECT + + typedef QList<QLocalSocket*> SocketList; + +public: + explicit NativeMessagingHost(DatabaseTabWidget* parent = 0); + ~NativeMessagingHost(); + int init(); + void run(); + void stop(); + +public slots: + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +signals: + void quit(); + +private: + void readLength(); + void readStdIn(const quint32 length); + void sendReplyToAllClients(const QJsonObject& json); + +private slots: + void databaseLocked(); + void databaseUnlocked(); + void newLocalConnection(); + void newLocalMessage(); + void disconnectSocket(); + +private: + QMutex m_mutex; + BrowserClients m_browserClients; + BrowserService m_browserService; + QSharedPointer<QLocalServer> m_localServer; + SocketList m_socketList; +}; + +#endif // NATIVEMESSAGINGHOST_H diff --git a/src/browser/Variant.cpp b/src/browser/Variant.cpp new file mode 100644 index 000000000..2d42ac4ec --- /dev/null +++ b/src/browser/Variant.cpp @@ -0,0 +1,37 @@ +/* +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Variant.h" + +QVariantMap qo2qv(const QObject* object, const QStringList& ignoredProperties) +{ + QVariantMap result; + const QMetaObject* metaobject = object->metaObject(); + int count = metaobject->propertyCount(); + for (int i = 0; i < count; ++i) { + QMetaProperty metaproperty = metaobject->property(i); + const char* name = metaproperty.name(); + + if (ignoredProperties.contains(QLatin1String(name)) || (!metaproperty.isReadable())) { + continue; + } + + QVariant value = object->property(name); + result[QLatin1String(name)] = value; + } + return result; +} diff --git a/src/browser/Variant.h b/src/browser/Variant.h new file mode 100644 index 000000000..e467b4211 --- /dev/null +++ b/src/browser/Variant.h @@ -0,0 +1,25 @@ +/* +* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef VARIANT_H +#define VARIANT_H + +#include <QtCore> + +QVariantMap qo2qv(const QObject* object, const QStringList& ignoredProperties = QStringList(QString(QLatin1String("objectName")))); + +#endif // VARIANT_H |