/* * Copyright (C) 2012 Felix Geyer * Copyright (C) 2017 KeePassXC Team * * 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 2 or (at your option) * version 3 of the License. * * 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 . */ #include "AutoType.h" #include #include #include #include #include "config-keepassx.h" #include "autotype/AutoTypePlatformPlugin.h" #include "autotype/AutoTypeSelectDialog.h" #include "autotype/PickcharsDialog.h" #include "core/Resources.h" #include "core/Tools.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "gui/osutils/OSUtils.h" namespace { // Basic Auto-Type placeholder associations const QHash g_placeholderToKey = {{"tab", Qt::Key_Tab}, {"enter", Qt::Key_Enter}, {"space", Qt::Key_Space}, {"up", Qt::Key_Up}, {"down", Qt::Key_Down}, {"left", Qt::Key_Left}, {"right", Qt::Key_Right}, {"insert", Qt::Key_Insert}, {"ins", Qt::Key_Insert}, {"delete", Qt::Key_Delete}, {"del", Qt::Key_Delete}, {"home", Qt::Key_Home}, {"end", Qt::Key_End}, {"pgup", Qt::Key_PageUp}, {"pgdown", Qt::Key_PageDown}, {"backspace", Qt::Key_Backspace}, {"bs", Qt::Key_Backspace}, {"bksp", Qt::Key_Backspace}, {"break", Qt::Key_Pause}, {"capslock", Qt::Key_CapsLock}, {"esc", Qt::Key_Escape}, {"help", Qt::Key_Help}, {"numlock", Qt::Key_NumLock}, {"ptrsc", Qt::Key_Print}, {"scrolllock", Qt::Key_ScrollLock}, {"win", Qt::Key_Meta}, {"lwin", Qt::Key_Meta}, {"rwin", Qt::Key_Meta}, {"apps", Qt::Key_Menu}, {"help", Qt::Key_Help}, {"add", Qt::Key_Plus}, {"subtract", Qt::Key_Minus}, {"multiply", Qt::Key_Asterisk}, {"divide", Qt::Key_Slash}, {"leftbrace", Qt::Key_BraceLeft}, {"{", Qt::Key_unknown}, {"rightbrace", Qt::Key_BraceRight}, {"}", Qt::Key_unknown}, {"leftparen", Qt::Key_ParenLeft}, {"(", Qt::Key_unknown}, {"rightparen", Qt::Key_ParenRight}, {")", Qt::Key_unknown}, {"[", Qt::Key_unknown}, {"]", Qt::Key_unknown}, {"+", Qt::Key_unknown}, {"%", Qt::Key_unknown}, {"^", Qt::Key_unknown}, {"~", Qt::Key_unknown}, {"#", Qt::Key_unknown}, {"numpad0", Qt::Key_0}, {"numpad1", Qt::Key_1}, {"numpad2", Qt::Key_2}, {"numpad3", Qt::Key_3}, {"numpad4", Qt::Key_4}, {"numpad5", Qt::Key_5}, {"numpad6", Qt::Key_6}, {"numpad7", Qt::Key_7}, {"numpad8", Qt::Key_8}, {"numpad9", Qt::Key_9}, {"f1", Qt::Key_F1}, {"f2", Qt::Key_F2}, {"f3", Qt::Key_F3}, {"f4", Qt::Key_F4}, {"f5", Qt::Key_F5}, {"f6", Qt::Key_F6}, {"f7", Qt::Key_F7}, {"f8", Qt::Key_F8}, {"f9", Qt::Key_F9}, {"f10", Qt::Key_F10}, {"f11", Qt::Key_F11}, {"f12", Qt::Key_F12}, {"f13", Qt::Key_F13}, {"f14", Qt::Key_F14}, {"f15", Qt::Key_F15}, {"f16", Qt::Key_F16}}; } // namespace AutoType* AutoType::m_instance = nullptr; AutoType::AutoType(QObject* parent, bool test) : QObject(parent) , m_pluginLoader(new QPluginLoader(this)) , m_plugin(nullptr) , m_executor(nullptr) , m_windowState(WindowState::Normal) , m_windowForGlobal(0) , m_lastMatch(nullptr, QString()) , m_lastMatchRetypeTimer(nullptr) { // configure timer to reset last match m_lastMatchRetypeTimer.setSingleShot(true); connect(&m_lastMatchRetypeTimer, &QTimer::timeout, this, [this] { m_lastMatch = {nullptr, QString()}; emit autotypeRetypeTimeout(); }); // prevent crash when the plugin has unresolved symbols m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint); QString pluginName = "keepassxc-autotype-"; if (!test) { pluginName += QApplication::platformName(); } else { pluginName += "test"; } QString pluginPath = resources()->pluginPath(pluginName); if (!pluginPath.isEmpty()) { #ifdef WITH_XC_AUTOTYPE loadPlugin(pluginPath); #endif } connect(qApp, SIGNAL(aboutToQuit()), SLOT(unloadPlugin())); } AutoType::~AutoType() { if (m_executor) { delete m_executor; m_executor = nullptr; } } void AutoType::loadPlugin(const QString& pluginPath) { m_pluginLoader->setFileName(pluginPath); QObject* pluginInstance = m_pluginLoader->instance(); if (pluginInstance) { m_plugin = qobject_cast(pluginInstance); m_executor = nullptr; if (m_plugin) { if (m_plugin->isAvailable()) { m_executor = m_plugin->createExecutor(); connect(osUtils, &OSUtilsBase::globalShortcutTriggered, this, [this](const QString& name, const QString& initialSearch) { if (name == "autotype") { startGlobalAutoType(initialSearch); } }); } else { unloadPlugin(); } } } if (!m_plugin) { qWarning("Unable to load auto-type plugin:\n%s", qPrintable(m_pluginLoader->errorString())); } } void AutoType::unloadPlugin() { if (m_executor) { delete m_executor; m_executor = nullptr; } if (m_plugin) { m_plugin->unload(); m_plugin = nullptr; } } AutoType* AutoType::instance() { if (!m_instance) { m_instance = new AutoType(qApp); } return m_instance; } void AutoType::createTestInstance() { Q_ASSERT(!m_instance); m_instance = new AutoType(qApp, true); } QStringList AutoType::windowTitles() { if (!m_plugin) { return QStringList(); } return m_plugin->windowTitles(); } void AutoType::raiseWindow() { #if defined(Q_OS_MACOS) m_plugin->raiseOwnWindow(); #endif } bool AutoType::registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers, QString* error) { if (!m_plugin) { return false; } return osUtils->registerGlobalShortcut("autotype", key, modifiers, error); } void AutoType::unregisterGlobalShortcut() { osUtils->unregisterGlobalShortcut("autotype"); } /** * Core Autotype function that will execute actions */ void AutoType::executeAutoTypeActions(const Entry* entry, const QString& sequence, WId window, AutoTypeExecutor::Mode mode) { QString error; auto actions = parseSequence(sequence, entry, error); if (!error.isEmpty()) { auto errorMsg = tr("The requested Auto-Type sequence cannot be used due to an error:"); errorMsg.append(QString("\n%1\n%2").arg(sequence, error)); if (getMainWindow()) { MessageBox::critical(getMainWindow(), tr("Auto-Type Error"), errorMsg); } qWarning() << errorMsg; emit autotypeRejected(); return; } if (!m_inAutoType.tryLock()) { return; } // Explicitly hide the main window if no target window is specified if (window == 0) { #if defined(Q_OS_MACOS) // Check for accessibility permission if (!macUtils()->enableAccessibility()) { MessageBox::information(nullptr, tr("Permission Required"), tr("KeePassXC requires the Accessibility permission in order to perform entry " "level Auto-Type. If you already granted permission, you may have to restart " "KeePassXC.")); return; } macUtils()->raiseLastActiveWindow(); m_plugin->hideOwnWindow(); #else if (getMainWindow()) { getMainWindow()->minimizeOrHide(); } #endif } else { // Restore window state (macOS only) then raise the target window restoreWindowState(); QCoreApplication::processEvents(); m_plugin->raiseWindow(window); } // Restore executor mode m_executor->mode = mode; int delay = qMax(100, config()->get(Config::AutoTypeStartDelay).toInt()); Tools::wait(delay); // Grab the current active window after everything settles if (window == 0) { window = m_plugin->activeWindow(); } for (const auto& action : asConst(actions)) { if (m_plugin->activeWindow() != window) { qWarning("Active window changed, interrupting auto-type."); emit autotypeRejected(); m_inAutoType.unlock(); return; } constexpr int max_retries = 5; for (int i = 1; i <= max_retries; i++) { auto result = action->exec(m_executor); QCoreApplication::processEvents(QEventLoop::AllEvents, 10); if (result.isOk()) { break; } if (!result.canRetry() || i == max_retries) { if (getMainWindow()) { MessageBox::critical(getMainWindow(), tr("Auto-Type Error"), result.errorString()); } emit autotypeRejected(); m_inAutoType.unlock(); return; } Tools::wait(delay); } } m_windowForGlobal = 0; m_windowTitleForGlobal.clear(); emit autotypePerformed(); m_inAutoType.unlock(); } /** * Single Autotype entry-point function * Look up the Auto-Type sequence for the given entry then perfom Auto-Type in the active window */ void AutoType::performAutoType(const Entry* entry) { if (!m_plugin) { return; } auto sequences = entry->autoTypeSequences(); if (!sequences.isEmpty()) { executeAutoTypeActions(entry, sequences.first()); } } /** * Extra Autotype entry-point function * Perfom Auto-Type of the directly specified sequence in the active window */ void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& sequence) { if (!m_plugin) { return; } executeAutoTypeActions(entry, sequence); } void AutoType::startGlobalAutoType(const QString& search) { // Never Auto-Type into KeePassXC itself if (qApp->focusWindow()) { return; } m_windowForGlobal = m_plugin->activeWindow(); m_windowTitleForGlobal = m_plugin->activeWindowTitle(); #ifdef Q_OS_MACOS // Determine if the user has given proper permissions to KeePassXC to perform Auto-Type static bool accessibilityChecked = false; if (!accessibilityChecked) { if (macUtils()->enableAccessibility() && macUtils()->enableScreenRecording()) { accessibilityChecked = true; } else if (getMainWindow()) { // Does not have required permissions to Auto-Type, ignore the event MessageBox::information( nullptr, tr("Permission Required"), 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.")); return; } } m_windowState = WindowState::Normal; if (getMainWindow()) { if (getMainWindow()->isMinimized()) { m_windowState = WindowState::Minimized; } if (getMainWindow()->isHidden()) { m_windowState = WindowState::Hidden; } } #endif emit globalAutoTypeTriggered(search); } /** * Global Autotype entry-point function * Perform global Auto-Type on the active window */ void AutoType::performGlobalAutoType(const QList>& dbList, const QString& search) { if (!m_plugin) { return; } if (!m_inGlobalAutoTypeDialog.tryLock()) { return; } if (m_windowTitleForGlobal.isEmpty()) { m_inGlobalAutoTypeDialog.unlock(); return; } QList matchList; bool hideExpired = config()->get(Config::AutoTypeHideExpiredEntry).toBool(); for (const auto& db : dbList) { const QList dbEntries = db->rootGroup()->entriesRecursive(); for (auto entry : dbEntries) { auto group = entry->group(); if (!group || !group->resolveAutoTypeEnabled() || !entry->autoTypeEnabled()) { continue; } if (hideExpired && entry->isExpired()) { continue; } auto sequences = entry->autoTypeSequences(m_windowTitleForGlobal).toSet(); for (const auto& sequence : sequences) { matchList << AutoTypeMatch(entry, sequence); } } } // Show the selection dialog if we always ask, have multiple matches, or no matches if (getMainWindow() && (config()->get(Config::Security_AutoTypeAsk).toBool() || matchList.size() > 1 || matchList.isEmpty())) { // Close any open modal windows that would interfere with the process getMainWindow()->closeModalWindow(); auto* selectDialog = new AutoTypeSelectDialog(); selectDialog->setMatches(matchList, dbList, m_lastMatch); if (!search.isEmpty()) { selectDialog->setSearchString(search); } connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject); connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match, bool virtualMode) { m_lastMatch = match; m_lastMatchRetypeTimer.start(config()->get(Config::GlobalAutoTypeRetypeTime).toInt() * 1000); executeAutoTypeActions(match.first, match.second, m_windowForGlobal, virtualMode ? AutoTypeExecutor::Mode::VIRTUAL : AutoTypeExecutor::Mode::NORMAL); resetAutoTypeState(); }); connect(selectDialog, &QDialog::rejected, this, [this] { restoreWindowState(); resetAutoTypeState(); emit autotypeRejected(); }); #ifdef Q_OS_MACOS m_plugin->raiseOwnWindow(); Tools::wait(50); #endif selectDialog->show(); selectDialog->raise(); selectDialog->activateWindow(); } else if (!matchList.isEmpty()) { // Only one match and not asking, do it! executeAutoTypeActions(matchList.first().first, matchList.first().second, m_windowForGlobal); resetAutoTypeState(); } else { // We should never get here resetAutoTypeState(); emit autotypeRejected(); } } void AutoType::restoreWindowState() { #ifdef Q_OS_MAC if (getMainWindow()) { if (m_windowState == WindowState::Minimized) { getMainWindow()->showMinimized(); } else if (m_windowState == WindowState::Hidden) { getMainWindow()->hideWindow(); } } #endif } void AutoType::resetAutoTypeState() { m_windowForGlobal = 0; m_windowTitleForGlobal.clear(); Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock()); m_inGlobalAutoTypeDialog.unlock(); } /** * Parse an autotype sequence into a list of AutoTypeActions. * If error is provided then syntax checking will be performed. */ QList> AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QString& error, bool syntaxOnly) { if (!entry) { error = tr("Invalid entry provided"); return {}; } const int maxTypeDelay = 500; const int maxWaitDelay = 10000; const int maxRepetition = 100; QList> actions; actions << QSharedPointer::create(); actions << QSharedPointer::create(qMax(0, config()->get(Config::AutoTypeDelay).toInt()), true); // Replace escaped braces with a template for easier regex QString sequence = entrySequence; sequence.replace("{{}", "{LEFTBRACE}"); sequence.replace("{}}", "{RIGHTBRACE}"); // Quick test for bracket syntax if (sequence.count("{") != sequence.count("}")) { error = tr("Bracket imbalance detected, found extra { or }"); return {}; } // Group 1 = modifier key (opt) // Group 2 = full placeholder // Group 3 = inner placeholder (allows nested placeholders) // Group 4 = repeat (opt) // Group 5 = character QRegularExpression regex("([+%^#]*)(?:({((?>[^{}]+?|(?2))+?)(?:\\s+(\\d+))?})|(.))"); auto results = regex.globalMatch(sequence); while (results.hasNext()) { auto match = results.next(); // Parse modifier keys Qt::KeyboardModifiers modifiers; if (match.captured(1).contains('+')) { modifiers |= Qt::ShiftModifier; } if (match.captured(1).contains('^')) { modifiers |= Qt::ControlModifier; } if (match.captured(1).contains('%')) { modifiers |= Qt::AltModifier; } if (match.captured(1).contains('#')) { modifiers |= Qt::MetaModifier; } const auto fullPlaceholder = match.captured(2); auto placeholder = match.captured(3).toLower(); auto repeat = 1; if (!match.captured(4).isEmpty()) { repeat = match.captured(4).toInt(); } auto character = match.captured(5); if (placeholder.isEmpty()) { if (!character.isEmpty()) { // Type a single character with modifier actions << QSharedPointer::create(character[0], modifiers); } continue; } if (g_placeholderToKey.contains(placeholder)) { // Basic key placeholder, allow repeat if (repeat > maxRepetition) { error = tr("Too many repetitions detected, max is %1: %2").arg(maxRepetition).arg(fullPlaceholder); return {}; } QSharedPointer action; if (g_placeholderToKey[placeholder] == Qt::Key_unknown) { action = QSharedPointer::create(placeholder[0], modifiers); } else { action = QSharedPointer::create(g_placeholderToKey[placeholder], modifiers); } for (int i = 1; i <= repeat && i <= maxRepetition; ++i) { actions << action; } } else if (placeholder.startsWith("delay=")) { // Change keypress delay int delay = placeholder.replace("delay=", "").toInt(); if (delay > maxTypeDelay) { error = tr("Very slow key press detected, max is %1: %2").arg(maxTypeDelay).arg(fullPlaceholder); return {}; } actions << QSharedPointer::create(qBound(0, delay, maxTypeDelay), true); } else if (placeholder == "delay") { // Mid typing delay (wait) if (repeat > maxWaitDelay) { error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder); return {}; } actions << QSharedPointer::create(qBound(0, repeat, maxWaitDelay)); } else if (placeholder == "clearfield") { // Platform-specific field clearing actions << QSharedPointer::create(); } else if (placeholder == "totp") { // Entry totp (requires special handling) QString totp = entry->totp(); for (const auto& ch : totp) { actions << QSharedPointer::create(ch); } } else if (placeholder == "pickchars") { // Ignore this if we are syntax checking if (syntaxOnly) { continue; } // Show pickchars dialog for entry's password auto password = entry->resolvePlaceholder(entry->password()); if (!password.isEmpty()) { PickcharsDialog pickcharsDialog(password); if (pickcharsDialog.exec() == QDialog::Accepted && !pickcharsDialog.selectedChars().isEmpty()) { for (const auto& ch : pickcharsDialog.selectedChars()) { actions << QSharedPointer::create(ch); if (pickcharsDialog.pressTab()) { actions << QSharedPointer::create(g_placeholderToKey["tab"]); } } if (pickcharsDialog.pressTab()) { // Remove extra tab actions.removeLast(); } } } } else if (placeholder.startsWith("t-conv:")) { // Reset to the original capture to preserve case placeholder = match.captured(3); placeholder.replace("t-conv:", "", Qt::CaseInsensitive); if (!placeholder.isEmpty()) { auto sep = placeholder[0]; auto parts = placeholder.split(sep); if (parts.size() >= 4) { auto resolved = entry->resolveMultiplePlaceholders(parts[1]); auto type = parts[2].toLower(); if (type == "base64") { resolved = resolved.toUtf8().toBase64(); } else if (type == "hex") { resolved = resolved.toUtf8().toHex(); } else if (type == "uri") { resolved = QUrl::toPercentEncoding(resolved.toUtf8()); } else if (type == "uri-dec") { resolved = QUrl::fromPercentEncoding(resolved.toUtf8()); } else if (type.startsWith("u")) { resolved = resolved.toUpper(); } else if (type.startsWith("l")) { resolved = resolved.toLower(); } else { error = tr("Invalid conversion type: %1").arg(type); return {}; } for (const QChar& ch : resolved) { actions << QSharedPointer::create(ch); } } else { error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder); return {}; } } else { error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder); return {}; } } else if (placeholder.startsWith("t-replace-rx:")) { // Reset to the original capture to preserve case placeholder = match.captured(3); placeholder.replace("t-replace-rx:", "", Qt::CaseInsensitive); if (!placeholder.isEmpty()) { auto sep = placeholder[0]; auto parts = placeholder.split(sep); if (parts.size() >= 5) { auto resolvedText = entry->resolveMultiplePlaceholders(parts[1]); auto resolvedSearch = entry->resolveMultiplePlaceholders(parts[2]); auto resolvedReplace = entry->resolveMultiplePlaceholders(parts[3]); // Replace $ with \\ to support Qt substitutions resolvedReplace.replace(QRegularExpression(R"(\$(\d+))"), R"(\\1)"); auto searchRegex = QRegularExpression(resolvedSearch); if (!searchRegex.isValid()) { error = tr("Invalid regular expression syntax %1\n%2") .arg(resolvedSearch, searchRegex.errorString()); return {}; } auto resolved = resolvedText.replace(searchRegex, resolvedReplace); for (const QChar& ch : resolved) { actions << QSharedPointer::create(ch); } } else { error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder); return {}; } } else { error = tr("Invalid conversion syntax: %1").arg(fullPlaceholder); return {}; } } else if (placeholder.startsWith("mode=")) { auto mode = AutoTypeExecutor::Mode::NORMAL; if (placeholder.endsWith("virtual")) { mode = AutoTypeExecutor::Mode::VIRTUAL; } actions << QSharedPointer::create(mode); } else if (placeholder == "beep" || placeholder.startsWith("vkey") || placeholder.startsWith("appactivate") || placeholder.startsWith("c:")) { // Ignore these commands } else { // Attempt to resolve an entry attribute auto resolved = entry->resolvePlaceholder(fullPlaceholder); if (resolved != fullPlaceholder) { // Attribute resolved, add characters to action list for (const QChar& ch : resolved) { if (ch == '\n') { actions << QSharedPointer::create(g_placeholderToKey["enter"]); } else if (ch == '\t') { actions << QSharedPointer::create(g_placeholderToKey["tab"]); } else { actions << QSharedPointer::create(ch); } } } else { // Invalid placeholder, issue error and stop processing error = tr("Invalid placeholder: %1").arg(fullPlaceholder); return {}; } } } return actions; } /** * Verify if the syntax of an autotype sequence is correct and doesn't have invalid parameters */ bool AutoType::verifyAutoTypeSyntax(const QString& sequence, const Entry* entry, QString& error) { error.clear(); if (!sequence.isEmpty()) { parseSequence(sequence, entry, error, true); } return error.isEmpty(); }