Browser Integration: Add support for WebSocket listener

This commit is contained in:
varjolintu 2025-05-11 11:24:21 +03:00
parent f53c7e5af5
commit 83e73398cb
No known key found for this signature in database
GPG key ID: F6D95B66E258AD31
15 changed files with 297 additions and 34 deletions

View file

@ -491,7 +491,7 @@ endif()
include(CLangFormat)
set(QT_COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools)
set(QT_COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools WebSockets)
if(UNIX AND NOT APPLE)
if(WITH_XC_X11)
list(APPEND QT_COMPONENTS X11Extras)

View file

@ -46,7 +46,7 @@ static const QString BROWSER_REQUEST_REQUEST_AUTOTYPE = QStringLiteral("request-
static const QString BROWSER_REQUEST_SET_LOGIN = QStringLiteral("set-login");
static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate");
QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJsonObject& json)
template <typename T> QJsonObject BrowserAction::processClientMessage(T* socket, const QJsonObject& json)
{
if (json.isEmpty()) {
return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED);
@ -75,10 +75,14 @@ QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJso
return handleAction(socket, json);
}
// Explicit template instantiation
template QJsonObject BrowserAction::processClientMessage<QLocalSocket>(QLocalSocket*, const QJsonObject&);
template QJsonObject BrowserAction::processClientMessage<QWebSocket>(QWebSocket*, const QJsonObject&);
// Private functions
///////////////////////
QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject& json)
template <typename T> QJsonObject BrowserAction::handleAction(T* socket, const QJsonObject& json)
{
QString action = json.value("action").toString();
@ -262,7 +266,8 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJsonObject& json, const QString& action)
template <typename T>
QJsonObject BrowserAction::handleGeneratePassword(T* socket, const QJsonObject& json, const QString& action)
{
const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
@ -281,11 +286,12 @@ QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJ
}
// Show the existing password generator
browserService()->showPasswordGenerator({});
// browserService()->showPasswordGenerator({});
browserService()->showPasswordGenerator(KeyPairMessage<T>{});
return errorReply;
}
KeyPairMessage keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey};
KeyPairMessage<T> keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey};
browserService()->showPasswordGenerator(keyPairMessage);
return {};

View file

@ -26,6 +26,7 @@
#include <QString>
class QLocalSocket;
class QWebSocket;
struct BrowserRequest
{
@ -66,16 +67,16 @@ public:
explicit BrowserAction() = default;
~BrowserAction() = default;
QJsonObject processClientMessage(QLocalSocket* socket, const QJsonObject& json);
template <typename T> QJsonObject processClientMessage(T* socket, const QJsonObject& json);
private:
QJsonObject handleAction(QLocalSocket* socket, const QJsonObject& json);
template <typename T> QJsonObject handleAction(T* socket, 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(QLocalSocket* socket, const QJsonObject& json, const QString& action);
template <typename T> QJsonObject handleGeneratePassword(T* socket, const QJsonObject& json, const QString& action);
QJsonObject handleSetLogin(const QJsonObject& json, const QString& action);
QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action);
QJsonObject handleGetDatabaseGroups(const QJsonObject& json, const QString& action);

View file

@ -24,6 +24,7 @@
#include "BrowserHost.h"
#include "BrowserMessageBuilder.h"
#include "BrowserSettings.h"
#include "BrowserWebSocketHost.h"
#include "core/EntryAttributes.h"
#include "core/Tools.h"
#include "gui/MainWindow.h"
@ -53,6 +54,7 @@
#include <QProgressDialog>
#include <QStringView>
#include <QUrl>
#include <QtWebSockets/qwebsocket.h>
const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
@ -78,12 +80,17 @@ Q_GLOBAL_STATIC(BrowserService, s_browserService);
BrowserService::BrowserService()
: QObject()
, m_browserHost(new BrowserHost)
, m_browserWebSocketHost(new BrowserWebSocketHost)
, m_dialogActive(false)
, m_bringToFrontRequested(false)
, m_prevWindowState(WindowState::Normal)
, m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224"))
{
connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage);
connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processLocalSocketClientMessage);
connect(m_browserWebSocketHost,
&BrowserWebSocketHost::clientMessageReceived,
this,
&BrowserService::processWebSocketClientMessage);
connect(getMainWindow(), &MainWindow::databaseUnlocked, this, &BrowserService::databaseUnlocked);
connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked);
connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged);
@ -109,8 +116,12 @@ void BrowserService::setEnabled(bool enabled)
}
m_browserHost->start();
if (browserSettings()->webSocketSupport()) {
m_browserWebSocketHost->start();
}
} else {
m_browserHost->stop();
m_browserWebSocketHost->stop();
}
}
@ -527,7 +538,7 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& entriesToConfirm,
return allowedEntries;
}
void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
template <typename T> void BrowserService::showPasswordGenerator(const KeyPairMessage<T>& keyPairMessage)
{
if (!m_passwordGenerator) {
m_passwordGenerator = PasswordGeneratorWidget::popupGenerator();
@ -539,7 +550,11 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
if (!m_passwordGenerator->isPasswordGenerated()) {
auto errorMessage = browserMessageBuilder()->getErrorReply(
"generate-password", ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage);
if constexpr (std::is_same<T, QWebSocket>::value) {
m_browserWebSocketHost->sendClientMessage(keyPairMessage.socket, errorMessage);
} else {
m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage);
}
}
QTimer::singleShot(50, this, [&] { hideWindow(); });
@ -550,12 +565,24 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
m_passwordGenerator.data(),
[this, keyPairMessage](const QString& password) {
const Parameters params{{"password", password}};
m_browserHost->sendClientMessage(keyPairMessage.socket,
browserMessageBuilder()->buildResponse("generate-password",
keyPairMessage.nonce,
params,
keyPairMessage.publicKey,
keyPairMessage.secretKey));
if constexpr (std::is_same<T, QWebSocket>::value) {
m_browserWebSocketHost->sendClientMessage(
keyPairMessage.socket,
browserMessageBuilder()->buildResponse("generate-password",
keyPairMessage.nonce,
params,
keyPairMessage.publicKey,
keyPairMessage.secretKey));
} else {
m_browserHost->sendClientMessage(
keyPairMessage.socket,
browserMessageBuilder()->buildResponse("generate-password",
keyPairMessage.nonce,
params,
keyPairMessage.publicKey,
keyPairMessage.secretKey));
}
});
}
@ -565,6 +592,9 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
m_passwordGenerator->activateWindow();
}
template void BrowserService::showPasswordGenerator<QLocalSocket>(const KeyPairMessage<QLocalSocket>&);
template void BrowserService::showPasswordGenerator<QWebSocket>(const KeyPairMessage<QWebSocket>&);
bool BrowserService::isPasswordGeneratorRequested() const
{
return m_passwordGenerator && m_passwordGenerator->isVisible();
@ -1763,11 +1793,23 @@ void BrowserService::handleDatabaseUnlockDialogFinished(bool accepted, DatabaseW
}
}
void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObject& message)
void BrowserService::processLocalSocketClientMessage(QLocalSocket* socket, const QJsonObject& message)
{
auto response = processClientMessage<QLocalSocket>(socket, message);
m_browserHost->sendClientMessage(socket, response);
}
void BrowserService::processWebSocketClientMessage(QWebSocket* socket, const QJsonObject& message)
{
auto response = processClientMessage<QWebSocket>(socket, message);
m_browserWebSocketHost->sendClientMessage(socket, response);
}
template <typename T> QJsonObject BrowserService::processClientMessage(T* socket, const QJsonObject& message)
{
auto clientID = message["clientID"].toString();
if (clientID.isEmpty()) {
return;
return {};
}
// Create a new client action if we haven't seen this id yet
@ -1776,6 +1818,7 @@ void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObjec
}
auto& action = m_browserClients.value(clientID);
auto response = action->processClientMessage(socket, message);
m_browserHost->sendClientMessage(socket, response);
return action->processClientMessage<T>(socket, message);
// auto response = action->processClientMessage(socket, message);
// m_browserHost->sendClientMessage(socket, response);
}

View file

@ -26,6 +26,7 @@
#include "gui/PasswordGeneratorWidget.h"
class QLocalSocket;
class QWebSocket;
typedef QPair<QString, QString> StringPair;
typedef QList<StringPair> StringPairList;
@ -35,9 +36,9 @@ enum
max_length = 16 * 1024
};
struct KeyPairMessage
template <typename T> struct KeyPairMessage
{
QLocalSocket* socket;
T* socket;
QString nonce;
QString publicKey;
QString secretKey;
@ -58,6 +59,7 @@ struct EntryParameters
class DatabaseWidget;
class BrowserHost;
class BrowserWebSocketHost;
class BrowserAction;
class BrowserService : public QObject
@ -82,7 +84,7 @@ public:
QJsonArray getDatabaseEntries();
QJsonObject createNewGroup(const QString& groupName, bool isPasskeysGroup = false);
QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
template <typename T> void showPasswordGenerator(const KeyPairMessage<T>& keyPairMessage);
bool isPasswordGeneratorRequested() const;
QSharedPointer<Database> getDatabase(const QUuid& rootGroupUuid = {});
QSharedPointer<Database> selectedDatabase();
@ -137,7 +139,7 @@ public:
signals:
void requestUnlock();
void passwordGenerated(QLocalSocket* socket, const QString& password, const QString& nonce);
void passwordGenerated(QWebSocket* socket, const QString& password, const QString& nonce);
public slots:
void databaseLocked(DatabaseWidget* dbWidget);
@ -145,7 +147,8 @@ public slots:
void activeDatabaseChanged(DatabaseWidget* dbWidget);
private slots:
void processClientMessage(QLocalSocket* socket, const QJsonObject& message);
void processLocalSocketClientMessage(QLocalSocket* socket, const QJsonObject& message);
void processWebSocketClientMessage(QWebSocket* socket, const QJsonObject& message);
void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget);
private:
@ -208,8 +211,10 @@ private:
void hideWindow() const;
void raiseWindow(const bool force = false);
void updateWindowState();
template <typename T> QJsonObject processClientMessage(T* socket, const QJsonObject& message);
QPointer<BrowserHost> m_browserHost;
QPointer<BrowserWebSocketHost> m_browserWebSocketHost;
QHash<QString, QSharedPointer<BrowserAction>> m_browserClients;
bool m_dialogActive;

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand
*
@ -295,3 +295,12 @@ QString BrowserSettings::replaceTildeHomePath(QString location)
return location;
}
void BrowserSettings:: setWebSocketSupport(bool enabled)
{
config()->set(Config::Browser_WebSocketSupport, enabled);
}
bool BrowserSettings::webSocketSupport()
{
return config()->get(Config::Browser_WebSocketSupport).toBool();
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand
*
@ -82,6 +82,8 @@ public:
void updateBinaryPaths();
QString replaceHomePath(QString location);
QString replaceTildeHomePath(QString location);
void setWebSocketSupport(bool enabled);
bool webSocketSupport();
private:
static BrowserSettings* m_instance;

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2025 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
@ -166,6 +166,7 @@ void BrowserSettingsWidget::loadSettings()
m_ui->browserTypeComboBox->setCurrentIndex(typeIndex);
}
m_ui->customBrowserLocation->setText(settings->replaceHomePath(settings->customBrowserLocation()));
m_ui->webSocketSupport->setChecked(settings->webSocketSupport());
#ifdef QT_DEBUG
m_ui->customExtensionId->setText(settings->customExtensionId());
@ -241,6 +242,7 @@ void BrowserSettingsWidget::saveSettings()
settings->setSupportKphFields(m_ui->supportKphFields->isChecked());
settings->setAllowLocalhostWithPasskeys(m_ui->allowLocalhostWithPasskeys->isChecked());
settings->setNoMigrationPrompt(m_ui->noMigrationPrompt->isChecked());
settings->setWebSocketSupport(m_ui->webSocketSupport->isChecked());
#ifdef QT_DEBUG
settings->setCustomExtensionId(m_ui->customExtensionId->text());

View file

@ -340,6 +340,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="webSocketSupport">
<property name="toolTip">
<string>Listens to connections using WebSocket in addition to native messaging.</string>
</property>
<property name="text">
<string>Enable WebSocket listener</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allowGetDatabaseEntriesRequest">
<property name="toolTip">

View file

@ -0,0 +1,59 @@
/*
* Copyright (C) 2025 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 KEEPASSXC_BROWSERWEBSOCKETHOST_H
#define KEEPASSXC_BROWSERWEBSOCKETHOST_H
#include <QJsonObject>
#include <QObject>
#include <QPointer>
class QWebSocketServer;
class QWebSocket;
class QString;
class BrowserWebSocketHost : public QObject
{
Q_OBJECT
public:
explicit BrowserWebSocketHost(QObject* parent = nullptr);
~BrowserWebSocketHost() override;
void start();
void stop();
void broadcastClientMessage(const QJsonObject& json);
void sendClientMessage(QWebSocket* socket, const QJsonObject& json);
signals:
void clientMessageReceived(QWebSocket* socket, const QJsonObject& json);
private slots:
void clientConnected();
void readClientMessage(QString message);
void clientDisconnected();
private:
void sendClientData(QWebSocket* socket, const QString& data);
private:
QPointer<QWebSocketServer> m_webSocketServer;
QList<QWebSocket*> m_socketList;
};
#endif // KEEPASSXC_BROWSERWEBSOCKETHOST_H

View file

@ -0,0 +1,123 @@
/*
* Copyright (C) 2025 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 "BrowserShared.h"
#include "BrowserWebSocketHost.h"
#include <QJsonDocument>
#include <QtWebSockets/qwebsocket.h>
#include <QtWebSockets/qwebsocketserver.h>
#ifdef Q_OS_WIN
#include <fcntl.h>
#undef NOMINMAX
#define NOMINMAX
#include <windows.h>
#else
#include <sys/socket.h>
#endif
BrowserWebSocketHost::BrowserWebSocketHost(QObject* parent)
: QObject(parent)
{
m_webSocketServer =
new QWebSocketServer(QStringLiteral("KeePassXC HTTP server"), QWebSocketServer::NonSecureMode, this);
}
BrowserWebSocketHost::~BrowserWebSocketHost()
{
stop();
}
void BrowserWebSocketHost::start()
{
int socketDesc = m_webSocketServer->nativeDescriptor();
if (socketDesc) {
int max = BrowserShared::NATIVEMSG_MAX_LENGTH;
setsockopt(socketDesc, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<char*>(&max), sizeof(max));
}
if (!m_webSocketServer->isListening()) {
m_webSocketServer->listen(QHostAddress::LocalHost, 7580);
connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &BrowserWebSocketHost::clientConnected);
connect(m_webSocketServer, &QWebSocketServer::closed, this, &BrowserWebSocketHost::stop);
}
}
void BrowserWebSocketHost::stop()
{
m_socketList.clear();
m_webSocketServer->close();
}
void BrowserWebSocketHost::clientConnected()
{
auto socket = m_webSocketServer->nextPendingConnection();
if (socket) {
m_socketList.append(socket);
connect(socket, &QWebSocket::textMessageReceived, this, &BrowserWebSocketHost::readClientMessage);
connect(socket, &QWebSocket::disconnected, this, &BrowserWebSocketHost::clientDisconnected);
}
}
void BrowserWebSocketHost::readClientMessage(QString message)
{
auto* socket = qobject_cast<QWebSocket*>(QObject::sender());
if (!socket || !socket->isValid()) {
return;
}
socket->setReadBufferSize(BrowserShared::NATIVEMSG_MAX_LENGTH);
socket->setOutgoingFrameSize(BrowserShared::NATIVEMSG_MAX_LENGTH);
QJsonParseError error;
auto json = QJsonDocument::fromJson(message.toUtf8(), &error);
if (json.isNull()) {
qWarning() << "Failed to read proxy message: " << error.errorString();
return;
}
emit clientMessageReceived(socket, json.object());
}
void BrowserWebSocketHost::broadcastClientMessage(const QJsonObject& json)
{
QString reply(QJsonDocument(json).toJson(QJsonDocument::Compact));
for (const auto socket : m_socketList) {
sendClientData(socket, reply);
}
}
void BrowserWebSocketHost::sendClientMessage(QWebSocket* socket, const QJsonObject& json)
{
QString reply(QJsonDocument(json).toJson(QJsonDocument::Compact));
sendClientData(socket, reply);
}
void BrowserWebSocketHost::sendClientData(QWebSocket* socket, const QString& data)
{
if (socket && socket->isValid() && socket->state() == QAbstractSocket::ConnectedState) {
socket->sendTextMessage(data);
socket->flush();
}
}
void BrowserWebSocketHost::clientDisconnected()
{
auto socket = qobject_cast<QWebSocket*>(QObject::sender());
m_socketList.removeOne(socket);
}

View file

@ -1,4 +1,4 @@
# Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
# Copyright (C) 2025 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
@ -28,6 +28,7 @@ if(WITH_XC_BROWSER)
BrowserService.cpp
BrowserSettings.cpp
BrowserShared.cpp
BrowserWebSocketHost.cpp
CustomTableWidget.cpp
NativeMessageInstaller.cpp)
@ -41,5 +42,5 @@ if(WITH_XC_BROWSER)
endif()
add_library(browser STATIC ${browser_SOURCES})
target_link_libraries(browser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${BOTAN_LIBRARIES})
target_link_libraries(browser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network Qt5::WebSockets ${BOTAN_LIBRARIES})
endif()

View file

@ -175,6 +175,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Browser_CustomBrowserType, {QS("Browser/CustomBrowserType"), Local, -1}},
{Config::Browser_CustomBrowserLocation, {QS("Browser/CustomBrowserLocation"), Local, {}}},
{Config::Browser_AllowLocalhostWithPasskeys, {QS("Browser/Browser_AllowLocalhostWithPasskeys"), Roaming, false}},
{Config::Browser_WebSocketSupport, {QS("Browser/WebSocketSupport"), Roaming, false}},
#ifdef QT_DEBUG
{Config::Browser_CustomExtensionId, {QS("Browser/CustomExtensionId"), Local, {}}},
#endif

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
@ -155,6 +155,7 @@ public:
Browser_CustomBrowserType,
Browser_CustomBrowserLocation,
Browser_AllowLocalhostWithPasskeys,
Browser_WebSocketSupport,
#ifdef QT_DEBUG
Browser_CustomExtensionId,
#endif

View file

@ -63,7 +63,7 @@ void TestBrowser::testChangePublicKeys()
json["publicKey"] = PUBLICKEY;
json["nonce"] = NONCE;
auto response = m_browserAction->processClientMessage(nullptr, json);
auto response = m_browserAction->processClientMessage<QLocalSocket>(nullptr, json);
QCOMPARE(response["action"].toString(), QString("change-public-keys"));
QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false);
QCOMPARE(response["success"].toString(), TRUE_STR);