From 13a9ac8f576539aace93a2d5faa2c4d35474f95e Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 24 Mar 2019 08:51:40 -0400 Subject: [PATCH] Adding --no-password option to CLI I also added tests for the --key-file option, which was untested. --- src/cli/Add.cpp | 2 + src/cli/Clip.cpp | 2 + src/cli/Command.cpp | 4 + src/cli/Command.h | 1 + src/cli/Edit.cpp | 2 + src/cli/Extract.cpp | 21 +++-- src/cli/List.cpp | 2 + src/cli/Locate.cpp | 2 + src/cli/Merge.cpp | 7 ++ src/cli/Remove.cpp | 2 + src/cli/Show.cpp | 3 + src/cli/Utils.cpp | 14 ++-- src/cli/Utils.h | 1 + src/cli/keepassxc-cli.1 | 6 ++ tests/TestCli.cpp | 91 +++++++++++++++++++-- tests/TestCli.h | 6 ++ tests/data/KeyFileProtected.kdbx | Bin 0 -> 1637 bytes tests/data/KeyFileProtected.key | Bin 0 -> 128 bytes tests/data/KeyFileProtectedNoPassword.kdbx | Bin 0 -> 1589 bytes tests/data/KeyFileProtectedNoPassword.key | Bin 0 -> 128 bytes 20 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 tests/data/KeyFileProtected.kdbx create mode 100644 tests/data/KeyFileProtected.key create mode 100644 tests/data/KeyFileProtectedNoPassword.kdbx create mode 100644 tests/data/KeyFileProtectedNoPassword.key 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 0000000000000000000000000000000000000000..eeda44d58a1451e17b6a3eee343ab939ec1e137e GIT binary patch literal 1637 zcmV-r2AcT;*`k_f`%AR|00aO65C8xGF~RcYzi~rQzE}kzYW!ON0|Wp70096100bZa z002jncjk5vEsMs9hPYAu5btjdtpn-lj3ZI_=w!Ry3X}&B00007q?JYcNPOA~M}0A@ zvD{A!ivR!s00BY;0000aRaHqu5C8xG?_+J>j44D*k@u;j1LFz|1pxp607(b{000mG z00000000F60000@2mk;800008000001OWg508j(~000O8002S(0000}AOHXW?}x9V zLVxS16a<)17Q8TCRi9-mD&4n^M@VKUiJIkVkX%?Tpp%ntDLgfAYFR&@d zacNShCSu+*>&f!&6+hA?{lcyc6+)1FXvmn<2{&-eCRs{dDMBDHz++w6-~<2w1s!j- zu%JYO{^yNmAr}Ab;xVVpwObOm*wf6!w{fbrx=kXcq3T=OM?##s1tMs4Pdbu}`f-nULVftc4;Bff+>lY2@5TXX6dRoltae-Tzo2%V%PP;!Fqf z#_bN zZnI?ux1vW#s=PT4x``{@>=rdxjK|2{l2*nk`#(`3aFPJ)UH2=?*avrxHK*l{{v##p znlLxKFJ){M(=A)4Azq)>>sWMI=$tWMch=_V>ytoaeXhL()t|3g&B9hrzep{IxIP0qLPx{Xh-+hxL zA*E+pb+7`Vnr##?!a}#jc&i=XQg>Ekf(tv@(D*KYLr9 zo$ZY-Etr^AEp;$Iwb%Kbrda|x*FJ#}9VIusoPG=CE2rNAs_?%gU*lErFJUY(?R52 zeho-gfqAc&3qmKR6%S7R#s0AblY!|)OdJ7U!*b8nX4SweN~37#2<5H8Hy~LW&BZ=x zr$ui;psEj&B_4AhkLuuIDgtO1FE6`qf(rZXl87K{2m`dkDgj}V`hZDPRpYE^-UF3S zIqf#{YLlbVpxPFH(7^Cwea1rge|jjG}Sl4$0%Fn%NG$F zkYX1tGVotS4-|pbA0N0QOv!jTN6=(ZZlHw-t7#-Tv41nHvb=GN+yTl=MbWO-SIPW& z>TaccBZN=_rsrJli9sQ@eA;`0?$7kp(Q>vGMI&vn12DFg)&qOQx>o<{(R!k-J|1!Q z;!@saBUNfMgR1SE<=<6fB`znUi@ww4hV~iDN1<@`ST**4JHX37!WjcXvf(ihxoI!a z(J7g|yC)e=oi=PZ?3 zU*P)bdQ4TCN*ME$8}{WVOue>c0SPLHg&H|MH#s1$9gf?GmD%_2epJGZFbebq7ap)o jH1A2&vD!{QN}ojF@i0h;)_)N3bt=KmHx34p00000?EUyp literal 0 HcmV?d00001 diff --git a/tests/data/KeyFileProtected.key b/tests/data/KeyFileProtected.key new file mode 100644 index 0000000000000000000000000000000000000000..31314b95318f07b2498bde8fd9d9496842a9d052 GIT binary patch literal 128 zcmV-`0Du1i3x;>{)6@0u+AzK@WB1H(>1342=tSEKGiwUPBm}23!4+Zyw-|5EWX)3t z^%mKQ{pMjzAx|^749=@U4%qXJ9UoAd;0GAX>o5moY&qWS!HAgvjecLW+$Zt&sJ+ApOF)?b literal 0 HcmV?d00001 diff --git a/tests/data/KeyFileProtectedNoPassword.kdbx b/tests/data/KeyFileProtectedNoPassword.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..6ee188da88ccf499663d0ba81c1643e299ce63ca GIT binary patch literal 1589 zcmV-52Fm#Z*`k_f`%AR|00aO65C8xGF~RcYzi~rQzE}kzYW!ON0|Wp70096100bZa z004NI3Yz|ujSg|>{`eSY^KlG8z;KK0SThCb8eM{omHY<~0002PD?eKRh|(dn6#(f& z;|f>{ivR!s00BY;0000aRaHqu5C8xG?_+J>j44D*k@u;j1LFz|1pxp607(b{00093 z00000000F60000@2mk;800008000001OWg508j(~000C4002S(0000}AOHXW35|w= zKl2zXlAnc@J9q?VaQ6_9*fadcz;LX_^J<=|1OWg509FJ5000vJ000001ONa44GIkk z7J*|W29)88{HK(IH){CWPdS`6tHmU;a2{CnP|R-Bmt?bw3KGuObcTTmwT~j z3%W2s#v`{q`2c*kmYT(7N%GV{7AJfM@gIC@G=V;BB*wl*`NkB)XuPlkumk`A*3n!e z9)%fFBM|g){+HpaE#GP2DQZ$r#{rnDgwxbqFVWQuG#gt~3DWYNCIepW1Mj5+mZ7-o zC@8ZEKf(+^NJClJp9eDN{%Q;Y^$&s<755JHKj9cZOL zVcA$ZMVBc^gV#hgoW^e)wPvWx!QBni!-*vWxYAgthogX=Kv@oQPG~gF;duPvIb(ec zrJlgB2$${-SR2||!j3>vxItol`?)Ro7+#uRHHt(1;zXs5dOJ3?WSo>8cOH8;By22= z*4Ybk>OSRLJsf^;8D4fzel)DnDfxDQz0x_EP2r{Y*Db6PpS<_ZiUv_J$vwA4h68VU+?prgwqw?J1}AEi7XG2Gyz{YDAUZ>>+|opm zF|ZSKt4l+4Ju$5pTUmVzwaG^)!d_QJ6}DLJaY*c6#4jX7SjQc0C>)^*Q;>JfO$Oa3 zcOwU*`0iOhh%=CzFRgkN4I>3@5C1K7NF6L`pfw46xQlLaD$Vq9#tCWDj!=Id;IXsF z-zkotQ9+3@&-i@(%roQ=P$K$Q6NtV>*xuHhX(0O_8Tp$#O`-DR27;;n?SA@m05n0s zoMH@p=hzGM8god&hTzmn#jLNKB#H3EP{8;98RN@!)gbokwzt}m-D|GN9;8pJ#%@Q3+d`+Qs)32EzZ0*@Mb2F%n@1;bT*R!Vq)6H|Rj11TFk=v>G6zFm#mS#=rQD z)VZLspoB#J{|Lauv>wSftP>(F$35ew687I60Y&yEZq-%GebZ=@=>k!3h z3m5uxYjIC3>sii|1oL%dllz}vU;mHj{90!RkDfr&ux}`(D zboO2o!5GcfH*PN1EY4%tUX&}rNHz)$%;Te(UP4`s2=R4fVMR9G0ad-$m**6B^=~Dy z?~#+KI&5&~O!Q(Yow7p&jYwgTirw*lshfXRpwP+P6(SvgngWRZmYCUpssqcFQE`p% zsp6n9J3s^bborFyES zy#Ws6;KShxc}F2iIRyq@cIFjmvJ|Oio|ZVIP^^W+mWab9I{xK7Mb^2=4NHUn>MP5R nPJjxbaMQufWkfKk_K?c8QhaJE6B306Rw%*~P00000j;Pag literal 0 HcmV?d00001 diff --git a/tests/data/KeyFileProtectedNoPassword.key b/tests/data/KeyFileProtectedNoPassword.key new file mode 100644 index 0000000000000000000000000000000000000000..a6131f09ed17e60906a0d5de0f55bee655472903 GIT binary patch literal 128 zcmV-`0Du1y;JWU2Yb2`-qo>j<}MI%KC>Cde=3ES#wAa0Hh?ZZqkU1s7`w?K|%P literal 0 HcmV?d00001