Skip to content

Commit

Permalink
Complete refactor of Browser Integration classes
Browse files Browse the repository at this point in the history
* Removed option to attach KeePassXC to the browser extension. Users must use the proxy application to communicate with KeePassXC.
* Significantly streamlined proxy code. Used same implementation of stdin/stdout interface across all platforms.
* Moved browser service entry point to BrowserService class instead of NativeMessagingHost. BrowserService now coordinates the communication to/from clients.
* Moved settings page definition out of MainWindow
* Decoupled BrowserService from DatabaseTabWidget
* Reduced complexity of various functions and cleaned the ABI (public vs private).
* Eliminated BrowserClients class, moved functionality into the BrowserService
* Renamed HostInstaller to NativeMessageInstaller and renamed NativeMessageHost to BrowserHost.
* Recognize XDG_CONFIG_HOME when installing native message file on Linux. Fix #4121 and fix #4123.
  • Loading branch information
droidmonkey committed May 13, 2020
1 parent dcff507 commit 8117e1a
Show file tree
Hide file tree
Showing 43 changed files with 1,217 additions and 1,918 deletions.
1 change: 0 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
endif()

set(BROWSER_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/browser)
add_subdirectory(browser)
add_subdirectory(proxy)
if(WITH_XC_BROWSER)
Expand Down
123 changes: 49 additions & 74 deletions src/browser/BrowserAction.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2017 Sami Vänttinen <[email protected]>
* Copyright (C) 2017 KeePassXC Team <[email protected]>
* Copyright (C) 2020 KeePassXC Team <[email protected]>
*
* 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
Expand All @@ -17,24 +16,43 @@
*/

#include "BrowserAction.h"
#include "BrowserService.h"
#include "BrowserSettings.h"
#include "NativeMessagingBase.h"
#include "BrowserShared.h"
#include "config-keepassx.h"
#include "core/Global.h"

#include <QJsonDocument>
#include <QJsonParseError>
#include <sodium.h>
#include <sodium/crypto_box.h>
#include <sodium/randombytes.h>

BrowserAction::BrowserAction(BrowserService& browserService)
: m_mutex(QMutex::Recursive)
, m_browserService(browserService)
, m_associated(false)
namespace
{
enum
{
ERROR_KEEPASS_DATABASE_NOT_OPENED = 1,
ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED = 2,
ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED = 3,
ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE = 4,
ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED = 5,
ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED = 6,
ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE = 7,
ERROR_KEEPASS_ASSOCIATION_FAILED = 8,
ERROR_KEEPASS_KEY_CHANGE_FAILED = 9,
ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED = 10,
ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND = 11,
ERROR_KEEPASS_INCORRECT_ACTION = 12,
ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED = 13,
ERROR_KEEPASS_NO_URL_PROVIDED = 14,
ERROR_KEEPASS_NO_LOGINS_FOUND = 15,
ERROR_KEEPASS_NO_GROUPS_FOUND = 16,
ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP = 17
};
}

QJsonObject BrowserAction::readResponse(const QJsonObject& json)
QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)
{
if (json.isEmpty()) {
return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED);
Expand All @@ -51,11 +69,10 @@ QJsonObject BrowserAction::readResponse(const QJsonObject& json)
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

QMutexLocker locker(&m_mutex);
if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !m_browserService.isDatabaseOpened()) {
if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !browserService()->isDatabaseOpened()) {
if (m_clientPublicKey.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED);
} else if (!m_browserService.openDatabase(triggerUnlock)) {
} else if (!browserService()->openDatabase(triggerUnlock)) {
return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
}
Expand Down Expand Up @@ -98,7 +115,6 @@ QJsonObject BrowserAction::handleAction(const QJsonObject& json)

QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QString& action)
{
QMutexLocker locker(&m_mutex);
const QString nonce = json.value("nonce").toString();
const QString clientPublicKey = json.value("publicKey").toString();

Expand Down Expand Up @@ -130,7 +146,7 @@ QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const

QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();
const QJsonObject decrypted = decryptMessage(encrypted, nonce);
Expand All @@ -153,7 +169,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const
// Update a legacy database hash if found
const QJsonArray hashes = decrypted.value("connectedKeys").toArray();
if (!hashes.isEmpty()) {
const QString legacyHash = getLegacyDatabaseHash();
const QString legacyHash = browserService()->getDatabaseHash(true);
if (hashes.contains(legacyHash)) {
message["oldHash"] = legacyHash;
}
Expand All @@ -167,7 +183,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const

QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();
const QJsonObject decrypted = decryptMessage(encrypted, nonce);
Expand All @@ -181,12 +197,11 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}

QMutexLocker locker(&m_mutex);
if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) {
// Check for identification key. If it's not found, ensure backwards compatibility and use the current public
// key
const QString idKey = decrypted.value("idKey").toString();
const QString id = m_browserService.storeKey((idKey.isEmpty() ? key : idKey));
const QString id = browserService()->storeKey((idKey.isEmpty() ? key : idKey));
if (id.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
}
Expand All @@ -205,7 +220,7 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin

QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();
const QJsonObject decrypted = decryptMessage(encrypted, nonce);
Expand All @@ -220,8 +235,7 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS
return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED);
}

QMutexLocker locker(&m_mutex);
const QString key = m_browserService.getKey(id);
const QString key = browserService()->getKey(id);
if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
Expand All @@ -238,11 +252,10 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS

QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();

QMutexLocker locker(&m_mutex);
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
Expand All @@ -269,7 +282,7 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
const QString submit = decrypted.value("submitUrl").toString();
const QString auth = decrypted.value("httpAuth").toString();
const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0 ? true : false;
const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, "", keyList, httpAuth);
const QJsonArray users = browserService()->findMatchingEntries(id, url, submit, "", keyList, httpAuth);

if (users.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND);
Expand Down Expand Up @@ -311,11 +324,10 @@ QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const

QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();

QMutexLocker locker(&m_mutex);
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
Expand All @@ -339,27 +351,27 @@ QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString
const QString groupUuid = decrypted.value("groupUuid").toString();
const QString realm;

BrowserService::ReturnValue result = BrowserService::ReturnValue::Success;
bool result = true;
if (uuid.isEmpty()) {
m_browserService.addEntry(id, login, password, url, submitUrl, realm, group, groupUuid);
browserService()->addEntry(id, login, password, url, submitUrl, realm, group, groupUuid);
} else {
result = m_browserService.updateEntry(id, uuid, login, password, url, submitUrl);
result = browserService()->updateEntry(id, uuid, login, password, url, submitUrl);
}

const QString newNonce = incrementNonce(nonce);

QJsonObject message = buildMessage(newNonce);
message["count"] = QJsonValue::Null;
message["entries"] = QJsonValue::Null;
message["error"] = getReturnValue(result);
message["error"] = result ? QStringLiteral("success") : QStringLiteral("error");
message["hash"] = hash;

return buildResponse(action, message, newNonce);
}

QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();
const QJsonObject decrypted = decryptMessage(encrypted, nonce);
Expand All @@ -374,8 +386,7 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt

QString command = decrypted.value("action").toString();
if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) {
QMutexLocker locker(&m_mutex);
m_browserService.lockDatabase();
browserService()->lockDatabase();

const QString newNonce = incrementNonce(nonce);
QJsonObject message = buildMessage(newNonce);
Expand All @@ -388,11 +399,10 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt

QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();

QMutexLocker locker(&m_mutex);
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
Expand All @@ -407,7 +417,7 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

const QJsonObject groups = m_browserService.getDatabaseGroups();
const QJsonObject groups = browserService()->getDatabaseGroups();
if (groups.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_GROUPS_FOUND);
}
Expand All @@ -422,11 +432,10 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons

QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const QString& action)
{
const QString hash = getDatabaseHash();
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();

QMutexLocker locker(&m_mutex);
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
Expand All @@ -442,7 +451,7 @@ QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const Q
}

QString group = decrypted.value("groupName").toString();
const QJsonObject newGroup = m_browserService.createNewGroup(group);
const QJsonObject newGroup = browserService()->createNewGroup(group);
if (newGroup.isEmpty() || newGroup["name"].toString().isEmpty() || newGroup["uuid"].toString().isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP);
}
Expand Down Expand Up @@ -524,38 +533,6 @@ QString BrowserAction::getErrorMessage(const int errorCode) const
}
}

QString BrowserAction::getReturnValue(const BrowserService::ReturnValue returnValue) const
{
switch (returnValue) {
case BrowserService::ReturnValue::Success:
return QString("success");
case BrowserService::ReturnValue::Error:
return QString("error");
case BrowserService::ReturnValue::Canceled:
return QString("canceled");
}
return QString("error");
}

QString BrowserAction::getDatabaseHash()
{
QMutexLocker locker(&m_mutex);
QByteArray hash =
QCryptographicHash::hash(m_browserService.getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex();
return QString(hash);
}

QString BrowserAction::getLegacyDatabaseHash()
{
QMutexLocker locker(&m_mutex);
QByteArray hash =
QCryptographicHash::hash(
(m_browserService.getDatabaseRootUuid() + m_browserService.getDatabaseRecycleBinUuid()).toUtf8(),
QCryptographicHash::Sha256)
.toHex();
return QString(hash);
}

QString BrowserAction::encryptMessage(const QJsonObject& message, const QString& nonce)
{
if (message.isEmpty() || nonce.isEmpty()) {
Expand Down Expand Up @@ -586,7 +563,6 @@ QJsonObject BrowserAction::decryptMessage(const QString& message, const QString&

QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce)
{
QMutexLocker locker(&m_mutex);
const QByteArray ma = plaintext.toUtf8();
const QByteArray na = base64Decode(nonce);
const QByteArray ca = base64Decode(m_clientPublicKey);
Expand All @@ -598,7 +574,7 @@ QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce)
std::vector<unsigned char> sk(sa.cbegin(), sa.cend());

std::vector<unsigned char> e;
e.resize(NATIVE_MSG_MAX_LENGTH);
e.resize(BrowserShared::NATIVEMSG_MAX_LENGTH);

if (m.empty() || n.empty() || ck.empty() || sk.empty()) {
return QString();
Expand All @@ -614,7 +590,6 @@ QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce)

QByteArray BrowserAction::decrypt(const QString& encrypted, const QString& nonce)
{
QMutexLocker locker(&m_mutex);
const QByteArray ma = base64Decode(encrypted);
const QByteArray na = base64Decode(nonce);
const QByteArray ca = base64Decode(m_clientPublicKey);
Expand All @@ -626,7 +601,7 @@ QByteArray BrowserAction::decrypt(const QString& encrypted, const QString& nonce
std::vector<unsigned char> sk(sa.cbegin(), sa.cend());

std::vector<unsigned char> d;
d.resize(NATIVE_MSG_MAX_LENGTH);
d.resize(BrowserShared::NATIVEMSG_MAX_LENGTH);

if (m.empty() || n.empty() || ck.empty() || sk.empty()) {
return QByteArray();
Expand Down
Loading

0 comments on commit 8117e1a

Please sign in to comment.