From b82ea87d82b77b0b606a8c2749ab54f784b9b8d0 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 29 Sep 2018 19:00:47 +0200 Subject: [PATCH] Add CLI tests and improve coding style and i18n The CLI module was lacking unit test coverage and showed some severe coding style violations, which this patch addresses. In addition, all uses of qCritical() with untranslatble raw char* sequences were removed in favor of proper locale strings. These are written to STDERR through QTextStreams and support output redirection for testing purposes. With this change, error messages don't depend on the global Qt logging settings and targets anymore and go directly to the terminal or into a file if needed. This patch also fixes a bug discovered during unit test development, where the extract command would just dump the raw XML contents without decrypting embedded Salsa20-protected values first, making the XML export mostly useless, since passwords are scrambled. Lastly, all CLI commands received a dedicated -h/--help option. --- CMakeLists.txt | 6 +- Dockerfile | 6 +- ci/trusty/Dockerfile | 2 + src/CMakeLists.txt | 1 + src/browser/BrowserAction.cpp | 1 - src/browser/BrowserSettings.cpp | 5 - src/browser/BrowserSettings.h | 1 - src/cli/Add.cpp | 46 +- src/cli/Clip.cpp | 37 +- src/cli/Command.cpp | 11 +- src/cli/Command.h | 2 +- src/cli/Diceware.cpp | 22 +- src/cli/Edit.cpp | 52 +- src/cli/Estimate.cpp | 84 +- src/cli/Extract.cpp | 40 +- src/cli/Generate.cpp | 39 +- src/cli/List.cpp | 39 +- src/cli/List.h | 2 +- src/cli/Locate.cpp | 29 +- src/cli/Locate.h | 2 +- src/cli/Merge.cpp | 38 +- src/cli/Remove.cpp | 33 +- src/cli/Remove.h | 2 +- src/cli/Show.cpp | 41 +- src/cli/Show.h | 2 +- src/cli/Utils.cpp | 77 +- src/cli/Utils.h | 15 +- src/cli/keepassxc-cli.cpp | 14 +- src/core/Bootstrap.cpp | 236 ++++++ src/core/Bootstrap.h | 34 + src/core/Database.cpp | 24 +- src/core/Database.h | 3 +- src/core/PasswordGenerator.cpp | 36 +- src/core/PasswordGenerator.h | 1 - src/core/Tools.cpp | 368 +++------ src/core/Tools.h | 42 +- src/crypto/CryptoHash.cpp | 7 - src/crypto/CryptoHash.h | 1 - src/crypto/SymmetricCipher.cpp | 8 +- src/crypto/SymmetricCipherGcrypt.cpp | 2 - src/format/Kdbx3Reader.cpp | 8 - src/format/Kdbx4Reader.cpp | 8 - src/format/KdbxReader.cpp | 31 +- src/format/KdbxReader.h | 2 + src/format/KdbxXmlReader.cpp | 1 - src/format/KdbxXmlWriter.cpp | 31 +- src/format/KdbxXmlWriter.h | 4 + src/format/KeePass2.cpp | 5 +- src/format/KeePass2.h | 3 +- src/format/KeePass2Reader.cpp | 2 +- src/gui/Application.h | 2 +- src/gui/DatabaseWidget.h | 2 +- .../DatabaseSettingsWidgetEncryption.cpp | 2 +- src/main.cpp | 87 +- tests/CMakeLists.txt | 12 +- tests/TestCli.cpp | 756 ++++++++++++++++++ tests/TestCli.h | 69 ++ tests/TestKdbx4.cpp | 12 +- tests/TestPasswordGenerator.cpp | 131 +++ tests/TestPasswordGenerator.h | 33 + tests/TestSymmetricCipher.cpp | 337 ++++---- tests/TestSymmetricCipher.h | 13 +- tests/gui/CMakeLists.txt | 2 +- tests/gui/TemporaryFile.cpp | 93 --- tests/gui/TemporaryFile.h | 65 -- tests/gui/TestGui.cpp | 358 +++++---- tests/gui/TestGui.h | 9 +- 67 files changed, 2250 insertions(+), 1239 deletions(-) create mode 100644 src/core/Bootstrap.cpp create mode 100644 src/core/Bootstrap.h create mode 100644 tests/TestCli.cpp create mode 100644 tests/TestCli.h create mode 100644 tests/TestPasswordGenerator.cpp create mode 100644 tests/TestPasswordGenerator.h delete mode 100644 tests/gui/TemporaryFile.cpp delete mode 100644 tests/gui/TemporaryFile.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fa25c96175..2520063f98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -303,16 +303,16 @@ endif() include(CLangFormat) if(UNIX AND NOT APPLE) - find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools DBus REQUIRED) + find_package(Qt5 COMPONENTS Core Network Concurrent Gui Widgets Test LinguistTools DBus REQUIRED) elseif(APPLE) - find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools REQUIRED + find_package(Qt5 COMPONENTS Core Network Concurrent Gui Widgets Test LinguistTools REQUIRED HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH ) find_package(Qt5 COMPONENTS MacExtras HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH ) else() - find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools REQUIRED) + find_package(Qt5 COMPONENTS Core Network Concurrent Gui Widgets Test LinguistTools REQUIRED) endif() if(Qt5Core_VERSION VERSION_LESS "5.2.0") diff --git a/Dockerfile b/Dockerfile index 89ee044647..f770547a1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ FROM ubuntu:14.04 -ENV REBUILD_COUNTER=8 +ENV REBUILD_COUNTER=9 ENV QT5_VERSION=qt510 ENV QT5_PPA_VERSION=qt-5.10.1 @@ -55,7 +55,9 @@ RUN set -x \ libxtst-dev \ mesa-common-dev \ libyubikey-dev \ - libykpers-1-dev + libykpers-1-dev \ + xclip \ + xvfb ENV PATH="/opt/${QT5_VERSION}/bin:${PATH}" ENV CMAKE_PREFIX_PATH="/opt/${QT5_VERSION}/lib/cmake" diff --git a/ci/trusty/Dockerfile b/ci/trusty/Dockerfile index 04aee25a53..a9b9276d1d 100644 --- a/ci/trusty/Dockerfile +++ b/ci/trusty/Dockerfile @@ -39,6 +39,7 @@ RUN set -x \ clang-3.6 \ libclang-common-3.6-dev \ clang-format-3.6 \ + llvm-3.6 \ cmake3 \ make \ libgcrypt20-18-dev \ @@ -54,6 +55,7 @@ RUN set -x \ libykpers-1-dev \ libxi-dev \ libxtst-dev \ + xclip \ xvfb ENV PATH="/opt/${QT5_VERSION}/bin:${PATH}" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3621067e81..4df7245743 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,7 @@ set(keepassx_SOURCES core/EntryAttributes.cpp core/EntrySearcher.cpp core/FilePath.cpp + core/Bootstrap.cpp core/Global.h core/Group.cpp core/InactivityTimer.cpp diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index 60cf8506a8..f9e1845023 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -271,7 +271,6 @@ QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const { const QString nonce = json.value("nonce").toString(); const QString password = browserSettings()->generatePassword(); - const QString bits = QString::number(browserSettings()->getbits()); // For some reason this always returns 1140 bits? if (nonce.isEmpty() || password.isEmpty()) { return QJsonObject(); diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index f7abdece98..96d8f98d7b 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -485,11 +485,6 @@ QString BrowserSettings::generatePassword() } } -int BrowserSettings::getbits() -{ - return m_passwordGenerator.getbits(); -} - void BrowserSettings::updateBinaryPaths(QString customProxyLocation) { bool isProxy = supportBrowserProxy(); diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h index 3e84ba37dd..5dc28593a1 100644 --- a/src/browser/BrowserSettings.h +++ b/src/browser/BrowserSettings.h @@ -112,7 +112,6 @@ class BrowserSettings PasswordGenerator::CharClasses passwordCharClasses(); PasswordGenerator::GeneratorFlags passwordGeneratorFlags(); QString generatePassword(); - int getbits(); void updateBinaryPaths(QString customProxyLocation = QString()); private: diff --git a/src/cli/Add.cpp b/src/cli/Add.cpp index 5c97299b82..81a5cad135 100644 --- a/src/cli/Add.cpp +++ b/src/cli/Add.cpp @@ -41,22 +41,20 @@ Add::~Add() int Add::execute(const QStringList& arguments) { - - QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream inputTextStream(Utils::STDIN, QIODevice::ReadOnly); + QTextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); - QCommandLineOption username(QStringList() << "u" - << "username", + QCommandLineOption username(QStringList() << "u" << "username", QObject::tr("Username for the entry."), QObject::tr("username")); parser.addOption(username); @@ -64,23 +62,22 @@ int Add::execute(const QStringList& arguments) QCommandLineOption url(QStringList() << "url", QObject::tr("URL for the entry."), QObject::tr("URL")); parser.addOption(url); - QCommandLineOption prompt(QStringList() << "p" - << "password-prompt", + QCommandLineOption prompt(QStringList() << "p" << "password-prompt", QObject::tr("Prompt for the entry's password.")); parser.addOption(prompt); - QCommandLineOption generate(QStringList() << "g" - << "generate", + QCommandLineOption generate(QStringList() << "g" << "generate", QObject::tr("Generate a password for the entry.")); parser.addOption(generate); - QCommandLineOption length(QStringList() << "l" - << "password-length", + QCommandLineOption length(QStringList() << "l" << "password-length", QObject::tr("Length for the generated password."), QObject::tr("length")); parser.addOption(length); parser.addPositionalArgument("entry", QObject::tr("Path of the entry to add.")); + + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -89,11 +86,11 @@ int Add::execute(const QStringList& arguments) return EXIT_FAILURE; } - QString databasePath = args.at(0); - QString entryPath = args.at(1); + const QString& databasePath = args.at(0); + const QString& entryPath = args.at(1); - Database* db = Database::unlockFromStdin(databasePath, parser.value(keyFile)); - if (db == nullptr) { + Database* db = Database::unlockFromStdin(databasePath, parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + if (!db) { return EXIT_FAILURE; } @@ -101,13 +98,13 @@ int Add::execute(const QStringList& arguments) // the entry. QString passwordLength = parser.value(length); if (!passwordLength.isEmpty() && !passwordLength.toInt()) { - qCritical("Invalid value for password length %s.", qPrintable(passwordLength)); + errorTextStream << QObject::tr("Invalid value for password length %1.").arg(passwordLength) << endl; return EXIT_FAILURE; } Entry* entry = db->rootGroup()->addEntryWithPath(entryPath); if (!entry) { - qCritical("Could not create entry with path %s.", qPrintable(entryPath)); + errorTextStream << QObject::tr("Could not create entry with path %1.").arg(entryPath) << endl; return EXIT_FAILURE; } @@ -120,8 +117,7 @@ int Add::execute(const QStringList& arguments) } if (parser.isSet(prompt)) { - outputTextStream << "Enter password for new entry: "; - outputTextStream.flush(); + outputTextStream << QObject::tr("Enter password for new entry: ") << flush; QString password = Utils::getPassword(); entry->setPassword(password); } else if (parser.isSet(generate)) { @@ -130,7 +126,7 @@ int Add::execute(const QStringList& arguments) if (passwordLength.isEmpty()) { passwordGenerator.setLength(PasswordGenerator::DefaultLength); } else { - passwordGenerator.setLength(passwordLength.toInt()); + passwordGenerator.setLength(static_cast(passwordLength.toInt())); } passwordGenerator.setCharClasses(PasswordGenerator::DefaultCharset); @@ -141,10 +137,10 @@ int Add::execute(const QStringList& arguments) QString errorMessage = db->saveToFile(databasePath); if (!errorMessage.isEmpty()) { - qCritical("Writing the database failed %s.", qPrintable(errorMessage)); + errorTextStream << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl; return EXIT_FAILURE; } - outputTextStream << "Successfully added entry " << entry->title() << "." << endl; + outputTextStream << QObject::tr("Successfully added entry %1.").arg(entry->title()) << endl; return EXIT_SUCCESS; } diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 886f8ecc71..0b78a24b40 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -42,20 +42,19 @@ Clip::~Clip() int Clip::execute(const QStringList& arguments) { - - QTextStream out(stdout); + QTextStream out(Utils::STDOUT); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); parser.addPositionalArgument("entry", QObject::tr("Path of the entry to clip.", "clip = copy to clipboard")); - parser.addPositionalArgument( - "timeout", QObject::tr("Timeout in seconds before clearing the clipboard."), QString("[timeout]")); + parser.addPositionalArgument("timeout", + QObject::tr("Timeout in seconds before clearing the clipboard."), "[timeout]"); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -64,29 +63,30 @@ int Clip::execute(const QStringList& arguments) return EXIT_FAILURE; } - Database* db = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); + Database* db = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); if (!db) { return EXIT_FAILURE; } - return this->clipEntry(db, args.at(1), args.value(2)); + return clipEntry(db, args.at(1), args.value(2)); } int Clip::clipEntry(Database* database, QString entryPath, QString timeout) { + QTextStream err(Utils::STDERR); int timeoutSeconds = 0; if (!timeout.isEmpty() && !timeout.toInt()) { - qCritical("Invalid timeout value %s.", qPrintable(timeout)); + err << QObject::tr("Invalid timeout value %1.").arg(timeout) << endl; return EXIT_FAILURE; } else if (!timeout.isEmpty()) { timeoutSeconds = timeout.toInt(); } - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); Entry* entry = database->rootGroup()->findEntry(entryPath); if (!entry) { - qCritical("Entry %s not found.", qPrintable(entryPath)); + err << QObject::tr("Entry %1 not found.").arg(entryPath) << endl; return EXIT_FAILURE; } @@ -95,20 +95,23 @@ int Clip::clipEntry(Database* database, QString entryPath, QString timeout) return exitCode; } - outputTextStream << "Entry's password copied to the clipboard!" << endl; + outputTextStream << QObject::tr("Entry's password copied to the clipboard!") << endl; if (!timeoutSeconds) { return exitCode; } + QString lastLine = ""; while (timeoutSeconds > 0) { - outputTextStream << "\rClearing the clipboard in " << timeoutSeconds << " seconds..."; - outputTextStream.flush(); + outputTextStream << '\r' << QString(lastLine.size(), ' ') << '\r'; + lastLine = QObject::tr("Clearing the clipboard in %1 second(s)...", "", timeoutSeconds).arg(timeoutSeconds); + outputTextStream << lastLine << flush; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - timeoutSeconds--; + --timeoutSeconds; } Utils::clipText(""); - outputTextStream << "\nClipboard cleared!" << endl; + outputTextStream << '\r' << QString(lastLine.size(), ' ') << '\r'; + outputTextStream << QObject::tr("Clipboard cleared!") << endl; return EXIT_SUCCESS; } diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index ef69488889..c85e5d95de 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -41,19 +41,14 @@ Command::~Command() { } -int Command::execute(const QStringList&) -{ - return EXIT_FAILURE; -} - QString Command::getDescriptionLine() { - QString response = this->name; + QString response = name; QString space(" "); - QString spaces = space.repeated(15 - this->name.length()); + QString spaces = space.repeated(15 - name.length()); response = response.append(spaces); - response = response.append(this->description); + response = response.append(description); response = response.append("\n"); return response; } diff --git a/src/cli/Command.h b/src/cli/Command.h index 2ebdd77b9b..7ad49440aa 100644 --- a/src/cli/Command.h +++ b/src/cli/Command.h @@ -29,7 +29,7 @@ class Command { public: virtual ~Command(); - virtual int execute(const QStringList& arguments); + virtual int execute(const QStringList& arguments) = 0; QString name; QString description; QString getDescriptionLine(); diff --git a/src/cli/Diceware.cpp b/src/cli/Diceware.cpp index 4cdda0a73a..72cbd19605 100644 --- a/src/cli/Diceware.cpp +++ b/src/cli/Diceware.cpp @@ -24,6 +24,7 @@ #include #include "core/PassphraseGenerator.h" +#include "Utils.h" Diceware::Diceware() { @@ -37,26 +38,25 @@ Diceware::~Diceware() int Diceware::execute(const QStringList& arguments) { - QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream in(Utils::STDIN, QIODevice::ReadOnly); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); - QCommandLineOption words(QStringList() << "W" - << "words", + parser.setApplicationDescription(description); + QCommandLineOption words(QStringList() << "W" << "words", QObject::tr("Word count for the diceware passphrase."), - QObject::tr("count")); + QObject::tr("count", "CLI parameter")); parser.addOption(words); - QCommandLineOption wordlistFile(QStringList() << "w" - << "word-list", + QCommandLineOption wordlistFile(QStringList() << "w" << "word-list", QObject::tr("Wordlist for the diceware generator.\n[Default: EFF English]"), QObject::tr("path")); parser.addOption(wordlistFile); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); if (!args.isEmpty()) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli diceware"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli diceware"); return EXIT_FAILURE; } @@ -76,12 +76,12 @@ int Diceware::execute(const QStringList& arguments) } if (!dicewareGenerator.isValid()) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli diceware"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli diceware"); return EXIT_FAILURE; } QString password = dicewareGenerator.generatePassphrase(); - outputTextStream << password << endl; + out << password << endl; return EXIT_SUCCESS; } diff --git a/src/cli/Edit.cpp b/src/cli/Edit.cpp index 056a0a595a..c2f0677941 100644 --- a/src/cli/Edit.cpp +++ b/src/cli/Edit.cpp @@ -41,22 +41,20 @@ Edit::~Edit() int Edit::execute(const QStringList& arguments) { - - QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream in(Utils::STDIN, QIODevice::ReadOnly); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); - QCommandLineOption username(QStringList() << "u" - << "username", + QCommandLineOption username(QStringList() << "u" << "username", QObject::tr("Username for the entry."), QObject::tr("username")); parser.addOption(username); @@ -64,61 +62,58 @@ int Edit::execute(const QStringList& arguments) QCommandLineOption url(QStringList() << "url", QObject::tr("URL for the entry."), QObject::tr("URL")); parser.addOption(url); - QCommandLineOption title(QStringList() << "t" - << "title", + QCommandLineOption title(QStringList() << "t" << "title", QObject::tr("Title for the entry."), QObject::tr("title")); parser.addOption(title); - QCommandLineOption prompt(QStringList() << "p" - << "password-prompt", + QCommandLineOption prompt(QStringList() << "p" << "password-prompt", QObject::tr("Prompt for the entry's password.")); parser.addOption(prompt); - QCommandLineOption generate(QStringList() << "g" - << "generate", + QCommandLineOption generate(QStringList() << "g" << "generate", QObject::tr("Generate a password for the entry.")); parser.addOption(generate); - QCommandLineOption length(QStringList() << "l" - << "password-length", + QCommandLineOption length(QStringList() << "l" << "password-length", QObject::tr("Length for the generated password."), QObject::tr("length")); parser.addOption(length); parser.addPositionalArgument("entry", QObject::tr("Path of the entry to edit.")); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli edit"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli edit"); return EXIT_FAILURE; } - QString databasePath = args.at(0); - QString entryPath = args.at(1); + const QString& databasePath = args.at(0); + const QString& entryPath = args.at(1); - Database* db = Database::unlockFromStdin(databasePath, parser.value(keyFile)); - if (db == nullptr) { + Database* db = Database::unlockFromStdin(databasePath, parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + if (!db) { return EXIT_FAILURE; } QString passwordLength = parser.value(length); if (!passwordLength.isEmpty() && !passwordLength.toInt()) { - qCritical("Invalid value for password length %s.", qPrintable(passwordLength)); + err << QObject::tr("Invalid value for password length: %1").arg(passwordLength) << endl; return EXIT_FAILURE; } Entry* entry = db->rootGroup()->findEntryByPath(entryPath); if (!entry) { - qCritical("Could not find entry with path %s.", qPrintable(entryPath)); + err << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl; return EXIT_FAILURE; } if (parser.value("username").isEmpty() && parser.value("url").isEmpty() && parser.value("title").isEmpty() && !parser.isSet(prompt) && !parser.isSet(generate)) { - qCritical("Not changing any field for entry %s.", qPrintable(entryPath)); + err << QObject::tr("Not changing any field for entry %1.").arg(entryPath) << endl; return EXIT_FAILURE; } @@ -137,8 +132,7 @@ int Edit::execute(const QStringList& arguments) } if (parser.isSet(prompt)) { - outputTextStream << "Enter new password for entry: "; - outputTextStream.flush(); + out << QObject::tr("Enter new password for entry: ") << flush; QString password = Utils::getPassword(); entry->setPassword(password); } else if (parser.isSet(generate)) { @@ -147,7 +141,7 @@ int Edit::execute(const QStringList& arguments) if (passwordLength.isEmpty()) { passwordGenerator.setLength(PasswordGenerator::DefaultLength); } else { - passwordGenerator.setLength(passwordLength.toInt()); + passwordGenerator.setLength(static_cast(passwordLength.toInt())); } passwordGenerator.setCharClasses(PasswordGenerator::DefaultCharset); @@ -160,10 +154,10 @@ int Edit::execute(const QStringList& arguments) QString errorMessage = db->saveToFile(databasePath); if (!errorMessage.isEmpty()) { - qCritical("Writing the database failed %s.", qPrintable(errorMessage)); + err << QObject::tr("Writing the database failed: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } - outputTextStream << "Successfully edited entry " << entry->title() << "." << endl; + out << QObject::tr("Successfully edited entry %1.").arg(entry->title()) << endl; return EXIT_SUCCESS; } diff --git a/src/cli/Estimate.cpp b/src/cli/Estimate.cpp index 9a2ab0b0f7..c249d7b1f0 100644 --- a/src/cli/Estimate.cpp +++ b/src/cli/Estimate.cpp @@ -16,6 +16,7 @@ */ #include "Estimate.h" +#include "cli/Utils.h" #include #include @@ -44,117 +45,126 @@ Estimate::~Estimate() static void estimate(const char* pwd, bool advanced) { - double e; - int len = strlen(pwd); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + + double e = 0.0; + int len = static_cast(strlen(pwd)); if (!advanced) { - e = ZxcvbnMatch(pwd, 0, 0); - printf("Length %d\tEntropy %.3f\tLog10 %.3f\n", len, e, e * 0.301029996); + e = ZxcvbnMatch(pwd, nullptr, nullptr); + out << QObject::tr("Length %1").arg(len, 0) << '\t' + << QObject::tr("Entropy %1").arg(e, 0, 'f', 3) << '\t' + << QObject::tr("Log10 %1").arg(e * 0.301029996, 0, 'f', 3) << endl; } else { - int ChkLen; + int ChkLen = 0; ZxcMatch_t *info, *p; double m = 0.0; - e = ZxcvbnMatch(pwd, 0, &info); + e = ZxcvbnMatch(pwd, nullptr, &info); for (p = info; p; p = p->Next) { m += p->Entrpy; } m = e - m; - printf("Length %d\tEntropy %.3f\tLog10 %.3f\n Multi-word extra bits %.1f\n", len, e, e * 0.301029996, m); + out << QObject::tr("Length %1").arg(len) << '\t' + << QObject::tr("Entropy %1").arg(e, 0, 'f', 3) << '\t' + << QObject::tr("Log10 %1").arg(e * 0.301029996, 0, 'f', 3) << "\n " + << QObject::tr("Multi-word extra bits %1").arg(m, 0, 'f', 1) << endl; p = info; ChkLen = 0; while (p) { int n; switch (static_cast(p->Type)) { case BRUTE_MATCH: - printf(" Type: Bruteforce "); + out << " " << QObject::tr("Type: Bruteforce") << " "; break; case DICTIONARY_MATCH: - printf(" Type: Dictionary "); + out << " " << QObject::tr("Type: Dictionary") << " "; break; case DICT_LEET_MATCH: - printf(" Type: Dict+Leet "); + out << " " << QObject::tr("Type: Dict+Leet") << " "; break; case USER_MATCH: - printf(" Type: User Words "); + out << " " << QObject::tr("Type: User Words") << " "; break; case USER_LEET_MATCH: - printf(" Type: User+Leet "); + out << " " << QObject::tr("Type: User+Leet") << " "; break; case REPEATS_MATCH: - printf(" Type: Repeated "); + out << " " << QObject::tr("Type: Repeated") << " "; break; case SEQUENCE_MATCH: - printf(" Type: Sequence "); + out << " " << QObject::tr("Type: Sequence") << " "; break; case SPATIAL_MATCH: - printf(" Type: Spatial "); + out << " " << QObject::tr("Type: Spatial") << " "; break; case DATE_MATCH: - printf(" Type: Date "); + out << " " << QObject::tr("Type: Date") << " "; break; case BRUTE_MATCH + MULTIPLE_MATCH: - printf(" Type: Bruteforce(Rep)"); + out << " " << QObject::tr("Type: Bruteforce(Rep)") << " "; break; case DICTIONARY_MATCH + MULTIPLE_MATCH: - printf(" Type: Dictionary(Rep)"); + out << " " << QObject::tr("Type: Dictionary(Rep)") << " "; break; case DICT_LEET_MATCH + MULTIPLE_MATCH: - printf(" Type: Dict+Leet(Rep) "); + out << " " << QObject::tr("Type: Dict+Leet(Rep)") << " "; break; case USER_MATCH + MULTIPLE_MATCH: - printf(" Type: User Words(Rep)"); + out << " " << QObject::tr("Type: User Words(Rep)") << " "; break; case USER_LEET_MATCH + MULTIPLE_MATCH: - printf(" Type: User+Leet(Rep) "); + out << " " << QObject::tr("Type: User+Leet(Rep)") << " "; break; case REPEATS_MATCH + MULTIPLE_MATCH: - printf(" Type: Repeated(Rep) "); + out << " " << QObject::tr("Type: Repeated(Rep)") << " "; break; case SEQUENCE_MATCH + MULTIPLE_MATCH: - printf(" Type: Sequence(Rep) "); + out << " " << QObject::tr("Type: Sequence(Rep)") << " "; break; case SPATIAL_MATCH + MULTIPLE_MATCH: - printf(" Type: Spatial(Rep) "); + out << " " << QObject::tr("Type: Spatial(Rep)") << " "; break; case DATE_MATCH + MULTIPLE_MATCH: - printf(" Type: Date(Rep) "); + out << " " << QObject::tr("Type: Date(Rep)") << " "; break; default: - printf(" Type: Unknown%d ", p->Type); + out << " " << QObject::tr("Type: Unknown%1").arg(p->Type) << " "; break; } ChkLen += p->Length; - printf(" Length %d Entropy %6.3f (%.2f) ", p->Length, p->Entrpy, p->Entrpy * 0.301029996); + + out << QObject::tr("Length %1").arg(p->Length) << '\t' + << QObject::tr("Entropy %1 (%2)").arg(p->Entrpy, 6, 'f', 3).arg(p->Entrpy * 0.301029996, 0, 'f', 2) << '\t'; for (n = 0; n < p->Length; ++n, ++pwd) { - printf("%c", *pwd); + out << *pwd; } - printf("\n"); + out << endl; p = p->Next; } ZxcvbnFreeInfo(info); if (ChkLen != len) { - printf("*** Password length (%d) != sum of length of parts (%d) ***\n", len, ChkLen); + out << QObject::tr("*** Password length (%1) != sum of length of parts (%2) ***").arg(len).arg(ChkLen) << endl; } } } int Estimate::execute(const QStringList& arguments) { - QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream in(Utils::STDIN, QIODevice::ReadOnly); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("password", QObject::tr("Password for which to estimate the entropy."), "[password]"); - QCommandLineOption advancedOption(QStringList() << "a" - << "advanced", + QCommandLineOption advancedOption(QStringList() << "a" << "advanced", QObject::tr("Perform advanced analysis on the password.")); parser.addOption(advancedOption); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); if (args.size() > 1) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli estimate"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli estimate"); return EXIT_FAILURE; } @@ -162,7 +172,7 @@ int Estimate::execute(const QStringList& arguments) if (args.size() == 1) { password = args.at(0); } else { - password = inputTextStream.readLine(); + password = in.readLine(); } estimate(password.toLatin1(), parser.isSet(advancedOption)); diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index f0004e6882..cc39c469ad 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -43,17 +43,17 @@ Extract::~Extract() int Extract::execute(const QStringList& arguments) { - QTextStream out(stdout); - QTextStream errorTextStream(stderr); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database to extract.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -62,8 +62,7 @@ int Extract::execute(const QStringList& arguments) return EXIT_FAILURE; } - out << QObject::tr("Insert password to unlock %1: ").arg(args.at(0)); - out.flush(); + out << QObject::tr("Insert password to unlock %1: ").arg(args.at(0)) << flush; auto compositeKey = QSharedPointer::create(); @@ -74,52 +73,51 @@ int Extract::execute(const QStringList& arguments) QString keyFilePath = parser.value(keyFile); if (!keyFilePath.isEmpty()) { + // LCOV_EXCL_START auto fileKey = QSharedPointer::create(); QString errorMsg; if (!fileKey->load(keyFilePath, &errorMsg)) { - errorTextStream << QObject::tr("Failed to load key file %1 : %2").arg(keyFilePath).arg(errorMsg); - errorTextStream << endl; + err << QObject::tr("Failed to load key file %1: %2").arg(keyFilePath).arg(errorMsg) << endl; return EXIT_FAILURE; } if (fileKey->type() != FileKey::Hashed) { - errorTextStream << QObject::tr("WARNING: You are using a legacy key file format which may become\n" - "unsupported in the future.\n\n" - "Please consider generating a new key file."); - errorTextStream << endl; + err << QObject::tr("WARNING: You are using a legacy key file format which may become\n" + "unsupported in the future.\n\n" + "Please consider generating a new key file.") << endl; } + // LCOV_EXCL_STOP compositeKey->addKey(fileKey); } - QString databaseFilename = args.at(0); + const QString& databaseFilename = args.at(0); QFile dbFile(databaseFilename); if (!dbFile.exists()) { - qCritical("File %s does not exist.", qPrintable(databaseFilename)); + err << QObject::tr("File %1 does not exist.").arg(databaseFilename) << endl; return EXIT_FAILURE; } if (!dbFile.open(QIODevice::ReadOnly)) { - qCritical("Unable to open file %s.", qPrintable(databaseFilename)); + err << QObject::tr("Unable to open file %1.").arg(databaseFilename) << endl; return EXIT_FAILURE; } KeePass2Reader reader; reader.setSaveXml(true); - Database* db = reader.readDatabase(&dbFile, compositeKey); - delete db; + QScopedPointer db(reader.readDatabase(&dbFile, compositeKey)); QByteArray xmlData = reader.reader()->xmlData(); if (reader.hasError()) { if (xmlData.isEmpty()) { - qCritical("Error while reading the database:\n%s", qPrintable(reader.errorString())); + err << QObject::tr("Error while reading the database:\n%1").arg(reader.errorString()) << endl; } else { - qWarning("Error while parsing the database:\n%s\n", qPrintable(reader.errorString())); + err << QObject::tr("Error while parsing the database:\n%1").arg(reader.errorString()) << endl; } return EXIT_FAILURE; } - out << xmlData.constData() << "\n"; + out << xmlData.constData() << endl; return EXIT_SUCCESS; } diff --git a/src/cli/Generate.cpp b/src/cli/Generate.cpp index 6a5be3f07d..7780aa8294 100644 --- a/src/cli/Generate.cpp +++ b/src/cli/Generate.cpp @@ -19,6 +19,7 @@ #include #include "Generate.h" +#include "cli/Utils.h" #include #include @@ -37,38 +38,32 @@ Generate::~Generate() int Generate::execute(const QStringList& arguments) { - QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream in(Utils::STDIN, QIODevice::ReadOnly); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + out.setCodec("UTF-8"); // force UTF-8 to prevent ??? characters in extended-ASCII passwords QCommandLineParser parser; - parser.setApplicationDescription(this->description); - QCommandLineOption len(QStringList() << "L" - << "length", + parser.setApplicationDescription(description); + QCommandLineOption len(QStringList() << "L" << "length", QObject::tr("Length of the generated password"), QObject::tr("length")); parser.addOption(len); - QCommandLineOption lower(QStringList() << "l" - << "lower", + QCommandLineOption lower(QStringList() << "l" << "lower", QObject::tr("Use lowercase characters")); parser.addOption(lower); - QCommandLineOption upper(QStringList() << "u" - << "upper", + QCommandLineOption upper(QStringList() << "u" << "upper", QObject::tr("Use uppercase characters")); parser.addOption(upper); - QCommandLineOption numeric(QStringList() << "n" - << "numeric", + QCommandLineOption numeric(QStringList() << "n" << "numeric", QObject::tr("Use numbers.")); parser.addOption(numeric); - QCommandLineOption special(QStringList() << "s" - << "special", + QCommandLineOption special(QStringList() << "s" << "special", QObject::tr("Use special characters")); parser.addOption(special); - QCommandLineOption extended(QStringList() << "e" - << "extended", + QCommandLineOption extended(QStringList() << "e" << "extended", QObject::tr("Use extended ASCII")); parser.addOption(extended); - QCommandLineOption exclude(QStringList() << "x" - << "exclude", + QCommandLineOption exclude(QStringList() << "x" << "exclude", QObject::tr("Exclude character set"), QObject::tr("chars")); parser.addOption(exclude); @@ -78,12 +73,12 @@ int Generate::execute(const QStringList& arguments) QCommandLineOption every_group(QStringList() << "every-group", QObject::tr("Include characters from every selected group")); parser.addOption(every_group); - + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); if (!args.isEmpty()) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli generate"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli generate"); return EXIT_FAILURE; } @@ -93,7 +88,7 @@ int Generate::execute(const QStringList& arguments) passwordGenerator.setLength(PasswordGenerator::DefaultLength); } else { int length = parser.value(len).toInt(); - passwordGenerator.setLength(length); + passwordGenerator.setLength(static_cast(length)); } PasswordGenerator::CharClasses classes = 0x0; @@ -128,12 +123,12 @@ int Generate::execute(const QStringList& arguments) passwordGenerator.setExcludedChars(parser.value(exclude)); if (!passwordGenerator.isValid()) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli generate"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli generate"); return EXIT_FAILURE; } QString password = passwordGenerator.generatePassword(); - outputTextStream << password << endl; + out << password << endl; return EXIT_SUCCESS; } diff --git a/src/cli/List.cpp b/src/cli/List.cpp index b39b3fc147..4d1ebcfc50 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -19,6 +19,7 @@ #include #include "List.h" +#include "cli/Utils.h" #include #include @@ -39,22 +40,20 @@ List::~List() int List::execute(const QStringList& arguments) { - QTextStream out(stdout); + QTextStream out(Utils::STDOUT); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); - parser.addPositionalArgument("group", QObject::tr("Path of the group to list. Default is /"), QString("[group]")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + parser.addPositionalArgument("group", QObject::tr("Path of the group to list. Default is /"), "[group]"); + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); - - QCommandLineOption recursiveOption(QStringList() << "R" - << "recursive", + QCommandLineOption recursiveOption(QStringList() << "R" << "recursive", QObject::tr("Recursive mode, list elements recursively")); parser.addOption(recursiveOption); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -65,33 +64,33 @@ int List::execute(const QStringList& arguments) bool recursive = parser.isSet(recursiveOption); - Database* db = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); - if (db == nullptr) { + QScopedPointer db(Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR)); + if (!db) { return EXIT_FAILURE; } if (args.size() == 2) { - return this->listGroup(db, recursive, args.at(1)); + return listGroup(db.data(), recursive, args.at(1)); } - return this->listGroup(db, recursive); + return listGroup(db.data(), recursive); } -int List::listGroup(Database* database, bool recursive, QString groupPath) +int List::listGroup(Database* database, bool recursive, const QString& groupPath) { - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); + if (groupPath.isEmpty()) { - outputTextStream << database->rootGroup()->print(recursive); - outputTextStream.flush(); + out << database->rootGroup()->print(recursive) << flush; return EXIT_SUCCESS; } Group* group = database->rootGroup()->findGroupByPath(groupPath); - if (group == nullptr) { - qCritical("Cannot find group %s.", qPrintable(groupPath)); + if (!group) { + err << QObject::tr("Cannot find group %1.").arg(groupPath) << endl; return EXIT_FAILURE; } - outputTextStream << group->print(recursive); - outputTextStream.flush(); + out << group->print(recursive) << flush; return EXIT_SUCCESS; } diff --git a/src/cli/List.h b/src/cli/List.h index 00c3769722..5697d93904 100644 --- a/src/cli/List.h +++ b/src/cli/List.h @@ -26,7 +26,7 @@ class List : public Command List(); ~List(); int execute(const QStringList& arguments); - int listGroup(Database* database, bool recursive, QString groupPath = QString("")); + int listGroup(Database* database, bool recursive, const QString& groupPath = {}); }; #endif // KEEPASSXC_LIST_H diff --git a/src/cli/Locate.cpp b/src/cli/Locate.cpp index f803728855..3bca8ae1da 100644 --- a/src/cli/Locate.cpp +++ b/src/cli/Locate.cpp @@ -1,3 +1,5 @@ +#include + /* * Copyright (C) 2017 KeePassXC Team * @@ -25,6 +27,7 @@ #include #include "cli/Utils.h" +#include "core/Global.h" #include "core/Database.h" #include "core/Entry.h" #include "core/Group.h" @@ -41,18 +44,17 @@ Locate::~Locate() int Locate::execute(const QStringList& arguments) { - - QTextStream out(stdout); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addPositionalArgument("term", QObject::tr("Search term.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -61,26 +63,27 @@ int Locate::execute(const QStringList& arguments) return EXIT_FAILURE; } - Database* db = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); + QScopedPointer db(Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR)); if (!db) { return EXIT_FAILURE; } - return this->locateEntry(db, args.at(1)); + return locateEntry(db.data(), args.at(1)); } -int Locate::locateEntry(Database* database, QString searchTerm) +int Locate::locateEntry(Database* database, const QString& searchTerm) { + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); QStringList results = database->rootGroup()->locate(searchTerm); if (results.isEmpty()) { - outputTextStream << "No results for that search term" << endl; - return EXIT_SUCCESS; + err << "No results for that search term." << endl; + return EXIT_FAILURE; } - for (QString result : results) { - outputTextStream << result << endl; + for (const QString& result : asConst(results)) { + out << result << endl; } return EXIT_SUCCESS; } diff --git a/src/cli/Locate.h b/src/cli/Locate.h index 3677a034df..3355d41ec6 100644 --- a/src/cli/Locate.h +++ b/src/cli/Locate.h @@ -26,7 +26,7 @@ class Locate : public Command Locate(); ~Locate(); int execute(const QStringList& arguments); - int locateEntry(Database* database, QString searchTerm); + int locateEntry(Database* database, const QString& searchTerm); }; #endif // KEEPASSXC_LOCATE_H diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index ea7e6636a2..a5b4a2cb7e 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -15,8 +15,6 @@ * along with this program. If not, see . */ -#include - #include "Merge.h" #include @@ -24,6 +22,9 @@ #include "core/Database.h" #include "core/Merger.h" +#include "cli/Utils.h" + +#include Merge::Merge() { @@ -37,29 +38,28 @@ Merge::~Merge() int Merge::execute(const QStringList& arguments) { - QTextStream out(stdout); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database1", QObject::tr("Path of the database to merge into.")); parser.addPositionalArgument("database2", QObject::tr("Path of the database to merge from.")); - QCommandLineOption samePasswordOption(QStringList() << "s" - << "same-credentials", + QCommandLineOption samePasswordOption(QStringList() << "s" << "same-credentials", QObject::tr("Use the same credentials for both database files.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); - QCommandLineOption keyFileFrom(QStringList() << "f" - << "key-file-from", + QCommandLineOption keyFileFrom(QStringList() << "f" << "key-file-from", QObject::tr("Key file of the database to merge from."), QObject::tr("path")); parser.addOption(keyFileFrom); parser.addOption(samePasswordOption); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -68,30 +68,30 @@ int Merge::execute(const QStringList& arguments) return EXIT_FAILURE; } - Database* db1 = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); - if (db1 == nullptr) { + QScopedPointer db1(Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR)); + if (!db1) { return EXIT_FAILURE; } - Database* db2; + QScopedPointer db2; if (!parser.isSet("same-credentials")) { - db2 = Database::unlockFromStdin(args.at(1), parser.value(keyFileFrom)); + db2.reset(Database::unlockFromStdin(args.at(1), parser.value(keyFileFrom), Utils::STDOUT, Utils::STDERR)); } else { - db2 = Database::openDatabaseFile(args.at(1), db1->key()); + db2.reset(Database::openDatabaseFile(args.at(1), db1->key())); } - if (db2 == nullptr) { + if (!db2) { return EXIT_FAILURE; } - Merger merger(db2, db1); + Merger merger(db2.data(), db1.data()); merger.merge(); QString errorMessage = db1->saveToFile(args.at(0)); if (!errorMessage.isEmpty()) { - qCritical("Unable to save database to file : %s", qPrintable(errorMessage)); + err << QObject::tr("Unable to save database to file : %1").arg(errorMessage) << endl; return EXIT_FAILURE; } - out << "Successfully merged the database files.\n"; + out << "Successfully merged the database files." << endl; return EXIT_SUCCESS; } diff --git a/src/cli/Remove.cpp b/src/cli/Remove.cpp index 64a5976e9a..5bbfd67e43 100644 --- a/src/cli/Remove.cpp +++ b/src/cli/Remove.cpp @@ -44,40 +44,41 @@ Remove::~Remove() int Remove::execute(const QStringList& arguments) { - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream out(Utils::STDERR, QIODevice::WriteOnly); QCommandLineParser parser; - parser.setApplicationDescription(QCoreApplication::translate("main", "Remove an entry from the database.")); - parser.addPositionalArgument("database", QCoreApplication::translate("main", "Path of the database.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + parser.setApplicationDescription(QCoreApplication::tr("main", "Remove an entry from the database.")); + parser.addPositionalArgument("database", QCoreApplication::tr("main", "Path of the database.")); + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); - parser.addPositionalArgument("entry", QCoreApplication::translate("main", "Path of the entry to remove.")); + parser.addPositionalArgument("entry", QCoreApplication::tr("main", "Path of the entry to remove.")); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli rm"); + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli rm"); return EXIT_FAILURE; } - Database* db = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); - if (db == nullptr) { + QScopedPointer db(Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR)); + if (!db) { return EXIT_FAILURE; } - return this->removeEntry(db, args.at(0), args.at(1)); + return removeEntry(db.data(), args.at(0), args.at(1)); } -int Remove::removeEntry(Database* database, QString databasePath, QString entryPath) +int Remove::removeEntry(Database* database, const QString& databasePath, const QString& entryPath) { + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); Entry* entry = database->rootGroup()->findEntryByPath(entryPath); if (!entry) { - qCritical("Entry %s not found.", qPrintable(entryPath)); + err << QObject::tr("Entry %1 not found.").arg(entryPath) << endl; return EXIT_FAILURE; } @@ -92,14 +93,14 @@ int Remove::removeEntry(Database* database, QString databasePath, QString entryP QString errorMessage = database->saveToFile(databasePath); if (!errorMessage.isEmpty()) { - qCritical("Unable to save database to file : %s", qPrintable(errorMessage)); + err << QObject::tr("Unable to save database to file: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } if (recycled) { - outputTextStream << "Successfully recycled entry " << entryTitle << "." << endl; + out << QObject::tr("Successfully recycled entry %1.").arg(entryTitle) << endl; } else { - outputTextStream << "Successfully deleted entry " << entryTitle << "." << endl; + out << QObject::tr("Successfully deleted entry %1.").arg(entryTitle) << endl; } return EXIT_SUCCESS; diff --git a/src/cli/Remove.h b/src/cli/Remove.h index 5465530eda..33d62f4bd6 100644 --- a/src/cli/Remove.h +++ b/src/cli/Remove.h @@ -28,7 +28,7 @@ class Remove : public Command Remove(); ~Remove(); int execute(const QStringList& arguments); - int removeEntry(Database* database, QString databasePath, QString entryPath); + int removeEntry(Database* database, const QString& databasePath, const QString& entryPath); }; #endif // KEEPASSXC_REMOVE_H diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index be188c75c9..5e2ec14b4f 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -15,17 +15,19 @@ * along with this program. If not, see . */ +#include "Show.h" + #include #include -#include "Show.h" - #include #include #include "core/Database.h" #include "core/Entry.h" #include "core/Group.h" +#include "core/Global.h" +#include "Utils.h" Show::Show() { @@ -39,19 +41,17 @@ Show::~Show() int Show::execute(const QStringList& arguments) { - QTextStream out(stdout); + QTextStream out(Utils::STDOUT); QCommandLineParser parser; - parser.setApplicationDescription(this->description); + parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); - QCommandLineOption keyFile(QStringList() << "k" - << "key-file", + QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); QCommandLineOption attributes( - QStringList() << "a" - << "attributes", + QStringList() << "a" << "attributes", QObject::tr( "Names of the attributes to show. " "This option can be specified more than once, with each attribute shown one-per-line in the given order. " @@ -59,6 +59,7 @@ int Show::execute(const QStringList& arguments) QObject::tr("attribute")); parser.addOption(attributes); parser.addPositionalArgument("entry", QObject::tr("Name of the entry to show.")); + parser.addHelpOption(); parser.process(arguments); const QStringList args = parser.positionalArguments(); @@ -67,23 +68,23 @@ int Show::execute(const QStringList& arguments) return EXIT_FAILURE; } - Database* db = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); - if (db == nullptr) { + QScopedPointer db(Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR)); + if (!db) { return EXIT_FAILURE; } - return this->showEntry(db, parser.values(attributes), args.at(1)); + return showEntry(db.data(), parser.values(attributes), args.at(1)); } -int Show::showEntry(Database* database, QStringList attributes, QString entryPath) +int Show::showEntry(Database* database, QStringList attributes, const QString& entryPath) { - - QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream in(Utils::STDIN, QIODevice::ReadOnly); + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); Entry* entry = database->rootGroup()->findEntry(entryPath); if (!entry) { - qCritical("Could not find entry with path %s.", qPrintable(entryPath)); + err << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl; return EXIT_FAILURE; } @@ -95,16 +96,16 @@ int Show::showEntry(Database* database, QStringList attributes, QString entryPat // Iterate over the attributes and output them line-by-line. bool sawUnknownAttribute = false; - for (QString attribute : attributes) { + for (const QString& attribute : asConst(attributes)) { if (!entry->attributes()->contains(attribute)) { sawUnknownAttribute = true; - qCritical("ERROR: unknown attribute '%s'.", qPrintable(attribute)); + err << QObject::tr("ERROR: unknown attribute %1.").arg(attribute) << endl; continue; } if (showAttributeNames) { - outputTextStream << attribute << ": "; + out << attribute << ": "; } - outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(attribute)) << endl; + out << entry->resolveMultiplePlaceholders(entry->attributes()->value(attribute)) << endl; } return sawUnknownAttribute ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/src/cli/Show.h b/src/cli/Show.h index 18b6d7049e..fe16546c3b 100644 --- a/src/cli/Show.h +++ b/src/cli/Show.h @@ -26,7 +26,7 @@ class Show : public Command Show(); ~Show(); int execute(const QStringList& arguments); - int showEntry(Database* database, QStringList attributes, QString entryPath); + int showEntry(Database* database, QStringList attributes, const QString& entryPath); }; #endif // KEEPASSXC_SHOW_H diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index 35e7cce389..8a0f5abe3f 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -25,9 +25,25 @@ #endif #include -#include -void Utils::setStdinEcho(bool enable = true) +namespace Utils +{ +/** + * STDOUT file handle for the CLI. + */ +FILE* STDOUT = stdout; + +/** + * STDERR file handle for the CLI. + */ +FILE* STDERR = stderr; + +/** + * STDIN file handle for the CLI. + */ +FILE* STDIN = stdin; + +void setStdinEcho(bool enable = true) { #ifdef Q_OS_WIN HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); @@ -56,28 +72,55 @@ void Utils::setStdinEcho(bool enable = true) #endif } -QString Utils::getPassword() +QStringList nextPasswords = {}; + +/** + * Set the next password returned by \link getPassword() instead of reading it from STDIN. + * Multiple calls to this method will fill a queue of passwords. + * This function is intended for testing purposes. + * + * @param password password to return next + */ +void setNextPassword(const QString& password) +{ + nextPasswords.append(password); +} + +/** + * Read a user password from STDIN or return a password previously + * set by \link setNextPassword(). + * + * @return the password + */ +QString getPassword() { - static QTextStream inputTextStream(stdin, QIODevice::ReadOnly); - static QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + QTextStream out(STDOUT, QIODevice::WriteOnly); + + // return preset password if one is set + if (!nextPasswords.isEmpty()) { + auto password = nextPasswords.takeFirst(); + // simulate user entering newline + out << endl; + return password; + } + + QTextStream in(STDIN, QIODevice::ReadOnly); setStdinEcho(false); - QString line = inputTextStream.readLine(); + QString line = in.readLine(); setStdinEcho(true); - - // The new line was also not echoed, but we do want to echo it. - outputTextStream << "\n"; - outputTextStream.flush(); + out << endl; return line; } -/* +/** * A valid and running event loop is needed to use the global QClipboard, * so we need to use this from the CLI. */ -int Utils::clipText(const QString& text) +int clipText(const QString& text) { + QTextStream err(Utils::STDERR); QString programName = ""; QStringList arguments; @@ -98,16 +141,18 @@ int Utils::clipText(const QString& text) #endif if (programName.isEmpty()) { - qCritical("No program defined for clipboard manipulation"); + err << QObject::tr("No program defined for clipboard manipulation"); + err.flush(); return EXIT_FAILURE; } - QProcess* clipProcess = new QProcess(nullptr); + auto* clipProcess = new QProcess(nullptr); clipProcess->start(programName, arguments); clipProcess->waitForStarted(); if (clipProcess->state() != QProcess::Running) { - qCritical("Unable to start program %s", qPrintable(programName)); + err << QObject::tr("Unable to start program %1").arg(programName); + err.flush(); return EXIT_FAILURE; } @@ -120,3 +165,5 @@ int Utils::clipText(const QString& text) return clipProcess->exitCode(); } + +} // namespace Utils diff --git a/src/cli/Utils.h b/src/cli/Utils.h index 1f80511833..868ccdef5b 100644 --- a/src/cli/Utils.h +++ b/src/cli/Utils.h @@ -19,13 +19,18 @@ #define KEEPASSXC_UTILS_H #include +#include -class Utils +namespace Utils { -public: - static void setStdinEcho(bool enable); - static QString getPassword(); - static int clipText(const QString& text); +extern FILE* STDOUT; +extern FILE* STDERR; +extern FILE* STDIN; + +void setStdinEcho(bool enable); +QString getPassword(); +void setNextPassword(const QString& password); +int clipText(const QString& text); }; #endif // KEEPASSXC_UTILS_H diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index 1462f92b91..97efd8c08f 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -25,7 +25,7 @@ #include #include "config-keepassx.h" -#include "core/Tools.h" +#include "core/Bootstrap.h" #include "crypto/Crypto.h" #if defined(WITH_ASAN) && defined(WITH_LSAN) @@ -34,17 +34,17 @@ int main(int argc, char** argv) { -#ifdef QT_NO_DEBUG - Tools::disableCoreDumps(); -#endif - if (!Crypto::init()) { qFatal("Fatal error while testing the cryptographic functions:\n%s", qPrintable(Crypto::errorString())); return EXIT_FAILURE; } QCoreApplication app(argc, argv); - app.setApplicationVersion(KEEPASSX_VERSION); + QCoreApplication::setApplicationVersion(KEEPASSX_VERSION); + +#ifdef QT_NO_DEBUG + Bootstrap::bootstrapApplication(); +#endif QTextStream out(stdout); QStringList arguments; @@ -69,7 +69,7 @@ int main(int argc, char** argv) // recognized by this parser. parser.parse(arguments); - if (parser.positionalArguments().size() < 1) { + if (parser.positionalArguments().empty()) { if (parser.isSet("version")) { // Switch to parser.showVersion() when available (QT 5.4). out << KEEPASSX_VERSION << endl; diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp new file mode 100644 index 0000000000..2c25b25056 --- /dev/null +++ b/src/core/Bootstrap.cpp @@ -0,0 +1,236 @@ +/* + * 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 "Bootstrap.h" +#include "core/Config.h" +#include "core/Translator.h" + +#ifdef Q_OS_WIN +#include // for createWindowsDACL() +#include // for Sleep(), SetDllDirectoryA(), SetSearchPathMode(), ... +#endif + +namespace Bootstrap +{ +/** + * When QNetworkAccessManager is instantiated it regularly starts polling + * all network interfaces to see if anything changes and if so, what. This + * creates a latency spike every 10 seconds on Mac OS 10.12+ and Windows 7 >= + * when on a wifi connection. + * So here we disable it for lack of better measure. + * This will also cause this message: QObject::startTimer: Timers cannot + * have negative intervals + * For more info see: + * - https://bugreports.qt.io/browse/QTBUG-40332 + * - https://bugreports.qt.io/browse/QTBUG-46015 + */ +static inline void applyEarlyQNetworkAccessManagerWorkaround() +{ + qputenv("QT_BEARER_POLL_TIMEOUT", QByteArray::number(-1)); +} + +/** + * 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() +{ +#ifdef QT_NO_DEBUG + disableCoreDumps(); +#endif + setupSearchPaths(); + applyEarlyQNetworkAccessManagerWorkaround(); + Translator::installTranslators(); + +#ifdef Q_OS_MAC + // Don't show menu icons on OSX + QApplication::setAttribute(Qt::AA_DontShowIconsInMenus); +#endif +} + +/** + * Restore the main window's state after launch + * + * @param mainWindow the main window whose state to restore + */ +void restoreMainWindowState(MainWindow& mainWindow) +{ + // start minimized if configured + bool minimizeOnStartup = config()->get("GUI/MinimizeOnStartup").toBool(); + bool minimizeToTray = config()->get("GUI/MinimizeToTray").toBool(); +#ifndef Q_OS_LINUX + if (minimizeOnStartup) { +#else + // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at + // the same time (which would happen if both minimize on startup and minimize to tray are set) + // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. + if (minimizeOnStartup && !minimizeToTray) { +#endif + mainWindow.setWindowState(Qt::WindowMinimized); + } + if (!(minimizeOnStartup && minimizeToTray)) { + mainWindow.show(); + } + + if (config()->get("OpenPreviousDatabasesOnStartup").toBool()) { + const QStringList fileNames = config()->get("LastOpenedDatabases").toStringList(); + for (const QString& filename : fileNames) { + if (!filename.isEmpty() && QFile::exists(filename)) { + mainWindow.openDatabase(filename); + } + } + } +} + +// LCOV_EXCL_START +void disableCoreDumps() +{ + // default to true + // there is no point in printing a warning if this is not implemented on the platform + bool success = true; + +#if defined(HAVE_RLIMIT_CORE) + struct rlimit limit; + limit.rlim_cur = 0; + limit.rlim_max = 0; + success = success && (setrlimit(RLIMIT_CORE, &limit) == 0); +#endif + +#if defined(HAVE_PR_SET_DUMPABLE) + success = success && (prctl(PR_SET_DUMPABLE, 0) == 0); +#endif + +// Mac OS X +#ifdef HAVE_PT_DENY_ATTACH + success = success && (ptrace(PT_DENY_ATTACH, 0, 0, 0) == 0); +#endif + +#ifdef Q_OS_WIN + success = success && createWindowsDACL(); +#endif + + if (!success) { + qWarning("Unable to disable core dumps."); + } +} + +// +// This function grants the user associated with the process token minimal access rights and +// denies everything else on Windows. This includes PROCESS_QUERY_INFORMATION and +// PROCESS_VM_READ access rights that are required for MiniDumpWriteDump() or ReadProcessMemory(). +// We do this using a discretionary access control list (DACL). Effectively this prevents +// crash dumps and disallows other processes from accessing our memory. This works as long +// as you do not have admin privileges, since then you are able to grant yourself the +// SeDebugPrivilege or SeTakeOwnershipPrivilege and circumvent the DACL. +// +bool createWindowsDACL() +{ + bool bSuccess = false; + +#ifdef Q_OS_WIN + // Process token and user + HANDLE hToken = nullptr; + PTOKEN_USER pTokenUser = nullptr; + DWORD cbBufferSize = 0; + + // Access control list + PACL pACL = nullptr; + DWORD cbACL = 0; + + // Open the access token associated with the calling process + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { + goto Cleanup; + } + + // Retrieve the token information in a TOKEN_USER structure + GetTokenInformation(hToken, TokenUser, nullptr, 0, &cbBufferSize); + + pTokenUser = static_cast(HeapAlloc(GetProcessHeap(), 0, cbBufferSize)); + if (pTokenUser == nullptr) { + goto Cleanup; + } + + if (!GetTokenInformation(hToken, TokenUser, pTokenUser, cbBufferSize, &cbBufferSize)) { + goto Cleanup; + } + + if (!IsValidSid(pTokenUser->User.Sid)) { + 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); + + // Create and initialize an ACL + pACL = static_cast(HeapAlloc(GetProcessHeap(), 0, cbACL)); + if (pACL == nullptr) { + goto Cleanup; + } + + if (!InitializeAcl(pACL, cbACL, ACL_REVISION)) { + goto Cleanup; + } + + // Add allowed access control entries, everything else is denied + if (!AddAccessAllowedAce( + pACL, + ACL_REVISION, + SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE, // same as protected process + pTokenUser->User.Sid // pointer to the trustee's SID + )) { + goto Cleanup; + } + + // Set discretionary access control list + bSuccess = ERROR_SUCCESS + == SetSecurityInfo(GetCurrentProcess(), // object handle + SE_KERNEL_OBJECT, // type of object + DACL_SECURITY_INFORMATION, // change only the objects DACL + nullptr, + nullptr, // do not change owner or group + pACL, // DACL specified + nullptr // do not change SACL + ); + +Cleanup: + + if (pACL != nullptr) { + HeapFree(GetProcessHeap(), 0, pACL); + } + if (pTokenUser != nullptr) { + HeapFree(GetProcessHeap(), 0, pTokenUser); + } + if (hToken != nullptr) { + CloseHandle(hToken); + } +#endif + + return bSuccess; +} +// LCOV_EXCL_STOP + +void setupSearchPaths() +{ +#ifdef Q_OS_WIN + // Make sure Windows doesn't load DLLs from the current working directory + SetDllDirectoryA(""); + SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE); +#endif +} + +} // namespace Bootstrap diff --git a/src/core/Bootstrap.h b/src/core/Bootstrap.h new file mode 100644 index 0000000000..0e9db155a3 --- /dev/null +++ b/src/core/Bootstrap.h @@ -0,0 +1,34 @@ +/* + * 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_BOOTSTRAP_H +#define KEEPASSXC_BOOTSTRAP_H + +#include "gui/MainWindow.h" + +namespace Bootstrap +{ +void bootstrapApplication(); +void restoreMainWindowState(MainWindow& mainWindow); +void disableCoreDumps(); +bool createWindowsDACL(); +void setupSearchPaths(); +}; + + +#endif //KEEPASSXC_BOOTSTRAP_H diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 5b7a3c07d2..5116fd1996 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -47,7 +47,7 @@ Database::Database() , m_emitModified(false) , m_uuid(QUuid::createUuid()) { - m_data.cipher = KeePass2::CIPHER_AES; + m_data.cipher = KeePass2::CIPHER_AES256; m_data.compressionAlgo = CompressionGZip; // instantiate default AES-KDF with legacy KDBX3 flag set @@ -501,14 +501,14 @@ Database* Database::openDatabaseFile(const QString& fileName, QSharedPointer::create(); - QTextStream outputTextStream(stdout); - QTextStream errorTextStream(stderr); + QTextStream out(outputDescriptor); + QTextStream err(errorDescriptor); - outputTextStream << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename); - outputTextStream.flush(); + out << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename); + out.flush(); QString line = Utils::getPassword(); auto passwordKey = QSharedPointer::create(); @@ -518,11 +518,19 @@ Database* Database::unlockFromStdin(QString databaseFilename, QString keyFilenam if (!keyFilename.isEmpty()) { auto fileKey = QSharedPointer::create(); QString errorMessage; + // LCOV_EXCL_START if (!fileKey->load(keyFilename, &errorMessage)) { - errorTextStream << QObject::tr("Failed to load key file %1: %2").arg(keyFilename, errorMessage); - errorTextStream << endl; + err << QObject::tr("Failed to load key file %1: %2").arg(keyFilename, errorMessage)<< endl; return nullptr; } + + if (fileKey->type() != FileKey::Hashed) { + err << QObject::tr("WARNING: You are using a legacy key file format which may become\n" + "unsupported in the future.\n\n" + "Please consider generating a new key file.") << endl; + } + // LCOV_EXCL_STOP + compositeKey->addKey(fileKey); } diff --git a/src/core/Database.h b/src/core/Database.h index a5ae3effad..7108ded310 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -131,7 +131,8 @@ class Database : public QObject static Database* databaseByUuid(const QUuid& uuid); static Database* openDatabaseFile(const QString& fileName, QSharedPointer key); - static Database* unlockFromStdin(QString databaseFilename, QString keyFilename = QString("")); + static Database* unlockFromStdin(QString databaseFilename, QString keyFilename = {}, + FILE* outputDescriptor = stdout, FILE* errorDescriptor = stderr); signals: void groupDataChanged(Group* group); diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp index 01b7150721..3dbcdaad8b 100644 --- a/src/core/PasswordGenerator.cpp +++ b/src/core/PasswordGenerator.cpp @@ -80,21 +80,21 @@ QString PasswordGenerator::generatePassword() const QString password; if (m_flags & CharFromEveryGroup) { - for (int i = 0; i < groups.size(); i++) { - int pos = randomGen()->randomUInt(groups[i].size()); + for (const auto& group : groups) { + int pos = randomGen()->randomUInt(static_cast(group.size())); - password.append(groups[i][pos]); + password.append(group[pos]); } for (int i = groups.size(); i < m_length; i++) { - int pos = randomGen()->randomUInt(passwordChars.size()); + int pos = randomGen()->randomUInt(static_cast(passwordChars.size())); password.append(passwordChars[pos]); } // shuffle chars for (int i = (password.size() - 1); i >= 1; i--) { - int j = randomGen()->randomUInt(i + 1); + int j = randomGen()->randomUInt(static_cast(i + 1)); QChar tmp = password[i]; password[i] = password[j]; @@ -102,7 +102,7 @@ QString PasswordGenerator::generatePassword() const } } else { for (int i = 0; i < m_length; i++) { - int pos = randomGen()->randomUInt(passwordChars.size()); + int pos = randomGen()->randomUInt(static_cast(passwordChars.size())); password.append(passwordChars[pos]); } @@ -111,21 +111,6 @@ QString PasswordGenerator::generatePassword() const return password; } -int PasswordGenerator::getbits() const -{ - const QVector groups = passwordGroups(); - - int bits = 0; - QVector passwordChars; - for (const PasswordGroup& group : groups) { - bits += group.size(); - } - - bits *= m_length; - - return bits; -} - bool PasswordGenerator::isValid() const { if (m_classes == 0) { @@ -138,11 +123,8 @@ bool PasswordGenerator::isValid() const return false; } - if (passwordGroups().size() == 0) { - return false; - } + return !passwordGroups().isEmpty(); - return true; } QVector PasswordGenerator::passwordGroups() const @@ -298,9 +280,9 @@ QVector PasswordGenerator::passwordGroups() const j = group.indexOf(ch); } } - if (group.size() > 0) { + if (!group.isEmpty()) { passwordGroups.replace(i, group); - i++; + ++i; } else { passwordGroups.remove(i); } diff --git a/src/core/PasswordGenerator.h b/src/core/PasswordGenerator.h index 1d6ac73f67..cb6402d0fc 100644 --- a/src/core/PasswordGenerator.h +++ b/src/core/PasswordGenerator.h @@ -66,7 +66,6 @@ class PasswordGenerator bool isValid() const; QString generatePassword() const; - int getbits() const; static const int DefaultLength = 16; static const char* DefaultExcludedChars; diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 458d429888..60c50b4516 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -18,19 +18,20 @@ */ #include "Tools.h" +#include "core/Config.h" +#include "core/Translator.h" #include #include #include #include #include -#include - #include +#include + #ifdef Q_OS_WIN -#include // for SetSecurityInfo() -#include // for Sleep(), SetDllDirectoryA(), SetSearchPathMode(), ... +#include // for Sleep() #endif #ifdef Q_OS_UNIX @@ -56,294 +57,161 @@ namespace Tools { - - QString humanReadableFileSize(qint64 bytes, quint32 precision) - { - constexpr auto kibibyte = 1024; - double size = bytes; - - QStringList units = QStringList() << "B" - << "KiB" - << "MiB" - << "GiB"; - int i = 0; - int maxI = units.size() - 1; - - while ((size >= kibibyte) && (i < maxI)) { - size /= kibibyte; - i++; - } - - return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i)); +QString humanReadableFileSize(qint64 bytes, quint32 precision) +{ + constexpr auto kibibyte = 1024; + double size = bytes; + + QStringList units = QStringList() << "B" + << "KiB" + << "MiB" + << "GiB"; + int i = 0; + int maxI = units.size() - 1; + + while ((size >= kibibyte) && (i < maxI)) { + size /= kibibyte; + i++; } - bool hasChild(const QObject* parent, const QObject* child) - { - if (!parent || !child) { - return false; - } + return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i)); +} - const QObjectList children = parent->children(); - for (QObject* c : children) { - if (child == c || hasChild(c, child)) { - return true; - } - } +bool hasChild(const QObject* parent, const QObject* child) +{ + if (!parent || !child) { return false; } - bool readFromDevice(QIODevice* device, QByteArray& data, int size) - { - QByteArray buffer; - buffer.resize(size); - - qint64 readResult = device->read(buffer.data(), size); - if (readResult == -1) { - return false; - } else { - buffer.resize(readResult); - data = buffer; + const QObjectList children = parent->children(); + for (QObject* c : children) { + if (child == c || hasChild(c, child)) { return true; } } + return false; +} - bool readAllFromDevice(QIODevice* device, QByteArray& data) - { - QByteArray result; - qint64 readBytes = 0; - qint64 readResult; - do { - result.resize(result.size() + 16384); - readResult = device->read(result.data() + readBytes, result.size() - readBytes); - if (readResult > 0) { - readBytes += readResult; - } - } while (readResult > 0); +bool readFromDevice(QIODevice* device, QByteArray& data, int size) +{ + QByteArray buffer; + buffer.resize(size); - if (readResult == -1) { - return false; - } else { - result.resize(static_cast(readBytes)); - data = result; - return true; - } + qint64 readResult = device->read(buffer.data(), size); + if (readResult == -1) { + return false; + } else { + buffer.resize(readResult); + data = buffer; + return true; } +} - QString imageReaderFilter() - { - const QList formats = QImageReader::supportedImageFormats(); - QStringList formatsStringList; - - for (const QByteArray& format : formats) { - for (int i = 0; i < format.size(); i++) { - if (!QChar(format.at(i)).isLetterOrNumber()) { - continue; - } - } - - formatsStringList.append("*." + QString::fromLatin1(format).toLower()); +bool readAllFromDevice(QIODevice* device, QByteArray& data) +{ + QByteArray result; + qint64 readBytes = 0; + qint64 readResult; + do { + result.resize(result.size() + 16384); + readResult = device->read(result.data() + readBytes, result.size() - readBytes); + if (readResult > 0) { + readBytes += readResult; } - - return formatsStringList.join(" "); } + while (readResult > 0); - bool isHex(const QByteArray& ba) - { - for (const unsigned char c : ba) { - if (!std::isxdigit(c)) { - return false; - } - } - + if (readResult == -1) { + return false; + } else { + result.resize(static_cast(readBytes)); + data = result; return true; } +} - bool isBase64(const QByteArray& ba) - { - constexpr auto pattern = R"(^(?:[a-z0-9+]{4})*(?:[a-z0-9+]{3}=|[a-z0-9+]{2}==)?$)"; - QRegExp regexp(pattern, Qt::CaseInsensitive, QRegExp::RegExp2); - - QString base64 = QString::fromLatin1(ba.constData(), ba.size()); - - return regexp.exactMatch(base64); - } - - void sleep(int ms) - { - Q_ASSERT(ms >= 0); +QString imageReaderFilter() +{ + const QList formats = QImageReader::supportedImageFormats(); + QStringList formatsStringList; - if (ms == 0) { - return; + for (const QByteArray& format : formats) { + for (int i = 0; i < format.size(); i++) { + if (!QChar(format.at(i)).isLetterOrNumber()) { + continue; + } } -#ifdef Q_OS_WIN - Sleep(uint(ms)); -#else - timespec ts; - ts.tv_sec = ms / 1000; - ts.tv_nsec = (ms % 1000) * 1000 * 1000; - nanosleep(&ts, nullptr); -#endif + formatsStringList.append("*." + QString::fromLatin1(format).toLower()); } - void wait(int ms) - { - Q_ASSERT(ms >= 0); + return formatsStringList.join(" "); +} - if (ms == 0) { - return; - } - - QElapsedTimer timer; - timer.start(); - - if (ms <= 50) { - QCoreApplication::processEvents(QEventLoop::AllEvents, ms); - sleep(qMax(ms - static_cast(timer.elapsed()), 0)); - } else { - int timeLeft; - do { - timeLeft = ms - timer.elapsed(); - if (timeLeft > 0) { - QCoreApplication::processEvents(QEventLoop::AllEvents, timeLeft); - sleep(10); - } - } while (!timer.hasExpired(ms)); +bool isHex(const QByteArray& ba) +{ + for (const unsigned char c : ba) { + if (!std::isxdigit(c)) { + return false; } } - void disableCoreDumps() - { - // default to true - // there is no point in printing a warning if this is not implemented on the platform - bool success = true; + return true; +} -#if defined(HAVE_RLIMIT_CORE) - struct rlimit limit; - limit.rlim_cur = 0; - limit.rlim_max = 0; - success = success && (setrlimit(RLIMIT_CORE, &limit) == 0); -#endif +bool isBase64(const QByteArray& ba) +{ + constexpr auto pattern = R"(^(?:[a-z0-9+]{4})*(?:[a-z0-9+]{3}=|[a-z0-9+]{2}==)?$)"; + QRegExp regexp(pattern, Qt::CaseInsensitive, QRegExp::RegExp2); -#if defined(HAVE_PR_SET_DUMPABLE) - success = success && (prctl(PR_SET_DUMPABLE, 0) == 0); -#endif + QString base64 = QString::fromLatin1(ba.constData(), ba.size()); -// Mac OS X -#ifdef HAVE_PT_DENY_ATTACH - success = success && (ptrace(PT_DENY_ATTACH, 0, 0, 0) == 0); -#endif + return regexp.exactMatch(base64); +} -#ifdef Q_OS_WIN - success = success && createWindowsDACL(); -#endif +void sleep(int ms) +{ + Q_ASSERT(ms >= 0); - if (!success) { - qWarning("Unable to disable core dumps."); - } + if (ms == 0) { + return; } - void setupSearchPaths() - { #ifdef Q_OS_WIN - // Make sure Windows doesn't load DLLs from the current working directory - SetDllDirectoryA(""); - SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE); + Sleep(uint(ms)); +#else + timespec ts; + ts.tv_sec = ms/1000; + ts.tv_nsec = (ms%1000)*1000*1000; + nanosleep(&ts, nullptr); #endif - } - - // - // This function grants the user associated with the process token minimal access rights and - // denies everything else on Windows. This includes PROCESS_QUERY_INFORMATION and - // PROCESS_VM_READ access rights that are required for MiniDumpWriteDump() or ReadProcessMemory(). - // We do this using a discretionary access control list (DACL). Effectively this prevents - // crash dumps and disallows other processes from accessing our memory. This works as long - // as you do not have admin privileges, since then you are able to grant yourself the - // SeDebugPrivilege or SeTakeOwnershipPrivilege and circumvent the DACL. - // - bool createWindowsDACL() - { - bool bSuccess = false; - -#ifdef Q_OS_WIN - // Process token and user - HANDLE hToken = nullptr; - PTOKEN_USER pTokenUser = nullptr; - DWORD cbBufferSize = 0; - - // Access control list - PACL pACL = nullptr; - DWORD cbACL = 0; - - // Open the access token associated with the calling process - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { - goto Cleanup; - } - - // Retrieve the token information in a TOKEN_USER structure - GetTokenInformation(hToken, TokenUser, nullptr, 0, &cbBufferSize); - - pTokenUser = static_cast(HeapAlloc(GetProcessHeap(), 0, cbBufferSize)); - if (pTokenUser == nullptr) { - goto Cleanup; - } - - if (!GetTokenInformation(hToken, TokenUser, pTokenUser, cbBufferSize, &cbBufferSize)) { - goto Cleanup; - } - - if (!IsValidSid(pTokenUser->User.Sid)) { - 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); - - // Create and initialize an ACL - pACL = static_cast(HeapAlloc(GetProcessHeap(), 0, cbACL)); - if (pACL == nullptr) { - goto Cleanup; - } +void wait(int ms) +{ + Q_ASSERT(ms >= 0); - if (!InitializeAcl(pACL, cbACL, ACL_REVISION)) { - goto Cleanup; - } + if (ms == 0) { + return; + } - // Add allowed access control entries, everything else is denied - if (!AddAccessAllowedAce( - pACL, - ACL_REVISION, - SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE, // same as protected process - pTokenUser->User.Sid // pointer to the trustee's SID - )) { - goto Cleanup; - } + QElapsedTimer timer; + timer.start(); - // Set discretionary access control list - bSuccess = ERROR_SUCCESS - == SetSecurityInfo(GetCurrentProcess(), // object handle - SE_KERNEL_OBJECT, // type of object - DACL_SECURITY_INFORMATION, // change only the objects DACL - nullptr, - nullptr, // do not change owner or group - pACL, // DACL specified - nullptr // do not change SACL - ); - - Cleanup: - - if (pACL != nullptr) { - HeapFree(GetProcessHeap(), 0, pACL); - } - if (pTokenUser != nullptr) { - HeapFree(GetProcessHeap(), 0, pTokenUser); - } - if (hToken != nullptr) { - CloseHandle(hToken); + if (ms <= 50) { + QCoreApplication::processEvents(QEventLoop::AllEvents, ms); + sleep(qMax(ms - static_cast(timer.elapsed()), 0)); + } else { + int timeLeft; + do { + timeLeft = ms - timer.elapsed(); + if (timeLeft > 0) { + QCoreApplication::processEvents(QEventLoop::AllEvents, timeLeft); + sleep(10); + } } -#endif - - return bSuccess; + while (!timer.hasExpired(ms)); } +} } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 4f75b750bc..aec9373047 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -30,31 +30,27 @@ class QIODevice; namespace Tools { +QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); +bool hasChild(const QObject* parent, const QObject* child); +bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); +bool readAllFromDevice(QIODevice* device, QByteArray& data); +QString imageReaderFilter(); +bool isHex(const QByteArray& ba); +bool isBase64(const QByteArray& ba); +void sleep(int ms); +void wait(int ms); + +template +RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) +{ + RandomAccessIterator it = std::lower_bound(begin, end, value); - QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); - bool hasChild(const QObject* parent, const QObject* child); - bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); - bool readAllFromDevice(QIODevice* device, QByteArray& data); - QString imageReaderFilter(); - bool isHex(const QByteArray& ba); - bool isBase64(const QByteArray& ba); - void sleep(int ms); - void wait(int ms); - void disableCoreDumps(); - void setupSearchPaths(); - bool createWindowsDACL(); - - template - RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) - { - RandomAccessIterator it = std::lower_bound(begin, end, value); - - if ((it == end) || (value < *it)) { - return end; - } else { - return it; - } + if ((it == end) || (value < *it)) { + return end; + } else { + return it; } +} } // namespace Tools diff --git a/src/crypto/CryptoHash.cpp b/src/crypto/CryptoHash.cpp index 12c6bf7910..3c5e4c2840 100644 --- a/src/crypto/CryptoHash.cpp +++ b/src/crypto/CryptoHash.cpp @@ -98,13 +98,6 @@ void CryptoHash::setKey(const QByteArray& data) Q_ASSERT(error == 0); } -void CryptoHash::reset() -{ - Q_D(CryptoHash); - - gcry_md_reset(d->ctx); -} - QByteArray CryptoHash::result() const { Q_D(const CryptoHash); diff --git a/src/crypto/CryptoHash.h b/src/crypto/CryptoHash.h index bd312121ab..02f90eb4de 100644 --- a/src/crypto/CryptoHash.h +++ b/src/crypto/CryptoHash.h @@ -34,7 +34,6 @@ class CryptoHash explicit CryptoHash(Algorithm algo, bool hmac = false); ~CryptoHash(); void addData(const QByteArray& data); - void reset(); QByteArray result() const; void setKey(const QByteArray& data); diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index 0467ad7c2f..828d3a998a 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -94,7 +94,7 @@ QString SymmetricCipher::errorString() const SymmetricCipher::Algorithm SymmetricCipher::cipherToAlgorithm(const QUuid& cipher) { - if (cipher == KeePass2::CIPHER_AES) { + if (cipher == KeePass2::CIPHER_AES256) { return Aes256; } else if (cipher == KeePass2::CIPHER_CHACHA20) { return ChaCha20; @@ -109,15 +109,17 @@ SymmetricCipher::Algorithm SymmetricCipher::cipherToAlgorithm(const QUuid& ciphe QUuid SymmetricCipher::algorithmToCipher(Algorithm algo) { switch (algo) { + case Aes128: + return KeePass2::CIPHER_AES128; case Aes256: - return KeePass2::CIPHER_AES; + return KeePass2::CIPHER_AES256; case ChaCha20: return KeePass2::CIPHER_CHACHA20; case Twofish: return KeePass2::CIPHER_TWOFISH; default: qWarning("SymmetricCipher::algorithmToCipher: invalid algorithm %d", algo); - return QUuid(); + return {}; } } diff --git a/src/crypto/SymmetricCipherGcrypt.cpp b/src/crypto/SymmetricCipherGcrypt.cpp index c7a5e6a071..e3bc88cbf0 100644 --- a/src/crypto/SymmetricCipherGcrypt.cpp +++ b/src/crypto/SymmetricCipherGcrypt.cpp @@ -185,8 +185,6 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data) bool SymmetricCipherGcrypt::processInPlace(QByteArray& data, quint64 rounds) { - // TODO: check block size - gcry_error_t error; char* rawData = data.data(); diff --git a/src/format/Kdbx3Reader.cpp b/src/format/Kdbx3Reader.cpp index aeacaad3de..0114a0b767 100644 --- a/src/format/Kdbx3Reader.cpp +++ b/src/format/Kdbx3Reader.cpp @@ -110,14 +110,6 @@ Database* Kdbx3Reader::readDatabaseImpl(QIODevice* device, return nullptr; } - QBuffer buffer; - if (saveXml()) { - m_xmlData = xmlDevice->readAll(); - buffer.setBuffer(&m_xmlData); - buffer.open(QIODevice::ReadOnly); - xmlDevice = &buffer; - } - Q_ASSERT(xmlDevice); KdbxXmlReader xmlReader(KeePass2::FILE_VERSION_3_1); diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp index 7b94d34f81..5a024a254a 100644 --- a/src/format/Kdbx4Reader.cpp +++ b/src/format/Kdbx4Reader.cpp @@ -124,14 +124,6 @@ Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device, return nullptr; } - QBuffer buffer; - if (saveXml()) { - m_xmlData = xmlDevice->readAll(); - buffer.setBuffer(&m_xmlData); - buffer.open(QIODevice::ReadOnly); - xmlDevice = &buffer; - } - Q_ASSERT(xmlDevice); KdbxXmlReader xmlReader(KeePass2::FILE_VERSION_4, binaryPool()); diff --git a/src/format/KdbxReader.cpp b/src/format/KdbxReader.cpp index 5e14b8a9f1..13a792fd5d 100644 --- a/src/format/KdbxReader.cpp +++ b/src/format/KdbxReader.cpp @@ -1,3 +1,5 @@ +#include + /* * Copyright (C) 2018 KeePassXC Team * @@ -18,6 +20,9 @@ #include "KdbxReader.h" #include "core/Database.h" #include "core/Endian.h" +#include "format/KdbxXmlWriter.h" + +#include #define UUID_LENGTH 16 @@ -92,7 +97,14 @@ Database* KdbxReader::readDatabase(QIODevice* device, QSharedPointer m_db; QPair m_kdbxSignature; diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp index 76fa032217..c9e5c31af9 100644 --- a/src/format/KdbxXmlReader.cpp +++ b/src/format/KdbxXmlReader.cpp @@ -82,7 +82,6 @@ Database* KdbxXmlReader::readDatabase(QIODevice* device) * @param db database to read into * @param randomStream random stream to use for decryption */ -#include "QDebug" void KdbxXmlReader::readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream) { m_error = false; diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp index 5ad1e34aea..c26d316dc3 100644 --- a/src/format/KdbxXmlWriter.cpp +++ b/src/format/KdbxXmlWriter.cpp @@ -358,10 +358,10 @@ void KdbxXmlWriter::writeEntry(const Entry* entry) bool protect = (((key == "Title") && m_meta->protectTitle()) || ((key == "UserName") && m_meta->protectUsername()) - || ((key == "Password") && m_meta->protectPassword()) - || ((key == "URL") && m_meta->protectUrl()) - || ((key == "Notes") && m_meta->protectNotes()) - || entry->attributes()->isProtected(key)); + || ((key == "Password") && m_meta->protectPassword()) + || ((key == "URL") && m_meta->protectUrl()) + || ((key == "Notes") && m_meta->protectNotes()) + || entry->attributes()->isProtected(key)); writeString("Key", key); @@ -369,7 +369,7 @@ void KdbxXmlWriter::writeEntry(const Entry* entry) QString value; if (protect) { - if (m_randomStream) { + if (!m_innerStreamProtectionDisabled && m_randomStream) { m_xml.writeAttribute("Protected", "True"); bool ok; QByteArray rawData = m_randomStream->process(entry->attributes()->value(key).toUtf8(), &ok); @@ -596,3 +596,24 @@ void KdbxXmlWriter::raiseError(const QString& errorMessage) m_error = true; m_errorStr = errorMessage; } + +/** + * Disable inner stream protection and write protected fields + * in plaintext instead. This is useful for plaintext XML exports + * where the inner stream key is not available. + * + * @param disable true to disable protection + */ +void KdbxXmlWriter::disableInnerStreamProtection(bool disable) +{ + m_innerStreamProtectionDisabled = disable; +} + +/** + * @return true if inner stream protection is disabled and protected + * fields will be saved in plaintext + */ +bool KdbxXmlWriter::innerStreamProtectionDisabled() const +{ + return m_innerStreamProtectionDisabled; +} diff --git a/src/format/KdbxXmlWriter.h b/src/format/KdbxXmlWriter.h index 51a8034979..2f7215b46a 100644 --- a/src/format/KdbxXmlWriter.h +++ b/src/format/KdbxXmlWriter.h @@ -41,6 +41,8 @@ class KdbxXmlWriter KeePass2RandomStream* randomStream = nullptr, const QByteArray& headerHash = QByteArray()); void writeDatabase(const QString& filename, Database* db); + void disableInnerStreamProtection(bool disable); + bool innerStreamProtectionDisabled() const; bool hasError(); QString errorString(); @@ -81,6 +83,8 @@ class KdbxXmlWriter const quint32 m_kdbxVersion; + bool m_innerStreamProtectionDisabled = false; + QXmlStreamWriter m_xml; QPointer m_db; QPointer m_meta; diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp index 5aad1f7f21..9c07144844 100644 --- a/src/format/KeePass2.cpp +++ b/src/format/KeePass2.cpp @@ -23,7 +23,8 @@ #define UUID_LENGTH 16 -const QUuid KeePass2::CIPHER_AES = QUuid("31c1f2e6-bf71-4350-be58-05216afc5aff"); +const QUuid KeePass2::CIPHER_AES128 = QUuid("61ab05a1-9464-41c3-8d74-3a563df8dd35"); +const QUuid KeePass2::CIPHER_AES256 = QUuid("31c1f2e6-bf71-4350-be58-05216afc5aff"); const QUuid KeePass2::CIPHER_TWOFISH = QUuid("ad68f29f-576f-4bb9-a36a-d47af965346c"); const QUuid KeePass2::CIPHER_CHACHA20 = QUuid("d6038a2b-8b6f-4cb5-a524-339a31dbb59a"); @@ -47,7 +48,7 @@ const QString KeePass2::KDFPARAM_ARGON2_SECRET("K"); const QString KeePass2::KDFPARAM_ARGON2_ASSOCDATA("A"); const QList> KeePass2::CIPHERS{ - qMakePair(KeePass2::CIPHER_AES, QObject::tr("AES: 256-bit")), + qMakePair(KeePass2::CIPHER_AES256, QObject::tr("AES: 256-bit")), qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish: 256-bit")), qMakePair(KeePass2::CIPHER_CHACHA20, QObject::tr("ChaCha20: 256-bit")) }; diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h index a8e97c5bdf..02fe635ca0 100644 --- a/src/format/KeePass2.h +++ b/src/format/KeePass2.h @@ -46,7 +46,8 @@ namespace KeePass2 const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian; -extern const QUuid CIPHER_AES; +extern const QUuid CIPHER_AES128; +extern const QUuid CIPHER_AES256; extern const QUuid CIPHER_TWOFISH; extern const QUuid CIPHER_CHACHA20; diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index 2a6d611182..8cdb8ff430 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -93,7 +93,7 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, QSharedPointersetSaveXml(m_saveXml); - return m_reader->readDatabase(device, key, keepDatabase); + return m_reader->readDatabase(device, std::move(key), keepDatabase); } bool KeePass2Reader::hasError() const diff --git a/src/gui/Application.h b/src/gui/Application.h index a6c6fdf905..3fdd8af90e 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -22,8 +22,8 @@ #include #include -class QLockFile; +class QLockFile; class QSocketNotifier; class Application : public QApplication diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 896703eb68..399c9ec82a 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -210,7 +210,7 @@ private slots: void setIconFromParent(); void replaceDatabase(Database* db); - Database* m_db; + QPointer m_db; QWidget* m_mainWidget; EditEntryWidget* m_editEntryWidget; EditEntryWidget* m_historyEditEntryWidget; diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp index 2a3cf7cbbb..63a1ccef8f 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp @@ -81,7 +81,7 @@ void DatabaseSettingsWidgetEncryption::initialize() } if (!m_db->key()) { m_db->setKey(QSharedPointer::create()); - m_db->setCipher(KeePass2::CIPHER_AES); + m_db->setCipher(KeePass2::CIPHER_AES256); isDirty = true; } diff --git a/src/main.cpp b/src/main.cpp index 687988762c..7b7ac5e414 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,19 +16,18 @@ * along with this program. If not, see . */ -#include #include #include +#include #include "config-keepassx.h" -#include "core/Config.h" +#include "core/Bootstrap.h" #include "core/Tools.h" -#include "core/Translator.h" +#include "core/Config.h" #include "crypto/Crypto.h" #include "gui/Application.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" - #include "cli/Utils.h" #if defined(WITH_ASAN) && defined(WITH_LSAN) @@ -45,55 +44,29 @@ Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) #endif #endif -static inline void earlyQNetworkAccessManagerWorkaround() -{ - // When QNetworkAccessManager is instantiated it regularly starts polling - // all network interfaces to see if anything changes and if so, what. This - // creates a latency spike every 10 seconds on Mac OS 10.12+ and Windows 7 >= - // when on a wifi connection. - // So here we disable it for lack of better measure. - // This will also cause this message: QObject::startTimer: Timers cannot - // have negative intervals - // For more info see: - // - https://bugreports.qt.io/browse/QTBUG-40332 - // - https://bugreports.qt.io/browse/QTBUG-46015 - qputenv("QT_BEARER_POLL_TIMEOUT", QByteArray::number(-1)); -} - int main(int argc, char** argv) { -#ifdef QT_NO_DEBUG - Tools::disableCoreDumps(); -#endif - Tools::setupSearchPaths(); - - earlyQNetworkAccessManagerWorkaround(); - Application app(argc, argv); Application::setApplicationName("keepassxc"); Application::setApplicationVersion(KEEPASSX_VERSION); // don't set organizationName as that changes the return value of // QStandardPaths::writableLocation(QDesktopServices::DataLocation) + Bootstrap::bootstrapApplication(); QCommandLineParser parser; - parser.setApplicationDescription( - QCoreApplication::translate("main", "KeePassXC - cross-platform password manager")); - parser.addPositionalArgument( - "filename", - QCoreApplication::translate("main", "filenames of the password databases to open (*.kdbx)"), - "[filename(s)]"); + parser.setApplicationDescription(QCoreApplication::translate("main", "KeePassXC - cross-platform password manager")); + parser.addPositionalArgument("filename", + QCoreApplication::translate("main", "filenames of the password databases to open (*.kdbx)"), "[filename(s)]"); QCommandLineOption configOption( "config", QCoreApplication::translate("main", "path to a custom config file"), "config"); QCommandLineOption keyfileOption( "keyfile", QCoreApplication::translate("main", "key file of the database"), "keyfile"); QCommandLineOption pwstdinOption("pw-stdin", - QCoreApplication::translate("main", "read password of the database from stdin")); + QCoreApplication::translate("main", "read password of the database from stdin")); // This is needed under Windows where clients send --parent-window parameter with Native Messaging connect method - QCommandLineOption parentWindowOption(QStringList() << "pw" - << "parent-window", - QCoreApplication::translate("main", "Parent window handle"), - "handle"); + QCommandLineOption parentWindowOption( + QStringList() << "pw" << "parent-window", QCoreApplication::translate("main", "Parent window handle"), "handle"); parser.addHelpOption(); QCommandLineOption versionOption = parser.addVersionOption(); @@ -109,9 +82,7 @@ int main(int argc, char** argv) if (!fileNames.isEmpty()) { app.sendFileNamesToRunningInstance(fileNames); } - qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.") - .toUtf8() - .constData(); + qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.").toUtf8().constData(); return 0; } @@ -129,46 +100,14 @@ int main(int argc, char** argv) Config::createConfigFromFile(parser.value(configOption)); } - Translator::installTranslators(); - -#ifdef Q_OS_MAC - // Don't show menu icons on OSX - QApplication::setAttribute(Qt::AA_DontShowIconsInMenus); -#endif - MainWindow mainWindow; app.setMainWindow(&mainWindow); - QObject::connect(&app, SIGNAL(anotherInstanceStarted()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(openFile(QString)), &mainWindow, SLOT(openDatabase(QString))); QObject::connect(&app, SIGNAL(quitSignalReceived()), &mainWindow, SLOT(appExit()), Qt::DirectConnection); - // start minimized if configured - bool minimizeOnStartup = config()->get("GUI/MinimizeOnStartup").toBool(); - bool minimizeToTray = config()->get("GUI/MinimizeToTray").toBool(); -#ifndef Q_OS_LINUX - if (minimizeOnStartup) { -#else - // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at - // the same time (which would happen if both minimize on startup and minimize to tray are set) - // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. - if (minimizeOnStartup && !minimizeToTray) { -#endif - mainWindow.setWindowState(Qt::WindowMinimized); - } - if (!(minimizeOnStartup && minimizeToTray)) { - mainWindow.show(); - } - - if (config()->get("OpenPreviousDatabasesOnStartup").toBool()) { - const QStringList fileNames = config()->get("LastOpenedDatabases").toStringList(); - for (const QString& filename : fileNames) { - if (!filename.isEmpty() && QFile::exists(filename)) { - mainWindow.openDatabase(filename); - } - } - } + Bootstrap::restoreMainWindowState(mainWindow); const bool pwstdin = parser.isSet(pwstdinOption); for (const QString& filename : fileNames) { @@ -187,7 +126,7 @@ int main(int argc, char** argv) } } - int exitCode = app.exec(); + int exitCode = Application::exec(); #if defined(WITH_ASAN) && defined(WITH_LSAN) // do leak check here to prevent massive tail of end-of-process leak errors from third-party libraries diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 73262bae07..562b45e4d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -163,7 +163,10 @@ add_unit_test(NAME testentry SOURCES TestEntry.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testmerge SOURCES TestMerge.cpp - LIBS testsupport ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) + +add_unit_test(NAME testpasswordgenerator SOURCES TestPasswordGenerator.cpp + LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testtotp SOURCES TestTotp.cpp LIBS ${TEST_LIBRARIES}) @@ -180,7 +183,7 @@ add_unit_test(NAME testrandom SOURCES TestRandom.cpp add_unit_test(NAME testentrysearcher SOURCES TestEntrySearcher.cpp LIBS ${TEST_LIBRARIES}) -add_unit_test(NAME testcsvexporter SOURCES TestCsvExporter.cpp +add_unit_test(NAME testcsveporter SOURCES TestCsvExporter.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testykchallengeresponsekey @@ -193,6 +196,11 @@ add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp add_unit_test(NAME testtools SOURCES TestTools.cpp LIBS ${TEST_LIBRARIES}) + if(WITH_GUI_TESTS) + # CLI clip tests need X environment on Linux + add_unit_test(NAME testcli SOURCES TestCli.cpp + LIBS testsupport cli ${TEST_LIBRARIES}) + add_subdirectory(gui) endif(WITH_GUI_TESTS) diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp new file mode 100644 index 0000000000..02bc0ba3f5 --- /dev/null +++ b/tests/TestCli.cpp @@ -0,0 +1,756 @@ +/* + * 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 "TestCli.h" +#include "config-keepassx-tests.h" +#include "core/Global.h" +#include "core/Config.h" +#include "core/Bootstrap.h" +#include "core/Tools.h" +#include "core/PasswordGenerator.h" +#include "crypto/Crypto.h" +#include "format/KeePass2.h" +#include "format/Kdbx3Reader.h" +#include "format/Kdbx4Reader.h" +#include "format/Kdbx4Writer.h" +#include "format/Kdbx3Writer.h" +#include "format/KdbxXmlReader.h" + +#include "cli/Command.h" +#include "cli/Utils.h" +#include "cli/Add.h" +#include "cli/Clip.h" +#include "cli/Diceware.h" +#include "cli/Edit.h" +#include "cli/Estimate.h" +#include "cli/Extract.h" +#include "cli/Generate.h" +#include "cli/List.h" +#include "cli/Locate.h" +#include "cli/Merge.h" +#include "cli/Remove.h" +#include "cli/Show.h" + +#include +#include +#include +#include +#include + +#include + +QTEST_MAIN(TestCli) + +void TestCli::initTestCase() +{ + QVERIFY(Crypto::init()); + + Config::createTempFileInstance(); + Bootstrap::bootstrapApplication(); + + // Load the NewDatabase.kdbx file into temporary storage + QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx")); + QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); + sourceDbFile.close(); +} + +void TestCli::init() +{ + m_dbFile.reset(new QTemporaryFile()); + m_dbFile->open(); + m_dbFile->write(m_dbData); + m_dbFile->flush(); + + m_stdinFile.reset(new QTemporaryFile()); + m_stdinFile->open(); + m_stdinHandle = fdopen(m_stdinFile->handle(), "r+"); + Utils::STDIN = m_stdinHandle; + + m_stdoutFile.reset(new QTemporaryFile()); + m_stdoutFile->open(); + m_stdoutHandle = fdopen(m_stdoutFile->handle(), "r+"); + Utils::STDOUT = m_stdoutHandle; + + m_stderrFile.reset(new QTemporaryFile()); + m_stderrFile->open(); + m_stderrHandle = fdopen(m_stderrFile->handle(), "r+"); + Utils::STDERR = m_stderrHandle; +} + +void TestCli::cleanup() +{ + m_dbFile.reset(); + + m_stdinFile.reset(); + m_stdinHandle = stdin; + Utils::STDIN = stdin; + + m_stdoutFile.reset(); + Utils::STDOUT = stdout; + m_stdoutHandle = stdout; + + m_stderrFile.reset(); + m_stderrHandle = stderr; + Utils::STDERR = stderr; +} + +void TestCli::cleanupTestCase() +{ +} + +QSharedPointer TestCli::readTestDatabase() const +{ + Utils::setNextPassword("a"); + auto db = QSharedPointer(Database::unlockFromStdin(m_dbFile->fileName(), "", m_stdoutHandle)); + m_stdoutFile->seek(ftell(m_stdoutHandle)); // re-synchronize handles + return db; +} + +void TestCli::testCommand() +{ + QCOMPARE(Command::getCommands().size(), 12); + QVERIFY(Command::getCommand("add")); + QVERIFY(Command::getCommand("clip")); + QVERIFY(Command::getCommand("diceware")); + QVERIFY(Command::getCommand("edit")); + QVERIFY(Command::getCommand("estimate")); + QVERIFY(Command::getCommand("extract")); + QVERIFY(Command::getCommand("generate")); + QVERIFY(Command::getCommand("locate")); + QVERIFY(Command::getCommand("ls")); + QVERIFY(Command::getCommand("merge")); + QVERIFY(Command::getCommand("rm")); + QVERIFY(Command::getCommand("show")); + QVERIFY(!Command::getCommand("doesnotexist")); +} + +void TestCli::testAdd() +{ + Add addCmd; + QVERIFY(!addCmd.name.isEmpty()); + QVERIFY(addCmd.getDescriptionLine().contains(addCmd.name)); + + Utils::setNextPassword("a"); + addCmd.execute({"add", "-u", "newuser", "--url", "https://example.com/", "-g", "-l", "20", m_dbFile->fileName(), "/newuser-entry"}); + + auto db = readTestDatabase(); + auto* entry = db->rootGroup()->findEntryByPath("/newuser-entry"); + QVERIFY(entry); + QCOMPARE(entry->username(), QString("newuser")); + QCOMPARE(entry->url(), QString("https://example.com/")); + QCOMPARE(entry->password().size(), 20); + + Utils::setNextPassword("a"); + Utils::setNextPassword("newpassword"); + addCmd.execute({"add", "-u", "newuser2", "--url", "https://example.net/", "-g", "-l", "20", "-p", m_dbFile->fileName(), "/newuser-entry2"}); + + db = readTestDatabase(); + entry = db->rootGroup()->findEntryByPath("/newuser-entry2"); + QVERIFY(entry); + QCOMPARE(entry->username(), QString("newuser2")); + QCOMPARE(entry->url(), QString("https://example.net/")); + QCOMPARE(entry->password(), QString("newpassword")); +} + +void TestCli::testClip() +{ + QClipboard* clipboard = QGuiApplication::clipboard(); + clipboard->clear(); + + Clip clipCmd; + QVERIFY(!clipCmd.name.isEmpty()); + QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name)); + + Utils::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry"}); + + m_stderrFile->reset(); + QString errorOutput(m_stderrFile->readAll()); + + if (errorOutput.contains("Unable to start program") + || errorOutput.contains("No program defined for clipboard manipulation")) { + QSKIP("Clip test skipped due to missing clipboard tool"); + } + + QCOMPARE(clipboard->text(), QString("Password")); + + Utils::setNextPassword("a"); + QFuture future = QtConcurrent::run(&clipCmd, &Clip::execute, QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"}); + + QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString("Password"), 500); + QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 1500); + + future.waitForFinished(); +} + +void TestCli::testDiceware() +{ + Diceware dicewareCmd; + QVERIFY(!dicewareCmd.name.isEmpty()); + QVERIFY(dicewareCmd.getDescriptionLine().contains(dicewareCmd.name)); + + dicewareCmd.execute({"diceware"}); + m_stdoutFile->reset(); + QString passphrase(m_stdoutFile->readLine()); + QVERIFY(!passphrase.isEmpty()); + + dicewareCmd.execute({"diceware", "-W", "2"}); + m_stdoutFile->seek(passphrase.toLatin1().size()); + passphrase = m_stdoutFile->readLine(); + QCOMPARE(passphrase.split(" ").size(), 2); + + auto pos = m_stdoutFile->pos(); + dicewareCmd.execute({"diceware", "-W", "10"}); + m_stdoutFile->seek(pos); + passphrase = m_stdoutFile->readLine(); + QCOMPARE(passphrase.split(" ").size(), 10); + + QTemporaryFile wordFile; + wordFile.open(); + for (int i = 0; i < 4500; ++i) { + wordFile.write(QString("word" + QString::number(i) + "\n").toLatin1()); + } + wordFile.close(); + + pos = m_stdoutFile->pos(); + dicewareCmd.execute({"diceware", "-W", "11", "-w", wordFile.fileName()}); + m_stdoutFile->seek(pos); + passphrase = m_stdoutFile->readLine(); + const auto words = passphrase.split(" "); + QCOMPARE(words.size(), 11); + QRegularExpression regex("^word\\d+$"); + for (const auto& word: words) { + QVERIFY2(regex.match(word).hasMatch(), qPrintable("Word " + word + " was not on the word list")); + } +} + +void TestCli::testEdit() +{ + Edit editCmd; + QVERIFY(!editCmd.name.isEmpty()); + QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name)); + + Utils::setNextPassword("a"); + editCmd.execute({"edit", "-u", "newuser", "--url", "https://otherurl.example.com/", "-t", "newtitle", m_dbFile->fileName(), "/Sample Entry"}); + + auto db = readTestDatabase(); + auto* entry = db->rootGroup()->findEntryByPath("/newtitle"); + QVERIFY(entry); + QCOMPARE(entry->username(), QString("newuser")); + QCOMPARE(entry->url(), QString("https://otherurl.example.com/")); + QCOMPARE(entry->password(), QString("Password")); + + Utils::setNextPassword("a"); + editCmd.execute({"edit", "-g", m_dbFile->fileName(), "/newtitle"}); + db = readTestDatabase(); + entry = db->rootGroup()->findEntryByPath("/newtitle"); + QVERIFY(entry); + QCOMPARE(entry->username(), QString("newuser")); + QCOMPARE(entry->url(), QString("https://otherurl.example.com/")); + QVERIFY(!entry->password().isEmpty()); + QVERIFY(entry->password() != QString("Password")); + + Utils::setNextPassword("a"); + editCmd.execute({"edit", "-g", "-l", "34", "-t", "yet another title", m_dbFile->fileName(), "/newtitle"}); + db = readTestDatabase(); + entry = db->rootGroup()->findEntryByPath("/yet another title"); + QVERIFY(entry); + QCOMPARE(entry->username(), QString("newuser")); + QCOMPARE(entry->url(), QString("https://otherurl.example.com/")); + QVERIFY(entry->password() != QString("Password")); + QCOMPARE(entry->password().size(), 34); + + Utils::setNextPassword("a"); + Utils::setNextPassword("newpassword"); + editCmd.execute({"edit", "-p", m_dbFile->fileName(), "/yet another title"}); + db = readTestDatabase(); + entry = db->rootGroup()->findEntryByPath("/yet another title"); + QVERIFY(entry); + QCOMPARE(entry->password(), QString("newpassword")); +} + +void TestCli::testEstimate_data() +{ + QTest::addColumn("input"); + QTest::addColumn("length"); + QTest::addColumn("entropy"); + QTest::addColumn("log10"); + QTest::addColumn("searchStrings"); + + QTest::newRow("Dictionary") + << "password" << "8" << "1.0" << "0.3" + << QStringList{"Type: Dictionary", "\tpassword"}; + + QTest::newRow("Spatial") + << "zxcv" << "4" << "10.3" << "3.1" + << QStringList{"Type: Spatial", "\tzxcv"}; + + QTest::newRow("Spatial(Rep)") + << "sdfgsdfg" << "8" << "11.3" << "3.4" + << QStringList{"Type: Spatial(Rep)", "\tsdfgsdfg"}; + + QTest::newRow("Dictionary / Sequence") + << "password123" << "11" << "4.5" << "1.3" + << QStringList{"Type: Dictionary", "Type: Sequence", "\tpassword", "\t123"}; + + QTest::newRow("Dict+Leet") + << "p455w0rd" << "8" << "2.5" << "0.7" + << QStringList{"Type: Dict+Leet", "\tp455w0rd"}; + + QTest::newRow("Dictionary(Rep)") + << "hellohello" << "10" << "7.3" << "2.2" + << QStringList{"Type: Dictionary(Rep)", "\thellohello"}; + + QTest::newRow("Sequence(Rep) / Dictionary") + << "456456foobar" << "12" << "16.7" << "5.0" + << QStringList{"Type: Sequence(Rep)", "Type: Dictionary", "\t456456", "\tfoobar"}; + + QTest::newRow("Bruteforce(Rep) / Bruteforce") + << "xzxzy" << "5" << "16.1" << "4.8" + << QStringList{"Type: Bruteforce(Rep)", "Type: Bruteforce", "\txzxz", "\ty"}; + + QTest::newRow("Dictionary / Date(Rep)") + << "pass20182018" << "12" << "15.1" << "4.56" + << QStringList{"Type: Dictionary", "Type: Date(Rep)", "\tpass", "\t20182018"}; + + QTest::newRow("Dictionary / Date / Bruteforce") + << "mypass2018-2" << "12" << "32.9" << "9.9" + << QStringList{"Type: Dictionary", "Type: Date", "Type: Bruteforce", "\tmypass", "\t2018", "\t-2"}; + + QTest::newRow("Strong Password") + << "E*!%.Qw{t.X,&bafw)\"Q!ah$%;U/" << "28" << "165.7" << "49.8" + << QStringList{"Type: Bruteforce", "\tE*"}; + + // TODO: detect passphrases and adjust entropy calculation accordingly (issue #2347) + QTest::newRow("Strong Passphrase") + << "squint wooing resupply dangle isolation axis headsman" << "53" << "151.2" << "45.5" + << QStringList{"Type: Dictionary", "Type: Bruteforce", "Multi-word extra bits 22.0", "\tsquint", "\t ", "\twooing"}; +} + +void TestCli::testEstimate() +{ + QFETCH(QString, input); + QFETCH(QString, length); + QFETCH(QString, entropy); + QFETCH(QString, log10); + QFETCH(QStringList, searchStrings); + + Estimate estimateCmd; + QVERIFY(!estimateCmd.name.isEmpty()); + QVERIFY(estimateCmd.getDescriptionLine().contains(estimateCmd.name)); + + QTextStream in(m_stdinFile.data()); + QTextStream out(m_stdoutFile.data()); + + in << input << endl; + auto inEnd = in.pos(); + in.seek(0); + estimateCmd.execute({"estimate"}); + auto outEnd = out.pos(); + out.seek(0); + auto result = out.readAll(); + QVERIFY(result.startsWith("Length " + length)); + QVERIFY(result.contains("Entropy " + entropy)); + QVERIFY(result.contains("Log10 " + log10)); + + // seek to end of stream + in.seek(inEnd); + out.seek(outEnd); + + in << input << endl; + in.seek(inEnd); + estimateCmd.execute({"estimate", "-a"}); + out.seek(outEnd); + result = out.readAll(); + QVERIFY(result.startsWith("Length " + length)); + QVERIFY(result.contains("Entropy " + entropy)); + QVERIFY(result.contains("Log10 " + log10)); + for (const auto& string: asConst(searchStrings)) { + QVERIFY2(result.contains(string), qPrintable("String " + string + " missing")); + } +} + +void TestCli::testExtract() +{ + Extract extractCmd; + QVERIFY(!extractCmd.name.isEmpty()); + QVERIFY(extractCmd.getDescriptionLine().contains(extractCmd.name)); + + Utils::setNextPassword("a"); + extractCmd.execute({"extract", m_dbFile->fileName()}); + + m_stdoutFile->seek(0); + m_stdoutFile->readLine(); // skip prompt line + + KdbxXmlReader reader(KeePass2::FILE_VERSION_3_1); + QScopedPointer db(new Database()); + reader.readDatabase(m_stdoutFile.data(), db.data()); + QVERIFY(!reader.hasError()); + QVERIFY(db.data()); + auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry"); + QVERIFY(entry); + QCOMPARE(entry->password(), QString("Password")); +} + +void TestCli::testGenerate_data() +{ + QTest::addColumn("parameters"); + QTest::addColumn("pattern"); + + QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$"; + QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$"; + QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$"; + QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "-u"} << "^[A-Z]{15}$"; + QTest::newRow("numbers")<< QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$"; + QTest::newRow("special") + << QStringList{"generate", "-L", "200", "-s"} + << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)"; + QTest::newRow("special (exclude)") + << QStringList{"generate", "-L", "200", "-s" , "-x", "+.?@&"} + << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!-<=>#$%^`~]{200}$)"; + QTest::newRow("extended") + << QStringList{"generate", "-L", "50", "-e"} + << R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)"; + QTest::newRow("numbers + lowercase + uppercase") + << QStringList{"generate", "-L", "16", "-n", "-u", "-l"} + << "^[0-9a-zA-Z]{16}$"; + QTest::newRow("numbers + lowercase + uppercase (exclude)") + << QStringList{"generate", "-L", "500", "-n", "-u", "-l", "-x", "abcdefg0123@"} + << "^[^abcdefg0123@]{500}$"; + QTest::newRow("numbers + lowercase + uppercase (exclude similar)") + << QStringList{"generate", "-L", "200", "-n", "-u", "-l", "--exclude-similar"} + << "^[^l1IO0]{200}$"; + QTest::newRow("uppercase + lowercase (every)") + << QStringList{"generate", "-L", "2", "-u", "-l", "--every-group"} + << "^[a-z][A-Z]|[A-Z][a-z]$"; + QTest::newRow("numbers + lowercase (every)") + << QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} + << "^[a-z][0-9]|[0-9][a-z]$"; +} + +void TestCli::testGenerate() +{ + QFETCH(QStringList, parameters); + QFETCH(QString, pattern); + + Generate generateCmd; + QVERIFY(!generateCmd.name.isEmpty()); + QVERIFY(generateCmd.getDescriptionLine().contains(generateCmd.name)); + + qint64 pos = 0; + // run multiple times to make accidental passes unlikely + for (int i = 0; i < 10; ++i) { + generateCmd.execute(parameters); + m_stdoutFile->seek(pos); + QRegularExpression regex(pattern); + QString password = QString::fromUtf8(m_stdoutFile->readLine()); + pos = m_stdoutFile->pos(); + QVERIFY2(regex.match(password).hasMatch(), qPrintable("Password " + password + " does not match pattern " + pattern)); + } +} + +void TestCli::testList() +{ + List listCmd; + QVERIFY(!listCmd.name.isEmpty()); + QVERIFY(listCmd.getDescriptionLine().contains(listCmd.name)); + + Utils::setNextPassword("a"); + listCmd.execute({"ls", m_dbFile->fileName()}); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n" + "General/\n" + "Windows/\n" + "Network/\n" + "Internet/\n" + "eMail/\n" + "Homebanking/\n")); + + qint64 pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + listCmd.execute({"ls", "-R", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n" + "General/\n" + " [empty]\n" + "Windows/\n" + " [empty]\n" + "Network/\n" + " [empty]\n" + "Internet/\n" + " [empty]\n" + "eMail/\n" + " [empty]\n" + "Homebanking/\n" + " [empty]\n")); + + pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + listCmd.execute({"ls", m_dbFile->fileName(), "/General/"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("[empty]\n")); + + pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + listCmd.execute({"ls", m_dbFile->fileName(), "/DoesNotExist/"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->reset(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Cannot find group /DoesNotExist/.\n")); +} + +void TestCli::testLocate() +{ + Locate locateCmd; + QVERIFY(!locateCmd.name.isEmpty()); + QVERIFY(locateCmd.getDescriptionLine().contains(locateCmd.name)); + + Utils::setNextPassword("a"); + locateCmd.execute({"locate", m_dbFile->fileName(), "Sample"}); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n")); + + qint64 pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + locateCmd.execute({"locate", m_dbFile->fileName(), "Does Not Exist"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->reset(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("No results for that search term.\n")); + + // write a modified database + auto db = readTestDatabase(); + QVERIFY(db); + auto* group = db->rootGroup()->findGroupByPath("/General/"); + QVERIFY(group); + auto* entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setTitle("New Entry"); + group->addEntry(entry); + QTemporaryFile tmpFile; + tmpFile.open(); + Kdbx4Writer writer; + writer.writeDatabase(&tmpFile, db.data()); + tmpFile.close(); + + pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + locateCmd.execute({"locate", tmpFile.fileName(), "New"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("/General/New Entry\n")); + + pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + locateCmd.execute({"locate", tmpFile.fileName(), "Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n/General/New Entry\n")); +} + +void TestCli::testMerge() +{ + Merge mergeCmd; + QVERIFY(!mergeCmd.name.isEmpty()); + QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name)); + + Kdbx4Writer writer; + Kdbx4Reader reader; + + // load test database and save a copy + auto db = readTestDatabase(); + QVERIFY(db); + QTemporaryFile targetFile1; + targetFile1.open(); + writer.writeDatabase(&targetFile1, db.data()); + targetFile1.close(); + + // save another copy with a different password + QTemporaryFile targetFile2; + targetFile2.open(); + auto oldKey = db->key(); + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create("b")); + db->setKey(key); + writer.writeDatabase(&targetFile2, db.data()); + targetFile2.close(); + db->setKey(oldKey); + + // then add a new entry to the in-memory database and save another copy + auto* entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setTitle("Some Website"); + entry->setPassword("secretsecretsecret"); + auto* group = db->rootGroup()->findGroupByPath("/Internet/"); + QVERIFY(group); + group->addEntry(entry); + QTemporaryFile sourceFile; + sourceFile.open(); + writer.writeDatabase(&sourceFile, db.data()); + sourceFile.close(); + + qint64 pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + mergeCmd.execute({"merge", "-s", targetFile1.fileName(), sourceFile.fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully merged the database files.\n")); + + QFile readBack(targetFile1.fileName()); + readBack.open(QIODevice::ReadOnly); + QScopedPointer mergedDb(reader.readDatabase(&readBack, oldKey)); + readBack.close(); + QVERIFY(mergedDb); + auto* entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website"); + QVERIFY(entry1); + QCOMPARE(entry1->title(), QString("Some Website")); + QCOMPARE(entry1->password(), QString("secretsecretsecret")); + + // try again with different passwords for both files + pos = m_stdoutFile->pos(); + Utils::setNextPassword("b"); + Utils::setNextPassword("a"); + mergeCmd.execute({"merge", targetFile2.fileName(), sourceFile.fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); + m_stdoutFile->readLine(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully merged the database files.\n")); + + readBack.setFileName(targetFile2.fileName()); + readBack.open(QIODevice::ReadOnly); + mergedDb.reset(reader.readDatabase(&readBack, key)); + readBack.close(); + QVERIFY(mergedDb); + entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website"); + QVERIFY(entry1); + QCOMPARE(entry1->title(), QString("Some Website")); + QCOMPARE(entry1->password(), QString("secretsecretsecret")); +} + +void TestCli::testRemove() +{ + Remove removeCmd; + QVERIFY(!removeCmd.name.isEmpty()); + QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name)); + + Kdbx3Reader reader; + Kdbx3Writer writer; + + // load test database and save a copy with disabled recycle bin + auto db = readTestDatabase(); + QVERIFY(db); + QTemporaryFile fileCopy; + fileCopy.open(); + db->metadata()->setRecycleBinEnabled(false); + writer.writeDatabase(&fileCopy, db.data()); + fileCopy.close(); + + qint64 pos = m_stdoutFile->pos(); + + // delete entry and verify + Utils::setNextPassword("a"); + removeCmd.execute({"rm", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n")); + + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create("a")); + QFile readBack(m_dbFile->fileName()); + readBack.open(QIODevice::ReadOnly); + QScopedPointer readBackDb(reader.readDatabase(&readBack, key)); + readBack.close(); + QVERIFY(readBackDb); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); + QVERIFY(readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + + pos = m_stdoutFile->pos(); + + // try again, this time without recycle bin + Utils::setNextPassword("a"); + removeCmd.execute({"rm", fileCopy.fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully deleted entry Sample Entry.\n")); + + readBack.setFileName(fileCopy.fileName()); + readBack.open(QIODevice::ReadOnly); + readBackDb.reset(reader.readDatabase(&readBack, key)); + readBack.close(); + QVERIFY(readBackDb); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + + pos = m_stdoutFile->pos(); + + // finally, try deleting a non-existent entry + Utils::setNextPassword("a"); + removeCmd.execute({"rm", fileCopy.fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->reset(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry /Sample Entry not found.\n")); +} + +void TestCli::testShow() +{ + Show showCmd; + QVERIFY(!showCmd.name.isEmpty()); + QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name)); + + Utils::setNextPassword("a"); + showCmd.execute({"show", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Title: Sample Entry\n" + "UserName: User Name\n" + "Password: Password\n" + "URL: http://www.somesite.com/\n" + "Notes: Notes\n")); + + qint64 pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + showCmd.execute({"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n")); + + pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + showCmd.execute({"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n" + "http://www.somesite.com/\n")); + + pos = m_stdoutFile->pos(); + Utils::setNextPassword("a"); + showCmd.execute({"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->reset(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("ERROR: unknown attribute DoesNotExist.\n")); +} diff --git a/tests/TestCli.h b/tests/TestCli.h new file mode 100644 index 0000000000..532d84a793 --- /dev/null +++ b/tests/TestCli.h @@ -0,0 +1,69 @@ +/* + * 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_TESTCLI_H +#define KEEPASSXC_TESTCLI_H + +#include "core/Database.h" + +#include +#include +#include +#include +#include + +class TestCli : public QObject +{ + Q_OBJECT + +private: + QSharedPointer readTestDatabase() const; + +private slots: + void initTestCase(); + void init(); + void cleanup(); + void cleanupTestCase(); + + void testCommand(); + void testAdd(); + void testClip(); + void testDiceware(); + void testEdit(); + void testEstimate_data(); + void testEstimate(); + void testExtract(); + void testGenerate_data(); + void testGenerate(); + void testList(); + void testLocate(); + void testMerge(); + void testRemove(); + void testShow(); + +private: + QByteArray m_dbData; + QScopedPointer m_dbFile; + QScopedPointer m_stdoutFile; + QScopedPointer m_stderrFile; + QScopedPointer m_stdinFile; + FILE* m_stdoutHandle = stdout; + FILE* m_stderrHandle = stderr; + FILE* m_stdinHandle = stdin; +}; + +#endif //KEEPASSXC_TESTCLI_H diff --git a/tests/TestKdbx4.cpp b/tests/TestKdbx4.cpp index 72c2d4c834..297cde284e 100644 --- a/tests/TestKdbx4.cpp +++ b/tests/TestKdbx4.cpp @@ -199,12 +199,12 @@ void TestKdbx4::testFormat400Upgrade_data() auto constexpr kdbx3 = KeePass2::FILE_VERSION_3_1 & KeePass2::FILE_VERSION_CRITICAL_MASK; auto constexpr kdbx4 = KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK; - QTest::newRow("Argon2 + AES") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES << false << kdbx4; - QTest::newRow("AES-KDF + AES") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES << false << kdbx4; - QTest::newRow("AES-KDF (legacy) + AES") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES << false << kdbx3; - QTest::newRow("Argon2 + AES + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES << true << kdbx4; - QTest::newRow("AES-KDF + AES + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES << true << kdbx4; - QTest::newRow("AES-KDF (legacy) + AES + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES << true << kdbx4; + QTest::newRow("Argon2 + AES") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES256 << false << kdbx4; + QTest::newRow("AES-KDF + AES") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << false << kdbx4; + QTest::newRow("AES-KDF (legacy) + AES") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << false << kdbx3; + QTest::newRow("Argon2 + AES + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES256 << true << kdbx4; + QTest::newRow("AES-KDF + AES + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << true << kdbx4; + QTest::newRow("AES-KDF (legacy) + AES + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << true << kdbx4; QTest::newRow("Argon2 + ChaCha20") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_CHACHA20 << false << kdbx4; QTest::newRow("AES-KDF + ChaCha20") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << false << kdbx4; diff --git a/tests/TestPasswordGenerator.cpp b/tests/TestPasswordGenerator.cpp new file mode 100644 index 0000000000..53cf25c312 --- /dev/null +++ b/tests/TestPasswordGenerator.cpp @@ -0,0 +1,131 @@ +/* + * 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 "TestPasswordGenerator.h" +#include "core/PasswordGenerator.h" +#include "crypto/Crypto.h" + +#include +#include + +QTEST_GUILESS_MAIN(TestPasswordGenerator) + +void TestPasswordGenerator::initTestCase() +{ + QVERIFY(Crypto::init()); +} + +void TestPasswordGenerator::testCharClasses() +{ + PasswordGenerator generator; + QVERIFY(!generator.isValid()); + generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters); + generator.setLength(16); + QVERIFY(generator.isValid()); + QCOMPARE(generator.generatePassword().size(), 16); + + generator.setLength(2000); + QString password = generator.generatePassword(); + QCOMPARE(password.size(), 2000); + QRegularExpression regex(R"(^[a-z]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::UpperLetters); + password = generator.generatePassword(); + regex.setPattern(R"(^[A-Z]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Numbers); + password = generator.generatePassword(); + regex.setPattern(R"(^\d+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Braces); + password = generator.generatePassword(); + regex.setPattern(R"(^[\(\)\[\]\{\}]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Punctuation); + password = generator.generatePassword(); + regex.setPattern(R"(^[\.,:;]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Quotes); + password = generator.generatePassword(); + regex.setPattern(R"(^["']+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Dashes); + password = generator.generatePassword(); + regex.setPattern(R"(^[\-/\\_|]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Math); + password = generator.generatePassword(); + regex.setPattern(R"(^[!\*\+\-<=>\?]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Logograms); + password = generator.generatePassword(); + regex.setPattern(R"(^[#`~%&^$@]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::EASCII); + password = generator.generatePassword(); + regex.setPattern(R"(^[^a-zA-Z0-9\.,:;"'\-/\\_|!\*\+\-<=>\?#`~%&^$@]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters + | PasswordGenerator::CharClass::UpperLetters | PasswordGenerator::CharClass::Braces); + password = generator.generatePassword(); + regex.setPattern(R"(^[a-zA-Z\(\)\[\]\{\}]+$)"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::Quotes + | PasswordGenerator::CharClass::Numbers | PasswordGenerator::CharClass::Dashes); + password = generator.generatePassword(); + regex.setPattern(R"(^["'\d\-/\\_|]+$)"); + QVERIFY(regex.match(password).hasMatch()); +} + +void TestPasswordGenerator::testLookalikeExclusion() +{ + PasswordGenerator generator; + generator.setLength(2000); + generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters); + QVERIFY(generator.isValid()); + QString password = generator.generatePassword(); + QCOMPARE(password.size(), 2000); + + generator.setFlags(PasswordGenerator::GeneratorFlag::ExcludeLookAlike); + password = generator.generatePassword(); + QRegularExpression regex("^[^lI0]+$"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters | + PasswordGenerator::CharClass::UpperLetters | PasswordGenerator::CharClass::Numbers); + password = generator.generatePassword(); + regex.setPattern("^[^lI01]+$"); + QVERIFY(regex.match(password).hasMatch()); + + generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters + | PasswordGenerator::CharClass::UpperLetters | PasswordGenerator::CharClass::Numbers + | PasswordGenerator::CharClass::EASCII); + password = generator.generatePassword(); + regex.setPattern("^[^lI01ï¹’]+$"); + QVERIFY(regex.match(password).hasMatch()); +} diff --git a/tests/TestPasswordGenerator.h b/tests/TestPasswordGenerator.h new file mode 100644 index 0000000000..5287e5bdef --- /dev/null +++ b/tests/TestPasswordGenerator.h @@ -0,0 +1,33 @@ +/* + * 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_TESTPASSWORDGENERATOR_H +#define KEEPASSXC_TESTPASSWORDGENERATOR_H + +#include + +class TestPasswordGenerator : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testCharClasses(); + void testLookalikeExclusion(); +}; + +#endif //KEEPASSXC_TESTPASSWORDGENERATOR_H diff --git a/tests/TestSymmetricCipher.cpp b/tests/TestSymmetricCipher.cpp index bec894c06f..b69e463b1d 100644 --- a/tests/TestSymmetricCipher.cpp +++ b/tests/TestSymmetricCipher.cpp @@ -26,162 +26,173 @@ #include "streams/SymmetricCipherStream.h" QTEST_GUILESS_MAIN(TestSymmetricCipher) +Q_DECLARE_METATYPE(SymmetricCipher::Algorithm); +Q_DECLARE_METATYPE(SymmetricCipher::Mode); +Q_DECLARE_METATYPE(SymmetricCipher::Direction); void TestSymmetricCipher::initTestCase() { QVERIFY(Crypto::init()); } -void TestSymmetricCipher::testAes128CbcEncryption() +void TestSymmetricCipher::testAlgorithmToCipher() { - // http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf - - QByteArray key = QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c"); - QByteArray iv = QByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); - QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); - plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); - QByteArray cipherText = QByteArray::fromHex("7649abac8119b246cee98e9b12e9197d"); - cipherText.append(QByteArray::fromHex("5086cb9b507219ee95db113a917678b2")); - bool ok; - - SymmetricCipher cipher(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Encrypt); - QVERIFY(cipher.init(key, iv)); - QCOMPARE(cipher.blockSize(), 16); - QCOMPARE(cipher.process(plainText, &ok), cipherText); - QVERIFY(ok); - - QBuffer buffer; - SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Encrypt); - QVERIFY(stream.init(key, iv)); - buffer.open(QIODevice::WriteOnly); - QVERIFY(stream.open(QIODevice::WriteOnly)); - QVERIFY(stream.reset()); - - buffer.reset(); - buffer.buffer().clear(); - QCOMPARE(stream.write(plainText.left(16)), qint64(16)); - QCOMPARE(buffer.data(), cipherText.left(16)); - QVERIFY(stream.reset()); - // make sure padding is written - QCOMPARE(buffer.data().size(), 32); - - buffer.reset(); - buffer.buffer().clear(); - QCOMPARE(stream.write(plainText.left(10)), qint64(10)); - QVERIFY(buffer.data().isEmpty()); - - QVERIFY(stream.reset()); - buffer.reset(); - buffer.buffer().clear(); - QCOMPARE(stream.write(plainText.left(10)), qint64(10)); - stream.close(); - QCOMPARE(buffer.data().size(), 16); + QCOMPARE(SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm::Aes128), KeePass2::CIPHER_AES128); + QCOMPARE(SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm::Aes256), KeePass2::CIPHER_AES256); + QCOMPARE(SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm::Twofish), KeePass2::CIPHER_TWOFISH); + QCOMPARE(SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm::ChaCha20), KeePass2::CIPHER_CHACHA20); + QCOMPARE(SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm::InvalidAlgorithm), QUuid()); } -void TestSymmetricCipher::testAes128CbcDecryption() +void TestSymmetricCipher::testEncryptionDecryption_data() { - QByteArray key = QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c"); - QByteArray iv = QByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); - QByteArray cipherText = QByteArray::fromHex("7649abac8119b246cee98e9b12e9197d"); - cipherText.append(QByteArray::fromHex("5086cb9b507219ee95db113a917678b2")); - QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); - plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); - bool ok; - - SymmetricCipher cipher(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); - QVERIFY(cipher.init(key, iv)); - QCOMPARE(cipher.blockSize(), 16); - QCOMPARE(cipher.process(cipherText, &ok), plainText); - QVERIFY(ok); - - // padded with 16 0x10 bytes - QByteArray cipherTextPadded = cipherText + QByteArray::fromHex("55e21d7100b988ffec32feeafaf23538"); - QBuffer buffer(&cipherTextPadded); - SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); - QVERIFY(stream.init(key, iv)); - buffer.open(QIODevice::ReadOnly); - QVERIFY(stream.open(QIODevice::ReadOnly)); + QTest::addColumn("algorithm"); + QTest::addColumn("mode"); + QTest::addColumn("direction"); + QTest::addColumn("key"); + QTest::addColumn("iv"); + QTest::addColumn("plainText"); + QTest::addColumn("cipherText"); - QCOMPARE(stream.read(10), plainText.left(10)); - buffer.reset(); - QVERIFY(stream.reset()); - QCOMPARE(stream.read(20), plainText.left(20)); - buffer.reset(); - QVERIFY(stream.reset()); - QCOMPARE(stream.read(16), plainText.left(16)); - buffer.reset(); - QVERIFY(stream.reset()); - QCOMPARE(stream.read(100), plainText); + // http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf + QTest::newRow("AES128-CBC Encryption") + << SymmetricCipher::Aes128 + << SymmetricCipher::Cbc + << SymmetricCipher::Encrypt + << QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c") + << QByteArray::fromHex("000102030405060708090a0b0c0d0e0f") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51") + << QByteArray::fromHex("7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2"); + + QTest::newRow("AES128-CBC Decryption") + << SymmetricCipher::Aes128 + << SymmetricCipher::Cbc + << SymmetricCipher::Decrypt + << QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c") + << QByteArray::fromHex("000102030405060708090a0b0c0d0e0f") + << QByteArray::fromHex("7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51"); + + QTest::newRow("AES256-CBC Encryption") + << SymmetricCipher::Aes256 + << SymmetricCipher::Cbc + << SymmetricCipher::Encrypt + << QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4") + << QByteArray::fromHex("000102030405060708090a0b0c0d0e0f") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51") + << QByteArray::fromHex("f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d"); + + QTest::newRow("AES256-CBC Decryption") + << SymmetricCipher::Aes256 + << SymmetricCipher::Cbc + << SymmetricCipher::Decrypt + << QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4") + << QByteArray::fromHex("000102030405060708090a0b0c0d0e0f") + << QByteArray::fromHex("f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51"); + + QTest::newRow("AES256-CTR Encryption") + << SymmetricCipher::Aes256 + << SymmetricCipher::Ctr + << SymmetricCipher::Encrypt + << QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4") + << QByteArray::fromHex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51") + << QByteArray::fromHex("601ec313775789a5b7a7f504bbf3d228f443e3ca4d62b59aca84e990cacaf5c5"); + + QTest::newRow("AES256-CTR Decryption") + << SymmetricCipher::Aes256 + << SymmetricCipher::Ctr + << SymmetricCipher::Decrypt + << QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4") + << QByteArray::fromHex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + << QByteArray::fromHex("601ec313775789a5b7a7f504bbf3d228f443e3ca4d62b59aca84e990cacaf5c5") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51"); } -void TestSymmetricCipher::testAes256CbcEncryption() +void TestSymmetricCipher::testEncryptionDecryption() { - // http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf + QFETCH(SymmetricCipher::Algorithm, algorithm); + QFETCH(SymmetricCipher::Mode, mode); + QFETCH(SymmetricCipher::Direction, direction); + QFETCH(QByteArray, key); + QFETCH(QByteArray, iv); + QFETCH(QByteArray, plainText); + QFETCH(QByteArray, cipherText); - QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); - QByteArray iv = QByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); - QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); - plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); - QByteArray cipherText = QByteArray::fromHex("f58c4c04d6e5f1ba779eabfb5f7bfbd6"); - cipherText.append(QByteArray::fromHex("9cfc4e967edb808d679f777bc6702c7d")); bool ok; - - SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Encrypt); + SymmetricCipher cipher(algorithm, mode, direction); QVERIFY(cipher.init(key, iv)); QCOMPARE(cipher.blockSize(), 16); - QCOMPARE(cipher.process(plainText, &ok), cipherText); QVERIFY(ok); - QBuffer buffer; - SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Encrypt); - QVERIFY(stream.init(key, iv)); - buffer.open(QIODevice::WriteOnly); - QVERIFY(stream.open(QIODevice::WriteOnly)); - QVERIFY(stream.reset()); - - buffer.reset(); - buffer.buffer().clear(); - QCOMPARE(stream.write(plainText.left(16)), qint64(16)); - QCOMPARE(buffer.data(), cipherText.left(16)); - QVERIFY(stream.reset()); - // make sure padding is written - QCOMPARE(buffer.data().size(), 32); - - buffer.reset(); - buffer.buffer().clear(); - QCOMPARE(stream.write(plainText.left(10)), qint64(10)); - QVERIFY(buffer.data().isEmpty()); - - QVERIFY(stream.reset()); - buffer.reset(); - buffer.buffer().clear(); - QCOMPARE(stream.write(plainText.left(10)), qint64(10)); - stream.close(); - QCOMPARE(buffer.data().size(), 16); + if (mode == SymmetricCipher::Cbc) { + QBuffer buffer; + SymmetricCipherStream stream(&buffer, algorithm, mode, direction); + QVERIFY(stream.init(key, iv)); + buffer.open(QIODevice::WriteOnly); + QVERIFY(stream.open(QIODevice::WriteOnly)); + QVERIFY(stream.reset()); + + buffer.reset(); + buffer.buffer().clear(); + QCOMPARE(stream.write(plainText.left(16)), qint64(16)); + QCOMPARE(buffer.data(), cipherText.left(16)); + QVERIFY(stream.reset()); + // make sure padding is written + QCOMPARE(buffer.data().size(), 32); + + buffer.reset(); + buffer.buffer().clear(); + QCOMPARE(stream.write(plainText.left(10)), qint64(10)); + QVERIFY(buffer.data().isEmpty()); + + QVERIFY(stream.reset()); + buffer.reset(); + buffer.buffer().clear(); + QCOMPARE(stream.write(plainText.left(10)), qint64(10)); + stream.close(); + QCOMPARE(buffer.data().size(), 16); + } } -void TestSymmetricCipher::testAes256CbcDecryption() +void TestSymmetricCipher::testAesCbcPadding_data() { - QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); - QByteArray iv = QByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); - QByteArray cipherText = QByteArray::fromHex("f58c4c04d6e5f1ba779eabfb5f7bfbd6"); - cipherText.append(QByteArray::fromHex("9cfc4e967edb808d679f777bc6702c7d")); - QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); - plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); - bool ok; + QTest::addColumn("key"); + QTest::addColumn("iv"); + QTest::addColumn("cipherText"); + QTest::addColumn("plainText"); + QTest::addColumn("padding"); + + QTest::newRow("AES128") + << QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c") + << QByteArray::fromHex("000102030405060708090a0b0c0d0e0f") + << QByteArray::fromHex("7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51") + << QByteArray::fromHex("55e21d7100b988ffec32feeafaf23538"); + + QTest::newRow("AES256") + << QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4") + << QByteArray::fromHex("000102030405060708090a0b0c0d0e0f") + << QByteArray::fromHex("f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51") + << QByteArray::fromHex("3a3aa5e0213db1a9901f9036cf5102d2"); +} - SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); - QVERIFY(cipher.init(key, iv)); - QCOMPARE(cipher.blockSize(), 16); +void TestSymmetricCipher::testAesCbcPadding() +{ + QFETCH(QByteArray, key); + QFETCH(QByteArray, iv); + QFETCH(QByteArray, cipherText); + QFETCH(QByteArray, plainText); + QFETCH(QByteArray, padding); - QCOMPARE(cipher.process(cipherText, &ok), plainText); - QVERIFY(ok); + // padded with 16 0x10 bytes + QByteArray cipherTextPadded = cipherText + padding; - // padded with 16 0x16 bytes - QByteArray cipherTextPadded = cipherText + QByteArray::fromHex("3a3aa5e0213db1a9901f9036cf5102d2"); QBuffer buffer(&cipherTextPadded); - SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); + SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); QVERIFY(stream.init(key, iv)); buffer.open(QIODevice::ReadOnly); QVERIFY(stream.open(QIODevice::ReadOnly)); @@ -198,42 +209,48 @@ void TestSymmetricCipher::testAes256CbcDecryption() QCOMPARE(stream.read(100), plainText); } -void TestSymmetricCipher::testAes256CtrEncryption() +void TestSymmetricCipher::testInplaceEcb_data() { - // http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf - - QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); - QByteArray ctr = QByteArray::fromHex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); - QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); - plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); - QByteArray cipherText = QByteArray::fromHex("601ec313775789a5b7a7f504bbf3d228"); - cipherText.append(QByteArray::fromHex("f443e3ca4d62b59aca84e990cacaf5c5")); - bool ok; - - SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Ctr, SymmetricCipher::Encrypt); - QVERIFY(cipher.init(key, ctr)); - QCOMPARE(cipher.blockSize(), 16); - - QCOMPARE(cipher.process(plainText, &ok), cipherText); - QVERIFY(ok); + QTest::addColumn("key"); + QTest::addColumn("plainText"); + QTest::addColumn("cipherText"); + + QTest::newRow("AES128") + << QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c") + << QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a") + << QByteArray::fromHex("3ad77bb40d7a3660a89ecaf32466ef97"); } -void TestSymmetricCipher::testAes256CtrDecryption() +void TestSymmetricCipher::testInplaceEcb() { - QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); - QByteArray ctr = QByteArray::fromHex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); - QByteArray cipherText = QByteArray::fromHex("601ec313775789a5b7a7f504bbf3d228"); - cipherText.append(QByteArray::fromHex("f443e3ca4d62b59aca84e990cacaf5c5")); - QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); - plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); - bool ok; - - SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Ctr, SymmetricCipher::Decrypt); - QVERIFY(cipher.init(key, ctr)); - QCOMPARE(cipher.blockSize(), 16); - - QCOMPARE(cipher.process(cipherText, &ok), plainText); - QVERIFY(ok); + QFETCH(QByteArray, key); + QFETCH(QByteArray, plainText); + QFETCH(QByteArray, cipherText); + + SymmetricCipher cipherInPlaceEnc(SymmetricCipher::Aes128, SymmetricCipher::Ecb, SymmetricCipher::Encrypt); + QVERIFY(cipherInPlaceEnc.init(key, QByteArray(16, 0))); + QCOMPARE(cipherInPlaceEnc.blockSize(), 16); + auto data = QByteArray(plainText); + QVERIFY(cipherInPlaceEnc.processInPlace(data)); + QCOMPARE(data, cipherText); + + SymmetricCipher cipherInPlaceDec(SymmetricCipher::Aes128, SymmetricCipher::Ecb, SymmetricCipher::Decrypt); + QVERIFY(cipherInPlaceDec.init(key, QByteArray(16, 0))); + QCOMPARE(cipherInPlaceDec.blockSize(), 16); + QVERIFY(cipherInPlaceDec.processInPlace(data)); + QCOMPARE(data, plainText); + + SymmetricCipher cipherInPlaceEnc2(SymmetricCipher::Aes128, SymmetricCipher::Ecb, SymmetricCipher::Encrypt); + QVERIFY(cipherInPlaceEnc2.init(key, QByteArray(16, 0))); + QCOMPARE(cipherInPlaceEnc2.blockSize(), 16); + data = QByteArray(plainText); + QVERIFY(cipherInPlaceEnc2.processInPlace(data, 100)); + + SymmetricCipher cipherInPlaceDec2(SymmetricCipher::Aes128, SymmetricCipher::Ecb, SymmetricCipher::Decrypt); + QVERIFY(cipherInPlaceDec2.init(key, QByteArray(16, 0))); + QCOMPARE(cipherInPlaceDec2.blockSize(), 16); + QVERIFY(cipherInPlaceDec2.processInPlace(data, 100)); + QCOMPARE(data, plainText); } void TestSymmetricCipher::testTwofish256CbcEncryption() diff --git a/tests/TestSymmetricCipher.h b/tests/TestSymmetricCipher.h index 9b82fd88a3..5eede0953a 100644 --- a/tests/TestSymmetricCipher.h +++ b/tests/TestSymmetricCipher.h @@ -27,12 +27,13 @@ class TestSymmetricCipher : public QObject private slots: void initTestCase(); - void testAes128CbcEncryption(); - void testAes128CbcDecryption(); - void testAes256CbcEncryption(); - void testAes256CbcDecryption(); - void testAes256CtrEncryption(); - void testAes256CtrDecryption(); + void testAlgorithmToCipher(); + void testEncryptionDecryption_data(); + void testEncryptionDecryption(); + void testAesCbcPadding_data(); + void testAesCbcPadding(); + void testInplaceEcb_data(); + void testInplaceEcb(); void testTwofish256CbcEncryption(); void testTwofish256CbcDecryption(); void testSalsa20(); diff --git a/tests/gui/CMakeLists.txt b/tests/gui/CMakeLists.txt index 6cae888303..a6e876f475 100644 --- a/tests/gui/CMakeLists.txt +++ b/tests/gui/CMakeLists.txt @@ -15,6 +15,6 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) -add_unit_test(NAME testgui SOURCES TestGui.cpp TemporaryFile.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testgui SOURCES TestGui.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/gui/TemporaryFile.cpp b/tests/gui/TemporaryFile.cpp deleted file mode 100644 index b6d20848b3..0000000000 --- a/tests/gui/TemporaryFile.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2016 Danny Su - * 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 "TemporaryFile.h" - -#include - -#ifdef Q_OS_WIN -const QString TemporaryFile::SUFFIX = ".win"; - -TemporaryFile::~TemporaryFile() -{ - if (m_tempFile.autoRemove()) { - m_file.remove(); - } -} -#endif - -bool TemporaryFile::open() -{ -#ifdef Q_OS_WIN - // Still call QTemporaryFile::open() so that it figures out the temporary - // file name to use. Assuming that by appending the SUFFIX to whatever - // QTemporaryFile chooses is also an available file. - bool tempFileOpened = m_tempFile.open(); - if (tempFileOpened) { - m_file.setFileName(filePath()); - return m_file.open(QIODevice::WriteOnly); - } - return false; -#else - return m_tempFile.open(); -#endif -} - -void TemporaryFile::close() -{ - m_tempFile.close(); -#ifdef Q_OS_WIN - m_file.close(); -#endif -} - -qint64 TemporaryFile::write(const char* data, qint64 maxSize) -{ -#ifdef Q_OS_WIN - return m_file.write(data, maxSize); -#else - return m_tempFile.write(data, maxSize); -#endif -} - -qint64 TemporaryFile::write(const QByteArray& byteArray) -{ -#ifdef Q_OS_WIN - return m_file.write(byteArray); -#else - return m_tempFile.write(byteArray); -#endif -} - -QString TemporaryFile::fileName() const -{ -#ifdef Q_OS_WIN - return QFileInfo(m_tempFile).fileName() + TemporaryFile::SUFFIX; -#else - return QFileInfo(m_tempFile).fileName(); -#endif -} - -QString TemporaryFile::filePath() const -{ -#ifdef Q_OS_WIN - return m_tempFile.fileName() + TemporaryFile::SUFFIX; -#else - return m_tempFile.fileName(); -#endif -} diff --git a/tests/gui/TemporaryFile.h b/tests/gui/TemporaryFile.h deleted file mode 100644 index 8a98d92359..0000000000 --- a/tests/gui/TemporaryFile.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016 Danny Su - * 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 . - */ - -#ifndef KEEPASSX_TEMPORARYFILE_H -#define KEEPASSX_TEMPORARYFILE_H - -#include -#include -#include - -/** - * QTemporaryFile::close() doesn't actually close the file according to - * http://doc.qt.io/qt-5/qtemporaryfile.html: "For as long as the - * QTemporaryFile object itself is not destroyed, the unique temporary file - * will exist and be kept open internally by QTemporaryFile." - * - * This behavior causes issues when running tests on Windows. If the file is - * not closed, the testSave test will fail due to Access Denied. The - * auto-reload test also fails from Windows not triggering file change - * notification because the file isn't actually closed by QTemporaryFile. - * - * This class isolates the Windows specific logic that uses QFile to really - * close the test file when requested to. - */ -class TemporaryFile : public QObject -{ - Q_OBJECT - -public: -#ifdef Q_OS_WIN - ~TemporaryFile(); -#endif - - bool open(); - void close(); - qint64 write(const char* data, qint64 maxSize); - qint64 write(const QByteArray& byteArray); - - QString fileName() const; - QString filePath() const; - -private: - QTemporaryFile m_tempFile; -#ifdef Q_OS_WIN - QFile m_file; - static const QString SUFFIX; -#endif -}; - -#endif // KEEPASSX_TEMPORARYFILE_H diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index e3671567cd..ee53eb7777 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -18,6 +18,7 @@ #include "TestGui.h" #include "TestGlobal.h" +#include "gui/Application.h" #include #include @@ -33,12 +34,12 @@ #include #include #include -#include #include #include #include #include "config-keepassx-tests.h" +#include "core/Bootstrap.h" #include "core/Config.h" #include "core/Database.h" #include "core/Entry.h" @@ -59,7 +60,6 @@ #include "gui/DatabaseTabWidget.h" #include "gui/DatabaseWidget.h" #include "gui/FileDialog.h" -#include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "gui/PasswordEdit.h" #include "gui/SearchWidget.h" @@ -74,22 +74,23 @@ #include "gui/masterkey/KeyComponentWidget.h" #include "keys/PasswordKey.h" +QTEST_MAIN(TestGui) + void TestGui::initTestCase() { QVERIFY(Crypto::init()); Config::createTempFileInstance(); // Disable autosave so we can test the modified file indicator config()->set("AutoSaveAfterEveryChange", false); - // Enable the tray icon so we can test hiding/restoring the window + // Enable the tray icon so we can test hiding/restoring the windowQByteArray config()->set("GUI/ShowTrayIcon", true); // Disable advanced settings mode (activate within individual tests to test advanced settings) config()->set("GUI/AdvancedSettings", false); - m_mainWindow = new MainWindow(); + m_mainWindow.reset(new MainWindow()); + Bootstrap::restoreMainWindowState(*m_mainWindow); m_tabWidget = m_mainWindow->findChild("tabWidget"); m_mainWindow->show(); - m_mainWindow->activateWindow(); - Tools::wait(50); // Load the NewDatabase.kdbx file into temporary storage QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx")); @@ -101,29 +102,32 @@ void TestGui::initTestCase() // Every test starts with opening the temp database void TestGui::init() { + m_dbFile.reset(new QTemporaryFile()); // Write the temp storage to a temp database file for use in our tests - QVERIFY(m_dbFile.open()); - QCOMPARE(m_dbFile.write(m_dbData), static_cast((m_dbData.size()))); - m_dbFile.close(); - - m_dbFileName = m_dbFile.fileName(); - m_dbFilePath = m_dbFile.filePath(); + QVERIFY(m_dbFile->open()); + QCOMPARE(m_dbFile->write(m_dbData), static_cast((m_dbData.size()))); + m_dbFileName = QFileInfo(m_dbFile->fileName()).fileName(); + m_dbFilePath = m_dbFile->fileName(); + m_dbFile->close(); fileDialog()->setNextFileName(m_dbFilePath); triggerAction("actionDatabaseOpen"); - QWidget* databaseOpenWidget = m_mainWindow->findChild("databaseOpenWidget"); - QLineEdit* editPassword = databaseOpenWidget->findChild("editPassword"); + auto* databaseOpenWidget = m_mainWindow->findChild("databaseOpenWidget"); + auto* editPassword = databaseOpenWidget->findChild("editPassword"); QVERIFY(editPassword); QTest::keyClicks(editPassword, "a"); QTest::keyClick(editPassword, Qt::Key_Enter); - Tools::wait(100); - QVERIFY(m_tabWidget->currentDatabaseWidget()); + QTRY_VERIFY(m_tabWidget->currentDatabaseWidget()); m_dbWidget = m_tabWidget->currentDatabaseWidget(); m_db = m_dbWidget->database(); + + // make sure window is activated or focus tests may fail + m_mainWindow->activateWindow(); + QApplication::processEvents(); } // Every test ends with closing the temp database without saving @@ -132,17 +136,21 @@ void TestGui::cleanup() // DO NOT save the database MessageBox::setNextAnswer(QMessageBox::No); triggerAction("actionDatabaseClose"); - Tools::wait(100); + QApplication::processEvents(); if (m_db) { delete m_db; } - m_db = nullptr; - if (m_dbWidget) { delete m_dbWidget; } - m_dbWidget = nullptr; + + m_dbFile->remove(); +} + +void TestGui::cleanupTestCase() +{ + m_dbFile->remove(); } void TestGui::testSettingsDefaultTabOrder() @@ -187,8 +195,9 @@ void TestGui::testCreateDatabase() // check key and encryption QCOMPARE(m_db->key()->keys().size(), 2); + QCOMPARE(m_db->kdf()->rounds(), 2); QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_ARGON2); - QCOMPARE(m_db->cipher(), KeePass2::CIPHER_AES); + QCOMPARE(m_db->cipher(), KeePass2::CIPHER_AES256); auto compositeKey = QSharedPointer::create(); compositeKey->addKey(QSharedPointer::create("test")); auto fileKey = QSharedPointer::create(); @@ -213,7 +222,40 @@ void TestGui::createDatabaseCallback() QTest::keyClick(wizard, Qt::Key_Enter); QCOMPARE(wizard->currentId(), 1); - QTest::keyClick(wizard, Qt::Key_Enter); + auto decryptionTimeSlider = wizard->currentPage()->findChild("decryptionTimeSlider"); + auto algorithmComboBox = wizard->currentPage()->findChild("algorithmComboBox"); + QTRY_VERIFY(decryptionTimeSlider->isVisible()); + QVERIFY(!algorithmComboBox->isVisible()); + auto advancedToggle = wizard->currentPage()->findChild("advancedSettingsButton"); + QTest::mouseClick(advancedToggle, Qt::MouseButton::LeftButton); + QTRY_VERIFY(!decryptionTimeSlider->isVisible()); + QVERIFY(algorithmComboBox->isVisible()); + + auto rounds = wizard->currentPage()->findChild("transformRoundsSpinBox"); + QVERIFY(rounds); + QVERIFY(rounds->isVisible()); + QTest::mouseClick(rounds, Qt::MouseButton::LeftButton); + QTest::keyClick(rounds, Qt::Key_A, Qt::ControlModifier); + QTest::keyClicks(rounds, "2"); + QTest::keyClick(rounds, Qt::Key_Tab); + QTest::keyClick(rounds, Qt::Key_Tab); + + auto memory = wizard->currentPage()->findChild("memorySpinBox"); + QVERIFY(memory); + QVERIFY(memory->isVisible()); + QTest::mouseClick(memory, Qt::MouseButton::LeftButton); + QTest::keyClick(memory, Qt::Key_A, Qt::ControlModifier); + QTest::keyClicks(memory, "50"); + QTest::keyClick(memory, Qt::Key_Tab); + + auto parallelism = wizard->currentPage()->findChild("parallelismSpinBox"); + QVERIFY(parallelism); + QVERIFY(parallelism->isVisible()); + QTest::mouseClick(parallelism, Qt::MouseButton::LeftButton); + QTest::keyClick(parallelism, Qt::Key_A, Qt::ControlModifier); + QTest::keyClicks(parallelism, "1"); + QTest::keyClick(parallelism, Qt::Key_Enter); + QCOMPARE(wizard->currentId(), 2); // enter password @@ -222,7 +264,7 @@ void TestGui::createDatabaseCallback() auto* passwordEdit = passwordWidget->findChild("enterPasswordEdit"); auto* passwordRepeatEdit = passwordWidget->findChild("repeatPasswordEdit"); QTRY_VERIFY(passwordEdit->isVisible()); - QVERIFY(passwordEdit->hasFocus()); + QTRY_VERIFY(passwordEdit->hasFocus()); QTest::keyClicks(passwordEdit, "test"); QTest::keyClick(passwordEdit, Qt::Key::Key_Tab); QTest::keyClicks(passwordRepeatEdit, "test"); @@ -247,25 +289,26 @@ void TestGui::createDatabaseCallback() QCOMPARE(fileCombo->currentText(), QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key")); // save database to temporary file - TemporaryFile tmpFile; + QTemporaryFile tmpFile; QVERIFY(tmpFile.open()); tmpFile.close(); - fileDialog()->setNextFileName(tmpFile.filePath()); + fileDialog()->setNextFileName(tmpFile.fileName()); QTest::keyClick(fileCombo, Qt::Key::Key_Enter); + tmpFile.remove(); } void TestGui::testMergeDatabase() { // It is safe to ignore the warning this line produces - QSignalSpy dbMergeSpy(m_dbWidget, SIGNAL(databaseMerged(Database*))); + QSignalSpy dbMergeSpy(m_dbWidget.data(), SIGNAL(databaseMerged(Database*))); // set file to merge from fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); triggerAction("actionDatabaseMerge"); - QWidget* databaseOpenMergeWidget = m_mainWindow->findChild("databaseOpenMergeWidget"); - QLineEdit* editPasswordMerge = databaseOpenMergeWidget->findChild("editPassword"); + auto* databaseOpenMergeWidget = m_mainWindow->findChild("databaseOpenMergeWidget"); + auto* editPasswordMerge = databaseOpenMergeWidget->findChild("editPassword"); QVERIFY(editPasswordMerge->isVisible()); m_tabWidget->currentDatabaseWidget()->setCurrentWidget(databaseOpenMergeWidget); @@ -300,11 +343,11 @@ void TestGui::testAutoreloadDatabase() // Test accepting new file in autoreload MessageBox::setNextAnswer(QMessageBox::Yes); // Overwrite the current database with the temp data - QVERIFY(m_dbFile.open()); - QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); - m_dbFile.close(); - Tools::wait(1500); + QVERIFY(m_dbFile->open()); + QVERIFY(m_dbFile->write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); + m_dbFile->close(); + Tools::wait(800); m_db = m_dbWidget->database(); // the General group contains one entry from the new db data @@ -318,10 +361,10 @@ void TestGui::testAutoreloadDatabase() // Test rejecting new file in autoreload MessageBox::setNextAnswer(QMessageBox::No); // Overwrite the current temp database with a new file - m_dbFile.open(); - QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); - m_dbFile.close(); - Tools::wait(1500); + m_dbFile->open(); + QVERIFY(m_dbFile->write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); + m_dbFile->close(); + Tools::wait(800); m_db = m_dbWidget->database(); @@ -342,10 +385,10 @@ void TestGui::testAutoreloadDatabase() // This is saying yes to merging the entries MessageBox::setNextAnswer(QMessageBox::Yes); // Overwrite the current database with the temp data - QVERIFY(m_dbFile.open()); - QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); - m_dbFile.close(); - Tools::wait(1500); + QVERIFY(m_dbFile->open()); + QVERIFY(m_dbFile->write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); + m_dbFile->close(); + Tools::wait(800); m_db = m_dbWidget->database(); @@ -361,17 +404,17 @@ void TestGui::testTabs() void TestGui::testEditEntry() { - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + auto* toolBar = m_mainWindow->findChild("toolBar"); int editCount = 0; // Select the first entry in the database - EntryView* entryView = m_dbWidget->findChild("entryView"); + auto* entryView = m_dbWidget->findChild("entryView"); QModelIndex entryItem = entryView->model()->index(0, 1); Entry* entry = entryView->entryFromIndex(entryItem); clickIndex(entryItem, entryView, Qt::LeftButton); // Confirm the edit action button is enabled - QAction* entryEditAction = m_mainWindow->findChild("actionEntryEdit"); + auto* entryEditAction = m_mainWindow->findChild("actionEntryEdit"); QVERIFY(entryEditAction->isEnabled()); QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction); QVERIFY(entryEditWidget->isVisible()); @@ -380,12 +423,12 @@ void TestGui::testEditEntry() // Edit the first entry ("Sample Entry") QTest::mouseClick(entryEditWidget, Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); QTest::keyClicks(titleEdit, "_test"); // Apply the edit - QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); QCOMPARE(entry->title(), QString("Sample Entry_test")); @@ -410,7 +453,7 @@ void TestGui::testEditEntry() // Test protected attributes editEntryWidget->setCurrentPage(1); - QPlainTextEdit* attrTextEdit = editEntryWidget->findChild("attributesEdit"); + auto* attrTextEdit = editEntryWidget->findChild("attributesEdit"); QTest::mouseClick(editEntryWidget->findChild("addAttributeButton"), Qt::LeftButton); QString attrText = "TEST TEXT"; QTest::keyClicks(attrTextEdit, attrText); @@ -422,11 +465,11 @@ void TestGui::testEditEntry() editEntryWidget->setCurrentPage(0); // Test mismatch passwords - QLineEdit* passwordEdit = editEntryWidget->findChild("passwordEdit"); + auto* passwordEdit = editEntryWidget->findChild("passwordEdit"); QString originalPassword = passwordEdit->text(); passwordEdit->setText("newpass"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); - MessageWidget* messageWiget = editEntryWidget->findChild("messageWidget"); + auto* messageWiget = editEntryWidget->findChild("messageWidget"); QTRY_VERIFY(messageWiget->isVisible()); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); QCOMPARE(passwordEdit->text(), QString("newpass")); @@ -469,9 +512,9 @@ void TestGui::testSearchEditEntry() // Regression test for Issue #1447 -- Uses example from issue description // Find buttons for group creation - EditGroupWidget* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); - QLineEdit* nameEdit = editGroupWidget->findChild("editName"); - QDialogButtonBox* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + auto* nameEdit = editGroupWidget->findChild("editName"); + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); // Add groups "Good" and "Bad" m_dbWidget->createGroup(); @@ -484,11 +527,11 @@ void TestGui::testSearchEditEntry() m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); // Find buttons for entry creation - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + auto* toolBar = m_mainWindow->findChild("toolBar"); QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); - QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); // Create "Doggy" in "Good" Group* goodGroup = m_dbWidget->currentGroup()->findChildByName(QString("Good")); @@ -501,8 +544,8 @@ void TestGui::testSearchEditEntry() m_dbWidget->groupView()->setCurrentGroup(badGroup); // Search for "Doggy" entry - SearchWidget* searchWidget = toolBar->findChild("SearchWidget"); - QLineEdit* searchTextEdit = searchWidget->findChild("searchEdit"); + auto* searchWidget = toolBar->findChild("SearchWidget"); + auto* searchTextEdit = searchWidget->findChild("searchEdit"); QTest::mouseClick(searchTextEdit, Qt::LeftButton); QTest::keyClicks(searchTextEdit, "Doggy"); QTRY_VERIFY(m_dbWidget->isInSearchMode()); @@ -518,11 +561,11 @@ void TestGui::testSearchEditEntry() void TestGui::testAddEntry() { - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); - EntryView* entryView = m_dbWidget->findChild("entryView"); + auto* toolBar = m_mainWindow->findChild("toolBar"); + auto* entryView = m_dbWidget->findChild("entryView"); // Find the new entry action - QAction* entryNewAction = m_mainWindow->findChild("actionEntryNew"); + auto* entryNewAction = m_mainWindow->findChild("actionEntryNew"); QVERIFY(entryNewAction->isEnabled()); // Find the button associated with the new entry action @@ -535,10 +578,10 @@ void TestGui::testAddEntry() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); // Add entry "test" and confirm added - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); QTest::keyClicks(titleEdit, "test"); - QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode); @@ -551,28 +594,12 @@ void TestGui::testAddEntry() // Add entry "something 2" QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "something 2"); - QLineEdit* passwordEdit = editEntryWidget->findChild("passwordEdit"); - QLineEdit* passwordRepeatEdit = editEntryWidget->findChild("passwordRepeatEdit"); + auto* passwordEdit = editEntryWidget->findChild("passwordEdit"); + auto* passwordRepeatEdit = editEntryWidget->findChild("passwordRepeatEdit"); QTest::keyClicks(passwordEdit, "something 2"); QTest::keyClicks(passwordRepeatEdit, "something 2"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); -/* All apply tests disabled due to data loss workaround - * that disables apply button on new entry creation - * - // Add entry "something 3" using the apply button then click ok - QTest::mouseClick(entryNewWidget, Qt::LeftButton); - QTest::keyClicks(titleEdit, "something 3"); - QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); - QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); - - // Add entry "something 4" using the apply button then click cancel - QTest::mouseClick(entryNewWidget, Qt::LeftButton); - QTest::keyClicks(titleEdit, "something 4"); - QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); - QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton); -*/ - // Add entry "something 5" but click cancel button (does NOT add entry) QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "something 5"); @@ -587,10 +614,10 @@ void TestGui::testAddEntry() void TestGui::testPasswordEntryEntropy() { - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + auto* toolBar = m_mainWindow->findChild("toolBar"); // Find the new entry action - QAction* entryNewAction = m_mainWindow->findChild("actionEntryNew"); + auto* entryNewAction = m_mainWindow->findChild("actionEntryNew"); QVERIFY(entryNewAction->isEnabled()); // Find the button associated with the new entry action @@ -603,18 +630,18 @@ void TestGui::testPasswordEntryEntropy() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); // Add entry "test" and confirm added - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); QTest::keyClicks(titleEdit, "test"); // Open the password generator - QToolButton* generatorButton = editEntryWidget->findChild("togglePasswordGeneratorButton"); + auto* generatorButton = editEntryWidget->findChild("togglePasswordGeneratorButton"); QTest::mouseClick(generatorButton, Qt::LeftButton); // Type in some password - QLineEdit* editNewPassword = editEntryWidget->findChild("editNewPassword"); - QLabel* entropyLabel = editEntryWidget->findChild("entropyLabel"); - QLabel* strengthLabel = editEntryWidget->findChild("strengthLabel"); + auto* editNewPassword = editEntryWidget->findChild("editNewPassword"); + auto* entropyLabel = editEntryWidget->findChild("entropyLabel"); + auto* strengthLabel = editEntryWidget->findChild("strengthLabel"); editNewPassword->setText(""); QTest::keyClicks(editNewPassword, "hello"); @@ -659,10 +686,10 @@ void TestGui::testPasswordEntryEntropy() void TestGui::testDicewareEntryEntropy() { - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + auto* toolBar = m_mainWindow->findChild("toolBar"); // Find the new entry action - QAction* entryNewAction = m_mainWindow->findChild("actionEntryNew"); + auto* entryNewAction = m_mainWindow->findChild("actionEntryNew"); QVERIFY(entryNewAction->isEnabled()); // Find the button associated with the new entry action @@ -675,27 +702,27 @@ void TestGui::testDicewareEntryEntropy() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); // Add entry "test" and confirm added - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); QTest::keyClicks(titleEdit, "test"); // Open the password generator - QToolButton* generatorButton = editEntryWidget->findChild("togglePasswordGeneratorButton"); + auto* generatorButton = editEntryWidget->findChild("togglePasswordGeneratorButton"); QTest::mouseClick(generatorButton, Qt::LeftButton); // Select Diceware - QTabWidget* tabWidget = editEntryWidget->findChild("tabWidget"); - QWidget* dicewareWidget = editEntryWidget->findChild("dicewareWidget"); + auto* tabWidget = editEntryWidget->findChild("tabWidget"); + auto* dicewareWidget = editEntryWidget->findChild("dicewareWidget"); tabWidget->setCurrentWidget(dicewareWidget); - QComboBox* comboBoxWordList = dicewareWidget->findChild("comboBoxWordList"); + auto* comboBoxWordList = dicewareWidget->findChild("comboBoxWordList"); comboBoxWordList->setCurrentText("eff_large.wordlist"); - QSpinBox* spinBoxWordCount = dicewareWidget->findChild("spinBoxWordCount"); + auto* spinBoxWordCount = dicewareWidget->findChild("spinBoxWordCount"); spinBoxWordCount->setValue(6); // Type in some password - QLabel* entropyLabel = editEntryWidget->findChild("entropyLabel"); - QLabel* strengthLabel = editEntryWidget->findChild("strengthLabel"); + auto* entropyLabel = editEntryWidget->findChild("entropyLabel"); + auto* strengthLabel = editEntryWidget->findChild("strengthLabel"); QCOMPARE(entropyLabel->text(), QString("Entropy: 77.55 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Good")); @@ -703,8 +730,8 @@ void TestGui::testDicewareEntryEntropy() void TestGui::testTotp() { - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); - EntryView* entryView = m_dbWidget->findChild("entryView"); + auto* toolBar = m_mainWindow->findChild("toolBar"); + auto* entryView = m_dbWidget->findChild("entryView"); QCOMPARE(entryView->model()->rowCount(), 1); @@ -716,36 +743,36 @@ void TestGui::testTotp() triggerAction("actionEntrySetupTotp"); - TotpSetupDialog* setupTotpDialog = m_dbWidget->findChild("TotpSetupDialog"); + auto* setupTotpDialog = m_dbWidget->findChild("TotpSetupDialog"); - Tools::wait(100); + QApplication::processEvents(); - QLineEdit* seedEdit = setupTotpDialog->findChild("seedEdit"); + auto* seedEdit = setupTotpDialog->findChild("seedEdit"); QString exampleSeed = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; QTest::keyClicks(seedEdit, exampleSeed); - QDialogButtonBox* setupTotpButtonBox = setupTotpDialog->findChild("buttonBox"); + auto* setupTotpButtonBox = setupTotpDialog->findChild("buttonBox"); QTest::mouseClick(setupTotpButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); - QAction* entryEditAction = m_mainWindow->findChild("actionEntryEdit"); + auto* entryEditAction = m_mainWindow->findChild("actionEntryEdit"); QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction); QTest::mouseClick(entryEditWidget, Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); editEntryWidget->setCurrentPage(1); - QPlainTextEdit* attrTextEdit = editEntryWidget->findChild("attributesEdit"); + auto* attrTextEdit = editEntryWidget->findChild("attributesEdit"); QTest::mouseClick(editEntryWidget->findChild("revealAttributeButton"), Qt::LeftButton); QCOMPARE(attrTextEdit->toPlainText(), exampleSeed); - QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); triggerAction("actionEntryTotp"); - TotpDialog* totpDialog = m_dbWidget->findChild("TotpDialog"); - QLabel* totpLabel = totpDialog->findChild("totpLabel"); + auto* totpDialog = m_dbWidget->findChild("TotpDialog"); + auto* totpLabel = totpDialog->findChild("totpLabel"); QCOMPARE(totpLabel->text().replace(" ", ""), entry->totp()); } @@ -755,16 +782,16 @@ void TestGui::testSearch() // Add canned entries for consistent testing Q_UNUSED(addCannedEntries()); - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + auto* toolBar = m_mainWindow->findChild("toolBar"); - SearchWidget* searchWidget = toolBar->findChild("SearchWidget"); + auto* searchWidget = toolBar->findChild("SearchWidget"); QVERIFY(searchWidget->isEnabled()); - QLineEdit* searchTextEdit = searchWidget->findChild("searchEdit"); + auto* searchTextEdit = searchWidget->findChild("searchEdit"); - EntryView* entryView = m_dbWidget->findChild("entryView"); + auto* entryView = m_dbWidget->findChild("entryView"); QVERIFY(entryView->isVisible()); - QAction* clearButton = searchWidget->findChild("clearIcon"); + auto* clearButton = searchWidget->findChild("clearIcon"); QVERIFY(!clearButton->isVisible()); // Enter search @@ -801,7 +828,7 @@ void TestGui::testSearch() QTest::keyClick(searchTextEdit, Qt::Key_Down); QTRY_VERIFY(entryView->hasFocus()); // Restore focus and search text selection - QTest::keyClick(m_mainWindow, Qt::Key_F, Qt::ControlModifier); + QTest::keyClick(m_mainWindow.data(), Qt::Key_F, Qt::ControlModifier); QTRY_COMPARE(searchTextEdit->selectedText(), QString("someTHING")); // Ensure Down focuses on entry view when search text is selected QTest::keyClick(searchTextEdit, Qt::Key_Down); @@ -862,7 +889,7 @@ void TestGui::testSearch() QCOMPARE(entry->title(), origTitle.append("_edited")); // Cancel search, should return to normal view - QTest::keyClick(m_mainWindow, Qt::Key_Escape); + QTest::keyClick(m_mainWindow.data(), Qt::Key_Escape); QTRY_COMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode); } @@ -871,10 +898,10 @@ void TestGui::testDeleteEntry() // Add canned entries for consistent testing Q_UNUSED(addCannedEntries()); - GroupView* groupView = m_dbWidget->findChild("groupView"); - EntryView* entryView = m_dbWidget->findChild("entryView"); - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); - QAction* entryDeleteAction = m_mainWindow->findChild("actionEntryDelete"); + auto* groupView = m_dbWidget->findChild("groupView"); + auto* entryView = m_dbWidget->findChild("entryView"); + auto* toolBar = m_mainWindow->findChild("toolBar"); + auto* entryDeleteAction = m_mainWindow->findChild("actionEntryDelete"); QWidget* entryDeleteWidget = toolBar->widgetForAction(entryDeleteAction); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode); @@ -934,7 +961,7 @@ void TestGui::testDeleteEntry() void TestGui::testCloneEntry() { - EntryView* entryView = m_dbWidget->findChild("entryView"); + auto* entryView = m_dbWidget->findChild("entryView"); QCOMPARE(entryView->model()->rowCount(), 1); @@ -944,8 +971,8 @@ void TestGui::testCloneEntry() triggerAction("actionEntryClone"); - CloneDialog* cloneDialog = m_dbWidget->findChild("CloneDialog"); - QDialogButtonBox* cloneButtonBox = cloneDialog->findChild("buttonBox"); + auto* cloneDialog = m_dbWidget->findChild("CloneDialog"); + auto* cloneButtonBox = cloneDialog->findChild("buttonBox"); QTest::mouseClick(cloneButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); QCOMPARE(entryView->model()->rowCount(), 2); @@ -956,11 +983,11 @@ void TestGui::testCloneEntry() void TestGui::testEntryPlaceholders() { - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); - EntryView* entryView = m_dbWidget->findChild("entryView"); + auto* toolBar = m_mainWindow->findChild("toolBar"); + auto* entryView = m_dbWidget->findChild("entryView"); // Find the new entry action - QAction* entryNewAction = m_mainWindow->findChild("actionEntryNew"); + auto* entryNewAction = m_mainWindow->findChild("actionEntryNew"); QVERIFY(entryNewAction->isEnabled()); // Find the button associated with the new entry action @@ -973,14 +1000,14 @@ void TestGui::testEntryPlaceholders() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); // Add entry "test" and confirm added - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); QTest::keyClicks(titleEdit, "test"); QLineEdit* usernameEdit = editEntryWidget->findChild("usernameEdit"); QTest::keyClicks(usernameEdit, "john"); QLineEdit* urlEdit = editEntryWidget->findChild("urlEdit"); QTest::keyClicks(urlEdit, "{TITLE}.{USERNAME}"); - QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); QCOMPARE(entryView->model()->rowCount(), 2); @@ -1000,8 +1027,8 @@ void TestGui::testEntryPlaceholders() void TestGui::testDragAndDropEntry() { - EntryView* entryView = m_dbWidget->findChild("entryView"); - GroupView* groupView = m_dbWidget->findChild("groupView"); + auto* entryView = m_dbWidget->findChild("entryView"); + auto* groupView = m_dbWidget->findChild("groupView"); QAbstractItemModel* groupModel = groupView->model(); QModelIndex sourceIndex = entryView->model()->index(0, 1); @@ -1029,11 +1056,7 @@ void TestGui::testDragAndDropGroup() // dropping parent on child is supposed to fail dragAndDropGroup(groupModel->index(0, 0, rootIndex), - groupModel->index(0, 0, groupModel->index(0, 0, rootIndex)), - -1, - false, - "NewDatabase", - 0); + groupModel->index(0, 0, groupModel->index(0, 0, rootIndex)), -1, false, "NewDatabase", 0); dragAndDropGroup(groupModel->index(1, 0, rootIndex), rootIndex, 0, true, "NewDatabase", 0); @@ -1063,6 +1086,7 @@ void TestGui::testSaveAs() fileInfo.refresh(); QCOMPARE(fileInfo.lastModified(), lastModified); + tmpFile.remove(); } void TestGui::testSave() @@ -1123,7 +1147,7 @@ void TestGui::testKeePass1Import() // Close the KeePass1 Database MessageBox::setNextAnswer(QMessageBox::No); triggerAction("actionDatabaseClose"); - Tools::wait(100); + QApplication::processEvents(); } void TestGui::testDatabaseLocking() @@ -1135,13 +1159,13 @@ void TestGui::testDatabaseLocking() QCOMPARE(m_tabWidget->tabText(0).remove('&'), origDbName + " [locked]"); - QAction* actionDatabaseMerge = m_mainWindow->findChild("actionDatabaseMerge", Qt::FindChildrenRecursively); + auto* actionDatabaseMerge = m_mainWindow->findChild("actionDatabaseMerge", Qt::FindChildrenRecursively); QCOMPARE(actionDatabaseMerge->isEnabled(), false); - QAction* actionDatabaseSave = m_mainWindow->findChild("actionDatabaseSave", Qt::FindChildrenRecursively); + auto* actionDatabaseSave = m_mainWindow->findChild("actionDatabaseSave", Qt::FindChildrenRecursively); QCOMPARE(actionDatabaseSave->isEnabled(), false); QWidget* dbWidget = m_tabWidget->currentDatabaseWidget(); - QWidget* unlockDatabaseWidget = dbWidget->findChild("unlockDatabaseWidget"); + auto* unlockDatabaseWidget = dbWidget->findChild("unlockDatabaseWidget"); QWidget* editPassword = unlockDatabaseWidget->findChild("editPassword"); QVERIFY(editPassword); @@ -1162,11 +1186,11 @@ void TestGui::testDragAndDropKdbxFiles() QMimeData badMimeData; badMimeData.setUrls({QUrl::fromLocalFile(badDatabaseFilePath)}); QDragEnterEvent badDragEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier); - qApp->notify(m_mainWindow, &badDragEvent); + qApp->notify(m_mainWindow.data(), &badDragEvent); QCOMPARE(badDragEvent.isAccepted(), false); QDropEvent badDropEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier); - qApp->notify(m_mainWindow, &badDropEvent); + qApp->notify(m_mainWindow.data(), &badDropEvent); QCOMPARE(badDropEvent.isAccepted(), false); QCOMPARE(m_tabWidget->count(), openedDatabasesCount); @@ -1175,20 +1199,19 @@ void TestGui::testDragAndDropKdbxFiles() QMimeData goodMimeData; goodMimeData.setUrls({QUrl::fromLocalFile(goodDatabaseFilePath)}); QDragEnterEvent goodDragEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier); - qApp->notify(m_mainWindow, &goodDragEvent); + qApp->notify(m_mainWindow.data(), &goodDragEvent); QCOMPARE(goodDragEvent.isAccepted(), true); QDropEvent goodDropEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier); - qApp->notify(m_mainWindow, &goodDropEvent); + qApp->notify(m_mainWindow.data(), &goodDropEvent); QCOMPARE(goodDropEvent.isAccepted(), true); QCOMPARE(m_tabWidget->count(), openedDatabasesCount + 1); MessageBox::setNextAnswer(QMessageBox::No); triggerAction("actionDatabaseClose"); - Tools::wait(100); - QCOMPARE(m_tabWidget->count(), openedDatabasesCount); + QTRY_COMPARE(m_tabWidget->count(), openedDatabasesCount); } void TestGui::testTrayRestoreHide() @@ -1197,29 +1220,20 @@ void TestGui::testTrayRestoreHide() QSKIP("QSystemTrayIcon::isSystemTrayAvailable() = false, skipping tray restore/hide test..."); } - QSystemTrayIcon* trayIcon = m_mainWindow->findChild(); + auto* trayIcon = m_mainWindow->findChild(); QVERIFY(m_mainWindow->isVisible()); trayIcon->activated(QSystemTrayIcon::Trigger); - Tools::wait(100); - QVERIFY(!m_mainWindow->isVisible()); + QTRY_VERIFY(!m_mainWindow->isVisible()); trayIcon->activated(QSystemTrayIcon::Trigger); - Tools::wait(100); - QVERIFY(m_mainWindow->isVisible()); + QTRY_VERIFY(m_mainWindow->isVisible()); trayIcon->activated(QSystemTrayIcon::Trigger); - Tools::wait(100); - QVERIFY(!m_mainWindow->isVisible()); + QTRY_VERIFY(!m_mainWindow->isVisible()); trayIcon->activated(QSystemTrayIcon::Trigger); - Tools::wait(100); - QVERIFY(m_mainWindow->isVisible()); -} - -void TestGui::cleanupTestCase() -{ - delete m_mainWindow; + QTRY_VERIFY(m_mainWindow->isVisible()); } int TestGui::addCannedEntries() @@ -1227,17 +1241,17 @@ int TestGui::addCannedEntries() int entries_added = 0; // Find buttons - QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + auto* toolBar = m_mainWindow->findChild("toolBar"); QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); - EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); - QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); - QLineEdit* passwordEdit = editEntryWidget->findChild("passwordEdit"); - QLineEdit* passwordRepeatEdit = editEntryWidget->findChild("passwordRepeatEdit"); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* passwordEdit = editEntryWidget->findChild("passwordEdit"); + auto* passwordRepeatEdit = editEntryWidget->findChild("passwordRepeatEdit"); // Add entry "test" and confirm added QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "test"); - QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); ++entries_added; @@ -1274,7 +1288,7 @@ void TestGui::checkDatabase(QString dbFileName) void TestGui::triggerAction(const QString& name) { - QAction* action = m_mainWindow->findChild(name); + auto* action = m_mainWindow->findChild(name); QVERIFY(action); QVERIFY(action->isEnabled()); action->trigger(); @@ -1312,5 +1326,3 @@ void TestGui::clickIndex(const QModelIndex& index, { QTest::mouseClick(view->viewport(), button, stateKey, view->visualRect(index).center()); } - -QTEST_MAIN(TestGui) diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index dc8a05e9b8..5e0b441b1d 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -19,17 +19,18 @@ #ifndef KEEPASSX_TESTGUI_H #define KEEPASSX_TESTGUI_H -#include "TemporaryFile.h" +#include "gui/MainWindow.h" #include #include #include +#include +#include class Database; class DatabaseTabWidget; class DatabaseWidget; class QAbstractItemView; -class MainWindow; class TestGui : public QObject { @@ -84,12 +85,12 @@ private slots: Qt::MouseButton button, Qt::KeyboardModifiers stateKey = 0); - QPointer m_mainWindow; + QScopedPointer m_mainWindow; QPointer m_tabWidget; QPointer m_dbWidget; QPointer m_db; QByteArray m_dbData; - TemporaryFile m_dbFile; + QScopedPointer m_dbFile; QString m_dbFileName; QString m_dbFilePath; };