-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Complete refactor of Browser Integration classes
* Removed option to attach KeePassXC to the browser extension. Users must use the proxy application to communicate with KeePassXC. * Significantly streamlined proxy code. Used same implementation of stdin/stdout interface across all platforms. * Moved browser service entry point to BrowserService class instead of NativeMessagingHost. BrowserService now coordinates the communication to/from clients. * Moved settings page definition out of MainWindow * Decoupled BrowserService from DatabaseTabWidget * Reduced complexity of various functions and cleaned the ABI (public vs private). * Eliminated BrowserClients class, moved functionality into the BrowserService * Renamed HostInstaller to NativeMessageInstaller and renamed NativeMessageHost to BrowserHost. * Recognize XDG_CONFIG_HOME when installing native message file on Linux. Fix #4121 and fix #4123.
- Loading branch information
1 parent
3b4057a
commit a145bf9
Showing
43 changed files
with
1,220 additions
and
1,918 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
@@ -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); | ||
|
@@ -51,11 +69,10 @@ QJsonObject BrowserAction::readResponse(const QJsonObject& json) | |
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); | ||
} | ||
|
||
QMutexLocker locker(&m_mutex); | ||
if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !m_browserService.isDatabaseOpened()) { | ||
if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !browserService()->isDatabaseOpened()) { | ||
if (m_clientPublicKey.isEmpty()) { | ||
return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); | ||
} else if (!m_browserService.openDatabase(triggerUnlock)) { | ||
} else if (!browserService()->openDatabase(triggerUnlock)) { | ||
return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); | ||
} | ||
} | ||
|
@@ -98,7 +115,6 @@ QJsonObject BrowserAction::handleAction(const QJsonObject& json) | |
|
||
QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QString& action) | ||
{ | ||
QMutexLocker locker(&m_mutex); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString clientPublicKey = json.value("publicKey").toString(); | ||
|
||
|
@@ -130,7 +146,7 @@ QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const | |
|
||
QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
const QJsonObject decrypted = decryptMessage(encrypted, nonce); | ||
|
@@ -153,7 +169,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const | |
// Update a legacy database hash if found | ||
const QJsonArray hashes = decrypted.value("connectedKeys").toArray(); | ||
if (!hashes.isEmpty()) { | ||
const QString legacyHash = getLegacyDatabaseHash(); | ||
const QString legacyHash = browserService()->getDatabaseHash(true); | ||
if (hashes.contains(legacyHash)) { | ||
message["oldHash"] = legacyHash; | ||
} | ||
|
@@ -167,7 +183,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const | |
|
||
QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
const QJsonObject decrypted = decryptMessage(encrypted, nonce); | ||
|
@@ -181,12 +197,11 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin | |
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); | ||
} | ||
|
||
QMutexLocker locker(&m_mutex); | ||
if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) { | ||
// Check for identification key. If it's not found, ensure backwards compatibility and use the current public | ||
// key | ||
const QString idKey = decrypted.value("idKey").toString(); | ||
const QString id = m_browserService.storeKey((idKey.isEmpty() ? key : idKey)); | ||
const QString id = browserService()->storeKey((idKey.isEmpty() ? key : idKey)); | ||
if (id.isEmpty()) { | ||
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); | ||
} | ||
|
@@ -205,7 +220,7 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin | |
|
||
QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
const QJsonObject decrypted = decryptMessage(encrypted, nonce); | ||
|
@@ -220,8 +235,7 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS | |
return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); | ||
} | ||
|
||
QMutexLocker locker(&m_mutex); | ||
const QString key = m_browserService.getKey(id); | ||
const QString key = browserService()->getKey(id); | ||
if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) { | ||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); | ||
} | ||
|
@@ -238,11 +252,10 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS | |
|
||
QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
|
||
QMutexLocker locker(&m_mutex); | ||
if (!m_associated) { | ||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); | ||
} | ||
|
@@ -269,7 +282,7 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin | |
const QString submit = decrypted.value("submitUrl").toString(); | ||
const QString auth = decrypted.value("httpAuth").toString(); | ||
const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0 ? true : false; | ||
const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, "", keyList, httpAuth); | ||
const QJsonArray users = browserService()->findMatchingEntries(id, url, submit, "", keyList, httpAuth); | ||
|
||
if (users.isEmpty()) { | ||
return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND); | ||
|
@@ -311,11 +324,10 @@ QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const | |
|
||
QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
|
||
QMutexLocker locker(&m_mutex); | ||
if (!m_associated) { | ||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); | ||
} | ||
|
@@ -339,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); | ||
|
@@ -374,8 +386,7 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt | |
|
||
QString command = decrypted.value("action").toString(); | ||
if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) { | ||
QMutexLocker locker(&m_mutex); | ||
m_browserService.lockDatabase(); | ||
browserService()->lockDatabase(); | ||
|
||
const QString newNonce = incrementNonce(nonce); | ||
QJsonObject message = buildMessage(newNonce); | ||
|
@@ -388,11 +399,10 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt | |
|
||
QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
|
||
QMutexLocker locker(&m_mutex); | ||
if (!m_associated) { | ||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); | ||
} | ||
|
@@ -407,7 +417,7 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons | |
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); | ||
} | ||
|
||
const QJsonObject groups = m_browserService.getDatabaseGroups(); | ||
const QJsonObject groups = browserService()->getDatabaseGroups(); | ||
if (groups.isEmpty()) { | ||
return getErrorReply(action, ERROR_KEEPASS_NO_GROUPS_FOUND); | ||
} | ||
|
@@ -422,11 +432,10 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons | |
|
||
QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const QString& action) | ||
{ | ||
const QString hash = getDatabaseHash(); | ||
const QString hash = browserService()->getDatabaseHash(); | ||
const QString nonce = json.value("nonce").toString(); | ||
const QString encrypted = json.value("message").toString(); | ||
|
||
QMutexLocker locker(&m_mutex); | ||
if (!m_associated) { | ||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); | ||
} | ||
|
@@ -442,7 +451,7 @@ QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const Q | |
} | ||
|
||
QString group = decrypted.value("groupName").toString(); | ||
const QJsonObject newGroup = m_browserService.createNewGroup(group); | ||
const QJsonObject newGroup = browserService()->createNewGroup(group); | ||
if (newGroup.isEmpty() || newGroup["name"].toString().isEmpty() || newGroup["uuid"].toString().isEmpty()) { | ||
return getErrorReply(action, ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP); | ||
} | ||
|
@@ -524,38 +533,6 @@ QString BrowserAction::getErrorMessage(const int errorCode) const | |
} | ||
} | ||
|
||
QString BrowserAction::getReturnValue(const BrowserService::ReturnValue returnValue) const | ||
{ | ||
switch (returnValue) { | ||
case BrowserService::ReturnValue::Success: | ||
return QString("success"); | ||
case BrowserService::ReturnValue::Error: | ||
return QString("error"); | ||
case BrowserService::ReturnValue::Canceled: | ||
return QString("canceled"); | ||
} | ||
return QString("error"); | ||
} | ||
|
||
QString BrowserAction::getDatabaseHash() | ||
{ | ||
QMutexLocker locker(&m_mutex); | ||
QByteArray hash = | ||
QCryptographicHash::hash(m_browserService.getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex(); | ||
return QString(hash); | ||
} | ||
|
||
QString BrowserAction::getLegacyDatabaseHash() | ||
{ | ||
QMutexLocker locker(&m_mutex); | ||
QByteArray hash = | ||
QCryptographicHash::hash( | ||
(m_browserService.getDatabaseRootUuid() + m_browserService.getDatabaseRecycleBinUuid()).toUtf8(), | ||
QCryptographicHash::Sha256) | ||
.toHex(); | ||
return QString(hash); | ||
} | ||
|
||
QString BrowserAction::encryptMessage(const QJsonObject& message, const QString& nonce) | ||
{ | ||
if (message.isEmpty() || nonce.isEmpty()) { | ||
|
@@ -586,7 +563,6 @@ QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& | |
|
||
QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce) | ||
{ | ||
QMutexLocker locker(&m_mutex); | ||
const QByteArray ma = plaintext.toUtf8(); | ||
const QByteArray na = base64Decode(nonce); | ||
const QByteArray ca = base64Decode(m_clientPublicKey); | ||
|
@@ -598,7 +574,7 @@ QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce) | |
std::vector<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(); | ||
|
@@ -614,7 +590,6 @@ QString BrowserAction::encrypt(const QString& plaintext, const QString& nonce) | |
|
||
QByteArray BrowserAction::decrypt(const QString& encrypted, const QString& nonce) | ||
{ | ||
QMutexLocker locker(&m_mutex); | ||
const QByteArray ma = base64Decode(encrypted); | ||
const QByteArray na = base64Decode(nonce); | ||
const QByteArray ca = base64Decode(m_clientPublicKey); | ||
|
@@ -626,7 +601,7 @@ QByteArray BrowserAction::decrypt(const QString& encrypted, const QString& nonce | |
std::vector<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(); | ||
|
Oops, something went wrong.