mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-24 23:09:44 -05:00
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.
This commit is contained in:
parent
91bccf75d5
commit
a7dd9f19f4
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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] <database> <entry> [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] <database> <entry>"
|
||||
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 <attribute>..."
|
||||
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"
|
||||
|
@ -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<void> 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"));
|
||||
}
|
||||
|
@ -58,7 +58,9 @@ private slots:
|
||||
|
||||
private:
|
||||
QByteArray m_dbData;
|
||||
QByteArray m_dbData2;
|
||||
QScopedPointer<TemporaryFile> m_dbFile;
|
||||
QScopedPointer<TemporaryFile> m_dbFile2;
|
||||
QScopedPointer<TemporaryFile> m_stdoutFile;
|
||||
QScopedPointer<TemporaryFile> m_stderrFile;
|
||||
QScopedPointer<TemporaryFile> m_stdinFile;
|
||||
|
Binary file not shown.
BIN
tests/data/NewDatabase2.kdbx
Normal file
BIN
tests/data/NewDatabase2.kdbx
Normal file
Binary file not shown.
@ -405,10 +405,9 @@ void TestGui::testTabs()
|
||||
void TestGui::testEditEntry()
|
||||
{
|
||||
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
|
||||
int editCount = 0;
|
||||
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
|
||||
|
||||
// Select the first entry in the database
|
||||
auto* entryView = m_dbWidget->findChild<EntryView*>("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<QLineEdit*>("seedEdit");
|
||||
seedEdit->setText("");
|
||||
|
||||
QString exampleSeed = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
|
||||
QTest::keyClicks(seedEdit, exampleSeed);
|
||||
|
Loading…
Reference in New Issue
Block a user