diff --git a/CMakeLists.txt b/CMakeLists.txt index 78155e3a..b61d7f78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -262,11 +262,6 @@ else() set(PROGNAME keepassxc) endif() -if(APPLE AND WITH_APP_BUNDLE AND "${CMAKE_INSTALL_PREFIX}" STREQUAL "/usr/local") - set(CMAKE_INSTALL_PREFIX "/Applications") - set(CMAKE_INSTALL_MANDIR "/usr/local/share/man") -endif() - if(MINGW) set(CLI_INSTALL_DIR ".") set(PROXY_INSTALL_DIR ".") @@ -274,9 +269,10 @@ if(MINGW) set(PLUGIN_INSTALL_DIR ".") set(DATA_INSTALL_DIR "share") elseif(APPLE AND WITH_APP_BUNDLE) - set(CLI_INSTALL_DIR "/usr/local/bin") - set(PROXY_INSTALL_DIR "/usr/local/bin") - set(BIN_INSTALL_DIR ".") + set(CMAKE_INSTALL_MANDIR "${PROGNAME}.app/Contents/Resources/man") + set(CLI_INSTALL_DIR "${PROGNAME}.app/Contents/MacOS") + set(PROXY_INSTALL_DIR "${PROGNAME}.app/Contents/MacOS") + set(BIN_INSTALL_DIR "${PROGNAME}.app/Contents/MacOS") set(PLUGIN_INSTALL_DIR "${PROGNAME}.app/Contents/PlugIns") set(DATA_INSTALL_DIR "${PROGNAME}.app/Contents/Resources") else() @@ -314,8 +310,8 @@ set(QT_COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools) if(UNIX AND NOT APPLE) find_package(Qt5 COMPONENTS ${QT_COMPONENTS} DBus REQUIRED) elseif(APPLE) - find_package(Qt5 COMPONENTS ${QT_COMPONENTS} REQUIRED HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH) - find_package(Qt5 COMPONENTS MacExtras HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH) + find_package(Qt5 COMPONENTS ${QT_COMPONENTS} REQUIRED HINTS /usr/local/opt/qt/lib/cmake /usr/local/Cellar/qt/*/lib/cmake ENV PATH) + find_package(Qt5 COMPONENTS MacExtras HINTS /usr/local/opt/qt/lib/cmake /usr/local/Cellar/qt/*/lib/cmake ENV PATH) else() find_package(Qt5 COMPONENTS ${QT_COMPONENTS} REQUIRED) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 29debb5a..14fbd588 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -175,7 +175,9 @@ if(APPLE) set(keepassx_SOURCES ${keepassx_SOURCES} core/ScreenLockListenerMac.cpp - core/MacPasteboard.cpp) + core/MacPasteboard.cpp + gui/macutils/MacUtils.cpp + gui/macutils/AppKitImpl.mm) endif() if(UNIX AND NOT APPLE) set(keepassx_SOURCES @@ -287,7 +289,7 @@ if(WITH_XC_KEESHARE) endif() if(APPLE) - target_link_libraries(keepassx_core "-framework Foundation") + target_link_libraries(keepassx_core "-framework Foundation -framework AppKit") if(Qt5MacExtras_FOUND) target_link_libraries(keepassx_core Qt5::MacExtras) endif() diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp index d8fcf6d7..60cec114 100644 --- a/src/autotype/mac/AutoTypeMac.cpp +++ b/src/autotype/mac/AutoTypeMac.cpp @@ -17,6 +17,7 @@ */ #include "AutoTypeMac.h" +#include "gui/macutils/MacUtils.h" #include @@ -25,8 +26,7 @@ #define INVALID_KEYCODE 0xFFFF AutoTypePlatformMac::AutoTypePlatformMac() - : m_appkit(new AppKit()) - , m_hotkeyRef(nullptr) + : m_hotkeyRef(nullptr) , m_hotkeyId({ 'kpx2', HOTKEY_ID }) { EventTypeSpec eventSpec; @@ -79,7 +79,7 @@ QStringList AutoTypePlatformMac::windowTitles() // WId AutoTypePlatformMac::activeWindow() { - return m_appkit->activeProcessId(); + return macUtils()->activeWindow(); } // @@ -159,7 +159,7 @@ AutoTypeExecutor* AutoTypePlatformMac::createExecutor() // bool AutoTypePlatformMac::raiseWindow(WId pid) { - return m_appkit->activateProcess(pid); + return macUtils()->raiseWindow(pid); } // @@ -167,7 +167,7 @@ bool AutoTypePlatformMac::raiseWindow(WId pid) // bool AutoTypePlatformMac::raiseLastActiveWindow() { - return m_appkit->activateProcess(m_appkit->lastActiveProcessId()); + return macUtils()->raiseLastActiveWindow(); } // @@ -175,7 +175,7 @@ bool AutoTypePlatformMac::raiseLastActiveWindow() // bool AutoTypePlatformMac::raiseOwnWindow() { - return m_appkit->activateProcess(m_appkit->ownProcessId()); + return macUtils()->raiseOwnWindow(); } // diff --git a/src/autotype/mac/AutoTypeMac.h b/src/autotype/mac/AutoTypeMac.h index d2c22478..875c2176 100644 --- a/src/autotype/mac/AutoTypeMac.h +++ b/src/autotype/mac/AutoTypeMac.h @@ -23,7 +23,6 @@ #include #include -#include "AppKit.h" #include "autotype/AutoTypePlatformPlugin.h" #include "autotype/AutoTypeAction.h" @@ -55,7 +54,6 @@ signals: void globalShortcutTriggered(); private: - std::unique_ptr m_appkit; EventHotKeyRef m_hotkeyRef; EventHotKeyID m_hotkeyId; diff --git a/src/autotype/mac/CMakeLists.txt b/src/autotype/mac/CMakeLists.txt index f2915bc0..f1c5387f 100644 --- a/src/autotype/mac/CMakeLists.txt +++ b/src/autotype/mac/CMakeLists.txt @@ -1,6 +1,8 @@ set(autotype_mac_SOURCES AutoTypeMac.cpp) -set(autotype_mac_mm_SOURCES AppKitImpl.mm) +set(autotype_mac_mm_SOURCES + ${CMAKE_SOURCE_DIR}/src/gui/macutils/AppKitImpl.mm + ${CMAKE_SOURCE_DIR}/src/gui/macutils/MacUtils.cpp) add_library(keepassx-autotype-cocoa MODULE ${autotype_mac_SOURCES} ${autotype_mac_mm_SOURCES}) set_target_properties(keepassx-autotype-cocoa PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon") diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 6b85c786..0ccd3106 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -33,7 +33,12 @@ #include "core/Group.h" #include "core/Metadata.h" #include "core/PasswordGenerator.h" +#include "core/Tools.h" #include "gui/MainWindow.h" +#include "gui/MessageBox.h" +#ifdef Q_OS_MACOS +#include "gui/macutils/MacUtils.h" +#endif const char BrowserService::KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings"; const char BrowserService::KEEPASSXCBROWSER_OLD_NAME[] = "keepassxc-browser Settings"; @@ -49,6 +54,7 @@ BrowserService::BrowserService(DatabaseTabWidget* parent) : m_dbTabWidget(parent) , m_dialogActive(false) , m_bringToFrontRequested(false) + , m_wasMinimized(false) , m_keepassBrowserUUID(QUuid::fromRfc4122(QByteArray::fromHex("de887cc3036343b8974b5911b8816224"))) { // Don't connect the signals when used from DatabaseSettingsWidgetBrowser (parent is nullptr) @@ -89,8 +95,9 @@ bool BrowserService::openDatabase(bool triggerUnlock) } if (triggerUnlock) { - getMainWindow()->bringToFront(); m_bringToFrontRequested = true; + m_wasMinimized = getMainWindow()->isMinimized(); + raiseWindow(true); } return false; @@ -157,7 +164,7 @@ QString BrowserService::storeKey(const QString& key) } bool contains; - QMessageBox::StandardButton dialogResult = QMessageBox::No; + MessageBox::Button dialogResult = MessageBox::Cancel; do { QInputDialog keyDialog; @@ -167,6 +174,7 @@ QString BrowserService::storeKey(const QString& key) "give it a unique name to identify and accept it.")); keyDialog.setOkButtonText(tr("Save and allow access")); keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint); + raiseWindow(); keyDialog.show(); keyDialog.activateWindow(); keyDialog.raise(); @@ -175,20 +183,23 @@ QString BrowserService::storeKey(const QString& key) id = keyDialog.textValue(); if (ok != QDialog::Accepted || id.isEmpty()) { + hideWindow(); return {}; } contains = db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); if (contains) { - dialogResult = QMessageBox::warning(nullptr, - tr("KeePassXC: Overwrite existing key?"), - tr("A shared encryption key with the name \"%1\" " - "already exists.\nDo you want to overwrite it?") - .arg(id), - QMessageBox::Yes | QMessageBox::No); + dialogResult = MessageBox::warning(nullptr, + tr("KeePassXC: Overwrite existing key?"), + tr("A shared encryption key with the name \"%1\" " + "already exists.\nDo you want to overwrite it?") + .arg(id), + MessageBox::Overwrite | MessageBox::Cancel, + MessageBox::Cancel); } - } while (contains && dialogResult == QMessageBox::No); + } while (contains && dialogResult == MessageBox::Cancel); + hideWindow(); db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key); return id; } @@ -367,26 +378,25 @@ void BrowserService::updateEntry(const QString& id, if (username.compare(login, Qt::CaseSensitive) != 0 || entry->password().compare(password, Qt::CaseSensitive) != 0) { - int dialogResult = QMessageBox::No; + MessageBox::Button dialogResult = MessageBox::No; if (!browserSettings()->alwaysAllowUpdate()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("KeePassXC: Update Entry")); - msgBox.setText(tr("Do you want to update the information in %1 - %2?").arg(QUrl(url).host(), username)); - msgBox.setStandardButtons(QMessageBox::Yes); - msgBox.addButton(QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); - msgBox.setWindowFlags(Qt::WindowStaysOnTopHint); - msgBox.activateWindow(); - msgBox.raise(); - dialogResult = msgBox.exec(); + raiseWindow(); + dialogResult = MessageBox::question(nullptr, + tr("KeePassXC: Update Entry"), + tr("Do you want to update the information in %1 - %2?") + .arg(QUrl(url).host(), username), + MessageBox::Save | MessageBox::Cancel, + MessageBox::Cancel, MessageBox::Raise); } - if (browserSettings()->alwaysAllowUpdate() || dialogResult == QMessageBox::Yes) { + if (browserSettings()->alwaysAllowUpdate() || dialogResult == MessageBox::Save) { entry->beginUpdate(); entry->setUsername(login); entry->setPassword(password); entry->endUpdate(); } + + hideWindow(); } } @@ -497,24 +507,24 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer curr progress.reset(); if (counter > 0) { - QMessageBox::information(nullptr, - tr("KeePassXC: Converted KeePassHTTP attributes"), - tr("Successfully converted attributes from %1 entry(s).\n" - "Moved %2 keys to custom data.", - "") - .arg(counter) - .arg(keyCounter), - QMessageBox::Ok); + MessageBox::information(nullptr, + tr("KeePassXC: Converted KeePassHTTP attributes"), + tr("Successfully converted attributes from %1 entry(s).\n" + "Moved %2 keys to custom data.", + "") + .arg(counter) + .arg(keyCounter), + MessageBox::Ok); } else if (counter == 0 && keyCounter > 0) { - QMessageBox::information(nullptr, - tr("KeePassXC: Converted KeePassHTTP attributes"), - tr("Successfully moved %n keys to custom data.", "", keyCounter), - QMessageBox::Ok); + MessageBox::information(nullptr, + tr("KeePassXC: Converted KeePassHTTP attributes"), + tr("Successfully moved %n keys to custom data.", "", keyCounter), + MessageBox::Ok); } else { - QMessageBox::information(nullptr, - tr("KeePassXC: No entry with KeePassHTTP attributes found!"), - tr("The active database does not contain an entry with KeePassHTTP attributes."), - QMessageBox::Ok); + MessageBox::information(nullptr, + tr("KeePassXC: No entry with KeePassHTTP attributes found!"), + tr("The active database does not contain an entry with KeePassHTTP attributes."), + MessageBox::Ok); } // Rename password groupName @@ -593,6 +603,11 @@ bool BrowserService::confirmEntries(QList& pwEntriesToConfirm, accessControlDialog.setUrl(url); accessControlDialog.setItems(pwEntriesToConfirm); + raiseWindow(); + accessControlDialog.show(); + accessControlDialog.activateWindow(); + accessControlDialog.raise(); + int res = accessControlDialog.exec(); if (accessControlDialog.remember()) { for (Entry* entry : pwEntriesToConfirm) { @@ -616,6 +631,7 @@ bool BrowserService::confirmEntries(QList& pwEntriesToConfirm, } m_dialogActive = false; + hideWindow(); if (res == QDialog::Accepted) { return true; } @@ -901,13 +917,40 @@ bool BrowserService::checkLegacySettings() return false; } - auto dialogResult = QMessageBox::warning(nullptr, - tr("KeePassXC: Legacy browser integration settings detected"), - tr("Legacy browser integration settings have been detected.\n" - "Do you want to upgrade the settings to the latest standard?\n" - "This is necessary to maintain compatibility with the browser plugin."), - QMessageBox::Yes | QMessageBox::No); - return dialogResult == QMessageBox::Yes; + auto dialogResult = MessageBox::warning(nullptr, + tr("KeePassXC: Legacy browser integration settings detected"), + tr("Legacy browser integration settings have been detected.\n" + "Do you want to upgrade the settings to the latest standard?\n" + "This is necessary to maintain compatibility with the browser plugin."), + MessageBox::Yes | MessageBox::No); + + return dialogResult == MessageBox::Yes; +} + +void BrowserService::hideWindow() const +{ + if (m_wasMinimized) { + getMainWindow()->showMinimized(); + } else { +#ifdef Q_OS_MACOS + macUtils()->raiseLastActiveWindow(); +#else + getMainWindow()->lower(); +#endif + } +} + +void BrowserService::raiseWindow(const bool force) +{ + m_wasMinimized = getMainWindow()->isMinimized(); +#ifdef Q_OS_MACOS + macUtils()->raiseOwnWindow(); + Tools::wait(500); +#else + if (force) { + getMainWindow()->bringToFront(); + } +#endif } void BrowserService::databaseLocked(DatabaseWidget* dbWidget) @@ -921,7 +964,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) { if (dbWidget) { if (m_bringToFrontRequested) { - getMainWindow()->lower(); + hideWindow(); m_bringToFrontRequested = false; } emit databaseUnlocked(); diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 335f2a13..74febe4f 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -117,11 +117,14 @@ private: bool moveSettingsToCustomData(Entry* entry, const QString& name) const; int moveKeysToCustomData(Entry* entry, QSharedPointer db) const; bool checkLegacySettings(); + void hideWindow() const; + void raiseWindow(const bool force = false); private: DatabaseTabWidget* const m_dbTabWidget; bool m_dialogActive; bool m_bringToFrontRequested; + bool m_wasMinimized; QUuid m_keepassBrowserUUID; }; diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 37a085ad..4c52e6b7 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -16,6 +16,7 @@ set(cli_SOURCES Add.cpp Clip.cpp + Create.cpp Command.cpp Diceware.cpp Edit.cpp @@ -46,6 +47,44 @@ install(TARGETS keepassxc-cli BUNDLE DESTINATION . COMPONENT Runtime RUNTIME DESTINATION ${CLI_INSTALL_DIR} COMPONENT Runtime) +if(APPLE AND WITH_APP_BUNDLE) + add_custom_command(TARGET keepassxc-cli + POST_BUILD + COMMAND ${CMAKE_INSTALL_NAME_TOOL} + -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore + "@executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore" + -change /usr/local/opt/qt/lib/QtGui.framework/Versions/5/QtGui + "@executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui" + -change /usr/local/opt/qt/lib/QtMacExtras.framework/Versions/5/QtMacExtras + "@executable_path/../Frameworks/QtMacExtras.framework/Versions/5/QtMacExtras" + -change /usr/local/opt/qt/lib/QtConcurrent.framework/Versions/5/QtConcurrent + "@executable_path/../Frameworks/QtConcurrent.framework/Versions/5/QtConcurrent" + -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore + "@executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore" + -change /usr/local/opt/qt/lib/QtNetwork.framework/Versions/5/QtNetwork + "@executable_path/../Frameworks/QtNetwork.framework/Versions/5/QtNetwork" + -change /usr/local/opt/qt/lib/QtWidgets.framework/Versions/5/QtWidgets + "@executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets" + -change /usr/local/opt/qt/lib/QtSvg.framework/Versions/5/QtSvg + "@executable_path/../Frameworks/QtSvg.framework/Versions/5/QtSvg" + -change /usr/local/opt/libgcrypt/lib/libgcrypt.20.dylib + "@executable_path/../Frameworks/libgcrypt.20.dylib" + -change /usr/local/opt/argon2/lib/libargon2.1.dylib + "@executable_path/../Frameworks/libargon2.1.dylib" + -change /usr/local/opt/libgpg-error/lib/libgpg-error.0.dylib + "@executable_path/../Frameworks/libgpg-error.0.dylib" + -change /usr/local/opt/libsodium/lib/libsodium.23.dylib + "@executable_path/../Frameworks/libsodium.23.dylib" + -change /usr/local/opt/qrencode/lib/libqrencode.4.dylib + "@executable_path/../Frameworks/libqrencode.4.dylib" + -change /usr/local/opt/libyubikey/lib/libyubikey.0.dylib + "@executable_path/../Frameworks/libyubikey.0.dylib" + -change /usr/local/opt/ykpers/lib/libykpers-1.1.dylib + "@executable_path/../Frameworks/libykpers-1.1.dylib" + keepassxc-cli + COMMENT "Changing linking of keepassxc-cli") +endif() + if(APPLE OR UNIX) install(FILES keepassxc-cli.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1/) execute_process(COMMAND mandb -q) diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index ff74089a..2d34770a 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -24,6 +24,7 @@ #include "Add.h" #include "Clip.h" +#include "Create.h" #include "Diceware.h" #include "Edit.h" #include "Estimate.h" @@ -69,6 +70,7 @@ void populateCommands() if (commands.isEmpty()) { commands.insert(QString("add"), new Add()); commands.insert(QString("clip"), new Clip()); + commands.insert(QString("create"), new Create()); commands.insert(QString("diceware"), new Diceware()); commands.insert(QString("edit"), new Edit()); commands.insert(QString("estimate"), new Estimate()); diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp new file mode 100644 index 00000000..9bea1f9e --- /dev/null +++ b/src/cli/Create.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 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 +#include + +#include +#include +#include +#include + +#include "Create.h" +#include "Utils.h" + +#include "core/Database.h" + +#include "keys/CompositeKey.h" +#include "keys/Key.h" + +Create::Create() +{ + name = QString("create"); + description = QObject::tr("Create a new database."); +} + +Create::~Create() +{ +} + +/** + * Create a database file using the command line. A key file and/or + * password can be specified to encrypt the password. If none is + * specified the function will fail. + * + * If a key file is specified but it can't be loaded, the function will + * fail. + * + * If the database is being saved in a non existant directory, the + * function will fail. + * + * @return EXIT_SUCCESS on success, or EXIT_FAILURE on failure + */ +int Create::execute(const QStringList& arguments) +{ + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); + + QCommandLineParser parser; + + parser.setApplicationDescription(description); + parser.addPositionalArgument("database", QObject::tr("Path of the database.")); + parser.addOption(Command::KeyFileOption); + + parser.addHelpOption(); + parser.process(arguments); + + const QStringList args = parser.positionalArguments(); + if (args.size() < 1) { + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli create"); + return EXIT_FAILURE; + } + + QString databaseFilename = args.at(0); + if (QFileInfo::exists(databaseFilename)) { + err << QObject::tr("File %1 already exists.").arg(databaseFilename) << endl; + return EXIT_FAILURE; + } + + auto key = QSharedPointer::create(); + + auto password = getPasswordFromStdin(); + if (!password.isNull()) { + key->addKey(password); + } + + QSharedPointer fileKey; + if(parser.isSet(Command::KeyFileOption)) { + if (!loadFileKey(parser.value(Command::KeyFileOption), fileKey)) { + err << QObject::tr("Loading the key file failed") << endl; + return EXIT_FAILURE; + } + } + + if (!fileKey.isNull()) { + key->addKey(fileKey); + } + + if (key->isEmpty()) { + err << QObject::tr("No key is set. Aborting database creation.") << endl; + return EXIT_FAILURE; + } + + Database db; + db.setKey(key); + + QString errorMessage; + if (!db.save(databaseFilename, &errorMessage, true, false)) { + err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + + out << QObject::tr("Successfully created new database.") << endl; + return EXIT_SUCCESS; +} + +/** + * Read optional password from stdin. + * + * @return Pointer to the PasswordKey or null if passwordkey is skipped + * by user + */ +QSharedPointer Create::getPasswordFromStdin() +{ + QSharedPointer passwordKey; + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + + out << QObject::tr("Insert password to encrypt database (Press enter to leave blank): "); + out.flush(); + QString password = Utils::getPassword(); + + if (!password.isEmpty()) { + passwordKey = QSharedPointer(new PasswordKey(password)); + } + + return passwordKey; +} + +/** + * Load a key file from disk. When the path specified does not exist a + * new file will be generated. No folders will be generated so the parent + * folder of the specified file nees to exist + * + * If the key file cannot be loaded or created the function will fail. + * + * @param path Path to the key file to be loaded + * @param fileKey Resulting fileKey + * @return true if the key file was loaded succesfully + */ +bool Create::loadFileKey(QString path, QSharedPointer& fileKey) +{ + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); + + QString error; + fileKey = QSharedPointer(new FileKey()); + + if (!QFileInfo::exists(path)) { + fileKey->create(path, &error); + + if (!error.isEmpty()) { + err << QObject::tr("Creating KeyFile %1 failed: %2").arg(path, error) << endl; + return false; + } + } + + if (!fileKey->load(path, &error)) { + err << QObject::tr("Loading KeyFile %1 failed: %2").arg(path, error) << endl; + return false; + } + + return true; +} diff --git a/src/cli/Create.h b/src/cli/Create.h new file mode 100644 index 00000000..9c14d37b --- /dev/null +++ b/src/cli/Create.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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 . + */ + +#ifndef KEEPASSXC_CREATE_H +#define KEEPASSXC_CREATE_H + +#include "Command.h" + +#include "keys/FileKey.h" +#include "keys/PasswordKey.h" + +class Create : public Command +{ +public: + Create(); + ~Create(); + int execute(const QStringList& arguments); + +private: + QSharedPointer getPasswordFromStdin(); + QSharedPointer getFileKeyFromStdin(); + bool loadFileKey(QString path, QSharedPointer& fileKey); +}; + +#endif // KEEPASSXC_CREATE_H diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index d7b0f9ad..22cf88a3 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -19,6 +19,9 @@ Adds a new entry to a database. A password can be generated (\fI-g\fP option), o .IP "clip [options] [timeout]" Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. +.IP "create [options] " +Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. + .IP "diceware [options]" Generate a random diceware passphrase. diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index a2399e74..b9e3853f 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -42,9 +42,7 @@ int main(int argc, char** argv) QCoreApplication app(argc, argv); QCoreApplication::setApplicationVersion(KEEPASSXC_VERSION); -#ifdef QT_NO_DEBUG - Bootstrap::bootstrapApplication(); -#endif + Bootstrap::bootstrap(); TextStream out(stdout); QStringList arguments; diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index a62cc5a9..10dd9b2d 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -16,12 +16,30 @@ */ #include "Bootstrap.h" +#include "config-keepassx.h" #include "core/Config.h" #include "core/Translator.h" +#include "gui/MessageBox.h" #ifdef Q_OS_WIN #include // for createWindowsDACL() #include // for Sleep(), SetDllDirectoryA(), SetSearchPathMode(), ... +#undef MessageBox +#endif + +#if defined(HAVE_RLIMIT_CORE) +#include +#endif + +#if defined(HAVE_PR_SET_DUMPABLE) +#include +#endif + +#ifdef HAVE_PT_DENY_ATTACH +// clang-format off +#include +#include +// clang-format on #endif namespace Bootstrap @@ -44,11 +62,10 @@ namespace Bootstrap } /** - * Perform early application bootstrapping such as setting up search paths, - * configuration OS security properties, and loading translators. - * A QApplication object has to be instantiated before calling this function. + * Perform early application bootstrapping that does not rely on a QApplication + * being present. */ - void bootstrapApplication() + void bootstrap() { #ifdef QT_NO_DEBUG disableCoreDumps(); @@ -56,6 +73,17 @@ namespace Bootstrap setupSearchPaths(); applyEarlyQNetworkAccessManagerWorkaround(); Translator::installTranslators(); + } + + /** + * Perform early application bootstrapping such as setting up search paths, + * configuration OS security properties, and loading translators. + * A QApplication object has to be instantiated before calling this function. + */ + void bootstrapApplication() + { + bootstrap(); + MessageBox::initializeButtonDefs(); #ifdef Q_OS_MACOS // Don't show menu icons on OSX @@ -137,6 +165,8 @@ namespace Bootstrap HANDLE hToken = nullptr; PTOKEN_USER pTokenUser = nullptr; DWORD cbBufferSize = 0; + PSID pLocalSystemSid = nullptr; + DWORD pLocalSystemSidSize = SECURITY_MAX_SID_SIZE; // Access control list PACL pACL = nullptr; @@ -163,8 +193,19 @@ namespace Bootstrap goto Cleanup; } + // Retrieve LocalSystem account SID + pLocalSystemSid = static_cast(HeapAlloc(GetProcessHeap(), 0, pLocalSystemSidSize)); + if (pLocalSystemSid == nullptr) { + goto Cleanup; + } + + if (!CreateWellKnownSid(WinLocalSystemSid, nullptr, pLocalSystemSid, &pLocalSystemSidSize)) { + goto Cleanup; + } + // Calculate the amount of memory that must be allocated for the DACL - cbACL = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pTokenUser->User.Sid); + cbACL = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pTokenUser->User.Sid) + + sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pLocalSystemSid); // Create and initialize an ACL pACL = static_cast(HeapAlloc(GetProcessHeap(), 0, cbACL)); @@ -186,6 +227,18 @@ namespace Bootstrap goto Cleanup; } +#ifdef WITH_XC_SSHAGENT + // OpenSSH for Windows ssh-agent service is running as LocalSystem + if (!AddAccessAllowedAce( + pACL, + ACL_REVISION, + PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE, // just enough for ssh-agent + pLocalSystemSid // known LocalSystem sid + )) { + goto Cleanup; + } +#endif + // Set discretionary access control list bSuccess = ERROR_SUCCESS == SetSecurityInfo(GetCurrentProcess(), // object handle @@ -202,6 +255,9 @@ namespace Bootstrap if (pACL != nullptr) { HeapFree(GetProcessHeap(), 0, pACL); } + if (pLocalSystemSid != nullptr) { + HeapFree(GetProcessHeap(), 0, pLocalSystemSid); + } if (pTokenUser != nullptr) { HeapFree(GetProcessHeap(), 0, pTokenUser); } diff --git a/src/core/Bootstrap.h b/src/core/Bootstrap.h index 95158fb8..de1a4d83 100644 --- a/src/core/Bootstrap.h +++ b/src/core/Bootstrap.h @@ -22,6 +22,7 @@ namespace Bootstrap { + void bootstrap(); void bootstrapApplication(); void restoreMainWindowState(MainWindow& mainWindow); void disableCoreDumps(); diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 213bc315..d7a3089c 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -24,6 +24,7 @@ #include "core/DatabaseIcons.h" #include "core/Group.h" #include "core/Metadata.h" +#include "core/Tools.h" #include "totp/totp.h" #include @@ -100,6 +101,32 @@ void Entry::setUpdateTimeinfo(bool value) m_updateTimeinfo = value; } +QString Entry::buildReference(const QUuid& uuid, const QString& field) +{ + Q_ASSERT(EntryAttributes::DefaultAttributes.count(field) > 0); + + QString uuidStr = Tools::uuidToHex(uuid).toUpper(); + QString shortField; + + if (field == EntryAttributes::TitleKey) { + shortField = "T"; + } else if (field == EntryAttributes::UserNameKey) { + shortField = "U"; + } else if (field == EntryAttributes::PasswordKey) { + shortField = "P"; + } else if (field == EntryAttributes::URLKey) { + shortField = "A"; + } else if (field == EntryAttributes::NotesKey) { + shortField = "N"; + } + + if (shortField.isEmpty()) { + return {}; + } + + return QString("{REF:%1@I:%2}").arg(shortField, uuidStr); +} + EntryReferenceType Entry::referenceType(const QString& referenceStr) { const QString referenceLowerStr = referenceStr.toLower(); @@ -130,7 +157,7 @@ const QUuid& Entry::uuid() const const QString Entry::uuidToHex() const { - return QString::fromLatin1(m_uuid.toRfc4122().toHex()); + return Tools::uuidToHex(m_uuid); } QImage Entry::icon() const @@ -304,11 +331,25 @@ QString Entry::notes() const return m_attributes->value(EntryAttributes::NotesKey); } +QString Entry::attribute(const QString& key) const +{ + return m_attributes->value(key); +} + bool Entry::isExpired() const { return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc(); } +bool Entry::isAttributeReferenceOf(const QString& key, const QUuid& uuid) const +{ + if (!m_attributes->isReference(key)) { + return false; + } + + return m_attributes->value(key).contains(Tools::uuidToHex(uuid), Qt::CaseInsensitive); +} + bool Entry::hasReferences() const { const QList keyList = EntryAttributes::DefaultAttributes; @@ -320,6 +361,26 @@ bool Entry::hasReferences() const return false; } +bool Entry::hasReferencesTo(const QUuid& uuid) const +{ + const QList keyList = EntryAttributes::DefaultAttributes; + for (const QString& key : keyList) { + if (isAttributeReferenceOf(key, uuid)) { + return true; + } + } + return false; +} + +void Entry::replaceReferencesWithValues(const Entry* other) +{ + for (const QString& key : EntryAttributes::DefaultAttributes) { + if (isAttributeReferenceOf(key, other->uuid())) { + setDefaultAttribute(key, other->attribute(key)); + } + } +} + EntryAttributes* Entry::attributes() { return m_attributes; @@ -496,6 +557,17 @@ void Entry::setNotes(const QString& notes) m_attributes->set(EntryAttributes::NotesKey, notes, m_attributes->isProtected(EntryAttributes::NotesKey)); } +void Entry::setDefaultAttribute(const QString& attribute, const QString& value) +{ + Q_ASSERT(EntryAttributes::isDefaultAttribute(attribute)); + + if (!EntryAttributes::isDefaultAttribute(attribute)) { + return; + } + + m_attributes->set(attribute, value, m_attributes->isProtected(attribute)); +} + void Entry::setExpires(const bool& value) { if (m_data.timeInfo.expires() != value) { @@ -654,16 +726,17 @@ Entry* Entry::clone(CloneFlags flags) const entry->m_attachments->copyDataFrom(m_attachments); if (flags & CloneUserAsRef) { - // Build the username reference - QString username = "{REF:U@I:" + uuidToHex() + "}"; entry->m_attributes->set( - EntryAttributes::UserNameKey, username.toUpper(), m_attributes->isProtected(EntryAttributes::UserNameKey)); + EntryAttributes::UserNameKey, + buildReference(uuid(), EntryAttributes::UserNameKey), + m_attributes->isProtected(EntryAttributes::UserNameKey)); } if (flags & ClonePassAsRef) { - QString password = "{REF:P@I:" + uuidToHex() + "}"; entry->m_attributes->set( - EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey)); + EntryAttributes::PasswordKey, + buildReference(uuid(), EntryAttributes::PasswordKey), + m_attributes->isProtected(EntryAttributes::PasswordKey)); } entry->m_autoTypeAssociations->copyDataFrom(m_autoTypeAssociations); diff --git a/src/core/Entry.h b/src/core/Entry.h index 123854dc..c5f59f2e 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -105,12 +105,16 @@ public: QString username() const; QString password() const; QString notes() const; + QString attribute(const QString& key) const; QString totp() const; QSharedPointer totpSettings() const; bool hasTotp() const; bool isExpired() const; + bool isAttributeReferenceOf(const QString& key, const QUuid& uuid) const; + void replaceReferencesWithValues(const Entry* other); bool hasReferences() const; + bool hasReferencesTo(const QUuid& uuid) const; EntryAttributes* attributes(); const EntryAttributes* attributes() const; EntryAttachments* attachments(); @@ -139,6 +143,7 @@ public: void setUsername(const QString& username); void setPassword(const QString& password); void setNotes(const QString& notes); + void setDefaultAttribute(const QString& attribute, const QString& value); void setExpires(const bool& value); void setExpiryTime(const QDateTime& dateTime); void setTotp(QSharedPointer settings); @@ -238,6 +243,7 @@ private: QString resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const; QString referenceFieldValue(EntryReferenceType referenceType) const; + static QString buildReference(const QUuid& uuid, const QString& field); static EntryReferenceType referenceType(const QString& referenceStr); template bool set(T& property, const T& value); diff --git a/src/core/Group.cpp b/src/core/Group.cpp index ec7633b8..6c4adbad 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -23,6 +23,9 @@ #include "core/DatabaseIcons.h" #include "core/Global.h" #include "core/Metadata.h" +#include "core/Tools.h" + +#include const int Group::DefaultIconNumber = 48; const int Group::RecycleBinIconNumber = 43; @@ -119,7 +122,7 @@ const QUuid& Group::uuid() const const QString Group::uuidToHex() const { - return QString::fromLatin1(m_uuid.toRfc4122().toHex()); + return Tools::uuidToHex(m_uuid); } QString Group::name() const @@ -548,6 +551,12 @@ QList Group::entriesRecursive(bool includeHistoryItems) const return entryList; } +QList Group::referencesRecursive(const Entry* entry) const +{ + auto entries = entriesRecursive(); + return QtConcurrent::blockingFiltered(entries, [entry](const Entry* e) { return e->hasReferencesTo(entry->uuid()); }); +} + Entry* Group::findEntryByUuid(const QUuid& uuid) const { if (uuid.isNull()) { diff --git a/src/core/Group.h b/src/core/Group.h index e3ef9a59..d0074d5d 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -151,6 +151,7 @@ public: QList entries(); const QList& entries() const; Entry* findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group = nullptr); + QList referencesRecursive(const Entry* entry) const; QList entriesRecursive(bool includeHistoryItems = false) const; QList groupsRecursive(bool includeSelf) const; QList groupsRecursive(bool includeSelf); diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index c80a15f7..7fc2a346 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #ifdef Q_OS_WIN @@ -38,23 +39,6 @@ #include // for nanosleep() #endif -#include "config-keepassx.h" - -#if defined(HAVE_RLIMIT_CORE) -#include -#endif - -#if defined(HAVE_PR_SET_DUMPABLE) -#include -#endif - -#ifdef HAVE_PT_DENY_ATTACH -// clang-format off -#include -#include -// clang-format on -#endif - namespace Tools { QString humanReadableFileSize(qint64 bytes, quint32 precision) @@ -197,34 +181,37 @@ namespace Tools } } -// Escape common regex symbols except for *, ?, and | -auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re"); + // Escape common regex symbols except for *, ?, and | + auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re"); -QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive) -{ - QString pattern = string; + QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive) + { + QString pattern = string; - // Wildcard support (*, ?, |) - if (useWildcards) { - pattern.replace(regexEscape, "\\\\1"); - pattern.replace("*", ".*"); - pattern.replace("?", "."); + // Wildcard support (*, ?, |) + if (useWildcards) { + pattern.replace(regexEscape, "\\\\1"); + pattern.replace("*", ".*"); + pattern.replace("?", "."); + } + + // Exact modifier + if (exactMatch) { + pattern = "^" + pattern + "$"; + } + + auto regex = QRegularExpression(pattern); + if (!caseSensitive) { + regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + + return regex; } - // Exact modifier - if (exactMatch) { - pattern = "^" + pattern + "$"; + QString uuidToHex(const QUuid& uuid) { + return QString::fromLatin1(uuid.toRfc4122().toHex()); } - auto regex = QRegularExpression(pattern); - if (!caseSensitive) { - regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); - } - - return regex; -} - - Buffer::Buffer() : raw(nullptr) , size(0) @@ -250,5 +237,4 @@ QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool return QByteArray(reinterpret_cast(raw), size ); } - } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index a3e74dc9..f6d0daaa 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -23,6 +23,7 @@ #include #include +#include #include @@ -39,7 +40,8 @@ namespace Tools bool isBase64(const QByteArray& ba); void sleep(int ms); void wait(int ms); - QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, + QString uuidToHex(const QUuid& uuid); + QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, bool exactMatch = false, bool caseSensitive = false); template diff --git a/src/core/Translator.cpp b/src/core/Translator.cpp index 595dadfa..f64a7a44 100644 --- a/src/core/Translator.cpp +++ b/src/core/Translator.cpp @@ -47,7 +47,8 @@ void Translator::installTranslators() #ifdef QT_DEBUG QString("%1/share/translations").arg(KEEPASSX_BINARY_DIR), #endif - filePath()->dataPath("translations")}; + filePath()->dataPath("translations") + }; bool translationsLoaded = false; for (const QString& path : paths) { diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp index 1b8b018e..ff7d46f2 100644 --- a/src/gui/DatabaseOpenDialog.cpp +++ b/src/gui/DatabaseOpenDialog.cpp @@ -25,7 +25,11 @@ DatabaseOpenDialog::DatabaseOpenDialog(QWidget* parent) , m_view(new DatabaseOpenWidget(this)) { setWindowTitle(tr("Unlock Database - KeePassXC")); +#ifdef Q_OS_MACOS + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); +#else setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::ForeignWindow); +#endif connect(m_view, SIGNAL(dialogFinished(bool)), this, SLOT(complete(bool))); } diff --git a/src/gui/DatabaseOpenDialog.h b/src/gui/DatabaseOpenDialog.h index 236d2e0c..f2860fcc 100644 --- a/src/gui/DatabaseOpenDialog.h +++ b/src/gui/DatabaseOpenDialog.h @@ -15,8 +15,8 @@ * along with this program. If not, see . */ -#ifndef KEEPASSX_AUTOTYPEUNLOCKDIALOG_H -#define KEEPASSX_AUTOTYPEUNLOCKDIALOG_H +#ifndef KEEPASSX_UNLOCKDATABASEDIALOG_H +#define KEEPASSX_UNLOCKDATABASEDIALOG_H #include "core/Global.h" @@ -37,7 +37,8 @@ public: { None, AutoType, - Merge + Merge, + Browser }; explicit DatabaseOpenDialog(QWidget* parent = nullptr); @@ -61,4 +62,4 @@ private: Intent m_intent = Intent::None; }; -#endif // KEEPASSX_AUTOTYPEUNLOCKDIALOG_H +#endif // KEEPASSX_UNLOCKDATABASEDIALOG_H diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index b45c5a8a..7e41035e 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -39,6 +39,9 @@ #include "gui/DatabaseOpenDialog.h" #include "gui/entry/EntryView.h" #include "gui/group/GroupView.h" +#ifdef Q_OS_MACOS +#include "gui/macutils/MacUtils.h" +#endif #include "gui/wizard/NewDatabaseWizard.h" DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) @@ -98,8 +101,8 @@ QSharedPointer DatabaseTabWidget::execNewDatabaseWizard() tr("Database creation error"), tr("The created database has no key or KDF, refusing to save it.\n" "This is definitely a bug, please report it to the developers."), - QMessageBox::Ok, - QMessageBox::Ok); + MessageBox::Ok, + MessageBox::Ok); return {}; } @@ -544,8 +547,8 @@ void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget, Databas m_databaseOpenDialog->setFilePath(filePath); #ifdef Q_OS_MACOS - if (intent == DatabaseOpenDialog::Intent::AutoType) { - autoType()->raiseWindow(); + if (intent == DatabaseOpenDialog::Intent::AutoType || intent == DatabaseOpenDialog::Intent::Browser) { + macUtils()->raiseOwnWindow(); Tools::wait(500); } #endif diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index f93588a6..dd56b923 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -131,7 +131,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) m_searchingLabel->setStyleSheet("color: rgb(0, 0, 0);" "background-color: rgb(255, 253, 160);" "border: 2px solid rgb(190, 190, 190);" - "border-radius: 2px;"); + "border-radius: 4px;"); m_searchingLabel->setVisible(false); m_previewView->hide(); @@ -425,61 +425,117 @@ void DatabaseWidget::setupTotp() setupTotpDialog->open(); } -void DatabaseWidget::deleteEntries() +void DatabaseWidget::deleteSelectedEntries() { const QModelIndexList selected = m_entryView->selectionModel()->selectedRows(); - - Q_ASSERT(!selected.isEmpty()); if (selected.isEmpty()) { return; } - // get all entry pointers as the indexes change when removing multiple entries + // Resolve entries from the selection model QList selectedEntries; for (const QModelIndex& index : selected) { selectedEntries.append(m_entryView->entryFromIndex(index)); } + // Confirm entry removal before moving forward auto* recycleBin = m_db->metadata()->recycleBin(); - bool inRecycleBin = recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()); - if (inRecycleBin || !m_db->metadata()->recycleBinEnabled()) { - QString prompt; - if (selected.size() == 1) { - prompt = tr("Do you really want to delete the entry \"%1\" for good?") - .arg(selectedEntries.first()->title().toHtmlEscaped()); - } else { - prompt = tr("Do you really want to delete %n entry(s) for good?", "", selected.size()); + bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid())) + || !m_db->metadata()->recycleBinEnabled(); + + if (!confirmDeleteEntries(selectedEntries, permanent)) { + return; + } + + // Find references to selected entries and prompt for direction if necessary + auto it = selectedEntries.begin(); + while (it != selectedEntries.end()) { + auto references = m_db->rootGroup()->referencesRecursive(*it); + if (!references.isEmpty()) { + // Ignore references that are selected for deletion + for (auto* entry : selectedEntries) { + references.removeAll(entry); + } + + if (!references.isEmpty()) { + // Prompt for reference handling + auto result = MessageBox::question( + this, + tr("Replace references to entry?"), + tr("Entry \"%1\" has %2 reference(s). " + "Do you want to overwrite references with values, skip this entry, or delete anyway?", "", + references.size()) + .arg((*it)->title().toHtmlEscaped()) + .arg(references.size()), + MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete, + MessageBox::Overwrite); + + if (result == MessageBox::Overwrite) { + for (auto* entry : references) { + entry->replaceReferencesWithValues(*it); + } + } else if (result == MessageBox::Skip) { + it = selectedEntries.erase(it); + continue; + } + } } - QMessageBox::StandardButton result = MessageBox::question( - this, tr("Delete entry(s)?", "", selected.size()), prompt, QMessageBox::Yes | QMessageBox::No); + it++; + } - if (result == QMessageBox::Yes) { - for (Entry* entry : asConst(selectedEntries)) { - delete entry; - } - refreshSearch(); + if (permanent) { + for (auto* entry : asConst(selectedEntries)) { + delete entry; } } else { - QString prompt; - if (selected.size() == 1) { - prompt = tr("Do you really want to move entry \"%1\" to the recycle bin?") - .arg(selectedEntries.first()->title().toHtmlEscaped()); - } else { - prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", selected.size()); - } - - QMessageBox::StandardButton result = MessageBox::question( - this, tr("Move entry(s) to recycle bin?", "", selected.size()), prompt, QMessageBox::Yes | QMessageBox::No); - - if (result == QMessageBox::No) { - return; - } - - for (Entry* entry : asConst(selectedEntries)) { + for (auto* entry : asConst(selectedEntries)) { m_db->recycleEntry(entry); } } + + refreshSearch(); +} + +bool DatabaseWidget::confirmDeleteEntries(QList entries, bool permanent) +{ + if (entries.isEmpty()) { + return false; + } + + if (permanent) { + QString prompt; + if (entries.size() == 1) { + prompt = tr("Do you really want to delete the entry \"%1\" for good?") + .arg(entries.first()->title().toHtmlEscaped()); + } else { + prompt = tr("Do you really want to delete %n entry(s) for good?", "", entries.size()); + } + + auto answer = MessageBox::question(this, + tr("Delete entry(s)?", "", entries.size()), + prompt, + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + return answer == MessageBox::Delete; + } else { + QString prompt; + if (entries.size() == 1) { + prompt = tr("Do you really want to move entry \"%1\" to the recycle bin?") + .arg(entries.first()->title().toHtmlEscaped()); + } else { + prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size()); + } + + auto answer = MessageBox::question(this, + tr("Move entry(s) to recycle bin?", "", entries.size()), + prompt, + MessageBox::Move | MessageBox::Cancel, + MessageBox::Cancel); + + return answer == MessageBox::Move; + } } void DatabaseWidget::setFocus() @@ -649,16 +705,27 @@ void DatabaseWidget::deleteGroup() bool isRecycleBin = recycleBin && (currentGroup == recycleBin); bool isRecycleBinSubgroup = recycleBin && currentGroup->findGroupByUuid(recycleBin->uuid()); if (inRecycleBin || isRecycleBin || isRecycleBinSubgroup || !m_db->metadata()->recycleBinEnabled()) { - QMessageBox::StandardButton result = MessageBox::question( - this, - tr("Delete group?"), - tr("Do you really want to delete the group \"%1\" for good?").arg(currentGroup->name().toHtmlEscaped()), - QMessageBox::Yes | QMessageBox::No); - if (result == QMessageBox::Yes) { + auto result = MessageBox::question(this, + tr("Delete group"), + tr("Do you really want to delete the group \"%1\" for good?") + .arg(currentGroup->name().toHtmlEscaped()), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + if (result == MessageBox::Delete) { delete currentGroup; } } else { - m_db->recycleGroup(currentGroup); + auto result = MessageBox::question(this, + tr("Move group to recycle bin?"), + tr("Do you really want to move the group " + "\"%1\" to the recycle bin?") + .arg(currentGroup->name().toHtmlEscaped()), + MessageBox::Move | MessageBox::Cancel, + MessageBox::Cancel); + if (result == MessageBox::Move) { + m_db->recycleGroup(currentGroup); + } } } @@ -1126,8 +1193,8 @@ bool DatabaseWidget::lock() if (currentMode() == DatabaseWidget::Mode::EditMode) { auto result = MessageBox::question(this, tr("Lock Database?"), tr("You are editing an entry. Discard changes and lock anyway?"), - QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); - if (result == QMessageBox::Cancel) { + MessageBox::Discard | MessageBox::Cancel, MessageBox::Cancel); + if (result == MessageBox::Cancel) { return false; } } @@ -1145,10 +1212,10 @@ bool DatabaseWidget::lock() msg = tr("Database was modified.\nSave changes?"); } auto result = MessageBox::question(this, tr("Save changes?"), msg, - QMessageBox::Yes | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Yes); - if (result == QMessageBox::Yes && !m_db->save(nullptr, false, false)) { + MessageBox::Save | MessageBox::Discard | MessageBox::Cancel, MessageBox::Save); + if (result == MessageBox::Save && !m_db->save(nullptr, false, false)) { return false; - } else if (result == QMessageBox::Cancel) { + } else if (result == MessageBox::Cancel) { return false; } } @@ -1202,9 +1269,9 @@ void DatabaseWidget::reloadDatabaseFile() auto result = MessageBox::question(this, tr("File has changed"), tr("The database file has changed. Do you want to load the changes?"), - QMessageBox::Yes | QMessageBox::No); + MessageBox::Yes | MessageBox::No); - if (result == QMessageBox::No) { + if (result == MessageBox::No) { // Notify everyone the database does not match the file m_db->markAsModified(); // Rewatch the database file @@ -1221,9 +1288,10 @@ void DatabaseWidget::reloadDatabaseFile() auto result = MessageBox::question(this, tr("Merge Request"), tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"), - QMessageBox::Yes | QMessageBox::No); + MessageBox::Merge | MessageBox::Cancel, + MessageBox::Merge); - if (result == QMessageBox::Yes) { + if (result == MessageBox::Merge) { // Merge the old database into the new one Merger merger(m_db.data(), db.data()); merger.merge(); @@ -1409,14 +1477,14 @@ bool DatabaseWidget::save(int attempt) if (attempt > 2 && useAtomicSaves) { // Saving failed 3 times, issue a warning and attempt to resolve - auto choice = MessageBox::question(this, + auto result = MessageBox::question(this, tr("Disable safe saves?"), tr("KeePassXC has failed to save the database multiple times. " "This is likely caused by file sync services holding a lock on " "the save file.\nDisable safe saves and try again?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes); - if (choice == QMessageBox::Yes) { + MessageBox::Disable | MessageBox::Cancel, + MessageBox::Disable); + if (result == MessageBox::Disable) { config()->set("UseAtomicSaves", false); return save(attempt + 1); } @@ -1491,13 +1559,13 @@ void DatabaseWidget::emptyRecycleBin() return; } - QMessageBox::StandardButton result = - MessageBox::question(this, - tr("Empty recycle bin?"), - tr("Are you sure you want to permanently delete everything from your recycle bin?"), - QMessageBox::Yes | QMessageBox::No); + auto result = MessageBox::question(this, + tr("Empty recycle bin?"), + tr("Are you sure you want to permanently delete everything from your recycle bin?"), + MessageBox::Empty | MessageBox::Cancel, + MessageBox::Cancel); - if (result == QMessageBox::Yes) { + if (result == MessageBox::Empty) { m_db->emptyRecycleBin(); refreshSearch(); } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index cda14912..71297610 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -149,7 +149,7 @@ public slots: void replaceDatabase(QSharedPointer db); void createEntry(); void cloneEntry(); - void deleteEntries(); + void deleteSelectedEntries(); void setFocus(); void copyTitle(); void copyUsername(); @@ -223,6 +223,7 @@ private: void setClipboardTextAndMinimize(const QString& text); void setIconFromParent(); void processAutoOpen(); + bool confirmDeleteEntries(QList entries, bool permanent); QSharedPointer m_db; diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index abb65c00..6ac29825 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -416,16 +416,18 @@ void EditWidgetIcons::removeCustomIcon() int iconUseCount = entriesWithSameIcon.size() + groupsWithSameIcon.size(); if (iconUseCount > 0) { - QMessageBox::StandardButton ans = - MessageBox::question(this, - tr("Confirm Delete"), - tr("This icon is used by %n entry(s), and will be replaced " - "by the default icon. Are you sure you want to delete it?", - "", - iconUseCount), - QMessageBox::Yes | QMessageBox::No); - if (ans == QMessageBox::No) { + + auto result = MessageBox::question(this, + tr("Confirm Delete"), + tr("This icon is used by %n entry(s), and will be replaced " + "by the default icon. Are you sure you want to delete it?", + "", + iconUseCount), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + if (result == MessageBox::Cancel) { // Early out, nothing is changed return; } else { diff --git a/src/gui/EditWidgetProperties.cpp b/src/gui/EditWidgetProperties.cpp index 0fb0575e..bd4d00b9 100644 --- a/src/gui/EditWidgetProperties.cpp +++ b/src/gui/EditWidgetProperties.cpp @@ -71,13 +71,14 @@ void EditWidgetProperties::setCustomData(CustomData* customData) void EditWidgetProperties::removeSelectedPluginData() { - if (QMessageBox::Yes - != MessageBox::question(this, - tr("Delete plugin data?"), - tr("Do you really want to delete the selected plugin data?\n" - "This may cause the affected plugins to malfunction."), - QMessageBox::Yes | QMessageBox::Cancel, - QMessageBox::Cancel)) { + auto result = MessageBox::question(this, + tr("Delete plugin data?"), + tr("Do you really want to delete the selected plugin data?\n" + "This may cause the affected plugins to malfunction."), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + if (result == MessageBox::Cancel) { return; } diff --git a/src/gui/EditWidgetProperties.ui b/src/gui/EditWidgetProperties.ui index f9756266..fb8ed503 100644 --- a/src/gui/EditWidgetProperties.ui +++ b/src/gui/EditWidgetProperties.ui @@ -28,6 +28,9 @@ + + QFormLayout::ExpandingFieldsGrow + diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 70df886c..c7d31796 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -321,7 +321,7 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry())); m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); - m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteEntries())); + m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); diff --git a/src/gui/MessageBox.cpp b/src/gui/MessageBox.cpp index 7aba6a2a..e3edd2db 100644 --- a/src/gui/MessageBox.cpp +++ b/src/gui/MessageBox.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2013 Felix Geyer + * Copyright (C) 2018 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 @@ -17,69 +18,149 @@ #include "MessageBox.h" -QMessageBox::StandardButton MessageBox::m_nextAnswer(QMessageBox::NoButton); +MessageBox::Button MessageBox::m_nextAnswer(MessageBox::NoButton); -QMessageBox::StandardButton MessageBox::critical(QWidget* parent, - const QString& title, - const QString& text, - QMessageBox::StandardButtons buttons, - QMessageBox::StandardButton defaultButton) +QMap +MessageBox::m_addedButtonLookup = + QMap(); + +QMap> +MessageBox::m_buttonDefs = + QMap>(); + +void MessageBox::initializeButtonDefs() { - if (m_nextAnswer == QMessageBox::NoButton) { - return QMessageBox::critical(parent, title, text, buttons, defaultButton); + m_buttonDefs = + QMap> + { + // Reimplementation of Qt StandardButtons + {Ok, {stdButtonText(QMessageBox::Ok), QMessageBox::ButtonRole::AcceptRole}}, + {Open, {stdButtonText(QMessageBox::Open), QMessageBox::ButtonRole::AcceptRole}}, + {Save, {stdButtonText(QMessageBox::Save), QMessageBox::ButtonRole::AcceptRole}}, + {Cancel, {stdButtonText(QMessageBox::Cancel), QMessageBox::ButtonRole::RejectRole}}, + {Close, {stdButtonText(QMessageBox::Close), QMessageBox::ButtonRole::RejectRole}}, + {Discard, {stdButtonText(QMessageBox::Discard), QMessageBox::ButtonRole::DestructiveRole}}, + {Apply, {stdButtonText(QMessageBox::Apply), QMessageBox::ButtonRole::ApplyRole}}, + {Reset, {stdButtonText(QMessageBox::Reset), QMessageBox::ButtonRole::ResetRole}}, + {RestoreDefaults, {stdButtonText(QMessageBox::RestoreDefaults), QMessageBox::ButtonRole::ResetRole}}, + {Help, {stdButtonText(QMessageBox::Help), QMessageBox::ButtonRole::HelpRole}}, + {SaveAll, {stdButtonText(QMessageBox::SaveAll), QMessageBox::ButtonRole::AcceptRole}}, + {Yes, {stdButtonText(QMessageBox::Yes), QMessageBox::ButtonRole::YesRole}}, + {YesToAll, {stdButtonText(QMessageBox::YesToAll), QMessageBox::ButtonRole::YesRole}}, + {No, {stdButtonText(QMessageBox::No), QMessageBox::ButtonRole::NoRole}}, + {NoToAll, {stdButtonText(QMessageBox::NoToAll), QMessageBox::ButtonRole::NoRole}}, + {Abort, {stdButtonText(QMessageBox::Abort), QMessageBox::ButtonRole::RejectRole}}, + {Retry, {stdButtonText(QMessageBox::Retry), QMessageBox::ButtonRole::AcceptRole}}, + {Ignore, {stdButtonText(QMessageBox::Ignore), QMessageBox::ButtonRole::AcceptRole}}, + + // KeePassXC Buttons + {Overwrite, {QMessageBox::tr("Overwrite"), QMessageBox::ButtonRole::AcceptRole}}, + {Delete, {QMessageBox::tr("Delete"), QMessageBox::ButtonRole::AcceptRole}}, + {Move, {QMessageBox::tr("Move"), QMessageBox::ButtonRole::AcceptRole}}, + {Empty, {QMessageBox::tr("Empty"), QMessageBox::ButtonRole::AcceptRole}}, + {Remove, {QMessageBox::tr("Remove"), QMessageBox::ButtonRole::AcceptRole}}, + {Skip, {QMessageBox::tr("Skip"), QMessageBox::ButtonRole::AcceptRole}}, + {Disable, {QMessageBox::tr("Disable"), QMessageBox::ButtonRole::AcceptRole}}, + {Merge, {QMessageBox::tr("Merge"), QMessageBox::ButtonRole::AcceptRole}}, + }; +} + +QString MessageBox::stdButtonText(QMessageBox::StandardButton button) +{ + QMessageBox buttonHost; + return buttonHost.addButton(button)->text(); +} + +MessageBox::Button MessageBox::messageBox(QWidget* parent, + QMessageBox::Icon icon, + const QString& title, + const QString& text, + MessageBox::Buttons buttons, + MessageBox::Button defaultButton, + MessageBox::Action action) +{ + if (m_nextAnswer == MessageBox::NoButton) { + QMessageBox msgBox(parent); + msgBox.setIcon(icon); + msgBox.setWindowTitle(title); + msgBox.setText(text); + + for (uint64_t b = First; b <= Last; b <<= 1) { + if (b & buttons) { + QString text = m_buttonDefs[static_cast