diff --git a/src/cli/Add.cpp b/src/cli/Add.cpp index dd9d0b50c..395b84919 100644 --- a/src/cli/Add.cpp +++ b/src/cli/Add.cpp @@ -50,6 +50,7 @@ int Add::execute(const QStringList& arguments) parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); QCommandLineOption username(QStringList() << "u" << "username", @@ -91,6 +92,7 @@ int Add::execute(const QStringList& arguments) const QString& entryPath = args.at(1); auto db = Utils::unlockDatabase(databasePath, + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 6e346f25c..31b421de6 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -49,6 +49,7 @@ int Clip::execute(const QStringList& arguments) parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); QCommandLineOption totp(QStringList() << "t" << "totp", @@ -67,6 +68,7 @@ int Clip::execute(const QStringList& arguments) } auto db = Utils::unlockDatabase(args.at(0), + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index 79d56c360..e64dd4aaa 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -46,6 +46,10 @@ const QCommandLineOption Command::KeyFileOption = QCommandLineOption(QStringList QObject::tr("Key file of the database."), QObject::tr("path")); +const QCommandLineOption Command::NoPasswordOption = + QCommandLineOption(QStringList() << "no-password", + QObject::tr("Deactivate password key for the database.")); + QMap commands; Command::~Command() diff --git a/src/cli/Command.h b/src/cli/Command.h index b74a312df..30af61702 100644 --- a/src/cli/Command.h +++ b/src/cli/Command.h @@ -40,6 +40,7 @@ public: static const QCommandLineOption QuietOption; static const QCommandLineOption KeyFileOption; + static const QCommandLineOption NoPasswordOption; }; #endif // KEEPASSXC_COMMAND_H diff --git a/src/cli/Edit.cpp b/src/cli/Edit.cpp index 7954648ce..76e996c98 100644 --- a/src/cli/Edit.cpp +++ b/src/cli/Edit.cpp @@ -49,6 +49,7 @@ int Edit::execute(const QStringList& arguments) parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); QCommandLineOption username(QStringList() << "u" << "username", @@ -95,6 +96,7 @@ int Edit::execute(const QStringList& arguments) const QString& entryPath = args.at(1); auto db = Utils::unlockDatabase(databasePath, + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index be5abb920..729687fe3 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -51,6 +51,7 @@ int Extract::execute(const QStringList& arguments) parser.addPositionalArgument("database", QObject::tr("Path of the database to extract.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); parser.addHelpOption(); parser.process(arguments); @@ -59,17 +60,19 @@ int Extract::execute(const QStringList& arguments) errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli extract"); return EXIT_FAILURE; } - - if (!parser.isSet(Command::QuietOption)) { - outputTextStream << QObject::tr("Insert password to unlock %1: ").arg(args.at(0)) << flush; - } - + auto compositeKey = QSharedPointer::create(); - QString line = Utils::getPassword(parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); - auto passwordKey = QSharedPointer::create(); - passwordKey->setPassword(line); - compositeKey->addKey(passwordKey); + if (!parser.isSet(Command::NoPasswordOption)) { + if (!parser.isSet(Command::QuietOption)) { + outputTextStream << QObject::tr("Insert password to unlock %1: ").arg(args.at(0)) << flush; + } + + QString line = Utils::getPassword(parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); + auto passwordKey = QSharedPointer::create(); + passwordKey->setPassword(line); + compositeKey->addKey(passwordKey); + } QString keyFilePath = parser.value(Command::KeyFileOption); if (!keyFilePath.isEmpty()) { diff --git a/src/cli/List.cpp b/src/cli/List.cpp index 11650d405..ebf7bfda1 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -48,6 +48,7 @@ int List::execute(const QStringList& arguments) parser.addPositionalArgument("group", QObject::tr("Path of the group to list. Default is /"), "[group]"); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); QCommandLineOption recursiveOption(QStringList() << "R" << "recursive", @@ -65,6 +66,7 @@ int List::execute(const QStringList& arguments) bool recursive = parser.isSet(recursiveOption); auto db = Utils::unlockDatabase(args.at(0), + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Locate.cpp b/src/cli/Locate.cpp index f25ce79af..81bbdd55d 100644 --- a/src/cli/Locate.cpp +++ b/src/cli/Locate.cpp @@ -50,6 +50,7 @@ int Locate::execute(const QStringList& arguments) parser.addPositionalArgument("term", QObject::tr("Search term.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); parser.addHelpOption(); parser.process(arguments); @@ -60,6 +61,7 @@ int Locate::execute(const QStringList& arguments) } auto db = Utils::unlockDatabase(args.at(0), + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index b0e4cabca..a7357394f 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -52,6 +52,7 @@ int Merge::execute(const QStringList& arguments) QObject::tr("Use the same credentials for both database files.")); parser.addOption(samePasswordOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); QCommandLineOption keyFileFromOption(QStringList() << "f" << "key-file-from", @@ -59,6 +60,10 @@ int Merge::execute(const QStringList& arguments) QObject::tr("path")); parser.addOption(keyFileFromOption); + QCommandLineOption noPasswordFromOption(QStringList() << "no-password-from", + QObject::tr("Deactivate password key for the database to merge from.")); + parser.addOption(noPasswordFromOption); + parser.addHelpOption(); parser.process(arguments); @@ -69,6 +74,7 @@ int Merge::execute(const QStringList& arguments) } auto db1 = Utils::unlockDatabase(args.at(0), + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); @@ -79,6 +85,7 @@ int Merge::execute(const QStringList& arguments) QSharedPointer db2; if (!parser.isSet("same-credentials")) { db2 = Utils::unlockDatabase(args.at(1), + !parser.isSet(noPasswordFromOption), parser.value(keyFileFromOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Remove.cpp b/src/cli/Remove.cpp index 4663d83ec..07da23b7b 100644 --- a/src/cli/Remove.cpp +++ b/src/cli/Remove.cpp @@ -51,6 +51,7 @@ int Remove::execute(const QStringList& arguments) parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); parser.addPositionalArgument("entry", QObject::tr("Path of the entry to remove.")); parser.addHelpOption(); parser.process(arguments); @@ -62,6 +63,7 @@ int Remove::execute(const QStringList& arguments) } auto db = Utils::unlockDatabase(args.at(0), + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index 9ae3f4d0f..d16fbfe3c 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -48,6 +48,8 @@ int Show::execute(const QStringList& arguments) parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addOption(Command::QuietOption); parser.addOption(Command::KeyFileOption); + parser.addOption(Command::NoPasswordOption); + QCommandLineOption totp(QStringList() << "t" << "totp", QObject::tr("Show the entry's current TOTP.")); @@ -72,6 +74,7 @@ int Show::execute(const QStringList& arguments) } auto db = Utils::unlockDatabase(args.at(0), + !parser.isSet(Command::NoPasswordOption), parser.value(Command::KeyFileOption), parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, Utils::STDERR); diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index 344e2b7f7..06d2bcf23 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -98,6 +98,7 @@ namespace Utils } // namespace Test QSharedPointer unlockDatabase(const QString& databaseFilename, + const bool isPasswordProtected, const QString& keyFilename, FILE* outputDescriptor, FILE* errorDescriptor) @@ -106,12 +107,13 @@ namespace Utils TextStream out(outputDescriptor); TextStream err(errorDescriptor); - out << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename) << flush; - - QString line = Utils::getPassword(outputDescriptor); - auto passwordKey = QSharedPointer::create(); - passwordKey->setPassword(line); - compositeKey->addKey(passwordKey); + if (isPasswordProtected) { + out << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename) << flush; + QString line = Utils::getPassword(outputDescriptor); + auto passwordKey = QSharedPointer::create(); + passwordKey->setPassword(line); + compositeKey->addKey(passwordKey); + } if (!keyFilename.isEmpty()) { auto fileKey = QSharedPointer::create(); diff --git a/src/cli/Utils.h b/src/cli/Utils.h index bb4d8f09a..bd89a2a5c 100644 --- a/src/cli/Utils.h +++ b/src/cli/Utils.h @@ -36,6 +36,7 @@ namespace Utils QString getPassword(FILE* outputDescriptor = STDOUT); int clipText(const QString& text); QSharedPointer unlockDatabase(const QString& databaseFilename, + const bool isPasswordProtected = true, const QString& keyFilename = {}, FILE* outputDescriptor = STDOUT, FILE* errorDescriptor = STDERR); diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 4cee31774..bd7f5d5c5 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -62,6 +62,9 @@ Displays debugging information. .IP "-k, --key-file " Specifies a path to a key file for unlocking the database. In a merge operation this option is used to specify the key file path for the first database. +.IP "--no-password" +Deactivate password key for the database. + .IP "-q, --quiet " Silence password prompt and other secondary outputs. @@ -77,6 +80,9 @@ Displays the program version. .IP "-f, --key-file-from " Path of the key file for the second database. +.IP "--no-password-from" +Deactivate password key for the database to merge from. + .IP "-s, --same-credentials" Use the same credentials for unlocking both database. diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index e2e66c2a4..3ba40b904 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -70,11 +70,23 @@ void TestCli::initTestCase() QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); sourceDbFile.close(); - // Load the NewDatabase.kdbx file into temporary storage + // Load the NewDatabase2.kdbx file into temporary storage QFile sourceDbFile2(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase2.kdbx")); QVERIFY(sourceDbFile2.open(QIODevice::ReadOnly)); QVERIFY(Tools::readAllFromDevice(&sourceDbFile2, m_dbData2)); sourceDbFile2.close(); + + // Load the KeyFileProtected.kdbx file into temporary storage + QFile sourceDbFile3(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtected.kdbx")); + QVERIFY(sourceDbFile3.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&sourceDbFile3, m_keyFileProtectedDbData)); + sourceDbFile3.close(); + + // Load the KeyFileProtectedNoPassword.kdbx file into temporary storage + QFile sourceDbFile4(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtectedNoPassword.kdbx")); + QVERIFY(sourceDbFile4.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&sourceDbFile4, m_keyFileProtectedNoPasswordDbData)); + sourceDbFile4.close(); } void TestCli::init() @@ -89,6 +101,16 @@ void TestCli::init() m_dbFile2->write(m_dbData2); m_dbFile2->close(); + m_keyFileProtectedDbFile.reset(new TemporaryFile()); + m_keyFileProtectedDbFile->open(); + m_keyFileProtectedDbFile->write(m_keyFileProtectedDbData); + m_keyFileProtectedDbFile->close(); + + m_keyFileProtectedNoPasswordDbFile.reset(new TemporaryFile()); + m_keyFileProtectedNoPasswordDbFile->open(); + m_keyFileProtectedNoPasswordDbFile->write(m_keyFileProtectedNoPasswordDbData); + m_keyFileProtectedNoPasswordDbFile->close(); + m_stdinFile.reset(new TemporaryFile()); m_stdinFile->open(); m_stdinHandle = fdopen(m_stdinFile->handle(), "r+"); @@ -131,7 +153,7 @@ void TestCli::cleanupTestCase() QSharedPointer TestCli::readTestDatabase() const { Utils::Test::setNextPassword("a"); - auto db = QSharedPointer(Utils::unlockDatabase(m_dbFile->fileName(), "", m_stdoutHandle)); + auto db = QSharedPointer(Utils::unlockDatabase(m_dbFile->fileName(), true, "", m_stdoutHandle)); m_stdoutFile->seek(ftell(m_stdoutHandle)); // re-synchronize handles return db; } @@ -320,7 +342,7 @@ void TestCli::testCreate() QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); Utils::Test::setNextPassword("a"); - auto db = QSharedPointer(Utils::unlockDatabase(databaseFilename, "", Utils::DEVNULL)); + auto db = QSharedPointer(Utils::unlockDatabase(databaseFilename, true, "", Utils::DEVNULL)); QVERIFY(db); // Should refuse to create the database if it already exists. @@ -349,7 +371,7 @@ void TestCli::testCreate() QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); Utils::Test::setNextPassword("a"); - auto db2 = QSharedPointer(Utils::unlockDatabase(databaseFilename2, keyfilePath, Utils::DEVNULL)); + auto db2 = QSharedPointer(Utils::unlockDatabase(databaseFilename2, true, keyfilePath, Utils::DEVNULL)); QVERIFY(db2); // Testing with existing keyfile @@ -366,7 +388,7 @@ void TestCli::testCreate() QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); Utils::Test::setNextPassword("a"); - auto db3 = QSharedPointer(Utils::unlockDatabase(databaseFilename3, keyfilePath, Utils::DEVNULL)); + auto db3 = QSharedPointer(Utils::unlockDatabase(databaseFilename3, true, keyfilePath, Utils::DEVNULL)); QVERIFY(db3); } @@ -659,6 +681,65 @@ void TestCli::testGenerate() } } +void TestCli::testKeyFileOption() +{ + List listCmd; + + QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtected.key")); + Utils::Test::setNextPassword("a"); + listCmd.execute({"ls", "-k", keyFilePath, m_keyFileProtectedDbFile->fileName()}); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("entry1\n" + "entry2\n")); + + // Should raise an error with no key file. + qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + listCmd.execute({"ls", m_keyFileProtectedDbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), + QByteArray("Error while reading the database: Wrong key or database file is corrupt. (HMAC mismatch)\n")); + + // Should raise an error if key file path is invalid. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + listCmd.execute({"ls", "-k", "invalidpath", m_keyFileProtectedDbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->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")); + listCmd.execute({"ls", "-k", keyFilePath, "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()}); + m_stdoutFile->reset(); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("entry1\n" + "entry2\n")); + + // Should raise an error with no key file. + qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); + listCmd.execute({"ls", "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), + QByteArray("Error while reading the database: Wrong key or database file is corrupt. (HMAC mismatch)\n")); +} + void TestCli::testList() { List listCmd; diff --git a/tests/TestCli.h b/tests/TestCli.h index f3655e6cd..cd8ebacfb 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -51,6 +51,8 @@ private slots: void testExtract(); void testGenerate_data(); void testGenerate(); + void testKeyFileOption(); + void testNoPasswordOption(); void testList(); void testLocate(); void testMerge(); @@ -61,8 +63,12 @@ private slots: private: QByteArray m_dbData; QByteArray m_dbData2; + QByteArray m_keyFileProtectedDbData; + QByteArray m_keyFileProtectedNoPasswordDbData; QScopedPointer m_dbFile; QScopedPointer m_dbFile2; + QScopedPointer m_keyFileProtectedDbFile; + QScopedPointer m_keyFileProtectedNoPasswordDbFile; QScopedPointer m_stdoutFile; QScopedPointer m_stderrFile; QScopedPointer m_stdinFile; diff --git a/tests/data/KeyFileProtected.kdbx b/tests/data/KeyFileProtected.kdbx new file mode 100644 index 000000000..eeda44d58 Binary files /dev/null and b/tests/data/KeyFileProtected.kdbx differ diff --git a/tests/data/KeyFileProtected.key b/tests/data/KeyFileProtected.key new file mode 100644 index 000000000..31314b953 Binary files /dev/null and b/tests/data/KeyFileProtected.key differ diff --git a/tests/data/KeyFileProtectedNoPassword.kdbx b/tests/data/KeyFileProtectedNoPassword.kdbx new file mode 100644 index 000000000..6ee188da8 Binary files /dev/null and b/tests/data/KeyFileProtectedNoPassword.kdbx differ diff --git a/tests/data/KeyFileProtectedNoPassword.key b/tests/data/KeyFileProtectedNoPassword.key new file mode 100644 index 000000000..a6131f09e Binary files /dev/null and b/tests/data/KeyFileProtectedNoPassword.key differ