diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b5a51e2..68ec8f1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.6 (unreleased) + +### Added +- Added CLI db-info command [#4231] + +### Changed +- Renamed CLI create command to db-create [#4231] + ## 2.5.3 (2020-01-19) ### Fixed diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index 1fefda104..cba3ddb75 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -28,9 +28,12 @@ Copies the password or the current TOTP (\fI-t\fP option) of a database entry to .IP "\fBclose\fP" In interactive mode, closes the currently opened database (see \fIopen\fP). -.IP "\fBcreate\fP [options] " +.IP "\fBdb-create\fP [options] " Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. +.IP "\fBdb-info\fP [options] " +Show a database's information. + .IP "\fBdiceware\fP [options]" Generates a random diceware passphrase. diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index f5c90df8d..5959bf328 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -30,6 +30,7 @@ set(cli_SOURCES Generate.cpp Help.cpp Import.cpp + Info.cpp List.cpp Locate.cpp Merge.cpp diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index 4d3bf8270..e76f633ef 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -37,6 +37,7 @@ #include "Generate.h" #include "Help.h" #include "Import.h" +#include "Info.h" #include "List.h" #include "Locate.h" #include "Merge.h" @@ -160,7 +161,8 @@ namespace Commands s_commands.insert(QStringLiteral("analyze"), QSharedPointer(new Analyze())); s_commands.insert(QStringLiteral("clip"), QSharedPointer(new Clip())); s_commands.insert(QStringLiteral("close"), QSharedPointer(new Close())); - s_commands.insert(QStringLiteral("create"), QSharedPointer(new Create())); + s_commands.insert(QStringLiteral("db-create"), QSharedPointer(new Create())); + s_commands.insert(QStringLiteral("db-info"), QSharedPointer(new Info())); s_commands.insert(QStringLiteral("diceware"), QSharedPointer(new Diceware())); s_commands.insert(QStringLiteral("edit"), QSharedPointer(new Edit())); s_commands.insert(QStringLiteral("estimate"), QSharedPointer(new Estimate())); diff --git a/src/cli/Info.cpp b/src/cli/Info.cpp new file mode 100644 index 000000000..4e80b75b4 --- /dev/null +++ b/src/cli/Info.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * 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 . + */ +#include +#include + +#include "Info.h" + +#include "core/Database.h" +#include "core/Metadata.h" +#include "format/KeePass2.h" + +#include "Utils.h" + +Info::Info() +{ + name = QString("db-show"); + description = QObject::tr("Show a database's information."); +} + +int Info::executeWithDatabase(QSharedPointer database, QSharedPointer) +{ + TextStream out(Utils::STDOUT, QIODevice::WriteOnly); + + out << QObject::tr("UUID: ") << database->uuid().toString() << endl; + out << QObject::tr("Name: ") << database->metadata()->name() << endl; + out << QObject::tr("Description: ") << database->metadata()->description() << endl; + for (auto& cipher : asConst(KeePass2::CIPHERS)) { + if (cipher.first == database->cipher()) { + out << QObject::tr("Cipher: ") << cipher.second << endl; + } + } + out << QObject::tr("KDF: ") << database->kdf()->toString() << endl; + if (database->metadata()->recycleBinEnabled()) { + out << QObject::tr("Recycle bin is enabled.") << endl; + } else { + out << QObject::tr("Recycle bin is not enabled.") << endl; + } + return EXIT_SUCCESS; +} diff --git a/src/cli/Info.h b/src/cli/Info.h new file mode 100644 index 000000000..1961a7b5f --- /dev/null +++ b/src/cli/Info.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_INFO_H +#define KEEPASSXC_INFO_H + +#include "DatabaseCommand.h" + +class Info : public DatabaseCommand +{ +public: + Info(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser); +}; + +#endif // KEEPASSXC_INFO_H diff --git a/src/crypto/kdf/AesKdf.cpp b/src/crypto/kdf/AesKdf.cpp index 0b2130cfe..d1daf1e5d 100644 --- a/src/crypto/kdf/AesKdf.cpp +++ b/src/crypto/kdf/AesKdf.cpp @@ -125,3 +125,8 @@ int AesKdf::benchmarkImpl(int msec) const return static_cast(rounds * (static_cast(msec) / timer.elapsed())); } + +QString AesKdf::toString() const +{ + return QObject::tr("AES (%1 rounds)").arg(QString::number(rounds())); +} diff --git a/src/crypto/kdf/AesKdf.h b/src/crypto/kdf/AesKdf.h index 84156e6fb..d71fbb1d1 100644 --- a/src/crypto/kdf/AesKdf.h +++ b/src/crypto/kdf/AesKdf.h @@ -30,6 +30,7 @@ public: QVariantMap writeParameters() override; bool transform(const QByteArray& raw, QByteArray& result) const override; QSharedPointer clone() const override; + QString toString() const override; protected: int benchmarkImpl(int msec) const override; diff --git a/src/crypto/kdf/Argon2Kdf.cpp b/src/crypto/kdf/Argon2Kdf.cpp index 0d449b5b5..31995fdd0 100644 --- a/src/crypto/kdf/Argon2Kdf.cpp +++ b/src/crypto/kdf/Argon2Kdf.cpp @@ -211,3 +211,8 @@ int Argon2Kdf::benchmarkImpl(int msec) const return 1; } + +QString Argon2Kdf::toString() const +{ + return QObject::tr("Argon2 (%1 rounds, %2 KB)").arg(QString::number(rounds()), QString::number(memory())); +} diff --git a/src/crypto/kdf/Argon2Kdf.h b/src/crypto/kdf/Argon2Kdf.h index 73b7f8529..6a16ee96e 100644 --- a/src/crypto/kdf/Argon2Kdf.h +++ b/src/crypto/kdf/Argon2Kdf.h @@ -36,6 +36,7 @@ public: bool setMemory(quint64 kibibytes); quint32 parallelism() const; bool setParallelism(quint32 threads); + QString toString() const override; protected: int benchmarkImpl(int msec) const override; diff --git a/src/crypto/kdf/Kdf.h b/src/crypto/kdf/Kdf.h index 368fb16f7..4e6455ede 100644 --- a/src/crypto/kdf/Kdf.h +++ b/src/crypto/kdf/Kdf.h @@ -44,6 +44,8 @@ public: virtual bool transform(const QByteArray& raw, QByteArray& result) const = 0; virtual QSharedPointer clone() const = 0; + virtual QString toString() const = 0; + int benchmark(int msec) const; protected: diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp index fbc393030..dc50ca001 100644 --- a/src/format/KeePass2.cpp +++ b/src/format/KeePass2.cpp @@ -48,9 +48,9 @@ const QString KeePass2::KDFPARAM_ARGON2_SECRET("K"); const QString KeePass2::KDFPARAM_ARGON2_ASSOCDATA("A"); const QList> KeePass2::CIPHERS{ - qMakePair(KeePass2::CIPHER_AES256, QObject::tr("AES: 256-bit")), - qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish: 256-bit")), - qMakePair(KeePass2::CIPHER_CHACHA20, QObject::tr("ChaCha20: 256-bit"))}; + qMakePair(KeePass2::CIPHER_AES256, QObject::tr("AES 256-bit")), + qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish 256-bit")), + qMakePair(KeePass2::CIPHER_CHACHA20, QObject::tr("ChaCha20 256-bit"))}; const QList> KeePass2::KDFS{ qMakePair(KeePass2::KDF_ARGON2, QObject::tr("Argon2 (KDBX 4 – recommended)")), diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 076f7f74e..d73cdeddd 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -44,6 +44,7 @@ #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" @@ -192,7 +193,8 @@ void TestCli::testBatchCommands() QVERIFY(Commands::getCommand("analyze")); QVERIFY(Commands::getCommand("clip")); QVERIFY(Commands::getCommand("close")); - QVERIFY(Commands::getCommand("create")); + QVERIFY(Commands::getCommand("db-create")); + QVERIFY(Commands::getCommand("db-info")); QVERIFY(Commands::getCommand("diceware")); QVERIFY(Commands::getCommand("edit")); QVERIFY(Commands::getCommand("estimate")); @@ -210,7 +212,7 @@ void TestCli::testBatchCommands() QVERIFY(Commands::getCommand("rmdir")); QVERIFY(Commands::getCommand("show")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 21); + QCOMPARE(Commands::getCommands().size(), 22); } void TestCli::testInteractiveCommands() @@ -220,7 +222,8 @@ void TestCli::testInteractiveCommands() QVERIFY(Commands::getCommand("analyze")); QVERIFY(Commands::getCommand("clip")); QVERIFY(Commands::getCommand("close")); - QVERIFY(Commands::getCommand("create")); + QVERIFY(Commands::getCommand("db-create")); + QVERIFY(Commands::getCommand("db-info")); QVERIFY(Commands::getCommand("diceware")); QVERIFY(Commands::getCommand("edit")); QVERIFY(Commands::getCommand("estimate")); @@ -238,7 +241,7 @@ void TestCli::testInteractiveCommands() QVERIFY(Commands::getCommand("rmdir")); QVERIFY(Commands::getCommand("show")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 21); + QCOMPARE(Commands::getCommands().size(), 22); } void TestCli::testAdd() @@ -548,7 +551,7 @@ void TestCli::testCreate() QString databaseFilename = testDir->path() + "/testCreate1.kdbx"; // Password Utils::Test::setNextPassword("a"); - createCmd.execute({"create", databaseFilename}); + createCmd.execute({"db-create", databaseFilename}); m_stderrFile->reset(); m_stdoutFile->reset(); @@ -563,7 +566,7 @@ void TestCli::testCreate() // Should refuse to create the database if it already exists. qint64 pos = m_stdoutFile->pos(); qint64 errPos = m_stderrFile->pos(); - createCmd.execute({"create", databaseFilename}); + createCmd.execute({"db-create", databaseFilename}); m_stdoutFile->seek(pos); m_stderrFile->seek(errPos); // Output should be empty when there is an error. @@ -577,7 +580,7 @@ void TestCli::testCreate() pos = m_stdoutFile->pos(); errPos = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); - createCmd.execute({"create", databaseFilename2, "-k", keyfilePath}); + createCmd.execute({"db-create", databaseFilename2, "-k", keyfilePath}); m_stdoutFile->seek(pos); m_stderrFile->seek(errPos); @@ -594,7 +597,7 @@ void TestCli::testCreate() pos = m_stdoutFile->pos(); errPos = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); - createCmd.execute({"create", databaseFilename3, "-k", keyfilePath}); + createCmd.execute({"db-create", databaseFilename3, "-k", keyfilePath}); m_stdoutFile->seek(pos); m_stderrFile->seek(errPos); @@ -607,6 +610,41 @@ void TestCli::testCreate() QVERIFY(db3); } +void TestCli::testInfo() +{ + Info infoCmd; + QVERIFY(!infoCmd.name.isEmpty()); + QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name)); + + Utils::Test::setNextPassword("a"); + infoCmd.execute({"db-info", m_dbFile->fileName()}); + m_stdoutFile->reset(); + m_stderrFile->reset(); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QVERIFY(m_stdoutFile->readLine().contains(QByteArray("UUID: "))); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Name: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Description: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Cipher: AES 256-bit\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("KDF: AES (6000 rounds)\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Recycle bin is enabled.\n")); + + // Test with quiet option. + qint64 pos = m_stdoutFile->pos(); + qint64 errPos = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + infoCmd.execute({"db-info", "-q", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QVERIFY(m_stdoutFile->readLine().contains(QByteArray("UUID: "))); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Name: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Description: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Cipher: AES 256-bit\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("KDF: AES (6000 rounds)\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Recycle bin is enabled.\n")); +} + void TestCli::testDiceware() { Diceware dicewareCmd; @@ -1446,10 +1484,10 @@ void TestCli::testMergeWithKeys() qint64 pos = m_stdoutFile->pos(); Utils::Test::setNextPassword("a"); - createCmd.execute({"create", sourceDatabaseFilename, "-k", sourceKeyfilePath}); + createCmd.execute({"db-create", sourceDatabaseFilename, "-k", sourceKeyfilePath}); Utils::Test::setNextPassword("b"); - createCmd.execute({"create", targetDatabaseFilename, "-k", targetKeyfilePath}); + createCmd.execute({"db-create", targetDatabaseFilename, "-k", targetKeyfilePath}); Utils::Test::setNextPassword("a"); auto sourceDatabase = QSharedPointer( diff --git a/tests/TestCli.h b/tests/TestCli.h index 4947ee472..44420d580 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -59,6 +59,7 @@ private slots: void testGenerate_data(); void testGenerate(); void testImport(); + void testInfo(); void testKeyFileOption(); void testNoPasswordOption(); void testHelp();