From a7dd9f19f45c63e31df6c502a234aa62abc931c3 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 10 Nov 2018 03:58:42 +0100 Subject: [PATCH] CLI: add commands to show and copy TOTP to clipboard (#2454) * Add CLI commands show --totp and totp-clip for handling TOTPs, resolves #2429. * Adding tests for new CLI TOTP commands * Update keepassxc-cli man page. --- src/cli/Clip.cpp | 27 +++++++++++-- src/cli/Clip.h | 2 +- src/cli/Show.cpp | 24 ++++++++++-- src/cli/Show.h | 2 +- src/cli/keepassxc-cli.1 | 17 ++++++-- tests/TestCli.cpp | 74 +++++++++++++++++++++++++++++++++++ tests/TestCli.h | 2 + tests/data/NewDatabase.kdbx | Bin 8350 -> 11486 bytes tests/data/NewDatabase2.kdbx | Bin 0 -> 8350 bytes tests/gui/TestGui.cpp | 7 +++- 10 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 tests/data/NewDatabase2.kdbx diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index a9135eff4..2cc15411b 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -51,6 +51,9 @@ int Clip::execute(const QStringList& arguments) QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + QCommandLineOption totp(QStringList() << "t" << "totp", + QObject::tr("Copy the current TOTP to the clipboard.")); + parser.addOption(totp); parser.addPositionalArgument("entry", QObject::tr("Path of the entry to clip.", "clip = copy to clipboard")); parser.addPositionalArgument("timeout", QObject::tr("Timeout in seconds before clearing the clipboard."), "[timeout]"); @@ -68,10 +71,10 @@ int Clip::execute(const QStringList& arguments) return EXIT_FAILURE; } - return clipEntry(db, args.at(1), args.value(2)); + return clipEntry(db, args.at(1), args.value(2), parser.isSet(totp)); } -int Clip::clipEntry(Database* database, const QString& entryPath, const QString& timeout) +int Clip::clipEntry(Database* database, const QString& entryPath, const QString& timeout, bool clipTotp) { TextStream err(Utils::STDERR); @@ -90,12 +93,28 @@ int Clip::clipEntry(Database* database, const QString& entryPath, const QString& return EXIT_FAILURE; } - int exitCode = Utils::clipText(entry->password()); + QString value; + if (clipTotp) { + if (!entry->hasTotp()) { + err << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << endl; + return EXIT_FAILURE; + } + + value = entry->totp(); + } else { + value = entry->password(); + } + + int exitCode = Utils::clipText(value); if (exitCode != EXIT_SUCCESS) { return exitCode; } - outputTextStream << QObject::tr("Entry's password copied to the clipboard!") << endl; + if (clipTotp) { + outputTextStream << QObject::tr("Entry's current TOTP copied to the clipboard!") << endl; + } else { + outputTextStream << QObject::tr("Entry's password copied to the clipboard!") << endl; + } if (!timeoutSeconds) { return exitCode; diff --git a/src/cli/Clip.h b/src/cli/Clip.h index e5e6390ae..9f7151322 100644 --- a/src/cli/Clip.h +++ b/src/cli/Clip.h @@ -26,7 +26,7 @@ public: Clip(); ~Clip(); int execute(const QStringList& arguments) override; - int clipEntry(Database* database, const QString& entryPath, const QString& timeout); + int clipEntry(Database* database, const QString& entryPath, const QString& timeout, bool clipTotp); }; #endif // KEEPASSXC_CLIP_H diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index e474e2489..7b42de7ab 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -50,6 +50,9 @@ int Show::execute(const QStringList& arguments) QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + QCommandLineOption totp(QStringList() << "t" << "totp", + QObject::tr("Show the entry's current TOTP.")); + parser.addOption(totp); QCommandLineOption attributes( QStringList() << "a" << "attributes", QObject::tr( @@ -73,10 +76,10 @@ int Show::execute(const QStringList& arguments) return EXIT_FAILURE; } - return showEntry(db.data(), parser.values(attributes), args.at(1)); + return showEntry(db.data(), parser.values(attributes), parser.isSet(totp), args.at(1)); } -int Show::showEntry(Database* database, QStringList attributes, const QString& entryPath) +int Show::showEntry(Database* database, QStringList attributes, bool showTotp, const QString& entryPath) { TextStream in(Utils::STDIN, QIODevice::ReadOnly); TextStream out(Utils::STDOUT, QIODevice::WriteOnly); @@ -88,9 +91,14 @@ int Show::showEntry(Database* database, QStringList attributes, const QString& e return EXIT_FAILURE; } + if (showTotp && !entry->hasTotp()) { + err << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << endl; + return EXIT_FAILURE; + } + // If no attributes specified, output the default attribute set. - bool showAttributeNames = attributes.isEmpty(); - if (attributes.isEmpty()) { + bool showAttributeNames = attributes.isEmpty() && !showTotp; + if (attributes.isEmpty() && !showTotp) { attributes = EntryAttributes::DefaultAttributes; } @@ -107,5 +115,13 @@ int Show::showEntry(Database* database, QStringList attributes, const QString& e } out << entry->resolveMultiplePlaceholders(entry->attributes()->value(attribute)) << endl; } + + if (showTotp) { + if (showAttributeNames) { + out << "TOTP: "; + } + out << entry->totp() << endl; + } + return sawUnknownAttribute ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/src/cli/Show.h b/src/cli/Show.h index 6d49d1207..32dab7efb 100644 --- a/src/cli/Show.h +++ b/src/cli/Show.h @@ -26,7 +26,7 @@ public: Show(); ~Show(); int execute(const QStringList& arguments) override; - int showEntry(Database* database, QStringList attributes, const QString& entryPath); + int showEntry(Database* database, QStringList attributes, bool showTotp, const QString& entryPath); }; #endif // KEEPASSXC_SHOW_H diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 877346646..0d618c9d1 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -17,7 +17,7 @@ keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager Adds a new entry to a database. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). .IP "clip [options] [timeout]" -Copies the password of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. +Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. .IP "diceware [options]" Generate a random diceware passphrase. @@ -47,7 +47,7 @@ Merges two databases together. The first database file is going to be replaced b Removes an entry from a database. If the database has a recycle bin, the entry will be moved there. If the entry is already in the recycle bin, it will be removed permanently. .IP "show [options] " -Shows the title, username, password, URL and notes of a database entry. Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the \fIclip\fP command section also applies here. +Shows the title, username, password, URL and notes of a database entry. Can also show the current TOTP. Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the \fIclip\fP command section also applies here. .SH OPTIONS @@ -102,12 +102,23 @@ Specify the title of the entry. Perform advanced analysis on the password. +.SS "Clip options" + +.IP "-t, --totp" +Copy the current TOTP instead of current password to clipboard. Will report an error +if no TOTP is configured for the entry. + + .SS "Show options" .IP "-a, --attributes ..." Names of the attributes to show. This option can be specified more than once, with each attribute shown one-per-line in the given order. If no attributes are -specified, a summary of the default attributes is given. +specified and \fI-t\fP is not specified, a summary of the default attributes is given. + +.IP "-t, --totp" +Also show the current TOTP. Will report an error if no TOTP is configured for the +entry. .SS "Diceware options" diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index c95b1f32b..b1c3a82e8 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -67,6 +67,12 @@ void TestCli::initTestCase() QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); sourceDbFile.close(); + + // Load the NewDatabase.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(); } void TestCli::init() @@ -76,6 +82,11 @@ void TestCli::init() m_dbFile->write(m_dbData); m_dbFile->close(); + m_dbFile2.reset(new TemporaryFile()); + m_dbFile2->open(); + m_dbFile2->write(m_dbData2); + m_dbFile2->close(); + m_stdinFile.reset(new TemporaryFile()); m_stdinFile->open(); m_stdinHandle = fdopen(m_stdinFile->handle(), "r+"); @@ -96,6 +107,8 @@ void TestCli::cleanup() { m_dbFile.reset(); + m_dbFile2.reset(); + m_stdinFile.reset(); m_stdinHandle = stdin; Utils::STDIN = stdin; @@ -168,6 +181,19 @@ void TestCli::testAdd() QCOMPARE(entry->password(), QString("newpassword")); } +bool isTOTP(const QString & value) { + QString val = value.trimmed(); + if (val.length() < 5 || val.length() > 6) { + return false; + } + for (int i = 0; i < val.length(); ++i) { + if (!value[i].isDigit()) { + return false; + } + } + return true; +} + void TestCli::testClip() { QClipboard* clipboard = QGuiApplication::clipboard(); @@ -177,6 +203,7 @@ void TestCli::testClip() QVERIFY(!clipCmd.name.isEmpty()); QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name)); + // Password Utils::Test::setNextPassword("a"); clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry"}); @@ -190,6 +217,13 @@ void TestCli::testClip() QCOMPARE(clipboard->text(), QString("Password")); + // TOTP + Utils::Test::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "--totp"}); + + QVERIFY(isTOTP(clipboard->text())); + + // Password with timeout Utils::Test::setNextPassword("a"); QFuture future = QtConcurrent::run(&clipCmd, &Clip::execute, QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"}); @@ -197,6 +231,21 @@ void TestCli::testClip() QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 1500); future.waitForFinished(); + + // TOTP with timeout + Utils::Test::setNextPassword("a"); + future = QtConcurrent::run(&clipCmd, &Clip::execute, QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1", "-t"}); + + QTRY_VERIFY_WITH_TIMEOUT(isTOTP(clipboard->text()), 500); + QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 1500); + + future.waitForFinished(); + + qint64 posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry"}); + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n")); } void TestCli::testDiceware() @@ -756,4 +805,29 @@ void TestCli::testShow() m_stderrFile->reset(); QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(m_stderrFile->readAll(), QByteArray("ERROR: unknown attribute DoesNotExist.\n")); + + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + showCmd.execute({"show", "-t", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QVERIFY(isTOTP(m_stdoutFile->readAll())); + + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + showCmd.execute({"show", "-a", "Title", m_dbFile->fileName(), "--totp", "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Sample Entry\n")); + QVERIFY(isTOTP(m_stdoutFile->readAll())); + + pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + showCmd.execute({"show", m_dbFile2->fileName(), "--totp", "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n")); } diff --git a/tests/TestCli.h b/tests/TestCli.h index 691269840..2c411f2ca 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -58,7 +58,9 @@ private slots: private: QByteArray m_dbData; + QByteArray m_dbData2; QScopedPointer m_dbFile; + QScopedPointer m_dbFile2; QScopedPointer m_stdoutFile; QScopedPointer m_stderrFile; QScopedPointer m_stdinFile; diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index 4e77724c7e8cfdf22bcd951ccfcaa4ae7f49d460..a8dfb5bd5e077aebc59017a1217a0d0b68109ae1 100644 GIT binary patch literal 11486 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 diff --git a/tests/data/NewDatabase2.kdbx b/tests/data/NewDatabase2.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..4e77724c7e8cfdf22bcd951ccfcaa4ae7f49d460 GIT binary patch literal 8350 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 literal 0 HcmV?d00001 diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 450f09474..8613d184e 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -405,10 +405,9 @@ void TestGui::testTabs() void TestGui::testEditEntry() { auto* toolBar = m_mainWindow->findChild("toolBar"); - int editCount = 0; + auto* entryView = m_dbWidget->findChild("entryView"); // Select the first entry in the database - auto* entryView = m_dbWidget->findChild("entryView"); QModelIndex entryItem = entryView->model()->index(0, 1); Entry* entry = entryView->entryFromIndex(entryItem); clickIndex(entryItem, entryView, Qt::LeftButton); @@ -420,6 +419,9 @@ void TestGui::testEditEntry() QVERIFY(entryEditWidget->isVisible()); QVERIFY(entryEditWidget->isEnabled()); + // Record current history count + int editCount = entry->historyItems().size(); + // Edit the first entry ("Sample Entry") QTest::mouseClick(entryEditWidget, Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); @@ -748,6 +750,7 @@ void TestGui::testTotp() QApplication::processEvents(); auto* seedEdit = setupTotpDialog->findChild("seedEdit"); + seedEdit->setText(""); QString exampleSeed = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; QTest::keyClicks(seedEdit, exampleSeed);