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.
This commit is contained in:
Janek Bevendorff 2018-09-29 19:00:47 +02:00
parent 18b22834c1
commit 113c8eb702
67 changed files with 2259 additions and 1250 deletions

View File

@ -303,16 +303,16 @@ endif()
include(CLangFormat)
if(UNIX AND NOT APPLE)
find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Svg Test LinguistTools DBus REQUIRED)
find_package(Qt5 COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools DBus REQUIRED)
elseif(APPLE)
find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Svg Test LinguistTools REQUIRED
find_package(Qt5 COMPONENTS Core Network Concurrent Gui Svg 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 Svg Test LinguistTools REQUIRED)
find_package(Qt5 COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools REQUIRED)
endif()
if(Qt5Core_VERSION VERSION_LESS "5.2.0")
@ -345,9 +345,7 @@ set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS_NONE QT_NO_DEBUG)
find_package(LibGPGError REQUIRED)
find_package(Gcrypt 1.7.0 REQUIRED)
find_package(Argon2 REQUIRED)
find_package(ZLIB REQUIRED)
find_package(QREncode REQUIRED)
set(CMAKE_REQUIRED_INCLUDES ${ZLIB_INCLUDE_DIR})

View File

@ -57,7 +57,9 @@ RUN set -x \
mesa-common-dev \
libyubikey-dev \
libykpers-1-dev \
libqrencode-dev
libqrencode-dev \
xclip \
xvfb
ENV PATH="/opt/${QT5_VERSION}/bin:${PATH}"
ENV CMAKE_PREFIX_PATH="/opt/${QT5_VERSION}/lib/cmake"

View File

@ -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 \
@ -56,6 +57,7 @@ RUN set -x \
libxi-dev \
libxtst-dev \
libqrencode-dev \
xclip \
xvfb
ENV PATH="/opt/${QT5_VERSION}/bin:${PATH}"

View File

@ -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

View File

@ -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();

View File

@ -485,11 +485,6 @@ QString BrowserSettings::generatePassword()
}
}
int BrowserSettings::getbits()
{
return m_passwordGenerator.getbits();
}
void BrowserSettings::updateBinaryPaths(QString customProxyLocation)
{
bool isProxy = supportBrowserProxy();

View File

@ -112,7 +112,6 @@ public:
PasswordGenerator::CharClasses passwordCharClasses();
PasswordGenerator::GeneratorFlags passwordGeneratorFlags();
QString generatePassword();
int getbits();
void updateBinaryPaths(QString customProxyLocation = QString());
private:

View File

@ -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<size_t>(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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();

View File

@ -24,6 +24,7 @@
#include <QTextStream>
#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;
}

View File

@ -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<size_t>(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;
}

View File

@ -16,6 +16,7 @@
*/
#include "Estimate.h"
#include "cli/Utils.h"
#include <QCommandLineParser>
#include <QTextStream>
@ -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<int>(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<int>(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));

View File

@ -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<CompositeKey>::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<FileKey>::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"
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.");
errorTextStream << endl;
"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<Database> 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;
}

View File

@ -19,6 +19,7 @@
#include <stdio.h>
#include "Generate.h"
#include "cli/Utils.h"
#include <QCommandLineParser>
#include <QTextStream>
@ -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<size_t>(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;
}

View File

@ -19,6 +19,7 @@
#include <stdio.h>
#include "List.h"
#include "cli/Utils.h"
#include <QCommandLineParser>
#include <QTextStream>
@ -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<Database> 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;
}

View File

@ -26,7 +26,7 @@ public:
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

View File

@ -1,3 +1,5 @@
#include <utility>
/*
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
@ -25,6 +27,7 @@
#include <QTextStream>
#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<Database> 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;
}

View File

@ -26,7 +26,7 @@ public:
Locate();
~Locate();
int execute(const QStringList& arguments);
int locateEntry(Database* database, QString searchTerm);
int locateEntry(Database* database, const QString& searchTerm);
};
#endif // KEEPASSXC_LOCATE_H

View File

@ -15,8 +15,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cstdlib>
#include "Merge.h"
#include <QCommandLineParser>
@ -24,6 +22,9 @@
#include "core/Database.h"
#include "core/Merger.h"
#include "cli/Utils.h"
#include <cstdlib>
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<Database> db1(Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR));
if (!db1) {
return EXIT_FAILURE;
}
Database* db2;
QScopedPointer<Database> 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;
}

View File

@ -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<Database> 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;

View File

@ -28,7 +28,7 @@ public:
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

View File

@ -15,17 +15,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Show.h"
#include <cstdlib>
#include <stdio.h>
#include "Show.h"
#include <QCommandLineParser>
#include <QTextStream>
#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<Database> 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;
}

View File

@ -26,7 +26,7 @@ public:
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

View File

@ -25,9 +25,25 @@
#endif
#include <QProcess>
#include <QTextStream>
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)
{
static QTextStream inputTextStream(stdin, QIODevice::ReadOnly);
static QTextStream outputTextStream(stdout, QIODevice::WriteOnly);
nextPasswords.append(password);
}
/**
* Read a user password from STDIN or return a password previously
* set by \link setNextPassword().
*
* @return the password
*/
QString getPassword()
{
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

View File

@ -19,13 +19,18 @@
#define KEEPASSXC_UTILS_H
#include <QtCore/qglobal.h>
#include <QTextStream>
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

View File

@ -25,7 +25,7 @@
#include <cli/Command.h>
#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;

236
src/core/Bootstrap.cpp Normal file
View File

@ -0,0 +1,236 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "Bootstrap.h"
#include "core/Config.h"
#include "core/Translator.h"
#ifdef Q_OS_WIN
#include <aclapi.h> // for createWindowsDACL()
#include <windows.h> // 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<PTOKEN_USER>(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<PACL>(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

34
src/core/Bootstrap.h Normal file
View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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

View File

@ -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<con
return db;
}
Database* Database::unlockFromStdin(QString databaseFilename, QString keyFilename)
Database* Database::unlockFromStdin(QString databaseFilename, QString keyFilename, FILE* outputDescriptor, FILE* errorDescriptor)
{
auto compositeKey = QSharedPointer<CompositeKey>::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<PasswordKey>::create();
@ -518,11 +518,19 @@ Database* Database::unlockFromStdin(QString databaseFilename, QString keyFilenam
if (!keyFilename.isEmpty()) {
auto fileKey = QSharedPointer<FileKey>::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);
}

View File

@ -131,7 +131,8 @@ public:
static Database* databaseByUuid(const QUuid& uuid);
static Database* openDatabaseFile(const QString& fileName, QSharedPointer<const CompositeKey> 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);

View File

@ -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<quint32>(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<quint32>(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<quint32>(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<quint32>(passwordChars.size()));
password.append(passwordChars[pos]);
}
@ -111,21 +111,6 @@ QString PasswordGenerator::generatePassword() const
return password;
}
int PasswordGenerator::getbits() const
{
const QVector<PasswordGroup> groups = passwordGroups();
int bits = 0;
QVector<QChar> 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<PasswordGroup> PasswordGenerator::passwordGroups() const
@ -298,9 +280,9 @@ QVector<PasswordGroup> PasswordGenerator::passwordGroups() const
j = group.indexOf(ch);
}
}
if (group.size() > 0) {
if (!group.isEmpty()) {
passwordGroups.replace(i, group);
i++;
++i;
} else {
passwordGroups.remove(i);
}

View File

@ -66,7 +66,6 @@ public:
bool isValid() const;
QString generatePassword() const;
int getbits() const;
static const int DefaultLength = 16;
static const char* DefaultExcludedChars;

View File

@ -18,19 +18,20 @@
*/
#include "Tools.h"
#include "core/Config.h"
#include "core/Translator.h"
#include <QCoreApplication>
#include <QIODevice>
#include <QImageReader>
#include <QLocale>
#include <QStringList>
#include <cctype>
#include <QElapsedTimer>
#include <cctype>
#ifdef Q_OS_WIN
#include <aclapi.h> // for SetSecurityInfo()
#include <windows.h> // for Sleep(), SetDllDirectoryA(), SetSearchPathMode(), ...
#include <windows.h> // for Sleep()
#endif
#ifdef Q_OS_UNIX
@ -56,9 +57,8 @@
namespace Tools
{
QString humanReadableFileSize(qint64 bytes, quint32 precision)
{
QString humanReadableFileSize(qint64 bytes, quint32 precision)
{
constexpr auto kibibyte = 1024;
double size = bytes;
@ -75,10 +75,10 @@ namespace Tools
}
return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i));
}
}
bool hasChild(const QObject* parent, const QObject* child)
{
bool hasChild(const QObject* parent, const QObject* child)
{
if (!parent || !child) {
return false;
}
@ -90,10 +90,10 @@ namespace Tools
}
}
return false;
}
}
bool readFromDevice(QIODevice* device, QByteArray& data, int size)
{
bool readFromDevice(QIODevice* device, QByteArray& data, int size)
{
QByteArray buffer;
buffer.resize(size);
@ -105,10 +105,10 @@ namespace Tools
data = buffer;
return true;
}
}
}
bool readAllFromDevice(QIODevice* device, QByteArray& data)
{
bool readAllFromDevice(QIODevice* device, QByteArray& data)
{
QByteArray result;
qint64 readBytes = 0;
qint64 readResult;
@ -118,7 +118,8 @@ namespace Tools
if (readResult > 0) {
readBytes += readResult;
}
} while (readResult > 0);
}
while (readResult > 0);
if (readResult == -1) {
return false;
@ -127,10 +128,10 @@ namespace Tools
data = result;
return true;
}
}
}
QString imageReaderFilter()
{
QString imageReaderFilter()
{
const QList<QByteArray> formats = QImageReader::supportedImageFormats();
QStringList formatsStringList;
@ -145,10 +146,10 @@ namespace Tools
}
return formatsStringList.join(" ");
}
}
bool isHex(const QByteArray& ba)
{
bool isHex(const QByteArray& ba)
{
for (const unsigned char c : ba) {
if (!std::isxdigit(c)) {
return false;
@ -156,20 +157,20 @@ namespace Tools
}
return true;
}
}
bool isBase64(const QByteArray& ba)
{
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)
{
void sleep(int ms)
{
Q_ASSERT(ms >= 0);
if (ms == 0) {
@ -180,14 +181,14 @@ namespace Tools
Sleep(uint(ms));
#else
timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000 * 1000;
ts.tv_sec = ms/1000;
ts.tv_nsec = (ms%1000)*1000*1000;
nanosleep(&ts, nullptr);
#endif
}
}
void wait(int ms)
{
void wait(int ms)
{
Q_ASSERT(ms >= 0);
if (ms == 0) {
@ -208,142 +209,9 @@ namespace Tools
QCoreApplication::processEvents(QEventLoop::AllEvents, timeLeft);
sleep(10);
}
} while (!timer.hasExpired(ms));
}
while (!timer.hasExpired(ms));
}
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.");
}
}
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
}
//
// 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<PTOKEN_USER>(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<PACL>(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;
}
}
} // namespace Tools

View File

@ -30,23 +30,19 @@ 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);
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 <typename RandomAccessIterator, typename T>
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)
{
template <typename RandomAccessIterator, typename T>
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)
{
RandomAccessIterator it = std::lower_bound(begin, end, value);
if ((it == end) || (value < *it)) {
@ -54,7 +50,7 @@ namespace Tools
} else {
return it;
}
}
}
} // namespace Tools

View File

@ -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);

View File

@ -34,7 +34,6 @@ public:
explicit CryptoHash(Algorithm algo, bool hmac = false);
~CryptoHash();
void addData(const QByteArray& data);
void reset();
QByteArray result() const;
void setKey(const QByteArray& data);

View File

@ -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 {};
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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());

View File

@ -1,3 +1,5 @@
#include <utility>
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
@ -18,6 +20,9 @@
#include "KdbxReader.h"
#include "core/Database.h"
#include "core/Endian.h"
#include "format/KdbxXmlWriter.h"
#include <QBuffer>
#define UUID_LENGTH 16
@ -92,7 +97,14 @@ Database* KdbxReader::readDatabase(QIODevice* device, QSharedPointer<const Compo
}
// read payload
return readDatabaseImpl(device, headerStream.storedData(), key, keepDatabase);
auto* db = readDatabaseImpl(device, headerStream.storedData(), std::move(key), keepDatabase);
if (saveXml()) {
m_xmlData.clear();
decryptXmlInnerStream(m_xmlData, db);
}
return db;
}
bool KdbxReader::hasError() const
@ -258,6 +270,23 @@ void KdbxReader::setInnerRandomStreamID(const QByteArray& data)
m_irsAlgo = irsAlgo;
}
/**
* Decrypt protected inner stream fields in XML dump on demand.
* Without the stream key from the KDBX header, the values become worthless.
*
* @param xmlOutput XML dump with decrypted fields
* @param db the database object for which to generate the decrypted XML dump
*/
void KdbxReader::decryptXmlInnerStream(QByteArray& xmlOutput, Database* db) const
{
QBuffer buffer;
buffer.setBuffer(&xmlOutput);
buffer.open(QIODevice::WriteOnly);
KdbxXmlWriter writer(m_kdbxVersion);
writer.disableInnerStreamProtection(true);
writer.writeDatabase(&buffer, db);
}
/**
* Raise an error. Use in case of an unexpected read error.
*

View File

@ -86,6 +86,8 @@ protected:
void raiseError(const QString& errorMessage);
void decryptXmlInnerStream(QByteArray& xmlOutput, Database* db) const;
QScopedPointer<Database> m_db;
QPair<quint32, quint32> m_kdbxSignature;

View File

@ -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;

View File

@ -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;
}

View File

@ -41,6 +41,8 @@ public:
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 @@ private:
const quint32 m_kdbxVersion;
bool m_innerStreamProtectionDisabled = false;
QXmlStreamWriter m_xml;
QPointer<Database> m_db;
QPointer<Metadata> m_meta;

View File

@ -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<QPair<QUuid, QString>> 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"))
};

View File

@ -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;

View File

@ -93,7 +93,7 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, QSharedPointer<const C
}
m_reader->setSaveXml(m_saveXml);
return m_reader->readDatabase(device, key, keepDatabase);
return m_reader->readDatabase(device, std::move(key), keepDatabase);
}
bool KeePass2Reader::hasError() const

View File

@ -22,8 +22,8 @@
#include <QApplication>
#include <QtNetwork/QLocalServer>
class QLockFile;
class QLockFile;
class QSocketNotifier;
class Application : public QApplication

View File

@ -216,7 +216,7 @@ private:
void setIconFromParent();
void replaceDatabase(Database* db);
Database* m_db;
QPointer<Database> m_db;
QWidget* m_mainWidget;
EditEntryWidget* m_editEntryWidget;
EditEntryWidget* m_historyEditEntryWidget;

View File

@ -81,7 +81,7 @@ void DatabaseSettingsWidgetEncryption::initialize()
}
if (!m_db->key()) {
m_db->setKey(QSharedPointer<CompositeKey>::create());
m_db->setCipher(KeePass2::CIPHER_AES);
m_db->setCipher(KeePass2::CIPHER_AES256);
isDirty = true;
}

View File

@ -16,19 +16,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QCommandLineParser>
#include <QFile>
#include <QTextStream>
#include <QCommandLineParser>
#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,43 +44,19 @@ 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");
@ -90,10 +65,8 @@ int main(int argc, char** argv)
QCommandLineOption pwstdinOption("pw-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");
QCommandLineOption helpOption = parser.addHelpOption();
QCommandLineOption versionOption = parser.addVersionOption();
@ -115,9 +88,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;
}
@ -135,46 +106,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) {
@ -193,7 +132,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

View File

@ -165,6 +165,9 @@ add_unit_test(NAME testentry SOURCES TestEntry.cpp
add_unit_test(NAME testmerge SOURCES TestMerge.cpp
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)

756
tests/TestCli.cpp Normal file
View File

@ -0,0 +1,756 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 <QFile>
#include <QClipboard>
#include <QFuture>
#include <QtConcurrent>
#include <QSet>
#include <cstdio>
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<Database> TestCli::readTestDatabase() const
{
Utils::setNextPassword("a");
auto db = QSharedPointer<Database>(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<void> 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<QString>("input");
QTest::addColumn<QString>("length");
QTest::addColumn<QString>("entropy");
QTest::addColumn<QString>("log10");
QTest::addColumn<QStringList>("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<Database> 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<QStringList>("parameters");
QTest::addColumn<QString>("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<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::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<Database> 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<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create("a"));
QFile readBack(m_dbFile->fileName());
readBack.open(QIODevice::ReadOnly);
QScopedPointer<Database> 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"));
}

69
tests/TestCli.h Normal file
View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_TESTCLI_H
#define KEEPASSXC_TESTCLI_H
#include "core/Database.h"
#include <QTest>
#include <QTextStream>
#include <QFile>
#include <QScopedPointer>
#include <QTemporaryFile>
class TestCli : public QObject
{
Q_OBJECT
private:
QSharedPointer<Database> 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<QTemporaryFile> m_dbFile;
QScopedPointer<QTemporaryFile> m_stdoutFile;
QScopedPointer<QTemporaryFile> m_stderrFile;
QScopedPointer<QTemporaryFile> m_stdinFile;
FILE* m_stdoutHandle = stdout;
FILE* m_stderrHandle = stderr;
FILE* m_stdinHandle = stdin;
};
#endif //KEEPASSXC_TESTCLI_H

View File

@ -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;

View File

@ -0,0 +1,131 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "TestPasswordGenerator.h"
#include "core/PasswordGenerator.h"
#include "crypto/Crypto.h"
#include <QTest>
#include <QRegularExpression>
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());
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_TESTPASSWORDGENERATOR_H
#define KEEPASSXC_TESTPASSWORDGENERATOR_H
#include <QObject>
class TestPasswordGenerator : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testCharClasses();
void testLookalikeExclusion();
};
#endif //KEEPASSXC_TESTPASSWORDGENERATOR_H

View File

@ -26,32 +26,110 @@
#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()
{
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::testEncryptionDecryption_data()
{
QTest::addColumn<SymmetricCipher::Algorithm>("algorithm");
QTest::addColumn<SymmetricCipher::Mode>("mode");
QTest::addColumn<SymmetricCipher::Direction>("direction");
QTest::addColumn<QByteArray>("key");
QTest::addColumn<QByteArray>("iv");
QTest::addColumn<QByteArray>("plainText");
QTest::addColumn<QByteArray>("cipherText");
// 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::testEncryptionDecryption()
{
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("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);
SymmetricCipher cipher(algorithm, mode, direction);
QVERIFY(cipher.init(key, iv));
QCOMPARE(cipher.blockSize(), 16);
QCOMPARE(cipher.process(plainText, &ok), cipherText);
QVERIFY(ok);
if (mode == SymmetricCipher::Cbc) {
QBuffer buffer;
SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Encrypt);
SymmetricCipherStream stream(&buffer, algorithm, mode, direction);
QVERIFY(stream.init(key, iv));
buffer.open(QIODevice::WriteOnly);
QVERIFY(stream.open(QIODevice::WriteOnly));
@ -76,26 +154,43 @@ void TestSymmetricCipher::testAes128CbcEncryption()
QCOMPARE(stream.write(plainText.left(10)), qint64(10));
stream.close();
QCOMPARE(buffer.data().size(), 16);
}
}
void TestSymmetricCipher::testAes128CbcDecryption()
void TestSymmetricCipher::testAesCbcPadding_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;
QTest::addColumn<QByteArray>("key");
QTest::addColumn<QByteArray>("iv");
QTest::addColumn<QByteArray>("cipherText");
QTest::addColumn<QByteArray>("plainText");
QTest::addColumn<QByteArray>("padding");
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);
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");
}
void TestSymmetricCipher::testAesCbcPadding()
{
QFETCH(QByteArray, key);
QFETCH(QByteArray, iv);
QFETCH(QByteArray, cipherText);
QFETCH(QByteArray, plainText);
QFETCH(QByteArray, padding);
// padded with 16 0x10 bytes
QByteArray cipherTextPadded = cipherText + QByteArray::fromHex("55e21d7100b988ffec32feeafaf23538");
QByteArray cipherTextPadded = cipherText + padding;
QBuffer buffer(&cipherTextPadded);
SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt);
QVERIFY(stream.init(key, iv));
@ -114,126 +209,48 @@ void TestSymmetricCipher::testAes128CbcDecryption()
QCOMPARE(stream.read(100), plainText);
}
void TestSymmetricCipher::testAes256CbcEncryption()
void TestSymmetricCipher::testInplaceEcb_data()
{
// http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
QTest::addColumn<QByteArray>("key");
QTest::addColumn<QByteArray>("plainText");
QTest::addColumn<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);
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);
QTest::newRow("AES128")
<< QByteArray::fromHex("2b7e151628aed2a6abf7158809cf4f3c")
<< QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a")
<< QByteArray::fromHex("3ad77bb40d7a3660a89ecaf32466ef97");
}
void TestSymmetricCipher::testAes256CbcDecryption()
void TestSymmetricCipher::testInplaceEcb()
{
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;
QFETCH(QByteArray, key);
QFETCH(QByteArray, plainText);
QFETCH(QByteArray, cipherText);
SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt);
QVERIFY(cipher.init(key, iv));
QCOMPARE(cipher.blockSize(), 16);
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);
QCOMPARE(cipher.process(cipherText, &ok), plainText);
QVERIFY(ok);
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);
// padded with 16 0x16 bytes
QByteArray cipherTextPadded = cipherText + QByteArray::fromHex("3a3aa5e0213db1a9901f9036cf5102d2");
QBuffer buffer(&cipherTextPadded);
SymmetricCipherStream stream(&buffer, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt);
QVERIFY(stream.init(key, iv));
buffer.open(QIODevice::ReadOnly);
QVERIFY(stream.open(QIODevice::ReadOnly));
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));
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);
}
void TestSymmetricCipher::testAes256CtrEncryption()
{
// 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);
}
void TestSymmetricCipher::testAes256CtrDecryption()
{
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);
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()

View File

@ -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();

View File

@ -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})

View File

@ -1,93 +0,0 @@
/*
* Copyright (C) 2016 Danny Su <contact@dannysu.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "TemporaryFile.h"
#include <QFileInfo>
#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
}

View File

@ -1,65 +0,0 @@
/*
* Copyright (C) 2016 Danny Su <contact@dannysu.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_TEMPORARYFILE_H
#define KEEPASSX_TEMPORARYFILE_H
#include <QFile>
#include <QObject>
#include <QTemporaryFile>
/**
* 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

View File

@ -18,6 +18,7 @@
#include "TestGui.h"
#include "TestGlobal.h"
#include "gui/Application.h"
#include <QAction>
#include <QApplication>
@ -33,12 +34,12 @@
#include <QPushButton>
#include <QSignalSpy>
#include <QSpinBox>
#include <QTemporaryFile>
#include <QTimer>
#include <QToolBar>
#include <QToolButton>
#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<DatabaseTabWidget*>("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<qint64>((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<qint64>((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<QWidget*>("databaseOpenWidget");
QLineEdit* editPassword = databaseOpenWidget->findChild<QLineEdit*>("editPassword");
auto* databaseOpenWidget = m_mainWindow->findChild<QWidget*>("databaseOpenWidget");
auto* editPassword = databaseOpenWidget->findChild<QLineEdit*>("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<CompositeKey>::create();
compositeKey->addKey(QSharedPointer<PasswordKey>::create("test"));
auto fileKey = QSharedPointer<FileKey>::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<QSlider*>("decryptionTimeSlider");
auto algorithmComboBox = wizard->currentPage()->findChild<QComboBox*>("algorithmComboBox");
QTRY_VERIFY(decryptionTimeSlider->isVisible());
QVERIFY(!algorithmComboBox->isVisible());
auto advancedToggle = wizard->currentPage()->findChild<QPushButton*>("advancedSettingsButton");
QTest::mouseClick(advancedToggle, Qt::MouseButton::LeftButton);
QTRY_VERIFY(!decryptionTimeSlider->isVisible());
QVERIFY(algorithmComboBox->isVisible());
auto rounds = wizard->currentPage()->findChild<QSpinBox*>("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<QSpinBox*>("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<QSpinBox*>("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<QLineEdit*>("enterPasswordEdit");
auto* passwordRepeatEdit = passwordWidget->findChild<QLineEdit*>("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<QWidget*>("databaseOpenMergeWidget");
QLineEdit* editPasswordMerge = databaseOpenMergeWidget->findChild<QLineEdit*>("editPassword");
auto* databaseOpenMergeWidget = m_mainWindow->findChild<QWidget*>("databaseOpenMergeWidget");
auto* editPasswordMerge = databaseOpenMergeWidget->findChild<QLineEdit*>("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<qint64>(unmodifiedMergeDatabase.size())));
m_dbFile.close();
Tools::wait(1500);
QVERIFY(m_dbFile->open());
QVERIFY(m_dbFile->write(unmodifiedMergeDatabase, static_cast<qint64>(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<qint64>(unmodifiedMergeDatabase.size())));
m_dbFile.close();
Tools::wait(1500);
m_dbFile->open();
QVERIFY(m_dbFile->write(unmodifiedMergeDatabase, static_cast<qint64>(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<qint64>(unmodifiedMergeDatabase.size())));
m_dbFile.close();
Tools::wait(1500);
QVERIFY(m_dbFile->open());
QVERIFY(m_dbFile->write(unmodifiedMergeDatabase, static_cast<qint64>(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<QToolBar*>("toolBar");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
int editCount = 0;
// Select the first entry in the database
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* entryView = m_dbWidget->findChild<EntryView*>("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<QAction*>("actionEntryEdit");
auto* entryEditAction = m_mainWindow->findChild<QAction*>("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*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "_test");
// Apply the edit
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("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<QPlainTextEdit*>("attributesEdit");
auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("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<QLineEdit*>("passwordEdit");
auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
QString originalPassword = passwordEdit->text();
passwordEdit->setText("newpass");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
MessageWidget* messageWiget = editEntryWidget->findChild<MessageWidget*>("messageWidget");
auto* messageWiget = editEntryWidget->findChild<MessageWidget*>("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*>("editGroupWidget");
QLineEdit* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
QDialogButtonBox* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("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<QToolBar*>("toolBar");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("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*>("SearchWidget");
QLineEdit* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("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<QToolBar*>("toolBar");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
// Find the new entry action
QAction* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
auto* entryNewAction = m_mainWindow->findChild<QAction*>("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*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "test");
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("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<QLineEdit*>("passwordEdit");
QLineEdit* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit");
auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
auto* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("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<QToolBar*>("toolBar");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
// Find the new entry action
QAction* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
auto* entryNewAction = m_mainWindow->findChild<QAction*>("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*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "test");
// Open the password generator
QToolButton* generatorButton = editEntryWidget->findChild<QToolButton*>("togglePasswordGeneratorButton");
auto* generatorButton = editEntryWidget->findChild<QToolButton*>("togglePasswordGeneratorButton");
QTest::mouseClick(generatorButton, Qt::LeftButton);
// Type in some password
QLineEdit* editNewPassword = editEntryWidget->findChild<QLineEdit*>("editNewPassword");
QLabel* entropyLabel = editEntryWidget->findChild<QLabel*>("entropyLabel");
QLabel* strengthLabel = editEntryWidget->findChild<QLabel*>("strengthLabel");
auto* editNewPassword = editEntryWidget->findChild<QLineEdit*>("editNewPassword");
auto* entropyLabel = editEntryWidget->findChild<QLabel*>("entropyLabel");
auto* strengthLabel = editEntryWidget->findChild<QLabel*>("strengthLabel");
editNewPassword->setText("");
QTest::keyClicks(editNewPassword, "hello");
@ -659,10 +686,10 @@ void TestGui::testPasswordEntryEntropy()
void TestGui::testDicewareEntryEntropy()
{
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
// Find the new entry action
QAction* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
auto* entryNewAction = m_mainWindow->findChild<QAction*>("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*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "test");
// Open the password generator
QToolButton* generatorButton = editEntryWidget->findChild<QToolButton*>("togglePasswordGeneratorButton");
auto* generatorButton = editEntryWidget->findChild<QToolButton*>("togglePasswordGeneratorButton");
QTest::mouseClick(generatorButton, Qt::LeftButton);
// Select Diceware
QTabWidget* tabWidget = editEntryWidget->findChild<QTabWidget*>("tabWidget");
QWidget* dicewareWidget = editEntryWidget->findChild<QWidget*>("dicewareWidget");
auto* tabWidget = editEntryWidget->findChild<QTabWidget*>("tabWidget");
auto* dicewareWidget = editEntryWidget->findChild<QWidget*>("dicewareWidget");
tabWidget->setCurrentWidget(dicewareWidget);
QComboBox* comboBoxWordList = dicewareWidget->findChild<QComboBox*>("comboBoxWordList");
auto* comboBoxWordList = dicewareWidget->findChild<QComboBox*>("comboBoxWordList");
comboBoxWordList->setCurrentText("eff_large.wordlist");
QSpinBox* spinBoxWordCount = dicewareWidget->findChild<QSpinBox*>("spinBoxWordCount");
auto* spinBoxWordCount = dicewareWidget->findChild<QSpinBox*>("spinBoxWordCount");
spinBoxWordCount->setValue(6);
// Type in some password
QLabel* entropyLabel = editEntryWidget->findChild<QLabel*>("entropyLabel");
QLabel* strengthLabel = editEntryWidget->findChild<QLabel*>("strengthLabel");
auto* entropyLabel = editEntryWidget->findChild<QLabel*>("entropyLabel");
auto* strengthLabel = editEntryWidget->findChild<QLabel*>("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<QToolBar*>("toolBar");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QCOMPARE(entryView->model()->rowCount(), 1);
@ -716,36 +743,36 @@ void TestGui::testTotp()
triggerAction("actionEntrySetupTotp");
TotpSetupDialog* setupTotpDialog = m_dbWidget->findChild<TotpSetupDialog*>("TotpSetupDialog");
auto* setupTotpDialog = m_dbWidget->findChild<TotpSetupDialog*>("TotpSetupDialog");
Tools::wait(100);
QApplication::processEvents();
QLineEdit* seedEdit = setupTotpDialog->findChild<QLineEdit*>("seedEdit");
auto* seedEdit = setupTotpDialog->findChild<QLineEdit*>("seedEdit");
QString exampleSeed = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
QTest::keyClicks(seedEdit, exampleSeed);
QDialogButtonBox* setupTotpButtonBox = setupTotpDialog->findChild<QDialogButtonBox*>("buttonBox");
auto* setupTotpButtonBox = setupTotpDialog->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(setupTotpButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
QAction* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode);
EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
editEntryWidget->setCurrentPage(1);
QPlainTextEdit* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
QCOMPARE(attrTextEdit->toPlainText(), exampleSeed);
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
triggerAction("actionEntryTotp");
TotpDialog* totpDialog = m_dbWidget->findChild<TotpDialog*>("TotpDialog");
QLabel* totpLabel = totpDialog->findChild<QLabel*>("totpLabel");
auto* totpDialog = m_dbWidget->findChild<TotpDialog*>("TotpDialog");
auto* totpLabel = totpDialog->findChild<QLabel*>("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<QToolBar*>("toolBar");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
SearchWidget* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
QVERIFY(searchWidget->isEnabled());
QLineEdit* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QVERIFY(entryView->isVisible());
QAction* clearButton = searchWidget->findChild<QAction*>("clearIcon");
auto* clearButton = searchWidget->findChild<QAction*>("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*>("groupView");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QAction* entryDeleteAction = m_mainWindow->findChild<QAction*>("actionEntryDelete");
auto* groupView = m_dbWidget->findChild<GroupView*>("groupView");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* entryDeleteAction = m_mainWindow->findChild<QAction*>("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*>("entryView");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QCOMPARE(entryView->model()->rowCount(), 1);
@ -944,8 +971,8 @@ void TestGui::testCloneEntry()
triggerAction("actionEntryClone");
CloneDialog* cloneDialog = m_dbWidget->findChild<CloneDialog*>("CloneDialog");
QDialogButtonBox* cloneButtonBox = cloneDialog->findChild<QDialogButtonBox*>("buttonBox");
auto* cloneDialog = m_dbWidget->findChild<CloneDialog*>("CloneDialog");
auto* cloneButtonBox = cloneDialog->findChild<QDialogButtonBox*>("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<QToolBar*>("toolBar");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
// Find the new entry action
QAction* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
auto* entryNewAction = m_mainWindow->findChild<QAction*>("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*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "test");
QLineEdit* usernameEdit = editEntryWidget->findChild<QLineEdit*>("usernameEdit");
QTest::keyClicks(usernameEdit, "john");
QLineEdit* urlEdit = editEntryWidget->findChild<QLineEdit*>("urlEdit");
QTest::keyClicks(urlEdit, "{TITLE}.{USERNAME}");
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("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*>("entryView");
GroupView* groupView = m_dbWidget->findChild<GroupView*>("groupView");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
auto* groupView = m_dbWidget->findChild<GroupView*>("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<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
auto* actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
QCOMPARE(actionDatabaseMerge->isEnabled(), false);
QAction* actionDatabaseSave = m_mainWindow->findChild<QAction*>("actionDatabaseSave", Qt::FindChildrenRecursively);
auto* actionDatabaseSave = m_mainWindow->findChild<QAction*>("actionDatabaseSave", Qt::FindChildrenRecursively);
QCOMPARE(actionDatabaseSave->isEnabled(), false);
QWidget* dbWidget = m_tabWidget->currentDatabaseWidget();
QWidget* unlockDatabaseWidget = dbWidget->findChild<QWidget*>("unlockDatabaseWidget");
auto* unlockDatabaseWidget = dbWidget->findChild<QWidget*>("unlockDatabaseWidget");
QWidget* editPassword = unlockDatabaseWidget->findChild<QLineEdit*>("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<QSystemTrayIcon*>();
auto* trayIcon = m_mainWindow->findChild<QSystemTrayIcon*>();
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<QToolBar*>("toolBar");
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QLineEdit* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
QLineEdit* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit");
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
auto* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit");
// Add entry "test" and confirm added
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QTest::keyClicks(titleEdit, "test");
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("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<QAction*>(name);
auto* action = m_mainWindow->findChild<QAction*>(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)

View File

@ -19,17 +19,18 @@
#ifndef KEEPASSX_TESTGUI_H
#define KEEPASSX_TESTGUI_H
#include "TemporaryFile.h"
#include "gui/MainWindow.h"
#include <QAbstractItemModel>
#include <QObject>
#include <QPointer>
#include <QScopedPointer>
#include <QTemporaryFile>
class Database;
class DatabaseTabWidget;
class DatabaseWidget;
class QAbstractItemView;
class MainWindow;
class TestGui : public QObject
{
@ -84,12 +85,12 @@ private:
Qt::MouseButton button,
Qt::KeyboardModifiers stateKey = 0);
QPointer<MainWindow> m_mainWindow;
QScopedPointer<MainWindow> m_mainWindow;
QPointer<DatabaseTabWidget> m_tabWidget;
QPointer<DatabaseWidget> m_dbWidget;
QPointer<Database> m_db;
QByteArray m_dbData;
TemporaryFile m_dbFile;
QScopedPointer<QTemporaryFile> m_dbFile;
QString m_dbFileName;
QString m_dbFilePath;
};