Skip to content

Commit

Permalink
Support for triggering Global Auto-Type from browser extension
Browse files Browse the repository at this point in the history
  • Loading branch information
varjolintu committed Oct 10, 2021
1 parent be6835e commit 87f13d7
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 63 deletions.
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
&& !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();
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

0 comments on commit 87f13d7

Please sign in to comment.