Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for triggering Global Auto-Type from browser extension #6272

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/autotype/AutoType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,14 @@ void AutoType::loadPlugin(const QString& pluginPath)
if (m_plugin) {
if (m_plugin->isAvailable()) {
m_executor = m_plugin->createExecutor();
connect(osUtils, &OSUtilsBase::globalShortcutTriggered, this, [this](const QString& name) {
if (name == "autotype") {
startGlobalAutoType();
}
});
connect(osUtils,
&OSUtilsBase::globalShortcutTriggered,
this,
[this](const QString& name, const QString& initialSearch) {
if (name == "autotype") {
startGlobalAutoType(initialSearch);
}
});
} else {
unloadPlugin();
}
Expand Down Expand Up @@ -359,7 +362,7 @@ void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& se
executeAutoTypeActions(entry, hideWindow, sequence);
}

void AutoType::startGlobalAutoType()
void AutoType::startGlobalAutoType(const QString& search)
{
// Never Auto-Type into KeePassXC itself
if (qApp->focusWindow()) {
Expand All @@ -382,6 +385,7 @@ void AutoType::startGlobalAutoType()
tr("KeePassXC requires the Accessibility and Screen Recorder permission in order to perform global "
"Auto-Type. Screen Recording is necessary to use the window title to find entries. If you "
"already granted permission, you may have to restart KeePassXC."));
qDebug() << "Oh noes macOS.";
return;
}
}
Expand All @@ -397,14 +401,14 @@ void AutoType::startGlobalAutoType()
}
#endif

emit globalAutoTypeTriggered();
emit globalAutoTypeTriggered(search);
}

/**
* Global Autotype entry-point function
* Perform global Auto-Type on the active window
*/
void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList)
void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList, const QString& search)
{
if (!m_plugin) {
return;
Expand Down Expand Up @@ -449,6 +453,10 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
auto* selectDialog = new AutoTypeSelectDialog();
selectDialog->setMatches(matchList, dbList);

if (!search.isEmpty()) {
selectDialog->setSearchString(search);
}

connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) {
executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal);
Expand Down
6 changes: 3 additions & 3 deletions src/autotype/AutoType.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@ class AutoType : public QObject
static void createTestInstance();

public slots:
void performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList);
void performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList, const QString& search = {});
void raiseWindow();

signals:
void globalAutoTypeTriggered();
void globalAutoTypeTriggered(const QString& search);
void autotypePerformed();
void autotypeRejected();

private slots:
void startGlobalAutoType();
void startGlobalAutoType(const QString& search);
void unloadPlugin();

private:
Expand Down
6 changes: 6 additions & 0 deletions src/autotype/AutoTypeSelectDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches, const
m_ui->searchCheckBox->setChecked(m_matches.isEmpty());
}

void AutoTypeSelectDialog::setSearchString(const QString& search)
{
m_ui->search->setText(search);
m_ui->searchCheckBox->setChecked(true);
}

void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
{
if (match.first) {
Expand Down
1 change: 1 addition & 0 deletions src/autotype/AutoTypeSelectDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class AutoTypeSelectDialog : public QDialog
~AutoTypeSelectDialog() override;

void setMatches(const QList<AutoTypeMatch>& matchList, const QList<QSharedPointer<Database>>& dbs);
void setSearchString(const QString& search);

signals:
void matchActivated(AutoTypeMatch match);
Expand Down
78 changes: 55 additions & 23 deletions src/browser/BrowserAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ namespace
};
}

const int BrowserAction::MaxUrlLength = 256;

QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)
{
if (json.isEmpty()) {
Expand All @@ -65,7 +67,7 @@ QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)

bool triggerUnlock = false;
const QString trigger = json.value("triggerUnlock").toString();
if (!trigger.isEmpty() && trigger.compare(TRUE_STR, Qt::CaseSensitive) == 0) {
if (!trigger.isEmpty() && trigger.compare(TRUE_STR) == 0) {
triggerUnlock = true;
}

Expand All @@ -74,7 +76,8 @@ QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !browserService()->isDatabaseOpened()) {
if (action.compare("change-public-keys") != 0 && action.compare("request-autotype") != 0
droidmonkey marked this conversation as resolved.
Show resolved Hide resolved
&& !browserService()->isDatabaseOpened()) {
if (m_clientPublicKey.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED);
} else if (!browserService()->openDatabase(triggerUnlock)) {
Expand All @@ -92,30 +95,32 @@ QJsonObject BrowserAction::handleAction(const QJsonObject& json)
{
QString action = json.value("action").toString();

if (action.compare("change-public-keys", Qt::CaseSensitive) == 0) {
if (action.compare("change-public-keys") == 0) {
return handleChangePublicKeys(json, action);
} else if (action.compare("get-databasehash", Qt::CaseSensitive) == 0) {
} else if (action.compare("get-databasehash") == 0) {
return handleGetDatabaseHash(json, action);
} else if (action.compare("associate", Qt::CaseSensitive) == 0) {
} else if (action.compare("associate") == 0) {
return handleAssociate(json, action);
} else if (action.compare("test-associate", Qt::CaseSensitive) == 0) {
} else if (action.compare("test-associate") == 0) {
return handleTestAssociate(json, action);
} else if (action.compare("get-logins", Qt::CaseSensitive) == 0) {
} else if (action.compare("get-logins") == 0) {
return handleGetLogins(json, action);
} else if (action.compare("generate-password", Qt::CaseSensitive) == 0) {
} else if (action.compare("generate-password") == 0) {
return handleGeneratePassword(json, action);
} else if (action.compare("set-login", Qt::CaseSensitive) == 0) {
} else if (action.compare("set-login") == 0) {
return handleSetLogin(json, action);
} else if (action.compare("lock-database", Qt::CaseSensitive) == 0) {
} else if (action.compare("lock-database") == 0) {
return handleLockDatabase(json, action);
} else if (action.compare("get-database-groups", Qt::CaseSensitive) == 0) {
} else if (action.compare("get-database-groups") == 0) {
return handleGetDatabaseGroups(json, action);
} else if (action.compare("create-new-group", Qt::CaseSensitive) == 0) {
} else if (action.compare("create-new-group") == 0) {
return handleCreateNewGroup(json, action);
} else if (action.compare("get-totp", Qt::CaseSensitive) == 0) {
} else if (action.compare("get-totp") == 0) {
return handleGetTotp(json, action);
} else if (action.compare("delete-entry", Qt::CaseSensitive) == 0) {
} else if (action.compare("delete-entry") == 0) {
return handleDeleteEntry(json, action);
} else if (action.compare("request-autotype") == 0) {
return handleGlobalAutoType(json, action);
}

// Action was not recognized
Expand Down Expand Up @@ -169,7 +174,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const
}

QString command = decrypted.value("action").toString();
if (!command.isEmpty() && command.compare("get-databasehash", Qt::CaseSensitive) == 0) {
if (!command.isEmpty() && command.compare("get-databasehash") == 0) {
const QString newNonce = incrementNonce(nonce);

QJsonObject message = buildMessage(newNonce);
Expand Down Expand Up @@ -206,7 +211,7 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}

if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) {
if (key.compare(m_clientPublicKey) == 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();
Expand Down Expand Up @@ -245,7 +250,7 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS
}

const QString key = browserService()->getKey(id);
if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) {
if (key.isEmpty() || key.compare(responseKey) != 0) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}

Expand Down Expand Up @@ -290,7 +295,7 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
const QString id = decrypted.value("id").toString();
const QString formUrl = decrypted.value("submitUrl").toString();
const QString auth = decrypted.value("httpAuth").toString();
const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0;
const bool httpAuth = auth.compare(TRUE_STR) == 0;
const QJsonArray users = browserService()->findMatchingEntries(id, siteUrl, formUrl, "", keyList, httpAuth);

if (users.isEmpty()) {
Expand Down Expand Up @@ -398,7 +403,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) {
if (!command.isEmpty() && command.compare("lock-database") == 0) {
browserService()->lockDatabase();

const QString newNonce = incrementNonce(nonce);
Expand Down Expand Up @@ -426,7 +431,7 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons
}

QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("get-database-groups", Qt::CaseSensitive) != 0) {
if (command.isEmpty() || command.compare("get-database-groups") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

Expand Down Expand Up @@ -459,7 +464,7 @@ QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const Q
}

QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("create-new-group", Qt::CaseSensitive) != 0) {
if (command.isEmpty() || command.compare("create-new-group") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

Expand Down Expand Up @@ -493,7 +498,7 @@ QJsonObject BrowserAction::handleGetTotp(const QJsonObject& json, const QString&
}

QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("get-totp", Qt::CaseSensitive) != 0) {
if (command.isEmpty() || command.compare("get-totp") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

Expand Down Expand Up @@ -527,7 +532,7 @@ QJsonObject BrowserAction::handleDeleteEntry(const QJsonObject& json, const QStr
}

QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("delete-entry", Qt::CaseSensitive) != 0) {
if (command.isEmpty() || command.compare("delete-entry") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

Expand All @@ -545,6 +550,33 @@ QJsonObject BrowserAction::handleDeleteEntry(const QJsonObject& json, const QStr
return buildResponse(action, message, newNonce);
}

QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const QString& action)
{
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();
const QJsonObject decrypted = decryptMessage(encrypted, nonce);

if (decrypted.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}

QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("request-autotype") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

const auto topLevelDomain = decrypted.value("search").toString();
droidmonkey marked this conversation as resolved.
Show resolved Hide resolved
if (topLevelDomain.length() > BrowserAction::MaxUrlLength) {
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}

browserService()->requestGlobalAutoType(topLevelDomain);

const QString newNonce = incrementNonce(nonce);
QJsonObject message = buildMessage(newNonce);
return buildResponse(action, message, newNonce);
}

QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const
{
QJsonObject response;
Expand Down
3 changes: 3 additions & 0 deletions src/browser/BrowserAction.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class BrowserAction
QJsonObject handleCreateNewGroup(const QJsonObject& json, const QString& action);
QJsonObject handleGetTotp(const QJsonObject& json, const QString& action);
QJsonObject handleDeleteEntry(const QJsonObject& json, const QString& action);
QJsonObject handleGlobalAutoType(const QJsonObject& json, const QString& action);

QJsonObject buildMessage(const QString& nonce) const;
QJsonObject buildResponse(const QString& action, const QJsonObject& message, const QString& nonce);
Expand All @@ -63,6 +64,8 @@ class BrowserAction
QString incrementNonce(const QString& nonce);

private:
static const int MaxUrlLength;

QString m_clientPublicKey;
QString m_publicKey;
QString m_secretKey;
Expand Down
24 changes: 17 additions & 7 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/

#include "BrowserService.h"

#include "BrowserAccessControlDialog.h"
#include "BrowserAction.h"
#include "BrowserEntryConfig.h"
Expand All @@ -28,6 +27,7 @@
#include "core/Tools.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
#endif
Expand Down Expand Up @@ -761,6 +761,11 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer<Database> db)
}
}

void BrowserService::requestGlobalAutoType(const QString& search)
{
emit osUtils->globalShortcutTriggered("autotype", search);
}

QList<Entry*>
BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr)
{
Expand Down Expand Up @@ -1012,6 +1017,12 @@ bool BrowserService::schemeFound(const QString& url)
return !address.scheme().isEmpty();
}

bool BrowserService::isIpAddress(const QString& host) const
{
QHostAddress address(host);
return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol;
}

bool BrowserService::removeFirstDomain(QString& hostname)
{
int pos = hostname.indexOf(".");
Expand Down Expand Up @@ -1088,7 +1099,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt
}

// Match the base domain
if (baseDomain(siteQUrl.host()) != baseDomain(entryQUrl.host())) {
if (getTopLevelDomainFromUrl(siteQUrl.host()) != getTopLevelDomainFromUrl(entryQUrl.host())) {
return false;
}

Expand All @@ -1105,15 +1116,14 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt
*
* Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk
*/
QString BrowserService::baseDomain(const QString& hostname) const
QString BrowserService::getTopLevelDomainFromUrl(const QString& url) const
{
QUrl qurl = QUrl::fromUserInput(hostname);
QUrl qurl = QUrl::fromUserInput(url);
QString host = qurl.host();

// If the hostname is an IP address, return it directly
QHostAddress hostAddress(hostname);
if (!hostAddress.isNull()) {
return hostname;
if (isIpAddress(host)) {
return host;
}

if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) {
Expand Down
Loading