mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-30 17:56:29 -05:00
5142981018
* Significantly improve user experience when using hardware keys on databases in both GUI and CLI modes. Prevent locking up the YubiKey USB interface for prolonged periods of time. Allows for other apps to use the key concurrently with KeePassXC. * Improve messages displayed to user when finding keys and when user interaction is required. Output specific error messages when handling hardware keys during database read/write. * Only poll for keys when previously used or upon user request. Prevent continuously polling keys when accessing the UI such as switching tabs and minimize/maximize. * Add support for using multiple hardware keys simultaneously. Keys are identified by their serial number which prevents using the wrong key during open and save operations. * Fixes #4400 * Fixes #4065 * Fixes #1050 * Fixes #1215 * Fixes #3087 * Fixes #1088 * Fixes #1869
1858 lines
69 KiB
C++
1858 lines
69 KiB
C++
/*
|
|
* Copyright (C) 2019 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/Bootstrap.h"
|
|
#include "core/Config.h"
|
|
#include "core/Global.h"
|
|
#include "core/Tools.h"
|
|
#include "crypto/Crypto.h"
|
|
#include "keys/drivers/YubiKey.h"
|
|
|
|
#include "cli/Add.h"
|
|
#include "cli/AddGroup.h"
|
|
#include "cli/Analyze.h"
|
|
#include "cli/Clip.h"
|
|
#include "cli/Command.h"
|
|
#include "cli/Create.h"
|
|
#include "cli/Diceware.h"
|
|
#include "cli/Edit.h"
|
|
#include "cli/Estimate.h"
|
|
#include "cli/Export.h"
|
|
#include "cli/Generate.h"
|
|
#include "cli/Help.h"
|
|
#include "cli/Import.h"
|
|
#include "cli/Info.h"
|
|
#include "cli/List.h"
|
|
#include "cli/Locate.h"
|
|
#include "cli/Merge.h"
|
|
#include "cli/Move.h"
|
|
#include "cli/Open.h"
|
|
#include "cli/Remove.h"
|
|
#include "cli/RemoveGroup.h"
|
|
#include "cli/Show.h"
|
|
#include "cli/Utils.h"
|
|
|
|
#include <QClipboard>
|
|
#include <QFuture>
|
|
#include <QSet>
|
|
#include <QSignalSpy>
|
|
#include <QTextStream>
|
|
#include <QtConcurrent>
|
|
|
|
QTEST_MAIN(TestCli)
|
|
|
|
QSharedPointer<Database> globalCurrentDatabase;
|
|
|
|
void TestCli::initTestCase()
|
|
{
|
|
QVERIFY(Crypto::init());
|
|
|
|
Config::createTempFileInstance();
|
|
Bootstrap::bootstrapApplication();
|
|
|
|
auto fd = new QFile();
|
|
#ifdef Q_OS_WIN
|
|
fd->open(fopen("nul", "w"), QIODevice::WriteOnly);
|
|
#else
|
|
fd->open(fopen("/dev/null", "w"), QIODevice::WriteOnly);
|
|
#endif
|
|
Utils::DEVNULL.setDevice(fd);
|
|
}
|
|
|
|
void TestCli::init()
|
|
{
|
|
const auto file = QString(KEEPASSX_TEST_DATA_DIR).append("/%1");
|
|
|
|
m_dbFile.reset(new TemporaryFile());
|
|
m_dbFile->copyFromFile(file.arg("NewDatabase.kdbx"));
|
|
|
|
m_dbFile2.reset(new TemporaryFile());
|
|
m_dbFile2->copyFromFile(file.arg("NewDatabase2.kdbx"));
|
|
|
|
m_xmlFile.reset(new TemporaryFile());
|
|
m_xmlFile->copyFromFile(file.arg("NewDatabase.xml"));
|
|
|
|
m_keyFileProtectedDbFile.reset(new TemporaryFile());
|
|
m_keyFileProtectedDbFile->copyFromFile(file.arg("KeyFileProtected.kdbx"));
|
|
|
|
m_keyFileProtectedNoPasswordDbFile.reset(new TemporaryFile());
|
|
m_keyFileProtectedNoPasswordDbFile->copyFromFile(file.arg("KeyFileProtectedNoPassword.kdbx"));
|
|
|
|
m_yubiKeyProtectedDbFile.reset(new TemporaryFile());
|
|
m_yubiKeyProtectedDbFile->copyFromFile(file.arg("YubiKeyProtectedPasswords.kdbx"));
|
|
|
|
m_stdout.reset(new QBuffer());
|
|
m_stdout->open(QIODevice::ReadWrite);
|
|
Utils::STDOUT.setDevice(m_stdout.data());
|
|
|
|
m_stderr.reset(new QBuffer());
|
|
m_stderr->open(QIODevice::ReadWrite);
|
|
Utils::STDERR.setDevice(m_stderr.data());
|
|
|
|
m_stdin.reset(new QBuffer());
|
|
m_stdin->open(QIODevice::ReadWrite);
|
|
Utils::STDIN.setDevice(m_stdin.data());
|
|
}
|
|
|
|
void TestCli::cleanup()
|
|
{
|
|
m_dbFile.reset();
|
|
m_dbFile2.reset();
|
|
m_keyFileProtectedDbFile.reset();
|
|
m_keyFileProtectedNoPasswordDbFile.reset();
|
|
m_yubiKeyProtectedDbFile.reset();
|
|
|
|
Utils::STDOUT.setDevice(nullptr);
|
|
Utils::STDERR.setDevice(nullptr);
|
|
Utils::STDIN.setDevice(nullptr);
|
|
}
|
|
|
|
QSharedPointer<Database> TestCli::readDatabase(const QString& filename, const QString& pw, const QString& keyfile)
|
|
{
|
|
auto db = QSharedPointer<Database>::create();
|
|
auto key = QSharedPointer<CompositeKey>::create();
|
|
|
|
if (filename.isEmpty()) {
|
|
// Open the default test database
|
|
key->addKey(QSharedPointer<PasswordKey>::create("a"));
|
|
if (!db->open(m_dbFile->fileName(), key)) {
|
|
return {};
|
|
}
|
|
} else {
|
|
// Open the specified database file using supplied credentials
|
|
key->addKey(QSharedPointer<PasswordKey>::create(pw));
|
|
if (!keyfile.isEmpty()) {
|
|
auto filekey = QSharedPointer<FileKey>::create();
|
|
filekey->load(keyfile);
|
|
key->addKey(filekey);
|
|
}
|
|
|
|
if (!db->open(filename, key)) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
int TestCli::execCmd(Command& cmd, const QStringList& args) const
|
|
{
|
|
// Move to end of stream
|
|
m_stdout->readAll();
|
|
m_stderr->readAll();
|
|
|
|
// Record stream position
|
|
auto outPos = m_stdout->pos();
|
|
auto errPos = m_stderr->pos();
|
|
|
|
// Execute command
|
|
int ret = cmd.execute(args);
|
|
|
|
// Move back to recorded position
|
|
m_stdout->seek(outPos);
|
|
m_stderr->seek(errPos);
|
|
|
|
// Skip over blank lines
|
|
QByteArray newline("\n");
|
|
while (m_stdout->peek(1) == newline) {
|
|
m_stdout->readLine();
|
|
}
|
|
while (m_stderr->peek(1) == newline) {
|
|
m_stdout->readLine();
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool TestCli::isTotp(const QString& value)
|
|
{
|
|
static const QRegularExpression totp("^\\d{6}$");
|
|
return totp.match(value.trimmed()).hasMatch();
|
|
}
|
|
|
|
void TestCli::setInput(const QString& input)
|
|
{
|
|
setInput(QStringList(input));
|
|
}
|
|
|
|
void TestCli::setInput(const QStringList& input)
|
|
{
|
|
auto ba = input.join("\n").toLatin1();
|
|
// Always end in newline
|
|
if (!ba.endsWith("\n")) {
|
|
ba.append("\n");
|
|
}
|
|
auto pos = m_stdin->pos();
|
|
m_stdin->write(ba);
|
|
m_stdin->seek(pos);
|
|
}
|
|
|
|
void TestCli::testBatchCommands()
|
|
{
|
|
Commands::setupCommands(false);
|
|
QVERIFY(Commands::getCommand("add"));
|
|
QVERIFY(Commands::getCommand("analyze"));
|
|
QVERIFY(Commands::getCommand("clip"));
|
|
QVERIFY(Commands::getCommand("close"));
|
|
QVERIFY(Commands::getCommand("db-create"));
|
|
QVERIFY(Commands::getCommand("db-info"));
|
|
QVERIFY(Commands::getCommand("diceware"));
|
|
QVERIFY(Commands::getCommand("edit"));
|
|
QVERIFY(Commands::getCommand("estimate"));
|
|
QVERIFY(Commands::getCommand("export"));
|
|
QVERIFY(Commands::getCommand("generate"));
|
|
QVERIFY(Commands::getCommand("help"));
|
|
QVERIFY(Commands::getCommand("import"));
|
|
QVERIFY(Commands::getCommand("locate"));
|
|
QVERIFY(Commands::getCommand("ls"));
|
|
QVERIFY(Commands::getCommand("merge"));
|
|
QVERIFY(Commands::getCommand("mkdir"));
|
|
QVERIFY(Commands::getCommand("mv"));
|
|
QVERIFY(Commands::getCommand("open"));
|
|
QVERIFY(Commands::getCommand("rm"));
|
|
QVERIFY(Commands::getCommand("rmdir"));
|
|
QVERIFY(Commands::getCommand("show"));
|
|
QVERIFY(!Commands::getCommand("doesnotexist"));
|
|
QCOMPARE(Commands::getCommands().size(), 22);
|
|
}
|
|
|
|
void TestCli::testInteractiveCommands()
|
|
{
|
|
Commands::setupCommands(true);
|
|
QVERIFY(Commands::getCommand("add"));
|
|
QVERIFY(Commands::getCommand("analyze"));
|
|
QVERIFY(Commands::getCommand("clip"));
|
|
QVERIFY(Commands::getCommand("close"));
|
|
QVERIFY(Commands::getCommand("db-create"));
|
|
QVERIFY(Commands::getCommand("db-info"));
|
|
QVERIFY(Commands::getCommand("diceware"));
|
|
QVERIFY(Commands::getCommand("edit"));
|
|
QVERIFY(Commands::getCommand("estimate"));
|
|
QVERIFY(Commands::getCommand("exit"));
|
|
QVERIFY(Commands::getCommand("generate"));
|
|
QVERIFY(Commands::getCommand("help"));
|
|
QVERIFY(Commands::getCommand("locate"));
|
|
QVERIFY(Commands::getCommand("ls"));
|
|
QVERIFY(Commands::getCommand("merge"));
|
|
QVERIFY(Commands::getCommand("mkdir"));
|
|
QVERIFY(Commands::getCommand("mv"));
|
|
QVERIFY(Commands::getCommand("open"));
|
|
QVERIFY(Commands::getCommand("quit"));
|
|
QVERIFY(Commands::getCommand("rm"));
|
|
QVERIFY(Commands::getCommand("rmdir"));
|
|
QVERIFY(Commands::getCommand("show"));
|
|
QVERIFY(!Commands::getCommand("doesnotexist"));
|
|
QCOMPARE(Commands::getCommands().size(), 22);
|
|
}
|
|
|
|
void TestCli::testAdd()
|
|
{
|
|
Add addCmd;
|
|
QVERIFY(!addCmd.name.isEmpty());
|
|
QVERIFY(addCmd.getDescriptionLine().contains(addCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(addCmd,
|
|
{"add",
|
|
"-u",
|
|
"newuser",
|
|
"--url",
|
|
"https://example.com/",
|
|
"-g",
|
|
"-L",
|
|
"20",
|
|
m_dbFile->fileName(),
|
|
"/newuser-entry"});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry."));
|
|
|
|
auto db = readDatabase();
|
|
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);
|
|
|
|
// Quiet option
|
|
setInput("a");
|
|
execCmd(addCmd, {"add", "-q", "-u", "newuser", "-g", "-L", "20", m_dbFile->fileName(), "/newentry-quiet"});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("/newentry-quiet");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->password().size(), 20);
|
|
|
|
setInput({"a", "newpassword"});
|
|
execCmd(addCmd,
|
|
{"add", "-u", "newuser2", "--url", "https://example.net/", "-p", m_dbFile->fileName(), "/newuser-entry2"});
|
|
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry2."));
|
|
|
|
db = readDatabase();
|
|
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"));
|
|
|
|
// Password generation options
|
|
setInput("a");
|
|
execCmd(addCmd, {"add", "-u", "newuser3", "-g", "-L", "34", m_dbFile->fileName(), "/newuser-entry3"});
|
|
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry3."));
|
|
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("/newuser-entry3");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->username(), QString("newuser3"));
|
|
QCOMPARE(entry->password().size(), 34);
|
|
QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
|
|
QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
|
|
|
|
setInput("a");
|
|
execCmd(addCmd,
|
|
{"add",
|
|
"-u",
|
|
"newuser4",
|
|
"-g",
|
|
"-L",
|
|
"20",
|
|
"--every-group",
|
|
"-s",
|
|
"-n",
|
|
"-U",
|
|
"-l",
|
|
m_dbFile->fileName(),
|
|
"/newuser-entry4"});
|
|
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry4."));
|
|
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("/newuser-entry4");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->username(), QString("newuser4"));
|
|
QCOMPARE(entry->password().size(), 20);
|
|
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
|
|
}
|
|
|
|
void TestCli::testAddGroup()
|
|
{
|
|
AddGroup addGroupCmd;
|
|
QVERIFY(!addGroupCmd.name.isEmpty());
|
|
QVERIFY(addGroupCmd.getDescriptionLine().contains(addGroupCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added group new_group.\n"));
|
|
|
|
auto db = readDatabase();
|
|
auto* group = db->rootGroup()->findGroupByPath("new_group");
|
|
QVERIFY(group);
|
|
QCOMPARE(group->name(), QString("new_group"));
|
|
|
|
// Trying to add the same group should fail.
|
|
setInput("a");
|
|
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
|
|
QVERIFY(m_stderr->readAll().contains("Group /new_group already exists!"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// Should be able to add groups down the tree.
|
|
setInput("a");
|
|
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group/newer_group"});
|
|
QVERIFY(m_stdout->readAll().contains("Successfully added group newer_group."));
|
|
|
|
db = readDatabase();
|
|
group = db->rootGroup()->findGroupByPath("new_group/newer_group");
|
|
QVERIFY(group);
|
|
QCOMPARE(group->name(), QString("newer_group"));
|
|
|
|
// Should fail if the path is invalid.
|
|
setInput("a");
|
|
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/invalid_group/newer_group"});
|
|
QVERIFY(m_stderr->readAll().contains("Group /invalid_group not found."));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// Should fail to add the root group.
|
|
setInput("a");
|
|
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/"});
|
|
QVERIFY(m_stderr->readAll().contains("Group / already exists!"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testAnalyze()
|
|
{
|
|
Analyze analyzeCmd;
|
|
QVERIFY(!analyzeCmd.name.isEmpty());
|
|
QVERIFY(analyzeCmd.getDescriptionLine().contains(analyzeCmd.name));
|
|
|
|
const QString hibpPath = QString(KEEPASSX_TEST_DATA_DIR).append("/hibp.txt");
|
|
|
|
setInput("a");
|
|
execCmd(analyzeCmd, {"analyze", "--hibp", hibpPath, m_dbFile->fileName()});
|
|
auto output = m_stdout->readAll();
|
|
QVERIFY(output.contains("Sample Entry"));
|
|
QVERIFY(output.contains("123"));
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testClip()
|
|
{
|
|
QClipboard* clipboard = QGuiApplication::clipboard();
|
|
clipboard->clear();
|
|
|
|
Clip clipCmd;
|
|
QVERIFY(!clipCmd.name.isEmpty());
|
|
QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name));
|
|
|
|
// Password
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry"});
|
|
QString errorOutput(m_stderr->readAll());
|
|
|
|
if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
|
|
QSKIP("Clip test skipped due to QClipboard and Wayland issues");
|
|
}
|
|
|
|
if (errorOutput.contains("Unable to start program")
|
|
|| errorOutput.contains("No program defined for clipboard manipulation")) {
|
|
QSKIP("Clip test skipped due to missing clipboard tool");
|
|
}
|
|
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QTRY_COMPARE(clipboard->text(), QString("Password"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n"));
|
|
|
|
// Quiet option
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "-q"});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QTRY_COMPARE(clipboard->text(), QString("Password"));
|
|
|
|
// Username
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "-a", "username"});
|
|
QTRY_COMPARE(clipboard->text(), QString("User Name"));
|
|
|
|
// TOTP
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "--totp"});
|
|
|
|
QTRY_VERIFY(isTotp(clipboard->text()));
|
|
|
|
// Password with timeout
|
|
setInput("a");
|
|
// clang-format off
|
|
QFuture<void> future = QtConcurrent::run(&clipCmd,
|
|
static_cast<int(Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
|
|
QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"});
|
|
// clang-format on
|
|
|
|
QTRY_COMPARE(clipboard->text(), QString("Password"));
|
|
QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 2000);
|
|
|
|
future.waitForFinished();
|
|
|
|
// TOTP with timeout
|
|
setInput("a");
|
|
future = QtConcurrent::run(&clipCmd,
|
|
static_cast<int (Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
|
|
QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1", "-t"});
|
|
|
|
QTRY_VERIFY(isTotp(clipboard->text()));
|
|
QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 2000);
|
|
|
|
future.waitForFinished();
|
|
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "0"});
|
|
QVERIFY(m_stderr->readAll().contains("Invalid timeout value 0.\n"));
|
|
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"});
|
|
QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n"));
|
|
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
|
|
QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
|
|
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry"});
|
|
QVERIFY(m_stderr->readAll().contains(
|
|
"ERROR: attribute TESTAttribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n"));
|
|
|
|
setInput("a");
|
|
execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry"});
|
|
QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n"));
|
|
}
|
|
|
|
void TestCli::testCreate()
|
|
{
|
|
Create createCmd;
|
|
QVERIFY(!createCmd.name.isEmpty());
|
|
QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
|
|
|
|
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
|
|
QString dbFilename;
|
|
|
|
// Testing password option, password mismatch
|
|
dbFilename = testDir->path() + "/testCreate_pw.kdbx";
|
|
setInput({"a", "b"});
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p"});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Error: Passwords do not match.\n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
|
|
|
|
// Testing password option
|
|
setInput({"a", "a"});
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p"});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
|
|
|
|
auto db = readDatabase(dbFilename, "a");
|
|
QVERIFY(db);
|
|
|
|
// Testing with empty password (deny it)
|
|
dbFilename = testDir->path() + "/testCreate_blankpw.kdbx";
|
|
setInput({"", "n"});
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p"});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QVERIFY(m_stderr->readLine().contains("empty password"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
|
|
|
|
// Testing with empty password (accept it)
|
|
setInput({"", "y"});
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p"});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QVERIFY(m_stderr->readLine().contains("empty password"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
|
|
|
|
db = readDatabase(dbFilename, "");
|
|
QVERIFY(db);
|
|
|
|
// Should refuse to create the database if it already exists.
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p"});
|
|
// Output should be empty when there is an error.
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QString errorMessage = QString("File " + dbFilename + " already exists.\n");
|
|
QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
|
|
|
|
// Should refuse to create without any key provided.
|
|
dbFilename = testDir->path() + "/testCreate_key.kdbx";
|
|
execCmd(createCmd, {"db-create", dbFilename});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("No key is set. Aborting database creation.\n"));
|
|
|
|
// Testing with keyfile creation
|
|
dbFilename = testDir->path() + "/testCreate_key2.kdbx";
|
|
QString keyfilePath = testDir->path() + "/keyfile.txt";
|
|
setInput({"a", "a"});
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
|
|
|
|
db = readDatabase(dbFilename, "a", keyfilePath);
|
|
QVERIFY(db);
|
|
|
|
// Testing with existing keyfile
|
|
dbFilename = testDir->path() + "/testCreate_key3.kdbx";
|
|
setInput({"a", "a"});
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
|
|
|
|
db = readDatabase(dbFilename, "a", keyfilePath);
|
|
QVERIFY(db);
|
|
|
|
// Invalid decryption time (format).
|
|
dbFilename = testDir->path() + "/testCreate_time.kdbx";
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "NAN"});
|
|
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
|
|
|
|
// Invalid decryption time (range).
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "10"});
|
|
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
|
|
|
|
int encryptionTime = 500;
|
|
// Custom encryption time
|
|
setInput({"a", "a"});
|
|
int epochBefore = QDateTime::currentMSecsSinceEpoch();
|
|
execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", QString::number(encryptionTime)});
|
|
// Removing 100ms to make sure we account for changes in computation time.
|
|
QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
|
|
QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
|
|
|
|
db = readDatabase(dbFilename, "a");
|
|
QVERIFY(db);
|
|
}
|
|
|
|
void TestCli::testInfo()
|
|
{
|
|
Info infoCmd;
|
|
QVERIFY(!infoCmd.name.isEmpty());
|
|
QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(infoCmd, {"db-info", m_dbFile->fileName()});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
|
|
|
|
// Test with quiet option.
|
|
setInput("a");
|
|
execCmd(infoCmd, {"db-info", "-q", m_dbFile->fileName()});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
|
|
}
|
|
|
|
void TestCli::testDiceware()
|
|
{
|
|
Diceware dicewareCmd;
|
|
QVERIFY(!dicewareCmd.name.isEmpty());
|
|
QVERIFY(dicewareCmd.getDescriptionLine().contains(dicewareCmd.name));
|
|
|
|
execCmd(dicewareCmd, {"diceware"});
|
|
QString passphrase(m_stdout->readLine());
|
|
QVERIFY(!passphrase.isEmpty());
|
|
|
|
execCmd(dicewareCmd, {"diceware", "-W", "2"});
|
|
passphrase = m_stdout->readLine();
|
|
QCOMPARE(passphrase.split(" ").size(), 2);
|
|
|
|
execCmd(dicewareCmd, {"diceware", "-W", "10"});
|
|
passphrase = m_stdout->readLine();
|
|
QCOMPARE(passphrase.split(" ").size(), 10);
|
|
|
|
// Testing with invalid word count
|
|
execCmd(dicewareCmd, {"diceware", "-W", "-10"});
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count -10\n"));
|
|
|
|
// Testing with invalid word count format
|
|
execCmd(dicewareCmd, {"diceware", "-W", "bleuh"});
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count bleuh\n"));
|
|
|
|
TemporaryFile wordFile;
|
|
wordFile.open();
|
|
for (int i = 0; i < 4500; ++i) {
|
|
wordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
|
|
}
|
|
wordFile.close();
|
|
|
|
execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", wordFile.fileName()});
|
|
passphrase = m_stdout->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"));
|
|
}
|
|
|
|
TemporaryFile smallWordFile;
|
|
smallWordFile.open();
|
|
for (int i = 0; i < 50; ++i) {
|
|
smallWordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
|
|
}
|
|
smallWordFile.close();
|
|
|
|
execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", smallWordFile.fileName()});
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("The word list is too small (< 1000 items)\n"));
|
|
}
|
|
|
|
void TestCli::testEdit()
|
|
{
|
|
Edit editCmd;
|
|
QVERIFY(!editCmd.name.isEmpty());
|
|
QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(editCmd,
|
|
{"edit",
|
|
"-u",
|
|
"newuser",
|
|
"--url",
|
|
"https://otherurl.example.com/",
|
|
"-t",
|
|
"newtitle",
|
|
m_dbFile->fileName(),
|
|
"/Sample Entry"});
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully edited entry newtitle.\n"));
|
|
|
|
auto db = readDatabase();
|
|
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"));
|
|
|
|
// Quiet option
|
|
setInput("a");
|
|
execCmd(editCmd, {"edit", m_dbFile->fileName(), "-q", "-t", "newertitle", "/newtitle"});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
setInput("a");
|
|
execCmd(editCmd, {"edit", "-g", m_dbFile->fileName(), "/newertitle"});
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("/newertitle");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->username(), QString("newuser"));
|
|
QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
|
|
QVERIFY(!entry->password().isEmpty());
|
|
QVERIFY(entry->password() != QString("Password"));
|
|
|
|
setInput("a");
|
|
execCmd(editCmd, {"edit", "-g", "-L", "34", "-t", "evennewertitle", m_dbFile->fileName(), "/newertitle"});
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
|
|
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);
|
|
QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
|
|
QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
|
|
|
|
setInput("a");
|
|
execCmd(editCmd,
|
|
{"edit",
|
|
"-g",
|
|
"-L",
|
|
"20",
|
|
"--every-group",
|
|
"-s",
|
|
"-n",
|
|
"--upper",
|
|
"-l",
|
|
m_dbFile->fileName(),
|
|
"/evennewertitle"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited entry evennewertitle.\n"));
|
|
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->password().size(), 20);
|
|
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
|
|
|
|
setInput({"a", "newpassword"});
|
|
execCmd(editCmd, {"edit", "-p", m_dbFile->fileName(), "/evennewertitle"});
|
|
db = readDatabase();
|
|
QVERIFY(db);
|
|
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
|
|
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));
|
|
|
|
setInput(input);
|
|
execCmd(estimateCmd, {"estimate", "-a"});
|
|
auto result = QString(m_stdout->readAll());
|
|
QVERIFY(result.contains("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::testExport()
|
|
{
|
|
Export exportCmd;
|
|
QVERIFY(!exportCmd.name.isEmpty());
|
|
QVERIFY(exportCmd.getDescriptionLine().contains(exportCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(exportCmd, {"export", m_dbFile->fileName()});
|
|
|
|
TemporaryFile xmlOutput;
|
|
xmlOutput.open(QIODevice::WriteOnly);
|
|
xmlOutput.write(m_stdout->readAll());
|
|
xmlOutput.close();
|
|
|
|
QScopedPointer<Database> db(new Database());
|
|
QVERIFY(db->import(xmlOutput.fileName()));
|
|
|
|
auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->password(), QString("Password"));
|
|
|
|
// Quiet option
|
|
QScopedPointer<Database> dbQuiet(new Database());
|
|
setInput("a");
|
|
execCmd(exportCmd, {"export", "-f", "xml", "-q", m_dbFile->fileName()});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
|
|
xmlOutput.open(QIODevice::WriteOnly);
|
|
xmlOutput.write(m_stdout->readAll());
|
|
xmlOutput.close();
|
|
|
|
QVERIFY(db->import(xmlOutput.fileName()));
|
|
|
|
// CSV exporting
|
|
setInput("a");
|
|
execCmd(exportCmd, {"export", "-f", "csv", m_dbFile->fileName()});
|
|
QByteArray csvHeader = m_stdout->readLine();
|
|
QCOMPARE(csvHeader, QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n"));
|
|
QByteArray csvData = m_stdout->readAll();
|
|
QVERIFY(csvData.contains(QByteArray(
|
|
"\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"\n")));
|
|
|
|
// test invalid format
|
|
setInput("a");
|
|
execCmd(exportCmd, {"export", "-f", "yaml", m_dbFile->fileName()});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Unsupported format yaml\n"));
|
|
}
|
|
|
|
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", "--upper"} << "^[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", "--upper", "-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", "--upper", "-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));
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
execCmd(generateCmd, parameters);
|
|
QRegularExpression regex(pattern);
|
|
#ifdef Q_OS_UNIX
|
|
QString password = QString::fromUtf8(m_stdout->readLine());
|
|
#else
|
|
QString password = QString::fromLatin1(m_stdout->readLine());
|
|
#endif
|
|
|
|
QVERIFY2(regex.match(password).hasMatch(),
|
|
qPrintable("Password " + password + " does not match pattern " + pattern));
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
}
|
|
|
|
// Testing with invalid password length
|
|
execCmd(generateCmd, {"generate", "-L", "-10"});
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length -10\n"));
|
|
|
|
execCmd(generateCmd, {"generate", "-L", "0"});
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length 0\n"));
|
|
|
|
// Testing with invalid word count format
|
|
execCmd(generateCmd, {"generate", "-L", "bleuh"});
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length bleuh\n"));
|
|
}
|
|
|
|
void TestCli::testImport()
|
|
{
|
|
Import importCmd;
|
|
QVERIFY(!importCmd.name.isEmpty());
|
|
QVERIFY(importCmd.getDescriptionLine().contains(importCmd.name));
|
|
|
|
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
|
|
QString databaseFilename = testDir->path() + "/testImport1.kdbx";
|
|
|
|
setInput({"a", "a"});
|
|
execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
|
|
|
|
auto db = readDatabase(databaseFilename, "a");
|
|
QVERIFY(db);
|
|
auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry 1");
|
|
QVERIFY(entry);
|
|
QCOMPARE(entry->username(), QString("User Name"));
|
|
|
|
// Should refuse to create the database if it already exists.
|
|
execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename});
|
|
// Output should be empty when there is an error.
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QString errorMessage = QString("File " + databaseFilename + " already exists.\n");
|
|
QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
|
|
|
|
// Quiet option
|
|
QScopedPointer<QTemporaryDir> testDirQuiet(new QTemporaryDir());
|
|
QString databaseFilenameQuiet = testDirQuiet->path() + "/testImport2.kdbx";
|
|
|
|
setInput({"a", "a"});
|
|
execCmd(importCmd, {"import", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
|
|
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray());
|
|
|
|
db = readDatabase(databaseFilenameQuiet, "a");
|
|
QVERIFY(db);
|
|
}
|
|
|
|
void TestCli::testKeyFileOption()
|
|
{
|
|
List listCmd;
|
|
|
|
QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtected.key"));
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-k", keyFilePath, m_keyFileProtectedDbFile->fileName()});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("entry1\n"
|
|
"entry2\n"));
|
|
|
|
// Should raise an error with no key file.
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", m_keyFileProtectedDbFile->fileName()});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
|
|
|
|
// Should raise an error if key file path is invalid.
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-k", "invalidpath", m_keyFileProtectedDbFile->fileName()});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Failed to load key file invalidpath"));
|
|
}
|
|
|
|
void TestCli::testNoPasswordOption()
|
|
{
|
|
List listCmd;
|
|
|
|
QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtectedNoPassword.key"));
|
|
execCmd(listCmd, {"ls", "-k", keyFilePath, "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
|
|
// Expecting no password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("entry1\n"
|
|
"entry2\n"));
|
|
|
|
// Should raise an error with no key file.
|
|
execCmd(listCmd, {"ls", "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
|
|
}
|
|
|
|
void TestCli::testList()
|
|
{
|
|
List listCmd;
|
|
QVERIFY(!listCmd.name.isEmpty());
|
|
QVERIFY(listCmd.getDescriptionLine().contains(listCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", m_dbFile->fileName()});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Sample Entry\n"
|
|
"General/\n"
|
|
"Windows/\n"
|
|
"Network/\n"
|
|
"Internet/\n"
|
|
"eMail/\n"
|
|
"Homebanking/\n"));
|
|
|
|
// Quiet option
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-q", m_dbFile->fileName()});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Sample Entry\n"
|
|
"General/\n"
|
|
"Windows/\n"
|
|
"Network/\n"
|
|
"Internet/\n"
|
|
"eMail/\n"
|
|
"Homebanking/\n"));
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-R", m_dbFile->fileName()});
|
|
QCOMPARE(m_stdout->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"
|
|
" Subgroup/\n"
|
|
" Subgroup Entry\n"));
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName()});
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Sample Entry\n"
|
|
"General/\n"
|
|
"General/[empty]\n"
|
|
"Windows/\n"
|
|
"Windows/[empty]\n"
|
|
"Network/\n"
|
|
"Network/[empty]\n"
|
|
"Internet/\n"
|
|
"Internet/[empty]\n"
|
|
"eMail/\n"
|
|
"eMail/[empty]\n"
|
|
"Homebanking/\n"
|
|
"Homebanking/Subgroup/\n"
|
|
"Homebanking/Subgroup/Subgroup Entry\n"));
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName(), "/Homebanking"});
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Subgroup/\n"
|
|
"Subgroup/Subgroup Entry\n"));
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", m_dbFile->fileName(), "/General/"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("[empty]\n"));
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", m_dbFile->fileName(), "/DoesNotExist/"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("Cannot find group /DoesNotExist/.\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testLocate()
|
|
{
|
|
Locate locateCmd;
|
|
QVERIFY(!locateCmd.name.isEmpty());
|
|
QVERIFY(locateCmd.getDescriptionLine().contains(locateCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(locateCmd, {"locate", m_dbFile->fileName(), "Sample"});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
|
|
|
|
// Quiet option
|
|
setInput("a");
|
|
execCmd(locateCmd, {"locate", m_dbFile->fileName(), "-q", "Sample"});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
|
|
|
|
setInput("a");
|
|
execCmd(locateCmd, {"locate", m_dbFile->fileName(), "Does Not Exist"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("No results for that search term.\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// write a modified database
|
|
auto db = readDatabase();
|
|
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);
|
|
|
|
TemporaryFile tmpFile;
|
|
tmpFile.open();
|
|
tmpFile.close();
|
|
db->saveAs(tmpFile.fileName());
|
|
|
|
setInput("a");
|
|
execCmd(locateCmd, {"locate", tmpFile.fileName(), "New"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("/General/New Entry\n"));
|
|
|
|
setInput("a");
|
|
execCmd(locateCmd, {"locate", tmpFile.fileName(), "Entry"});
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
|
|
}
|
|
|
|
void TestCli::testMerge()
|
|
{
|
|
Merge mergeCmd;
|
|
QVERIFY(!mergeCmd.name.isEmpty());
|
|
QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
|
|
|
|
// load test database and save copies
|
|
auto db = readDatabase();
|
|
QVERIFY(db);
|
|
TemporaryFile targetFile1;
|
|
targetFile1.open();
|
|
targetFile1.close();
|
|
|
|
TemporaryFile targetFile2;
|
|
targetFile2.open();
|
|
targetFile2.close();
|
|
|
|
TemporaryFile targetFile3;
|
|
targetFile3.open();
|
|
targetFile3.close();
|
|
|
|
db->saveAs(targetFile1.fileName());
|
|
db->saveAs(targetFile2.fileName());
|
|
|
|
// save another copy with a different password
|
|
auto oldKey = db->key();
|
|
auto key = QSharedPointer<CompositeKey>::create();
|
|
key->addKey(QSharedPointer<PasswordKey>::create("b"));
|
|
db->setKey(key);
|
|
db->saveAs(targetFile3.fileName());
|
|
|
|
// Restore the original password
|
|
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);
|
|
|
|
TemporaryFile sourceFile;
|
|
sourceFile.open();
|
|
sourceFile.close();
|
|
db->saveAs(sourceFile.fileName());
|
|
|
|
setInput("a");
|
|
execCmd(mergeCmd, {"merge", "-s", targetFile1.fileName(), sourceFile.fileName()});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QList<QByteArray> outLines1 = m_stdout->readAll().split('\n');
|
|
QVERIFY(outLines1.at(0).contains("Overwriting Internet"));
|
|
QVERIFY(outLines1.at(1).contains("Creating missing Some Website"));
|
|
QCOMPARE(outLines1.at(2),
|
|
QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile1.fileName()).toUtf8());
|
|
|
|
auto mergedDb = QSharedPointer<Database>::create();
|
|
QVERIFY(mergedDb->open(targetFile1.fileName(), oldKey));
|
|
|
|
auto* entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
|
|
QVERIFY(entry1);
|
|
QCOMPARE(entry1->title(), QString("Some Website"));
|
|
QCOMPARE(entry1->password(), QString("secretsecretsecret"));
|
|
|
|
// the dry run option should not modify the target database.
|
|
setInput("a");
|
|
execCmd(mergeCmd, {"merge", "--dry-run", "-s", targetFile2.fileName(), sourceFile.fileName()});
|
|
QList<QByteArray> outLines2 = m_stdout->readAll().split('\n');
|
|
QVERIFY(outLines2.at(0).contains("Overwriting Internet"));
|
|
QVERIFY(outLines2.at(1).contains("Creating missing Some Website"));
|
|
QCOMPARE(outLines2.at(2), QByteArray("Database was not modified by merge operation."));
|
|
|
|
mergedDb = QSharedPointer<Database>::create();
|
|
QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
|
|
entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
|
|
QVERIFY(!entry1);
|
|
|
|
// the dry run option can be used with the quiet option
|
|
setInput("a");
|
|
execCmd(mergeCmd, {"merge", "--dry-run", "-s", "-q", targetFile2.fileName(), sourceFile.fileName()});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
mergedDb = QSharedPointer<Database>::create();
|
|
QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
|
|
entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
|
|
QVERIFY(!entry1);
|
|
|
|
// try again with different passwords for both files
|
|
setInput({"b", "a"});
|
|
execCmd(mergeCmd, {"merge", targetFile3.fileName(), sourceFile.fileName()});
|
|
QList<QByteArray> outLines3 = m_stdout->readAll().split('\n');
|
|
QCOMPARE(outLines3.at(2),
|
|
QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile3.fileName()).toUtf8());
|
|
|
|
mergedDb = QSharedPointer<Database>::create();
|
|
QVERIFY(mergedDb->open(targetFile3.fileName(), key));
|
|
|
|
entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
|
|
QVERIFY(entry1);
|
|
QCOMPARE(entry1->title(), QString("Some Website"));
|
|
QCOMPARE(entry1->password(), QString("secretsecretsecret"));
|
|
|
|
// making sure that the message is different if the database was not
|
|
// modified by the merge operation.
|
|
setInput("a");
|
|
execCmd(mergeCmd, {"merge", "-s", sourceFile.fileName(), sourceFile.fileName()});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Database was not modified by merge operation.\n"));
|
|
|
|
// Quiet option
|
|
setInput("a");
|
|
execCmd(mergeCmd, {"merge", "-q", "-s", sourceFile.fileName(), sourceFile.fileName()});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// Quiet option without the -s option
|
|
setInput({"a", "a"});
|
|
execCmd(mergeCmd, {"merge", "-q", sourceFile.fileName(), sourceFile.fileName()});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testMergeWithKeys()
|
|
{
|
|
Create createCmd;
|
|
QVERIFY(!createCmd.name.isEmpty());
|
|
QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
|
|
|
|
Merge mergeCmd;
|
|
QVERIFY(!mergeCmd.name.isEmpty());
|
|
QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
|
|
|
|
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
|
|
|
|
QString sourceDatabaseFilename = testDir->path() + "/testSourceDatabase.kdbx";
|
|
QString sourceKeyfilePath = testDir->path() + "/testSourceKeyfile.txt";
|
|
|
|
QString targetDatabaseFilename = testDir->path() + "/testTargetDatabase.kdbx";
|
|
QString targetKeyfilePath = testDir->path() + "/testTargetKeyfile.txt";
|
|
|
|
setInput({"a", "a"});
|
|
execCmd(createCmd, {"db-create", sourceDatabaseFilename, "-p", "-k", sourceKeyfilePath});
|
|
|
|
setInput({"b", "b"});
|
|
execCmd(createCmd, {"db-create", targetDatabaseFilename, "-p", "-k", targetKeyfilePath});
|
|
|
|
auto sourceDatabase = readDatabase(sourceDatabaseFilename, "a", sourceKeyfilePath);
|
|
QVERIFY(sourceDatabase);
|
|
|
|
auto targetDatabase = readDatabase(targetDatabaseFilename, "b", targetKeyfilePath);
|
|
QVERIFY(targetDatabase);
|
|
|
|
auto* rootGroup = new Group();
|
|
rootGroup->setName("root");
|
|
rootGroup->setUuid(QUuid::createUuid());
|
|
auto* group = new Group();
|
|
group->setUuid(QUuid::createUuid());
|
|
group->setParent(rootGroup);
|
|
group->setName("Internet");
|
|
|
|
auto* entry = new Entry();
|
|
entry->setUuid(QUuid::createUuid());
|
|
entry->setTitle("Some Website");
|
|
entry->setPassword("secretsecretsecret");
|
|
group->addEntry(entry);
|
|
|
|
sourceDatabase->setRootGroup(rootGroup);
|
|
|
|
auto* otherRootGroup = new Group();
|
|
otherRootGroup->setName("root");
|
|
otherRootGroup->setUuid(QUuid::createUuid());
|
|
auto* otherGroup = new Group();
|
|
otherGroup->setUuid(QUuid::createUuid());
|
|
otherGroup->setParent(otherRootGroup);
|
|
otherGroup->setName("Internet");
|
|
|
|
auto* otherEntry = new Entry();
|
|
otherEntry->setUuid(QUuid::createUuid());
|
|
otherEntry->setTitle("Some Website 2");
|
|
otherEntry->setPassword("secretsecretsecret 2");
|
|
otherGroup->addEntry(otherEntry);
|
|
|
|
targetDatabase->setRootGroup(otherRootGroup);
|
|
|
|
sourceDatabase->saveAs(sourceDatabaseFilename);
|
|
targetDatabase->saveAs(targetDatabaseFilename);
|
|
|
|
setInput({"b", "a"});
|
|
execCmd(mergeCmd,
|
|
{"merge",
|
|
"-k",
|
|
targetKeyfilePath,
|
|
"--key-file-from",
|
|
sourceKeyfilePath,
|
|
targetDatabaseFilename,
|
|
sourceDatabaseFilename});
|
|
|
|
QList<QByteArray> lines = m_stdout->readAll().split('\n');
|
|
QVERIFY(lines.contains(
|
|
QString("Successfully merged %1 into %2.").arg(sourceDatabaseFilename, targetDatabaseFilename).toUtf8()));
|
|
}
|
|
|
|
void TestCli::testMove()
|
|
{
|
|
Move moveCmd;
|
|
QVERIFY(!moveCmd.name.isEmpty());
|
|
QVERIFY(moveCmd.getDescriptionLine().contains(moveCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "invalid_entry_path", "invalid_group_path"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray());
|
|
|
|
setInput("a");
|
|
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "invalid_group_path"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Could not find group with path invalid_group_path.\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray());
|
|
|
|
setInput("a");
|
|
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "General/"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readLine(), QByteArray());
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully moved entry Sample Entry to group General/.\n"));
|
|
|
|
auto db = readDatabase();
|
|
auto* entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
|
|
QVERIFY(entry);
|
|
|
|
// Test that not modified if the same group is destination.
|
|
setInput("a");
|
|
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "General/Sample Entry", "General/"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readLine(), QByteArray("Entry is already in group General/.\n"));
|
|
QCOMPARE(m_stdout->readLine(), QByteArray());
|
|
|
|
// sanity check
|
|
db = readDatabase();
|
|
entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
|
|
QVERIFY(entry);
|
|
}
|
|
|
|
void TestCli::testRemove()
|
|
{
|
|
Remove removeCmd;
|
|
QVERIFY(!removeCmd.name.isEmpty());
|
|
QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
|
|
|
|
// load test database and save a copy with disabled recycle bin
|
|
auto db = readDatabase();
|
|
QVERIFY(db);
|
|
TemporaryFile fileCopy;
|
|
fileCopy.open();
|
|
fileCopy.close();
|
|
|
|
db->metadata()->setRecycleBinEnabled(false);
|
|
db->saveAs(fileCopy.fileName());
|
|
|
|
// delete entry and verify
|
|
setInput("a");
|
|
execCmd(removeCmd, {"rm", m_dbFile->fileName(), "/Sample Entry"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n"));
|
|
|
|
auto readBackDb = readDatabase();
|
|
QVERIFY(readBackDb);
|
|
QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
|
|
QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
|
|
|
|
// try again, this time without recycle bin
|
|
setInput("a");
|
|
execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted entry Sample Entry.\n"));
|
|
|
|
readBackDb = readDatabase(fileCopy.fileName(), "a");
|
|
QVERIFY(readBackDb);
|
|
QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
|
|
QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
|
|
|
|
// finally, try deleting a non-existent entry
|
|
setInput("a");
|
|
execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("Entry /Sample Entry not found.\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// try deleting a directory, should fail
|
|
setInput("a");
|
|
execCmd(removeCmd, {"rm", fileCopy.fileName(), "/General"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("Entry /General not found.\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testRemoveGroup()
|
|
{
|
|
RemoveGroup removeGroupCmd;
|
|
QVERIFY(!removeGroupCmd.name.isEmpty());
|
|
QVERIFY(removeGroupCmd.getDescriptionLine().contains(removeGroupCmd.name));
|
|
|
|
// try deleting a directory, should recycle it first.
|
|
setInput("a");
|
|
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/General"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled group /General.\n"));
|
|
|
|
auto db = readDatabase();
|
|
auto* group = db->rootGroup()->findGroupByPath("General");
|
|
QVERIFY(!group);
|
|
|
|
// try deleting a directory again, should delete it permanently.
|
|
setInput("a");
|
|
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "Recycle Bin/General"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted group Recycle Bin/General.\n"));
|
|
|
|
db = readDatabase();
|
|
group = db->rootGroup()->findGroupByPath("Recycle Bin/General");
|
|
QVERIFY(!group);
|
|
|
|
// try deleting an invalid group, should fail.
|
|
setInput("a");
|
|
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "invalid"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("Group invalid not found.\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// Should fail to remove the root group.
|
|
setInput("a");
|
|
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/"});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray("Cannot remove root group from database.\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testRemoveQuiet()
|
|
{
|
|
Remove removeCmd;
|
|
QVERIFY(!removeCmd.name.isEmpty());
|
|
QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
|
|
|
|
// delete entry and verify
|
|
setInput("a");
|
|
execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
auto db = readDatabase();
|
|
QVERIFY(db);
|
|
|
|
QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
|
|
QVERIFY(db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
|
|
|
|
// remove the entry completely
|
|
setInput("a");
|
|
execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
db = readDatabase();
|
|
QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
|
|
QVERIFY(!db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
|
|
}
|
|
|
|
void TestCli::testShow()
|
|
{
|
|
Show showCmd;
|
|
QVERIFY(!showCmd.name.isEmpty());
|
|
QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", m_dbFile->fileName(), "/Sample Entry"});
|
|
m_stderr->readLine(); // Skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Title: Sample Entry\n"
|
|
"UserName: User Name\n"
|
|
"Password: PROTECTED\n"
|
|
"URL: http://www.somesite.com/\n"
|
|
"Notes: Notes\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-s", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Title: Sample Entry\n"
|
|
"UserName: User Name\n"
|
|
"Password: Password\n"
|
|
"URL: http://www.somesite.com/\n"
|
|
"Notes: Notes\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", m_dbFile->fileName(), "-q", "/Sample Entry"});
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Title: Sample Entry\n"
|
|
"UserName: User Name\n"
|
|
"Password: PROTECTED\n"
|
|
"URL: http://www.somesite.com/\n"
|
|
"Notes: Notes\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-a", "Password", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray("Password\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Sample Entry\n"
|
|
"http://www.somesite.com/\n"));
|
|
|
|
// Test case insensitivity
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("Sample Entry\n"
|
|
"http://www.somesite.com/\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QVERIFY(m_stderr->readAll().contains("ERROR: unknown attribute DoesNotExist.\n"));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-t", m_dbFile->fileName(), "/Sample Entry"});
|
|
QVERIFY(isTotp(m_stdout->readAll()));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "--totp", "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readLine(), QByteArray("Sample Entry\n"));
|
|
QVERIFY(isTotp(m_stdout->readAll()));
|
|
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
|
|
|
|
// Show with ambiguous attributes
|
|
setInput("a");
|
|
execCmd(showCmd, {"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"});
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
QVERIFY(m_stderr->readAll().contains(
|
|
"ERROR: attribute Testattribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n"));
|
|
}
|
|
|
|
void TestCli::testInvalidDbFiles()
|
|
{
|
|
Show showCmd;
|
|
QString nonExistentDbPath("/foo/bar/baz");
|
|
QString directoryName("/");
|
|
|
|
execCmd(showCmd, {"show", nonExistentDbPath, "/Sample Entry"});
|
|
QCOMPARE(QString(m_stderr->readAll()),
|
|
QObject::tr("Failed to open database file %1: not found").arg(nonExistentDbPath) + "\n");
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
execCmd(showCmd, {"show", directoryName, "whatever"});
|
|
QCOMPARE(QString(m_stderr->readAll()),
|
|
QObject::tr("Failed to open database file %1: not a plain file").arg(directoryName) + "\n");
|
|
|
|
// Create a write-only file and try to open it.
|
|
// QFileInfo.isReadable returns 'true' on Windows, even after the call to
|
|
// setPermissions(WriteOwner) and with NTFS permissions enabled, so this
|
|
// check doesn't work.
|
|
#if !defined(Q_OS_WIN)
|
|
QTemporaryFile tempFile;
|
|
QVERIFY(tempFile.open());
|
|
QString path = QFileInfo(tempFile).absoluteFilePath();
|
|
QVERIFY(tempFile.setPermissions(QFileDevice::WriteOwner));
|
|
execCmd(showCmd, {"show", path, "some entry"});
|
|
QCOMPARE(QString(m_stderr->readAll()),
|
|
QObject::tr("Failed to open database file %1: not readable").arg(path) + "\n");
|
|
#endif // Q_OS_WIN
|
|
}
|
|
|
|
/**
|
|
* Secret key for the YubiKey slot used by the unit test is
|
|
* 1c e3 0f d7 8d 20 dc fa 40 b5 0c 18 77 9a fb 0f 02 28 8d b7
|
|
* This secret can be on either slot but must be passive.
|
|
*/
|
|
void TestCli::testYubiKeyOption()
|
|
{
|
|
if (!YubiKey::instance()->isInitialized()) {
|
|
QSKIP("Unable to initialize YubiKey interface.");
|
|
}
|
|
|
|
YubiKey::instance()->findValidKeys();
|
|
|
|
// Wait for the hardware to respond
|
|
QSignalSpy detected(YubiKey::instance(), SIGNAL(detectComplete(bool)));
|
|
QTRY_VERIFY_WITH_TIMEOUT(detected.count() > 0, 2000);
|
|
|
|
auto keys = YubiKey::instance()->foundKeys();
|
|
if (keys.isEmpty()) {
|
|
QSKIP("No YubiKey devices were detected.");
|
|
}
|
|
|
|
bool wouldBlock = false;
|
|
QByteArray challenge("CLITest");
|
|
QByteArray response;
|
|
QByteArray expected("\xA2\x3B\x94\x00\xBE\x47\x9A\x30\xA9\xEB\x50\x9B\x85\x56\x5B\x6B\x30\x25\xB4\x8E", 20);
|
|
|
|
// Find a key that as configured for this test
|
|
YubiKeySlot pKey(0, 0);
|
|
for (auto key : keys) {
|
|
if (YubiKey::instance()->testChallenge(key, &wouldBlock) && !wouldBlock) {
|
|
YubiKey::instance()->challenge(key, challenge, response);
|
|
if (response == expected) {
|
|
pKey = key;
|
|
break;
|
|
}
|
|
Tools::wait(100);
|
|
}
|
|
}
|
|
|
|
if (pKey.first == 0 && pKey.second == 0) {
|
|
QSKIP("No YubiKey is properly configured to perform this test.");
|
|
}
|
|
|
|
List listCmd;
|
|
Add addCmd;
|
|
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-y", "2", m_yubiKeyProtectedDbFile->fileName()});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll(), QByteArray());
|
|
QCOMPARE(m_stdout->readAll(),
|
|
QByteArray("entry1\n"
|
|
"entry2\n"));
|
|
|
|
// Should raise an error with no yubikey slot.
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", m_yubiKeyProtectedDbFile->fileName()});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readLine(),
|
|
QByteArray("Error while reading the database: Invalid credentials were provided, please try again.\n"));
|
|
QCOMPARE(m_stderr->readLine(),
|
|
QByteArray("If this reoccurs, then your database file may be corrupt. (HMAC mismatch)\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// Should raise an error if yubikey slot is not a string
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-y", "invalidslot", m_yubiKeyProtectedDbFile->fileName()});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot invalidslot\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
|
|
// Should raise an error if yubikey slot is invalid.
|
|
setInput("a");
|
|
execCmd(listCmd, {"ls", "-y", "3", m_yubiKeyProtectedDbFile->fileName()});
|
|
m_stderr->readLine(); // skip password prompt
|
|
QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n"));
|
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
|
}
|
|
|
|
void TestCli::testCommandParsing_data()
|
|
{
|
|
QTest::addColumn<QString>("input");
|
|
QTest::addColumn<QStringList>("expectedOutput");
|
|
|
|
QTest::newRow("basic") << "hello world" << QStringList({"hello", "world"});
|
|
QTest::newRow("basic escaping") << "hello\\ world" << QStringList({"hello world"});
|
|
QTest::newRow("quoted string") << "\"hello world\"" << QStringList({"hello world"});
|
|
QTest::newRow("multiple params") << "show Passwords/Internet" << QStringList({"show", "Passwords/Internet"});
|
|
QTest::newRow("quoted string inside param")
|
|
<< R"(ls foo\ bar\ baz"quoted")" << QStringList({"ls", "foo bar baz\"quoted\""});
|
|
QTest::newRow("multiple whitespace") << "hello world" << QStringList({"hello", "world"});
|
|
QTest::newRow("single slash char") << "\\" << QStringList({"\\"});
|
|
QTest::newRow("double backslash entry name") << "show foo\\\\\\\\bar" << QStringList({"show", "foo\\\\bar"});
|
|
}
|
|
|
|
void TestCli::testCommandParsing()
|
|
{
|
|
QFETCH(QString, input);
|
|
QFETCH(QStringList, expectedOutput);
|
|
|
|
QStringList result = Utils::splitCommandString(input);
|
|
QCOMPARE(result.size(), expectedOutput.size());
|
|
for (int i = 0; i < expectedOutput.size(); ++i) {
|
|
QCOMPARE(result[i], expectedOutput[i]);
|
|
}
|
|
}
|
|
|
|
void TestCli::testOpen()
|
|
{
|
|
Open openCmd;
|
|
|
|
setInput("a");
|
|
execCmd(openCmd, {"open", m_dbFile->fileName()});
|
|
QVERIFY(openCmd.currentDatabase);
|
|
|
|
List listCmd;
|
|
// Set a current database, simulating interactive mode.
|
|
listCmd.currentDatabase = openCmd.currentDatabase;
|
|
execCmd(listCmd, {"ls"});
|
|
QByteArray expectedOutput("Sample Entry\n"
|
|
"General/\n"
|
|
"Windows/\n"
|
|
"Network/\n"
|
|
"Internet/\n"
|
|
"eMail/\n"
|
|
"Homebanking/\n");
|
|
QByteArray actualOutput = m_stdout->readAll();
|
|
actualOutput.truncate(expectedOutput.length());
|
|
QCOMPARE(actualOutput, expectedOutput);
|
|
}
|
|
|
|
void TestCli::testHelp()
|
|
{
|
|
Help helpCmd;
|
|
Commands::setupCommands(false);
|
|
|
|
execCmd(helpCmd, {"help"});
|
|
QVERIFY(m_stdout->readAll().contains("Available commands"));
|
|
|
|
List listCmd;
|
|
execCmd(helpCmd, {"help", "ls"});
|
|
QVERIFY(m_stdout->readAll().contains(listCmd.description.toLatin1()));
|
|
}
|